Skip to content

Commit

Permalink
tut_spa: added pyodide canvas & setup code
Browse files Browse the repository at this point in the history
  • Loading branch information
pthom committed Dec 22, 2024
1 parent 43345c2 commit 775b026
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 3 deletions.
3 changes: 3 additions & 0 deletions tutorial/jbook/discover_immediate.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Intro

```{codes_include} discover/widget_edit
```

## What is an Immediate Mode GUI

Graphical User Interfaces (GUIs) handle how your application is presented to the user.
Expand Down
3 changes: 2 additions & 1 deletion tutorial/single_page_book_app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ <h1>Immediate GUI Tutorial</h1>
<button id="maximize-btn">Maximize</button>
</div>
<div id="canvas-content" style="background-color: blue;">
<!-- Canvas or placeholder content goes here -->
<canvas class="canvas-emscripten" id="canvas" tabindex="0" oncontextmenu="event.preventDefault()"></canvas>
</div>
</div>
<script src="pyodide_dist/pyodide.js"></script>
<script type="module" src="resources_singlepage/app.js"></script>
</body>
</html>
1 change: 1 addition & 0 deletions tutorial/single_page_book_app/pyodide_dist
20 changes: 19 additions & 1 deletion tutorial/single_page_book_app/resources_singlepage/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,32 @@ import { initTOC, tocRoot } from "./toc_loader.js";
import { loadPage } from "./page_loader.js";
import {registerCanvasDragEvents} from "./canvas_drag"
import { registerSidebarToggle } from "./toggle_sidebars.js";
import { initializePyodideHelper, runPythonCode } from "./pyodide_helper.js";

async function initializeAll() {
await initTOC();
registerCanvasDragEvents();
registerSidebarToggle();

const rootPage = tocRoot()
loadPage(rootPage.file + ".md");

await initializePyodideHelper();

// Test Python code
await runPythonCode(`
import sys
print(sys.version)
print("Hello from Python!")
from imgui_bundle import imgui, hello_imgui
def gui():
imgui.text("Hello, world!")
imgui.show_demo_window()
hello_imgui.run(gui)
`);


}

document.addEventListener("DOMContentLoaded", initializeAll);
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language"
import { python } from "@codemirror/lang-python";
import { cpp } from "@codemirror/lang-cpp";
import { marked } from "marked";
import { runPythonCode } from "./pyodide_helper.js";


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

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

function _createEditorRunPythonCodeButton(editorView) {
const runButton = document.createElement("button");
runButton.textContent = "Run Python Code";
runButton.classList.add("run-python-button");

runButton.addEventListener("click", () => {
// Get the current text from the editor
const currentCode = editorView.state.doc.toString();
runPythonCode(currentCode);
});

return runButton;
}

async function _initializeCodeMirrorEditors(baseUrlPath) {
const containers = document.querySelectorAll(".code-editor-tab-container");
Expand Down Expand Up @@ -101,13 +116,20 @@ async function _initializeCodeMirrorEditors(baseUrlPath) {
extensions.push(cpp());
}

new EditorView({
const editorView = new EditorView({
state: EditorState.create({
doc: codeContent,
extensions,
}),
parent: cmPlaceholder,
});

// If it's a Python editor, add a "Run" button below it
if (language === "python") {
const runButton = _createEditorRunPythonCodeButton(editorView);
tabPane.appendChild(runButton);
}

} catch (error) {
console.error(`Failed to fetch file: ${filePath}`, error);
tabPane.innerHTML = `<div class="code-editor-tab-error">Error loading file: ${filePath}</div>`;
Expand Down
174 changes: 174 additions & 0 deletions tutorial/single_page_book_app/resources_singlepage/pyodide_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Canvas setup
// =======================================

let _gPyodide = null;

function _getEmscriptenCanvas() {
let canvas = document.getElementById("canvas");

// WebGL2 context check
canvas.addEventListener("webglcontextlost", function (event) {
alert("WebGL context lost, please reload the page");
event.preventDefault();
}, false);

if (typeof WebGL2RenderingContext === 'undefined') {
alert("WebGL 2 not supported by this browser");
return null;
}
return canvas;
}

async function _passCanvasToPyodide() {
const canvas = _getEmscriptenCanvas();
// console.log("initEmscriptenCanvas canvas:", canvas);
// Example: Expose canvas to Python
_gPyodide.canvas.setCanvas3D(canvas); // Set canvas for 3D rendering
// console.log("initEmscriptenCanvas canvas set");
}

// Handle canvas resizing (not called at the moment)
function _passCanvasSizeToEmscripten() {
const canvas = _getEmscriptenCanvas();
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
// Inform your rendering context about the resize if necessary
}

// GUI utilities
// ===============================================
function _showLoadingModal() {
}

function _hideLoadingModal() {
}

function _updateProgress(progress, message) {
console.log(`Progress: ${progress} - ${message}`);
}

function _displayError(message) {
console.error(message);
}

function _clearError() {
}

// Initial loading
//================================================


// Initialize Pyodide and load packages with progress updates
async function _loadPyodideAndPackages() {
try {
_showLoadingModal();
_updateProgress(0, 'Loading Pyodide...');
_gPyodide = await loadPyodide();
const pythonVersion = _gPyodide.runPython("import sys; sys.version");
_updateProgress(7, 'Pyodide loaded.');
console.log("Python version:", pythonVersion);
await _gPyodide.loadPackage("micropip");
await _gPyodide.loadPackage("micropip"); // firefox needs this to be loaded twice...
_updateProgress(10, 'micropip loaded.');

// Important:
// SDL support in Pyodide is experimental. The flag is used to bypass certain issues.
_gPyodide._api._skip_unwind_fatal_error = true;

// Determine the base URL dynamically
const baseUrl = `${window.location.origin}${window.location.pathname}`;
console.log('Base URL:', baseUrl);

// List of packages to install
const packages = [
// For imgui_bundle below
// -----------------------
'numpy', // 2.8 MB
'pydantic', // 1.3 + 0.4 MB = 1.7 MB
'typing_extensions', // 34 KB
'munch', // 10 KB
'imgui_bundle', // 9.7 MB (with 3 MB for demos_assets, 6 MB native)
'pillow', // 964 KB

// // For fiatlight below
// // --------------------
// 'requests', // 61KB, For word count demo (we download the Hamlet text)
// 'pandas', // 5.4 MB
// 'matplotlib', // 6.2 MB
// 'opencv-python', // 11 MB
// baseUrl + `/pyodide_dist/fiatlight-0.1.0-py3-none-any.whl`, // 3.5 MB
//
// // For scatter_widget_bundle
// // --------------------------
// "scikit-learn", // 6.3 MB
// "scipy", // 13 MB
// baseUrl + "/pyodide_dist/scatter_widget_bundle-0.1.0-py3-none-any.whl", // 8.3 KB
];

const totalSteps = packages.length;
let currentStep = 1;

for (const pkg of packages) {
_updateProgress(10 + (currentStep / totalSteps) * 80, `Installing ${pkg}...`);
await _gPyodide.runPythonAsync(`
import micropip;
await micropip.install('${pkg}')
`);
console.log(`${pkg} loaded.`);
currentStep++;
}

_updateProgress(100, 'All packages loaded.');
// Optionally, add a slight delay before hiding the modal
await new Promise(resolve => setTimeout(resolve, 500));
_hideLoadingModal();
console.log('Pyodide and packages loaded.');
} catch (error) {
console.error('Error loading Pyodide or packages:', error);
_displayError('Failed to load Pyodide or install packages. See console for details.');
_hideLoadingModal();
}
}

// Function to run Python code
export async function runPythonCode(code) {
if (!_gPyodide) {
console.error('Pyodide not loaded yet');
displayError('Pyodide is still loading. Please wait a moment and try again.');
return;
}

// Clear previous errors before running new code
_clearError();

try {
// Redirect stdout and stderr
_gPyodide.setStdout({
batched: (s) => console.log(s),
});
_gPyodide.setStderr({
batched: (s) => {
console.error(s);
_displayError(s);
},
});

// Execute the code
await _gPyodide.runPythonAsync(code);

// Optionally, call a specific function
// await pyodide.runPythonAsync('main()');

} catch (err) {
console.error('Caught PythonError:', err);
_displayError(err.toString());
}
}


//
export async function initializePyodideHelper()
{
await _loadPyodideAndPackages();
_passCanvasToPyodide();
}
26 changes: 26 additions & 0 deletions tutorial/single_page_book_app/resources_singlepage/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ body {
height: calc(100% - 2rem);
}

.canvas-emscripten {
/*position: absolute;*/
/*top: 0;*/
/*left: 0;*/
/*width: 100%;*/
/*height: 100%;*/
/*overflow: hidden;*/
}


/* ==========================
Breadcrumbs
========================== */
Expand Down Expand Up @@ -317,6 +327,22 @@ body {
}
}

.run-python-button {
margin-top: 0.5rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 0.9rem;
}

.run-python-button:hover {
background-color: #0056b3;
}


/* ==========================
Responsive Layout
========================== */
Expand Down

0 comments on commit 775b026

Please sign in to comment.