Compare commits

...

5 Commits

Author SHA1 Message Date
minjaesong
de82435f6e tsvm: mouse coord fix 2026-05-24 12:40:51 +09:00
minjaesong
054295fdab fsh: graphics mode bug fix 2026-05-24 12:27:55 +09:00
minjaesong
26303c63af more fshell 2026-05-24 09:50:21 +09:00
minjaesong
2ff471a066 docs: implementation plan for interactive fSh widgets
Bite-sized tasks for the spec at
docs/superpowers/specs/2026-05-24-fsh-interactive-widgets-design.md.
Verification uses node --check for JS syntax and a final manual smoke
test in the emulator; the TSVM cannot be machine-invoked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 02:02:58 +09:00
minjaesong
dfcc0c7729 docs: design spec for interactive fSh widgets
Spec for making com.fsh.todo_list and com.fsh.quick_access functional,
with state persisted to assets/disk0/home/config/fshrc. Includes an
IOSpace.kt change to expose right-click as MMIO[36] bit 1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 01:49:42 +09:00
5 changed files with 846 additions and 106 deletions

View File

@@ -1,5 +0,0 @@
[EXEC_FUNS]
nes,A:/home/tvnes/tvnes.js {0}
[COL_HL_EXT]
nes,156

View File

@@ -1,24 +1,414 @@
graphics.setBackground(2,1,3); graphics.setBackground(2,1,3)
graphics.resetPalette(); graphics.resetPalette()
const GL = require("gl")
const win = require("wintex")
const keysym = require("keysym")
function captureUserInput() { function captureUserInput() {
sys.poke(-40, 1); sys.poke(-40, 1)
} }
function getKeyPushed(keyOrder) { function getKeyPushed(keyOrder) {
return sys.peek(-41 - keyOrder); return sys.peek(-41 - keyOrder)
} }
let _fsh = {}; function readMousePos() {
_fsh.titlebarTex = new GL.Texture(2, 14, base64.atob("/u/+/v3+/f39/f39/f39/f39/P39/Pz8/Pv7+w==")); let lx = sys.peek(-33) & 0xFF
_fsh.scrdim = con.getmaxyx(); let hx = sys.peek(-34) & 0xFF
_fsh.scrwidth = _fsh.scrdim[1]; let ly = sys.peek(-35) & 0xFF
_fsh.scrheight = _fsh.scrdim[0]; let hy = sys.peek(-36) & 0xFF
_fsh.brandName = "f\xb3Sh"; return [(hx << 8) | lx, (hy << 8) | ly]
}
function readMouseButtons() {
return sys.peek(-37) & 0xFF
}
// Returns true if any of the eight key event buffer slots holds keycode `kc`.
function isKeyDown(kc) {
for (let i = 0; i < 8; i++) {
if ((sys.peek(-41 - i) & 0xFF) === kc) return true
}
return false
}
let _fsh = {}
// Config file path
_fsh.CONFIG_PATH = "A:/home/config/fshrc"
// Widget row caps (must match the loop bounds in draw())
_fsh.TODO_MAX_ROWS = 13 // todoWidget draws i = 0..12
_fsh.QA_MAX_ROWS = 22 // quickAccessWidget draws i = 0..21
_fsh.TODO_TEXT_WIDTH = 24 // visible characters per todo row
_fsh.QA_LABEL_WIDTH = 24 // visible characters per QA label
_fsh.QA_CMD_WIDTH = 60 // command path field width in dialog
// Highlight foreground for keyboard focus on widget lists. The background
// stays transparent (255) so the wallpaper continues to show through.
_fsh.HL_FG = 230
_fsh.HL_BG = 255
// Dialog colour pair. Background MUST be opaque (bg 255 is transparent
// in TSVM and lets the pixel-layer wallpaper bleed through dialog cells).
_fsh.DIALOG_FG = 254
_fsh.DIALOG_BG = 242
_fsh.FIELD_BG = 240
_fsh.DIALOG_DIM_FG = 249
// Default Quick Access entries when fshrc is missing or empty
_fsh.DEFAULT_QA = [
["Files", "/tvdos/bin/zsh.js"],
["Editor", "/tvdos/bin/edit.js"],
["BASIC", "/tbas/basic.js"],
["DOS Shell", "/tvdos/bin/command.js /fancy"]
]
// Mouse button bits (MMIO[36] layout per IOSpace.kt)
_fsh.MB_LEFT = 1
_fsh.MB_RIGHT = 2
// Current focus: null or {widgetId: string, index: number}.
// Index uses the same convention as hitTest: 0..length-1 are entries,
// `length` is the "+ Click to add" row.
_fsh.focus = null
// Parse fshrc text into {todos: [[text, done], ...], qa: [[label, cmd], ...]}.
// Returns null for both arrays when input is empty/whitespace.
_fsh.parseConfig = function(text) {
let todos = []
let qa = []
let section = null
if (!text) return {todos: todos, qa: qa}
let lines = text.split("\n")
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
// strip trailing \r if any
if (line.length && line.charCodeAt(line.length - 1) === 13) {
line = line.substring(0, line.length - 1)
}
if (line.length === 0) continue
if (line.charAt(0) === "[") {
let close = line.indexOf("]")
if (close > 0) {
let name = line.substring(1, close).trim().toUpperCase()
if (name === "TODO" || name === "QUICK_ACCESS") section = name
else section = null // unknown section: ignore until next header
}
continue
}
if (section === "TODO") {
if (line.length < 2) continue
let marker = line.charAt(0)
if ((marker === "+" || marker === "-") && line.charAt(1) === " ") {
todos.push([line.substring(2), marker === "+"])
}
} else if (section === "QUICK_ACCESS") {
let comma = line.indexOf(",")
if (comma <= 0) continue // need a non-empty label
let label = line.substring(0, comma)
let cmd = line.substring(comma + 1)
qa.push([label, cmd])
}
}
return {todos: todos, qa: qa}
}
// Build fshrc text from in-memory model. Inverse of parseConfig.
_fsh.serializeConfig = function(todos, qa) {
let out = "[TODO]\n"
for (let i = 0; i < todos.length; i++) {
let t = todos[i]
out += (t[1] ? "+ " : "- ") + t[0] + "\n"
}
out += "\n[QUICK_ACCESS]\n"
for (let i = 0; i < qa.length; i++) {
out += qa[i][0] + "," + qa[i][1] + "\n"
}
return out
}
// Read fshrc; populate todoWidget.todoList and quickAccessWidget.entries.
// Falls back to defaults on missing/empty/malformed file.
_fsh.loadConfig = function() {
let f = files.open(_fsh.CONFIG_PATH)
let parsed = {todos: [], qa: []}
if (f.exists) {
try {
parsed = _fsh.parseConfig(f.sread())
} catch (e) {
serial.printerr("fsh.loadConfig: parse failed: " + e)
parsed = {todos: [], qa: []}
}
}
todoWidget.todoList = parsed.todos
quickAccessWidget.entries = (parsed.qa.length > 0)
? parsed.qa
: _fsh.DEFAULT_QA.slice() // copy so saves don't mutate the constant
}
// Persist the current in-memory todos + QA entries to fshrc.
_fsh.saveConfig = function() {
try {
let f = files.open(_fsh.CONFIG_PATH)
if (!f.exists) f.mkFile()
f.swrite(_fsh.serializeConfig(todoWidget.todoList, quickAccessWidget.entries))
} catch (e) {
serial.printerr("fsh.saveConfig: write failed: " + e)
}
}
// Draw the bordered popup background. (row, col) is the top-left, (h, w)
// the size. Paints an opaque interior first (otherwise the wallpaper bleeds
// through cells with bg 255), then delegates frame drawing to wintex so the
// corner/edge glyphs always connect correctly.
_fsh.drawDialogFrame = function(row, col, h, w, title) {
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
for (let y = 0; y < h; y++) {
con.move(row + y, col)
print(' '.repeat(w))
}
let wo = new win.WindowObject(col, row, w, h, function(){}, function(){}, title)
wo.isHighlighted = true
wo.titleBack = _fsh.DIALOG_BG
wo.drawFrame()
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
}
// Slide the visible window so the caret stays inside (cursor at the
// rightmost column once it passes the field width).
_fsh.fieldScroll = function(cursor, width) {
return cursor < width ? 0 : cursor - width + 1
}
// Draw a single-line bordered input field at (row, col) with given width.
// content is the current text; cursor is the caret offset within content
// focused brightens the border colour.
_fsh.drawDialogField = function(row, col, width, content, cursor, focused) {
let frameFg = focused ? _fsh.DIALOG_FG : _fsh.DIALOG_DIM_FG
// Clear the field area (3 rows × width+2 cols) with FIELD_BG first so any
// stale chars from a previous render are wiped before we draw on top.
con.color_pair(_fsh.DIALOG_FG, _fsh.FIELD_BG)
con.move(row + 1, col + 1)
print(' '.repeat(width))
// Top border
con.color_pair(frameFg, _fsh.DIALOG_BG)
con.move(row, col)
print('\u00DA') // ┌
print('\u00C4'.repeat(width)) // ─
print('\u00BF') // ┐
// Vertical borders + content
con.move(row + 1, col)
print('\u00B3') // │
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
let scroll = _fsh.fieldScroll(cursor, width)
let visible = content.substring(scroll, scroll + width)
print(visible)
con.color_pair(frameFg, _fsh.DIALOG_BG)
con.move(row + 1, col + width + 1)
print('\u00B3') // │
// Bottom border
con.move(row + 2, col)
print('\u00C0') // └
print('\u00C4'.repeat(width)) // ─
print('\u00D9') // ┘
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
}
// Draw a button as "[ Label ]" at the given position; highlights when focused.
_fsh.drawDialogButton = function(row, col, label, focused) {
if (focused) con.color_pair(_fsh.HL_FG, _fsh.DIALOG_BG)
else con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
con.move(row, col)
print("[ " + label + " ]")
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
}
// Modal dialog. opts = {
// title: string,
// fields: [{label, initial, width}, ...],
// allowDelete: bool,
// }
// Returns {action: "ok"|"cancel"|"delete", values: [string, ...]}.
_fsh.showDialog = function(opts) {
let fields = opts.fields
let values = fields.map(function(f) { return f.initial || "" })
// Caret position per field. Start at end of any pre-filled initial text.
let cursors = values.map(function(v) { return v.length })
// Layout
let maxFieldW = fields.reduce(function(m, f) { return Math.max(m, f.width) }, 16)
let titleW = (opts.title ? opts.title.length : 0) + 4
let w = Math.max(maxFieldW + 6, titleW + 4, 24)
let buttonsRow = 2 + fields.length * 4 + 1 // 1 label + 3 field rows per field
let h = buttonsRow + 2
let screen = con.getmaxyx()
let row = Math.max(2, Math.floor((screen[0] - h) / 2))
let col = Math.max(2, Math.floor((screen[1] - w) / 2))
// Buttons list: indices follow Tab order after the last field
let buttons = [{label: "OK", action: "ok"}, {label: "Cancel", action: "cancel"}]
if (opts.allowDelete) buttons.splice(1, 0, {label: "Delete", action: "delete"})
let focusIdx = 0 // 0..fields.length-1 = field; then buttons
let totalFocus = fields.length + buttons.length
let done = null // {action, values} when set
function render() {
_fsh.drawDialogFrame(row, col, h, w, opts.title)
// Fields
for (let i = 0; i < fields.length; i++) {
let labelRow = row + 1 + i * 4
let fieldRow = labelRow + 1
con.color_pair(_fsh.DIALOG_FG, _fsh.DIALOG_BG)
con.move(labelRow, col + 2)
print(fields[i].label + ":")
_fsh.drawDialogField(fieldRow, col + 2, fields[i].width,
values[i], cursors[i], i === focusIdx)
}
// Buttons centred on buttonsRow
let totalBtnW = buttons.reduce(function(s, b) { return s + b.label.length + 5 }, 0) - 1
let bx = col + Math.floor((w - totalBtnW) / 2)
for (let i = 0; i < buttons.length; i++) {
let bIdx = fields.length + i
_fsh.drawDialogButton(row + buttonsRow, bx, buttons[i].label, bIdx === focusIdx)
bx += buttons[i].label.length + 5
}
// Position the visible caret. Inside a field: place it on the content
// row at the cursor offset (corrected for horizontal scroll). On a
// button: hide the caret entirely.
if (focusIdx < fields.length) {
let fldWidth = fields[focusIdx].width
let scroll = _fsh.fieldScroll(cursors[focusIdx], fldWidth)
let contentRow = row + 1 + focusIdx * 4 + 2
let contentCol = col + 2 + 1 + (cursors[focusIdx] - scroll)
con.move(contentRow, contentCol)
con.curs_set(1)
} else {
con.curs_set(0)
}
}
render()
// Note: con.getch() returns TSVM scancodes (defined in JS_INIT.js as
// con.KEY_UP=200, KEY_DOWN=208, KEY_LEFT=203, KEY_RIGHT=205,
// con.KEY_BACKSPACE=8, KEY_TAB=9, KEY_RETURN=10). Esc isn't in JS_INIT's
// map — it arrives as ASCII 27 via keyTyped().
while (done === null) {
let k = con.getch()
if (k === 27) { // Esc
done = {action: "cancel", values: values}
break
}
if (k === con.KEY_TAB) {
focusIdx = (focusIdx + 1) % totalFocus
render()
continue
}
// Up/Down always cycles focus across fields/buttons.
if (k === con.KEY_UP) {
focusIdx = (focusIdx - 1 + totalFocus) % totalFocus
render()
continue
}
if (k === con.KEY_DOWN) {
focusIdx = (focusIdx + 1) % totalFocus
render()
continue
}
// Left/Right moves the caret inside a field; on a button it cycles.
if (k === con.KEY_LEFT) {
if (focusIdx < fields.length) {
if (cursors[focusIdx] > 0) {
cursors[focusIdx] -= 1
render()
}
} else {
focusIdx = (focusIdx - 1 + totalFocus) % totalFocus
render()
}
continue
}
if (k === con.KEY_RIGHT) {
if (focusIdx < fields.length) {
if (cursors[focusIdx] < values[focusIdx].length) {
cursors[focusIdx] += 1
render()
}
} else {
focusIdx = (focusIdx + 1) % totalFocus
render()
}
continue
}
// On a field
if (focusIdx < fields.length) {
if (k === con.KEY_RETURN) {
if (focusIdx < fields.length - 1) {
focusIdx += 1
} else {
focusIdx = fields.length // move to OK button
}
render()
continue
}
if (k === con.KEY_BACKSPACE) {
let c = cursors[focusIdx]
if (c > 0) {
let v = values[focusIdx]
values[focusIdx] = v.substring(0, c - 1) + v.substring(c)
cursors[focusIdx] = c - 1
render()
}
continue
}
// Printable: insert at the caret.
if (k >= 32 && k < 256 && values[focusIdx].length < fields[focusIdx].width * 4) {
let v = values[focusIdx]
let c = cursors[focusIdx]
values[focusIdx] = v.substring(0, c) + String.fromCharCode(k) + v.substring(c)
cursors[focusIdx] = c + 1
render()
}
continue
}
// On a button
if (k === con.KEY_RETURN || k === 32) {
done = {action: buttons[focusIdx - fields.length].action, values: values}
break
}
}
con.curs_set(0)
return done
}
// Map (mouse char x, mouse char y) to a row index for a widget drawn at
// (xoff, yoff) with `length` existing entries and `maxRows` total rows.
// Returns null / {kind:"add"} / {kind:"item", index: i}.
_fsh.hitTestList = function(charX, charY, xoff, yoff, textWidth, length, maxRows) {
// Each row sits at (yoff + i + 2, xoff..xoff + textWidth + 1).
// Column range: icon at xoff, text at xoff+2 .. xoff+1+textWidth.
// Allow clicks anywhere on the row's char cells (icon + text region).
let relY = charY - yoff - 2
if (relY < 0 || relY >= maxRows) return null
if (charX < xoff || charX > xoff + 1 + textWidth) return null
if (relY < length) return {kind: "item", index: relY}
if (relY === length) return {kind: "add"}
return null
}
_fsh.titlebarTex = new GL.Texture(2, 14, base64.atob("/u/+/v3+/f39/f39/f39/f39/P39/Pz8/Pv7+w=="))
_fsh.scrdim = con.getmaxyx()
_fsh.scrwidth = _fsh.scrdim[1]
_fsh.scrheight = _fsh.scrdim[0]
_fsh.brandName = "f\xb3Sh"
_fsh.brandLogoTexSmall = new GL.Texture(24, 14, gzip.decomp(base64.atob( _fsh.brandLogoTexSmall = new GL.Texture(24, 14, gzip.decomp(base64.atob(
"H4sIAAAAAAAAAPv/Hy/4Qbz458+fIeILQQBIwoSh6qECuMVBukCmIJkDVQ+RQNgLE0MX/w+1lyhxqIUwTLJ/sQMAcIXsbVABAAA=" "H4sIAAAAAAAAAPv/Hy/4Qbz458+fIeILQQBIwoSh6qECuMVBukCmIJkDVQ+RQNgLE0MX/w+1lyhxqIUwTLJ/sQMAcIXsbVABAAA="
))); )))
_fsh.scrlayout = ["com.fsh.clock","com.fsh.calendar","com.fsh.todo_list", "com.fsh.quick_access"]; _fsh.scrlayout = ["com.fsh.clock","com.fsh.calendar","com.fsh.todo_list", "com.fsh.quick_access"]
_fsh.drawWallpaper = function() { _fsh.drawWallpaper = function() {
let wp = files.open("A:/home/wall.bytes") let wp = files.open("A:/home/wall.bytes")
@@ -28,85 +418,85 @@ _fsh.drawWallpaper = function() {
wp.pread(b, 250880, 0) wp.pread(b, 250880, 0)
dma.ramToFrame(b, 0, 250880) dma.ramToFrame(b, 0, 250880)
sys.free(b) sys.free(b)
}; }
_fsh.drawTitlebar = function(titletext) { _fsh.drawTitlebar = function(titletext) {
GL.drawTexPattern(_fsh.titlebarTex, 0, 0, 560, 14); GL.drawTexPattern(_fsh.titlebarTex, 0, 0, 560, 14)
if (titletext === undefined || titletext.length == 0) { if (titletext === undefined || titletext.length == 0) {
con.move(1,1); con.move(1,1)
print(" ".repeat(_fsh.scrwidth)); print(" ".repeat(_fsh.scrwidth))
GL.drawTexImageOver(_fsh.brandLogoTexSmall, 268, 0); GL.drawTexImageOver(_fsh.brandLogoTexSmall, 268, 0)
} }
else { else {
con.color_pair(240, 255); con.color_pair(240, 255)
GL.drawTexPattern(_fsh.titlebarTex, 268, 0, 24, 14); GL.drawTexPattern(_fsh.titlebarTex, 268, 0, 24, 14)
con.move(1, 1 + (_fsh.scrwidth - titletext.length) / 2); con.move(1, 1 + (_fsh.scrwidth - titletext.length) / 2)
print(titletext); print(titletext)
} }
con.color_pair(254, 255); con.color_pair(254, 255)
}; }
_fsh.Widget = function(id, w, h) { _fsh.Widget = function(id, w, h) {
this.identifier = id; this.identifier = id
this.width = w; this.width = w
this.height = h; this.height = h
if (!this.identifier) { if (!this.identifier) {
this.identifier = ""; this.identifier = ""
} }
//this.update = function() {}; //this.update = function() {}
/** /**
* Params charXoff and charYoff are ZERO-BASED! * Params charXoff and charYoff are ZERO-BASED!
*/ */
this.draw = function(charXoff, charYoff) {}; this.draw = function(charXoff, charYoff) {}
} }
_fsh.widgets = {} _fsh.widgets = {}
_fsh.registerNewWidget = function(widget) { _fsh.registerNewWidget = function(widget) {
_fsh.widgets[widget.identifier] = widget; _fsh.widgets[widget.identifier] = widget
} }
let clockWidget = new _fsh.Widget("com.fsh.clock", _fsh.scrwidth - 8, 7*2); let clockWidget = new _fsh.Widget("com.fsh.clock", _fsh.scrwidth - 8, 7*2)
clockWidget.numberSheet = new GL.SpriteSheet(19, 22, new GL.Texture(190, 22, gzip.decomp(base64.atob( clockWidget.numberSheet = new GL.SpriteSheet(19, 22, new GL.Texture(190, 22, gzip.decomp(base64.atob(
"H4sIAAAAAAAAAMWVW3LEMAgE739aHcFJJV5ZMD2I9ToVfcl4GBr80HF8r/FaR1ozMuIyoUu87lEXI0al5qVR5AebSwchSaNE6Nyo1Nw5HXF3SfPT4Bshl"+ "H4sIAAAAAAAAAMWVW3LEMAgE739aHcFJJV5ZMD2I9ToVfcl4GBr80HF8r/FaR1ozMuIyoUu87lEXI0al5qVR5AebSwchSaNE6Nyo1Nw5HXF3SfPT4Bshl"+
"EycA8RD96mLlHbuhTgOrfLnUDZspafbSQWk56WEGvQEtWaWwgb8iz7a8AOXhsraO/q9Qw2/GnXovfVN+q2wM/p/oddn2cjF239GX3y11+SWCtc6FTHC1v"+ "EycA8RD96mLlHbuhTgOrfLnUDZspafbSQWk56WEGvQEtWaWwgb8iz7a8AOXhsraO/q9Qw2/GnXovfVN+q2wM/p/oddn2cjF239GX3y11+SWCtc6FTHC1v"+
"TVPkDPWWn0w+DDz93UX9v9mF5KIsQ6OdN2KJoB4ui1bXXr0AMp0YfiQo//4XhpK8555dsNehAqVS5uhb5iHn3Kko769J59KmLBe/TSR7hcsd+hr+HnrwR"+ "TVPkDPWWn0w+DDz93UX9v9mF5KIsQ6OdN2KJoB4ui1bXXr0AMp0YfiQo//4XhpK8555dsNehAqVS5uhb5iHn3Kko769J59KmLBe/TSR7hcsd+hr+HnrwR"+
"9uvRF9+D3MP14gN7lqx+8OuNT+uqt3NFX3SN9fTbeeHNq+C29pRWzX5+Rcm7SZyjOKJ/2hkSPqul4xN279DrSYvCrNu2NI7ZMp1ouBxK3KBVVnEeAUWbK"+ "9uvRF9+D3MP14gN7lqx+8OuNT+uqt3NFX3SN9fTbeeHNq+C29pRWzX5+Rcm7SZyjOKJ/2hkSPqul4xN279DrSYvCrNu2NI7ZMp1ouBxK3KBVVnEeAUWbK"+
"MUDn5DPsPxmUqHZQjGpy2hergM3EVBAAAA==" "MUDn5DPsPxmUqHZQjGpy2hergM3EVBAAAA=="
)))); ))))
clockWidget.clockColon = new GL.Texture(4, 3, base64.atob("7+/v7+/v7+/v7+/v")); clockWidget.clockColon = new GL.Texture(4, 3, base64.atob("7+/v7+/v7+/v7+/v"))
clockWidget.monthNames = ["Spring", "Summer", "Autumn", "Winter"]; clockWidget.monthNames = ["Spring", "Summer", "Autumn", "Winter"]
clockWidget.dayNames = ["Mondag ", "Tysdag ", "Midtveke", "Torsdag ", "Fredag ", "Laurdag ", "Sundag ", "Verddag "]; clockWidget.dayNames = ["Mondag ", "Tysdag ", "Midtveke", "Torsdag ", "Fredag ", "Laurdag ", "Sundag ", "Verddag "]
clockWidget.draw = function(charXoff, charYoff) { clockWidget.draw = function(charXoff, charYoff) {
con.color_pair(254, 255); con.color_pair(254, 255)
let xoff = charXoff * 7; let xoff = charXoff * 7
let yoff = charYoff * 14 + 3; let yoff = charYoff * 14 + 3
let timeInMinutes = ((sys.currentTimeInMills() / 60000)|0); let timeInMinutes = ((sys.currentTimeInMills() / 60000)|0)
let mins = timeInMinutes % 60; let mins = timeInMinutes % 60
let hours = ((timeInMinutes / 60)|0) % 24; let hours = ((timeInMinutes / 60)|0) % 24
let ordinalDay = ((timeInMinutes / (60*24))|0) % 120; let ordinalDay = ((timeInMinutes / (60*24))|0) % 120
let visualDay = (ordinalDay % 30) + 1; let visualDay = (ordinalDay % 30) + 1
let months = ((timeInMinutes / (60*24*30))|0) % 4; let months = ((timeInMinutes / (60*24*30))|0) % 4
let dayName = ordinalDay % 7; // 0 for Mondag let dayName = ordinalDay % 7 // 0 for Mondag
if (ordinalDay == 119) dayName = 7; // Verddag if (ordinalDay == 119) dayName = 7 // Verddag
let years = ((timeInMinutes / (60*24*30*120))|0) + 125; let years = ((timeInMinutes / (60*24*30*120))|0) + 125
// draw timepiece // draw timepiece
GL.drawSprite(clockWidget.numberSheet, (hours / 10)|0, 0, xoff, yoff, 1); GL.drawSprite(clockWidget.numberSheet, (hours / 10)|0, 0, xoff, yoff, 1)
GL.drawSprite(clockWidget.numberSheet, hours % 10, 0, xoff + 24, yoff, 1); GL.drawSprite(clockWidget.numberSheet, hours % 10, 0, xoff + 24, yoff, 1)
GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 5, 1); GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 5, 1)
GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 14, 1); GL.drawTexImage(clockWidget.clockColon, xoff + 48, yoff + 14, 1)
GL.drawSprite(clockWidget.numberSheet, (mins / 10)|0, 0, xoff + 57, yoff, 1); GL.drawSprite(clockWidget.numberSheet, (mins / 10)|0, 0, xoff + 57, yoff, 1)
GL.drawSprite(clockWidget.numberSheet, mins % 10, 0, xoff + 81, yoff, 1); GL.drawSprite(clockWidget.numberSheet, mins % 10, 0, xoff + 81, yoff, 1)
// print month and date // print month and date
con.move(1 + charYoff, 17 + charXoff); con.move(1 + charYoff, 17 + charXoff)
print(clockWidget.monthNames[months]+" "+visualDay); print(clockWidget.monthNames[months]+" "+visualDay)
// print year and dayname // print year and dayname
con.move(2 + charYoff, 17 + charXoff); con.move(2 + charYoff, 17 + charXoff)
print("\xE7"+years+" "+clockWidget.dayNames[dayName]); print("\xE7"+years+" "+clockWidget.dayNames[dayName])
}; }
let calendarWidget = new _fsh.Widget("com.fsh.calendar", (_fsh.scrwidth - 8) / 2, 7*6) let calendarWidget = new _fsh.Widget("com.fsh.calendar", (_fsh.scrwidth - 8) / 2, 7*6)
@@ -171,70 +561,284 @@ calendarWidget.draw = function(charXoff, charYoff) {
let todoWidget = new _fsh.Widget("com.fsh.todo_list", (_fsh.scrwidth - 8) / 2, 7*10) let todoWidget = new _fsh.Widget("com.fsh.todo_list", (_fsh.scrwidth - 8) / 2, 7*10)
todoWidget.todoList = [["Hello, world!", true]] todoWidget.todoList = [["Hello, world!", true]]
todoWidget.draw = function(charXoff, charYoff) { todoWidget.draw = function(charXoff, charYoff) {
let focusIndex = (_fsh.focus && _fsh.focus.widgetId === todoWidget.identifier)
? _fsh.focus.index : -1
con.color_pair(254, 255) con.color_pair(254, 255)
let xoff = charXoff * 7 let xoff = charXoff * 7
let yoff = charYoff * 14 + 3 let yoff = charYoff * 14 + 3
con.move(charYoff, charXoff) con.move(charYoff, charXoff)
print("========== TODO ==========") print('\u00CD'.repeat(10)+" TODO "+'\u00CD'.repeat(10))
for (let i = 0; i <= 12; i++) { for (let i = 0; i <= 12; i++) {
let list = todoWidget.todoList[i] || ["Click to add", null] let list = todoWidget.todoList[i] || ["Click to add"+" ".repeat(_fsh.TODO_TEXT_WIDTH - 12), null]
let isFocused = (i === focusIndex)
if (list[1] === null) con.color_pair(249, 255) if (isFocused) con.color_pair(_fsh.HL_FG, _fsh.HL_BG)
else if (list[1] === null) con.color_pair(249, 255)
else con.color_pair(254, 255) else con.color_pair(254, 255)
con.move(charYoff + i + 2, charXoff) con.move(charYoff + i + 2, charXoff)
con.addch((list[1] === null) ? 43 : (list[1]) ? 0x9F : 0x9E) con.addch((list[1] === null) ? 43 : (list[1]) ? 0x9F : 0x9E)
if (i > todoWidget.todoList.length) { if (i > todoWidget.todoList.length) {
// Filler row \u2014 keep underscores but don't highlight (can't focus here)
con.color_pair(254, 255)
for (let k = 0; k < 24; k++) { for (let k = 0; k < 24; k++) {
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95) con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
} }
} }
else { else {
con.move(charYoff + i + 2, charXoff + 2) con.move(charYoff + i + 2, charXoff + 2)
print(`${list[0]}`) // Pad text to TODO_TEXT_WIDTH so the highlight bar covers full row
let text = `${list[0]}`
if (text.length > _fsh.TODO_TEXT_WIDTH) text = text.substring(0, _fsh.TODO_TEXT_WIDTH)
if (isFocused) text = text + " ".repeat(_fsh.TODO_TEXT_WIDTH - text.length)
print(text)
} }
} }
} }
let quickAccessWidget = new _fsh.Widget("com.fsh.quick_access", (_fsh.scrwidth - 8) / 2, 7*20) let quickAccessWidget = new _fsh.Widget("com.fsh.quick_access", (_fsh.scrwidth - 8) / 2, 7*20)
quickAccessWidget.entries = [ quickAccessWidget.entries = [ // TODO read from /home/config/fshrc
["Files", "/tvdos/bin/explorer.js"], ["Files", "/tvdos/bin/zfm.js"],
["Editor", "/tvdos/bin/edit.js"], ["Editor", "/tvdos/bin/edit.js"],
["BASIC", "/tbas/basic.js"], ["BASIC", "/tbas/basic.js"],
["DOS Shell", "/tvdos/bin/command.js /fancy"] ["DOS Shell", "/tvdos/bin/command.js -fancy"]
] ]
quickAccessWidget.draw = function(charXoff, charYoff) { quickAccessWidget.draw = function(charXoff, charYoff) {
let focusIndex = (_fsh.focus && _fsh.focus.widgetId === quickAccessWidget.identifier)
? _fsh.focus.index : -1
con.color_pair(254, 255) con.color_pair(254, 255)
let xoff = charXoff * 7 let xoff = charXoff * 7
let yoff = charYoff * 14 + 3 let yoff = charYoff * 14 + 3
con.move(charYoff, charXoff) con.move(charYoff, charXoff)
print("====== QUICK ACCESS ======") print('\u00CD'.repeat(6)+" QUICK ACCESS "+'\u00CD'.repeat(6))
for (let i = 0; i <= 21; i++) { for (let i = 0; i <= 21; i++) {
let list = quickAccessWidget.entries[i] || ["Click to add", null] let list = quickAccessWidget.entries[i] || ["Click to add"+" ".repeat(_fsh.QA_LABEL_WIDTH - 12), null]
let isFocused = (i === focusIndex)
if (list[1] === null) con.color_pair(249, 255) if (isFocused) con.color_pair(_fsh.HL_FG, _fsh.HL_BG)
else if (list[1] === null) con.color_pair(249, 255)
else con.color_pair(254, 255) else con.color_pair(254, 255)
con.move(charYoff + i + 2, charXoff) con.move(charYoff + i + 2, charXoff)
con.addch((list[1] === null) ? 0xF9 : (list[1]) ? 7 : 0x7F) con.addch((list[1] === null) ? 0xF9 : (list[1]) ? 7 : 0x7F)
if (i > quickAccessWidget.entries.length) { if (i > quickAccessWidget.entries.length) {
con.color_pair(254, 255)
for (let k = 0; k < 24; k++) { for (let k = 0; k < 24; k++) {
con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95) con.mvaddch(charYoff + i + 2, charXoff + 2 + k, 95)
} }
} }
else { else {
con.move(charYoff + i + 2, charXoff + 2) con.move(charYoff + i + 2, charXoff + 2)
print(`${list[0]}`) let text = `${list[0]}`
if (text.length > _fsh.QA_LABEL_WIDTH) text = text.substring(0, _fsh.QA_LABEL_WIDTH)
if (isFocused) text = text + " ".repeat(_fsh.QA_LABEL_WIDTH - text.length)
print(text)
} }
} }
} }
todoWidget.hitTest = function(charX, charY, xoff, yoff) {
return _fsh.hitTestList(charX, charY, xoff, yoff,
_fsh.TODO_TEXT_WIDTH, todoWidget.todoList.length, _fsh.TODO_MAX_ROWS)
}
quickAccessWidget.hitTest = function(charX, charY, xoff, yoff) {
return _fsh.hitTestList(charX, charY, xoff, yoff,
_fsh.QA_LABEL_WIDTH, quickAccessWidget.entries.length, _fsh.QA_MAX_ROWS)
}
// Re-render the whole shell. Use after a dialog closes (which clobbered
// the underlying char cells) or after execApp returns.
_fsh.redrawAll = function() {
con.color_pair(254, 255)
con.clear()
graphics.clearPixels(255)
graphics.clearPixels2(255)
graphics.setFramebufferScroll(0, 0)
_fsh.drawWallpaper()
_fsh.drawTitlebar()
_fsh.widgets["com.fsh.clock"].draw(25, 3)
_fsh.widgets["com.fsh.calendar"].draw(12, 8)
_fsh.widgets["com.fsh.todo_list"].draw(10, 17)
_fsh.widgets["com.fsh.quick_access"].draw(47, 8)
}
_fsh.openAddTodoDialog = function() {
let res = _fsh.showDialog({
title: "New Todo",
fields: [{label: "Text", initial: "", width: _fsh.TODO_TEXT_WIDTH}],
allowDelete: false
})
_fsh.redrawAll()
if (res.action !== "ok") return
let text = res.values[0].trim()
if (text.length === 0) return
if (todoWidget.todoList.length >= _fsh.TODO_MAX_ROWS) return
todoWidget.todoList.push([text, false])
_fsh.saveConfig()
}
_fsh.openEditTodoDialog = function(index) {
let entry = todoWidget.todoList[index]
if (!entry) return
let res = _fsh.showDialog({
title: "Edit Todo",
fields: [{label: "Text", initial: entry[0], width: _fsh.TODO_TEXT_WIDTH}],
allowDelete: true
})
_fsh.redrawAll()
if (res.action === "cancel") return
if (res.action === "delete") {
todoWidget.todoList.splice(index, 1)
_fsh.saveConfig()
return
}
let text = res.values[0].trim()
if (text.length === 0) return
todoWidget.todoList[index] = [text, entry[1]]
_fsh.saveConfig()
}
_fsh.openAddQaDialog = function() {
let res = _fsh.showDialog({
title: "New Quick Access",
fields: [
{label: "Label", initial: "", width: _fsh.QA_LABEL_WIDTH},
{label: "Command", initial: "", width: _fsh.QA_CMD_WIDTH}
],
allowDelete: false
})
_fsh.redrawAll()
if (res.action !== "ok") return
let label = res.values[0].trim()
let cmd = res.values[1].trim()
if (label.length === 0 || cmd.length === 0) return
if (quickAccessWidget.entries.length >= _fsh.QA_MAX_ROWS) return
quickAccessWidget.entries.push([label, cmd])
_fsh.saveConfig()
}
_fsh.openEditQaDialog = function(index) {
let entry = quickAccessWidget.entries[index]
if (!entry) return
let res = _fsh.showDialog({
title: "Edit Quick Access",
fields: [
{label: "Label", initial: entry[0], width: _fsh.QA_LABEL_WIDTH},
{label: "Command", initial: entry[1], width: _fsh.QA_CMD_WIDTH}
],
allowDelete: true
})
_fsh.redrawAll()
if (res.action === "cancel") return
if (res.action === "delete") {
quickAccessWidget.entries.splice(index, 1)
_fsh.saveConfig()
return
}
let label = res.values[0].trim()
let cmd = res.values[1].trim()
if (label.length === 0 || cmd.length === 0) return
quickAccessWidget.entries[index] = [label, cmd]
_fsh.saveConfig()
}
_fsh.toggleTodoDone = function(index) {
let entry = todoWidget.todoList[index]
if (!entry) return
entry[1] = !entry[1]
_fsh.saveConfig()
}
// Launch a Quick Access entry. cmd is the verbatim string the user typed.
// We split on first space to derive a program path + args; if the path
// has no leading "/", we treat it as relative to the current drive.
_fsh.launchEntry = function(label, cmd) {
let firstSpace = cmd.indexOf(" ")
let progPath = (firstSpace >= 0) ? cmd.substring(0, firstSpace) : cmd
let argTail = (firstSpace >= 0) ? cmd.substring(firstSpace + 1) : ""
let fullPath = progPath.startsWith("/") ? ("A:" + progPath) : progPath
try {
let f = files.open(fullPath)
if (!f.exists) {
serial.printerr("fsh.launchEntry: not found: " + fullPath)
return
}
let code = f.sread()
let tokens = [progPath].concat(argTail.length ? argTail.split(" ") : [])
// erase all pixels and draw wallpaper
con.reset_graphics()
con.clear()
graphics.clearPixels(255)
graphics.clearPixels2(255)
_fsh.drawWallpaper()
con.curs_set(1)
execApp(code, tokens)
} catch (e) {
serial.printerr("fsh.launchEntry: " + label + " failed: " + e)
}
con.curs_set(0)
graphics.setBackground(2,1,3)
graphics.resetPalette()
// Apps (e.g. zfm) may switch to graphics mode 0; restore mode 3 so the
// clock widget on framebuffer 2 is composited again.
graphics.setGraphicsMode(3)
_fsh.redrawAll()
}
// Layout map: widget positions hard-coded to match the draw calls below.
_fsh.layouts = {
"com.fsh.todo_list": {xoff: 10, yoff: 17, widget: null},
"com.fsh.quick_access": {xoff: 47, yoff: 8, widget: null}
}
// Find which widget (if any) was hit by (charX, charY). Returns
// {widgetId, hit} or null.
_fsh.findHit = function(charX, charY) {
let ids = ["com.fsh.todo_list", "com.fsh.quick_access"]
for (let i = 0; i < ids.length; i++) {
let id = ids[i]
let layout = _fsh.layouts[id]
let widget = _fsh.widgets[id]
let hit = widget.hitTest(charX, charY, layout.xoff, layout.yoff)
if (hit) return {widgetId: id, hit: hit}
}
return null
}
_fsh.dispatchLeft = function(widgetId, hit) {
if (hit.kind === "add") {
if (widgetId === "com.fsh.todo_list") _fsh.openAddTodoDialog()
else _fsh.openAddQaDialog()
return
}
// hit.kind === "item"
if (widgetId === "com.fsh.todo_list") {
_fsh.toggleTodoDone(hit.index)
} else {
let entry = quickAccessWidget.entries[hit.index]
if (entry) _fsh.launchEntry(entry[0], entry[1])
}
}
_fsh.dispatchRight = function(widgetId, hit) {
if (hit.kind !== "item") return
if (widgetId === "com.fsh.todo_list") _fsh.openEditTodoDialog(hit.index)
else _fsh.openEditQaDialog(hit.index)
}
// change graphics mode and check if it's supported // change graphics mode and check if it's supported
graphics.setGraphicsMode(3) graphics.setGraphicsMode(3)
@@ -260,29 +864,126 @@ _fsh.drawWallpaper()
_fsh.drawTitlebar() _fsh.drawTitlebar()
// TEST // Load persisted state before the first draw
con.move(2,1); _fsh.loadConfig();
print("fSh is very much in-dev! Hit backspace to exit")
// keyEventBuffers (read via sys.peek(-41-i)) holds *raw libGDX keycodes*,
// not the cooked TSVM scancodes that con.getch() returns. Existing fsh.js
// already uses 67 for Backspace (libGDX DEL); follow the same scheme here.
const KEY_ESC = keysym.ESCAPE
const KEY_ENTER = keysym.ENTER
const KEY_UP = keysym.UP
const KEY_DOWN = keysym.DOWN
const KEY_LEFT = keysym.LEFT
const KEY_RIGHT = keysym.RIGHT
const KEY_LSHIFT = keysym.SHIFT_LEFT
const KEY_RSHIFT = keysym.SHIFT_RIGHT
let prevButtons = 0
let prevMouseCharX = -1
let prevMouseCharY = -1
let keyLatch = {} // {keycode: true} while the key is held — debounces "just pressed"
// TODO update for events: key down (updates some widgets), timer (updates clock and calendar widgets)
while (true) { while (true) {
captureUserInput(); captureUserInput()
if (getKeyPushed(0) == 67) break;
_fsh.widgets["com.fsh.clock"].draw(25, 3); // -- keyboard --
_fsh.widgets["com.fsh.calendar"].draw(12, 8); if (isKeyDown(KEY_ESC)) break;
_fsh.widgets["com.fsh.todo_list"].draw(10, 17);
_fsh.widgets["com.fsh.quick_access"].draw(47, 8);
sys.spin();sys.spin() let shiftDown = isKeyDown(KEY_LSHIFT) || isKeyDown(KEY_RSHIFT)
let enterPressed = false
// Edge-detect each navigation key
function edge(kc) {
let down = isKeyDown(kc)
let was = !!keyLatch[kc]
keyLatch[kc] = down
return down && !was
}
if (edge(KEY_ENTER)) enterPressed = true;
let navUp = edge(KEY_UP)
let navDown = edge(KEY_DOWN)
let navLeft = edge(KEY_LEFT)
let navRight = edge(KEY_RIGHT)
// -- mouse --
let pos = readMousePos()
let charX = (pos[0] / 7) | 0
let charY = (pos[1] / 14) | 0
let mouseMoved = (charX !== prevMouseCharX || charY !== prevMouseCharY)
prevMouseCharX = charX
prevMouseCharY = charY
let buttons = readMouseButtons()
let leftEdge = ((buttons & _fsh.MB_LEFT) !== 0) && ((prevButtons & _fsh.MB_LEFT) === 0)
let rightEdge = ((buttons & _fsh.MB_RIGHT) !== 0) && ((prevButtons & _fsh.MB_RIGHT) === 0)
prevButtons = buttons
// -- focus update --
if (navUp || navDown || navLeft || navRight) {
if (!_fsh.focus) _fsh.focus = {widgetId: "com.fsh.todo_list", index: 0}
if (navUp || navDown) {
let layout = _fsh.layouts[_fsh.focus.widgetId]
let maxRows = (_fsh.focus.widgetId === "com.fsh.todo_list")
? _fsh.TODO_MAX_ROWS : _fsh.QA_MAX_ROWS
let length = (_fsh.focus.widgetId === "com.fsh.todo_list")
? todoWidget.todoList.length : quickAccessWidget.entries.length
let maxIdx = Math.min(length, maxRows - 1)
let next = _fsh.focus.index + (navDown ? 1 : -1)
if (next < 0) next = 0
if (next > maxIdx) next = maxIdx
_fsh.focus.index = next
} else {
// Left/right switches widget
let other = (_fsh.focus.widgetId === "com.fsh.todo_list")
? "com.fsh.quick_access" : "com.fsh.todo_list"
let otherLength = (other === "com.fsh.todo_list")
? todoWidget.todoList.length : quickAccessWidget.entries.length
let otherMaxRows = (other === "com.fsh.todo_list")
? _fsh.TODO_MAX_ROWS : _fsh.QA_MAX_ROWS
let otherMaxIdx = Math.min(otherLength, otherMaxRows - 1)
_fsh.focus = {widgetId: other, index: Math.min(_fsh.focus.index, otherMaxIdx)}
}
} else if (mouseMoved) {
let h = _fsh.findHit(charX, charY)
_fsh.focus = h ? {widgetId: h.widgetId, index: h.hit.kind === "add"
? ((h.widgetId === "com.fsh.todo_list")
? todoWidget.todoList.length
: quickAccessWidget.entries.length)
: h.hit.index} : null
}
// -- mouse click dispatch --
if (leftEdge) {
let h = _fsh.findHit(charX, charY)
if (h) _fsh.dispatchLeft(h.widgetId, h.hit)
} else if (rightEdge) {
let h = _fsh.findHit(charX, charY)
if (h) _fsh.dispatchRight(h.widgetId, h.hit)
}
// -- keyboard dispatch (synthesise click at focus) --
if (enterPressed && _fsh.focus) {
let length = (_fsh.focus.widgetId === "com.fsh.todo_list")
? todoWidget.todoList.length : quickAccessWidget.entries.length
let hit = (_fsh.focus.index < length)
? {kind: "item", index: _fsh.focus.index}
: (_fsh.focus.index === length ? {kind: "add"} : null)
if (hit) {
if (shiftDown) _fsh.dispatchRight(_fsh.focus.widgetId, hit)
else _fsh.dispatchLeft(_fsh.focus.widgetId, hit)
}
}
// -- redraw --
_fsh.widgets["com.fsh.clock"].draw(25, 3)
_fsh.widgets["com.fsh.calendar"].draw(12, 8)
_fsh.widgets["com.fsh.todo_list"].draw(10, 17)
_fsh.widgets["com.fsh.quick_access"].draw(47, 8)
sys.spin(); sys.spin()
} }
con.move(3,1); con.reset_graphics()
con.color_pair(201,255); con.clear()
print("cya!");
let konsht = 3412341241;
println(konsht);
let pppp = graphics.getCursorYX();
println(pppp.toString());

View File

@@ -49,7 +49,7 @@ 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 TRUE, 0 for FALSE) 36 RO: Mouse down? (1 for LEFT, 2 for RIGHT, 3 for BOTH)
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

@@ -3,6 +3,8 @@ package net.torvald.tsvm.peripheral
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input import com.badlogic.gdx.Input
import com.badlogic.gdx.InputProcessor import com.badlogic.gdx.InputProcessor
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.viewport.Viewport
import net.torvald.AddressOverflowException import net.torvald.AddressOverflowException
import net.torvald.DanglingPointerException import net.torvald.DanglingPointerException
import net.torvald.UnsafeHelper import net.torvald.UnsafeHelper
@@ -18,10 +20,18 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
return vm return vm
} }
/** Absolute x-position of the computer GUI */ /**
var guiPosX = 0 * Viewport that maps screen pixels (as reported by `Gdx.input.x/y`) to the VM's
/** Absolute y-position of the computer GUI */ * logical framebuffer coordinate space. The host application owns the rendering
var guiPosY = 0 * camera, so the host is responsible for installing a viewport whose world
* coordinates match the VM framebuffer (origin top-left, world size = framebuffer
* size in pixels) and whose screen rectangle matches where the VM is drawn.
*
* If left null, `Gdx.input.x/y` is forwarded verbatim — only correct when the VM
* occupies the entire window at 1:1 scale.
*/
var inputViewport: Viewport? = null
private val tmpMouseVec = Vector2()
/** Accepts a keycode */ /** Accepts a keycode */
private val keyboardBuffer = CircularArray<Byte>(32, true) private val keyboardBuffer = CircularArray<Byte>(32, true)
@@ -98,7 +108,7 @@ 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 -> mouseDown.toInt().toByte() 36L -> mouseButtons.toByte() // only bits 0..1 used; higher bits intentionally truncated
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
@@ -280,7 +290,7 @@ 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 mouseDown = false private var mouseButtons: Int = 0 // bit 0 = LEFT, bit 1 = RIGHT
private var systemUptime = 0L private var systemUptime = 0L
private var rtc = 0L private var rtc = 0L
@@ -296,10 +306,22 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
keyEventBuffers.fill(0) keyEventBuffers.fill(0)
if (isFocused) { if (isFocused) {
// store mouse info // store mouse info; unproject through the host-provided viewport so the
mouseX = (Gdx.input.x + guiPosX).toShort() // VM sees logical framebuffer pixels regardless of window magnification,
mouseY = (Gdx.input.y + guiPosY).toShort() // letterboxing or sub-region placement done by an embedding GDX app.
mouseDown = Gdx.input.isTouched val vp = inputViewport
if (vp != null) {
tmpMouseVec.set(Gdx.input.x.toFloat(), Gdx.input.y.toFloat())
vp.unproject(tmpMouseVec)
mouseX = tmpMouseVec.x.toInt().toShort()
mouseY = tmpMouseVec.y.toInt().toShort()
}
else {
mouseX = Gdx.input.x.toShort()
mouseY = Gdx.input.y.toShort()
}
mouseButtons = (if (Gdx.input.isButtonPressed(Input.Buttons.LEFT)) 1 else 0) or
(if (Gdx.input.isButtonPressed(Input.Buttons.RIGHT)) 2 else 0)
// strobe keys to fill the key read buffer // strobe keys to fill the key read buffer
var keysPushed = 0 var keysPushed = 0
@@ -313,7 +335,7 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
} }
} }
else { else {
mouseDown = false mouseButtons = 0
} }
} }

View File

@@ -8,6 +8,8 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.graphics.g2d.TextureRegion import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.graphics.glutils.FrameBuffer import com.badlogic.gdx.graphics.glutils.FrameBuffer
import com.badlogic.gdx.graphics.glutils.ShaderProgram import com.badlogic.gdx.graphics.glutils.ShaderProgram
import com.badlogic.gdx.utils.viewport.StretchViewport
import com.badlogic.gdx.utils.viewport.Viewport
import net.torvald.terrarum.DefaultGL32Shaders import net.torvald.terrarum.DefaultGL32Shaders
import net.torvald.tsvm.peripheral.* import net.torvald.tsvm.peripheral.*
import net.torvald.tsvm.peripheral.GraphicsAdapter.Companion.DRAW_SHADER_VERT import net.torvald.tsvm.peripheral.GraphicsAdapter.Companion.DRAW_SHADER_VERT
@@ -48,6 +50,14 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
lateinit var batch: SpriteBatch lateinit var batch: SpriteBatch
lateinit var camera: OrthographicCamera lateinit var camera: OrthographicCamera
/**
* Maps window pixels to the VM framebuffer (origin top-left, world size =
* viewportWidth × viewportHeight). Stretches to fill the whole window so it
* matches the `MAGN`-scaled blit at the end of [renderGame]. Handed to
* [IOSpace.inputViewport] so mouse coordinates unproject correctly.
*/
lateinit var inputViewport: Viewport
var gpu: GraphicsAdapter? = null var gpu: GraphicsAdapter? = null
lateinit var vmRunner: VMRunner lateinit var vmRunner: VMRunner
lateinit var coroutineJob: Thread lateinit var coroutineJob: Thread
@@ -103,9 +113,20 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
gpuFBO = FrameBuffer(Pixmap.Format.RGBA8888, viewportWidth, viewportHeight, false) gpuFBO = FrameBuffer(Pixmap.Format.RGBA8888, viewportWidth, viewportHeight, false)
winFBO = FrameBuffer(Pixmap.Format.RGBA8888, viewportWidth, viewportHeight, false) winFBO = FrameBuffer(Pixmap.Format.RGBA8888, viewportWidth, viewportHeight, false)
val inputCam = OrthographicCamera().also {
it.setToOrtho(true, viewportWidth.toFloat(), viewportHeight.toFloat())
}
inputViewport = StretchViewport(viewportWidth.toFloat(), viewportHeight.toFloat(), inputCam)
inputViewport.update(Gdx.graphics.width, Gdx.graphics.height, true)
init() init()
} }
override fun resize(width: Int, height: Int) {
super.resize(width, height)
inputViewport.update(width, height, true)
}
private fun init() { private fun init() {
vm.init() vm.init()
@@ -148,6 +169,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
} }
Gdx.input.inputProcessor = vm.getIO() Gdx.input.inputProcessor = vm.getIO()
vm.getIO().inputViewport = inputViewport
if (usememvwr) memvwr = Memvwr(vm) if (usememvwr) memvwr = Memvwr(vm)