Skip to content

Mobile improvements#48

Draft
ohuet wants to merge 17 commits intolqs:mainfrom
ohuet:mobile-improvements
Draft

Mobile improvements#48
ohuet wants to merge 17 commits intolqs:mainfrom
ohuet:mobile-improvements

Conversation

@ohuet
Copy link
Copy Markdown
Contributor

@ohuet ohuet commented May 6, 2026

Waiting for merge of #46

ohuet and others added 17 commits May 4, 2026 17:30
- src/lib/workbench.ts: pack/unpack IndexedDB stores + localStorage as a
  fflate zip; async API with worker, sync fallback when postMessage hits
  DataCloneError/OOM; emits phased progress (loading, reading, compressing,
  decompressing, restoring, finalizing).
- src/lib/download.ts: helpers for "Download" right-click action; raw blob
  for a single file, fflate zip with the same OOM fallback for folders or
  multi-selection.
- src/components/win2k/ProgressDialog.tsx: modal Win2k progress window
  reused by every long-running operation.
- src/components/win2k/Progress.tsx: indeterminate (marquee) mode via CSS
  keyframe when percent is null.
- src/components/win2k/Taskbar.tsx:
   reorder Start menu (Welcome, GitHub, sep, settings, sep, workbench, sep,
   Shut Down)
   wire Export/Import Workbench callbacks.
- src/components/App.tsx: handlers for workbench export/import (with
  FileReader byte-level progress) and right-click download; shared progress
  state and info/error dialog.
- src/components/Desktop.tsx, src/components/FolderWindow.tsx: new Download
  / Download as .zip context menu entry; onDownload prop drilled from App.
- src/lib/ui-strings.ts:
   new keys for workbench export/import, progress phases, and downloads
   rename "Reset to Default" label to "Reset Workbench" in all 10 languages.
- package.json, package-lock.json: add fflate dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A 261 MB .workbench file was driving Firefox past 6 GB of RAM during
import. The previous flow buffered the full compressed bundle, paid a
worker→main-thread structured-clone of the entire decompressed archive
(Uint8Arrays nested inside an object are not transferable), and then put
every entry into a single IndexedDB transaction whose pending-write queue
held yet another structured-clone copy until commit.

Switch import to fflate's streaming Unzip, fed 1 MB at a time via
File.slice().arrayBuffer() β€” Blob.stream() can preload the entire blob in
some Firefox versions, slicing reads only the requested range from disk.
Each entry's destination buffer is pre-allocated from originalSize so we
skip the chunks[]+combined concatenation peak. Writes are batched to a
4 MB IDB transaction soft cap (a single oversized entry triggers an
immediate flush) with a setTimeout(0) yield after each commit so GC can
collect the just-closed transaction's clones before the next batch lands.
Skip a voice in renderSamples when wave AND vol each have any
CTRL_DISABLED bit set, instead of requiring the same disabled bit on
both sides. The bitwise form missed the (wave=RESET, vol=STOPPED)
case that arises mid-song when the player ends a sample and stops
volume separately, leaving getSample reading a stuck wave.pos and
leaking the last sample byte into the mix.

Auto-increment dram_addr on each DRAM read/write through port 0x107
to match real GF1 hardware and DOSBox; without it REP OUTSB / REP
INSB to 0x107 funnel every byte through a single offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/components/win2k/Window.tsx:
   new capBtnText helper for text-labelled caption buttons (sunken when active)
   onZoomToggle/zoomActive props render a 2x button before min/max/close
- src/components/ConsoleView.tsx:
   new zoom prop multiplies the 640x480 container, both canvases, and the
   <pre> transform; mouse mapping unchanged (uses rect.width/rect.height);
   pixelated rendering added to the text canvas
- src/components/EmulatorView.tsx:
   consoleZoom state (1 or 2), reset on EXE restart; propagated to Window
   clientW/clientH and to ConsoleView; onZoomToggle wired only when isConsole

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/components/win2k/Window.tsx:
   new svgFullscreen icon (four corner brackets)
   onFullscreenToggle prop renders the button after the 2Γ— button
- src/components/EmulatorView.tsx:
   handleFullscreenToggle uses the standard Fullscreen API on a wrapper
   ref around ConsoleView; fullscreenchange listener syncs isFullscreen
   so the browser ESC exit is reflected in state
   resize listener (active only in fullscreen) recomputes fsScale =
   min(vw/baseW, vh/baseH) to fit-aspect-ratio 4:3 with black bars
   ConsoleView is wrapped in a flex-centered 100vwΓ—100vh container with
   an inner transform: scale(fsScale) scaler; both divs have no style
   when not fullscreen, so windowed behaviour is unchanged
   button wired only when isConsole; mouse handler already adapts via
   getBoundingClientRect post-transform

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/components/win2k/Window.tsx:
   capBtnText reserves 1px of bottom-right padding when not sunken to
   compensate for Tahoma's glyph offset, so "2Γ—" sits visually centered
   svgFullscreen background position moved from "top 2px left 3px" to
   "top 1px left 2px" β€” the 9Γ—9 icon in a 14Γ—16 content area centers at
   (1.5, 2.5), so 1/2 biases up-left while 2/3 biased down-right

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/lib/emu/dos/mouse.ts:
    add textCursorType/textScreenMask/textCursorMask/textCursorStart/textCursorEnd to DosMouseState
    initialize defaults (screenMask=0x77FF, cursorMask=0x7700) in createDosMouseState and AX=00h reset
    implement INT 33h AX=0Ah (BX=0 software masks, BX=1 hardware scanlines stored)
    add getTextModeCursorOverride helper applying (orig AND screenMask) XOR cursorMask
- src/components/ConsoleView.tsx:
    apply mouse cell override in drawTextModeBitmap, drawTextMode and DOM rendering loop
    re-render on mouse move in text mode so the cursor follows the pointer
    hide browser cursor (cursor: none) on text canvas and pre when DOS text mouse is visible
- src/lib/emu/dos/mouse.ts:
    add getDisplayedTextCursor helper combining BIOS cursor and hardware mouse cursor
    in hardware mode the unique CRTC text cursor follows the mouse cell with the
    program's chosen scanlines (BIOS cursor suppressed while mouse is visible)
- src/components/ConsoleView.tsx:
    drawTextModeBitmap, drawTextMode and DOM rendering now read the cursor through
    getDisplayedTextCursor so hardware mouse mode replaces the application cursor
    clamp cursor scanlines to charH-1 so 80x25 cursor settings (start/end up to 15)
    do not render past the cell in 80x50 mode (charH=8)
    hide browser cursor in both software and hardware text mouse modes
- src/lib/emu/dos/vga.ts:
    add fontRAM Uint8Array (8 banks x 256 chars x 32 bytes = full plane 2 layout)
    loadRomFontIntoBank copies the ROM 8x8 or 8x16 font into a chosen bank,
    zeroing the unused tail so a previously taller font does not bleed through
    getCharMapBanks decodes Sequencer reg 0x03 into mapA/mapB indices and a
    fontSwitchActive flag (true when mapA != mapB - the 512-character mode
    where attribute bit 3 picks the bank instead of fg intensity)
    constructor now seeds bank 0 with the 8x16 ROM
- src/lib/emu/dos/video.ts:
    INT 10h AX=1100h/1110h: copy CX chars x BH bytes from ES:BP into
    fontRAM[BL*8192 + DX*32 ..]; AX=1110h also recalculates char height
    AX=1101h/1102h/1104h: load matching ROM font into bank BL without recalc
    AX=1103h: write BL into Sequencer Character Map Select (seqRegs[3])
    AX=1120h/1121h: store user font pointer into INT 1Fh / INT 43h vector
    AX=1122h/1123h: load matching ROM font into bank 0 + update INT 43h vector
    AX=1111h/1112h/1114h still switch screenRows + charHeight as before; they
    now also seed bank 0 from the matching ROM
    setVideoMode reseeds bank 0 with the new mode's ROM font on each switch
    drawCharGraphics resolves the bank via getCharMapBanks and reads from
    fontRAM[bank*8192 + ch*32]
- src/components/ConsoleView.tsx:
    drawTextModeBitmap reads from fontRAM with bank resolved per cell;
    in 512-character mode foreground is restricted to 8 colors as on real VGA
    drop unused VGA_FONT_8X8_ROM / VGA_FONT_8X16_ROM imports
- src/lib/dos-settings.ts:
    textRenderer default flipped from 'dom' to 'canvas' so DOS programs that
    redefine glyphs via INT 10h AX=11xx render correctly out of the box;
    DOM mode (browser CSS font, allows text selection) remains available in
    settings for users who prefer it
- src/components/win2k/Window.tsx:
    new sysMenuPos state, opens a MenuDropdown anchored at the icon's
    bottom-left when the title-bar icon is single-clicked
    double-click on the icon closes the window (DOUBLE_CLICK_MS = 400)
    menu items: Restore / Move / Size / Minimize / Maximize / Close (Alt+F4)
    plus Zoom 2Γ— and Full Screen entries (with checked state) when the
    matching toggle callbacks are provided
    items are grayed per WS_THICKFRAME / WS_MINIMIZEBOX / WS_MAXIMIZEBOX and
    the current min/max state, mirroring real Windows behaviour
    new onSystemMove / onSystemSize / fullscreenActive props
- src/components/EmulatorView.tsx:
    handleSystemMove drives windowPos from the pointer until next click;
    Escape cancels back to the original position
    handleSystemSize drives canvasSize from the pointer in the same way and
    applies the result to the emulator on commit
    pass fullscreenActive=isFullscreen so the system-menu Full Screen entry
    reflects the current state with a check mark
- src/lib/ui-strings.ts:
    add sysMaximize / sysMove / sysSize / sysZoom2x / sysFullscreen to the
    UiStrings interface
    translate the new keys for the 10 supported locales (en / fr / de / es /
    ja / zh-CN / pt-BR / it / pl / ko)
The DOS canvas renderer kept stale canvas.width / canvas.height after a
resolution switch, so a 640x480 mode 12h framebuffer drawn into a canvas
left at 640x400 (from a previous 80x25 text frame) was vertically clipped
and stretched by CSS scaling, hiding the bottom 80 pixels in apps like
NeoPaint.

- src/lib/emu/dos/vga.ts:
    initFramebuffer detects when the new ImageData has different dimensions
    than the previous one and notifies via the new onFramebufferResize hook
- src/lib/emu/emulator.ts:
    new onVideoModeChange callback; initVgaModeXHook wires
    vga.onFramebufferResize to it
- src/components/ConsoleView.tsx:
    set onVideoModeChange to render() so the JSX re-applies width/height
    props to the graphics canvas DOM element on each resolution change
    add distinct key="gfx" / key="text" props on the two <canvas> branches
    so React unmounts and remounts when switching between text and graphics
    rendering paths instead of reusing the DOM node with stale dimensions
Text-mode rendering used to be stretched vertically by 1.2Γ— so 25 rows of
16-px glyphs filled the 480-px window, which made some horizontal strokes
end up two CSS pixels tall while others stayed at one (visible thicker
bottom on letters like "o"). The window is now sized to match the active
video mode: 480 in graphics, ROWS Γ— charH (= 400 for 80Γ—25 and 80Γ—50) in
text β€” every glyph row maps 1:1 to a CSS pixel.

- src/components/ConsoleView.tsx:
    dispH now derives from emu.isGraphicsMode, screenRows and charHeight
    new onScreenLayoutChange prop fires from emu.onVideoModeChange so the
    parent can resize the surrounding window in lock-step
- src/components/EmulatorView.tsx:
    consoleClientH passed to Window is computed from the live emu state
    (graphics mode β†’ 480, text β†’ ROWS Γ— charH); a layout-bump state forces
    the recompute when ConsoleView reports a screen-layout change
The scaleX measurement effect did not include useCanvas in its deps. Apps
launched while the canvas renderer was active never mounted the <pre>
element on first render, the effect bailed early on a null preRef, and
scaleX stayed at its initial value of 1; switching to the DOM renderer
afterwards rendered the <pre> with no horizontal compensation, so 80 cols
worth of natural font advance overflowed the 640-px target and stretched
horizontally.

- src/components/ConsoleView.tsx:
    add useCanvas to the scaleX useEffect deps and skip the measurement
    while the canvas renderer is active so the run that matters always
    fires once <pre> is in the DOM
The Win32 canvas renderer used to size its backing surface to
ceil(COLS Γ— naturalCharW) (~768 for Cascadia Mono at 16 px) and let CSS
downsample to 640. The non-integer 768β†’640 ratio dropped one pixel from
some columns, so vertical strokes on glyphs like '0' or '||' rendered
unevenly thin/thick.

The fix renders the canvas at exactly COLS Γ— 8 = 640 px and applies
ctx.scale(8/naturalCharW, 1) so the natural-width glyphs are uniformly
compressed into 8-px cells. The first attempt also shifted text up by
one row in cmd.exe: the new cw matched the JSX width={640} attribute
exactly, so the canvas-resize branch never fired and ctx.textBaseline
stayed at its default 'alphabetic' instead of 'top' β€” fillText then
drew glyphs above their intended y, hiding row 0 off-screen and putting
the cursor (drawn by fillRect, unaffected by textBaseline) one line
below the typed text.

- src/components/ConsoleView.tsx:
    drawTextMode now sets canvas.width = COLS Γ— 8 and applies
    ctx.scale(8/naturalCharW, 1) so the font is uniformly squished into
    each 8-px column with no browser downsampling artefact
    move ctx.font and ctx.textBaseline = 'top' assignments out of the
    canvas-resize branch so they are reapplied on every frame, even when
    the JSX attributes already match the target dimensions
Android virtual keyboards (Gboard, etc.) deliver typed characters via
beforeinput events with `e.data` instead of keydown β€” keydown arrives
with keyCode=229, an empty `e.code` and `e.key="Unidentified"`, so the
existing scancode lookup never matches and nothing reaches the emulator.
Hook a beforeinput handler that synthesises both make and break codes
(Android emits no keyup), and add an `e.key` fallback in keydown for the
sources that do emit a keydown but leave `e.code` blank (Bluetooth
keyboards). On desktop, preventDefault in keydown cancels the would-be
text insertion, so beforeinput never fires for physical frappes β€” no
behavioural change there.

- src/components/ConsoleView.tsx:
    add CHAR_TO_SCANCODE reverse table (US layout) for printable chars
    add KEY_TO_SCANCODE / KEY_IS_EXTENDED fallbacks for empty-code events
    skip preventDefault on keyCode=229 keydowns to avoid disturbing the
    IME composition pipeline
    new handleBeforeInput captures insertText / deleteContentBackward /
    insertLineBreak and emits scan + scan|0x80 in one go since Android
    sends no matching keyup
    wire onBeforeInput on the hidden console input plus
    autoCapitalize/autoComplete/autoCorrect="off" and spellcheck={false}
    so the IME does not transform the typed character
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant