initial commit
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/ble.cpp. Do not edit.
|
||||
|
||||
---@class BleDevice
|
||||
---@field name string
|
||||
---@field address string
|
||||
---@field rssi integer
|
||||
|
||||
---@class BleLib
|
||||
ble = {}
|
||||
|
||||
---Starts the BLE stack.
|
||||
---@param name? string Local device name.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function ble.init(name) end
|
||||
|
||||
---Stops the BLE stack and releases its memory.
|
||||
function ble.deinit() end
|
||||
|
||||
---Scans for advertising devices.
|
||||
---@param durationMs? integer Defaults to 3000.
|
||||
---@return BleDevice[]? devices
|
||||
---@return string? error
|
||||
function ble.scan(durationMs) end
|
||||
|
||||
---Connects to a peripheral.
|
||||
---@param address string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function ble.connect(address) end
|
||||
|
||||
---Disconnects from the connected peripheral.
|
||||
function ble.disconnect() end
|
||||
|
||||
---Whether a peripheral is connected.
|
||||
---@return boolean
|
||||
function ble.isConnected() end
|
||||
|
||||
---Reads a characteristic value.
|
||||
---@param serviceUuid string
|
||||
---@param characteristicUuid string
|
||||
---@return string? value
|
||||
---@return string? error
|
||||
function ble.read(serviceUuid, characteristicUuid) end
|
||||
|
||||
---Writes a characteristic value.
|
||||
---@param serviceUuid string
|
||||
---@param characteristicUuid string
|
||||
---@param value string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function ble.write(serviceUuid, characteristicUuid, value) end
|
||||
|
||||
---Starts advertising.
|
||||
---@param name? string Advertised device name.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function ble.startAdvertising(name) end
|
||||
|
||||
---Stops advertising.
|
||||
function ble.stopAdvertising() end
|
||||
@@ -0,0 +1,79 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/fs.cpp. Do not edit.
|
||||
|
||||
---@class FsLib
|
||||
---@field MAX_READ_BYTES integer Largest portable whole-file or line read.
|
||||
fs = {}
|
||||
fs.MAX_READ_BYTES = 65536
|
||||
|
||||
---Whether a path exists.
|
||||
---@param path string Absolute SD-card path; traversal components are rejected.
|
||||
---@return boolean
|
||||
function fs.exists(path) end
|
||||
|
||||
---Returns the size of a file in bytes.
|
||||
---@param path string Absolute SD-card path.
|
||||
---@return integer? size
|
||||
---@return string? error Present when the path is missing or not a file.
|
||||
function fs.fileSize(path) end
|
||||
|
||||
---Lists the directories in a directory.
|
||||
---@param path string Absolute directory path.
|
||||
---@return string[]? names Sorted names, excluding hidden entries.
|
||||
---@return string? error
|
||||
function fs.listDirs(path) end
|
||||
|
||||
---Lists the files in a directory.
|
||||
---@param path string Absolute directory path.
|
||||
---@return string[]? names Sorted names, excluding hidden entries.
|
||||
---@return string? error
|
||||
function fs.listFiles(path) end
|
||||
|
||||
---Creates a directory.
|
||||
---@param path string Absolute directory path.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function fs.mkdir(path) end
|
||||
|
||||
---Reads a whole file.
|
||||
---@param path string Absolute file path.
|
||||
---@param maxBytes integer Maximum bytes to allocate, up to fs.MAX_READ_BYTES; oversized files fail rather than truncate.
|
||||
---@return string? content
|
||||
---@return string? error
|
||||
function fs.readFile(path, maxBytes) end
|
||||
|
||||
---Reads one line starting at a byte offset.
|
||||
---@param path string Absolute file path.
|
||||
---@param offset integer Zero-based byte offset; a mid-line offset advances to the next line.
|
||||
---@param maxBytes integer Maximum line bytes to allocate, up to fs.MAX_READ_BYTES.
|
||||
---@return string? line Nil at end of file or on failure.
|
||||
---@return integer? nextOffset Byte offset of the following line.
|
||||
---@return string? error Present when the file cannot be read or the line exceeds maxBytes.
|
||||
function fs.readLineAt(path, offset, maxBytes) end
|
||||
|
||||
---Removes a file.
|
||||
---@param path string Absolute file path. Firmware-protected roots cannot be removed.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function fs.remove(path) end
|
||||
|
||||
---Removes a directory and everything below it.
|
||||
---@param path string Absolute directory path. Firmware-protected roots cannot be removed.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function fs.removeTree(path) end
|
||||
|
||||
---Renames a file or directory.
|
||||
---@param source string Absolute source path.
|
||||
---@param destination string Absolute destination path, which must not exist.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function fs.rename(source, destination) end
|
||||
|
||||
---Atomically replaces the destination or leaves its previous contents intact.
|
||||
---@param path string Absolute file path.
|
||||
---@param content string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function fs.writeFile(path, content) end
|
||||
@@ -0,0 +1,151 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/gui.cpp. Do not edit.
|
||||
|
||||
---@alias GuiColor integer
|
||||
---@alias GuiFont integer
|
||||
---@alias GuiTextStyle integer
|
||||
|
||||
---@class GuiLib
|
||||
---@field FONT_SMALL GuiFont Small auxiliary text.
|
||||
---@field FONT_UI GuiFont Normal controls and labels.
|
||||
---@field FONT_BODY GuiFont Normal reading text.
|
||||
---@field FONT_LARGE GuiFont Headings and prominent values.
|
||||
---@field STYLE_NORMAL GuiTextStyle
|
||||
---@field STYLE_BOLD GuiTextStyle
|
||||
gui = {}
|
||||
gui.FONT_SMALL = 0
|
||||
gui.FONT_UI = 0
|
||||
gui.FONT_BODY = 0
|
||||
gui.FONT_LARGE = 0
|
||||
gui.STYLE_NORMAL = 0
|
||||
gui.STYLE_BOLD = 0
|
||||
|
||||
---Returns the live frame width.
|
||||
---@return integer
|
||||
function gui.getWidth() end
|
||||
|
||||
---Returns the live frame height.
|
||||
---@return integer
|
||||
function gui.getHeight() end
|
||||
|
||||
---Rotates the live frame without changing the saved preference.
|
||||
---@param degrees integer 0, 90, 180, or 270 clockwise.
|
||||
function gui.setRotation(degrees) end
|
||||
|
||||
---Returns the rotation of the live frame.
|
||||
---@return integer Degrees clockwise for the live frame.
|
||||
function gui.getRotation() end
|
||||
|
||||
---Returns an opaque native color. E-ink implementations quantize RGB to available grayscale.
|
||||
---@param r integer 0 through 255.
|
||||
---@param g integer 0 through 255.
|
||||
---@param b integer 0 through 255.
|
||||
---@return GuiColor
|
||||
function gui.color(r, g, b) end
|
||||
|
||||
---Clears the frame.
|
||||
---@param color? GuiColor Defaults to white.
|
||||
function gui.clear(color) end
|
||||
|
||||
---Fills a rectangle.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param w integer
|
||||
---@param h integer
|
||||
---@param color GuiColor
|
||||
function gui.fillRect(x, y, w, h, color) end
|
||||
|
||||
---Outlines a rectangle.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param w integer
|
||||
---@param h integer
|
||||
---@param color GuiColor
|
||||
function gui.drawRect(x, y, w, h, color) end
|
||||
|
||||
---Draws a line.
|
||||
---@param x1 integer
|
||||
---@param y1 integer
|
||||
---@param x2 integer
|
||||
---@param y2 integer
|
||||
---@param color GuiColor
|
||||
---@param width? integer Defaults to one pixel.
|
||||
function gui.drawLine(x1, y1, x2, y2, color, width) end
|
||||
|
||||
---Draws a single pixel.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param color GuiColor
|
||||
function gui.drawPixel(x, y, color) end
|
||||
|
||||
---Outlines a circle.
|
||||
---@param x integer Center.
|
||||
---@param y integer Center.
|
||||
---@param radius integer
|
||||
---@param color GuiColor
|
||||
---@param width? integer Defaults to one pixel.
|
||||
function gui.drawCircle(x, y, radius, color, width) end
|
||||
|
||||
---Fills a circle.
|
||||
---@param x integer Center.
|
||||
---@param y integer Center.
|
||||
---@param radius integer
|
||||
---@param color GuiColor
|
||||
---@param background? GuiColor Surface behind an anti-aliased edge.
|
||||
function gui.fillCircle(x, y, radius, color, background) end
|
||||
|
||||
---Draws an anti-aliased rounded fill, optional gradient, and optional border in one pass.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param w integer
|
||||
---@param h integer
|
||||
---@param radius integer
|
||||
---@param background GuiColor Surface behind the anti-aliased edge.
|
||||
---@param top? GuiColor Fill, or gradient top; omitted for no fill.
|
||||
---@param bottom? GuiColor Gradient bottom; defaults to top. Panels without a gradient use top.
|
||||
---@param border? GuiColor Omitted for no border.
|
||||
function gui.roundRect(x, y, w, h, radius, background, top, bottom, border) end
|
||||
|
||||
---Temporarily gives the app the full panel, including firmware chrome.
|
||||
---@param on boolean
|
||||
function gui.setFullscreen(on) end
|
||||
|
||||
---Fills a polygon.
|
||||
---@param xs integer[]
|
||||
---@param ys integer[]
|
||||
---@param color GuiColor
|
||||
function gui.fillPolygon(xs, ys, color) end
|
||||
|
||||
---Draws a bitmap.
|
||||
---@param path string Absolute BMP path.
|
||||
---@param x? integer Left edge; defaults to centered.
|
||||
---@param y? integer Top edge; defaults to centered.
|
||||
---@param maxWidth? integer Defaults to panel width.
|
||||
---@param maxHeight? integer Defaults to panel height.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function gui.drawBmp(path, x, y, maxWidth, maxHeight) end
|
||||
|
||||
---Measures a text run.
|
||||
---@param font GuiFont Use a named gui.FONT_* role.
|
||||
---@param text string
|
||||
---@param style? GuiTextStyle Defaults to gui.STYLE_NORMAL.
|
||||
---@return integer
|
||||
function gui.getTextWidth(font, text, style) end
|
||||
|
||||
---Returns the line height of a font role.
|
||||
---@param font GuiFont Use a named gui.FONT_* role.
|
||||
---@param style? GuiTextStyle Defaults to gui.STYLE_NORMAL.
|
||||
---@return integer
|
||||
function gui.getFontHeight(font, style) end
|
||||
|
||||
---Draws a text run with its top-left corner at x, y.
|
||||
---@param font GuiFont Use a named gui.FONT_* role.
|
||||
---@param x integer Left edge.
|
||||
---@param y integer Top edge.
|
||||
---@param text string
|
||||
---@param color? GuiColor Defaults to black.
|
||||
---@param style? GuiTextStyle Defaults to gui.STYLE_NORMAL.
|
||||
---@param background? GuiColor Omitted for transparent text.
|
||||
function gui.drawText(font, x, y, text, color, style, background) end
|
||||
@@ -0,0 +1,71 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/http.cpp. Do not edit.
|
||||
|
||||
---@class HttpRequestOptions
|
||||
---@field maxBytes integer Maximum response-body bytes to allocate, up to http.MAX_RESPONSE_BYTES; zero is valid for HEAD.
|
||||
---@field headers? table<string, string>
|
||||
|
||||
---@class HttpResponse
|
||||
---@field status integer HTTP status code.
|
||||
---@field body string Response body, including for non-2xx responses.
|
||||
|
||||
---@class HttpDownloadOptions
|
||||
---@field maxBytes integer Required maximum, from 1 through 16777216.
|
||||
---@field expectedSize? integer Exact expected byte count.
|
||||
---@field sha256? string Exact expected SHA-256 as 64 hexadecimal characters.
|
||||
|
||||
---@class HttpLib
|
||||
---@field MAX_RESPONSE_BYTES integer Largest portable in-memory response body.
|
||||
http = {}
|
||||
http.MAX_RESPONSE_BYTES = 65536
|
||||
|
||||
---Performs a GET request.
|
||||
---@param url string
|
||||
---@param options HttpRequestOptions
|
||||
---@return HttpResponse? response
|
||||
---@return string? error Transport failure or response body exceeding maxBytes.
|
||||
function http.get(url, options) end
|
||||
|
||||
---Performs a HEAD request.
|
||||
---@param url string
|
||||
---@param options HttpRequestOptions
|
||||
---@return HttpResponse? response Body is empty.
|
||||
---@return string? error
|
||||
function http.head(url, options) end
|
||||
|
||||
---Performs a DELETE request.
|
||||
---@param url string
|
||||
---@param options HttpRequestOptions
|
||||
---@return HttpResponse? response
|
||||
---@return string? error
|
||||
function http.delete(url, options) end
|
||||
|
||||
---Performs a POST request.
|
||||
---@param url string
|
||||
---@param body string
|
||||
---@param options HttpRequestOptions
|
||||
---@return HttpResponse? response
|
||||
---@return string? error
|
||||
function http.post(url, body, options) end
|
||||
|
||||
---Performs a PATCH request.
|
||||
---@param url string
|
||||
---@param body string
|
||||
---@param options HttpRequestOptions
|
||||
---@return HttpResponse? response
|
||||
---@return string? error
|
||||
function http.patch(url, body, options) end
|
||||
|
||||
---Streams authenticated HTTPS to a new file and removes partial or unverified output.
|
||||
---@param url string HTTPS URL.
|
||||
---@param destination string Absolute path which must not exist.
|
||||
---@param options HttpDownloadOptions
|
||||
---@return integer? bytesWritten
|
||||
---@return string? error
|
||||
function http.download(url, destination, options) end
|
||||
|
||||
---Percent-encodes a string for use in a URL.
|
||||
---@param input string
|
||||
---@return string
|
||||
function http.urlencode(input) end
|
||||
@@ -0,0 +1,18 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/log.cpp. Do not edit.
|
||||
|
||||
---@class LogLib
|
||||
log = {}
|
||||
|
||||
---Writes a debug message to the firmware log.
|
||||
---@param message string
|
||||
function log.debug(message) end
|
||||
|
||||
---Writes an informational message to the firmware log.
|
||||
---@param message string
|
||||
function log.info(message) end
|
||||
|
||||
---Writes an error message to the firmware log.
|
||||
---@param message string
|
||||
function log.error(message) end
|
||||
@@ -0,0 +1,155 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/node.cpp. Do not edit.
|
||||
|
||||
---@alias NodeId integer
|
||||
---@alias NodeType "box"|"text"|"button"|"custom"
|
||||
---@alias NodeDirection "up"|"down"|"left"|"right"
|
||||
|
||||
---@class NodeSpec
|
||||
---@field type NodeType
|
||||
---@field w? number|"fill"|"auto"
|
||||
---@field h? number|"fill"|"auto"
|
||||
---@field pad? number
|
||||
---@field gap? number
|
||||
---@field align? "start"|"center"|"end"|"stretch"
|
||||
---@field justify? "start"|"center"|"end"|"between"
|
||||
---@field row? boolean
|
||||
---@field at? table Absolute-position fields.
|
||||
---@field capture? boolean
|
||||
---@field interactive? boolean
|
||||
---@field label? string
|
||||
---@field font? GuiFont
|
||||
|
||||
---@class NodeStyle
|
||||
---@field color? GuiColor
|
||||
---@field background? GuiColor Background offered to descendants.
|
||||
---@field fill? GuiColor Surface painted by a box.
|
||||
---@field border? GuiColor
|
||||
---@field face? GuiColor Default button surface.
|
||||
---@field pressedFace? GuiColor Pressed button surface.
|
||||
---@field pressedColor? GuiColor Pressed button text.
|
||||
---@field focusColor? GuiColor Distinct outline for directional focus.
|
||||
---@field radius? integer
|
||||
---@field font? GuiFont
|
||||
---@field textStyle? GuiTextStyle
|
||||
|
||||
---@class NodeLib
|
||||
node = {}
|
||||
|
||||
---Drops the current tree; all existing IDs become invalid.
|
||||
function node.reset() end
|
||||
|
||||
---Creates a node, optionally as a child of an existing one.
|
||||
---@param parent? NodeId Nil creates a root.
|
||||
---@param spec NodeSpec
|
||||
---@return NodeId
|
||||
function node.create(parent, spec) end
|
||||
|
||||
---Adopts an existing root as a child.
|
||||
---@param parent NodeId
|
||||
---@param child NodeId Existing root without a parent.
|
||||
function node.attach(parent, child) end
|
||||
|
||||
---Changes a node's requested size before layout.
|
||||
---@param id NodeId
|
||||
---@param w? number|"fill"|"auto"
|
||||
---@param h? number|"fill"|"auto"
|
||||
function node.setSize(id, w, h) end
|
||||
|
||||
---Measures and places a subtree.
|
||||
---@param root NodeId
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@param w integer
|
||||
---@param h integer
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function node.layout(root, x, y, w, h) end
|
||||
|
||||
---Releases temporary measurement and placement inputs after layout.
|
||||
function node.dropScratch() end
|
||||
|
||||
---Returns the deepest interactive node under a point.
|
||||
---@param root NodeId
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return NodeId?
|
||||
function node.hit(root, x, y) end
|
||||
|
||||
---Returns a node's placed rectangle.
|
||||
---@param id NodeId
|
||||
---@return integer x
|
||||
---@return integer y
|
||||
---@return integer w
|
||||
---@return integer h
|
||||
function node.getRect(id) end
|
||||
|
||||
---Replaces a node's text and marks it for repaint.
|
||||
---@param id NodeId
|
||||
---@param text string
|
||||
function node.setLabel(id, text) end
|
||||
|
||||
---Returns a node's text.
|
||||
---@param id NodeId
|
||||
---@return string?
|
||||
function node.getLabel(id) end
|
||||
|
||||
---Returns a node's parent.
|
||||
---@param id NodeId
|
||||
---@return NodeId?
|
||||
function node.getParent(id) end
|
||||
|
||||
---Sets the style roles a subtree inherits.
|
||||
---@param id NodeId
|
||||
---@param style NodeStyle
|
||||
function node.setStyle(id, style) end
|
||||
|
||||
---Marks a node for repaint.
|
||||
---@param id NodeId
|
||||
function node.invalidate(id) end
|
||||
|
||||
---Sets a node's pressed state.
|
||||
---@param id NodeId
|
||||
---@param pressed boolean
|
||||
function node.setPressed(id, pressed) end
|
||||
|
||||
---Whether a node is pressed.
|
||||
---@param id NodeId
|
||||
---@return boolean
|
||||
function node.isPressed(id) end
|
||||
|
||||
---Focuses the first interactive node in layout order.
|
||||
---@param root NodeId
|
||||
---@return NodeId? focused
|
||||
function node.focusFirst(root) end
|
||||
|
||||
---Changes focus and invalidates the previously and newly focused nodes.
|
||||
---@param id? NodeId Nil clears focus.
|
||||
function node.setFocus(id) end
|
||||
|
||||
---Returns the focused node.
|
||||
---@return NodeId?
|
||||
function node.getFocus() end
|
||||
|
||||
---Moves to the nearest interactive node in the requested direction without wrapping.
|
||||
---@param root NodeId
|
||||
---@param direction NodeDirection
|
||||
---@return NodeId? focused Current focus when no candidate exists.
|
||||
function node.moveFocus(root, direction) end
|
||||
|
||||
---Registers the painter every custom node calls.
|
||||
---@param painter fun(id: NodeId, x: integer, y: integer, w: integer, h: integer)
|
||||
function node.setPainter(painter) end
|
||||
|
||||
---Paints dirty nodes; the firmware owns publication to the physical display.
|
||||
---@param root NodeId
|
||||
function node.draw(root) end
|
||||
|
||||
---Returns the number of nodes in the tree.
|
||||
---@return integer
|
||||
function node.getCount() end
|
||||
|
||||
---Returns the tree's memory use.
|
||||
---@return integer bytes
|
||||
function node.getFootprint() end
|
||||
@@ -0,0 +1,22 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/runtime/runtime.cpp. Do not edit.
|
||||
|
||||
-- Runtime layout:
|
||||
-- /.lua/apps/<AppId>/main.lua application entry point
|
||||
-- /.lua/apps/<AppId>/<Subapp>/main.lua nested route, omitted from the launcher
|
||||
-- /.lua/data/<AppId>/ persistent app data, preserved across updates
|
||||
-- /.lua/lib/<module>.lua shared require() modules
|
||||
-- require() also searches the running application's directory
|
||||
--
|
||||
-- The firmware does not clear the frame before calling draw(), and commits changed
|
||||
-- display content after each callback batch using the panel's own refresh policy.
|
||||
-- Timer callbacks are registered directly with timer.after/every.
|
||||
|
||||
---Required. Runs once before the first draw; failing here stops the app.
|
||||
---@param arg? string The string passed to sys.launch or sys.replace.
|
||||
function init(arg) end
|
||||
|
||||
---Optional frame loop, called once after init and then at most 30 FPS, best effort.
|
||||
---@param deltaMs integer Monotonic milliseconds since the previous draw; zero on the first.
|
||||
function draw(deltaMs) end
|
||||
@@ -0,0 +1,26 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/settings.cpp. Do not edit.
|
||||
|
||||
---@class SettingsLib
|
||||
settings = {}
|
||||
|
||||
---Returns the saved rotation in degrees clockwise.
|
||||
---@return integer
|
||||
function settings.getRotation() end
|
||||
|
||||
---Applies and persists the screen rotation.
|
||||
---@param degrees integer 0, 90, 180, or 270 clockwise.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function settings.setRotation(degrees) end
|
||||
|
||||
---Returns the active POSIX timezone rule.
|
||||
---@return string
|
||||
function settings.getTimezone() end
|
||||
|
||||
---Applies and persists a POSIX timezone rule.
|
||||
---@param timezone string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function settings.setTimezone(timezone) end
|
||||
@@ -0,0 +1,60 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/sys.cpp. Do not edit.
|
||||
|
||||
---@alias Feature "touch"|"buttons"
|
||||
|
||||
---@class SysLib
|
||||
sys = {}
|
||||
|
||||
---Returns the implemented API contract version.
|
||||
---@return integer
|
||||
function sys.getAPIVersion() end
|
||||
|
||||
---Whether the firmware implements a complete optional feature contract.
|
||||
---@param feature Feature
|
||||
---@return boolean
|
||||
function sys.hasFeature(feature) end
|
||||
|
||||
---Returns monotonic milliseconds since boot.
|
||||
---@return integer
|
||||
function sys.getMillis() end
|
||||
|
||||
---Returns the immutable first path component of the running app.
|
||||
---@return string
|
||||
function sys.getAppID() end
|
||||
|
||||
---Returns the running app title, initially the app ID.
|
||||
---@return string
|
||||
function sys.getAppTitle() end
|
||||
|
||||
---Returns the current app's guaranteed-existing persistent data directory.
|
||||
---@return string Absolute path under /.lua/data, preserved across app updates.
|
||||
function sys.getAppDataPath() end
|
||||
|
||||
---Changes the running app's display title.
|
||||
---@param title string
|
||||
function sys.setAppTitle(title) end
|
||||
|
||||
---Launches /.lua/apps/<path>/main.lua and pushes the current route.
|
||||
---@param path string App-relative directory path; traversal is rejected.
|
||||
---@param arg? string Passed to init(arg).
|
||||
function sys.launch(path, arg) end
|
||||
|
||||
---Launches an app path without retaining the current route.
|
||||
---@param path string App-relative directory path; traversal is rejected.
|
||||
---@param arg? string Passed to init(arg).
|
||||
function sys.replace(path, arg) end
|
||||
|
||||
---Returns to the previous app, or the launcher when history is empty.
|
||||
function sys.back() end
|
||||
|
||||
---Returns heap statistics.
|
||||
---@return integer freeBytes
|
||||
---@return integer totalBytes
|
||||
---@return integer largestFreeBlock
|
||||
function sys.getMemory() end
|
||||
|
||||
---Whether network time synchronization has completed.
|
||||
---@return boolean
|
||||
function sys.isClockSynced() end
|
||||
@@ -0,0 +1,26 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/timer.cpp. Do not edit.
|
||||
|
||||
---@alias TimerId integer
|
||||
---@alias TimerCallback fun()
|
||||
|
||||
---@class TimerLib
|
||||
timer = {}
|
||||
|
||||
---Runs a callback once after a delay.
|
||||
---@param intervalMs integer Positive delay; callback timing is best effort and never early.
|
||||
---@param callback TimerCallback Retained until it fires or is cancelled.
|
||||
---@return TimerId
|
||||
function timer.after(intervalMs, callback) end
|
||||
|
||||
---Runs a callback repeatedly.
|
||||
---@param intervalMs integer Positive interval; callback timing is best effort and never early.
|
||||
---@param callback TimerCallback Retained until cancelled.
|
||||
---@return TimerId
|
||||
function timer.every(intervalMs, callback) end
|
||||
|
||||
---Cancels a timer and releases its callback.
|
||||
---@param id TimerId
|
||||
---@return boolean Whether an active timer was cancelled.
|
||||
function timer.cancel(id) end
|
||||
@@ -0,0 +1,51 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/core/wifi.cpp. Do not edit.
|
||||
|
||||
---@class WifiNetwork
|
||||
---@field ssid string
|
||||
---@field rssi integer
|
||||
---@field secure boolean
|
||||
|
||||
---@alias WifiState "disconnected"|"connecting"|"connected"|"not_found"|"failed"
|
||||
|
||||
---@class WifiStatus
|
||||
---@field state WifiState
|
||||
---@field ssid string
|
||||
---@field ip string
|
||||
---@field rssi integer
|
||||
|
||||
---@class WifiLib
|
||||
wifi = {}
|
||||
|
||||
---Scans for visible networks.
|
||||
---@return WifiNetwork[]? networks
|
||||
---@return string? error
|
||||
function wifi.scan() end
|
||||
|
||||
---With credentials, saves and joins that network. Without them, reconnects saved credentials.
|
||||
---@param ssid? string
|
||||
---@param password? string Omit for an open network.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function wifi.connect(ssid, password) end
|
||||
|
||||
---Returns the current connection state.
|
||||
---@return WifiStatus
|
||||
function wifi.getStatus() end
|
||||
|
||||
---Whether the station is associated and has an address.
|
||||
---@return boolean
|
||||
function wifi.isConnected() end
|
||||
|
||||
---Returns the station address.
|
||||
---@return string IPv4 address, or 0.0.0.0 when disconnected.
|
||||
function wifi.getLocalIP() end
|
||||
|
||||
---Disconnects while retaining saved credentials.
|
||||
function wifi.disconnect() end
|
||||
|
||||
---Disconnects and erases saved credentials.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function wifi.forget() end
|
||||
@@ -0,0 +1,46 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/features/buttons.cpp. Do not edit.
|
||||
|
||||
---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
|
||||
|
||||
-- Roles, not physical buttons: a device maps whatever hardware it has onto them, and
|
||||
-- up/down/left/right are the directions node.moveFocus already takes.
|
||||
|
||||
---@class InputLib
|
||||
input = input or {}
|
||||
|
||||
---Returns the roles this device reports, so an app can label only the actions it has.
|
||||
---@return Button[]
|
||||
function input.getButtons() end
|
||||
|
||||
---Whether any button is held.
|
||||
---@return boolean
|
||||
function input.isAnyPressed() end
|
||||
|
||||
---Whether a button is held.
|
||||
---@param button Button
|
||||
---@return boolean
|
||||
function input.isPressed(button) end
|
||||
|
||||
---Whether a button went down since the last poll.
|
||||
---@param button Button
|
||||
---@return boolean
|
||||
function input.wasPressed(button) end
|
||||
|
||||
---Whether a button came up since the last poll.
|
||||
---@param button Button
|
||||
---@return boolean
|
||||
function input.wasReleased(button) end
|
||||
|
||||
---Fired when a button goes down.
|
||||
---@param button Button
|
||||
function on_button_down(button) end
|
||||
|
||||
---Fired when a button comes up.
|
||||
---@param button Button
|
||||
function on_button_up(button) end
|
||||
|
||||
---Tap alias, fired on release like a click, after on_button_up.
|
||||
---@param button Button
|
||||
function on_button(button) end
|
||||
@@ -0,0 +1,52 @@
|
||||
---@meta
|
||||
|
||||
-- Generated from native/src/bindings/features/touch.cpp. Do not edit.
|
||||
|
||||
---@class SettingsLib
|
||||
settings = settings or {}
|
||||
|
||||
---Persists the panel's touch calibration.
|
||||
---@param x0 integer Raw reading at the left edge.
|
||||
---@param y0 integer Raw reading at the top edge.
|
||||
---@param x1 integer Raw reading at the right edge.
|
||||
---@param y1 integer Raw reading at the bottom edge.
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function settings.setCalibration(x0, y0, x1, y1) end
|
||||
|
||||
---@class InputLib
|
||||
input = input or {}
|
||||
|
||||
---Returns the calibrated touch point, or nothing when the panel is not touched.
|
||||
---@return integer? x
|
||||
---@return integer? y
|
||||
function input.getTouch() end
|
||||
|
||||
---Returns the uncalibrated touch reading, or nothing when the panel is not touched.
|
||||
---@return integer? x
|
||||
---@return integer? y
|
||||
function input.getRawTouch() end
|
||||
|
||||
---Whether the panel is currently touched.
|
||||
---@return boolean
|
||||
function input.isTouched() end
|
||||
|
||||
---Fired when the finger lands.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function on_touch_down(x, y) end
|
||||
|
||||
---Fired when the finger moves while down, after the firmware's jitter filter.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function on_touch_move(x, y) end
|
||||
|
||||
---Fired when the finger lifts.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function on_touch_up(x, y) end
|
||||
|
||||
---Tap alias, fired on release like a click, after on_touch_up.
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
function on_touch(x, y) end
|
||||
@@ -0,0 +1,45 @@
|
||||
-- Button hints as a shared module rather than a firmware paint routine: what the strip looks
|
||||
-- like is composition, and only the device knows which roles exist to label.
|
||||
|
||||
local hints = {}
|
||||
|
||||
-- Reading order across the strip, so a device showing three of them still reads left to right.
|
||||
local ORDER = {"back", "left", "up", "down", "right", "confirm"}
|
||||
|
||||
local function available()
|
||||
local roles = {}
|
||||
if input and input.getButtons then
|
||||
for _, role in ipairs(input.getButtons()) do roles[role] = true end
|
||||
end
|
||||
return roles
|
||||
end
|
||||
|
||||
---Paints the labels this device can actually act on, and returns the height it used.
|
||||
---@param actions table<string, string> Label per Button role; roles the device lacks are skipped.
|
||||
---@param options table|nil `y`, `font`, `color`, and `background` overrides.
|
||||
function hints.draw(actions, options)
|
||||
options = options or {}
|
||||
local font = options.font or gui.FONT_SMALL
|
||||
local color = options.color or gui.color(0, 0, 0)
|
||||
local background = options.background or gui.color(255, 255, 255)
|
||||
local height = gui.getFontHeight(font) + 6
|
||||
local y = options.y or (gui.getHeight() - height)
|
||||
|
||||
local roles = available()
|
||||
local labels = {}
|
||||
for _, role in ipairs(ORDER) do
|
||||
if roles[role] and actions[role] then labels[#labels + 1] = actions[role] end
|
||||
end
|
||||
|
||||
gui.fillRect(0, y, gui.getWidth(), height, background)
|
||||
if #labels == 0 then return height end
|
||||
|
||||
local slot = gui.getWidth() // #labels
|
||||
for index, label in ipairs(labels) do
|
||||
local left = slot * (index - 1) + (slot - gui.getTextWidth(font, label)) // 2
|
||||
gui.drawText(font, left, y + 3, label, color, gui.STYLE_NORMAL, background)
|
||||
end
|
||||
return height
|
||||
end
|
||||
|
||||
return hints
|
||||
+437
@@ -0,0 +1,437 @@
|
||||
local ui = {}
|
||||
|
||||
---@alias UiHandler fun(id: NodeId, x?: integer, y?: integer)
|
||||
|
||||
---@class UiSpec
|
||||
---@field [integer] NodeId Child nodes.
|
||||
---@field w? number|"fill"|"auto"
|
||||
---@field h? number|"fill"|"auto"
|
||||
---@field pad? number
|
||||
---@field gap? number
|
||||
---@field align? "start"|"center"|"end"|"stretch"
|
||||
---@field justify? "start"|"center"|"end"|"between"
|
||||
---@field row? boolean
|
||||
---@field at? table
|
||||
---@field capture? boolean
|
||||
---@field label? string
|
||||
---@field font? GuiFont
|
||||
---@field style? GuiTextStyle
|
||||
---@field fit? integer Maximum label width.
|
||||
---@field background? GuiColor
|
||||
---@field face? GuiColor
|
||||
---@field pressedFace? GuiColor
|
||||
---@field pressedColor? GuiColor
|
||||
---@field focusColor? GuiColor
|
||||
---@field focusWidth? integer
|
||||
---@field textStyle? GuiTextStyle
|
||||
---@field on_enter? UiHandler
|
||||
---@field on_exit? UiHandler
|
||||
---@field on_click? UiHandler
|
||||
---@field paint? fun(id: NodeId, x: integer, y: integer, w: integer, h: integer)
|
||||
|
||||
---@class UiConfirmSpec
|
||||
---@field title string
|
||||
---@field message? string
|
||||
---@field ok? string
|
||||
---@field cancel? string|false
|
||||
---@field w? number
|
||||
---@field background? GuiColor
|
||||
---@field border? GuiColor
|
||||
---@field on_ok? UiHandler
|
||||
---@field on_cancel? UiHandler
|
||||
---@field on_outside? UiHandler
|
||||
|
||||
local THEME_PATH = "/.lua/theme"
|
||||
local THEMES = {
|
||||
light = {background = {255, 255, 255}, color = {0, 0, 0}, accent = {0, 120, 255}, radius = 6},
|
||||
dark = {background = {18, 18, 20}, color = {235, 235, 235}, accent = {166, 118, 255}, radius = 6},
|
||||
mono = {background = {255, 255, 255}, color = {0, 0, 0}, accent = {0, 0, 0}, radius = 0},
|
||||
}
|
||||
|
||||
local enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
local laidOut = false
|
||||
local themeName
|
||||
local activeScreen
|
||||
local applyPalette
|
||||
|
||||
local function mix(a, b, amount)
|
||||
local out = {}
|
||||
for i = 1, 3 do out[i] = math.floor(a[i] + (b[i] - a[i]) * amount + 0.5) end
|
||||
return out
|
||||
end
|
||||
|
||||
local function color(rgb)
|
||||
return gui.color(rgb[1], rgb[2], rgb[3])
|
||||
end
|
||||
|
||||
local function palette(seed)
|
||||
local background, foreground, accent = seed.background, seed.color, seed.accent
|
||||
return {
|
||||
background = color(background),
|
||||
color = color(foreground),
|
||||
muted = color(mix(foreground, background, 0.45)),
|
||||
accent = color(accent),
|
||||
face = color(mix(background, foreground, 0.08)),
|
||||
pressedFace = color(accent),
|
||||
pressedColor = color(background),
|
||||
focusColor = color(accent),
|
||||
focusWidth = 2,
|
||||
radius = seed.radius,
|
||||
}
|
||||
end
|
||||
|
||||
local function loadTheme(name)
|
||||
themeName = THEMES[name] and name or "light"
|
||||
ui.theme = palette(THEMES[themeName])
|
||||
end
|
||||
|
||||
---@return string
|
||||
function ui.getTheme()
|
||||
return themeName
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function ui.themeNames()
|
||||
local names = {}
|
||||
for name in pairs(THEMES) do names[#names + 1] = name end
|
||||
table.sort(names)
|
||||
return names
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function ui.setTheme(name)
|
||||
if not THEMES[name] then return nil, "Unknown theme" end
|
||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
||||
if not ok then return nil, err end
|
||||
loadTheme(name)
|
||||
if activeScreen then
|
||||
applyPalette(activeScreen.root)
|
||||
gui.clear(ui.theme.background)
|
||||
node.invalidate(activeScreen.root)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local savedTheme = fs.readFile(THEME_PATH, 32)
|
||||
loadTheme(savedTheme and savedTheme:match("^%s*(.-)%s*$") or "light")
|
||||
|
||||
local function clearState()
|
||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
end
|
||||
|
||||
node.setPainter(function(id, x, y, w, h)
|
||||
local painter = painters[id]
|
||||
if painter then painter(id, x, y, w, h) end
|
||||
end)
|
||||
|
||||
local STYLE_KEYS = {
|
||||
"color", "fill", "border", "face", "pressedFace", "pressedColor",
|
||||
"focusColor", "focusWidth", "radius", "font", "textStyle",
|
||||
}
|
||||
|
||||
local function applyStyle(id, spec)
|
||||
local style, hasStyle = {}, false
|
||||
for _, key in ipairs(STYLE_KEYS) do
|
||||
if spec[key] ~= nil then
|
||||
style[key], hasStyle = spec[key], true
|
||||
end
|
||||
end
|
||||
if spec.background ~= nil then
|
||||
style.background, style.fill, hasStyle = spec.background, spec.background, true
|
||||
end
|
||||
if hasStyle then node.setStyle(id, style) end
|
||||
end
|
||||
|
||||
local function build(spec, kind)
|
||||
spec = spec or {}
|
||||
if laidOut then ui.reset() end
|
||||
|
||||
local children = {}
|
||||
for index, child in ipairs(spec) do
|
||||
children[index] = child
|
||||
spec[index] = nil
|
||||
end
|
||||
|
||||
spec.type = kind
|
||||
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
|
||||
local id = node.create(nil, spec)
|
||||
for _, child in ipairs(children) do node.attach(id, child) end
|
||||
|
||||
applyStyle(id, spec)
|
||||
enterHandlers[id] = spec.on_enter
|
||||
exitHandlers[id] = spec.on_exit
|
||||
clickHandlers[id] = spec.on_click
|
||||
painters[id] = spec.paint
|
||||
return id
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.box(spec)
|
||||
return build(spec, "box")
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.spacer(spec)
|
||||
spec = spec or {}
|
||||
return build({w = spec.w, h = spec.h}, "box")
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param spec? UiSpec
|
||||
---@return NodeId
|
||||
function ui.text(text, spec)
|
||||
spec = spec or {}
|
||||
spec.label = text
|
||||
spec.textStyle, spec.style = spec.style, nil
|
||||
return build(spec, "text")
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param spec? UiSpec
|
||||
---@return NodeId
|
||||
function ui.label(text, spec)
|
||||
spec = spec or {}
|
||||
local font, style = spec.font or gui.FONT_UI, spec.style or gui.STYLE_NORMAL
|
||||
if spec.fit and gui.getTextWidth(font, text, style) > spec.fit then
|
||||
while #text > 1 and gui.getTextWidth(font, text .. "~", style) > spec.fit do
|
||||
text = text:sub(1, -2)
|
||||
end
|
||||
text = text .. "~"
|
||||
end
|
||||
spec.w = gui.getTextWidth(font, text, style)
|
||||
spec.h = gui.getFontHeight(font, style)
|
||||
spec.font, spec.fit = font, nil
|
||||
return ui.text(text, spec)
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.button(spec)
|
||||
spec = spec or {}
|
||||
spec.pad = spec.pad or 8
|
||||
spec.align = spec.align or "center"
|
||||
local label, font = spec.label, spec.font
|
||||
spec.label = nil
|
||||
local id = build(spec, "button")
|
||||
if label then node.create(id, {type = "text", label = label, font = font or gui.FONT_UI}) end
|
||||
return id
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.custom(spec)
|
||||
return build(spec, "custom")
|
||||
end
|
||||
|
||||
---@param id NodeId
|
||||
---@param text string
|
||||
function ui.setText(id, text)
|
||||
if node.getLabel(id) == text then return end
|
||||
node.setLabel(id, text)
|
||||
node.invalidate(id)
|
||||
end
|
||||
|
||||
---@param id NodeId
|
||||
function ui.invalidate(id)
|
||||
node.invalidate(id)
|
||||
end
|
||||
|
||||
---@param spec UiConfirmSpec
|
||||
---@return NodeId
|
||||
function ui.confirm(spec)
|
||||
local card = {
|
||||
w = spec.w or 0.85,
|
||||
pad = 16,
|
||||
gap = 12,
|
||||
background = spec.background or ui.theme.background,
|
||||
border = spec.border or ui.theme.muted,
|
||||
ui.text(spec.title),
|
||||
}
|
||||
if spec.message then card[#card + 1] = ui.text(spec.message, {color = ui.theme.muted}) end
|
||||
|
||||
local buttons = {row = true, gap = 8, justify = "end"}
|
||||
if spec.cancel ~= false then
|
||||
buttons[#buttons + 1] = ui.button{label = spec.cancel or "cancel", on_click = spec.on_cancel}
|
||||
end
|
||||
buttons[#buttons + 1] = ui.button{label = spec.ok or "ok", on_click = spec.on_ok}
|
||||
card[#card + 1] = ui.box(buttons)
|
||||
|
||||
return ui.box{
|
||||
at = {x = 0, y = 0}, w = "fill", h = "fill", capture = true,
|
||||
align = "center", justify = "center", on_click = spec.on_outside,
|
||||
ui.box(card),
|
||||
}
|
||||
end
|
||||
|
||||
function ui.reset()
|
||||
node.reset()
|
||||
clearState()
|
||||
laidOut = false
|
||||
activeScreen = nil
|
||||
end
|
||||
|
||||
---@class UiScreen
|
||||
local Screen = {}
|
||||
Screen.__index = Screen
|
||||
|
||||
applyPalette = function(root)
|
||||
node.setStyle(root, {
|
||||
color = ui.theme.color,
|
||||
background = ui.theme.background,
|
||||
border = ui.theme.muted,
|
||||
face = ui.theme.face,
|
||||
pressedFace = ui.theme.pressedFace,
|
||||
pressedColor = ui.theme.pressedColor,
|
||||
focusColor = ui.theme.focusColor,
|
||||
focusWidth = ui.theme.focusWidth,
|
||||
radius = ui.theme.radius,
|
||||
font = gui.FONT_UI,
|
||||
})
|
||||
end
|
||||
|
||||
---@param root NodeId
|
||||
---@param style? NodeStyle
|
||||
---@return UiScreen
|
||||
function ui.screen(root, style)
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
if style then node.setStyle(root, style) end
|
||||
local screen = setmetatable({root = root}, Screen)
|
||||
activeScreen = screen
|
||||
screen:relayout()
|
||||
return screen
|
||||
end
|
||||
|
||||
function Screen:relayout()
|
||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
if not ok then error(err, 2) end
|
||||
node.dropScratch()
|
||||
laidOut = true
|
||||
gui.clear(ui.theme.background)
|
||||
end
|
||||
|
||||
function Screen:draw()
|
||||
node.draw(self.root)
|
||||
end
|
||||
|
||||
local function inside(id, x, y)
|
||||
local rx, ry, rw, rh = node.getRect(id)
|
||||
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
|
||||
end
|
||||
|
||||
local function enter(id, x, y)
|
||||
node.setPressed(id, true)
|
||||
local handler = enterHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
end
|
||||
|
||||
local function exit(id, x, y)
|
||||
node.setPressed(id, false)
|
||||
local handler = exitHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:down(x, y)
|
||||
local focused = node.getFocus()
|
||||
if focused then
|
||||
node.setFocus(nil)
|
||||
local handler = exitHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
|
||||
local target = node.hit(self.root, x, y)
|
||||
if not target then return false end
|
||||
self.captured, self.inside = target, true
|
||||
enter(target, x, y)
|
||||
return true
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:move(x, y)
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local isInside = inside(target, x, y)
|
||||
if isInside ~= self.inside then
|
||||
self.inside = isInside
|
||||
if isInside then enter(target, x, y) else exit(target, x, y) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:up(x, y)
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local wasActive = self.inside
|
||||
local releasedInside = inside(target, x, y)
|
||||
local handler = releasedInside and clickHandlers[target] or nil
|
||||
self.captured, self.inside = nil, nil
|
||||
if wasActive then exit(target, x, y) end
|
||||
if handler then handler(target, x, y) end
|
||||
return true
|
||||
end
|
||||
|
||||
local DIRECTIONS = {up = true, down = true, left = true, right = true}
|
||||
|
||||
local function focusFirst(screen)
|
||||
local focused = node.focusFirst(screen.root)
|
||||
if focused then
|
||||
local handler = enterHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
return focused
|
||||
end
|
||||
|
||||
---@param name string Button name; directions and confirm are handled.
|
||||
---@param pressed boolean
|
||||
---@return boolean handled
|
||||
function Screen:button(name, pressed)
|
||||
if type(pressed) ~= "boolean" then error("button state must be boolean", 2) end
|
||||
|
||||
if DIRECTIONS[name] then
|
||||
if not pressed then return true end
|
||||
local previous = node.getFocus()
|
||||
if not previous then
|
||||
focusFirst(self)
|
||||
return true
|
||||
end
|
||||
local focused = node.moveFocus(self.root, name)
|
||||
if focused ~= previous then
|
||||
local leave = exitHandlers[previous]
|
||||
if leave then leave(previous) end
|
||||
local arrive = enterHandlers[focused]
|
||||
if arrive then arrive(focused) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if name ~= "confirm" then return false end
|
||||
local focused = node.getFocus() or focusFirst(self)
|
||||
if not focused then return false end
|
||||
if pressed then
|
||||
node.setPressed(focused, true)
|
||||
self.confirming = focused
|
||||
else
|
||||
local target = self.confirming
|
||||
self.confirming = nil
|
||||
if target then
|
||||
node.setPressed(target, false)
|
||||
local handler = clickHandlers[target]
|
||||
if handler then handler(target) end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return ui
|
||||
@@ -0,0 +1,36 @@
|
||||
package.path = "./lua/lib/?.lua;" .. package.path
|
||||
|
||||
local drawn = {}
|
||||
local roles = {"confirm", "back", "right"}
|
||||
|
||||
gui = {
|
||||
FONT_SMALL = 0,
|
||||
STYLE_NORMAL = 0,
|
||||
color = function(r, g, b) return r * 65536 + g * 256 + b end,
|
||||
getWidth = function() return 300 end,
|
||||
getHeight = function() return 240 end,
|
||||
getFontHeight = function() return 8 end,
|
||||
getTextWidth = function(_, text) return #text * 6 end,
|
||||
fillRect = function(x, y, w, h) drawn.strip = {x, y, w, h} end,
|
||||
drawText = function(_, x, _, text) drawn[#drawn + 1] = {text = text, x = x} end,
|
||||
}
|
||||
|
||||
input = {getButtons = function() return roles end}
|
||||
|
||||
local hints = require("hints")
|
||||
|
||||
local height = hints.draw({back = "Close", confirm = "Select", up = "Scroll"})
|
||||
assert(height == 14, "height covers the font plus padding")
|
||||
assert(drawn.strip[2] == 226, "the strip sits at the bottom by default")
|
||||
|
||||
-- "up" is labelled but this device lacks it, so it never reaches the strip.
|
||||
assert(#drawn == 2, "only labelled roles the device reports are drawn")
|
||||
assert(drawn[1].text == "Close" and drawn[2].text == "Select", "reading order, not action order")
|
||||
assert(drawn[1].x == 60 and drawn[2].x == 207, "each label centres in its slot")
|
||||
|
||||
drawn = {}
|
||||
roles = {}
|
||||
assert(hints.draw({confirm = "Select"}) == 14, "a device with no buttons still clears the strip")
|
||||
assert(#drawn == 0 and drawn.strip, "nothing is labelled, but the strip is cleared")
|
||||
|
||||
print("ok")
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package.path = "./lua/lib/?.lua;" .. package.path
|
||||
|
||||
local files = {}
|
||||
local cleared, invalidated = nil, {}
|
||||
|
||||
fs = {
|
||||
readFile = function(path, maxBytes)
|
||||
local value = files[path]
|
||||
if value and #value > maxBytes then return nil, "too large" end
|
||||
return value, value and nil or "missing"
|
||||
end,
|
||||
writeFile = function(path, value)
|
||||
files[path] = value
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
gui = {
|
||||
FONT_SMALL = 0,
|
||||
FONT_UI = 1,
|
||||
FONT_BODY = 2,
|
||||
FONT_LARGE = 3,
|
||||
STYLE_NORMAL = 0,
|
||||
STYLE_BOLD = 1,
|
||||
color = function(r, g, b) return r * 65536 + g * 256 + b end,
|
||||
getWidth = function() return 320 end,
|
||||
getHeight = function() return 480 end,
|
||||
getTextWidth = function(_, text) return #text * 6 end,
|
||||
getFontHeight = function() return 8 end,
|
||||
clear = function(color) cleared = color end,
|
||||
}
|
||||
|
||||
local nodes, focus, painter = {}, nil, nil
|
||||
local buttonCount = 0
|
||||
|
||||
local function interactiveNodes()
|
||||
local result = {}
|
||||
for id, entry in ipairs(nodes) do
|
||||
if entry.interactive then result[#result + 1] = id end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
node = {
|
||||
reset = function()
|
||||
nodes, focus, buttonCount = {}, nil, 0
|
||||
end,
|
||||
create = function(parent, spec)
|
||||
local id = #nodes + 1
|
||||
local x = 0
|
||||
if spec.type == "button" then
|
||||
buttonCount = buttonCount + 1
|
||||
x = (buttonCount - 1) * 100
|
||||
end
|
||||
nodes[id] = {
|
||||
parent = parent,
|
||||
type = spec.type,
|
||||
label = spec.label,
|
||||
interactive = spec.interactive,
|
||||
rect = {x, 0, 90, 50},
|
||||
pressed = false,
|
||||
style = {},
|
||||
}
|
||||
return id
|
||||
end,
|
||||
attach = function(parent, child) nodes[child].parent = parent end,
|
||||
setSize = function() end,
|
||||
setStyle = function(id, style)
|
||||
for key, value in pairs(style) do nodes[id].style[key] = value end
|
||||
end,
|
||||
layout = function() return true end,
|
||||
dropScratch = function() end,
|
||||
hit = function(_, x, y)
|
||||
for _, id in ipairs(interactiveNodes()) do
|
||||
local rect = nodes[id].rect
|
||||
if x >= rect[1] and x < rect[1] + rect[3] and y >= rect[2] and y < rect[2] + rect[4] then
|
||||
return id
|
||||
end
|
||||
end
|
||||
end,
|
||||
getRect = function(id) return table.unpack(nodes[id].rect) end,
|
||||
setLabel = function(id, text) nodes[id].label = text end,
|
||||
getLabel = function(id) return nodes[id].label end,
|
||||
invalidate = function(id) invalidated[#invalidated + 1] = id end,
|
||||
setPressed = function(id, pressed) nodes[id].pressed = pressed end,
|
||||
isPressed = function(id) return nodes[id].pressed end,
|
||||
setPainter = function(callback) painter = callback end,
|
||||
draw = function()
|
||||
if painter then
|
||||
for id, entry in ipairs(nodes) do
|
||||
if entry.type == "custom" then painter(id, table.unpack(entry.rect)) end
|
||||
end
|
||||
end
|
||||
end,
|
||||
focusFirst = function()
|
||||
focus = interactiveNodes()[1]
|
||||
return focus
|
||||
end,
|
||||
setFocus = function(id) focus = id end,
|
||||
getFocus = function() return focus end,
|
||||
moveFocus = function(_, direction)
|
||||
local items, current = interactiveNodes(), nil
|
||||
for index, id in ipairs(items) do
|
||||
if id == focus then current = index break end
|
||||
end
|
||||
local step = (direction == "right" or direction == "down") and 1 or -1
|
||||
local nextIndex = current and current + step or 1
|
||||
if items[nextIndex] then focus = items[nextIndex] end
|
||||
return focus
|
||||
end,
|
||||
}
|
||||
|
||||
local ui = require("ui")
|
||||
|
||||
assert(ui.getTheme() == "light")
|
||||
assert(table.concat(ui.themeNames(), ",") == "dark,light,mono")
|
||||
local ok, err = ui.setTheme("missing")
|
||||
assert(ok == nil and err == "Unknown theme")
|
||||
assert(ui.setTheme("dark") == true)
|
||||
assert(files["/.lua/theme"] == "dark" and ui.getTheme() == "dark")
|
||||
|
||||
local events = {}
|
||||
local function handler(name)
|
||||
return function(id, x, y)
|
||||
events[#events + 1] = {name, id, x, y}
|
||||
end
|
||||
end
|
||||
|
||||
local first = ui.button{
|
||||
label = "one",
|
||||
on_enter = handler("enter"),
|
||||
on_exit = handler("exit"),
|
||||
on_click = handler("click"),
|
||||
}
|
||||
local second = ui.button{
|
||||
label = "two",
|
||||
on_enter = handler("enter"),
|
||||
on_exit = handler("exit"),
|
||||
on_click = handler("click"),
|
||||
}
|
||||
local screen = ui.screen(ui.box{row = true, first, second})
|
||||
|
||||
assert(screen:down(10, 10))
|
||||
assert(node.isPressed(first))
|
||||
assert(screen:move(95, 10))
|
||||
assert(not node.isPressed(first))
|
||||
assert(screen:move(10, 10))
|
||||
assert(node.isPressed(first))
|
||||
assert(screen:up(10, 10))
|
||||
assert(not node.isPressed(first))
|
||||
|
||||
local expectedTouch = {"enter", "exit", "enter", "exit", "click"}
|
||||
for index, name in ipairs(expectedTouch) do
|
||||
local event = events[index]
|
||||
assert(event and event[1] == name and event[2] == first)
|
||||
assert(event[3] == 10 or event[3] == 95)
|
||||
assert(event[4] == 10)
|
||||
end
|
||||
|
||||
events = {}
|
||||
assert(screen:button("right", true))
|
||||
assert(screen:button("right", false))
|
||||
assert(node.getFocus() == first)
|
||||
assert(screen:button("right", true))
|
||||
assert(node.getFocus() == second)
|
||||
assert(screen:button("confirm", true))
|
||||
assert(node.isPressed(second))
|
||||
assert(screen:button("confirm", false))
|
||||
assert(not node.isPressed(second))
|
||||
assert(screen:button("back", true) == false)
|
||||
|
||||
local expectedButtons = {
|
||||
{"enter", first},
|
||||
{"exit", first},
|
||||
{"enter", second},
|
||||
{"click", second},
|
||||
}
|
||||
for index, expected in ipairs(expectedButtons) do
|
||||
local event = events[index]
|
||||
assert(event and event[1] == expected[1] and event[2] == expected[2])
|
||||
assert(event[3] == nil and event[4] == nil)
|
||||
end
|
||||
|
||||
local before = #invalidated
|
||||
assert(ui.setTheme("mono") == true)
|
||||
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
||||
assert(cleared == ui.theme.background)
|
||||
|
||||
screen:draw()
|
||||
print("ok")
|
||||
Reference in New Issue
Block a user