fix(picow): make the device panel a draggable, non-modal floating window
The first iframe panel was a full-screen modal with a backdrop: it covered the canvas and blocked the editor, so you couldn't watch the board react or keep clicking buttons/wiring while it was open, and it couldn't be moved. Now it's a small floating panel docked top-right, draggable by its title bar, resizable, with NO backdrop — the canvas and editor stay fully interactive underneath. Also: the relay and async-led device pages now show a big colored ON/OFF indicator (these are web-server demos; the GP2/onboard LED isn't drawn on the canvas, so the panel is where you see the state flip).
This commit is contained in:
parent
80d2086907
commit
1478f7f543
|
|
@ -73,13 +73,21 @@ async def wifi_connect():
|
|||
led_on = False
|
||||
|
||||
def page():
|
||||
# The board's onboard LED isn't drawn on the canvas, so show the state
|
||||
# here; the buttons reload the page so you can see it flip.
|
||||
state = "ON" if led_on else "OFF"
|
||||
return ("<!DOCTYPE html><html><body><h2>Pico W LED: %s</h2>"
|
||||
"<button onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button> "
|
||||
"<button onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
|
||||
"</body></html>") % state
|
||||
# The board's onboard LED isn't drawn on the canvas, so show a big
|
||||
# state indicator here; the buttons reload so you can see it flip.
|
||||
color = "#22c55e" if led_on else "#666"
|
||||
label = "LED ON" if led_on else "LED OFF"
|
||||
return ("<!DOCTYPE html><html><head>"
|
||||
"<meta name=viewport content='width=device-width,initial-scale=1'></head>"
|
||||
"<body style='font-family:sans-serif;text-align:center;padding:24px;background:#fafafa'>"
|
||||
"<h2>Pico W Onboard LED</h2>"
|
||||
"<div style='margin:18px auto;width:170px;padding:26px 0;border-radius:14px;"
|
||||
"background:%s;color:#fff;font-size:24px;font-weight:bold'>%s</div>"
|
||||
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
|
||||
"onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button>"
|
||||
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
|
||||
"onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
|
||||
"</body></html>") % (color, label)
|
||||
|
||||
async def handle_client(reader, writer):
|
||||
global led_on
|
||||
|
|
@ -143,11 +151,19 @@ s.listen(1)
|
|||
print("Open browser: http://%s/" % ip)
|
||||
|
||||
def page():
|
||||
state = "ON" if relay_state == 0 else "OFF"
|
||||
return ("<html><body><h2>Pico W Relay: %s</h2>"
|
||||
"<button onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button> "
|
||||
"<button onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
|
||||
"</body></html>") % state
|
||||
on = (relay_state == 0)
|
||||
color = "#22c55e" if on else "#666"
|
||||
label = "RELAY ON" if on else "RELAY OFF"
|
||||
return ("<html><head><meta name=viewport content='width=device-width,initial-scale=1'></head>"
|
||||
"<body style='font-family:sans-serif;text-align:center;padding:24px;background:#fafafa'>"
|
||||
"<h2>Pico W Relay (GP2)</h2>"
|
||||
"<div style='margin:18px auto;width:170px;padding:26px 0;border-radius:14px;"
|
||||
"background:%s;color:#fff;font-size:24px;font-weight:bold'>%s</div>"
|
||||
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
|
||||
"onclick=\\"fetch('on').then(()=>location.reload())\\">ON</button>"
|
||||
"<button style='font-size:18px;padding:12px 28px;margin:6px' "
|
||||
"onclick=\\"fetch('off').then(()=>location.reload())\\">OFF</button>"
|
||||
"</body></html>") % (color, label)
|
||||
|
||||
# Wrap each client in try/except: a browser that drops the connection
|
||||
# mid-response would otherwise raise ECONNRESET and kill the server loop.
|
||||
|
|
|
|||
|
|
@ -1,83 +1,105 @@
|
|||
/**
|
||||
* openDeviceGateway
|
||||
*
|
||||
* Opens an emulated board's IoT-gateway page inside an in-app iframe panel
|
||||
* (same tab) instead of a new browser tab.
|
||||
* Opens an emulated board's IoT-gateway page in a small, FLOATING, draggable
|
||||
* panel docked to the top-right — NOT a modal overlay and NOT a new tab.
|
||||
*
|
||||
* Why: the Raspberry Pi Pico W emulation runs in THIS browser tab, driven by
|
||||
* requestAnimationFrame. Opening the gateway in a new tab backgrounds the
|
||||
* emulation tab, the browser pauses its rAF, the simulated chip freezes, and
|
||||
* the gateway can no longer reach the server running on it (the request times
|
||||
* out / returns 502). Rendering the served page in an iframe keeps the
|
||||
* emulation tab in the foreground, so the chip keeps running and answers.
|
||||
* Why not a new tab: the Raspberry Pi Pico W emulation runs in THIS browser
|
||||
* tab, driven by requestAnimationFrame. A new tab backgrounds the emulation
|
||||
* tab, the browser pauses its rAF, the simulated chip freezes, and the gateway
|
||||
* can no longer reach the server on it (request times out / 502). An in-tab
|
||||
* iframe keeps the emulation in the foreground so the chip keeps answering.
|
||||
*
|
||||
* (The ESP32 doesn't need this — its server runs in QEMU on the backend, which
|
||||
* is unaffected by browser tab visibility.)
|
||||
* Why not a modal: the panel must NOT block the canvas or the editor — the
|
||||
* user needs to watch the board react (LED/relay) and keep pressing buttons,
|
||||
* wiring components, editing code while the device page is open. So: no
|
||||
* backdrop, draggable by its title bar, resizable, parked out of the way.
|
||||
*
|
||||
* Plain DOM, no React state, so it can be invoked from anywhere (serial-monitor
|
||||
* link, canvas WiFi badge) without threading props.
|
||||
* (The ESP32 doesn't need any of this — its server runs in QEMU on the
|
||||
* backend, immune to tab visibility — so it still opens in a new tab.)
|
||||
*/
|
||||
|
||||
const OVERLAY_ID = 'velxio-device-gateway-overlay';
|
||||
const PANEL_ID = 'velxio-device-gateway-panel';
|
||||
|
||||
export function openDeviceGateway(url: string): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
document.getElementById(OVERLAY_ID)?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = OVERLAY_ID;
|
||||
overlay.style.cssText =
|
||||
'position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,0.55);' +
|
||||
'display:flex;align-items:center;justify-content:center;';
|
||||
document.getElementById(PANEL_ID)?.remove();
|
||||
|
||||
const panel = document.createElement('div');
|
||||
panel.id = PANEL_ID;
|
||||
panel.style.cssText =
|
||||
'background:#1e1e1e;border:1px solid #3a3a3a;border-radius:10px;' +
|
||||
'width:min(440px,94vw);height:min(640px,90vh);display:flex;' +
|
||||
'flex-direction:column;overflow:hidden;box-shadow:0 12px 48px rgba(0,0,0,0.5);';
|
||||
'position:fixed;top:64px;right:20px;z-index:100000;' +
|
||||
'width:340px;height:500px;display:flex;flex-direction:column;' +
|
||||
'background:#1e1e1e;border:1px solid #444;border-radius:10px;overflow:hidden;' +
|
||||
'box-shadow:0 12px 48px rgba(0,0,0,0.55);resize:both;min-width:240px;min-height:220px;';
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.style.cssText =
|
||||
'display:flex;align-items:center;justify-content:space-between;gap:12px;' +
|
||||
'padding:8px 12px;background:#252526;color:#ddd;' +
|
||||
'font:13px -apple-system,BlinkMacSystemFont,sans-serif;border-bottom:1px solid #3a3a3a;';
|
||||
'flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;gap:10px;' +
|
||||
'padding:7px 10px;background:#2d2d2e;color:#ddd;cursor:move;user-select:none;' +
|
||||
'font:12px -apple-system,BlinkMacSystemFont,sans-serif;border-bottom:1px solid #444;';
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.textContent = 'Device web page (IoT Gateway)';
|
||||
title.style.cssText = 'font-weight:600;';
|
||||
title.textContent = 'Device web page';
|
||||
title.style.cssText = 'font-weight:600;white-space:nowrap;';
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.style.cssText = 'display:flex;align-items:center;gap:14px;';
|
||||
right.style.cssText = 'display:flex;align-items:center;gap:12px;';
|
||||
|
||||
const reload = document.createElement('button');
|
||||
reload.type = 'button';
|
||||
reload.textContent = 'Reload';
|
||||
reload.style.cssText = 'background:none;border:none;color:#4fc3f7;cursor:pointer;font-size:13px;padding:0;';
|
||||
reload.style.cssText = 'background:none;border:none;color:#4fc3f7;cursor:pointer;font-size:12px;padding:0;';
|
||||
|
||||
const openTab = document.createElement('a');
|
||||
openTab.textContent = 'Open in tab';
|
||||
openTab.href = url;
|
||||
openTab.target = '_blank';
|
||||
openTab.rel = 'noreferrer';
|
||||
openTab.style.cssText = 'color:#4fc3f7;text-decoration:none;font-size:13px;';
|
||||
openTab.style.cssText = 'color:#4fc3f7;text-decoration:none;font-size:12px;';
|
||||
|
||||
const close = document.createElement('button');
|
||||
close.type = 'button';
|
||||
close.textContent = 'Close';
|
||||
close.style.cssText = 'background:#3a3a3a;border:none;color:#fff;cursor:pointer;font-size:13px;border-radius:5px;padding:4px 10px;';
|
||||
close.style.cssText = 'background:#444;border:none;color:#fff;cursor:pointer;font-size:12px;border-radius:4px;padding:3px 9px;';
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = url;
|
||||
iframe.style.cssText = 'flex:1;border:none;width:100%;background:#fff;';
|
||||
iframe.style.cssText = 'flex:1 1 auto;border:none;width:100%;background:#fff;';
|
||||
|
||||
reload.onclick = () => { iframe.src = url; };
|
||||
const dismiss = () => overlay.remove();
|
||||
close.onclick = dismiss;
|
||||
overlay.onclick = (e) => { if (e.target === overlay) dismiss(); };
|
||||
close.onclick = () => panel.remove();
|
||||
|
||||
// Drag by the title bar. Switch from right-anchored to left/top on grab so
|
||||
// the panel follows the cursor cleanly.
|
||||
bar.addEventListener('mousedown', (e) => {
|
||||
if (e.target !== bar && e.target !== title) return; // not on a button
|
||||
e.preventDefault();
|
||||
const rect = panel.getBoundingClientRect();
|
||||
const dx = e.clientX - rect.left;
|
||||
const dy = e.clientY - rect.top;
|
||||
panel.style.right = 'auto';
|
||||
panel.style.left = rect.left + 'px';
|
||||
panel.style.top = rect.top + 'px';
|
||||
// While dragging, don't let the iframe swallow mouse events.
|
||||
iframe.style.pointerEvents = 'none';
|
||||
const onMove = (m: MouseEvent) => {
|
||||
const maxX = window.innerWidth - 60;
|
||||
const maxY = window.innerHeight - 30;
|
||||
panel.style.left = Math.max(0, Math.min(maxX, m.clientX - dx)) + 'px';
|
||||
panel.style.top = Math.max(0, Math.min(maxY, m.clientY - dy)) + 'px';
|
||||
};
|
||||
const onUp = () => {
|
||||
iframe.style.pointerEvents = '';
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
|
||||
right.append(reload, openTab, close);
|
||||
bar.append(title, right);
|
||||
panel.append(bar, iframe);
|
||||
overlay.append(panel);
|
||||
document.body.append(overlay);
|
||||
document.body.append(panel);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue