Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
02e94cf
Initial plan
Copilot May 8, 2026
7277c5f
Add canvas support to pyide directive
Copilot May 8, 2026
4adbc66
Address code review: add error handling and explicit canvas min-height
Copilot May 8, 2026
56de60b
Aktualisieren von pyide.md
mikebarkmin May 9, 2026
5ab26e1
add graphical output support for pygame
mikebarkmin May 9, 2026
0cc1786
change order in ci workflow
mikebarkmin May 9, 2026
cdc9316
fix resize on touch devices
mikebarkmin May 9, 2026
2d0b0de
add fullscreen mode
mikebarkmin May 9, 2026
3ebf06c
Fix Pytamaro Pyodide integration: add PyProxy object handling
mikebarkmin May 10, 2026
7578084
Improve Pytamaro FFI error logging and fix try-catch formatting
mikebarkmin May 10, 2026
abfab88
fix: render pytamaro show_graphic() output in pyide
mikebarkmin May 11, 2026
b670583
fix(pyide): handle chunked pytamaro stdout markers
mikebarkmin May 11, 2026
c401e00
fix(pyide): capture pytamaro output without newline
mikebarkmin May 11, 2026
ad79658
fix(pyide): force output mode for pytamaro runs
mikebarkmin May 11, 2026
a14bca3
fix: do not auto-focus editor after hitting run in pyide
Copilot May 15, 2026
171fd23
add code examples
mikebarkmin May 15, 2026
1b50126
improve pygame canvas documentation
mikebarkmin May 15, 2026
cdbb0b7
fix: restore editor interactivity after exception without auto-focusing
Copilot May 15, 2026
092751e
fix: quit pygame after script execution to release keyboard event lis…
Copilot May 15, 2026
3904fd3
fix: improve pygame cleanup readability and log cleanup errors
Copilot May 15, 2026
4bfbbd0
fix indentation
mikebarkmin May 15, 2026
a77a6b7
Merge branch 'copilot/add-canvas-support-to-pyide' of github.com:open…
mikebarkmin May 15, 2026
467597b
fix: full-height layout for all IDE directives on small screens
mikebarkmin May 15, 2026
66a1703
fix excalidraw wrong db path
mikebarkmin May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-pytamaro-show-graphic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperbook/markdown": patch
---

Fix pytamaro show_graphic not rendering images in pyide output. Parse `@@@PYTAMARO_DATA_URI_BEGIN@@@` / `@@@PYTAMARO_DATA_URI_END@@@` markers in stdout and render them as inline `<img>` elements.
7 changes: 7 additions & 0 deletions .changeset/large-chicken-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hyperbook/markdown": minor
"hyperbook-studio": minor
"hyperbook": minor
---

Add graphical output support to pyide. For example for pygame.
4 changes: 2 additions & 2 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Test packages
run: pnpm test
- name: Build packages
run: pnpm build
- name: Test packages
run: pnpm test
8 changes: 8 additions & 0 deletions packages/markdown/assets/directive-onlineide/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
border-radius: 8px;
background: #1e1e1e;
overflow: hidden;
display: flex;
flex-direction: column;
height: var(--onlineide-height, calc(100dvh - 80px));

.jo_iconButton.img_whole-window-dark {
display: none;
Expand All @@ -15,6 +18,11 @@
}
}

.directive-onlineide .java-online {
flex: 1;
min-height: 0;
}

.directive-onlineide .menu {
display: flex;
border-top: 1px solid var(--color-spacer);
Expand Down
113 changes: 113 additions & 0 deletions packages/markdown/assets/directive-p5/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,107 @@ hyperbook.p5 = (function () {
return sketchCode;
};

function setupSplitter(elem, container, editorContainer, splitter) {
if (!container || !editorContainer || !splitter) return;

const minPanelSize = 120;

const getIsHorizontal = () =>
getComputedStyle(elem).flexDirection.startsWith("row");

const applySplitSize = (rawSize, isHorizontal) => {
const total = isHorizontal ? elem.clientWidth : elem.clientHeight;
const splitterSize = isHorizontal ? splitter.offsetWidth : splitter.offsetHeight;
const maxSize = Math.max(minPanelSize, total - splitterSize - minPanelSize);
const clamped = Math.max(minPanelSize, Math.min(rawSize, maxSize));
container.style.flex = `0 0 ${clamped}px`;
return clamped;
};

const applyStoredSplitSize = () => {
const isHorizontal = getIsHorizontal();
elem.classList.toggle("split-horizontal", isHorizontal);
elem.classList.toggle("split-vertical", !isHorizontal);
const key = isHorizontal ? "splitHorizontal" : "splitVertical";
const rawStored = Number(elem.dataset[key]);
if (!Number.isFinite(rawStored) || rawStored <= 0) {
container.style.flex = "";
return;
}
applySplitSize(rawStored, isHorizontal);
};

applyStoredSplitSize();

splitter.addEventListener("pointerdown", (event) => {
event.preventDefault();
splitter.setPointerCapture(event.pointerId);

const isHorizontal = getIsHorizontal();
const key = isHorizontal ? "splitHorizontal" : "splitVertical";
const startPointer = isHorizontal ? event.clientX : event.clientY;
const startSize = isHorizontal
? container.getBoundingClientRect().width
: container.getBoundingClientRect().height;

elem.classList.add("resizing");

const onPointerMove = (moveEvent) => {
const pointer = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
const delta = pointer - startPointer;
const size = applySplitSize(startSize + delta, isHorizontal);
elem.dataset[key] = String(Math.round(size));
};

const onPointerUp = () => {
elem.classList.remove("resizing");
splitter.removeEventListener("pointermove", onPointerMove);
splitter.removeEventListener("pointerup", onPointerUp);
splitter.removeEventListener("pointercancel", onPointerUp);
};

splitter.addEventListener("pointermove", onPointerMove);
splitter.addEventListener("pointerup", onPointerUp);
splitter.addEventListener("pointercancel", onPointerUp);
});

window.addEventListener("resize", applyStoredSplitSize);
}

const updateFullscreenButtonState = (elem, button) => {
if (!elem || !button) return;
const isFullscreen = document.fullscreenElement === elem;
const label = hyperbook.i18n.get("ide-fullscreen-enter");
button.textContent = "⛶";
button.title = label;
button.setAttribute("aria-label", label);
button.classList.toggle("active", isFullscreen);
};

const toggleFullscreen = async (elem) => {
if (!elem) return;
if (document.fullscreenElement === elem) {
await document.exitFullscreen();
return;
}
await elem.requestFullscreen();
};

const syncFullscreenButtons = () => {
const elems = document.querySelectorAll(".directive-p5");
elems.forEach((elem) => {
const fullscreen = elem.querySelector("button.fullscreen");
updateFullscreenButtonState(elem, fullscreen);
});
};

function initElement(elem) {
if (elem.getAttribute("data-p5-initialized") === "true") return;
elem.setAttribute("data-p5-initialized", "true");

const container = elem.querySelector(".container");
const editorContainer = elem.querySelector(".editor-container");
const splitter = elem.querySelector(".splitter");
const editor = elem.getElementsByClassName("editor")[0];
/** @type {HTMLButtonElement} */
const update = elem.getElementsByClassName("update")[0];
Expand All @@ -37,6 +137,18 @@ hyperbook.p5 = (function () {
const copyEl = elem.getElementsByClassName("copy")[0];
const resetEl = elem.getElementsByClassName("reset")[0];
const downloadEl = elem.getElementsByClassName("download")[0];
const fullscreenEl = elem.getElementsByClassName("fullscreen")[0];

setupSplitter(elem, container, editorContainer, splitter);

fullscreenEl?.addEventListener("click", async () => {
try {
await toggleFullscreen(elem);
} catch (error) {
console.error(error.message);
}
});
updateFullscreenButtonState(elem, fullscreenEl);

if (frame) {
frame.srcdoc = frame.srcdoc.replaceAll(
Expand Down Expand Up @@ -117,6 +229,7 @@ hyperbook.p5 = (function () {
});

observer.observe(document.body, { childList: true, subtree: true });
document.addEventListener("fullscreenchange", syncFullscreenButtons);

return { init };
})();
70 changes: 67 additions & 3 deletions packages/markdown/assets/directive-p5/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ code-input {

.directive-p5 .container {
width: 100%;
min-height: 120px;
min-width: 120px;
border: 1px solid var(--color-spacer);
border-radius: 8px;
overflow: hidden;
Expand All @@ -22,7 +24,50 @@ code-input {
width: 100%;
display: flex;
flex-direction: column;
height: 400px;
min-height: 120px;
min-width: 120px;
flex: 1 1 0;
}

.directive-p5:not(.standalone) {
height: var(--p5-height, calc(100dvh - 80px));
}

.directive-p5:not(.standalone) .container {
flex: 1 1 0;
}

.directive-p5 .splitter {
background: var(--color-spacer);
border-radius: 999px;
flex-shrink: 0;
touch-action: none;
opacity: 0.45;
transition: opacity 0.15s ease-in-out;
}

.directive-p5 .splitter:hover {
opacity: 0.65;
}

.directive-p5.split-vertical .splitter {
width: 100%;
height: 4px;
cursor: row-resize;
}

.directive-p5.split-horizontal .splitter {
width: 4px;
height: 100%;
cursor: col-resize;
}

.directive-p5.resizing {
user-select: none;
}

.directive-p5.resizing .splitter {
opacity: 0.75;
}

.directive-p5 .editor {
Expand All @@ -38,6 +83,7 @@ code-input {
border-bottom: none;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
overflow: hidden;
}

.directive-p5 .buttons.bottom {
Expand All @@ -58,10 +104,17 @@ code-input {
cursor: pointer;
}

.directive-p5 .buttons:last-child {
.directive-p5 .buttons button:last-child {
border-right: none;
}

.directive-p5 button.fullscreen {
flex: 0 0 auto;
min-width: 42px;
width: 42px;
padding: 8px 0;
}

.directive-p5 button:hover {
background-color: var(--color-spacer);
}
Expand All @@ -76,10 +129,21 @@ code-input {
margin: 0 auto;
}

.directive-p5:fullscreen {
width: 100vw;
height: 100dvh !important;
padding: 12px;
box-sizing: border-box;
background-color: var(--color-background, var(--color--background, #fff));
}

.directive-p5:fullscreen::backdrop {
background-color: var(--color-background, var(--color--background, #fff));
}

@media screen and (min-width: 1024px) {
.directive-p5:not(.standalone) {
flex-direction: row;
height: calc(100dvh - 128px);
.container {
flex: 1;
height: 100% !important;
Expand Down
Loading
Loading