From 15587a0d7685a0d4af588cd09da04bfc33c268f3 Mon Sep 17 00:00:00 2001 From: minjaesong Date: Tue, 26 May 2026 04:38:41 +0900 Subject: [PATCH] various mouse nav fixes, font rom update --- assets/disk0/tvdos/bin/taut.js | 35 +++++++++--- assets/disk0/tvdos/bin/zfm.js | 34 ++++++++++-- assets/disk0/tvdos/include/playgui.mjs | 50 ++++++++++++++++++ assets/disk0/tvdos/include/wintex.mjs | 21 ++++---- .../src/net/torvald/tsvm/rom/FontROM7x14.png | Bin 3704 -> 3300 bytes .../src/net/torvald/tsvm/VMEmuExecutable.kt | 36 ++++++++++++- 6 files changed, 153 insertions(+), 23 deletions(-) diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js index ffd0134..29e4c13 100644 --- a/assets/disk0/tvdos/bin/taut.js +++ b/assets/disk0/tvdos/bin/taut.js @@ -3703,13 +3703,12 @@ function openRetunePopup() { if (sel < 0) sel = 0 let scroll = centerScroll(sel, 0, listH, n) - // OK/Cancel button placement (bottom inside row) - const btnRow = py + ph - 2 - const labelOK = `[ OK ]`.length - const labelCan = `[ Cancel ]`.length - const totalW = labelOK + 2 + labelCan - const btnXOk = px + ((pw - totalW) >>> 1) - const btnXCan = btnXOk + labelOK + 2 + let done = false + let confirmed = false + const buttons = makePopupButtonRow(py + ph - 2, px, pw, [ + { label: 'OK', action: () => { confirmed = true; done = true }, default: true }, + { label: 'Cancel', action: () => { done = true } }, + ]) const repaint = () => { con.color_pair(230, colPopupBack) @@ -4089,8 +4088,18 @@ const MOUSE_POPUP_STACK = [] // Wrap push/pop so closing a popup also drops any onHoverLeave that would otherwise // be invoked against the popup's stale regions on the next mouse move. +// +// When the pop happens with a mouse button still held, the popup was almost certainly +// closed by a click. We arm `swallowResidualClick` so the trailing mouse_up (and any +// echo mouse_down from that same physical click) doesn't leak into the panel that the +// popup was covering. A keyboard close leaves no button held, so this is a no-op. +let swallowResidualClick = false function pushMousePopup(regions) { MOUSE_POPUP_STACK.push(regions); lastHoveredRegion = null } -function popMousePopup() { MOUSE_POPUP_STACK.pop(); lastHoveredRegion = null } +function popMousePopup() { + MOUSE_POPUP_STACK.pop() + lastHoveredRegion = null + if ((sys.peek(-37) & 0x07) !== 0) swallowResidualClick = true +} function pixelToCell(px, py) { return [(py / CELL_PH | 0) + 1, (px / CELL_PW | 0) + 1] // [cy, cx], 1-indexed @@ -4108,6 +4117,16 @@ function dispatchMouseEvent(event) { const t = event[0] if (t !== 'mouse_down' && t !== 'mouse_wheel' && t !== 'mouse_up' && t !== 'mouse_move') return false + // Eat residual events from the click that just closed a popup. The flag is armed + // by popMousePopup when a button was still held at pop time; it clears on the + // matching mouse_up so the next fresh press goes through normally. + if (swallowResidualClick && MOUSE_POPUP_STACK.length === 0) { + if (t === 'mouse_up') { swallowResidualClick = false; return true } + if (t === 'mouse_down') { return true } + if (t === 'mouse_move') { return true } + // mouse_wheel passes through — it's its own gesture, not part of the closing click + } + const [cy, cx] = pixelToCell(event[1], event[2]) const pool = (MOUSE_POPUP_STACK.length > 0) ? MOUSE_POPUP_STACK[MOUSE_POPUP_STACK.length - 1] diff --git a/assets/disk0/tvdos/bin/zfm.js b/assets/disk0/tvdos/bin/zfm.js index 0372e1e..f7feb37 100644 --- a/assets/disk0/tvdos/bin/zfm.js +++ b/assets/disk0/tvdos/bin/zfm.js @@ -737,6 +737,7 @@ function actActivate() { firstRunLatch = true con.curs_set(0); clearScr() refreshFilePanelCache(windowMode) + pendingPostExecDrain = true redraw() } } @@ -927,6 +928,7 @@ function actMore() { firstRunLatch = true con.curs_set(0); clearScr() refreshFilePanelCache(windowMode) + pendingPostExecDrain = true redraw() } } @@ -985,11 +987,17 @@ function setupPanelMouseRegions() { } }, onClick: (cy, cx, btn) => { - if (btn !== 1) return const target = scroll[windowMode] + rowIdx if (target >= dirFileList[windowMode].length) return - cursor[windowMode] = target - actActivate() + if (btn === 1) { + cursor[windowMode] = target + actActivate() + } + else if (btn === 2) { + cursor[windowMode] = target + drawFilePanel() + actMore() + } } }) } @@ -1026,11 +1034,19 @@ _redraw() // like fsh.js can hand off with the mouse button still held; without this, // input.withEvent's first call edge-detects that as a fresh mouse_down at the // cursor and activates whichever file row happens to sit there. -input.withEvent(() => {}) +// +// The same problem reappears after every child app returns, but draining +// inside the dispatcher callback is undone by TVDOS.SYS:1235 (input.withEvent +// unconditionally writes inputwork.oldMouse = its-stale-local-snapshot at the +// end of the outer call). So actActivate / actMore set pendingPostExecDrain +// and the main loop calls drainInheritedInput() AFTER input.withEvent returns. +function drainInheritedInput() { input.withEvent(() => {}) } +drainInheritedInput() let redrawRequested = false let exit = false let firstRunLatch = true +let pendingPostExecDrain = false while (!exit) { input.withEvent(event => { @@ -1066,6 +1082,16 @@ while (!exit) { _redraw() } }) + + // Re-baseline mouse state AFTER input.withEvent returns so its trailing + // `inputwork.oldMouse = mouse` (TVDOS.SYS:1235) doesn't overwrite the + // freshly-correct state with the stale snapshot taken at the start of the + // outer call. Without this, a child app exited by a click leaves zfm with + // oldBtns=0 while the user is still holding → spurious mouse_down next poll. + if (pendingPostExecDrain) { + pendingPostExecDrain = false + drainInheritedInput() + } } con.curs_set(1) diff --git a/assets/disk0/tvdos/include/playgui.mjs b/assets/disk0/tvdos/include/playgui.mjs index 1697dd9..7eeb7a8 100644 --- a/assets/disk0/tvdos/include/playgui.mjs +++ b/assets/disk0/tvdos/include/playgui.mjs @@ -385,6 +385,10 @@ const ag_workMid = new Float32Array(AG_WORK_N) const ag_workTmp = new Float32Array(AG_WORK_N >> 1) const ag_bandEnergy = new Float32Array(AG_N_BANDS) +// Sub-500 Hz residual — drops out of the wavelet modulator set on purpose, +// but we keep its RMS around to drive the bass mark. +let ag_bassEnergy = 0 + // Persistence buffer — float intensity per cell, plus the glyph last written // there. Decay shrinks intensity each frame; new beam samples overwrite the // glyph and bump intensity. @@ -397,6 +401,7 @@ const ag_cellFg = new Int16Array(AG_VIS_H * AG_VIS_W).fill(-1) const ag_waveGlyph = new Int16Array(AG_LANE_W * 3).fill(-1) const ag_stereoGlyph = new Int16Array(AG_LANE_W).fill(-1) const ag_stereoFg = new Int16Array(AG_LANE_W).fill(-1) +let ag_lastBassFg = -1 // Render rate-limiter — playmp2 spins ~32 Hz, playtad ~1 Hz, playwav ~100 Hz // at decode time. Clamp visual refresh to 20 Hz so each caller can spam @@ -579,6 +584,16 @@ function ag_analyseHaar() { ag_bandEnergy[lv] = rms > 1 ? 1 : rms len = half } + // Residual approximation in ag_workMid[0..len-1] holds the sub-500 Hz + // energy that the modulator pipeline intentionally discards. Reuse it + // to drive the bass mark. + let bassSumSq = 0 + for (let i = 0; i < len; i++) { + const v = ag_workMid[i] + bassSumSq += v * v + } + const bassRms = Math.sqrt(bassSumSq / len) * 1.8 + ag_bassEnergy = bassRms > 1 ? 1 : bassRms } // ── Wavescope (rows 3..5) ────────────────────────────────────────────────── @@ -652,6 +667,16 @@ const AG_XY_CY = AG_VIS_H >> 1 // centre row const AG_XY_SX = 18 // (L−R) → horizontal extent ±36 cells const AG_XY_SY = 9 // (L+R) → vertical extent ±18 cells +// Bass mark: 2×2 cell indicator pinned to the centre of the vectorscope so +// the bass "subwoofer" sits underneath the beam's pivot point. Half-blocks +// form a compact 16×16-pixel "dot" centred in the 16×32-pixel 2×2 area. +const AG_BASS_VIS_R0 = AG_XY_CY - 1 +const AG_BASS_VIS_C0 = AG_XY_CX - 1 +const AG_BASS_VIS_R1 = AG_BASS_VIS_R0 + 1 +const AG_BASS_VIS_C1 = AG_BASS_VIS_C0 + 1 +const AG_BASS_SCR_R = AG_ROW_VIS_TOP + AG_BASS_VIS_R0 +const AG_BASS_SCR_C = AG_COL_INSIDE_L + AG_BASS_VIS_C0 + // Glyphs. const AG_G_DOT = 0xFA // · const AG_G_BSL = 0x5C // \\ @@ -741,7 +766,10 @@ function ag_drawVisualiser() { for (let r = 0; r < AG_VIS_H; r++) { const rowOff = r * AG_VIS_W const screenY = AG_ROW_VIS_TOP + r + const inBassRow = (r === AG_BASS_VIS_R0 || r === AG_BASS_VIS_R1) for (let c = 0; c < AG_VIS_W; c++) { + // Bass mark owns its 2×2 cells — let ag_drawBassMark() paint them. + if (inBassRow && (c === AG_BASS_VIS_C0 || c === AG_BASS_VIS_C1)) continue const idx = rowOff + c const e = ag_persist[idx] let levelIdx = (e * 5) | 0 @@ -758,6 +786,25 @@ function ag_drawVisualiser() { } } +// ── Bass mark (rows 29-30, cols 2-3) ─────────────────────────────────────── +// Brightness-only indicator driven by the sub-500 Hz residual of the Haar +// pyramid. Uses indices 1..4 of the beam palette so the dot never falls all +// the way to background — a quiet track still shows a faint amber ember. + +function ag_drawBassMark() { + let idx = (ag_bassEnergy * 4) | 0 + if (idx > 3) idx = 3 + if (idx < 0) idx = 0 + const fg = AG_BEAM_PAL[idx + 1] + if (fg === ag_lastBassFg) return + ag_lastBassFg = fg + ag_color(fg, AG_COL_BG) + ag_mvprn(AG_BASS_SCR_R, AG_BASS_SCR_C, 0xDC) + ag_mvprn(AG_BASS_SCR_R, AG_BASS_SCR_C + 1, 0xDC) + ag_mvprn(AG_BASS_SCR_R + 1, AG_BASS_SCR_C, 0xDF) + ag_mvprn(AG_BASS_SCR_R + 1, AG_BASS_SCR_C + 1, 0xDF) +} + // ── Stereo energy bar (row 31) ───────────────────────────────────────────── // // Same idea as playtaud.drawStereo() but driven by raw PCM: for each sample, @@ -840,6 +887,8 @@ function audioInit(params) { ag_cellGlyph.fill(-1); ag_cellFg.fill(-1) ag_waveGlyph.fill(-1) ag_stereoGlyph.fill(-1); ag_stereoFg.fill(-1) + ag_bassEnergy = 0 + ag_lastBassFg = -1 con.curs_set(0) con.clear() @@ -861,6 +910,7 @@ function audioRender() { ag_updateXYScope() ag_drawWavescope() ag_drawVisualiser() + ag_drawBassMark() ag_drawStereo() } diff --git a/assets/disk0/tvdos/include/wintex.mjs b/assets/disk0/tvdos/include/wintex.mjs index 8103bdd..f7103ec 100644 --- a/assets/disk0/tvdos/include/wintex.mjs +++ b/assets/disk0/tvdos/include/wintex.mjs @@ -240,7 +240,7 @@ function showDialog(opts) { const c = opts.colours || {} const fg = (c.fg != null) ? c.fg : 254 - const bg = (c.bg != null) ? c.bg : 242 + const bg = (c.bg != null) ? c.bg : 243 const fieldBg = (c.fieldBg != null) ? c.fieldBg : 240 const dimFg = (c.dimFg != null) ? c.dimFg : 249 const hlFg = (c.hlFg != null) ? c.hlFg : 230 @@ -323,25 +323,26 @@ function showDialog(opts) { print(f.label + ':') // Top border - con.color_pair(frameFg, bg) + con.color_pair(fieldBg, bg) con.move(fbRow, fbCol) - print('\u00DA' + '\u00C4'.repeat(fw) + '\u00BF') + print('\u00EC' + '\u00A9'.repeat(fw) + '\u00ED') // Side borders + content - con.color_pair(frameFg, bg) con.move(fbRow + 1, fbCol) - print('\u00B3') + print('\u00AB') + con.color_pair(fg, fieldBg) const s = fieldScroll(cursors[i], fw) const vis = values[i].substring(s, s + fw) print(vis.padEnd(fw, ' ')) - con.color_pair(frameFg, bg) + + con.color_pair(fieldBg, bg) con.move(fbRow + 1, fbCol + fw + 1) - print('\u00B3') + print('\u00AA') // Bottom border con.move(fbRow + 2, fbCol) - print('\u00C0' + '\u00C4'.repeat(fw) + '\u00D9') + print('\u00F4' + '\u00AC'.repeat(fw) + '\u00F5') con.color_pair(fg, bg) } @@ -484,7 +485,7 @@ function showDialog(opts) { render() return } - if (ks === '') { + if (ks === '\x08') { const cur = cursors[focusIdx] if (cur > 0) { const v = values[focusIdx] @@ -494,7 +495,7 @@ function showDialog(opts) { } return } - if (ks === '' || ks === '') { + if (ks === '') { const cur = cursors[focusIdx] const v = values[focusIdx] if (cur < v.length) { diff --git a/tsvm_core/src/net/torvald/tsvm/rom/FontROM7x14.png b/tsvm_core/src/net/torvald/tsvm/rom/FontROM7x14.png index da8dc6463aebee4b5f0949ecae383d14418163bf..fcda676d729b3a9979b6bb39aa8d8d2a751fe44e 100644 GIT binary patch delta 3297 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 delta 3688 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 0) applyMouseInputMappingForPanel(vm, panelIndex) } private fun updateGame(delta: Float) { @@ -434,6 +464,10 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: this.panelsX = panelsX this.panelsY = panelsY resize(windowWidth * panelsX, windowHeight * panelsY) + // Panel positions shifted, so every VM needs its mouse origin re-mapped. + vms.forEachIndexed { index, info -> + info?.vm?.let { applyMouseInputMappingForPanel(it, index) } + } } override fun resize(width: Int, height: Int) {