tsvm: more mouse coord fix, taut: mouse support

This commit is contained in:
minjaesong
2026-05-24 19:01:31 +09:00
parent de82435f6e
commit 6d20d346f5
6 changed files with 679 additions and 74 deletions

View File

@@ -908,9 +908,13 @@ while (true) {
let navRight = edge(KEY_RIGHT) let navRight = edge(KEY_RIGHT)
// -- mouse -- // -- mouse --
// MMIO returns VM-screen pixel coords (origin at the top-left of the framebuffer).
// Widget xoff/yoff are passed straight into con.move(y, x), which is 1-indexed, so
// we offset by +1 here. Without this the click registers one cell up-and-left from
// where the user's pointer is, because pixel 0 = con.move(1, 1).
let pos = readMousePos() let pos = readMousePos()
let charX = (pos[0] / 7) | 0 let charX = (pos[0] / 7 | 0) + 1
let charY = (pos[1] / 14) | 0 let charY = (pos[1] / 14 | 0) + 1
let mouseMoved = (charX !== prevMouseCharX || charY !== prevMouseCharY) let mouseMoved = (charX !== prevMouseCharX || charY !== prevMouseCharY)
prevMouseCharX = charX prevMouseCharX = charX
prevMouseCharY = charY prevMouseCharY = charY

View File

@@ -1114,13 +1114,18 @@ inputwork.repeatCount = 0;
* where: * where:
* "key_down", <key symbol string>, <repeat count>, keycode0, keycode1 .. keycode7 * "key_down", <key symbol string>, <repeat count>, keycode0, keycode1 .. keycode7
* "key_change", <key symbol string (what went up)>, 0, keycode0, keycode1 .. keycode7 (remaining keys that are held down) * "key_change", <key symbol string (what went up)>, 0, keycode0, keycode1 .. keycode7 (remaining keys that are held down)
* "mouse_down", pos-x, pos-y, 1 // yes there's only one mouse button :p * "mouse_down", pos-x, pos-y, <button mask: 1=left, 2=right, 4=middle>, keycode0..keycode7
* "mouse_up", pos-x, pos-y, 0 * "mouse_up", pos-x, pos-y, <button mask of the released button>, keycode0..keycode7
* "mouse_move", pos-x, pos-y, <button down?>, oldpos-x, oldpos-y * "mouse_move", pos-x, pos-y, <currently-held button mask>, oldpos-x, oldpos-y, keycode0..keycode7
* "mouse_wheel", pos-x, pos-y, <-1 for wheel up, +1 for wheel down>, keycode0..keycode7
*
* Button mask values come from MMIO[36] bits 0..2 (terranmon.txt:52-58). The wheel
* bits (6, 7) latch in hardware and clear on read, so a one-shot detent fires once.
* Every mouse event carries the currently-held key buffer (same shape as key_down)
* so handlers can detect modifiers like Shift+wheel via `event.includes(<keysym>)`.
*/ */
input.withEvent = function(callback) { input.withEvent = function(callback) {
// TODO mouse event
function arrayEq(a,b) { function arrayEq(a,b) {
for (let i = 0; i < a.length; ++i) { for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false; if (a[i] !== b[i]) return false;
@@ -1141,7 +1146,33 @@ input.withEvent = function(callback) {
sys.poke(-40, 255); sys.poke(-40, 255);
let keys = [sys.peek(-41),sys.peek(-42),sys.peek(-43),sys.peek(-44),sys.peek(-45),sys.peek(-46),sys.peek(-47),sys.peek(-48)]; let keys = [sys.peek(-41),sys.peek(-42),sys.peek(-43),sys.peek(-44),sys.peek(-45),sys.peek(-46),sys.peek(-47),sys.peek(-48)];
let mouse = [sys.peek(-33) | (sys.peek(-34) << 8), sys.peek(-35) | (sys.peek(-36) << 8), sys.peek(-37)]; let mx = (sys.peek(-33) & 0xFF) | ((sys.peek(-34) & 0xFF) << 8);
let my = (sys.peek(-35) & 0xFF) | ((sys.peek(-36) & 0xFF) << 8);
let mb = sys.peek(-37) & 0xFF; // bits 0..2 = L/R/M held, bit 6 = wheel up, bit 7 = wheel down
let mouse = [mx, my, mb];
// --- mouse dispatch ---
let oldMouse = inputwork.oldMouse;
let hasOld = oldMouse && oldMouse.length === 3;
let oldBtns = hasOld ? (oldMouse[2] & 0x07) : 0;
let curBtns = mb & 0x07;
let wheelUp = (mb & 0x40) !== 0;
let wheelDn = (mb & 0x80) !== 0;
if (wheelUp) callback(["mouse_wheel", mx, my, -1].concat(keys));
if (wheelDn) callback(["mouse_wheel", mx, my, 1].concat(keys));
let pressed = curBtns & ~oldBtns;
let released = oldBtns & ~curBtns;
for (let b = 1; b <= 4; b <<= 1) {
if (pressed & b) callback(["mouse_down", mx, my, b].concat(keys));
if (released & b) callback(["mouse_up", mx, my, b].concat(keys));
}
if (hasOld && (mx !== oldMouse[0] || my !== oldMouse[1])) {
callback(["mouse_move", mx, my, curBtns, oldMouse[0], oldMouse[1]].concat(keys));
}
// --- end mouse dispatch ---
let keyChanged = !arrayEq(keys, inputwork.oldKeys) let keyChanged = !arrayEq(keys, inputwork.oldKeys)
let keyDiff = arrayDiff(keys, inputwork.oldKeys) let keyDiff = arrayDiff(keys, inputwork.oldKeys)

View File

@@ -3378,23 +3378,48 @@ function openHelpPopup() {
repaint() repaint()
let done = false let done = false
const buttons = makePopupButtonRow(HELP_POPUP_Y + HELP_POPUP_H - 1, HELP_POPUP_X, HELP_POPUP_W, [
{ label: 'OK', action: () => { done = true }, default: true },
])
buttons.repaint()
let eventJustReceived = true let eventJustReceived = true
pushMousePopup(buttons.regions.concat([
// Scroll body: wheel scrolls help text.
{ x: HELP_CONTENT_X, y: HELP_CONTENT_Y, w: HELP_CONTENT_W, h: HELP_CONTENT_H, onWheel: (cy, cx, dy) => {
scroll += dy * 3
if (scroll < 0) scroll = 0
if (scroll > maxScroll) scroll = maxScroll
repaint()
buttons.repaint()
}},
]))
const scrollAndRepaint = () => { repaint(); buttons.repaint() }
while (!done) { while (!done) {
input.withEvent(ev => { input.withEvent(ev => {
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
eventJustReceived = false; return
}
if (dispatchMouseEvent(ev)) return
if (ev[0] !== 'key_down') return if (ev[0] !== 'key_down') return
if (eventJustReceived) { eventJustReceived = false; return }
const ks = ev[1] const ks = ev[1]
const shiftDown = (ev.includes(59) || ev.includes(60))
if (ks === '<ESC>' || ks === '!' || ks === 'q' || ks === '\n') { done = true } if (buttons.keyHandler(ks, shiftDown)) return
else if (ks === '<UP>') { if (scroll > 0) { scroll -= 1; repaint() } } if (ks === '<ESC>' || ks === '!' || ks === 'q') { done = true }
else if (ks === '<DOWN>') { if (scroll < maxScroll) { scroll += 1; repaint() } } else if (ks === '<UP>') { if (scroll > 0) { scroll -= 1; scrollAndRepaint() } }
else if (ks === '<PAGE_UP>') { scroll = Math.max(0, scroll - HELP_CONTENT_H); repaint() } else if (ks === '<DOWN>') { if (scroll < maxScroll) { scroll += 1; scrollAndRepaint() } }
else if (ks === '<PAGE_DOWN>') { scroll = Math.min(maxScroll, scroll + HELP_CONTENT_H); repaint() } else if (ks === '<PAGE_UP>') { scroll = Math.max(0, scroll - HELP_CONTENT_H); scrollAndRepaint() }
else if (ks === '<HOME>') { scroll = 0; repaint() } else if (ks === '<PAGE_DOWN>') { scroll = Math.min(maxScroll, scroll + HELP_CONTENT_H); scrollAndRepaint() }
else if (ks === '<END>') { scroll = maxScroll; repaint() } else if (ks === '<HOME>') { scroll = 0; scrollAndRepaint() }
else if (ks === '<END>') { scroll = maxScroll; scrollAndRepaint() }
}) })
} }
popMousePopup()
drawAll() drawAll()
} }
@@ -3432,6 +3457,76 @@ const popupDrawFrame = (wo) => {
} }
} }
// Render a centred button "[ Label ]" at (y, x). State drives the colour scheme so
// the button can appear normal / keyboard-focused / mouse-hovered / both.
// state: 0 = normal, 1 = focused, 2 = hovered, 3 = focused + hovered
function drawPopupButton(y, x, label, state) {
const txt = `[ ${label} ]`
let fore, back
if (state === 1) { fore = colWHITE; back = colTabBarBack2 } // focused
else if (state === 2) { fore = colWHITE; back = colHighlight } // hovered
else if (state === 3) { fore = colBLACK; back = colWHITE } // focused + hovered
else { fore = 230; back = colPopupBack } // normal
con.color_pair(fore, back)
con.move(y, x)
print(txt)
con.color_pair(colStatus, 255)
return { x: x, y: y, w: txt.length, h: 1 }
}
// Build a row of OK/Cancel-style buttons centred under a popup. Each entry:
// { label, action() } (and an optional `default: true` to pre-focus)
// Returns:
// - `regions`: an array suitable for MOUSE_POPUP_STACK.push (handles hover + click)
// - `keyHandler(ks) -> bool`: feed key symbols here; returns true if it consumed Tab/Enter
// - `repaint()`: redraw all buttons with their current focus/hover state
// - `focus`, `hover`: getters/setters via methods (so popups can drive Esc → Cancel)
function makePopupButtonRow(y, popupX, popupW, defs) {
// Lay out buttons centred along row `y`. Label widths are tracked so we can compute hits.
const labels = defs.map(d => `[ ${d.label} ]`)
const totalW = labels.reduce((s, l) => s + l.length, 0) + 2 * (defs.length - 1)
const startX = popupX + ((popupW - totalW) >>> 1)
let cursor = startX
const buttons = defs.map((d, i) => {
const w = labels[i].length
const b = { x: cursor, y, w, label: d.label, action: d.action }
cursor += w + 2
return b
})
let focus = Math.max(0, defs.findIndex(d => d.default))
if (focus < 0) focus = 0
let hover = -1
const repaint = () => {
buttons.forEach((b, i) => {
const st = (i === focus ? 1 : 0) | (i === hover ? 2 : 0)
drawPopupButton(b.y, b.x, b.label, st)
})
}
const regions = buttons.map((b, i) => ({
x: b.x, y: b.y, w: b.w, h: b.h || 1,
onClick: (cy, cx, btn) => { if (btn === 1) b.action() },
onHover: () => { if (hover !== i) { hover = i; repaint() } },
onHoverLeave: () => { if (hover === i) { hover = -1; repaint() } },
}))
// Tab/Shift+Tab cycles focus; Enter activates. Returns true if the key was consumed.
const keyHandler = (ks, shiftDown) => {
if (ks === '\t' || ks === '<TAB>') {
focus = (focus + (shiftDown ? defs.length - 1 : 1)) % defs.length
repaint()
return true
}
if (ks === '\n') { buttons[focus].action(); return true }
return false
}
return { regions, keyHandler, repaint,
getFocus: () => focus, setFocus: (i) => { focus = i; repaint() },
activate: (i) => buttons[i].action() }
}
function drawGotoPopup(popup, buf) { function drawGotoPopup(popup, buf) {
con.color_pair(230, colPopupBack) con.color_pair(230, colPopupBack)
popup.drawFrame() popup.drawFrame()
@@ -3463,8 +3558,8 @@ function applyGoto(num) {
} }
function openConfirmQuit() { function openConfirmQuit() {
const pw = 25 + hasUnsavedChanges * 4 const pw = 28 + hasUnsavedChanges * 4
const ph = 5 + hasUnsavedChanges const ph = 6 + hasUnsavedChanges
const px = ((SCRW - pw) / 2 | 0) + 1 const px = ((SCRW - pw) / 2 | 0) + 1
const py = ((SCRH - ph) / 2 | 0) const py = ((SCRH - ph) / 2 | 0)
@@ -3477,9 +3572,7 @@ function openConfirmQuit() {
con.move(py + 2, px + 2) con.move(py + 2, px + 2)
con.color_pair(colWHITE, colPopupBack) con.color_pair(colWHITE, colPopupBack)
print('Exit Microtone? ') print('Exit Microtone?')
con.color_pair(230, 240)
print('[Y/N]')
if (hasUnsavedChanges) { if (hasUnsavedChanges) {
con.move(py + 3, px + 2) con.move(py + 3, px + 2)
@@ -3487,29 +3580,40 @@ function openConfirmQuit() {
print('You have unsaved changes.') print('You have unsaved changes.')
} }
con.color_pair(colStatus, 255) // reset colour
let result = false let result = false
let done = false let done = false
const buttons = makePopupButtonRow(py + ph - 2, px, pw, [
{ label: 'Yes', action: () => { result = true; done = true }, default: true },
{ label: 'No', action: () => { done = true } },
])
buttons.repaint()
pushMousePopup(buttons.regions)
let eventJustReceived = true let eventJustReceived = true
while (!done) { while (!done) {
input.withEvent(ev => { input.withEvent(ev => {
if (eventJustReceived && ev[0] === 'mouse_down') { eventJustReceived = false; return }
if (dispatchMouseEvent(ev)) return
if (ev[0] !== 'key_down') return if (ev[0] !== 'key_down') return
if (1 !== ev[2]) return if (1 !== ev[2]) return
const ks = ev[1] const ks = ev[1]
const shiftDown = (ev.includes(59) || ev.includes(60))
if (ks === 'y' || ks === 'Y' || ks === '\n') { result = true; done = true } if (buttons.keyHandler(ks, shiftDown)) return
if (ks === 'y' || ks === 'Y') { result = true; done = true }
else if (ks === 'n' || ks === 'N' || ks === '<ESC>') { done = true } else if (ks === 'n' || ks === 'N' || ks === '<ESC>') { done = true }
}) })
} }
popMousePopup()
if (!result) drawAll() if (!result) drawAll()
return result return result
} }
function openGotoPopup() { function openGotoPopup() {
const pw = GOTO_POPUP_W const pw = GOTO_POPUP_W
const ph = GOTO_POPUP_H const ph = GOTO_POPUP_H + 2
const px = ((SCRW - pw) / 2 | 0) + 1 const px = ((SCRW - pw) / 2 | 0) + 1
const py = ((SCRH - ph) / 2 | 0) const py = ((SCRH - ph) / 2 | 0)
@@ -3519,36 +3623,45 @@ function openGotoPopup() {
let buf = '' let buf = ''
let done = false let done = false
drawGotoPopup(popup, buf) let commit = false
const buttons = makePopupButtonRow(py + ph - 2, px, pw, [
{ label: 'OK', action: () => { commit = true; done = true }, default: true },
{ label: 'Cancel', action: () => { done = true } },
])
const repaintAll = () => { drawGotoPopup(popup, buf); buttons.repaint() }
repaintAll()
pushMousePopup(buttons.regions)
let eventJustReceived = true let eventJustReceived = true
while (!done) { while (!done) {
input.withEvent(ev => { input.withEvent(ev => {
if (ev[0] !== 'key_down') return if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
const ks = ev[1]
if (1 !== ev[2]) return // not key just hit
if (eventJustReceived) { // filter lingering input
eventJustReceived = false eventJustReceived = false
return return
} }
if (dispatchMouseEvent(ev)) return
if (ev[0] !== 'key_down') return
const ks = ev[1]
if (1 !== ev[2]) return // not key just hit
const shiftDown = (ev.includes(59) || ev.includes(60))
if (buttons.keyHandler(ks, shiftDown)) return
if (ks === '<ESC>' || ks === 'x') { if (ks === '<ESC>' || ks === 'x') {
done = true done = true
} else if (ks === '\n') {
if (buf.length > 0) applyGoto(parseInt(buf, 16))
done = true
} else if (ks === '\u0008') { } else if (ks === '\u0008') {
buf = buf.slice(0, -1) buf = buf.slice(0, -1)
drawGotoPopup(popup, buf) repaintAll()
} else if (ks.length === 1 && '0123456789abcdefABCDEF'.includes(ks) && buf.length < 3) { } else if (ks.length === 1 && '0123456789abcdefABCDEF'.includes(ks) && buf.length < 3) {
buf += ks.toUpperCase() buf += ks.toUpperCase()
drawGotoPopup(popup, buf) repaintAll()
} }
}) })
} }
popMousePopup()
if (commit && buf.length > 0) applyGoto(parseInt(buf, 16))
drawAll() drawAll()
} }
@@ -3575,7 +3688,7 @@ function openRetunePopup() {
const pw = 42 const pw = 42
const listH = Math.min(n, 15) const listH = Math.min(n, 15)
const ph = listH + 5 const ph = listH + 7
const px = ((SCRW - pw) / 2 | 0) const px = ((SCRW - pw) / 2 | 0)
const py = ((SCRH - ph) / 2 | 0) const py = ((SCRH - ph) / 2 | 0)
const listX = px + 2 const listX = px + 2
@@ -3590,6 +3703,14 @@ function openRetunePopup() {
if (sel < 0) sel = 0 if (sel < 0) sel = 0
let scroll = centerScroll(sel, 0, listH, n) 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
const repaint = () => { const repaint = () => {
con.color_pair(230, colPopupBack) con.color_pair(230, colPopupBack)
popup.drawFrame() popup.drawFrame()
@@ -3637,41 +3758,60 @@ function openRetunePopup() {
} }
} }
con.move(py + ph - 2, px + 2) con.move(py + ph - 3, px + 2)
con.color_pair(colVoiceHdr, colPopupBack) con.color_pair(colVoiceHdr, colPopupBack)
print(`\u008418u `) print(`\u008418u `)
con.color_pair(colStatus, colPopupBack) con.color_pair(colStatus, colPopupBack)
print(`Sel `) print(`Sel `)
con.color_pair(colVoiceHdr, colPopupBack) con.color_pair(colVoiceHdr, colPopupBack)
print(`ent `)
con.color_pair(colStatus, colPopupBack)
print(`OK `)
con.color_pair(colVoiceHdr, colPopupBack)
print(`m `) print(`m `)
con.color_pair(colStatus, colPopupBack) con.color_pair(colStatus, colPopupBack)
print(`Method `) print(`Method`)
con.color_pair(colVoiceHdr, colPopupBack)
print(`Q `) buttons.repaint()
con.color_pair(colStatus, colPopupBack)
print(`Cancel`)
con.color_pair(colStatus, 255) con.color_pair(colStatus, 255)
} }
repaint() repaint()
let done = false
let confirmed = false
let eventJustReceived = true let eventJustReceived = true
pushMousePopup(buttons.regions.concat([
// List rows: click to select, double-click semantics omitted (clarity over speed).
{ x: listX, y: listY, w: listW, h: listH, onClick: (cy, cx, btn) => {
if (btn !== 1) return
const r = cy - listY
const idx = scroll + r
if (idx < 0 || idx >= n) return
sel = idx; repaint()
}, onWheel: (cy, cx, dy) => {
sel += dy * 3
if (sel < 0) sel = 0
if (sel >= n) sel = n - 1
scroll = centerScroll(sel, scroll, listH, n)
repaint()
}},
// Method label clickable
{ x: px + 2, y: py + 2, w: listW, h: 1, onClick: (cy, cx, btn) => {
if (btn !== 1) return
method = methodCycle[(methodCycle.indexOf(method) + 1) % methodCycle.length]
repaint()
}},
]))
while (!done) { while (!done) {
input.withEvent(ev => { input.withEvent(ev => {
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
eventJustReceived = false; return
}
if (dispatchMouseEvent(ev)) return
if (ev[0] !== 'key_down') return if (ev[0] !== 'key_down') return
if (eventJustReceived) { eventJustReceived = false; return }
const ks = ev[1] const ks = ev[1]
const shiftDown = (ev.includes(59) || ev.includes(60))
if (ks === 'Q') { done = true } if (buttons.keyHandler(ks, shiftDown)) return
else if (ks === '\n') { confirmed = true; done = true } if (ks === 'Q' || ks === '<ESC>') { done = true }
else if (ks === 'M' || ks === 'm') { else if (ks === 'M' || ks === 'm') {
method = methodCycle[(methodCycle.indexOf(method) + 1) % methodCycle.length] method = methodCycle[(methodCycle.indexOf(method) + 1) % methodCycle.length]
repaint() repaint()
@@ -3692,6 +3832,8 @@ function openRetunePopup() {
}) })
} }
popMousePopup()
if (confirmed) { if (confirmed) {
const target = entries[sel] const target = entries[sel]
if (target && target.index !== PITCH_PRESET_IDX) { if (target && target.index !== PITCH_PRESET_IDX) {
@@ -3729,7 +3871,7 @@ function openFlagsPopup() {
let sel = 0 let sel = 0
const pw = 28 const pw = 28
const ph = items.length + 4 const ph = items.length + 6
const px = ((SCRW - pw) / 2 | 0) + 1 const px = ((SCRW - pw) / 2 | 0) + 1
const py = ((SCRH - ph) / 2 | 0) const py = ((SCRH - ph) / 2 | 0)
@@ -3737,6 +3879,13 @@ function openFlagsPopup() {
popup.isHighlighted = true popup.isHighlighted = true
popup.titleBack = colPopupBack popup.titleBack = colPopupBack
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 = () => { const repaint = () => {
con.color_pair(230, colPopupBack) con.color_pair(230, colPopupBack)
popup.drawFrame() popup.drawFrame()
@@ -3760,32 +3909,48 @@ function openFlagsPopup() {
} }
} }
con.move(py + ph - 2, px + 2) con.move(py + ph - 3, px + 2)
con.color_pair(colVoiceHdr, colPopupBack); print(`\u008418u `) con.color_pair(colVoiceHdr, colPopupBack); print(`\u008418u `)
con.color_pair(colStatus, colPopupBack); print('Sel ') con.color_pair(colStatus, colPopupBack); print('Sel ')
con.color_pair(colVoiceHdr, colPopupBack); print('sp ') con.color_pair(colVoiceHdr, colPopupBack); print('sp ')
con.color_pair(colStatus, colPopupBack); print('Tick ') con.color_pair(colStatus, colPopupBack); print('Tick')
con.color_pair(colVoiceHdr, colPopupBack); print('ent ')
con.color_pair(colStatus, colPopupBack); print('OK ') buttons.repaint()
con.color_pair(colVoiceHdr, colPopupBack); print('Q ')
con.color_pair(colStatus, colPopupBack); print('X')
con.color_pair(colStatus, 255) con.color_pair(colStatus, 255)
} }
repaint() repaint()
let done = false
let confirmed = false
let eventJustReceived = true let eventJustReceived = true
pushMousePopup(buttons.regions.concat([
// Clickable rows — each maps to a selectable index.
{ x: px + 2, y: py + 1, w: pw - 4, h: items.length, onClick: (cy, cx, btn) => {
if (btn !== 1) return
const i = cy - (py + 1)
const it = items[i]
if (!it || !it.kind) return
sel = selectables.indexOf(i)
if (sel < 0) sel = 0
if (it.kind === 'tone') toneMode = it.idx
else if (it.kind === 'intp') intpMode = it.idx
repaint()
}},
]))
while (!done) { while (!done) {
input.withEvent(ev => { input.withEvent(ev => {
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
eventJustReceived = false; return
}
if (dispatchMouseEvent(ev)) return
if (ev[0] !== 'key_down') return if (ev[0] !== 'key_down') return
const ks = ev[1] const ks = ev[1]
if (eventJustReceived) { eventJustReceived = false; return } const shiftDown = (ev.includes(59) || ev.includes(60))
if (buttons.keyHandler(ks, shiftDown)) return
if (ks === '<ESC>' || ks === 'q' || ks === 'Q') { done = true; return } if (ks === '<ESC>' || ks === 'q' || ks === 'Q') { done = true; return }
if (ks === '\n') { confirmed = true; done = true; return }
if (ks === '<UP>' && sel > 0) { sel--; repaint(); return } if (ks === '<UP>' && sel > 0) { sel--; repaint(); return }
if (ks === '<DOWN>' && sel < selectables.length-1) { sel++; repaint(); return } if (ks === '<DOWN>' && sel < selectables.length-1) { sel++; repaint(); return }
if (ks === ' ') { if (ks === ' ') {
@@ -3798,6 +3963,8 @@ function openFlagsPopup() {
}) })
} }
popMousePopup()
if (confirmed) { if (confirmed) {
const newFlags = (initialTrackerMixerflags & ~0x1F) | const newFlags = (initialTrackerMixerflags & ~0x1F) |
(toneMode & 3) | ((intpMode & 7) << 2) (toneMode & 3) | ((intpMode & 7) << 2)
@@ -3838,12 +4005,37 @@ function openInlineHexEdit(y, x, digits, initialValue) {
repaint() repaint()
let eventJustReceived = true let eventJustReceived = true
// Field spans " $XX " — onClick on a digit moves the cursor there.
// Outside-click commits (Enter); right-click cancels.
// Region order matters: dispatchMouseEvent searches in reverse, so the
// field region (registered last) is tested before the catch-all.
pushMousePopup([
{ x: 1, y: 1, w: SCRW, h: SCRH, onClick: (cy, cx, btn) => {
if (btn === 1) done = true
else if (btn === 2) { cancelled = true; done = true }
}},
{ x: x + 2, y: y, w: digits, h: 1, onClick: (cy, cx, btn) => {
if (btn === 1) { cur = cx - (x + 2); repaint() }
else if (btn === 2) { cancelled = true; done = true }
}, onWheel: (cy, cx, dy) => {
// Wheel adjusts the digit under the cursor.
const digit = parseInt(buf[cur], 16)
const next = (digit + (dy < 0 ? 1 : -1) + 16) & 0xF
buf = buf.substring(0, cur) + next.toString(16).toUpperCase() + buf.substring(cur + 1)
repaint()
}},
])
while (!done) { while (!done) {
input.withEvent(ev => { input.withEvent(ev => {
if (eventJustReceived && (ev[0] === 'key_down' || ev[0] === 'mouse_down')) {
eventJustReceived = false; return
}
if (dispatchMouseEvent(ev)) return
if (ev[0] !== 'key_down') return if (ev[0] !== 'key_down') return
if (1 !== ev[2]) return if (1 !== ev[2]) return
const ks = ev[1] const ks = ev[1]
if (eventJustReceived) { eventJustReceived = false; return }
if (ks === '<ESC>') { cancelled = true; done = true; return } if (ks === '<ESC>') { cancelled = true; done = true; return }
if (ks === '\n') { done = true; return } if (ks === '\n') { done = true; return }
@@ -3861,6 +4053,8 @@ function openInlineHexEdit(y, x, digits, initialValue) {
}) })
} }
popMousePopup()
return cancelled ? null : parseInt(buf, 16) return cancelled ? null : parseInt(buf, 16)
} }
@@ -3879,6 +4073,339 @@ function isExternalPanel(p) {
return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE
} }
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MOUSE INPUT
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Region registry. Coordinates are 1-indexed text cell positions. Each region:
// {x, y, w, h, onClick(cy, cx, btn, ev)?, onWheel(cy, cx, dy, ev)?, onRelease(...)?}
// MOUSE_GLOBAL — tabs + transport, live for the whole session.
// MOUSE_PANEL — per-panel viewport handlers, cleared whenever the panel changes.
// MOUSE_POPUP_STACK — popups push their own region set on open and pop on close;
// while non-empty, only the topmost set receives mouse events.
const MOUSE_GLOBAL = []
const MOUSE_PANEL = []
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.
function pushMousePopup(regions) { MOUSE_POPUP_STACK.push(regions); lastHoveredRegion = null }
function popMousePopup() { MOUSE_POPUP_STACK.pop(); lastHoveredRegion = null }
function pixelToCell(px, py) {
return [(py / CELL_PH | 0) + 1, (px / CELL_PW | 0) + 1] // [cy, cx], 1-indexed
}
function regionHits(r, cy, cx) {
return cy >= r.y && cy < r.y + r.h && cx >= r.x && cx < r.x + r.w
}
// Dispatch a mouse event to the topmost matching region. Returns true if handled.
// `mouse_move` also fires onHoverLeave for the previously-hovered region so popups can
// repaint un-hovered buttons without tracking that themselves.
let lastHoveredRegion = null
function dispatchMouseEvent(event) {
const t = event[0]
if (t !== 'mouse_down' && t !== 'mouse_wheel' && t !== 'mouse_up' && t !== 'mouse_move') return false
const [cy, cx] = pixelToCell(event[1], event[2])
const pool = (MOUSE_POPUP_STACK.length > 0)
? MOUSE_POPUP_STACK[MOUSE_POPUP_STACK.length - 1]
: MOUSE_PANEL.concat(MOUSE_GLOBAL)
if (t === 'mouse_move') {
let hit = null
for (let i = pool.length - 1; i >= 0; i--) {
const r = pool[i]
if (regionHits(r, cy, cx) && (r.onHover || r.onHoverLeave)) { hit = r; break }
}
if (hit !== lastHoveredRegion) {
if (lastHoveredRegion && lastHoveredRegion.onHoverLeave) lastHoveredRegion.onHoverLeave()
lastHoveredRegion = hit
}
if (hit && hit.onHover) { hit.onHover(cy, cx, event); return true }
return false
}
for (let i = pool.length - 1; i >= 0; i--) {
const r = pool[i]
if (!regionHits(r, cy, cx)) continue
if (t === 'mouse_down' && r.onClick) { r.onClick(cy, cx, event[3], event); return true }
if (t === 'mouse_wheel' && r.onWheel) { r.onWheel(cy, cx, event[3], event); return true }
if (t === 'mouse_up' && r.onRelease) { r.onRelease(cy, cx, event[3], event); return true }
}
return false
}
function clearPanelMouseRegions() { MOUSE_PANEL.length = 0 }
function addPanelMouseRegion(x, y, w, h, handlers) { MOUSE_PANEL.push(Object.assign({x, y, w, h}, handlers)) }
function addGlobalMouseRegion(x, y, w, h, handlers) { MOUSE_GLOBAL.push(Object.assign({x, y, w, h}, handlers)) }
// Apply the same panel-switch logic the Tab key path uses.
function switchToPanel(newPanel) {
if (newPanel === currentPanel) return
const wasTimeline = (currentPanel === VIEW_TIMELINE)
currentPanel = newPanel
applyMuteTransition(currentPanel)
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
if (isExternalPanel(currentPanel)) {
clearPanelMouseRegions()
con.clear(); drawAlwaysOnElems(); drawControlHint()
pendingExternalDraw = true
} else {
rebuildPanelMouseRegions()
drawAll()
}
}
// --- Tab bar regions (registered once; tab geometry is constant) ---
function registerTabRegions() {
let col = 2 // XOFF, mirrors drawTabBar
for (let i = 0; i < PANEL_NAMES.length; i++) {
const w = 1 + PANEL_NAMES[i].length + 1 // spcL + name + spcR
const tabIdx = i
addGlobalMouseRegion(col, 3, w, 1, {
onClick: (cy, cx, btn) => { if (btn === 1) switchToPanel(tabIdx) }
})
col += w + (i < PANEL_NAMES.length - 1 ? TAB_GAP : 0)
}
}
// --- Transport regions (rows 1-2 on the right edge) ---
// Order j: 0=stop, 1=playrow, 2=playcue, 3=playall — mirrors drawStatusBar's loop.
function registerTransportRegions() {
for (let j = 0; j < 4; j++) {
const glyphCol = SCRW - 5 * (j + 1) + 3
const idx = j
addGlobalMouseRegion(glyphCol - 1, 1, 3, 2, {
onClick: (cy, cx, btn) => {
if (btn !== 1) return
if (idx === 0) {
if (playbackMode !== PLAYMODE_NONE) { stopPlayback(); drawAlwaysOnElems(); redrawPanel() }
return
}
// The play handlers vary by panel — match the keyboard shortcut mapping.
if (currentPanel === VIEW_PATTERN_DETAILS) {
if (idx === 1) startPlayPatternRow()
else startPlayPattern()
drawPatternsContents(panelPatterns)
} else {
if (idx === 1) startPlayRow()
else if (idx === 2) startPlayCue()
else startPlaySong()
redrawPanel()
}
drawAlwaysOnElems()
}
})
}
}
// --- Per-panel viewport regions ---
function rebuildPanelMouseRegions() {
clearPanelMouseRegions()
if (currentPanel === VIEW_TIMELINE) registerTimelineMouse()
else if (currentPanel === VIEW_CUES) registerOrdersMouse()
else if (currentPanel === VIEW_PATTERN_DETAILS) registerPatternsMouse()
else if (currentPanel === VIEW_PROJECT) registerProjectMouse()
}
function registerTimelineMouse() {
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
onClick: (cy, cx, btn) => {
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
const viewRow = cy - PTNVIEW_OFFSET_Y
const targetRow = scrollRow + viewRow
if (targetRow < 0 || targetRow >= ROWS_PER_PAT) return
const oldCursor = cursorRow
const oldVoxOff = voiceOff
cursorRow = targetRow
const relCol = cx - PTNVIEW_OFFSET_X
if (relCol >= 0) {
const colSlot = (relCol / COLSIZE_TIMELINE_FULL) | 0
const targetVox = voiceOff + colSlot
if (targetVox >= 0 && targetVox < song.numVoices) {
cursorVox = targetVox
const fieldX = relCol - colSlot * COLSIZE_TIMELINE_FULL
let field = 0
for (let k = 0; k < TL_FIELD_OFFSETS.length; k++) if (fieldX >= TL_FIELD_OFFSETS[k]) field = k
timelineColCursor = field
}
}
clampCursor(); clampVoice()
if (voiceOff !== oldVoxOff || Math.abs(cursorRow - oldCursor) >= PTNVIEW_HEIGHT) drawAll()
else {
drawPatternView(); drawVoiceHeaders(); drawSeparators(separatorStyle)
drawAlwaysOnElems(); drawVoiceDetail()
}
},
onWheel: (cy, cx, dy) => {
if (playbackMode !== PLAYMODE_NONE) return
cursorRow += dy * 3
clampCursor()
drawPatternView(); drawSeparators(separatorStyle); drawAlwaysOnElems(); drawVoiceDetail()
}
})
}
function registerOrdersMouse() {
// Layout (1-indexed cells, mirrors drawOrdersRowAt):
// cols 1..3 = row number (no column meaning)
// col 4 = gap
// cols 5..10 = CMD (ordersColCursor = 0)
// col 11 = gap
// cols 12 + s*4 .. 12 + s*4 + 3 = voice slot s on screen
// (ordersColCursor = ordersVoiceOff + s + 1)
//
// Returns the ordersColCursor value for a given cx, or -1 if not on a column.
const colAtX = (cx) => {
if (cx >= ORDERS_CMD_X && cx < ORDERS_CMD_X + 6) return 0
if (cx >= ORDERS_VOICE_X) {
const slot = ((cx - ORDERS_VOICE_X) / ORDERS_VOICE_COL_W) | 0
if (slot < 0 || slot >= VOCSIZE_ORDERS) return -1
const v = ordersVoiceOff + slot
if (v >= song.numVoices) return -1
return v + 1
}
return -1
}
const hscrollBy = (dx) => {
const maxOff = Math.max(0, song.numVoices - VOCSIZE_ORDERS)
const next = Math.max(0, Math.min(maxOff, ordersVoiceOff + dx))
if (next === ordersVoiceOff) return false
ordersVoiceOff = next
return true
}
// Header row: click selects a column without touching the row; wheel scrolls
// voice columns horizontally (it's the natural place for column navigation).
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y - 1, SCRW, 1, {
onClick: (cy, cx, btn) => {
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
const col = colAtX(cx)
if (col < 0) return
ordersColCursor = col
clampOrdersHoriz(); redrawPanel(); drawAlwaysOnElems()
},
onWheel: (cy, cx, dy) => {
if (hscrollBy(dy * 3)) { redrawPanel(); drawAlwaysOnElems() }
},
})
// Content rows: click sets the row and (when on a column) the column too;
// wheel scrolls vertically; Shift+wheel scrolls horizontally.
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
onClick: (cy, cx, btn, ev) => {
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
const viewRow = cy - PTNVIEW_OFFSET_Y
const targetIdx = ordersScroll + viewRow
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
if (targetIdx < 0 || targetIdx > maxCue) return
ordersCursor = targetIdx
const col = colAtX(cx)
if (col >= 0) ordersColCursor = col
scrollOrdersTo(ordersCursor)
clampOrdersHoriz()
redrawPanel(); drawAlwaysOnElems()
},
onWheel: (cy, cx, dy, ev) => {
const shiftDown = (ev.includes(59) || ev.includes(60))
if (shiftDown) {
if (hscrollBy(dy * 3)) { redrawPanel(); drawAlwaysOnElems() }
return
}
const maxCue = song.lastActiveCue < 0 ? 0 : song.lastActiveCue
ordersCursor += dy * 3
if (ordersCursor < 0) ordersCursor = 0
if (ordersCursor > maxCue) ordersCursor = maxCue
scrollOrdersTo(ordersCursor)
redrawPanel(); drawAlwaysOnElems()
}
})
}
function registerPatternsMouse() {
// Left column: pattern list. cx in [PATEDITOR_LIST_X, PATEDITOR_SEP1_X)
addPanelMouseRegion(PATEDITOR_LIST_X, PTNVIEW_OFFSET_Y,
PATEDITOR_SEP1_X - PATEDITOR_LIST_X, PTNVIEW_HEIGHT, {
onClick: (cy, cx, btn) => {
if (btn !== 1 || song.numPats === 0 || playbackMode !== PLAYMODE_NONE) return
const targetIdx = patternListScroll + (cy - PTNVIEW_OFFSET_Y)
if (targetIdx < 0 || targetIdx >= song.numPats) return
patternIdx = targetIdx
clampPatternIdx(); simStateKey = ''
drawPatternsContents(panelPatterns)
},
onWheel: (cy, cx, dy) => {
if (song.numPats === 0) return
patternIdx += dy
clampPatternIdx(); simStateKey = ''
drawPatternsContents(panelPatterns)
}
})
// Middle grid: pattern editor cells. cx in [PATEDITOR_GRID_X, PATEDITOR_DETAIL_X)
addPanelMouseRegion(PATEDITOR_GRID_X, PTNVIEW_OFFSET_Y,
PATEDITOR_DETAIL_X - PATEDITOR_GRID_X, PTNVIEW_HEIGHT, {
onClick: (cy, cx, btn) => {
if (btn !== 1 || song.numPats === 0 || playbackMode !== PLAYMODE_NONE) return
const targetRow = patternGridScroll + (cy - PTNVIEW_OFFSET_Y)
if (targetRow < 0 || targetRow >= ROWS_PER_PAT) return
patternGridRow = targetRow
const cellRel = cx - PATEDITOR_CELL_X
const fieldOffsets = [0, 5, 8, 11, 14, 15]
let field = 0
for (let k = 0; k < fieldOffsets.length; k++) if (cellRel >= fieldOffsets[k]) field = k
if (field < 0) field = 0; if (field > 5) field = 5
patternGridCol = field
clampPatternGrid(); simStateKey = ''
drawPatternsContents(panelPatterns)
},
onWheel: (cy, cx, dy) => {
if (song.numPats === 0) return
patternGridRow += dy * 3
clampPatternGrid(); simStateKey = ''
drawPatternsContents(panelPatterns)
}
})
}
function registerProjectMouse() {
addPanelMouseRegion(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, {
onClick: (cy, cx, btn) => {
if (btn !== 1 || playbackMode !== PLAYMODE_NONE) return
// Meta rows occupy PTNVIEW_OFFSET_Y .. PTNVIEW_OFFSET_Y + PROJ_META_ROWS_COUNT - 1.
// The song list starts at PROJ_SONGLIST_Y + 1.
const metaRow = cy - PTNVIEW_OFFSET_Y
if (metaRow >= 0 && metaRow < PROJ_META_ROWS_COUNT) {
projectCursor = metaRow
clampProjectCursor(); redrawPanel()
return
}
const songRow = cy - (PROJ_SONGLIST_Y + 1)
if (songRow >= 0) {
const songIdx = projectSongScroll + songRow
if (songIdx >= 0 && songIdx < songsMeta.numSongs) {
projectCursor = PROJ_META_ROWS_COUNT + songIdx
clampProjectCursor(); redrawPanel()
}
}
},
onWheel: (cy, cx, dy) => {
const rowsVis = projectSongListRowsVisible()
const maxScroll = Math.max(0, songsMeta.numSongs - rowsVis)
projectSongScroll += dy * 3
if (projectSongScroll < 0) projectSongScroll = 0
if (projectSongScroll > maxScroll) projectSongScroll = maxScroll
redrawPanel()
}
})
}
registerTabRegions()
registerTransportRegions()
rebuildPanelMouseRegions()
// Launching a sub-program from inside an input.withEvent callback causes the triggering // Launching a sub-program from inside an input.withEvent callback causes the triggering
// Tab event to leak into the sub-program's own withEvent call (the event hasn't been // Tab event to leak into the sub-program's own withEvent call (the event hasn't been
// consumed yet when the callback is still executing). We avoid this by deferring the // consumed yet when the callback is still executing). We avoid this by deferring the
@@ -3888,6 +4415,7 @@ let pendingExternalDraw = false
while (!exitFlag) { while (!exitFlag) {
input.withEvent(event => { input.withEvent(event => {
if (dispatchMouseEvent(event)) return
if (event[0] !== "key_down") return if (event[0] !== "key_down") return
const keysym = event[1] const keysym = event[1]
const keyJustHit = (1 == event[2]) const keyJustHit = (1 == event[2])
@@ -3914,9 +4442,11 @@ while (!exitFlag) {
if (isExternalPanel(currentPanel)) { if (isExternalPanel(currentPanel)) {
// Redraw header now so the tab highlight is visible immediately, // Redraw header now so the tab highlight is visible immediately,
// but defer the actual sub-program launch to after withEvent returns. // but defer the actual sub-program launch to after withEvent returns.
clearPanelMouseRegions()
con.clear(); drawAlwaysOnElems(); drawControlHint() con.clear(); drawAlwaysOnElems(); drawControlHint()
pendingExternalDraw = true pendingExternalDraw = true
} else { } else {
rebuildPanelMouseRegions()
drawAll() drawAll()
} }
return return
@@ -3947,9 +4477,11 @@ while (!exitFlag) {
applyMuteTransition(currentPanel) applyMuteTransition(currentPanel)
if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters() if (wasTimeline && currentPanel !== VIEW_TIMELINE) clearVoiceMeters()
if (isExternalPanel(currentPanel)) { if (isExternalPanel(currentPanel)) {
clearPanelMouseRegions()
con.clear(); drawAlwaysOnElems(); drawControlHint() con.clear(); drawAlwaysOnElems(); drawControlHint()
redrawPanel() redrawPanel()
} else { } else {
rebuildPanelMouseRegions()
drawAll() drawAll()
} }
} }

View File

@@ -49,7 +49,13 @@ MMIO
0..31 RO: Raw Keyboard Buffer read. Won't shift the key buffer 0..31 RO: Raw Keyboard Buffer read. Won't shift the key buffer
32..33 RO: Mouse X pos 32..33 RO: Mouse X pos
34..35 RO: Mouse Y pos 34..35 RO: Mouse Y pos
36 RO: Mouse down? (1 for LEFT, 2 for RIGHT, 3 for BOTH) 36 RO: Mouse down?
bit 0: left
bit 1: right
bit 2: middle
bit 6: wheel up
bit 7: wheel down
37 RW: Read/Write single key input. Key buffer will be shifted. Manual writing is 37 RW: Read/Write single key input. Key buffer will be shifted. Manual writing is
usually unnecessary as such action must be automatically managed via LibGDX usually unnecessary as such action must be automatically managed via LibGDX
input processing. input processing.

View File

@@ -12,6 +12,7 @@ import net.torvald.tsvm.CircularArray
import net.torvald.tsvm.VM import net.torvald.tsvm.VM
import net.torvald.tsvm.isNonZero import net.torvald.tsvm.isNonZero
import net.torvald.tsvm.toInt import net.torvald.tsvm.toInt
import java.util.concurrent.atomic.AtomicInteger
import kotlin.experimental.and import kotlin.experimental.and
class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor { class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
@@ -32,6 +33,13 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
*/ */
var inputViewport: Viewport? = null var inputViewport: Viewport? = null
private val tmpMouseVec = Vector2() private val tmpMouseVec = Vector2()
// Letterbox offset and renderable area inside the inputViewport, set by the host VMGUI.
// After unproject, mouse pixel coords are shifted by (inputOriginX, inputOriginY) and
// clamped to (inputAreaW, inputAreaH) so apps see VM-screen pixel coords (0..drawWidth).
var inputOriginX: Int = 0
var inputOriginY: Int = 0
var inputAreaW: Int = Int.MAX_VALUE
var inputAreaH: Int = Int.MAX_VALUE
/** Accepts a keycode */ /** Accepts a keycode */
private val keyboardBuffer = CircularArray<Byte>(32, true) private val keyboardBuffer = CircularArray<Byte>(32, true)
@@ -108,7 +116,12 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
in 0..31 -> keyboardBuffer[(addr.toInt())] ?: -1 in 0..31 -> keyboardBuffer[(addr.toInt())] ?: -1
in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte() in 32..33 -> (mouseX.toInt() shr (adi - 32).times(8)).toByte()
in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte() in 34..35 -> (mouseY.toInt() shr (adi - 34).times(8)).toByte()
36L -> mouseButtons.toByte() // only bits 0..1 used; higher bits intentionally truncated 36L -> {
// bit 0: left, bit 1: right, bit 2: middle, bit 6: wheel up, bit 7: wheel down
// Wheel bits are latched on scrolled() and cleared on read so a one-shot
// detent fires exactly once for the polling app.
(mouseButtons or wheelLatch.getAndSet(0)).toByte()
}
37L -> { 37L -> {
val key = keyboardBuffer.removeTail() ?: -1 val key = keyboardBuffer.removeTail() ?: -1
keyPushed = !keyboardBuffer.isEmpty // Clear flag when buffer becomes empty keyPushed = !keyboardBuffer.isEmpty // Clear flag when buffer becomes empty
@@ -290,7 +303,9 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
private var mouseX: Short = 0 private var mouseX: Short = 0
private var mouseY: Short = 0 private var mouseY: Short = 0
private var mouseButtons: Int = 0 // bit 0 = LEFT, bit 1 = RIGHT private var mouseButtons: Int = 0 // bit 0 = LEFT, bit 1 = RIGHT, bit 2 = MIDDLE
// bits 6 (wheel up) and 7 (wheel down) — set by scrolled(), cleared on MMIO[36] read
private val wheelLatch = AtomicInteger(0)
private var systemUptime = 0L private var systemUptime = 0L
private var rtc = 0L private var rtc = 0L
@@ -310,18 +325,24 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
// VM sees logical framebuffer pixels regardless of window magnification, // VM sees logical framebuffer pixels regardless of window magnification,
// letterboxing or sub-region placement done by an embedding GDX app. // letterboxing or sub-region placement done by an embedding GDX app.
val vp = inputViewport val vp = inputViewport
val rawX: Int
val rawY: Int
if (vp != null) { if (vp != null) {
tmpMouseVec.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat()) tmpMouseVec.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
vp.unproject(tmpMouseVec) vp.unproject(tmpMouseVec)
mouseX = tmpMouseVec.x.toInt().toShort() rawX = tmpMouseVec.x.toInt()
mouseY = tmpMouseVec.y.toInt().toShort() rawY = tmpMouseVec.y.toInt()
} }
else { else {
mouseX = Gdx.input.x.toShort() rawX = Gdx.input.x
mouseY = Gdx.input.y.toShort() rawY = Gdx.input.y
} }
mouseButtons = (if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) 1 else 0) or // Subtract the letterbox origin so apps see VM-screen pixel coords (0..drawWidth).
(if (Gdx.input.isButtonPressed(Input.Buttons.RIGHT)) 2 else 0) mouseX = (rawX - inputOriginX).coerceIn(0, inputAreaW - 1).toShort()
mouseY = (rawY - inputOriginY).coerceIn(0, inputAreaH - 1).toShort()
mouseButtons = (if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) 1 else 0) or
(if (Gdx.input.isButtonPressed(Input.Buttons.RIGHT)) 2 else 0) or
(if (Gdx.input.isButtonPressed(Input.Buttons.MIDDLE)) 4 else 0)
// strobe keys to fill the key read buffer // strobe keys to fill the key read buffer
var keysPushed = 0 var keysPushed = 0
@@ -398,8 +419,15 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
} }
} }
override fun scrolled(p0: Float, p1: Float): Boolean { override fun scrolled(amountX: Float, amountY: Float): Boolean {
return false // LibGDX: amountY > 0 = scroll DOWN (toward user), amountY < 0 = scroll UP.
// Latch bits 6/7 of MMIO[36]; the latch is cleared the next time MMIO[36] is read.
if (Gdx.input.inputProcessor !== this) return false
when {
amountY < 0f -> wheelLatch.updateAndGet { it or 0x40 }
amountY > 0f -> wheelLatch.updateAndGet { it or 0x80 }
}
return true
} }
override fun keyUp(p0: Int): Boolean { override fun keyUp(p0: Int): Boolean {

View File

@@ -170,6 +170,10 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
Gdx.input.inputProcessor = vm.getIO() Gdx.input.inputProcessor = vm.getIO()
vm.getIO().inputViewport = inputViewport vm.getIO().inputViewport = inputViewport
vm.getIO().inputOriginX = (viewportWidth - loaderInfo.drawWidth) / 2
vm.getIO().inputOriginY = (viewportHeight - loaderInfo.drawHeight) / 2
vm.getIO().inputAreaW = loaderInfo.drawWidth
vm.getIO().inputAreaH = loaderInfo.drawHeight
if (usememvwr) memvwr = Memvwr(vm) if (usememvwr) memvwr = Memvwr(vm)