Skip to content

Commit 775b026

Browse files
committed
tut_spa: added pyodide canvas & setup code
1 parent 43345c2 commit 775b026

7 files changed

Lines changed: 248 additions & 3 deletions

File tree

tutorial/jbook/discover_immediate.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Intro
22

3+
```{codes_include} discover/widget_edit
4+
```
5+
36
## What is an Immediate Mode GUI
47

58
Graphical User Interfaces (GUIs) handle how your application is presented to the user.

tutorial/single_page_book_app/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ <h1>Immediate GUI Tutorial</h1>
3535
<button id="maximize-btn">Maximize</button>
3636
</div>
3737
<div id="canvas-content" style="background-color: blue;">
38-
<!-- Canvas or placeholder content goes here -->
38+
<canvas class="canvas-emscripten" id="canvas" tabindex="0" oncontextmenu="event.preventDefault()"></canvas>
3939
</div>
4040
</div>
41+
<script src="pyodide_dist/pyodide.js"></script>
4142
<script type="module" src="resources_singlepage/app.js"></script>
4243
</body>
4344
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../bindings/pyodide_web_demo/pyodide_dist

tutorial/single_page_book_app/resources_singlepage/app.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,32 @@ import { initTOC, tocRoot } from "./toc_loader.js";
22
import { loadPage } from "./page_loader.js";
33
import {registerCanvasDragEvents} from "./canvas_drag"
44
import { registerSidebarToggle } from "./toggle_sidebars.js";
5+
import { initializePyodideHelper, runPythonCode } from "./pyodide_helper.js";
56

67
async function initializeAll() {
78
await initTOC();
89
registerCanvasDragEvents();
910
registerSidebarToggle();
10-
1111
const rootPage = tocRoot()
1212
loadPage(rootPage.file + ".md");
13+
14+
await initializePyodideHelper();
15+
16+
// Test Python code
17+
await runPythonCode(`
18+
import sys
19+
print(sys.version)
20+
print("Hello from Python!")
21+
22+
from imgui_bundle import imgui, hello_imgui
23+
def gui():
24+
imgui.text("Hello, world!")
25+
imgui.show_demo_window()
26+
27+
hello_imgui.run(gui)
28+
`);
29+
30+
1331
}
1432

1533
document.addEventListener("DOMContentLoaded", initializeAll);

tutorial/single_page_book_app/resources_singlepage/code_editor.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language"
44
import { python } from "@codemirror/lang-python";
55
import { cpp } from "@codemirror/lang-cpp";
66
import { marked } from "marked";
7+
import { runPythonCode } from "./pyodide_helper.js";
8+
79

810
// _ stands for private function to this module. Let's adopt this convention!
911

@@ -51,6 +53,19 @@ async function _processCodesIncludeInMarkdown(mdText, baseUrlPath) {
5153
return processedLines.join("\n");
5254
}
5355

56+
function _createEditorRunPythonCodeButton(editorView) {
57+
const runButton = document.createElement("button");
58+
runButton.textContent = "Run Python Code";
59+
runButton.classList.add("run-python-button");
60+
61+
runButton.addEventListener("click", () => {
62+
// Get the current text from the editor
63+
const currentCode = editorView.state.doc.toString();
64+
runPythonCode(currentCode);
65+
});
66+
67+
return runButton;
68+
}
5469

5570
async function _initializeCodeMirrorEditors(baseUrlPath) {
5671
const containers = document.querySelectorAll(".code-editor-tab-container");
@@ -101,13 +116,20 @@ async function _initializeCodeMirrorEditors(baseUrlPath) {
101116
extensions.push(cpp());
102117
}
103118

104-
new EditorView({
119+
const editorView = new EditorView({
105120
state: EditorState.create({
106121
doc: codeContent,
107122
extensions,
108123
}),
109124
parent: cmPlaceholder,
110125
});
126+
127+
// If it's a Python editor, add a "Run" button below it
128+
if (language === "python") {
129+
const runButton = _createEditorRunPythonCodeButton(editorView);
130+
tabPane.appendChild(runButton);
131+
}
132+
111133
} catch (error) {
112134
console.error(`Failed to fetch file: ${filePath}`, error);
113135
tabPane.innerHTML = `<div class="code-editor-tab-error">Error loading file: ${filePath}</div>`;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Canvas setup
2+
// =======================================
3+
4+
let _gPyodide = null;
5+
6+
function _getEmscriptenCanvas() {
7+
let canvas = document.getElementById("canvas");
8+
9+
// WebGL2 context check
10+
canvas.addEventListener("webglcontextlost", function (event) {
11+
alert("WebGL context lost, please reload the page");
12+
event.preventDefault();
13+
}, false);
14+
15+
if (typeof WebGL2RenderingContext === 'undefined') {
16+
alert("WebGL 2 not supported by this browser");
17+
return null;
18+
}
19+
return canvas;
20+
}
21+
22+
async function _passCanvasToPyodide() {
23+
const canvas = _getEmscriptenCanvas();
24+
// console.log("initEmscriptenCanvas canvas:", canvas);
25+
// Example: Expose canvas to Python
26+
_gPyodide.canvas.setCanvas3D(canvas); // Set canvas for 3D rendering
27+
// console.log("initEmscriptenCanvas canvas set");
28+
}
29+
30+
// Handle canvas resizing (not called at the moment)
31+
function _passCanvasSizeToEmscripten() {
32+
const canvas = _getEmscriptenCanvas();
33+
canvas.width = canvas.clientWidth;
34+
canvas.height = canvas.clientHeight;
35+
// Inform your rendering context about the resize if necessary
36+
}
37+
38+
// GUI utilities
39+
// ===============================================
40+
function _showLoadingModal() {
41+
}
42+
43+
function _hideLoadingModal() {
44+
}
45+
46+
function _updateProgress(progress, message) {
47+
console.log(`Progress: ${progress} - ${message}`);
48+
}
49+
50+
function _displayError(message) {
51+
console.error(message);
52+
}
53+
54+
function _clearError() {
55+
}
56+
57+
// Initial loading
58+
//================================================
59+
60+
61+
// Initialize Pyodide and load packages with progress updates
62+
async function _loadPyodideAndPackages() {
63+
try {
64+
_showLoadingModal();
65+
_updateProgress(0, 'Loading Pyodide...');
66+
_gPyodide = await loadPyodide();
67+
const pythonVersion = _gPyodide.runPython("import sys; sys.version");
68+
_updateProgress(7, 'Pyodide loaded.');
69+
console.log("Python version:", pythonVersion);
70+
await _gPyodide.loadPackage("micropip");
71+
await _gPyodide.loadPackage("micropip"); // firefox needs this to be loaded twice...
72+
_updateProgress(10, 'micropip loaded.');
73+
74+
// Important:
75+
// SDL support in Pyodide is experimental. The flag is used to bypass certain issues.
76+
_gPyodide._api._skip_unwind_fatal_error = true;
77+
78+
// Determine the base URL dynamically
79+
const baseUrl = `${window.location.origin}${window.location.pathname}`;
80+
console.log('Base URL:', baseUrl);
81+
82+
// List of packages to install
83+
const packages = [
84+
// For imgui_bundle below
85+
// -----------------------
86+
'numpy', // 2.8 MB
87+
'pydantic', // 1.3 + 0.4 MB = 1.7 MB
88+
'typing_extensions', // 34 KB
89+
'munch', // 10 KB
90+
'imgui_bundle', // 9.7 MB (with 3 MB for demos_assets, 6 MB native)
91+
'pillow', // 964 KB
92+
93+
// // For fiatlight below
94+
// // --------------------
95+
// 'requests', // 61KB, For word count demo (we download the Hamlet text)
96+
// 'pandas', // 5.4 MB
97+
// 'matplotlib', // 6.2 MB
98+
// 'opencv-python', // 11 MB
99+
// baseUrl + `/pyodide_dist/fiatlight-0.1.0-py3-none-any.whl`, // 3.5 MB
100+
//
101+
// // For scatter_widget_bundle
102+
// // --------------------------
103+
// "scikit-learn", // 6.3 MB
104+
// "scipy", // 13 MB
105+
// baseUrl + "/pyodide_dist/scatter_widget_bundle-0.1.0-py3-none-any.whl", // 8.3 KB
106+
];
107+
108+
const totalSteps = packages.length;
109+
let currentStep = 1;
110+
111+
for (const pkg of packages) {
112+
_updateProgress(10 + (currentStep / totalSteps) * 80, `Installing ${pkg}...`);
113+
await _gPyodide.runPythonAsync(`
114+
import micropip;
115+
await micropip.install('${pkg}')
116+
`);
117+
console.log(`${pkg} loaded.`);
118+
currentStep++;
119+
}
120+
121+
_updateProgress(100, 'All packages loaded.');
122+
// Optionally, add a slight delay before hiding the modal
123+
await new Promise(resolve => setTimeout(resolve, 500));
124+
_hideLoadingModal();
125+
console.log('Pyodide and packages loaded.');
126+
} catch (error) {
127+
console.error('Error loading Pyodide or packages:', error);
128+
_displayError('Failed to load Pyodide or install packages. See console for details.');
129+
_hideLoadingModal();
130+
}
131+
}
132+
133+
// Function to run Python code
134+
export async function runPythonCode(code) {
135+
if (!_gPyodide) {
136+
console.error('Pyodide not loaded yet');
137+
displayError('Pyodide is still loading. Please wait a moment and try again.');
138+
return;
139+
}
140+
141+
// Clear previous errors before running new code
142+
_clearError();
143+
144+
try {
145+
// Redirect stdout and stderr
146+
_gPyodide.setStdout({
147+
batched: (s) => console.log(s),
148+
});
149+
_gPyodide.setStderr({
150+
batched: (s) => {
151+
console.error(s);
152+
_displayError(s);
153+
},
154+
});
155+
156+
// Execute the code
157+
await _gPyodide.runPythonAsync(code);
158+
159+
// Optionally, call a specific function
160+
// await pyodide.runPythonAsync('main()');
161+
162+
} catch (err) {
163+
console.error('Caught PythonError:', err);
164+
_displayError(err.toString());
165+
}
166+
}
167+
168+
169+
//
170+
export async function initializePyodideHelper()
171+
{
172+
await _loadPyodideAndPackages();
173+
_passCanvasToPyodide();
174+
}

tutorial/single_page_book_app/resources_singlepage/style.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ body {
198198
height: calc(100% - 2rem);
199199
}
200200

201+
.canvas-emscripten {
202+
/*position: absolute;*/
203+
/*top: 0;*/
204+
/*left: 0;*/
205+
/*width: 100%;*/
206+
/*height: 100%;*/
207+
/*overflow: hidden;*/
208+
}
209+
210+
201211
/* ==========================
202212
Breadcrumbs
203213
========================== */
@@ -317,6 +327,22 @@ body {
317327
}
318328
}
319329

330+
.run-python-button {
331+
margin-top: 0.5rem;
332+
background-color: #007bff;
333+
color: white;
334+
border: none;
335+
border-radius: 4px;
336+
padding: 0.5rem 1rem;
337+
cursor: pointer;
338+
font-size: 0.9rem;
339+
}
340+
341+
.run-python-button:hover {
342+
background-color: #0056b3;
343+
}
344+
345+
320346
/* ==========================
321347
Responsive Layout
322348
========================== */

0 commit comments

Comments
 (0)