From 66913cf05dcc2386342c5d774370632ec452f0aa Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Fri, 7 Aug 2026 14:46:19 -0700 Subject: [PATCH 1/9] add experiment --- cli/buildengine.ts | 4 ++-- pxteditor/experiments.ts | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/cli/buildengine.ts b/cli/buildengine.ts index 42ba3d8aab93..cee175edb349 100644 --- a/cli/buildengine.ts +++ b/cli/buildengine.ts @@ -86,7 +86,7 @@ export const buildEngines: Map = { codal: { id: "codal", updateEngineAsync: updateCodalBuildAsync, - buildAsync: () => runBuildCmdAsync("python", "build.py"), + buildAsync: () => runBuildCmdAsync("python3", "build.py"), setPlatformAsync: noopAsync, patchHexInfo: patchCodalHexInfo, prepBuildDirAsync: prepCodalBuildDirAsync, @@ -99,7 +99,7 @@ export const buildEngines: Map = { dockercodal: { id: "dockercodal", updateEngineAsync: updateCodalBuildAsync, - buildAsync: () => runDockerAsync(["python", "build.py"]), + buildAsync: () => runDockerAsync(["python3", "build.py"]), setPlatformAsync: noopAsync, patchHexInfo: patchCodalHexInfo, prepBuildDirAsync: prepCodalBuildDirAsync, diff --git a/pxteditor/experiments.ts b/pxteditor/experiments.ts index 8f3496acaa78..8d1c0d449eb9 100644 --- a/pxteditor/experiments.ts +++ b/pxteditor/experiments.ts @@ -178,6 +178,13 @@ export function all(): Experiment[] { feedbackUrl: "https://github.com/microsoft/pxt/issues/10694", enableOnline: true }, + { + id: "jacdacUI", + name: lf("New Jacdac UI"), + description: lf("Simplified Jacdac UI"), + // feedbackUrl: "https://github.com/microsoft/pxt/issues/10694", + enableOnline: true + }, ]; return exps.filter(experiment => ids.indexOf(experiment.id) > -1) From 60dbcbf9c1ee3fcadad23303d1b6b83a494fae86 Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Fri, 7 Aug 2026 15:45:03 -0700 Subject: [PATCH 2/9] switching in place --- localtypings/pxteditor.d.ts | 1 + theme/tutorial-sidebar.less | 30 ++++++++++++ webapp/src/app.tsx | 32 ++++++++----- webapp/src/sidepanel.tsx | 95 ++++++++++++++++++++++++++++--------- 4 files changed, 124 insertions(+), 34 deletions(-) diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index 7e8ee576d315..56947f24c381 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -998,6 +998,7 @@ declare namespace pxt.editor { stopSimulator(unload?: boolean, opts?: SimulatorStartOptions): void; restartSimulator(): void; startSimulator(opts?: SimulatorStartOptions): void; + reinitializeSimulatorAsync(): Promise; runSimulator(): void; isSimulatorRunning(): boolean; expandSimulator(): void; diff --git a/theme/tutorial-sidebar.less b/theme/tutorial-sidebar.less index 1431a4b6bfca..ff88af8e4d66 100644 --- a/theme/tutorial-sidebar.less +++ b/theme/tutorial-sidebar.less @@ -454,6 +454,36 @@ &.ui.items { margin-top: 0; } } +.jacdac-view-toggle { + display: inline-flex; + align-self: flex-start; + gap: 0.35rem; + margin: 0.75rem 0.75rem 0.25rem 0.75rem; + padding: 0.35rem; + border: 1px solid var(--pxt-target-foreground0); + border-radius: 0.75rem; + background: var(--pxt-target-background1); + + > .ui.button { + border: 1px solid var(--pxt-target-foreground0) !important; + border-radius: 0.5rem !important; + box-shadow: none !important; + margin: 0 !important; + color: var(--pxt-button-secondary-foreground) !important; + + &:not(.active) { + background: transparent !important; + } + + &.active { + background: var(--pxt-colors-blue-background) !important; + border-color: var(--pxt-colors-blue-background) !important; + color: var(--pxt-colors-blue-foreground) !important; + box-shadow: 0 0 0 1px var(--pxt-colors-blue-background), 0 0.15rem 0.35rem rgba(0, 0, 0, 0.18) !important; + } + } +} + // Mini sim is visible when tab is hidden #root.tabTutorial { &:not(.fullscreensim) .simulator-container.hidden { diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 435d3ff22c14..eb7a80cd77cc 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -1130,7 +1130,25 @@ export class ProjectView public async componentDidMount() { this.allEditors.forEach(e => e.prepare()) - await simulator.initAsync({ + await simulator.initAsync(this.createSimulatorInitOptions()); + + // we now have editors prepared + this.forceUpdate(); + // start blockly load + this.loadBlocklyAsync(); + + // subscribe to user preference changes (for simulator or non-render subscriptions) + data.subscribe(this.cloudStatusSubscriber, `${cloud.HEADER_CLOUDSTATE}:*`); + data.subscribe(this.headerChangeSubscriber, "header:*"); + this.editorMountComplete.resolve(undefined); + } + + public async reinitializeSimulatorAsync() { + await simulator.initAsync(this.createSimulatorInitOptions()); + } + + private createSimulatorInitOptions(): Parameters[0] { + return { orphanException: brk => { // TODO: start debugging session // TODO: user friendly error message @@ -1194,17 +1212,7 @@ export class ProjectView pkg.mainEditorPkg().setSimState(k, v) }, editor: this.state.header ? this.state.header.editor : '' - }); - - // we now have editors prepared - this.forceUpdate(); - // start blockly load - this.loadBlocklyAsync(); - - // subscribe to user preference changes (for simulator or non-render subscriptions) - data.subscribe(this.cloudStatusSubscriber, `${cloud.HEADER_CLOUDSTATE}:*`); - data.subscribe(this.headerChangeSubscriber, "header:*"); - this.editorMountComplete.resolve(undefined); + }; } public componentWillUnmount() { diff --git a/webapp/src/sidepanel.tsx b/webapp/src/sidepanel.tsx index 6826f8956737..3e07949fe12b 100644 --- a/webapp/src/sidepanel.tsx +++ b/webapp/src/sidepanel.tsx @@ -21,6 +21,7 @@ interface SidepanelState { height?: number; lastResizeHeight?: number; shouldResize?: boolean; + jacdacView?: "simulator" | "twins"; } interface SidepanelProps extends ISettingsProps { @@ -87,6 +88,11 @@ export class Sidepanel extends data.Component { this.props.setEditorOffset(); } + if (this.state.jacdacView === "simulator" && state.jacdacView === "twins") { + void this.props.parent.reinitializeSimulatorAsync() + .then(() => this.props.parent.restartSimulator()); + } + this.updateShouldResize(); } @@ -143,6 +149,17 @@ export class Sidepanel extends data.Component { } } + protected setJacdacView = (jacdacView: "simulator" | "twins") => { + if (this.state.jacdacView !== jacdacView) { + if (jacdacView === "twins") { + this.props.parent.stopSimulator(true); + this.setState({ jacdacView }); + return; + } + this.setState({ jacdacView }); + } + } + protected handleSimPanelRef = (c: HTMLDivElement) => { this.simPanelRef = c; if (c && typeof ResizeObserver !== "undefined") { @@ -209,6 +226,12 @@ export class Sidepanel extends data.Component { const showHostMultiplayerGameButton = isMultiplayerGame && !pxt.shell.isTimeMachineEmbed(); + // TODO: for now, we show the jacdacUI even if we've received no jacdac messages over WebUSB + const jacdacUI = hasSimulator && + pxt.appTarget?.appTheme?.experiments?.some(e => e === "jacdacUI") + && !pxt.shell.isTimeMachineEmbed(); + const jacdacView = this.state.jacdacView || "simulator"; + const simContainerClassName = classList( "simulator-container", !this.props.tutorialSimSidebar && "hidden" @@ -242,6 +265,8 @@ export class Sidepanel extends data.Component { onTutorialComplete={onTutorialComplete} setParentHeight={newSize => this.setComponentHeight(newSize, false)} /> : undefined; + // TODO: this is where we check if jacdacUI experiment is enabled and add switch + // TODO: to toggle between the simulator view and the twin view return
{!hasSimulator && <>
@@ -252,30 +277,56 @@ export class Sidepanel extends data.Component {
} } {hasSimulator &&
+ {jacdacUI &&
+
}
-
- {showHostMultiplayerGameButton &&
- -
} - - {showKeymap && } -
- {pxt.options.debug &&
-
- {showOpenInVscodeButton &&
-
- {showHostMultiplayerGameButton &&
- {showSerialButtons &&
- - -
} - - {showFileList && } - {showFullscreenButton &&
} + {jacdacUI && jacdacView === "twins" ? +
+ {lf("Twins placeholder")} +
: + <> +
+ {showHostMultiplayerGameButton &&
+ +
} + + {showKeymap && } +
+ {pxt.options.debug &&
+
+ {showOpenInVscodeButton &&
+
+ {showHostMultiplayerGameButton &&
+ {showSerialButtons &&
+ + +
} + + {showFileList && } + {showFullscreenButton &&
} + }
} From c86e0be995dfc4957b6465ca7445e01115d200d3 Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Mon, 10 Aug 2026 11:55:39 -0700 Subject: [PATCH 3/9] update jacdacView from "twins" to "devices" in Sidepanel component --- webapp/src/sidepanel.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/webapp/src/sidepanel.tsx b/webapp/src/sidepanel.tsx index 3e07949fe12b..dd0c23356d7f 100644 --- a/webapp/src/sidepanel.tsx +++ b/webapp/src/sidepanel.tsx @@ -21,7 +21,7 @@ interface SidepanelState { height?: number; lastResizeHeight?: number; shouldResize?: boolean; - jacdacView?: "simulator" | "twins"; + jacdacView?: "simulator" | "devices"; } interface SidepanelProps extends ISettingsProps { @@ -88,7 +88,7 @@ export class Sidepanel extends data.Component { this.props.setEditorOffset(); } - if (this.state.jacdacView === "simulator" && state.jacdacView === "twins") { + if (this.state.jacdacView === "simulator" && state.jacdacView === "devices") { void this.props.parent.reinitializeSimulatorAsync() .then(() => this.props.parent.restartSimulator()); } @@ -149,9 +149,9 @@ export class Sidepanel extends data.Component { } } - protected setJacdacView = (jacdacView: "simulator" | "twins") => { + protected setJacdacView = (jacdacView: "simulator" | "devices") => { if (this.state.jacdacView !== jacdacView) { - if (jacdacView === "twins") { + if (jacdacView === "devices") { this.props.parent.stopSimulator(true); this.setState({ jacdacView }); return; @@ -290,18 +290,18 @@ export class Sidepanel extends data.Component { onClick={() => this.setJacdacView("simulator")} />
}
- {jacdacUI && jacdacView === "twins" ? -
- {lf("Twins placeholder")} + {jacdacUI && jacdacView === "devices" ? +
+ {lf("Devices placeholder")}
: <>
From da2fa48e3e8580d9ea134233e84dbca7313d4db2 Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Sat, 15 Aug 2026 12:16:48 -0700 Subject: [PATCH 4/9] add setMode method to SimulatorDriver and update jacdacView handling in Sidepanel --- pxtsim/simdriver.ts | 6 ++++ webapp/src/sidepanel.tsx | 62 ++++++++++++++++------------------------ 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/pxtsim/simdriver.ts b/pxtsim/simdriver.ts index ffc6a78cd5ec..9cdcfe933037 100644 --- a/pxtsim/simdriver.ts +++ b/pxtsim/simdriver.ts @@ -350,6 +350,10 @@ namespace pxsim { this.singleSimulator = true } + public setMode(mode: "simulator" | "devices") { + console.log(`SimulatorDriver.setMode(${mode})`); + } + public postMessage(msg: pxsim.SimulatorMessage, source?: Window, frameID?: string) { if (this.hwdbg) { this.hwdbg.postMessage(msg) @@ -732,6 +736,8 @@ namespace pxsim { }); } + // TODO: we can use this to hide everything except the device view with jacdac + // TODO: probably better not to overload this public hide(completeHandler?: () => void) { this.suspend(); if (!this.options.removeElement) return; diff --git a/webapp/src/sidepanel.tsx b/webapp/src/sidepanel.tsx index dd0c23356d7f..f2e63816b41a 100644 --- a/webapp/src/sidepanel.tsx +++ b/webapp/src/sidepanel.tsx @@ -88,11 +88,6 @@ export class Sidepanel extends data.Component { this.props.setEditorOffset(); } - if (this.state.jacdacView === "simulator" && state.jacdacView === "devices") { - void this.props.parent.reinitializeSimulatorAsync() - .then(() => this.props.parent.restartSimulator()); - } - this.updateShouldResize(); } @@ -151,12 +146,9 @@ export class Sidepanel extends data.Component { protected setJacdacView = (jacdacView: "simulator" | "devices") => { if (this.state.jacdacView !== jacdacView) { - if (jacdacView === "devices") { - this.props.parent.stopSimulator(true); - this.setState({ jacdacView }); - return; - } this.setState({ jacdacView }); + // let the simdriver switch between the virtual and physical jacdac bus + simulator?.driver?.setMode(jacdacView) } } @@ -299,34 +291,28 @@ export class Sidepanel extends data.Component {
}
- {jacdacUI && jacdacView === "devices" ? -
- {lf("Devices placeholder")} -
: - <> -
- {showHostMultiplayerGameButton &&
- -
} - - {showKeymap && } -
- {pxt.options.debug &&
-
- {showOpenInVscodeButton &&
-
- {showHostMultiplayerGameButton &&
- {showSerialButtons &&
- - -
} - - {showFileList && } - {showFullscreenButton &&
} - } +
+ {showHostMultiplayerGameButton &&
+ +
} + + {showKeymap && } +
+ {pxt.options.debug &&
+
+ {showOpenInVscodeButton &&
+
+ {showHostMultiplayerGameButton &&
+ {showSerialButtons &&
+ + +
} + + {showFileList && } + {showFullscreenButton &&
}
} From e1e16823fb8640c31bc3cbe88b5a3ff93745c323 Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Sat, 15 Aug 2026 19:16:11 -0700 Subject: [PATCH 5/9] implement setMode method in SimulatorDriver and update Sidepanel to conditionally render components based on jacdacView state --- pxtsim/simdriver.ts | 41 +++++++++++++++++++++++++++++++++++++--- webapp/src/sidepanel.tsx | 3 +++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/pxtsim/simdriver.ts b/pxtsim/simdriver.ts index 9cdcfe933037..6ea705dd6e29 100644 --- a/pxtsim/simdriver.ts +++ b/pxtsim/simdriver.ts @@ -350,8 +350,45 @@ namespace pxsim { this.singleSimulator = true } + private removedElements: HTMLElement[] = []; + private removedIndices: number[] = []; + private jacdacIndex: number = -1; public setMode(mode: "simulator" | "devices") { - console.log(`SimulatorDriver.setMode(${mode})`); + if (this.jacdacIndex != -1 && mode === "simulator") { + // add frames back to the DOM + const jacdacFrame = this.simFrames()[0]; + this.removedElements.forEach((elem, index) => { + if (index < this.jacdacIndex) { + this.container.insertBefore(elem, jacdacFrame.parentElement); + } else { + this.container.appendChild(elem); + } + }); + this.removedElements = []; + this.removedIndices = []; + this.jacdacIndex = -1; + this.postMessageCore(jacdacFrame, { + type: "simulatorMode", + source: MESSAGE_SOURCE, + }); + this.start(); + } else if (this.jacdacIndex === -1 && mode === "devices") { + this.suspend(); + const frames = this.simFrames(); + frames.forEach((frame,index) => { + if (frame.dataset[FRAME_DATA_MESSAGE_CHANNEL] !== "jacdac/pxt-jacdac") { + this.removedElements.push(frame.parentElement); + this.removedIndices.push(index); + } else { + this.jacdacIndex = index; + this.postMessageCore(frame, { + type: "devicesMode", + source: MESSAGE_SOURCE, + }); + } + }); + this.removedElements.forEach(elem => this.container.removeChild(elem)); + } } public postMessage(msg: pxsim.SimulatorMessage, source?: Window, frameID?: string) { @@ -736,8 +773,6 @@ namespace pxsim { }); } - // TODO: we can use this to hide everything except the device view with jacdac - // TODO: probably better not to overload this public hide(completeHandler?: () => void) { this.suspend(); if (!this.options.removeElement) return; diff --git a/webapp/src/sidepanel.tsx b/webapp/src/sidepanel.tsx index f2e63816b41a..bd73a284369d 100644 --- a/webapp/src/sidepanel.tsx +++ b/webapp/src/sidepanel.tsx @@ -295,6 +295,8 @@ export class Sidepanel extends data.Component { {showHostMultiplayerGameButton &&
} + {jacdacUI && jacdacView === "devices" ? <> : ( + <> {showKeymap && }
@@ -313,6 +315,7 @@ export class Sidepanel extends data.Component { {showFileList && } {showFullscreenButton &&
} + )}
} From b0493066af48f2d54d8cbc8b4568af35da6cbd0f Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Tue, 18 Aug 2026 13:56:38 -0700 Subject: [PATCH 6/9] separate physical bus from sim bus --- pxtsim/simdriver.ts | 14 ++++++++++++-- webapp/src/app.tsx | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pxtsim/simdriver.ts b/pxtsim/simdriver.ts index 6ea705dd6e29..a3eb8e88787d 100644 --- a/pxtsim/simdriver.ts +++ b/pxtsim/simdriver.ts @@ -350,10 +350,12 @@ namespace pxsim { this.singleSimulator = true } + private mode: "simulator" | "devices" = "simulator" private removedElements: HTMLElement[] = []; private removedIndices: number[] = []; private jacdacIndex: number = -1; public setMode(mode: "simulator" | "devices") { + this.mode = mode if (this.jacdacIndex != -1 && mode === "simulator") { // add frames back to the DOM const jacdacFrame = this.simFrames()[0]; @@ -396,6 +398,12 @@ namespace pxsim { this.hwdbg.postMessage(msg) return } + if (this.mode === "simulator" && (msg as any)?.sender === "packetio") { + const messageChannel = msg.type === "messagepacket" && (msg as SimulatorControlMessage).channel; + if (messageChannel === "jacdac") { + return; // don't send packetio jacdac messages to sims when in simulator mode + } + } const depEditors = this.dependentEditors(); let frames = this.simFrames(); @@ -411,7 +419,8 @@ namespace pxsim { broadcastmsg.srcFrameIndex = this.simFrames().findIndex((item) => item.contentWindow === source); const sourceFrame = broadcastmsg.srcFrameIndex >= 0 ? this.simFrames()[broadcastmsg.srcFrameIndex] : undefined; // jacdac messages from a board sim other than first should be dropped - if (broadcastmsg.srcFrameIndex > 0 && mkcdFrames.find(f => f === sourceFrame) && messageChannel === "jacdac") + if (broadcastmsg.srcFrameIndex > 0 && mkcdFrames.find(f => f === sourceFrame) + && messageChannel === "jacdac") return; // if the editor is hosted in a multi-editor setting // don't start extra frames @@ -518,7 +527,8 @@ namespace pxsim { if (source && frame.contentWindow == source) continue; // if jacdac message, don't send to other (board) simulator frames if (i > 0 && !frame.dataset[FRAME_DATA_MESSAGE_CHANNEL] && - msg.type === "messagepacket" && (msg as pxsim.SimulatorControlMessage).channel === "jacdac") continue; + msg.type === "messagepacket" && + (msg as pxsim.SimulatorControlMessage).channel === "jacdac") continue; // frame not in DOM if (!frame.contentWindow) continue; diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index eb7a80cd77cc..ebb80308cca7 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -5773,7 +5773,7 @@ function initPacketIO() { broadcast: false, channel: type, data: payload, - sender: "packetio", + sender: "packetio", // QUESTION: shouldn't this be 'source' instead of 'sender'? }, "*") }); From 0c3b6866a9eeb3f5e23c98d5e4e5059ff80c58c2 Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Thu, 20 Aug 2026 11:32:58 -0700 Subject: [PATCH 7/9] add jacdac message handling to Sidepanel and fix event listener management in SerialIndicator --- webapp/src/serialindicator.tsx | 2 +- webapp/src/sidepanel.tsx | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/webapp/src/serialindicator.tsx b/webapp/src/serialindicator.tsx index 71f15e649f44..e03f46a75580 100644 --- a/webapp/src/serialindicator.tsx +++ b/webapp/src/serialindicator.tsx @@ -28,7 +28,7 @@ export class SerialIndicator extends data.Component { } componentDidMount(): void { + window.addEventListener("message", this.setActive.bind(this)) this.updateShouldResize(); } @@ -87,11 +89,11 @@ export class Sidepanel extends data.Component { if ((this.state.height || state.height) && this.state.height != state.height) { this.props.setEditorOffset(); } - this.updateShouldResize(); } componentWillUnmount(): void { + window.removeEventListener("message", this.setActive.bind(this)) if (this.simResizeObserver && this.simRef) { this.simResizeObserver.unobserve(this.simRef); } @@ -101,6 +103,14 @@ export class Sidepanel extends data.Component { } } + private setActive(ev: MessageEvent) { + // check for packetio jacdac messages and show the jacdacUI toggle if so + const msg = ev.data; + if (msg?.type === "messagepacket" && msg?.sender === "packetio" && msg?.channel === "jacdac") { + this.setState({ showSimulatorDevicesToggle: true }); + } + } + private updateShouldResize() { const shouldResize = pxt.BrowserUtils.isTabletSize() || this.props.tutorialSimSidebar; if (shouldResize != this.state.shouldResize) { @@ -218,10 +228,11 @@ export class Sidepanel extends data.Component { const showHostMultiplayerGameButton = isMultiplayerGame && !pxt.shell.isTimeMachineEmbed(); - // TODO: for now, we show the jacdacUI even if we've received no jacdac messages over WebUSB const jacdacUI = hasSimulator && pxt.appTarget?.appTheme?.experiments?.some(e => e === "jacdacUI") - && !pxt.shell.isTimeMachineEmbed(); + && !pxt.shell.isTimeMachineEmbed() && this.state.showSimulatorDevicesToggle || + true; // TODO, for now always show the toggle - not clear we want to show it only when jacdac messages are received, + const jacdacView = this.state.jacdacView || "simulator"; const simContainerClassName = classList( From 6fdc5398b9e68e94ce38a3173ed17c5a1a3d8ebe Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Thu, 20 Aug 2026 14:51:04 -0700 Subject: [PATCH 8/9] add jacdac button and editor plumbing --- localtypings/pxtarget.d.ts | 1 + localtypings/pxteditor.d.ts | 1 + pxtlib/main.ts | 1 + theme/common.less | 4 ++- theme/highcontrast.less | 6 +++-- theme/serial.less | 16 ++++++++---- webapp/src/app.tsx | 28 +++++++++++++++++--- webapp/src/jacdac.tsx | 51 +++++++++++++++++++++++++++++++++++++ webapp/src/sidepanel.tsx | 19 ++++++++++++-- 9 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 webapp/src/jacdac.tsx diff --git a/localtypings/pxtarget.d.ts b/localtypings/pxtarget.d.ts index a7eb379bb604..659329a2ba6f 100644 --- a/localtypings/pxtarget.d.ts +++ b/localtypings/pxtarget.d.ts @@ -544,6 +544,7 @@ declare namespace pxt { forceEnableAiErrorHelp?: boolean; // Enables the AI Error Help feature, regardless of geo setting. shareHomepageContent?: boolean; // Show buttons to share links to homepage content more easily showProjectDescription?: boolean; // Show project description in pxtjson editor and share dialog + showJacdac?: boolean; // show jacdac button in the sidebar } interface DownloadDialogTheme { diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index 56947f24c381..0f3664d73c56 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -831,6 +831,7 @@ declare namespace pxt.editor { feedback?: FeedbackState; themePickerOpen?: boolean; errorListNote?: string; + showJacdac?: boolean; } export interface EditorState { diff --git a/pxtlib/main.ts b/pxtlib/main.ts index e1566fa2d63a..493e806e518a 100644 --- a/pxtlib/main.ts +++ b/pxtlib/main.ts @@ -522,6 +522,7 @@ namespace pxt { export const CONFIG_NAME = "pxt.json" export const SIMSTATE_JSON = ".simstate.json" export const SERIAL_EDITOR_FILE = "serial.txt" + export const JACDAC_EDITOR_FILE = "jacdac.txt" export const README_FILE = "README.md" export const GITIGNORE_FILE = ".gitignore" export const ASSETS_FILE = "assets.json" diff --git a/theme/common.less b/theme/common.less index 113a97f7a0e6..1950a5dc0318 100644 --- a/theme/common.less +++ b/theme/common.less @@ -1445,7 +1445,8 @@ Field editors } } -#serialPreview .label:focus { +#serialPreview .label:focus, +#jacdacPreview .label:focus { outline: 3px solid var(--pxt-focus-border) !important; outline-offset: -15px; } @@ -1642,6 +1643,7 @@ p.ui.font.small { #maineditor, #editortools, #serialPreview, + #jacdacPreview, .settings-menuitem, .help-dropdown-menuitem { display: none !important; diff --git a/theme/highcontrast.less b/theme/highcontrast.less index a35dfe90a264..be1934e3fa93 100644 --- a/theme/highcontrast.less +++ b/theme/highcontrast.less @@ -617,7 +617,8 @@ /* Serial editor */ - #serialPreview div { + #serialPreview div, + #jacdacPreview div { color: @HCtextColor; } @@ -640,7 +641,8 @@ border-color: @HCtextColor; } - #serialPreview .label { + #serialPreview .label, + #jacdacPreview .label { border: 10px solid @HCtextColor !important; &:hover { border-color: darken(@HCtextColor, 10.0) !important; diff --git a/theme/serial.less b/theme/serial.less index e8cde459f935..63806f3c080f 100644 --- a/theme/serial.less +++ b/theme/serial.less @@ -222,11 +222,13 @@ margin: 0 !important; } -#serialPreview { +#serialPreview, +#jacdacPreview { cursor: pointer; } -#serialPreview .label { +#serialPreview .label, +#jacdacPreview .label { width: 100%; background-color: var(--pxt-target-background2); font-size: 0.85em; @@ -245,16 +247,20 @@ } } -#serialPreview .label:hover { +#serialPreview .label:hover, +#jacdacPreview .label:hover { opacity: 0.8; } -#serialPreview .label:focus { +#serialPreview .label:focus, +#jacdacPreview .label:focus { outline: none; } .fullscreensim #serialPreview, -.simView #serialPreview { +.fullscreensim #jacdacPreview, +.simView #serialPreview, +.simView #jacdacPreview { display: none !important; z-index: -10 !important; } diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index ebb80308cca7..f63c5fdc1dc2 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -52,6 +52,7 @@ import * as monaco from "./monaco" import * as toolboxHelpers from "./toolboxHelpers" import * as pxtjson from "./pxtjson" import * as serial from "./serial" +import * as jacdac from "./jacdac" import * as blocks from "./blocks" import * as gitjson from "./gitjson" import * as serialindicator from "./serialindicator" @@ -137,6 +138,7 @@ export class ProjectView textEditor: monaco.Editor; pxtJsonEditor: pxtjson.Editor; serialEditor: serial.Editor; + jacdacEditor: jacdac.Editor; blocksEditor: blocks.Editor; gitjsonEditor: gitjson.Editor; assetEditor: assetEditor.AssetEditor; @@ -208,7 +210,8 @@ export class ProjectView activeTourConfig: undefined, mute: pxt.editor.MuteState.Unmuted, feedback: {showing: false, kind: "generic"}, // state that tracks if the feedback modal is showing and what kind - errorListCollapsed: true // error pane is collapsed by default + errorListCollapsed: true, // error pane is collapsed by default + showJacdac: pxt.appTarget.appTheme.showJacdac || false, // show jacdac button in the sidebar }; if (!this.settings.editorFontSize) this.settings.editorFontSize = /mobile/i.test(navigator.userAgent) ? 15 : 19; if (!this.settings.fileHistory) this.settings.fileHistory = []; @@ -222,6 +225,7 @@ export class ProjectView this.openSimSerial = this.openSimSerial.bind(this); this.openDeviceSerial = this.openDeviceSerial.bind(this); this.openSerial = this.openSerial.bind(this); + this.openJacdac = this.openJacdac.bind(this); this.toggleGreenScreen = this.toggleGreenScreen.bind(this); this.toggleScreenReaderModeAsync = this.toggleScreenReaderModeAsync.bind(this); this.toggleSimulatorFullscreen = this.toggleSimulatorFullscreen.bind(this); @@ -843,6 +847,20 @@ export class ProjectView this.setFile(mainEditorPkg.lookupFile("this/" + pxt.SERIAL_EDITOR_FILE)) } + openJacdac() { + if (this.editor == this.jacdacEditor) + return; // already showing + + const mainEditorPkg = pkg.mainEditorPkg() + if (!mainEditorPkg) return; // no project loaded + + if (!mainEditorPkg.lookupFile("this/" + pxt.JACDAC_EDITOR_FILE)) { + mainEditorPkg.setFile(pxt.JACDAC_EDITOR_FILE, "jacdac\n", true) + } + pxt.tickEvent("jacdac.editorOpened") + this.setFile(mainEditorPkg.lookupFile("this/" + pxt.JACDAC_EDITOR_FILE)) + } + openPreviousEditor() { this.preserveUndoStack = true; const id = this.state.header.id; @@ -1097,6 +1115,7 @@ export class ProjectView this.textEditor = new monaco.Editor(this); this.pxtJsonEditor = new pxtjson.Editor(this); this.serialEditor = new serial.Editor(this); + this.jacdacEditor = new jacdac.Editor(this); this.blocksEditor = new blocks.Editor(this); this.gitjsonEditor = new gitjson.Editor(this); this.assetEditor = new assetEditor.AssetEditor(this); @@ -1118,7 +1137,7 @@ export class ProjectView this.editorChangeHandler(); } } - this.allEditors = [this.pxtJsonEditor, this.gitjsonEditor, this.blocksEditor, this.serialEditor, this.assetEditor, this.textEditor] + this.allEditors = [this.pxtJsonEditor, this.gitjsonEditor, this.blocksEditor, this.serialEditor, this.jacdacEditor, this.assetEditor, this.textEditor] this.allEditors.forEach(e => e.changeCallback = changeHandler) this.editor = this.allEditors[this.allEditors.length - 1] } @@ -5568,7 +5587,7 @@ export class ProjectView const inDebugMode = this.state.debugging; const inHome = this.state.home && !sandbox; const inEditor = !!this.state.header && !inHome; - const { lightbox, greenScreen } = this.state; + const { lightbox, greenScreen, showJacdac } = this.state; const hideTutorialIteration = inTutorial && tutorialOptions.metadata?.hideIteration; const hideToolbox = inTutorial && tutorialOptions.metadata?.hideToolbox; // flyoutOnly has become a de facto css class for styling tutorials (especially minecraft HOC), so keep it if hideToolbox is true, even if flyoutOnly is false. @@ -5621,6 +5640,7 @@ export class ProjectView sandbox && this.isEmbedSimActive() ? 'simView' : '', isApp ? "app" : "", greenScreen ? "greenscreen" : "", + showJacdac ? "showJacdac" : "", logoWide ? "logo-wide" : "", isHeadless ? "headless" : "", flyoutOnly ? "flyoutOnly" : "", @@ -5677,6 +5697,7 @@ export class ProjectView +
+
+
+ + + {lf("Go back")} + +
+
+
+
+ ) + } +} diff --git a/webapp/src/sidepanel.tsx b/webapp/src/sidepanel.tsx index 744ef229d31a..52c820b5c8b3 100644 --- a/webapp/src/sidepanel.tsx +++ b/webapp/src/sidepanel.tsx @@ -8,10 +8,11 @@ import * as serialindicator from "./serialindicator" import * as simtoolbar from "./simtoolbar"; import * as simulator from "./simulator"; -import { Button } from "./sui"; +import { Button, Icon } from "./sui"; import { SimulatorPresenceBar } from "./components/SimulatorPresenceBar" import { TutorialContainer } from "./components/tutorial/TutorialContainer"; import { VerticalResizeContainer } from '../../react-common/components/controls/VerticalResizeContainer' +import { fireClickOnEnter } from "./util" import ISettingsProps = pxt.editor.ISettingsProps; import { classList } from "../../react-common/components/util"; @@ -29,6 +30,7 @@ interface SidepanelProps extends ISettingsProps { inHome: boolean; showKeymap?: boolean; showSerialButtons?: boolean; + showJacdacButton?: boolean; showFileList?: boolean; showFullscreenButton?: boolean; isMultiplayerGame?: boolean; @@ -43,6 +45,7 @@ interface SidepanelProps extends ISettingsProps { setEditorOffset?: () => void; showMiniSim: (visible?: boolean) => void; openSerial: (isSim: boolean) => void; + openJacdac: () => void; handleHardwareDebugClick: () => void; handleFullscreenButtonClick: () => void; } @@ -145,6 +148,10 @@ export class Sidepanel extends data.Component { this.props.openSerial(false); } + protected handleJacdacClick = () => { + this.props.openJacdac(); + } + protected handleSimOverlayClick = () => { const { tutorialOptions, handleFullscreenButtonClick } = this.props; if (!tutorialOptions || pxt.BrowserUtils.useOldTutorialLayout()) { @@ -216,7 +223,7 @@ export class Sidepanel extends data.Component { } renderCore() { - const { parent, inHome, showKeymap, showSerialButtons, showFileList, showFullscreenButton, isMultiplayerGame, + const { parent, inHome, showKeymap, showSerialButtons, showJacdacButton, showFileList, showFullscreenButton, isMultiplayerGame, collapseEditorTools, simSerialActive, deviceSerialActive, tutorialOptions, handleHardwareDebugClick, onTutorialStepChange, onTutorialComplete } = this.props; @@ -319,6 +326,14 @@ export class Sidepanel extends data.Component {
{showHostMultiplayerGameButton &&
+ {showJacdacButton &&
+
+
+ +
+ {lf("Show Jacdac Simulators/Devices")} +
+
} {showSerialButtons &&
From c865e1173bf56902f7b123e5ba3073cd2162f909 Mon Sep 17 00:00:00 2001 From: Thomas Ball Date: Mon, 24 Aug 2026 17:53:45 -0700 Subject: [PATCH 9/9] add jacdac simulator integration and styling --- pxtsim/simdriver.ts | 54 +++++++++++++++++++++++++++++++++++++ theme/jacdac.less | 29 ++++++++++++++++++++ theme/pxt.less | 1 + theme/tutorial-sidebar.less | 17 +++++++++++- webapp/src/jacdac.tsx | 24 +++++++++++++++++ webapp/src/sidepanel.tsx | 49 +++++++++++++++++++++------------ 6 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 theme/jacdac.less diff --git a/pxtsim/simdriver.ts b/pxtsim/simdriver.ts index a3eb8e88787d..29ef76ffa784 100644 --- a/pxtsim/simdriver.ts +++ b/pxtsim/simdriver.ts @@ -718,6 +718,60 @@ namespace pxsim { && this.loanedSimulator.querySelector("iframe"); } + // the jacdac simulator frame, visually relocated on top of another element + private jacdacOverlayWrapper: HTMLElement; + private jacdacOverlayTarget: HTMLElement; + private jacdacOverlayResizeObserver: ResizeObserver; + private jacdacOverlayReposition = () => { + if (!this.jacdacOverlayWrapper || !this.jacdacOverlayTarget) return; + const rect = this.jacdacOverlayTarget.getBoundingClientRect(); + const style = this.jacdacOverlayWrapper.style; + style.position = "fixed"; + style.top = `${rect.top}px`; + style.left = `${rect.left}px`; + style.width = `${rect.width}px`; + style.height = `${rect.height}px`; + style.zIndex = "1000"; + } + + // visually relocates the jacdac simulator frame on top of `target`, without + // reparenting its iframe: reparenting an iframe forces the browser to tear down + // and reload its nested browsing context, which wipes out all the running + // jacdac module simulators inside it + public showJacdacSimulator(target: HTMLElement): boolean { + this.hideJacdacSimulator(); + + const wrapper = pxsim.util.toArray(this.container.children) + .find(el => (el.querySelector("iframe") as HTMLIFrameElement)?.dataset[FRAME_DATA_MESSAGE_CHANNEL] === "jacdac/pxt-jacdac") as HTMLElement; + if (!wrapper) return false; + + this.jacdacOverlayWrapper = wrapper; + this.jacdacOverlayTarget = target; + this.jacdacOverlayReposition(); + + if (typeof ResizeObserver !== "undefined") { + this.jacdacOverlayResizeObserver = new ResizeObserver(this.jacdacOverlayReposition); + this.jacdacOverlayResizeObserver.observe(target); + } + window.addEventListener("resize", this.jacdacOverlayReposition); + window.addEventListener("scroll", this.jacdacOverlayReposition, true); + return true; + } + + public hideJacdacSimulator() { + if (!this.jacdacOverlayWrapper) return; + + const style = this.jacdacOverlayWrapper.style; + style.position = style.top = style.left = style.width = style.height = style.zIndex = ""; + + window.removeEventListener("resize", this.jacdacOverlayReposition); + window.removeEventListener("scroll", this.jacdacOverlayReposition, true); + this.jacdacOverlayResizeObserver?.disconnect(); + this.jacdacOverlayResizeObserver = undefined; + this.jacdacOverlayWrapper = undefined; + this.jacdacOverlayTarget = undefined; + } + private frameCleanupTimeout: any = undefined; private cancelFrameCleanup() { if (this.frameCleanupTimeout) { diff --git a/theme/jacdac.less b/theme/jacdac.less new file mode 100644 index 000000000000..41c338241ed0 --- /dev/null +++ b/theme/jacdac.less @@ -0,0 +1,29 @@ +/* Import all components */ +@import 'themes/default/globals/site.variables'; +@import 'themes/pxt/globals/site.variables'; + +/* Reference import */ +@import (reference) "semantic.less"; + +/*------------------- + Jacdac editor +--------------------*/ + +#jacdacEditor { + background-color: var(--pxt-target-background2); + color: var(--pxt-target-foreground2); +} + +#jacdacArea { + height: 90%; + padding: 1rem; + display: flex; + flex-direction: column; +} + +#jacdacSimulator { + // placeholder box: the actual jacdac sim iframe is positioned on top of + // this element (see SimulatorDriver.showJacdacSimulator), it is not a child of it + flex: 1; + min-height: 20rem; +} diff --git a/theme/pxt.less b/theme/pxt.less index 93bc940ae5ec..c0c92df5ba59 100644 --- a/theme/pxt.less +++ b/theme/pxt.less @@ -14,6 +14,7 @@ @import 'sidedoc-keyboard-nav-help'; @import 'home'; @import 'serial'; +@import 'jacdac'; @import 'docs'; @import 'debugger'; @import 'toolbox'; diff --git a/theme/tutorial-sidebar.less b/theme/tutorial-sidebar.less index ff88af8e4d66..f2943fd52a48 100644 --- a/theme/tutorial-sidebar.less +++ b/theme/tutorial-sidebar.less @@ -454,11 +454,18 @@ &.ui.items { margin-top: 0; } } +.jacdac-view-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin: 0.75rem 0.75rem 0.25rem 0.75rem; +} + .jacdac-view-toggle { display: inline-flex; align-self: flex-start; gap: 0.35rem; - margin: 0.75rem 0.75rem 0.25rem 0.75rem; + margin: 0; padding: 0.35rem; border: 1px solid var(--pxt-target-foreground0); border-radius: 0.75rem; @@ -484,6 +491,14 @@ } } +.jacdac-view-expand { + border: 1px solid var(--pxt-target-foreground0) !important; + border-radius: 0.5rem !important; + box-shadow: none !important; + background: var(--pxt-target-background1) !important; + color: var(--pxt-button-secondary-foreground) !important; +} + // Mini sim is visible when tab is hidden #root.tabTutorial { &:not(.fullscreensim) .simulator-container.hidden { diff --git a/webapp/src/jacdac.tsx b/webapp/src/jacdac.tsx index a2f610c82358..a524687bc836 100644 --- a/webapp/src/jacdac.tsx +++ b/webapp/src/jacdac.tsx @@ -2,12 +2,15 @@ import * as React from "react" import * as pkg from "./package" import * as sui from "./sui" import * as srceditor from "./srceditor" +import * as simulator from "./simulator" import { fireClickOnEnter } from "./util" import IProjectView = pxt.editor.IProjectView; export class Editor extends srceditor.Editor { + private simulatorContainerRef: HTMLDivElement; + constructor(public parent: IProjectView) { super(parent) this.goBack = this.goBack.bind(this); @@ -32,6 +35,25 @@ export class Editor extends srceditor.Editor { this.parent.openPreviousEditor() } + loadFileAsync(file: pkg.File, hc?: boolean): Promise { + // give the jacdac simulator a larger view; the board simulator(s) stay + // in the sidebar and keep running + if (this.simulatorContainerRef) + simulator.driver?.showJacdacSimulator(this.simulatorContainerRef); + return super.loadFileAsync(file, hc); + } + + unloadFileAsync(unloadToHome?: boolean): Promise { + simulator.driver?.hideJacdacSimulator(); + return super.unloadFileAsync(unloadToHome); + } + + private handleSimulatorRef = (el: HTMLDivElement) => { + this.simulatorContainerRef = el; + if (el) + simulator.driver?.showJacdacSimulator(el); + } + display() { return (
@@ -45,7 +67,9 @@ export class Editor extends srceditor.Editor {
+
) } } + diff --git a/webapp/src/sidepanel.tsx b/webapp/src/sidepanel.tsx index 52c820b5c8b3..3aa7a9d36edd 100644 --- a/webapp/src/sidepanel.tsx +++ b/webapp/src/sidepanel.tsx @@ -152,6 +152,12 @@ export class Sidepanel extends data.Component { this.props.openJacdac(); } + protected handleJacdacExpandClick = () => { + // loans the jacdac sim frame to the jacdac editor for a larger view; + // the microbit simulator stays put and keeps running + this.props.openJacdac(); + } + protected handleSimOverlayClick = () => { const { tutorialOptions, handleFullscreenButtonClick } = this.props; if (!tutorialOptions || pxt.BrowserUtils.useOldTutorialLayout()) { @@ -287,24 +293,33 @@ export class Sidepanel extends data.Component {
} } {hasSimulator &&
- {jacdacUI &&
-
}