Compare commits
16 Commits
main
..
app-tables
| Author | SHA1 | Date | |
|---|---|---|---|
| a09bcfaf06 | |||
| eac4084f80 | |||
| 94d298dd45 | |||
| 2046938d32 | |||
| e5ed980294 | |||
| 26f3fdb6e6 | |||
| 772618ef89 | |||
| b9f7c9347c | |||
| 75b3a2c490 | |||
| 445a9b2b8f | |||
| 4b89b27947 | |||
| 25b576a3d9 | |||
| 2fc3abc487 | |||
| 3f26c4ff10 | |||
| e3153f57c5 | |||
| e85adfa757 |
@@ -4,11 +4,28 @@ This is a clean contract for repositories under the same owner's control. Choose
|
|||||||
API without preserving old names, signatures, or behavior; consumers migrate to the contract.
|
API without preserving old names, signatures, or behavior; consumers migrate to the contract.
|
||||||
|
|
||||||
Every firmware implements all declarations under `lua/api/core/`. Optional hardware contracts
|
Every firmware implements all declarations under `lua/api/core/`. Optional hardware contracts
|
||||||
live under `lua/api/features/`; `sys.hasFeature(name)` guarantees the complete matching contract.
|
live under `lua/api/features/`, as one file or one directory per feature; `sys.hasFeature(name)`
|
||||||
|
guarantees the complete matching contract. Every namespace belongs to exactly one feature or to
|
||||||
|
core, so a feature is a provider pointer rather than a claim to validate: a panel is the `screen`
|
||||||
|
feature (`screen` and `tree`, including the saved rotation and theme), registered only when the
|
||||||
|
firmware supplies a `GuiProvider`, and calibration is `touch.setCalibration()`.
|
||||||
|
The runtime owns what survives a teardown and nothing else: `sys.startApp(path, args)` closes the
|
||||||
|
`lua_State`, loads a path, and hands the next one its arguments as JSON. Routing, history, titles
|
||||||
|
and data directories are that Lua file's, because a back stack that cannot outlive the VM is not a
|
||||||
|
back stack, and everything else about where an app came from can. Encoding happens in the state
|
||||||
|
that still holds the table, so unencodable arguments raise at the call rather than stranding a
|
||||||
|
launch.
|
||||||
|
A global namespace is a provider contract a firmware implements; anything this library provides
|
||||||
|
itself is a module instead, so `require` and globals divide by who supplies the code. Those live in
|
||||||
|
`native/src/bindings/lib/` and register into `package.preload`, which puts them ahead of the SD-card
|
||||||
|
searcher so nothing shadows an implementation. Pure-Lua modules stay in `lua/lib/`, where shadowing
|
||||||
|
is allowed because a module there is self-contained: it belongs in `lua/lib/` only if replacing it
|
||||||
|
can break nothing but itself.
|
||||||
Lua-language sources stay under `lua/`; C/C++ and the vendored interpreter stay under `native/`.
|
Lua-language sources stay under `lua/`; C/C++ and the vendored interpreter stay under `native/`.
|
||||||
Apps are fully trusted; keep permissions and sandboxing out of scope.
|
Apps are fully trusted; keep permissions and sandboxing out of scope.
|
||||||
Firmware commits dirty display content and owns panel refresh policy. Binding annotations under
|
Firmware commits dirty display content and owns panel refresh policy. Binding annotations under
|
||||||
`native/src/bindings/` generate matching `lua/api/` files; regenerate instead of editing files
|
`native/src/bindings/` generate matching `lua/api/` files; regenerate instead of editing files
|
||||||
marked generated. Each `@lua-module`/`@lua-augment` directive sits immediately above the
|
marked generated. Callbacks are fields on the table an app returns, so a `@lua-app` block generates
|
||||||
|
a class an app composes (`---@class PaintApp : App, TouchHandlers`) rather than global functions. Each `@lua-module`/`@lua-augment` directive sits immediately above the
|
||||||
`luaL_Reg` table it describes, which is how one source declares several namespaces. This repository owns portable `lua/lib/` modules; shared UI owns theme application and persistence. Use `ble` for BLE/GATT
|
`luaL_Reg` table it describes, which is how one source declares several namespaces. This repository owns portable `lua/lib/` modules; shared UI owns theme application and persistence. Use `ble` for BLE/GATT
|
||||||
and reserve `bt` for a future Classic Bluetooth contract.
|
and reserve `bt` for a future Classic Bluetooth contract.
|
||||||
|
|||||||
-174
@@ -1,174 +0,0 @@
|
|||||||
# Current API Landscape
|
|
||||||
|
|
||||||
Snapshot taken 2026-08-03 from the generated LuaLS stubs in Slate32 and CrossPoint Reader.
|
|
||||||
This inventory explains the gaps between the current firmwares and the contracts under
|
|
||||||
`lua/api/core/` and `lua/api/features/`.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
| | LCD / Slate32 | E-ink / CrossPoint |
|
|
||||||
|---|---:|---:|
|
|
||||||
| Declared functions | 77 | 67 |
|
|
||||||
| Names present in both | 27 | 27 |
|
|
||||||
| Same parameter list | 21 | 21 |
|
|
||||||
| Same name, different parameter list | 6 | 6 |
|
|
||||||
|
|
||||||
Matching declarations do not guarantee matching behavior. Important differences remain in
|
|
||||||
filesystem limits and ordering, TLS verification, network ownership, colors, and lifecycle.
|
|
||||||
|
|
||||||
## Original Common Ground
|
|
||||||
|
|
||||||
These names and parameter lists matched before the clean contract was defined. They are useful
|
|
||||||
implementation inventory, not constraints on `lua/api/core/`.
|
|
||||||
|
|
||||||
| Namespace | Functions | Remaining decisions |
|
|
||||||
|---|---|---|
|
|
||||||
| `http` | `get`, `head`, `delete`, `post`, `patch`, `download`, `urlencode` | The contract now returns `HttpResponse|nil, error`, requires a response `maxBytes`, and authenticates TLS. |
|
|
||||||
| `fs` | `exists`, `readFile`, `writeFile`, `listFiles`, `listDirs` | Standardize the read cap, sorted listings, path validation, and failure returns. |
|
|
||||||
| `log` | `debug`, `info`, `error` | Mostly ready. |
|
|
||||||
| `sys` | `delay`, `setTickInterval` | The clean contract drops both; callback timers replace periodic ticks and blocking delays. |
|
|
||||||
| `wifi` | `disconnect`, `isConnected` | Decide whether disconnect ownership is app-specific or device-wide. |
|
|
||||||
|
|
||||||
The basic `gui` drawing primitives are also intended for core. Colors will be opaque,
|
|
||||||
platform-native integers produced by `gui.color(r, g, b)`: RGB565 on LCD and quantized grayscale
|
|
||||||
on e-ink. Apps that need to be portable use `gui.color` rather than hard-coded values.
|
|
||||||
|
|
||||||
## Same Name, Different Signature
|
|
||||||
|
|
||||||
| Function | LCD | E-ink |
|
|
||||||
|---|---|---|
|
|
||||||
| `gui.clear` | `(color)` | `()` |
|
|
||||||
| `gui.drawLine` | `(x1, y1, x2, y2, color)` | `(x1, y1, x2, y2, width, color)` |
|
|
||||||
| `gui.drawText` | `(text, x, y, color, bg)` | `(font, x, y, text, color, style)` |
|
|
||||||
| `gui.fillCircle` | `(x, y, radius, color, bg)` | `(cx, cy, radius, color)` |
|
|
||||||
| `gui.getTextWidth` | `(text)` | `(font, text, style)` |
|
|
||||||
| `wifi.connect` | `(ssid, password)` | `()` using stored credentials |
|
|
||||||
|
|
||||||
These GUI differences are current implementation drift, not permanent display features. Wi-Fi
|
|
||||||
is also generic drift: both connection forms are useful, so the eventual shared signature can
|
|
||||||
make credentials optional after both firmwares implement both behaviors.
|
|
||||||
|
|
||||||
## Agreed GUI Direction
|
|
||||||
|
|
||||||
Basic geometry and text measurement belong in core with identical signatures:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
gui.getWidth()
|
|
||||||
gui.getHeight()
|
|
||||||
gui.color(r, g, b)
|
|
||||||
|
|
||||||
gui.clear(color?)
|
|
||||||
gui.fillRect(x, y, w, h, color)
|
|
||||||
gui.drawRect(x, y, w, h, color)
|
|
||||||
gui.drawLine(x1, y1, x2, y2, color, width?)
|
|
||||||
gui.fillCircle(x, y, radius, color, background?)
|
|
||||||
|
|
||||||
gui.getTextWidth(font, text, style?)
|
|
||||||
gui.getFontHeight(font, style?)
|
|
||||||
gui.drawText(font, x, y, text, color?, style?, background?)
|
|
||||||
```
|
|
||||||
|
|
||||||
Fonts are opaque, zero-based integer IDs selected through shared semantic constants:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
gui.FONT_SMALL
|
|
||||||
gui.FONT_UI
|
|
||||||
gui.FONT_BODY
|
|
||||||
gui.FONT_LARGE
|
|
||||||
```
|
|
||||||
|
|
||||||
Each implementation maps those roles to its available fonts or scales. Portable apps use the
|
|
||||||
named constants and metric functions rather than literal IDs or assumed pixel dimensions.
|
|
||||||
Physical names such as Bookerly and Noto Sans remain legacy implementation APIs, not part of
|
|
||||||
the core or e-ink feature contracts.
|
|
||||||
|
|
||||||
The optional background describes the surface behind anti-aliased output; it can have no visible
|
|
||||||
effect on a renderer without partial edge pixels. E-ink refresh policy and LCD live-frame
|
|
||||||
behavior remain feature-specific.
|
|
||||||
|
|
||||||
## Generic Features to Converge
|
|
||||||
|
|
||||||
These are not inherently LCD or e-ink concerns and should not be permanently assigned to a
|
|
||||||
display feature.
|
|
||||||
|
|
||||||
### Present only on E-ink
|
|
||||||
|
|
||||||
- Filesystem mutation and paging: `fileSize`, `mkdir`, `readLineAt`, `remove`, `removeTree`, `rename`.
|
|
||||||
These are intentionally under `lua/api/core/`; Slate32 must still implement them.
|
|
||||||
- Independent timers: `timer.after`, `timer.every`, `timer.cancel`
|
|
||||||
- `sys.uptime` / `sys.millis` naming
|
|
||||||
|
|
||||||
### Present only on LCD
|
|
||||||
|
|
||||||
- App routing: `sys.launch`, `sys.replace`, `sys.back`
|
|
||||||
- Runtime state: `sys.getAppName`, `setAppName`, `getMemory`, `isClockSynced`
|
|
||||||
- Wi-Fi management: `scan`, explicit credential connection, `forget`
|
|
||||||
- SD-backed `require` and app-local modules (runtime behavior, not a binding declaration)
|
|
||||||
- Native compact widget tree in `node.*`
|
|
||||||
|
|
||||||
The contract intentionally includes all of these generic capabilities. Both firmwares are under
|
|
||||||
our control, so they migrate to the clean API without compatibility aliases. The compact node
|
|
||||||
tree remains useful on e-ink for layout, painting, invalidation, and directional focus. Focus is
|
|
||||||
tree-level state with a deliberate appearance; moving it invalidates only the old and new nodes.
|
|
||||||
|
|
||||||
## Feature Contracts
|
|
||||||
|
|
||||||
- `buttons` guarantees physical-button polling/callbacks and button hints.
|
|
||||||
- `touch` guarantees calibrated/raw coordinates, touch callbacks, and calibration persistence.
|
|
||||||
|
|
||||||
A feature exists only where an app can call something a device may not have. Panel technology is
|
|
||||||
not such a case: firmware-owned publication and waveform policy already hold for every display, and
|
|
||||||
`gui.setFullscreen()` and `gui.roundRect()` are core, with a panel that has no gradient flattening
|
|
||||||
it exactly as `gui.color()` quantizes to grayscale. So the display is never a feature, and input is
|
|
||||||
never inferred from it. BLE/GATT remains
|
|
||||||
required core `ble`; a future `bt` namespace is reserved for Classic Bluetooth. Generic BMP,
|
|
||||||
polygon, shape, rotation, color, and text operations are core. Rotation and timezone settings are
|
|
||||||
also core, while theme persistence and application belong to shared `ui.lua`.
|
|
||||||
|
|
||||||
## Lifecycle Differences
|
|
||||||
|
|
||||||
| Concern | LCD | E-ink |
|
|
||||||
|---|---|---|
|
|
||||||
| Entry | required `init(arg)` | required `init()` |
|
|
||||||
| Navigation | Lua route stack | exit to C++ launcher |
|
|
||||||
| Input callbacks | touch down/move/up/tap | button callback and polling |
|
|
||||||
| Publication | drawing is immediately visible | apps currently call explicit refresh |
|
|
||||||
| Modules | app-local and shared `require` | effectively single-file apps |
|
|
||||||
|
|
||||||
The contract standardizes `init(arg?)`, navigation, and module loading. Optional `draw(deltaMs)`
|
|
||||||
runs once after initialization and then at most 30 FPS, best effort. The host passes monotonic
|
|
||||||
elapsed milliseconds, with zero on the first frame. Callback timers replace `on_tick`. Input
|
|
||||||
callbacks remain feature-specific. Firmware commits dirty content after callback batches and owns panel
|
|
||||||
refresh policy, including e-ink waveforms. `sys.getAPIVersion()` identifies the integer contract
|
|
||||||
version implemented by the firmware.
|
|
||||||
|
|
||||||
## Contract Rules
|
|
||||||
|
|
||||||
1. This is a clean API. Backward compatibility has no weight because all consumers are controlled
|
|
||||||
and migrate with the implementations.
|
|
||||||
2. A core function matches in name, parameters, returns, and observable behavior.
|
|
||||||
3. GUI coordinates use a top-left origin, including text; implementations translate driver
|
|
||||||
baselines and orientation details.
|
|
||||||
4. Invalid arguments raise Lua errors. Runtime failures return `nil, error`; filesystem reads
|
|
||||||
take an explicit `maxBytes` up to 64 KiB and fail rather than truncate. `fs.writeFile()` is
|
|
||||||
atomic: failure leaves the previous contents intact.
|
|
||||||
5. Apps live under `/.lua/apps`, shared modules under `/.lua/lib`, and persistent app data under
|
|
||||||
`/.lua/data/<AppId>`. Launch paths name directories relative to `apps`; firmware appends
|
|
||||||
`main.lua`. The first path component is the immutable app ID and determines `getAppDataPath()`.
|
|
||||||
6. `node` owns one focused ID outside its 16-byte nodes. Directional movement is geometric and
|
|
||||||
non-wrapping, with a distinct focused appearance.
|
|
||||||
7. `draw(deltaMs)` is the optional frame loop, capped at 30 FPS; the host supplies elapsed
|
|
||||||
monotonic milliseconds and callback-based `timer.after/every` handle periodic work.
|
|
||||||
8. Firmware commits dirty display content and owns panel refresh policy; no refresh API is
|
|
||||||
exposed to apps.
|
|
||||||
9. Apps are fully trusted; the contract adds no permissions or sandbox.
|
|
||||||
10. BLE/GATT is core `ble`; Classic Bluetooth remains undefined rather than sharing an inaccurate
|
|
||||||
namespace.
|
|
||||||
11. This repository owns portable modules under `lua/lib/`, including global theme behavior and the
|
|
||||||
declarative widget toolkit in `ui.lua`. Widgets expose only `on_enter`, `on_exit`, and
|
|
||||||
`on_click`: touch and directional focus map onto the same active-state lifecycle.
|
|
||||||
12. HTTPS authenticates certificates.
|
|
||||||
13. Generated firmware stubs are checked against this repository rather than copied here as
|
|
||||||
competing sources of truth.
|
|
||||||
14. Feature contracts contain only their declared hardware capability.
|
|
||||||
15. Runtime smoke tests cover semantics that LuaLS declarations cannot express.
|
|
||||||
@@ -4,15 +4,19 @@ CC ?= cc
|
|||||||
CXX ?= c++
|
CXX ?= c++
|
||||||
AR ?= ar
|
AR ?= ar
|
||||||
|
|
||||||
CONTRACTS := $(sort $(wildcard lua/api/core/*.lua lua/api/features/*.lua))
|
CONTRACTS := $(sort $(wildcard lua/api/core/*.lua lua/api/features/*.lua lua/api/features/*/*.lua \
|
||||||
|
lua/api/lib/*.lua))
|
||||||
LUA_C := $(sort $(wildcard native/src/vendor/lua/*.c))
|
LUA_C := $(sort $(wildcard native/src/vendor/lua/*.c))
|
||||||
LUA_OBJECTS := $(patsubst native/src/vendor/lua/%.c,_build/lua/%.o,$(LUA_C))
|
LUA_OBJECTS := $(patsubst native/src/vendor/lua/%.c,_build/lua/%.o,$(LUA_C))
|
||||||
LUA_LIBRARY := _build/liblua.a
|
LUA_LIBRARY := _build/liblua.a
|
||||||
|
CJSON_C := $(sort $(wildcard native/src/vendor/cjson/*.c))
|
||||||
|
CJSON_OBJECTS := $(patsubst native/src/vendor/cjson/%.c,_build/cjson/%.o,$(CJSON_C))
|
||||||
LUA_SMOKE := _build/lua-smoke
|
LUA_SMOKE := _build/lua-smoke
|
||||||
RUNTIME_TEST := _build/runtime-test
|
RUNTIME_TEST := _build/runtime-test
|
||||||
LUAC32 := _build/luac32
|
LUAC32 := _build/luac32
|
||||||
RUNTIME_CPP := $(sort $(wildcard native/src/runtime/*.cpp native/src/bindings/core/*.cpp \
|
RUNTIME_CPP := $(sort $(wildcard native/src/runtime/*.cpp native/src/bindings/core/*.cpp \
|
||||||
native/src/bindings/features/*.cpp) native/src/embedded_modules.cpp)
|
native/src/bindings/lib/*.cpp \
|
||||||
|
native/src/bindings/features/*.cpp native/src/bindings/features/*/*.cpp) native/src/embedded_modules.cpp)
|
||||||
|
|
||||||
.PHONY: api test embed compiledb format
|
.PHONY: api test embed compiledb format
|
||||||
|
|
||||||
@@ -46,9 +50,9 @@ test: $(LUA_SMOKE) $(RUNTIME_TEST) $(LUAC32)
|
|||||||
$(LUA_SMOKE): native/test/lua_smoke.c $(LUA_LIBRARY)
|
$(LUA_SMOKE): native/test/lua_smoke.c $(LUA_LIBRARY)
|
||||||
@$(CC) -std=c99 -DLUA_32BITS -I native/src/vendor/lua $< $(LUA_LIBRARY) -lm -ldl -o $@
|
@$(CC) -std=c99 -DLUA_32BITS -I native/src/vendor/lua $< $(LUA_LIBRARY) -lm -ldl -o $@
|
||||||
|
|
||||||
$(RUNTIME_TEST): $(RUNTIME_CPP) native/test/runtime_test.cpp $(LUA_LIBRARY)
|
$(RUNTIME_TEST): $(RUNTIME_CPP) native/test/runtime_test.cpp $(LUA_LIBRARY) $(CJSON_OBJECTS)
|
||||||
@$(CXX) -std=c++11 -Wall -Wextra -DLUA_32BITS -I native/include -I native/src/vendor/lua \
|
@$(CXX) -std=c++11 -Wall -Wextra -DLUA_32BITS -I native/include -I native/src/vendor/lua \
|
||||||
$(RUNTIME_CPP) native/test/runtime_test.cpp $(LUA_LIBRARY) -lm -ldl -o $@
|
$(RUNTIME_CPP) native/test/runtime_test.cpp $(CJSON_OBJECTS) $(LUA_LIBRARY) -lm -ldl -o $@
|
||||||
|
|
||||||
$(LUA_LIBRARY): $(LUA_OBJECTS)
|
$(LUA_LIBRARY): $(LUA_OBJECTS)
|
||||||
@$(AR) rcs $@ $^
|
@$(AR) rcs $@ $^
|
||||||
@@ -59,3 +63,8 @@ $(LUAC32): tools/dump.c $(LUA_LIBRARY)
|
|||||||
_build/lua/%.o: native/src/vendor/lua/%.c
|
_build/lua/%.o: native/src/vendor/lua/%.c
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
@$(CC) -std=c99 -DLUA_32BITS -I native/src/vendor/lua -c $< -o $@
|
@$(CC) -std=c99 -DLUA_32BITS -I native/src/vendor/lua -c $< -o $@
|
||||||
|
|
||||||
|
# gnu99 rather than c99: lua-cjson calls strncasecmp, which is POSIX rather than ISO C.
|
||||||
|
_build/cjson/%.o: native/src/vendor/cjson/%.c
|
||||||
|
@mkdir -p $(@D)
|
||||||
|
@$(CC) -std=gnu99 -DLUA_32BITS -I native/src/vendor/lua -I native/src/vendor/cjson -c $< -o $@
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ A future shared contract for Lua applications running on the ESP32 firmwares in
|
|||||||
`../slate32` and `../crosspoint-reader`.
|
`../slate32` and `../crosspoint-reader`.
|
||||||
|
|
||||||
Lua declarations and shared modules live under `lua/`; the vendored interpreter and shared C/C++
|
Lua declarations and shared modules live under `lua/`; the vendored interpreter and shared C/C++
|
||||||
runtime live under `native/`. [`LANDSCAPE.md`](LANDSCAPE.md) records how the contract differs from
|
runtime live under `native/`. Runtime bindings remain authoritative until this repository is wired
|
||||||
the current firmwares. Runtime bindings remain authoritative until this repository is wired into
|
into their tests.
|
||||||
their tests.
|
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -26,10 +25,14 @@ native/
|
|||||||
```
|
```
|
||||||
|
|
||||||
Every firmware implements all files under `lua/api/core/`. `sys.hasFeature(name)` declares
|
Every firmware implements all files under `lua/api/core/`. `sys.hasFeature(name)` declares
|
||||||
optional features; claiming one guarantees every API and behavior in its matching file. Features
|
optional features; claiming one guarantees every API and behavior in its matching file or
|
||||||
compose, so a device may expose both `touch` and `buttons`. Display technology is not a feature:
|
directory. Features compose, so a device may expose both `touch` and `buttons`. A panel is the
|
||||||
an e-ink `GuiProvider` flattens a gradient the way `gui.color()` quantizes to grayscale, and the
|
`screen` feature -- the `screen` and `tree` namespaces, including the saved rotation and theme --
|
||||||
firmware owns publication and waveform policy on every panel.
|
because a headless firmware supplies no `GuiProvider`. Every namespace belongs to exactly one
|
||||||
|
feature or to core, which is why touch calibration is `touch.setCalibration()` rather than a
|
||||||
|
shared settings namespace three features write to. Display technology is still not a feature:
|
||||||
|
an e-ink `GuiProvider` flattens a gradient the way `screen.color()` quantizes to grayscale, and
|
||||||
|
the firmware owns publication and waveform policy on every panel.
|
||||||
|
|
||||||
The contract is the app-facing Lua API, not the provider C++ interface. Shared binding
|
The contract is the app-facing Lua API, not the provider C++ interface. Shared binding
|
||||||
registrations carry LuaLS annotations; `tools/gen_api.py` mirrors them into `lua/api/`. Generated
|
registrations carry LuaLS annotations; `tools/gen_api.py` mirrors them into `lua/api/`. Generated
|
||||||
@@ -37,6 +40,12 @@ files are committed for editors and checked for drift by `make test`, so a names
|
|||||||
by the code that registers it, including the callbacks in `core/runtime.lua` and the feature files,
|
by the code that registers it, including the callbacks in `core/runtime.lua` and the feature files,
|
||||||
which are generated from the `Runtime::call*` sites that fire them.
|
which are generated from the `Runtime::call*` sites that fire them.
|
||||||
|
|
||||||
|
Callbacks are fields on the table an app returns, not globals, so each `@lua-app` block generates a
|
||||||
|
class rather than loose functions: `App` for the core contract, `TouchHandlers` and `ButtonHandlers`
|
||||||
|
alongside the namespaces they belong to. An app composes the ones it implements
|
||||||
|
(`---@class PaintApp : App, TouchHandlers`), which is as close to per-device stubs as static
|
||||||
|
declarations get -- what a firmware actually provides is still `sys.hasFeature()` at runtime.
|
||||||
|
|
||||||
Nothing under `lua/api/` ever runs: it is `---@meta` for editors and the drift check. `lua/lib/`
|
Nothing under `lua/api/` ever runs: it is `---@meta` for editors and the drift check. `lua/lib/`
|
||||||
is the opposite -- real modules that ship to the SD card, so composition like `ui.lua` and
|
is the opposite -- real modules that ship to the SD card, so composition like `ui.lua` and
|
||||||
`hints.lua` changes without a reflash.
|
`hints.lua` changes without a reflash.
|
||||||
@@ -47,44 +56,48 @@ provider is how `sys.hasFeature()` answers false, and its namespace additions ar
|
|||||||
registered.
|
registered.
|
||||||
|
|
||||||
The declarations are a clean target, not the intersection of today's APIs. Existing apps and
|
The declarations are a clean target, not the intersection of today's APIs. Existing apps and
|
||||||
firmwares migrate to it without compatibility aliases. Safe filesystem mutation, `node`, app
|
firmwares migrate to it without compatibility aliases. Safe filesystem mutation, app
|
||||||
navigation, module loading, and `ble` are core even where a firmware does not implement them yet.
|
navigation, module loading, and `ble` are core even where a firmware does not implement them yet.
|
||||||
Every app may use `require`; modules resolve from its app directory and `/.lua/lib`. This repository
|
Every app may use `require`; the entry file points `package.path` wherever it keeps apps and
|
||||||
owns portable shared modules such as `ui.lua`; firmware-specific modules stay with their firmware.
|
modules. This repository owns portable shared modules such as `ui.lua`; firmware-specific modules
|
||||||
|
stay with their firmware. The tree below is a convention of the Lua that boots, not something the
|
||||||
|
runtime knows -- it loads the one path it is given.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/.lua/
|
/.lua/
|
||||||
|
main.lua the entry file a firmware boots
|
||||||
apps/<AppId>/main.lua
|
apps/<AppId>/main.lua
|
||||||
apps/<AppId>/<Subapp>/main.lua
|
|
||||||
data/<AppId>/
|
data/<AppId>/
|
||||||
lib/<module>.lua
|
lib/<module>.lua
|
||||||
```
|
```
|
||||||
|
|
||||||
The runtime owns app loading: `Runtime::startApp()` opens a fresh `lua_State`, points `require` at
|
The runtime owns the teardown and nothing above it. `sys.startApp(path, args)` closes the
|
||||||
the app directory and `/.lua/lib`, runs the chunk, and calls `init(arg)`. `sys.launch`/`replace`/
|
`lua_State`, opens a fresh one, loads a path, and calls `start(args)` on the table it returns.
|
||||||
`back` only record intent, because swapping the state inside a callback would free the VM that is
|
The arguments cross as JSON, because the table they came from dies with the state that built it;
|
||||||
still executing; the firmware calls `applyPendingNavigation()` between batches. History, the
|
encoding happens while that state still lives, so an argument JSON cannot carry raises at the call
|
||||||
launcher fallback, app identity, and `sys.hasFeature()` all live there too, which is why
|
rather than stranding a launch.
|
||||||
|
|
||||||
|
That is the whole of navigation. Routing, history, titles and data directories are decided by the
|
||||||
|
Lua file a firmware boots, since the only thing that structurally cannot live there is a value
|
||||||
|
that has to outlive the VM -- and the arguments are that value. `sys.startApp` records intent and
|
||||||
|
returns, because swapping the state inside a callback would free the VM still executing it; the
|
||||||
|
firmware calls `applyPendingNavigation()` between batches. With app identity gone from C++,
|
||||||
`SysProvider` is down to `millis`, `memory`, and `isClockSynced`.
|
`SysProvider` is down to `millis`, `memory`, and `isClockSynced`.
|
||||||
|
|
||||||
`sys.launch("Settings/Calibration")` resolves the nested `main.lua`; only top-level apps appear in
|
`draw(deltaMs)` is an optional frame loop called once after `start()` and then at most 30 FPS,
|
||||||
the launcher. The first path component is the immutable app ID, so every Settings route shares
|
|
||||||
`sys.getAppDataPath()` and `/.lua/data/Settings`. Package updates do not touch persistent data.
|
|
||||||
|
|
||||||
`draw(deltaMs)` is an optional frame loop called once after `init()` and then at most 30 FPS,
|
|
||||||
best effort. The host passes monotonic elapsed milliseconds (`0` on the first frame). Timers take
|
best effort. The host passes monotonic elapsed milliseconds (`0` on the first frame). Timers take
|
||||||
Lua callbacks and return cancellation handles.
|
Lua callbacks and return cancellation handles.
|
||||||
|
|
||||||
The firmware decides whether an event reaches an app at all -- jitter filtering, chrome, and
|
The firmware decides whether an event reaches an app at all -- jitter filtering, chrome, and
|
||||||
debouncing are its business -- and `Runtime::call*` decides what the app sees. A release fires
|
debouncing are its business -- and `Runtime::call*` decides what the app sees. A release fires
|
||||||
`on_touch_up` then the `on_touch` alias, and a button release fires `on_button_up` then
|
`on_touch_up` then the `on_touch` alias, and a button release fires `on_button_up` then
|
||||||
`on_button`, so the ordering is identical on every device. Only a failed `init()` stops an app;
|
`on_button`, so the ordering is identical on every device. Only a failed `start()` stops an app;
|
||||||
every other callback logs through `LogProvider` and carries on. Calling a feature callback without
|
every other callback logs through `LogProvider` and carries on. Calling a feature callback without
|
||||||
its provider is a wiring bug and says so.
|
its provider is a wiring bug and says so.
|
||||||
|
|
||||||
The firmware commits dirty display content after callback batches and owns e-ink waveform
|
The firmware commits dirty display content after callback batches and owns e-ink waveform
|
||||||
policy; apps do not refresh the panel manually. Apps are fully trusted with the complete core API.
|
policy; apps do not refresh the panel manually. Apps are fully trusted with the complete core API.
|
||||||
Theme persistence and application belong to shared `ui.lua`, not the firmware `settings` binding.
|
Theme application belongs to shared `ui.lua`; `screen.setTheme()` only stores the name.
|
||||||
|
|
||||||
The native library vendors Lua 5.4.8 from GitHub tag `v5.4.8` and compiles it with `LUA_32BITS`.
|
The native library vendors Lua 5.4.8 from GitHub tag `v5.4.8` and compiles it with `LUA_32BITS`.
|
||||||
Command-line and upstream test entry points are excluded; `luaconf.h` carries one documented guard
|
Command-line and upstream test entry points are excluded; `luaconf.h` carries one documented guard
|
||||||
@@ -92,7 +105,7 @@ that lets the build flag select 32-bit number mode.
|
|||||||
|
|
||||||
## Shared UI
|
## Shared UI
|
||||||
|
|
||||||
Portable apps normally use the declarative `ui.lua` toolkit; `node` remains the low-level escape
|
Portable apps normally use the declarative `ui.lua` toolkit; `tree` remains the low-level escape
|
||||||
hatch. The baseline constructors are `screen`, `box`, `spacer`, `text`, `label`, `button`,
|
hatch. The baseline constructors are `screen`, `box`, `spacer`, `text`, `label`, `button`,
|
||||||
`custom`, and `confirm`. A screen accepts both touch and physical-button input.
|
`custom`, and `confirm`. A screen accepts both touch and physical-button input.
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"includeDir": "native/include",
|
"includeDir": "native/include",
|
||||||
"flags": [
|
"flags": [
|
||||||
"-DLUA_32BITS",
|
"-DLUA_32BITS",
|
||||||
"-I native/src/vendor/lua"
|
"-I native/src/vendor/lua",
|
||||||
|
"-I native/src/vendor/cjson"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-6
@@ -2,10 +2,16 @@
|
|||||||
|
|
||||||
-- Generated from native/src/bindings/core/ble.cpp. Do not edit.
|
-- Generated from native/src/bindings/core/ble.cpp. Do not edit.
|
||||||
|
|
||||||
---@class BleDevice
|
---@class BleObservation
|
||||||
---@field name string
|
|
||||||
---@field address string
|
---@field address string
|
||||||
|
---@field name string
|
||||||
---@field rssi integer
|
---@field rssi integer
|
||||||
|
---@field payload string Raw advertisement bytes.
|
||||||
|
---@field lastSeenMs integer sys.getMillis() at last sighting.
|
||||||
|
|
||||||
|
---@class BleFilter
|
||||||
|
---@field services? string[] Service-data UUIDs to keep.
|
||||||
|
---@field manufacturers? integer[] Manufacturer ids to keep.
|
||||||
|
|
||||||
---@class BleLib
|
---@class BleLib
|
||||||
ble = {}
|
ble = {}
|
||||||
@@ -23,11 +29,22 @@ function ble.deinit() end
|
|||||||
---@return boolean
|
---@return boolean
|
||||||
function ble.isInitialized() end
|
function ble.isInitialized() end
|
||||||
|
|
||||||
---Scans for advertising devices.
|
---Starts passively observing advertisements, coalesced per device.
|
||||||
---@param durationMs? integer Defaults to 3000.
|
---@param filter? BleFilter Keep only matching adverts; nil keeps all.
|
||||||
---@return BleDevice[]? devices
|
---@return true? ok
|
||||||
---@return string? error
|
---@return string? error
|
||||||
function ble.scan(durationMs) end
|
function ble.observe(filter) end
|
||||||
|
|
||||||
|
---Stops observing and clears the snapshot.
|
||||||
|
function ble.unobserve() end
|
||||||
|
|
||||||
|
---Whether advertisement observation is running.
|
||||||
|
---@return boolean
|
||||||
|
function ble.isObserving() end
|
||||||
|
|
||||||
|
---The current snapshot of observed devices.
|
||||||
|
---@return BleObservation[] devices
|
||||||
|
function ble.observed() end
|
||||||
|
|
||||||
---Connects to a peripheral.
|
---Connects to a peripheral.
|
||||||
---@param address string
|
---@param address string
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
---@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
|
|
||||||
@@ -2,21 +2,17 @@
|
|||||||
|
|
||||||
-- Generated from native/src/runtime/runtime.cpp. Do not edit.
|
-- Generated from native/src/runtime/runtime.cpp. Do not edit.
|
||||||
|
|
||||||
-- Runtime layout:
|
-- The firmware loads the path it was booted with into every fresh state and calls
|
||||||
-- /.lua/apps/<AppId>/main.lua application entry point
|
-- these on the table it returns. Where apps live, what surrounds them and which of
|
||||||
-- /.lua/apps/<AppId>/<Subapp>/main.lua nested route, omitted from the launcher
|
-- these an app itself sees are all that file's to decide, which is why an app
|
||||||
-- /.lua/data/<AppId>/ persistent app data, preserved across updates
|
-- composes the classes for the features it handles:
|
||||||
-- /.lua/lib/<module>.lua shared require() modules
|
--
|
||||||
-- require() also searches the running application's directory
|
-- ---@class PaintApp : App, TouchHandlers
|
||||||
--
|
--
|
||||||
-- The firmware does not clear the frame before calling draw(), and commits changed
|
-- 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.
|
-- display content after each callback batch using the panel's own refresh policy.
|
||||||
-- Timer callbacks are registered directly with timer.after/every.
|
-- Timer callbacks are registered directly with timer.after/every.
|
||||||
|
|
||||||
---Required. Runs once before the first draw; failing here stops the app.
|
---@class App
|
||||||
---@param arg? string The string passed to sys.launch or sys.replace.
|
---@field start fun(args?: table) Required. Mounts whatever the arguments describe; failing here leaves no app running.
|
||||||
function init(arg) end
|
---@field draw? fun(deltaMs: integer) Optional frame loop, called once after start and then at most 30 FPS, best effort.
|
||||||
|
|
||||||
---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
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
---@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
|
|
||||||
+15
-29
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
-- Generated from native/src/bindings/core/sys.cpp. Do not edit.
|
-- Generated from native/src/bindings/core/sys.cpp. Do not edit.
|
||||||
|
|
||||||
---@alias Feature "touch"|"buttons"
|
---@alias Feature "screen"|"touch"|"buttons"
|
||||||
|
|
||||||
---@class SysLib
|
---@class SysLib
|
||||||
sys = {}
|
sys = {}
|
||||||
@@ -20,34 +20,10 @@ function sys.hasFeature(feature) end
|
|||||||
---@return integer
|
---@return integer
|
||||||
function sys.getMillis() end
|
function sys.getMillis() end
|
||||||
|
|
||||||
---Returns the immutable first path component of the running app.
|
---Tears the runtime down and starts over from a Lua file, which is the only navigation there is: history, titles and where apps live are whatever that file makes of the arguments.
|
||||||
---@return string
|
---@param path string Absolute path to the Lua file to load; traversal is rejected.
|
||||||
function sys.getAppID() end
|
---@param args? table Plain data, carried across the teardown as JSON and handed to start(args). Raises on anything JSON cannot represent.
|
||||||
|
function sys.startApp(path, args) 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.
|
---Returns heap statistics.
|
||||||
---@return integer freeBytes
|
---@return integer freeBytes
|
||||||
@@ -58,3 +34,13 @@ function sys.getMemory() end
|
|||||||
---Whether network time synchronization has completed.
|
---Whether network time synchronization has completed.
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function sys.isClockSynced() end
|
function sys.isClockSynced() end
|
||||||
|
|
||||||
|
---Returns the active POSIX timezone rule.
|
||||||
|
---@return string
|
||||||
|
function sys.getTimezone() end
|
||||||
|
|
||||||
|
---Applies and persists a POSIX timezone rule.
|
||||||
|
---@param timezone string
|
||||||
|
---@return true? ok
|
||||||
|
---@return string? error
|
||||||
|
function sys.setTimezone(timezone) end
|
||||||
|
|||||||
@@ -5,42 +5,39 @@
|
|||||||
---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
|
---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
|
||||||
|
|
||||||
-- Roles, not physical buttons: a device maps whatever hardware it has onto them, and
|
-- 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.
|
-- up/down/left/right are the directions tree.moveFocus already takes.
|
||||||
|
|
||||||
---@class InputLib
|
---@class ButtonsLib
|
||||||
input = input or {}
|
buttons = {}
|
||||||
|
|
||||||
---Returns the roles this device reports, so an app can label only the actions it has.
|
---Returns the roles this device reports, so an app can label only the actions it has.
|
||||||
---@return Button[]
|
---@return Button[]
|
||||||
function input.getButtons() end
|
function buttons.getAll() end
|
||||||
|
|
||||||
---Whether any button is held.
|
---Whether any button is held.
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function input.isAnyPressed() end
|
function buttons.isAnyPressed() end
|
||||||
|
|
||||||
---Whether a button is held.
|
---Whether a button is held.
|
||||||
---@param button Button
|
---@param button Button
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function input.isPressed(button) end
|
function buttons.isPressed(button) end
|
||||||
|
|
||||||
---Whether a button went down since the last poll.
|
---Whether a button went down since the last poll.
|
||||||
---@param button Button
|
---@param button Button
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function input.wasPressed(button) end
|
function buttons.wasPressed(button) end
|
||||||
|
|
||||||
---Whether a button came up since the last poll.
|
---Whether a button came up since the last poll.
|
||||||
---@param button Button
|
---@param button Button
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function input.wasReleased(button) end
|
function buttons.wasReleased(button) end
|
||||||
|
|
||||||
---Fired when a button goes down.
|
-- What an app implements to see buttons, composed into its own class:
|
||||||
---@param button Button
|
--
|
||||||
function on_button_down(button) end
|
-- ---@class MenuApp : App, ButtonHandlers
|
||||||
|
|
||||||
---Fired when a button comes up.
|
---@class ButtonHandlers
|
||||||
---@param button Button
|
---@field onButtonDown? fun(button: Button) Fired when a button goes down.
|
||||||
function on_button_up(button) end
|
---@field onButtonUp? fun(button: Button) Fired when a button comes up.
|
||||||
|
---@field onButton? fun(button: Button) Tap alias, fired on release like a click, after onButtonUp.
|
||||||
---Tap alias, fired on release like a click, after on_button_up.
|
|
||||||
---@param button Button
|
|
||||||
function on_button(button) end
|
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
---@meta
|
||||||
|
|
||||||
|
-- Generated from native/src/bindings/features/screen/screen.cpp. Do not edit.
|
||||||
|
|
||||||
|
-- The panel itself; the widget tree it paints is `tree`, and
|
||||||
|
-- sys.hasFeature("screen") covers both.
|
||||||
|
---@alias ScreenColor integer
|
||||||
|
---@alias ScreenFont integer
|
||||||
|
---@alias ScreenTextStyle integer
|
||||||
|
|
||||||
|
---@class ScreenLib
|
||||||
|
---@field FONT_SMALL ScreenFont Small auxiliary text.
|
||||||
|
---@field FONT_UI ScreenFont Normal controls and labels.
|
||||||
|
---@field FONT_BODY ScreenFont Normal reading text.
|
||||||
|
---@field FONT_LARGE ScreenFont Headings and prominent values.
|
||||||
|
---@field STYLE_NORMAL ScreenTextStyle
|
||||||
|
---@field STYLE_BOLD ScreenTextStyle
|
||||||
|
screen = {}
|
||||||
|
screen.FONT_SMALL = 0
|
||||||
|
screen.FONT_UI = 0
|
||||||
|
screen.FONT_BODY = 0
|
||||||
|
screen.FONT_LARGE = 0
|
||||||
|
screen.STYLE_NORMAL = 0
|
||||||
|
screen.STYLE_BOLD = 0
|
||||||
|
|
||||||
|
---Returns the live frame width.
|
||||||
|
---@return integer
|
||||||
|
function screen.getWidth() end
|
||||||
|
|
||||||
|
---Returns the live frame height.
|
||||||
|
---@return integer
|
||||||
|
function screen.getHeight() end
|
||||||
|
|
||||||
|
---Rotates the panel and persists the choice, so there is one rotation
|
||||||
|
---rather than a live one and a saved one to reconcile.
|
||||||
|
---@param degrees integer 0, 90, 180, or 270 clockwise.
|
||||||
|
---@return true? ok
|
||||||
|
---@return string? error
|
||||||
|
function screen.setRotation(degrees) end
|
||||||
|
|
||||||
|
---Returns the rotation in degrees clockwise.
|
||||||
|
---@return integer
|
||||||
|
function screen.getRotation() end
|
||||||
|
|
||||||
|
---Returns the saved palette name. Apps read ui.getTheme() instead; this
|
||||||
|
---is the stored value, which only ui.setTheme() knows how to apply.
|
||||||
|
---@return string
|
||||||
|
function screen.getTheme() end
|
||||||
|
|
||||||
|
---Persists a palette name without applying it. Call ui.setTheme(), which
|
||||||
|
---writes through here and then rebuilds the palette and repaints.
|
||||||
|
---@param theme string
|
||||||
|
---@return true? ok
|
||||||
|
---@return string? error
|
||||||
|
function screen.setTheme(theme) 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 ScreenColor
|
||||||
|
function screen.color(r, g, b) end
|
||||||
|
|
||||||
|
---Clears the frame.
|
||||||
|
---@param color? ScreenColor Defaults to white.
|
||||||
|
function screen.clear(color) end
|
||||||
|
|
||||||
|
---Fills a rectangle.
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@param w integer
|
||||||
|
---@param h integer
|
||||||
|
---@param color ScreenColor
|
||||||
|
function screen.fillRect(x, y, w, h, color) end
|
||||||
|
|
||||||
|
---Outlines a rectangle.
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@param w integer
|
||||||
|
---@param h integer
|
||||||
|
---@param color ScreenColor
|
||||||
|
function screen.drawRect(x, y, w, h, color) end
|
||||||
|
|
||||||
|
---Draws a line.
|
||||||
|
---@param x1 integer
|
||||||
|
---@param y1 integer
|
||||||
|
---@param x2 integer
|
||||||
|
---@param y2 integer
|
||||||
|
---@param color ScreenColor
|
||||||
|
---@param width? integer Defaults to one pixel.
|
||||||
|
function screen.drawLine(x1, y1, x2, y2, color, width) end
|
||||||
|
|
||||||
|
---Draws a single pixel.
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
---@param color ScreenColor
|
||||||
|
function screen.drawPixel(x, y, color) end
|
||||||
|
|
||||||
|
---Outlines a circle.
|
||||||
|
---@param x integer Center.
|
||||||
|
---@param y integer Center.
|
||||||
|
---@param radius integer
|
||||||
|
---@param color ScreenColor
|
||||||
|
---@param width? integer Defaults to one pixel.
|
||||||
|
function screen.drawCircle(x, y, radius, color, width) end
|
||||||
|
|
||||||
|
---Fills a circle.
|
||||||
|
---@param x integer Center.
|
||||||
|
---@param y integer Center.
|
||||||
|
---@param radius integer
|
||||||
|
---@param color ScreenColor
|
||||||
|
---@param background? ScreenColor Surface behind an anti-aliased edge.
|
||||||
|
function screen.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 ScreenColor Surface behind the anti-aliased edge.
|
||||||
|
---@param top? ScreenColor Fill, or gradient top; omitted for no fill.
|
||||||
|
---@param bottom? ScreenColor Gradient bottom; defaults to top. Panels without a gradient use top.
|
||||||
|
---@param border? ScreenColor Omitted for no border.
|
||||||
|
function screen.roundRect(x, y, w, h, radius, background, top, bottom, border) end
|
||||||
|
|
||||||
|
---Fills a polygon.
|
||||||
|
---@param xs integer[]
|
||||||
|
---@param ys integer[]
|
||||||
|
---@param color ScreenColor
|
||||||
|
function screen.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 screen.drawBmp(path, x, y, maxWidth, maxHeight) end
|
||||||
|
|
||||||
|
---Measures a text run.
|
||||||
|
---@param font ScreenFont Use a named screen.FONT_* role.
|
||||||
|
---@param text string
|
||||||
|
---@param style? ScreenTextStyle Defaults to screen.STYLE_NORMAL.
|
||||||
|
---@return integer
|
||||||
|
function screen.getTextWidth(font, text, style) end
|
||||||
|
|
||||||
|
---Returns the line height of a font role.
|
||||||
|
---@param font ScreenFont Use a named screen.FONT_* role.
|
||||||
|
---@param style? ScreenTextStyle Defaults to screen.STYLE_NORMAL.
|
||||||
|
---@return integer
|
||||||
|
function screen.getFontHeight(font, style) end
|
||||||
|
|
||||||
|
---Draws a text run with its top-left corner at x, y.
|
||||||
|
---@param font ScreenFont Use a named screen.FONT_* role.
|
||||||
|
---@param x integer Left edge.
|
||||||
|
---@param y integer Top edge.
|
||||||
|
---@param text string
|
||||||
|
---@param color? ScreenColor Defaults to black.
|
||||||
|
---@param style? ScreenTextStyle Defaults to screen.STYLE_NORMAL.
|
||||||
|
---@param background? ScreenColor Omitted for transparent text.
|
||||||
|
function screen.drawText(font, x, y, text, color, style, background) end
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---@meta
|
---@meta
|
||||||
|
|
||||||
-- Generated from native/src/bindings/core/node.cpp. Do not edit.
|
-- Generated from native/src/bindings/features/screen/tree.cpp. Do not edit.
|
||||||
|
|
||||||
---@alias NodeId integer
|
---@alias NodeId integer
|
||||||
---@alias NodeType "box"|"text"|"button"|"custom"
|
---@alias NodeType "box"|"text"|"button"|"custom"
|
||||||
@@ -16,46 +16,45 @@
|
|||||||
---@field justify? "start"|"center"|"end"|"between"
|
---@field justify? "start"|"center"|"end"|"between"
|
||||||
---@field row? boolean
|
---@field row? boolean
|
||||||
---@field at? table Absolute-position fields.
|
---@field at? table Absolute-position fields.
|
||||||
---@field capture? boolean
|
|
||||||
---@field interactive? boolean
|
---@field interactive? boolean
|
||||||
---@field label? string
|
---@field label? string
|
||||||
---@field font? GuiFont
|
---@field font? ScreenFont
|
||||||
|
|
||||||
---@class NodeStyle
|
---@class NodeStyle
|
||||||
---@field color? GuiColor
|
---@field color? ScreenColor
|
||||||
---@field background? GuiColor Background offered to descendants.
|
---@field background? ScreenColor Background offered to descendants.
|
||||||
---@field fill? GuiColor Surface painted by a box.
|
---@field fill? ScreenColor Surface painted by a box.
|
||||||
---@field border? GuiColor
|
---@field border? ScreenColor
|
||||||
---@field face? GuiColor Default button surface.
|
---@field face? ScreenColor Default button surface.
|
||||||
---@field pressedFace? GuiColor Pressed button surface.
|
---@field pressedFace? ScreenColor Pressed button surface.
|
||||||
---@field pressedColor? GuiColor Pressed button text.
|
---@field pressedColor? ScreenColor Pressed button text.
|
||||||
---@field focusColor? GuiColor Distinct outline for directional focus.
|
---@field focusColor? ScreenColor Distinct outline for directional focus.
|
||||||
---@field radius? integer
|
---@field radius? integer
|
||||||
---@field font? GuiFont
|
---@field font? ScreenFont
|
||||||
---@field textStyle? GuiTextStyle
|
---@field textStyle? ScreenTextStyle
|
||||||
|
|
||||||
---@class NodeLib
|
---@class TreeLib
|
||||||
node = {}
|
tree = {}
|
||||||
|
|
||||||
---Drops the current tree; all existing IDs become invalid.
|
---Drops the current tree; all existing IDs become invalid.
|
||||||
function node.reset() end
|
function tree.reset() end
|
||||||
|
|
||||||
---Creates a node, optionally as a child of an existing one.
|
---Creates a node, optionally as a child of an existing one.
|
||||||
---@param parent? NodeId Nil creates a root.
|
---@param parent? NodeId Nil creates a root.
|
||||||
---@param spec NodeSpec
|
---@param spec NodeSpec
|
||||||
---@return NodeId
|
---@return NodeId
|
||||||
function node.create(parent, spec) end
|
function tree.create(parent, spec) end
|
||||||
|
|
||||||
---Adopts an existing root as a child.
|
---Adopts an existing root as a child.
|
||||||
---@param parent NodeId
|
---@param parent NodeId
|
||||||
---@param child NodeId Existing root without a parent.
|
---@param child NodeId Existing root without a parent.
|
||||||
function node.attach(parent, child) end
|
function tree.attach(parent, child) end
|
||||||
|
|
||||||
---Changes a node's requested size before layout.
|
---Changes a node's requested size before layout.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@param w? number|"fill"|"auto"
|
---@param w? number|"fill"|"auto"
|
||||||
---@param h? number|"fill"|"auto"
|
---@param h? number|"fill"|"auto"
|
||||||
function node.setSize(id, w, h) end
|
function tree.setSize(id, w, h) end
|
||||||
|
|
||||||
---Measures and places a subtree.
|
---Measures and places a subtree.
|
||||||
---@param root NodeId
|
---@param root NodeId
|
||||||
@@ -65,17 +64,17 @@ function node.setSize(id, w, h) end
|
|||||||
---@param h integer
|
---@param h integer
|
||||||
---@return true? ok
|
---@return true? ok
|
||||||
---@return string? error
|
---@return string? error
|
||||||
function node.layout(root, x, y, w, h) end
|
function tree.layout(root, x, y, w, h) end
|
||||||
|
|
||||||
---Releases temporary measurement and placement inputs after layout.
|
---Releases temporary measurement and placement inputs after layout.
|
||||||
function node.dropScratch() end
|
function tree.dropScratch() end
|
||||||
|
|
||||||
---Returns the deepest interactive node under a point.
|
---Returns the deepest interactive node under a point.
|
||||||
---@param root NodeId
|
---@param root NodeId
|
||||||
---@param x integer
|
---@param x integer
|
||||||
---@param y integer
|
---@param y integer
|
||||||
---@return NodeId?
|
---@return NodeId?
|
||||||
function node.hit(root, x, y) end
|
function tree.hit(root, x, y) end
|
||||||
|
|
||||||
---Returns a node's placed rectangle.
|
---Returns a node's placed rectangle.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
@@ -83,73 +82,89 @@ function node.hit(root, x, y) end
|
|||||||
---@return integer y
|
---@return integer y
|
||||||
---@return integer w
|
---@return integer w
|
||||||
---@return integer h
|
---@return integer h
|
||||||
function node.getRect(id) end
|
function tree.getRect(id) end
|
||||||
|
|
||||||
|
---Pans a scrollable node's children, clamped to the content. The node is marked dirty, so the next draw repaints it.
|
||||||
|
---@param id NodeId
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
function tree.setScroll(id, x, y) end
|
||||||
|
|
||||||
|
---Returns a scrollable node's current offset, or zeroes.
|
||||||
|
---@param id NodeId
|
||||||
|
---@return integer x, integer y
|
||||||
|
function tree.getScroll(id) end
|
||||||
|
|
||||||
|
---Returns how far each axis can pan before the content's far edge reaches the box. Zero on an axis whose content fits.
|
||||||
|
---@param id NodeId
|
||||||
|
---@return integer maxX, integer maxY
|
||||||
|
function tree.getScrollRange(id) end
|
||||||
|
|
||||||
---Replaces a node's text and marks it for repaint.
|
---Replaces a node's text and marks it for repaint.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@param text string
|
---@param text string
|
||||||
function node.setLabel(id, text) end
|
function tree.setLabel(id, text) end
|
||||||
|
|
||||||
---Returns a node's text.
|
---Returns a node's text.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@return string?
|
---@return string?
|
||||||
function node.getLabel(id) end
|
function tree.getLabel(id) end
|
||||||
|
|
||||||
---Returns a node's parent.
|
---Returns a node's parent.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@return NodeId?
|
---@return NodeId?
|
||||||
function node.getParent(id) end
|
function tree.getParent(id) end
|
||||||
|
|
||||||
---Sets the style roles a subtree inherits.
|
---Sets the style roles a subtree inherits.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@param style NodeStyle
|
---@param style NodeStyle
|
||||||
function node.setStyle(id, style) end
|
function tree.setStyle(id, style) end
|
||||||
|
|
||||||
---Marks a node for repaint.
|
---Marks a node for repaint.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
function node.invalidate(id) end
|
function tree.invalidate(id) end
|
||||||
|
|
||||||
---Sets a node's pressed state.
|
---Sets a node's pressed state.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@param pressed boolean
|
---@param pressed boolean
|
||||||
function node.setPressed(id, pressed) end
|
function tree.setPressed(id, pressed) end
|
||||||
|
|
||||||
---Whether a node is pressed.
|
---Whether a node is pressed.
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function node.isPressed(id) end
|
function tree.isPressed(id) end
|
||||||
|
|
||||||
---Focuses the first interactive node in layout order.
|
---Focuses the first interactive node in layout order.
|
||||||
---@param root NodeId
|
---@param root NodeId
|
||||||
---@return NodeId? focused
|
---@return NodeId? focused
|
||||||
function node.focusFirst(root) end
|
function tree.focusFirst(root) end
|
||||||
|
|
||||||
---Changes focus and invalidates the previously and newly focused nodes.
|
---Changes focus and invalidates the previously and newly focused nodes.
|
||||||
---@param id? NodeId Nil clears focus.
|
---@param id? NodeId Nil clears focus.
|
||||||
function node.setFocus(id) end
|
function tree.setFocus(id) end
|
||||||
|
|
||||||
---Returns the focused node.
|
---Returns the focused node.
|
||||||
---@return NodeId?
|
---@return NodeId?
|
||||||
function node.getFocus() end
|
function tree.getFocus() end
|
||||||
|
|
||||||
---Moves to the nearest interactive node in the requested direction without wrapping.
|
---Moves to the nearest interactive node in the requested direction without wrapping.
|
||||||
---@param root NodeId
|
---@param root NodeId
|
||||||
---@param direction NodeDirection
|
---@param direction NodeDirection
|
||||||
---@return NodeId? focused Current focus when no candidate exists.
|
---@return NodeId? focused Current focus when no candidate exists.
|
||||||
function node.moveFocus(root, direction) end
|
function tree.moveFocus(root, direction) end
|
||||||
|
|
||||||
---Registers the painter every custom node calls.
|
---Registers the painter every custom node calls. The clip arguments are the region of the node being painted now -- one band of a composited repaint -- so a painter can cull to it instead of redrawing itself once per band.
|
||||||
---@param painter fun(id: NodeId, x: integer, y: integer, w: integer, h: integer)
|
---@param painter fun(id: NodeId, x: integer, y: integer, w: integer, h: integer, clipX: integer, clipY: integer, clipW: integer, clipH: integer)
|
||||||
function node.setPainter(painter) end
|
function tree.setPainter(painter) end
|
||||||
|
|
||||||
---Paints dirty nodes; the firmware owns publication to the physical display.
|
---Paints dirty nodes; the firmware owns publication to the physical display.
|
||||||
---@param root NodeId
|
---@param root NodeId
|
||||||
function node.draw(root) end
|
function tree.draw(root) end
|
||||||
|
|
||||||
---Returns the number of nodes in the tree.
|
---Returns the number of nodes in the tree.
|
||||||
---@return integer
|
---@return integer
|
||||||
function node.getCount() end
|
function tree.getCount() end
|
||||||
|
|
||||||
---Returns the tree's memory use.
|
---Returns the tree's memory use.
|
||||||
---@return integer bytes
|
---@return integer bytes
|
||||||
function node.getFootprint() end
|
function tree.getFootprint() end
|
||||||
+25
-38
@@ -2,8 +2,22 @@
|
|||||||
|
|
||||||
-- Generated from native/src/bindings/features/touch.cpp. Do not edit.
|
-- Generated from native/src/bindings/features/touch.cpp. Do not edit.
|
||||||
|
|
||||||
---@class SettingsLib
|
---@class TouchLib
|
||||||
settings = settings or {}
|
touch = {}
|
||||||
|
|
||||||
|
---Returns the calibrated touch point, or nothing when the panel is not touched.
|
||||||
|
---@return integer? x
|
||||||
|
---@return integer? y
|
||||||
|
function touch.getPoint() end
|
||||||
|
|
||||||
|
---Returns the uncalibrated touch reading, or nothing when the panel is not touched.
|
||||||
|
---@return integer? x
|
||||||
|
---@return integer? y
|
||||||
|
function touch.getRawPoint() end
|
||||||
|
|
||||||
|
---Whether the panel is currently touched.
|
||||||
|
---@return boolean
|
||||||
|
function touch.isTouched() end
|
||||||
|
|
||||||
---Persists the panel's touch calibration.
|
---Persists the panel's touch calibration.
|
||||||
---@param x0 integer Raw reading at the left edge.
|
---@param x0 integer Raw reading at the left edge.
|
||||||
@@ -12,41 +26,14 @@ settings = settings or {}
|
|||||||
---@param y1 integer Raw reading at the bottom edge.
|
---@param y1 integer Raw reading at the bottom edge.
|
||||||
---@return true? ok
|
---@return true? ok
|
||||||
---@return string? error
|
---@return string? error
|
||||||
function settings.setCalibration(x0, y0, x1, y1) end
|
function touch.setCalibration(x0, y0, x1, y1) end
|
||||||
|
|
||||||
---@class InputLib
|
-- What an app implements to see raw touch, composed into its own class:
|
||||||
input = input or {}
|
--
|
||||||
|
-- ---@class PaintApp : App, TouchHandlers
|
||||||
|
|
||||||
---Returns the calibrated touch point, or nothing when the panel is not touched.
|
---@class TouchHandlers
|
||||||
---@return integer? x
|
---@field onTouchDown? fun(x: integer, y: integer) Fired when the finger lands.
|
||||||
---@return integer? y
|
---@field onTouchMove? fun(x: integer, y: integer) Fired when the finger moves while down, after the firmware's jitter filter.
|
||||||
function input.getTouch() end
|
---@field onTouchUp? fun(x: integer, y: integer) Fired when the finger lifts.
|
||||||
|
---@field onTouch? fun(x: integer, y: integer) Tap alias, fired on release like a click, after onTouchUp.
|
||||||
---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,27 @@
|
|||||||
|
---@meta cjson
|
||||||
|
|
||||||
|
-- Hand-written, unlike the rest of lua/api: the implementation is lua-cjson under
|
||||||
|
-- native/src/vendor/cjson, so there are no binding annotations to generate from.
|
||||||
|
-- Depth is capped at 32 in native/src/bindings/lib/cjson.cpp, because decoding
|
||||||
|
-- recurses on the C stack and the upstream default of 1000 assumes a server.
|
||||||
|
--
|
||||||
|
-- Both functions raise on bad input rather than returning nil plus a message, so
|
||||||
|
-- a response from the network is worth a pcall.
|
||||||
|
|
||||||
|
---@class CJsonLib
|
||||||
|
local cjson = {}
|
||||||
|
|
||||||
|
---The value a JSON null decodes to, distinct from nil so a key survives it.
|
||||||
|
cjson.null = nil
|
||||||
|
|
||||||
|
---Parses JSON text.
|
||||||
|
---@param text string
|
||||||
|
---@return any value Tables, strings, numbers, booleans, or cjson.null.
|
||||||
|
function cjson.decode(text) end
|
||||||
|
|
||||||
|
---Serializes plain Lua data as JSON.
|
||||||
|
---@param value any Sequences become arrays; every other table becomes an object.
|
||||||
|
---@return string text
|
||||||
|
function cjson.encode(value) end
|
||||||
|
|
||||||
|
return cjson
|
||||||
+11
-11
@@ -8,8 +8,8 @@ local ORDER = { "back", "left", "up", "down", "right", "confirm" }
|
|||||||
|
|
||||||
local function available()
|
local function available()
|
||||||
local roles = {}
|
local roles = {}
|
||||||
if input and input.getButtons then
|
if buttons and buttons.getAll then
|
||||||
for _, role in ipairs(input.getButtons()) do
|
for _, role in ipairs(buttons.getAll()) do
|
||||||
roles[role] = true
|
roles[role] = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -21,11 +21,11 @@ end
|
|||||||
---@param options table|nil `y`, `font`, `color`, and `background` overrides.
|
---@param options table|nil `y`, `font`, `color`, and `background` overrides.
|
||||||
function hints.draw(actions, options)
|
function hints.draw(actions, options)
|
||||||
options = options or {}
|
options = options or {}
|
||||||
local font = options.font or gui.FONT_SMALL
|
local font = options.font or screen.FONT_SMALL
|
||||||
local color = options.color or gui.color(0, 0, 0)
|
local color = options.color or screen.color(0, 0, 0)
|
||||||
local background = options.background or gui.color(255, 255, 255)
|
local background = options.background or screen.color(255, 255, 255)
|
||||||
local height = gui.getFontHeight(font) + 6
|
local height = screen.getFontHeight(font) + 6
|
||||||
local y = options.y or (gui.getHeight() - height)
|
local y = options.y or (screen.getHeight() - height)
|
||||||
|
|
||||||
local roles = available()
|
local roles = available()
|
||||||
local labels = {}
|
local labels = {}
|
||||||
@@ -35,15 +35,15 @@ function hints.draw(actions, options)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
gui.fillRect(0, y, gui.getWidth(), height, background)
|
screen.fillRect(0, y, screen.getWidth(), height, background)
|
||||||
if #labels == 0 then
|
if #labels == 0 then
|
||||||
return height
|
return height
|
||||||
end
|
end
|
||||||
|
|
||||||
local slot = gui.getWidth() // #labels
|
local slot = screen.getWidth() // #labels
|
||||||
for index, label in ipairs(labels) do
|
for index, label in ipairs(labels) do
|
||||||
local left = slot * (index - 1) + (slot - gui.getTextWidth(font, label)) // 2
|
local left = slot * (index - 1) + (slot - screen.getTextWidth(font, label)) // 2
|
||||||
gui.drawText(font, left, y + 3, label, color, gui.STYLE_NORMAL, background)
|
screen.drawText(font, left, y + 3, label, color, screen.STYLE_NORMAL, background)
|
||||||
end
|
end
|
||||||
return height
|
return height
|
||||||
end
|
end
|
||||||
|
|||||||
+301
-103
@@ -12,7 +12,6 @@ local ui = {}
|
|||||||
---@field justify? "start"|"center"|"end"|"between"
|
---@field justify? "start"|"center"|"end"|"between"
|
||||||
---@field row? boolean
|
---@field row? boolean
|
||||||
---@field at? table
|
---@field at? table
|
||||||
---@field capture? boolean
|
|
||||||
---@field label? string
|
---@field label? string
|
||||||
---@field font? GuiFont
|
---@field font? GuiFont
|
||||||
---@field style? GuiTextStyle
|
---@field style? GuiTextStyle
|
||||||
@@ -27,8 +26,10 @@ local ui = {}
|
|||||||
---@field on_enter? UiHandler
|
---@field on_enter? UiHandler
|
||||||
---@field on_exit? UiHandler
|
---@field on_exit? UiHandler
|
||||||
---@field on_click? UiHandler
|
---@field on_click? UiHandler
|
||||||
---@field paint? fun(id: NodeId, x: integer, y: integer, w: integer, h: integer)
|
---@field paint? fun(id: NodeId, x: integer, y: integer, w: integer, h: integer, clipX: integer, clipY: integer, clipW: integer, clipH: integer)
|
||||||
---@field press_style? boolean False for a widget that paints its own press feedback.
|
---@field press_style? boolean False for a widget that paints its own press feedback.
|
||||||
|
---@field scrollX? boolean Children measure unbounded across, and the box pans horizontally.
|
||||||
|
---@field scrollY? boolean Children measure unbounded down, and the box pans vertically.
|
||||||
|
|
||||||
---@class UiConfirmSpec
|
---@class UiConfirmSpec
|
||||||
---@field title string
|
---@field title string
|
||||||
@@ -42,7 +43,6 @@ local ui = {}
|
|||||||
---@field on_cancel? UiHandler
|
---@field on_cancel? UiHandler
|
||||||
---@field on_outside? UiHandler
|
---@field on_outside? UiHandler
|
||||||
|
|
||||||
local THEME_PATH = "/.lua/theme"
|
|
||||||
local THEMES = {
|
local THEMES = {
|
||||||
light = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 120, 255 }, radius = 6 },
|
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 },
|
dark = { background = { 18, 18, 20 }, color = { 235, 235, 235 }, accent = { 166, 118, 255 }, radius = 6 },
|
||||||
@@ -55,9 +55,29 @@ local enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
|||||||
local pressStyles = {}
|
local pressStyles = {}
|
||||||
local laidOut = false
|
local laidOut = false
|
||||||
local themeName
|
local themeName
|
||||||
local activeScreen
|
-- One tree per state, built by the function ui.mount() was given. Rebuilding is
|
||||||
|
-- cheap enough that nothing is retained between screens.
|
||||||
|
local builder, root, responder, insideResponder, confirming
|
||||||
|
local inset = 0
|
||||||
local applyPalette
|
local applyPalette
|
||||||
|
|
||||||
|
-- Scroll gestures live in the tree, not the app: a drag on any scrollX/scrollY box pans it,
|
||||||
|
-- a flick coasts, and a child button's tap is suppressed once the drag passes SLOP. C++ owns
|
||||||
|
-- only the clamped offset (setScroll/getScroll); the momentum and the tap-vs-pan decision are
|
||||||
|
-- here, keyed by node so every scrollable box gets them for free.
|
||||||
|
local SCROLL_SLOP = 8
|
||||||
|
local SCROLL_DECAY = 0.02
|
||||||
|
local SCROLL_STOP = 8
|
||||||
|
-- id -> {x=bool, y=bool}: which axes a box pans. Recorded at build, cleared on reset.
|
||||||
|
local scrollNodes = {}
|
||||||
|
-- id -> {sx, sy, vx, vy}: pan offset (float, floored into setScroll) and flick velocity.
|
||||||
|
local scrollState = {}
|
||||||
|
-- Set of ids still coasting, iterated each draw until they settle.
|
||||||
|
local flinging = {}
|
||||||
|
-- Per-gesture bookkeeping: the box being panned once past SLOP, and the raw drag deltas.
|
||||||
|
local panning, scrollAncestor, dragged
|
||||||
|
local downX, downY, lastX, lastY, pendingX, pendingY
|
||||||
|
|
||||||
local function mix(a, b, amount)
|
local function mix(a, b, amount)
|
||||||
local out = {}
|
local out = {}
|
||||||
for i = 1, 3 do
|
for i = 1, 3 do
|
||||||
@@ -67,7 +87,7 @@ local function mix(a, b, amount)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function color(rgb)
|
local function color(rgb)
|
||||||
return gui.color(rgb[1], rgb[2], rgb[3])
|
return screen.color(rgb[1], rgb[2], rgb[3])
|
||||||
end
|
end
|
||||||
|
|
||||||
local function palette(seed)
|
local function palette(seed)
|
||||||
@@ -113,31 +133,33 @@ function ui.setTheme(name)
|
|||||||
if not THEMES[name] then
|
if not THEMES[name] then
|
||||||
return nil, "Unknown theme"
|
return nil, "Unknown theme"
|
||||||
end
|
end
|
||||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
local ok, err = screen.setTheme(name)
|
||||||
if not ok then
|
if not ok then
|
||||||
return nil, err
|
return nil, err
|
||||||
end
|
end
|
||||||
loadTheme(name)
|
loadTheme(name)
|
||||||
if activeScreen then
|
if root then
|
||||||
applyPalette(activeScreen.root)
|
applyPalette(root)
|
||||||
gui.clear(ui.theme.background)
|
screen.clear(ui.theme.background)
|
||||||
node.invalidate(activeScreen.root)
|
tree.invalidate(root)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local savedTheme = fs.readFile(THEME_PATH, 32)
|
loadTheme(screen.getTheme())
|
||||||
loadTheme(savedTheme and savedTheme:match "^%s*(.-)%s*$" or "light")
|
|
||||||
|
|
||||||
local function clearState()
|
local function clearState()
|
||||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||||
pressStyles = {}
|
pressStyles = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
node.setPainter(function(id, x, y, w, h)
|
-- The clip is the slice of the node being repainted now, which for a composited repaint is
|
||||||
|
-- one band. A painter that ignores it still draws correctly; one that culls to it stops
|
||||||
|
-- redrawing its whole contents once per band it spans.
|
||||||
|
tree.setPainter(function(id, x, y, w, h, clipX, clipY, clipW, clipH)
|
||||||
local painter = painters[id]
|
local painter = painters[id]
|
||||||
if painter then
|
if painter then
|
||||||
painter(id, x, y, w, h)
|
painter(id, x, y, w, h, clipX, clipY, clipW, clipH)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -165,14 +187,16 @@ local function applyStyle(id, spec)
|
|||||||
style.background, style.fill, hasStyle = spec.background, spec.background, true
|
style.background, style.fill, hasStyle = spec.background, spec.background, true
|
||||||
end
|
end
|
||||||
if hasStyle then
|
if hasStyle then
|
||||||
node.setStyle(id, style)
|
tree.setStyle(id, style)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function build(spec, kind)
|
local function build(spec, kind)
|
||||||
spec = spec or {}
|
spec = spec or {}
|
||||||
|
-- Nodes outside a build would reset the arena under the screen already on the
|
||||||
|
-- panel, chrome included. Rebuilding is the only way to change one.
|
||||||
if laidOut then
|
if laidOut then
|
||||||
ui.reset()
|
error("build nodes from the function ui.mount() was given, then ui.rebuild()", 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
local children = {}
|
local children = {}
|
||||||
@@ -183,9 +207,9 @@ local function build(spec, kind)
|
|||||||
|
|
||||||
spec.type = kind
|
spec.type = kind
|
||||||
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
|
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
|
||||||
local id = node.create(nil, spec)
|
local id = tree.create(nil, spec)
|
||||||
for _, child in ipairs(children) do
|
for _, child in ipairs(children) do
|
||||||
node.attach(id, child)
|
tree.attach(id, child)
|
||||||
end
|
end
|
||||||
|
|
||||||
applyStyle(id, spec)
|
applyStyle(id, spec)
|
||||||
@@ -194,6 +218,10 @@ local function build(spec, kind)
|
|||||||
clickHandlers[id] = spec.on_click
|
clickHandlers[id] = spec.on_click
|
||||||
painters[id] = spec.paint
|
painters[id] = spec.paint
|
||||||
pressStyles[id] = spec.press_style ~= false
|
pressStyles[id] = spec.press_style ~= false
|
||||||
|
if spec.scrollX or spec.scrollY then
|
||||||
|
scrollNodes[id] = { x = spec.scrollX == true, y = spec.scrollY == true }
|
||||||
|
scrollState[id] = { sx = 0, sy = 0, vx = 0, vy = 0 }
|
||||||
|
end
|
||||||
return id
|
return id
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -212,13 +240,28 @@ end
|
|||||||
---@return integer side
|
---@return integer side
|
||||||
---@return integer columns
|
---@return integer columns
|
||||||
function ui.cardSide(count, pad, gap, reserve)
|
function ui.cardSide(count, pad, gap, reserve)
|
||||||
local columns = gui.getWidth() >= gui.getHeight() and 3 or 2
|
local width, height = ui.frame()
|
||||||
|
local columns = width >= height and 3 or 2
|
||||||
local rows = math.ceil(count / columns)
|
local rows = math.ceil(count / columns)
|
||||||
local byWidth = (gui.getWidth() - 2 * pad - (columns - 1) * gap) // columns
|
local byWidth = (width - 2 * pad - (columns - 1) * gap) // columns
|
||||||
local byHeight = (gui.getHeight() - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
local byHeight = (height - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
||||||
return math.min(byWidth, byHeight), columns
|
return math.min(byWidth, byHeight), columns
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---How much of the panel chrome took before the app was built. Set by whatever mounts the
|
||||||
|
---tree, because layout has not run yet when an app sizes itself.
|
||||||
|
---@param px integer
|
||||||
|
function ui.setInset(px)
|
||||||
|
inset = px
|
||||||
|
end
|
||||||
|
|
||||||
|
---The box the app is built into, which is the panel minus the chrome above it.
|
||||||
|
---@return integer w
|
||||||
|
---@return integer h
|
||||||
|
function ui.frame()
|
||||||
|
return screen.getWidth(), screen.getHeight() - inset
|
||||||
|
end
|
||||||
|
|
||||||
---@param spec UiSpec
|
---@param spec UiSpec
|
||||||
---@return NodeId
|
---@return NodeId
|
||||||
function ui.spacer(spec)
|
function ui.spacer(spec)
|
||||||
@@ -241,15 +284,15 @@ end
|
|||||||
---@return NodeId
|
---@return NodeId
|
||||||
function ui.label(text, spec)
|
function ui.label(text, spec)
|
||||||
spec = spec or {}
|
spec = spec or {}
|
||||||
local font, style = spec.font or gui.FONT_UI, spec.style or gui.STYLE_NORMAL
|
local font, style = spec.font or screen.FONT_UI, spec.style or screen.STYLE_NORMAL
|
||||||
if spec.fit and gui.getTextWidth(font, text, style) > spec.fit then
|
if spec.fit and screen.getTextWidth(font, text, style) > spec.fit then
|
||||||
while #text > 1 and gui.getTextWidth(font, text .. "~", style) > spec.fit do
|
while #text > 1 and screen.getTextWidth(font, text .. "~", style) > spec.fit do
|
||||||
text = text:sub(1, -2)
|
text = text:sub(1, -2)
|
||||||
end
|
end
|
||||||
text = text .. "~"
|
text = text .. "~"
|
||||||
end
|
end
|
||||||
spec.w = gui.getTextWidth(font, text, style)
|
spec.w = screen.getTextWidth(font, text, style)
|
||||||
spec.h = gui.getFontHeight(font, style)
|
spec.h = screen.getFontHeight(font, style)
|
||||||
spec.font, spec.fit = font, nil
|
spec.font, spec.fit = font, nil
|
||||||
return ui.text(text, spec)
|
return ui.text(text, spec)
|
||||||
end
|
end
|
||||||
@@ -264,7 +307,7 @@ function ui.button(spec)
|
|||||||
spec.label = nil
|
spec.label = nil
|
||||||
local id = build(spec, "button")
|
local id = build(spec, "button")
|
||||||
if label then
|
if label then
|
||||||
node.create(id, { type = "text", label = label, font = font or gui.FONT_UI })
|
tree.create(id, { type = "text", label = label, font = font or screen.FONT_UI })
|
||||||
end
|
end
|
||||||
return id
|
return id
|
||||||
end
|
end
|
||||||
@@ -278,16 +321,39 @@ end
|
|||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
---@param text string
|
---@param text string
|
||||||
function ui.setText(id, text)
|
function ui.setText(id, text)
|
||||||
if node.getLabel(id) == text then
|
if tree.getLabel(id) == text then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
node.setLabel(id, text)
|
tree.setLabel(id, text)
|
||||||
node.invalidate(id)
|
tree.invalidate(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param id NodeId
|
---@param id NodeId
|
||||||
function ui.invalidate(id)
|
function ui.invalidate(id)
|
||||||
node.invalidate(id)
|
tree.invalidate(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Pans a scrollable node, clamped to its content. The node is marked dirty, so the next
|
||||||
|
---ui.draw() repaints it -- there is nothing for the app to draw and no handle to refresh,
|
||||||
|
---which is the whole point of scrolling in the tree rather than in a painter.
|
||||||
|
---@param id NodeId
|
||||||
|
---@param x integer
|
||||||
|
---@param y integer
|
||||||
|
function ui.setScroll(id, x, y)
|
||||||
|
tree.setScroll(id, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param id NodeId
|
||||||
|
---@return integer x, integer y
|
||||||
|
function ui.getScroll(id)
|
||||||
|
return tree.getScroll(id)
|
||||||
|
end
|
||||||
|
|
||||||
|
---How far each axis can pan. Zero on an axis whose content already fits.
|
||||||
|
---@param id NodeId
|
||||||
|
---@return integer maxX, integer maxY
|
||||||
|
function ui.getScrollRange(id)
|
||||||
|
return tree.getScrollRange(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param spec UiConfirmSpec
|
---@param spec UiConfirmSpec
|
||||||
@@ -316,7 +382,9 @@ function ui.confirm(spec)
|
|||||||
at = { x = 0, y = 0 },
|
at = { x = 0, y = 0 },
|
||||||
w = "fill",
|
w = "fill",
|
||||||
h = "fill",
|
h = "fill",
|
||||||
capture = true,
|
-- A full-bleed layer painted last: hit-testing returns the topmost node, so taps on the
|
||||||
|
-- dim area land here and cannot reach the content beneath. on_outside handles them; with
|
||||||
|
-- none, they die at the root -- which is what makes the dialog modal.
|
||||||
align = "center",
|
align = "center",
|
||||||
justify = "center",
|
justify = "center",
|
||||||
on_click = spec.on_outside,
|
on_click = spec.on_outside,
|
||||||
@@ -325,20 +393,18 @@ function ui.confirm(spec)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ui.reset()
|
function ui.reset()
|
||||||
node.reset()
|
tree.reset()
|
||||||
clearState()
|
clearState()
|
||||||
laidOut = false
|
laidOut = false
|
||||||
activeScreen = nil
|
root, responder, insideResponder, confirming = nil, nil, nil, nil
|
||||||
|
scrollNodes, scrollState, flinging = {}, {}, {}
|
||||||
|
panning, scrollAncestor, dragged = nil, nil, false
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class UiScreen
|
|
||||||
local Screen = {}
|
|
||||||
Screen.__index = Screen
|
|
||||||
|
|
||||||
applyPalette = function(root)
|
applyPalette = function(root)
|
||||||
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
||||||
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
||||||
node.setStyle(root, {
|
tree.setStyle(root, {
|
||||||
color = ui.theme.color,
|
color = ui.theme.color,
|
||||||
background = ui.theme.background,
|
background = ui.theme.background,
|
||||||
face = ui.theme.face,
|
face = ui.theme.face,
|
||||||
@@ -346,47 +412,131 @@ applyPalette = function(root)
|
|||||||
pressedColor = ui.theme.pressedColor,
|
pressedColor = ui.theme.pressedColor,
|
||||||
focusColor = ui.theme.focusColor,
|
focusColor = ui.theme.focusColor,
|
||||||
radius = ui.theme.radius,
|
radius = ui.theme.radius,
|
||||||
font = gui.FONT_UI,
|
font = screen.FONT_UI,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param root NodeId
|
---Registers the function that builds the whole tree and shows what it returns.
|
||||||
---@param style? NodeStyle
|
---@param fn fun(): NodeId
|
||||||
---@return UiScreen
|
function ui.mount(fn)
|
||||||
function ui.screen(root, style)
|
builder = fn
|
||||||
node.setSize(root, "fill", "fill")
|
ui.rebuild()
|
||||||
applyPalette(root)
|
|
||||||
if style then
|
|
||||||
node.setStyle(root, style)
|
|
||||||
end
|
|
||||||
local screen = setmetatable({ root = root }, Screen)
|
|
||||||
activeScreen = screen
|
|
||||||
screen:relayout()
|
|
||||||
return screen
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Screen:relayout()
|
---Rebuilds the tree from scratch and repaints. Screens are not retained, so this
|
||||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
---is how a screen changes, a rotation is answered and a dialog opens.
|
||||||
|
function ui.rebuild()
|
||||||
|
ui.reset()
|
||||||
|
root = builder()
|
||||||
|
tree.setSize(root, "fill", "fill")
|
||||||
|
applyPalette(root)
|
||||||
|
local ok, err = tree.layout(root, 0, 0, screen.getWidth(), screen.getHeight())
|
||||||
if not ok then
|
if not ok then
|
||||||
error(err, 2)
|
error(err, 2)
|
||||||
end
|
end
|
||||||
node.dropScratch()
|
tree.dropScratch()
|
||||||
laidOut = true
|
laidOut = true
|
||||||
gui.clear(ui.theme.background)
|
screen.clear(ui.theme.background)
|
||||||
|
tree.draw(root)
|
||||||
|
-- A build allocates a spec table per node and drops them all here, and the next thing an
|
||||||
|
-- app does may be the one that needs a contiguous WiFi buffer. Collecting now costs a few
|
||||||
|
-- milliseconds on a screen change nobody can see, and leaves the heap in a known state
|
||||||
|
-- instead of one that depends on when the incremental GC last ran.
|
||||||
|
collectgarbage()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Screen:draw()
|
-- Pushes a node's float offset into the tree, then snaps our copy back to whatever the
|
||||||
node.draw(self.root)
|
-- clamp allowed -- without this the offset runs past the end while the content sits still
|
||||||
|
-- and the list ignores the first part of the drag back.
|
||||||
|
local function applyScroll(id, st)
|
||||||
|
ui.setScroll(id, math.floor(st.sx), math.floor(st.sy))
|
||||||
|
local cx, cy = ui.getScroll(id)
|
||||||
|
if cx and cx ~= math.floor(st.sx) then
|
||||||
|
st.sx, st.vx = cx, 0
|
||||||
|
end
|
||||||
|
if cy and cy ~= math.floor(st.sy) then
|
||||||
|
st.sy, st.vy = cy, 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Free settle: velocity decays to rest, applied as DECAY^seconds so the glide lasts the
|
||||||
|
-- same wall time whatever the frame rate. A paged box would instead ease toward the nearest
|
||||||
|
-- page boundary here -- same gesture, different ending -- so this is the one seam paging adds.
|
||||||
|
local function settleFree(st, seconds)
|
||||||
|
st.sx, st.sy = st.sx + st.vx * seconds, st.sy + st.vy * seconds
|
||||||
|
local keep = SCROLL_DECAY ^ seconds
|
||||||
|
st.vx, st.vy = st.vx * keep, st.vy * keep
|
||||||
|
if math.abs(st.vx) < SCROLL_STOP then
|
||||||
|
st.vx = 0
|
||||||
|
end
|
||||||
|
if math.abs(st.vy) < SCROLL_STOP then
|
||||||
|
st.vy = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One GC step a frame, because the collector's default pace is the painter's problem: a
|
||||||
|
-- band buffer needs a contiguous block, and letting the heap double before a cycle lets
|
||||||
|
-- garbage take the block the band was going to get. Stepping keeps the sawtooth shallow
|
||||||
|
-- enough that beginBuffer() keeps succeeding instead of falling back to the panel.
|
||||||
|
---@param deltaMs? integer Elapsed frame time, used to advance any active pan or flick.
|
||||||
|
function ui.draw(deltaMs)
|
||||||
|
local seconds = (deltaMs or 0) / 1000
|
||||||
|
if seconds > 0 then
|
||||||
|
if panning then
|
||||||
|
local st = scrollState[panning]
|
||||||
|
st.sx, st.sy = st.sx + pendingX, st.sy + pendingY
|
||||||
|
-- Velocity is measured over the frame the motion arrived in, so a finger that paused
|
||||||
|
-- before lifting reports zero and does not flick.
|
||||||
|
st.vx, st.vy = pendingX / seconds, pendingY / seconds
|
||||||
|
pendingX, pendingY = 0, 0
|
||||||
|
applyScroll(panning, st)
|
||||||
|
end
|
||||||
|
for id in pairs(flinging) do
|
||||||
|
local st = scrollState[id]
|
||||||
|
settleFree(st, seconds)
|
||||||
|
applyScroll(id, st)
|
||||||
|
if st.vx == 0 and st.vy == 0 then
|
||||||
|
flinging[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if root then
|
||||||
|
tree.draw(root)
|
||||||
|
end
|
||||||
|
collectgarbage "step"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The nearest ancestor (or self) that pans, or nil. A drag on a child button scrolls the
|
||||||
|
-- list it sits in, which is why the tap has to yield to the pan rather than the reverse.
|
||||||
|
local function scrollableAncestor(id)
|
||||||
|
while id do
|
||||||
|
if scrollNodes[id] then
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
id = tree.getParent(id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The nearest ancestor (or self) that handles a press, or nil. tree.hit returns the deepest
|
||||||
|
-- node by geometry -- a button's text label, say -- so dispatch bubbles up to whoever owns
|
||||||
|
-- the on_click/on_enter/on_exit, the way a responder chain does.
|
||||||
|
local function handlerAncestor(id)
|
||||||
|
while id do
|
||||||
|
if clickHandlers[id] or enterHandlers[id] or exitHandlers[id] then
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
id = tree.getParent(id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function inside(id, x, y)
|
local function inside(id, x, y)
|
||||||
local rx, ry, rw, rh = node.getRect(id)
|
local rx, ry, rw, rh = tree.getRect(id)
|
||||||
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
|
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
|
||||||
end
|
end
|
||||||
|
|
||||||
local function enter(id, x, y)
|
local function enter(id, x, y)
|
||||||
if pressStyles[id] then
|
if pressStyles[id] then
|
||||||
node.setPressed(id, true)
|
tree.setPressed(id, true)
|
||||||
end
|
end
|
||||||
local handler = enterHandlers[id]
|
local handler = enterHandlers[id]
|
||||||
if handler then
|
if handler then
|
||||||
@@ -396,7 +546,7 @@ end
|
|||||||
|
|
||||||
local function exit(id, x, y)
|
local function exit(id, x, y)
|
||||||
if pressStyles[id] then
|
if pressStyles[id] then
|
||||||
node.setPressed(id, false)
|
tree.setPressed(id, false)
|
||||||
end
|
end
|
||||||
local handler = exitHandlers[id]
|
local handler = exitHandlers[id]
|
||||||
if handler then
|
if handler then
|
||||||
@@ -407,40 +557,77 @@ end
|
|||||||
---@param x integer
|
---@param x integer
|
||||||
---@param y integer
|
---@param y integer
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:down(x, y)
|
function ui.down(x, y)
|
||||||
local focused = node.getFocus()
|
local focused = tree.getFocus()
|
||||||
if focused then
|
if focused then
|
||||||
node.setFocus(nil)
|
tree.setFocus(nil)
|
||||||
local handler = exitHandlers[focused]
|
local handler = exitHandlers[focused]
|
||||||
if handler then
|
if handler then
|
||||||
handler(focused)
|
handler(focused)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local target = node.hit(self.root, x, y)
|
local hitNode = root and tree.hit(root, x, y)
|
||||||
if not target then
|
if not hitNode then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
self.captured, self.inside = target, true
|
responder, scrollAncestor = handlerAncestor(hitNode), scrollableAncestor(hitNode)
|
||||||
enter(target, x, y)
|
insideResponder, dragged, panning = responder ~= nil, false, nil
|
||||||
return true
|
downX, downY, lastX, lastY = x, y, x, y
|
||||||
|
pendingX, pendingY = 0, 0
|
||||||
|
if scrollAncestor then
|
||||||
|
-- A finger down catches an in-progress glide, the way every touch UI does.
|
||||||
|
flinging[scrollAncestor] = nil
|
||||||
|
local st = scrollState[scrollAncestor]
|
||||||
|
st.vx, st.vy = 0, 0
|
||||||
|
end
|
||||||
|
if responder then
|
||||||
|
enter(responder, x, y)
|
||||||
|
end
|
||||||
|
return responder ~= nil or scrollAncestor ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param x integer
|
---@param x integer
|
||||||
---@param y integer
|
---@param y integer
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:move(x, y)
|
function ui.move(x, y)
|
||||||
local target = self.captured
|
if not responder and not scrollAncestor then
|
||||||
if not target then
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local isInside = inside(target, x, y)
|
if scrollAncestor then
|
||||||
if isInside ~= self.inside then
|
local flags = scrollNodes[scrollAncestor]
|
||||||
self.inside = isInside
|
if flags.x then
|
||||||
if isInside then
|
pendingX = pendingX + (lastX - x)
|
||||||
enter(target, x, y)
|
end
|
||||||
else
|
if flags.y then
|
||||||
exit(target, x, y)
|
pendingY = pendingY + (lastY - y)
|
||||||
|
end
|
||||||
|
lastX, lastY = x, y
|
||||||
|
if not dragged then
|
||||||
|
local past = (flags.x and math.abs(x - downX) > SCROLL_SLOP)
|
||||||
|
or (flags.y and math.abs(y - downY) > SCROLL_SLOP)
|
||||||
|
if past then
|
||||||
|
dragged, panning = true, scrollAncestor
|
||||||
|
-- The press became a scroll: drop the responder's feedback and keep it from clicking.
|
||||||
|
if responder and insideResponder then
|
||||||
|
exit(responder, x, y)
|
||||||
|
end
|
||||||
|
insideResponder = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if dragged then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if responder then
|
||||||
|
local isInside = inside(responder, x, y)
|
||||||
|
if isInside ~= insideResponder then
|
||||||
|
insideResponder = isInside
|
||||||
|
if isInside then
|
||||||
|
enter(responder, x, y)
|
||||||
|
else
|
||||||
|
exit(responder, x, y)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
@@ -449,28 +636,39 @@ end
|
|||||||
---@param x integer
|
---@param x integer
|
||||||
---@param y integer
|
---@param y integer
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:up(x, y)
|
function ui.up(x, y)
|
||||||
local target = self.captured
|
if not responder and not scrollAncestor then
|
||||||
if not target then
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local wasActive = self.inside
|
if dragged then
|
||||||
local releasedInside = inside(target, x, y)
|
-- The gesture was a pan: hand any velocity the last frames built to draw(), which coasts
|
||||||
local handler = releasedInside and clickHandlers[target] or nil
|
-- and clamps it. A drag that ended still carries zero velocity, so it simply stops.
|
||||||
self.captured, self.inside = nil, nil
|
if panning then
|
||||||
if wasActive then
|
flinging[panning] = true
|
||||||
exit(target, x, y)
|
end
|
||||||
|
responder, insideResponder, scrollAncestor, dragged, panning = nil, nil, nil, false, nil
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
if handler then
|
local target, wasActive = responder, insideResponder
|
||||||
handler(target, x, y)
|
responder, insideResponder, scrollAncestor = nil, nil, nil
|
||||||
|
if target then
|
||||||
|
if wasActive then
|
||||||
|
exit(target, x, y)
|
||||||
|
end
|
||||||
|
if inside(target, x, y) then
|
||||||
|
local handler = clickHandlers[target]
|
||||||
|
if handler then
|
||||||
|
handler(target, x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local DIRECTIONS = { up = true, down = true, left = true, right = true }
|
local DIRECTIONS = { up = true, down = true, left = true, right = true }
|
||||||
|
|
||||||
local function focusFirst(screen)
|
local function focusFirst()
|
||||||
local focused = node.focusFirst(screen.root)
|
local focused = tree.focusFirst(root)
|
||||||
if focused then
|
if focused then
|
||||||
local handler = enterHandlers[focused]
|
local handler = enterHandlers[focused]
|
||||||
if handler then
|
if handler then
|
||||||
@@ -483,7 +681,7 @@ end
|
|||||||
---@param name string Button name; directions and confirm are handled.
|
---@param name string Button name; directions and confirm are handled.
|
||||||
---@param pressed boolean
|
---@param pressed boolean
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:button(name, pressed)
|
function ui.buttonPress(name, pressed)
|
||||||
if type(pressed) ~= "boolean" then
|
if type(pressed) ~= "boolean" then
|
||||||
error("button state must be boolean", 2)
|
error("button state must be boolean", 2)
|
||||||
end
|
end
|
||||||
@@ -492,12 +690,12 @@ function Screen:button(name, pressed)
|
|||||||
if not pressed then
|
if not pressed then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local previous = node.getFocus()
|
local previous = tree.getFocus()
|
||||||
if not previous then
|
if not previous then
|
||||||
focusFirst(self)
|
focusFirst()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local focused = node.moveFocus(self.root, name)
|
local focused = tree.moveFocus(root, name)
|
||||||
if focused ~= previous then
|
if focused ~= previous then
|
||||||
local leave = exitHandlers[previous]
|
local leave = exitHandlers[previous]
|
||||||
if leave then
|
if leave then
|
||||||
@@ -514,18 +712,18 @@ function Screen:button(name, pressed)
|
|||||||
if name ~= "confirm" then
|
if name ~= "confirm" then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local focused = node.getFocus() or focusFirst(self)
|
local focused = tree.getFocus() or focusFirst()
|
||||||
if not focused then
|
if not focused then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
if pressed then
|
if pressed then
|
||||||
node.setPressed(focused, true)
|
tree.setPressed(focused, true)
|
||||||
self.confirming = focused
|
confirming = focused
|
||||||
else
|
else
|
||||||
local target = self.confirming
|
local target = confirming
|
||||||
self.confirming = nil
|
confirming = nil
|
||||||
if target then
|
if target then
|
||||||
node.setPressed(target, false)
|
tree.setPressed(target, false)
|
||||||
local handler = clickHandlers[target]
|
local handler = clickHandlers[target]
|
||||||
if handler then
|
if handler then
|
||||||
handler(target)
|
handler(target)
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
local ui = require "ui"
|
||||||
|
local FONT = screen.FONT_UI
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local KEY_W, KEY_H, KEY_GAP, ROW_GAP = 26, 30, 4, 5
|
||||||
|
local SIDE_W, MODE_W, SPACE_W, OK_W = 40, 54, 174, 58
|
||||||
|
local PAGES = {
|
||||||
|
lower = { "qwertyuiop", "asdfghjkl", "zxcvbnm" },
|
||||||
|
upper = { "QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM" },
|
||||||
|
numbers = { "1234567890", "-/:;()$&@", ".,?!'" },
|
||||||
|
symbols = { "[]{}#%^*+=", "_\\|~<>$&@", ".,?!'" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Keyed by node id, because the node itself is sixteen bytes in the firmware and holds
|
||||||
|
-- nothing a keyboard cares about.
|
||||||
|
local state = {}
|
||||||
|
|
||||||
|
local function chars(page, row)
|
||||||
|
return PAGES[page][row]
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rowWidth(count)
|
||||||
|
return count * KEY_W + (count - 1) * KEY_GAP
|
||||||
|
end
|
||||||
|
|
||||||
|
local function centeredX(rect, width)
|
||||||
|
return rect.x + (rect.w - width) // 2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function thirdRow(page, rect)
|
||||||
|
local value = chars(page, 3)
|
||||||
|
local width = SIDE_W * 2 + KEY_GAP * 2 + rowWidth(#value)
|
||||||
|
return value, centeredX(rect, width)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Pure geometry: which key covers a point, given a page and the rectangle the board was
|
||||||
|
-- placed in. Kept free of the node tree so the host tests can exercise the maths that
|
||||||
|
-- actually decides what a tap enters.
|
||||||
|
function M.keyAt(page, rect, x, y)
|
||||||
|
local localY = y - rect.y
|
||||||
|
if localY < 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local row = localY // (KEY_H + ROW_GAP) + 1
|
||||||
|
if row > 4 or localY % (KEY_H + ROW_GAP) >= KEY_H then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if row <= 2 then
|
||||||
|
local value = chars(page, row)
|
||||||
|
local localX = x - centeredX(rect, rowWidth(#value))
|
||||||
|
if localX < 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local column = localX // (KEY_W + KEY_GAP) + 1
|
||||||
|
if column <= #value and localX % (KEY_W + KEY_GAP) < KEY_W then
|
||||||
|
return (row - 1) * 10 + column, "char", value:sub(column, column)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if row == 3 then
|
||||||
|
local value, start = thirdRow(page, rect)
|
||||||
|
local localX = x - start
|
||||||
|
if localX < 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if localX < SIDE_W then
|
||||||
|
return 90, (page == "lower" or page == "upper") and "shift" or "symbols"
|
||||||
|
end
|
||||||
|
localX = localX - SIDE_W - KEY_GAP
|
||||||
|
local width = rowWidth(#value)
|
||||||
|
if localX >= 0 and localX < width then
|
||||||
|
local column = localX // (KEY_W + KEY_GAP) + 1
|
||||||
|
if localX % (KEY_W + KEY_GAP) < KEY_W then
|
||||||
|
return 20 + column, "char", value:sub(column, column)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
localX = localX - width - KEY_GAP
|
||||||
|
if localX >= 0 and localX < SIDE_W then
|
||||||
|
return 91, "backspace"
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local width = MODE_W + SPACE_W + OK_W + KEY_GAP * 2
|
||||||
|
local localX = x - centeredX(rect, width)
|
||||||
|
if localX < 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if localX < MODE_W then
|
||||||
|
return 100, "mode"
|
||||||
|
end
|
||||||
|
localX = localX - MODE_W - KEY_GAP
|
||||||
|
if localX >= 0 and localX < SPACE_W then
|
||||||
|
return 101, "space"
|
||||||
|
end
|
||||||
|
localX = localX - SPACE_W - KEY_GAP
|
||||||
|
if localX >= 0 and localX < OK_W then
|
||||||
|
return 102, "submit"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawArrow(x, y, width, color, down)
|
||||||
|
local middle = x + width // 2
|
||||||
|
if down then
|
||||||
|
screen.drawLine(middle, y + 8, middle, y + 20, color)
|
||||||
|
screen.drawLine(middle - 5, y + 15, middle, y + 20, color)
|
||||||
|
screen.drawLine(middle, y + 20, middle + 5, y + 15, color)
|
||||||
|
else
|
||||||
|
screen.drawLine(middle, y + 9, middle, y + 21, color)
|
||||||
|
screen.drawLine(middle - 5, y + 14, middle, y + 9, color)
|
||||||
|
screen.drawLine(middle, y + 9, middle + 5, y + 14, color)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawKey(id, label, x, y, width, pressed)
|
||||||
|
local theme = ui.theme
|
||||||
|
local face = pressed and theme.pressedFace or theme.face
|
||||||
|
local color = pressed and theme.pressedColor or theme.color
|
||||||
|
screen.roundRect(x, y, width, KEY_H, theme.radius, theme.background, face, nil, color)
|
||||||
|
if label == "shift" then
|
||||||
|
drawArrow(x, y, width, color, state[id].page == "upper")
|
||||||
|
else
|
||||||
|
screen.drawText(
|
||||||
|
FONT,
|
||||||
|
x + (width - screen.getTextWidth(FONT, label)) // 2,
|
||||||
|
y + (KEY_H - screen.getFontHeight(FONT)) // 2,
|
||||||
|
label,
|
||||||
|
color,
|
||||||
|
nil,
|
||||||
|
face
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Walks every key, handing each one to `visit`. Painting the whole board and repainting a
|
||||||
|
-- single key are the same traversal, so a key's position is defined in one place. Pure,
|
||||||
|
-- for the same reason keyAt is.
|
||||||
|
function M.eachKey(page, rect, visit)
|
||||||
|
local y = rect.y
|
||||||
|
|
||||||
|
for row = 1, 2 do
|
||||||
|
local value = chars(page, row)
|
||||||
|
local x = centeredX(rect, rowWidth(#value))
|
||||||
|
for column = 1, #value do
|
||||||
|
visit((row - 1) * 10 + column, value:sub(column, column), x, y, KEY_W)
|
||||||
|
x = x + KEY_W + KEY_GAP
|
||||||
|
end
|
||||||
|
y = y + KEY_H + ROW_GAP
|
||||||
|
end
|
||||||
|
|
||||||
|
local value, x = thirdRow(page, rect)
|
||||||
|
visit(90, (page == "lower" or page == "upper") and "shift" or (page == "numbers" and "#+=" or "123"), x, y, SIDE_W)
|
||||||
|
x = x + SIDE_W + KEY_GAP
|
||||||
|
for column = 1, #value do
|
||||||
|
visit(20 + column, value:sub(column, column), x, y, KEY_W)
|
||||||
|
x = x + KEY_W + KEY_GAP
|
||||||
|
end
|
||||||
|
visit(91, "<-", x, y, SIDE_W)
|
||||||
|
|
||||||
|
y = y + KEY_H + ROW_GAP
|
||||||
|
local width = MODE_W + SPACE_W + OK_W + KEY_GAP * 2
|
||||||
|
x = centeredX(rect, width)
|
||||||
|
visit(100, (page == "lower" or page == "upper") and "123" or "ABC", x, y, MODE_W)
|
||||||
|
x = x + MODE_W + KEY_GAP
|
||||||
|
visit(101, "space", x, y, SPACE_W)
|
||||||
|
x = x + SPACE_W + KEY_GAP
|
||||||
|
visit(102, "OK", x, y, OK_W)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rectOf(id)
|
||||||
|
local x, y, w, h = tree.getRect(id)
|
||||||
|
return { x = x, y = y, w = w, h = h }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function paint(id)
|
||||||
|
M.eachKey(state[id].page, rectOf(id), function(key, label, x, y, width)
|
||||||
|
drawKey(id, label, x, y, width, false)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Repaints one key in place. The keyboard is a single node, so there is no parent to
|
||||||
|
-- clear and nothing else on screen can have moved.
|
||||||
|
local function drawOneKey(id, target, pressed)
|
||||||
|
M.eachKey(state[id].page, rectOf(id), function(key, label, x, y, width)
|
||||||
|
if key == target then
|
||||||
|
drawKey(id, label, x, y, width, pressed)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function down(id, x, y)
|
||||||
|
local key = M.keyAt(state[id].page, rectOf(id), x, y)
|
||||||
|
state[id].activeKey = key
|
||||||
|
if key then
|
||||||
|
drawOneKey(id, key, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The release repaints the key but keeps activeKey, because ui fires on_exit before
|
||||||
|
-- on_click and the click still has to know which key the press started on.
|
||||||
|
local function unpress(id)
|
||||||
|
local key = state[id].activeKey
|
||||||
|
if key then
|
||||||
|
drawOneKey(id, key, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function press(id, x, y)
|
||||||
|
local st = state[id]
|
||||||
|
local key, action, char = M.keyAt(st.page, rectOf(id), x, y)
|
||||||
|
if not key or key ~= st.activeKey then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
st.activeKey = nil
|
||||||
|
|
||||||
|
local function changed()
|
||||||
|
if st.on_change then
|
||||||
|
st.on_change(st.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if action == "char" then
|
||||||
|
if #st.value < st.max_length then
|
||||||
|
st.value = st.value .. char
|
||||||
|
changed()
|
||||||
|
end
|
||||||
|
elseif action == "shift" then
|
||||||
|
st.page = st.page == "lower" and "upper" or "lower"
|
||||||
|
tree.invalidate(id)
|
||||||
|
elseif action == "symbols" then
|
||||||
|
st.page = st.page == "numbers" and "symbols" or "numbers"
|
||||||
|
tree.invalidate(id)
|
||||||
|
elseif action == "mode" then
|
||||||
|
st.page = (st.page == "lower" or st.page == "upper") and "numbers" or "lower"
|
||||||
|
tree.invalidate(id)
|
||||||
|
elseif action == "backspace" then
|
||||||
|
st.value = st.value:sub(1, -2)
|
||||||
|
changed()
|
||||||
|
elseif action == "space" then
|
||||||
|
if #st.value < st.max_length then
|
||||||
|
st.value = st.value .. " "
|
||||||
|
changed()
|
||||||
|
end
|
||||||
|
elseif action == "submit" and st.on_submit then
|
||||||
|
st.on_submit(st.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One custom-painted node keeps a full keyboard off the heap: ordinary ui.button keys
|
||||||
|
-- would add dozens of nodes and their styles while wifi is already holding buffers.
|
||||||
|
function M.new(spec)
|
||||||
|
spec = spec or {}
|
||||||
|
local id = ui.custom {
|
||||||
|
h = 4 * KEY_H + 3 * ROW_GAP,
|
||||||
|
paint = paint,
|
||||||
|
on_enter = down,
|
||||||
|
on_click = press,
|
||||||
|
on_exit = unpress,
|
||||||
|
press_style = false, -- a key highlights itself; the node never does
|
||||||
|
}
|
||||||
|
state[id] = {
|
||||||
|
value = spec.value or "",
|
||||||
|
max_length = spec.max_length or 64,
|
||||||
|
page = "lower",
|
||||||
|
on_change = spec.on_change,
|
||||||
|
on_submit = spec.on_submit,
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
-- A message that slides up from the bottom edge, holds, then slides back down.
|
||||||
|
--
|
||||||
|
-- The card sits inside a scroll container of its own height whose content is twice that:
|
||||||
|
-- an empty strip above the card. Sliding is therefore ui.setScroll() on the tree already
|
||||||
|
-- laid out -- a delta on stored coordinates -- rather than a rebuild a frame, and the
|
||||||
|
-- container scissors the half of the card that has not arrived yet.
|
||||||
|
--
|
||||||
|
-- Whatever mounts the tree includes toast.node() and calls toast.draw(deltaMs); show()
|
||||||
|
-- rebuilds so the node exists, and hide() rebuilds to drop it.
|
||||||
|
|
||||||
|
local ui = require "ui"
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local HEIGHT = 44
|
||||||
|
local MARGIN = 12
|
||||||
|
local SLIDE_MS = 180
|
||||||
|
|
||||||
|
local text, box
|
||||||
|
-- nil, or "in" / "hold" / "out" with the milliseconds spent in it.
|
||||||
|
local phase, elapsed, holdMs
|
||||||
|
|
||||||
|
---@param message string
|
||||||
|
---@param durationMs? integer How long the card holds before it slides away.
|
||||||
|
function M.show(message, durationMs)
|
||||||
|
text, holdMs = message, durationMs or 1500
|
||||||
|
phase, elapsed = "in", 0
|
||||||
|
ui.rebuild()
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.hide()
|
||||||
|
if phase then
|
||||||
|
phase, text, box = nil, nil, nil
|
||||||
|
ui.rebuild()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return NodeId|nil
|
||||||
|
function M.node()
|
||||||
|
if not phase then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
box = ui.box {
|
||||||
|
at = { x = MARGIN, y = screen.getHeight() - HEIGHT - MARGIN },
|
||||||
|
w = screen.getWidth() - 2 * MARGIN,
|
||||||
|
h = HEIGHT,
|
||||||
|
scrollY = true,
|
||||||
|
ui.box {
|
||||||
|
w = "fill",
|
||||||
|
ui.spacer { w = "fill", h = HEIGHT },
|
||||||
|
ui.box {
|
||||||
|
w = "fill",
|
||||||
|
h = HEIGHT,
|
||||||
|
pad = 12,
|
||||||
|
align = "center",
|
||||||
|
justify = "center",
|
||||||
|
background = ui.theme.face,
|
||||||
|
border = ui.theme.muted,
|
||||||
|
ui.text(text),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return box
|
||||||
|
end
|
||||||
|
|
||||||
|
local function progress()
|
||||||
|
if phase == "in" then
|
||||||
|
return elapsed / SLIDE_MS
|
||||||
|
elseif phase == "out" then
|
||||||
|
return 1 - elapsed / SLIDE_MS
|
||||||
|
end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
---Advances the slide. Safe to call every frame whether or not a toast is up.
|
||||||
|
---@param deltaMs integer
|
||||||
|
function M.draw(deltaMs)
|
||||||
|
if not phase then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
elapsed = elapsed + deltaMs
|
||||||
|
if phase == "in" and elapsed >= SLIDE_MS then
|
||||||
|
phase, elapsed = "hold", 0
|
||||||
|
elseif phase == "hold" and elapsed >= holdMs then
|
||||||
|
phase, elapsed = "out", 0
|
||||||
|
elseif phase == "out" and elapsed >= SLIDE_MS then
|
||||||
|
M.hide()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if box then
|
||||||
|
ui.setScroll(box, 0, math.floor(HEIGHT * progress()))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
+3
-3
@@ -3,7 +3,7 @@ package.path = "./lua/lib/?.lua;" .. package.path
|
|||||||
local drawn = {}
|
local drawn = {}
|
||||||
local roles = { "confirm", "back", "right" }
|
local roles = { "confirm", "back", "right" }
|
||||||
|
|
||||||
gui = {
|
screen = {
|
||||||
FONT_SMALL = 0,
|
FONT_SMALL = 0,
|
||||||
STYLE_NORMAL = 0,
|
STYLE_NORMAL = 0,
|
||||||
color = function(r, g, b)
|
color = function(r, g, b)
|
||||||
@@ -29,8 +29,8 @@ gui = {
|
|||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
input = {
|
buttons = {
|
||||||
getButtons = function()
|
getAll = function()
|
||||||
return roles
|
return roles
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-43
@@ -17,9 +17,17 @@ fs = {
|
|||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local savedTheme = "light"
|
||||||
local frameWidth, frameHeight = 320, 480
|
local frameWidth, frameHeight = 320, 480
|
||||||
|
|
||||||
gui = {
|
screen = {
|
||||||
|
getTheme = function()
|
||||||
|
return savedTheme
|
||||||
|
end,
|
||||||
|
setTheme = function(name)
|
||||||
|
savedTheme = name
|
||||||
|
return true
|
||||||
|
end,
|
||||||
FONT_SMALL = 0,
|
FONT_SMALL = 0,
|
||||||
FONT_UI = 1,
|
FONT_UI = 1,
|
||||||
FONT_BODY = 2,
|
FONT_BODY = 2,
|
||||||
@@ -59,7 +67,7 @@ local function interactiveNodes()
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
node = {
|
tree = {
|
||||||
reset = function()
|
reset = function()
|
||||||
nodes, focus, buttonCount = {}, nil, 0
|
nodes, focus, buttonCount = {}, nil, 0
|
||||||
end,
|
end,
|
||||||
@@ -84,6 +92,9 @@ node = {
|
|||||||
attach = function(parent, child)
|
attach = function(parent, child)
|
||||||
nodes[child].parent = parent
|
nodes[child].parent = parent
|
||||||
end,
|
end,
|
||||||
|
getParent = function(id)
|
||||||
|
return nodes[id].parent
|
||||||
|
end,
|
||||||
setSize = function() end,
|
setSize = function() end,
|
||||||
setStyle = function(id, style)
|
setStyle = function(id, style)
|
||||||
for key, value in pairs(style) do
|
for key, value in pairs(style) do
|
||||||
@@ -166,7 +177,7 @@ assert(table.concat(ui.themeNames(), ",") == "dark,light,mono")
|
|||||||
local ok, err = ui.setTheme "missing"
|
local ok, err = ui.setTheme "missing"
|
||||||
assert(ok == nil and err == "Unknown theme")
|
assert(ok == nil and err == "Unknown theme")
|
||||||
assert(ui.setTheme "dark" == true)
|
assert(ui.setTheme "dark" == true)
|
||||||
assert(files["/.lua/theme"] == "dark" and ui.getTheme() == "dark")
|
assert(savedTheme == "dark" and ui.getTheme() == "dark")
|
||||||
|
|
||||||
local events = {}
|
local events = {}
|
||||||
local function handler(name)
|
local function handler(name)
|
||||||
@@ -175,28 +186,31 @@ local function handler(name)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local first = ui.button {
|
local first, second
|
||||||
label = "one",
|
ui.mount(function()
|
||||||
on_enter = handler "enter",
|
first = ui.button {
|
||||||
on_exit = handler "exit",
|
label = "one",
|
||||||
on_click = handler "click",
|
on_enter = handler "enter",
|
||||||
}
|
on_exit = handler "exit",
|
||||||
local second = ui.button {
|
on_click = handler "click",
|
||||||
label = "two",
|
}
|
||||||
on_enter = handler "enter",
|
second = ui.button {
|
||||||
on_exit = handler "exit",
|
label = "two",
|
||||||
on_click = handler "click",
|
on_enter = handler "enter",
|
||||||
}
|
on_exit = handler "exit",
|
||||||
local screen = ui.screen(ui.box { row = true, first, second })
|
on_click = handler "click",
|
||||||
|
}
|
||||||
|
return ui.box { row = true, first, second }
|
||||||
|
end)
|
||||||
|
|
||||||
assert(screen:down(10, 10))
|
assert(ui.down(10, 10))
|
||||||
assert(node.isPressed(first))
|
assert(tree.isPressed(first))
|
||||||
assert(screen:move(95, 10))
|
assert(ui.move(95, 10))
|
||||||
assert(not node.isPressed(first))
|
assert(not tree.isPressed(first))
|
||||||
assert(screen:move(10, 10))
|
assert(ui.move(10, 10))
|
||||||
assert(node.isPressed(first))
|
assert(tree.isPressed(first))
|
||||||
assert(screen:up(10, 10))
|
assert(ui.up(10, 10))
|
||||||
assert(not node.isPressed(first))
|
assert(not tree.isPressed(first))
|
||||||
|
|
||||||
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
|
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
|
||||||
for index, name in ipairs(expectedTouch) do
|
for index, name in ipairs(expectedTouch) do
|
||||||
@@ -207,16 +221,16 @@ for index, name in ipairs(expectedTouch) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
events = {}
|
events = {}
|
||||||
assert(screen:button("right", true))
|
assert(ui.buttonPress("right", true))
|
||||||
assert(screen:button("right", false))
|
assert(ui.buttonPress("right", false))
|
||||||
assert(node.getFocus() == first)
|
assert(tree.getFocus() == first)
|
||||||
assert(screen:button("right", true))
|
assert(ui.buttonPress("right", true))
|
||||||
assert(node.getFocus() == second)
|
assert(tree.getFocus() == second)
|
||||||
assert(screen:button("confirm", true))
|
assert(ui.buttonPress("confirm", true))
|
||||||
assert(node.isPressed(second))
|
assert(tree.isPressed(second))
|
||||||
assert(screen:button("confirm", false))
|
assert(ui.buttonPress("confirm", false))
|
||||||
assert(not node.isPressed(second))
|
assert(not tree.isPressed(second))
|
||||||
assert(screen:button("back", true) == false)
|
assert(ui.buttonPress("back", true) == false)
|
||||||
|
|
||||||
local expectedButtons = {
|
local expectedButtons = {
|
||||||
{ "enter", first },
|
{ "enter", first },
|
||||||
@@ -235,7 +249,13 @@ assert(ui.setTheme "mono" == true)
|
|||||||
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
||||||
assert(cleared == ui.theme.background)
|
assert(cleared == ui.theme.background)
|
||||||
|
|
||||||
screen:draw()
|
ui.draw()
|
||||||
|
|
||||||
|
-- The inset chrome took comes off the height budget before anything else.
|
||||||
|
ui.setInset(44)
|
||||||
|
assert(select(2, ui.frame()) == 436, "the frame is the panel minus the chrome")
|
||||||
|
assert(select(1, ui.cardSide(5, 12, 8)) == 132, "a grid fits the app's box, not the panel")
|
||||||
|
ui.setInset(0)
|
||||||
|
|
||||||
-- 320x480 portrait: two columns, and the reserve comes off the height budget.
|
-- 320x480 portrait: two columns, and the reserve comes off the height budget.
|
||||||
local side, columns = ui.cardSide(5, 12, 8)
|
local side, columns = ui.cardSide(5, 12, 8)
|
||||||
@@ -251,24 +271,35 @@ frameWidth, frameHeight = 320, 480
|
|||||||
-- still is. Which node was hit is geometry, so the test names the target directly.
|
-- still is. Which node was hit is geometry, so the test names the target directly.
|
||||||
ui.reset()
|
ui.reset()
|
||||||
local pressedCalls = {}
|
local pressedCalls = {}
|
||||||
node.setPressed = function(_, on)
|
tree.setPressed = function(_, on)
|
||||||
pressedCalls[#pressedCalls + 1] = on
|
pressedCalls[#pressedCalls + 1] = on
|
||||||
end
|
end
|
||||||
local own = ui.custom { h = 20, press_style = false, on_click = function() end }
|
local own, styled
|
||||||
local styled = ui.button { h = 20, label = "ok", on_click = function() end }
|
ui.mount(function()
|
||||||
local board = ui.screen(ui.box { own, styled })
|
own = ui.custom { h = 20, press_style = false, on_click = function() end }
|
||||||
|
styled = ui.button { h = 20, label = "ok", on_click = function() end }
|
||||||
|
return ui.box { own, styled }
|
||||||
|
end)
|
||||||
local target
|
local target
|
||||||
node.hit = function()
|
tree.hit = function()
|
||||||
return target
|
return target
|
||||||
end
|
end
|
||||||
|
|
||||||
target = own
|
target = own
|
||||||
board:down(0, 0)
|
ui.down(0, 0)
|
||||||
board:up(0, 0)
|
ui.up(0, 0)
|
||||||
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
|
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
|
||||||
|
|
||||||
target = styled
|
target = styled
|
||||||
board:down(0, 0)
|
ui.down(0, 0)
|
||||||
assert(pressedCalls[1] == true, "an ordinary widget still gets its pressed style")
|
assert(pressedCalls[1] == true, "an ordinary widget still gets its pressed style")
|
||||||
|
|
||||||
|
-- Building outside a rebuild would reset the arena under the screen on the panel.
|
||||||
|
local built = pcall(ui.button, { label = "stray" })
|
||||||
|
assert(not built, "a node built after layout is refused")
|
||||||
|
local rebuilt = false
|
||||||
|
ui.rebuild()
|
||||||
|
rebuilt = true
|
||||||
|
assert(rebuilt and ui.down(0, 0), "a rebuild replaces the screen and keeps dispatch live")
|
||||||
|
|
||||||
print "ok"
|
print "ok"
|
||||||
|
|||||||
+120
-13
@@ -32,10 +32,11 @@ enum Type : uint8_t { BOX, TEXT, BUTTON, CUSTOM };
|
|||||||
|
|
||||||
enum Flag : uint8_t {
|
enum Flag : uint8_t {
|
||||||
ROW = 1 << 0, // main axis is horizontal
|
ROW = 1 << 0, // main axis is horizontal
|
||||||
CAPTURE = 1 << 1, // swallows the taps its children missed
|
|
||||||
INTERACTIVE = 1 << 2, // has an on_press
|
INTERACTIVE = 1 << 2, // has an on_press
|
||||||
DIRTY = 1 << 3,
|
DIRTY = 1 << 3,
|
||||||
PRESSED = 1 << 4,
|
PRESSED = 1 << 4,
|
||||||
|
SCROLL_X = 1 << 5, // children measure unbounded across, and pan horizontally
|
||||||
|
SCROLL_Y = 1 << 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum SizeMode : uint8_t { AUTO, PX, FRACTION, FILL };
|
enum SizeMode : uint8_t { AUTO, PX, FRACTION, FILL };
|
||||||
@@ -118,6 +119,15 @@ struct Spec {
|
|||||||
uint16_t last = NONE; // tail of the child list, so append is not a walk
|
uint16_t last = NONE; // tail of the child list, so append is not a walk
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A scroll container's pan offset and the size of what it holds. Sparse like
|
||||||
|
// styles, because a screen has one or two of these and Node has no room: the
|
||||||
|
// offsets are what setScroll() clamps against, and the content size is the only
|
||||||
|
// thing measure() learns that place() would otherwise throw away.
|
||||||
|
struct Scroll {
|
||||||
|
int16_t x = 0, y = 0;
|
||||||
|
int16_t contentW = 0, contentH = 0;
|
||||||
|
};
|
||||||
|
|
||||||
// Resistive panels land a few pixels off, so a hit box is larger than what was
|
// Resistive panels land a few pixels off, so a hit box is larger than what was
|
||||||
// painted.
|
// painted.
|
||||||
constexpr int SLOP = 4;
|
constexpr int SLOP = 4;
|
||||||
@@ -145,6 +155,7 @@ public:
|
|||||||
labelAt.clear();
|
labelAt.clear();
|
||||||
labels.clear();
|
labels.clear();
|
||||||
styles.clear();
|
styles.clear();
|
||||||
|
scrollState.clear();
|
||||||
error = nullptr;
|
error = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,8 +203,10 @@ public:
|
|||||||
return error == nullptr;
|
return error == nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deepest interactive node wins, so a tappable child beats its tappable
|
// Deepest node by geometry wins, so a child beats its parent and a later sibling
|
||||||
// parent. The list is singly linked, so "last match walking forward" stands
|
// beats an earlier one it overlaps. Whether that node responds to a press or a pan is
|
||||||
|
// dispatch's business, which bubbles up from here -- hit-testing is not fused with who
|
||||||
|
// handles the gesture. The list is singly linked, so "last match walking forward" stands
|
||||||
// in for "first match walking backward"; they name the same node.
|
// in for "first match walking backward"; they name the same node.
|
||||||
uint16_t hit(uint16_t id, int px, int py) const {
|
uint16_t hit(uint16_t id, int px, int py) const {
|
||||||
const Node& n = nodes[id];
|
const Node& n = nodes[id];
|
||||||
@@ -210,9 +223,65 @@ public:
|
|||||||
}
|
}
|
||||||
if (found != NONE)
|
if (found != NONE)
|
||||||
return found;
|
return found;
|
||||||
if (n.flags & CAPTURE)
|
return id;
|
||||||
return id;
|
}
|
||||||
return (n.flags & INTERACTIVE) ? id : NONE;
|
|
||||||
|
bool scrolls(uint16_t id) const {
|
||||||
|
return (nodes[id].flags & (SCROLL_X | SCROLL_Y)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Scroll* scrollOf(uint16_t id) const {
|
||||||
|
for (size_t at = 0; at < scrollState.size(); at++) {
|
||||||
|
if (scrollState[at].first == id)
|
||||||
|
return &scrollState[at].second;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// How far each axis can pan before the content's far edge reaches the box.
|
||||||
|
void scrollRange(uint16_t id, int& maxX, int& maxY) const {
|
||||||
|
maxX = maxY = 0;
|
||||||
|
const Scroll* s = scrollOf(id);
|
||||||
|
if (!s)
|
||||||
|
return;
|
||||||
|
if (nodes[id].flags & SCROLL_X)
|
||||||
|
maxX = s->contentW - nodes[id].w;
|
||||||
|
if (nodes[id].flags & SCROLL_Y)
|
||||||
|
maxY = s->contentH - nodes[id].h;
|
||||||
|
if (maxX < 0)
|
||||||
|
maxX = 0;
|
||||||
|
if (maxY < 0)
|
||||||
|
maxY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pans the subtree, clamped to the content. Applied as a delta to the stored
|
||||||
|
// coordinates rather than by placing again: place() reads Spec, which
|
||||||
|
// dropScratch() has already thrown away, and shifting keeps x/y in screen
|
||||||
|
// space so hit testing, getRect and every paint routine stay unchanged.
|
||||||
|
void setScroll(uint16_t id, int x, int y) {
|
||||||
|
if (!scrolls(id))
|
||||||
|
return;
|
||||||
|
Scroll* state = mutableScroll(id);
|
||||||
|
if (!state)
|
||||||
|
return;
|
||||||
|
int maxX = 0, maxY = 0;
|
||||||
|
scrollRange(id, maxX, maxY);
|
||||||
|
if (x < 0)
|
||||||
|
x = 0;
|
||||||
|
if (y < 0)
|
||||||
|
y = 0;
|
||||||
|
if (x > maxX)
|
||||||
|
x = maxX;
|
||||||
|
if (y > maxY)
|
||||||
|
y = maxY;
|
||||||
|
const int dx = x - state->x, dy = y - state->y;
|
||||||
|
if (dx == 0 && dy == 0)
|
||||||
|
return;
|
||||||
|
state->x = static_cast<int16_t>(x);
|
||||||
|
state->y = static_cast<int16_t>(y);
|
||||||
|
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next)
|
||||||
|
shift(c, -dx, -dy);
|
||||||
|
nodes[id].flags |= DIRTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout inputs are dead once place() has run. Callers drop them here rather
|
// Layout inputs are dead once place() has run. Callers drop them here rather
|
||||||
@@ -302,6 +371,25 @@ private:
|
|||||||
std::vector<uint16_t> labelAt;
|
std::vector<uint16_t> labelAt;
|
||||||
std::vector<char> labels;
|
std::vector<char> labels;
|
||||||
std::vector<std::pair<uint16_t, Style>> styles;
|
std::vector<std::pair<uint16_t, Style>> styles;
|
||||||
|
// Linear rather than the sorted search styles use: a screen has one or two
|
||||||
|
// scroll containers, so the binary search would cost more than the scan.
|
||||||
|
std::vector<std::pair<uint16_t, Scroll>> scrollState;
|
||||||
|
|
||||||
|
Scroll* mutableScroll(uint16_t id) {
|
||||||
|
for (size_t at = 0; at < scrollState.size(); at++) {
|
||||||
|
if (scrollState[at].first == id)
|
||||||
|
return &scrollState[at].second;
|
||||||
|
}
|
||||||
|
scrollState.push_back(std::make_pair(id, Scroll()));
|
||||||
|
return &scrollState.back().second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shift(uint16_t id, int dx, int dy) {
|
||||||
|
nodes[id].x = static_cast<int16_t>(nodes[id].x + dx);
|
||||||
|
nodes[id].y = static_cast<int16_t>(nodes[id].y + dy);
|
||||||
|
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next)
|
||||||
|
shift(c, dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
void fail(const char* message) {
|
void fail(const char* message) {
|
||||||
if (!error)
|
if (!error)
|
||||||
@@ -345,6 +433,22 @@ private:
|
|||||||
// by its content cannot tell a child what fraction of it to take.
|
// by its content cannot tell a child what fraction of it to take.
|
||||||
int innerH = h != UNKNOWN ? h - s.padT - s.padB : UNKNOWN;
|
int innerH = h != UNKNOWN ? h - s.padT - s.padB : UNKNOWN;
|
||||||
|
|
||||||
|
// A scrolled axis is unbounded for the children, so they take their natural
|
||||||
|
// size and the content is free to overflow the box. The box itself still
|
||||||
|
// needs a size of its own on that axis -- one derived from the content
|
||||||
|
// would grow to fit it and never scroll.
|
||||||
|
const uint8_t scrollFlags = nodes[id].flags & (SCROLL_X | SCROLL_Y);
|
||||||
|
if (scrollFlags & SCROLL_X) {
|
||||||
|
if (w == UNKNOWN)
|
||||||
|
fail("scroll-x needs a width");
|
||||||
|
innerW = UNKNOWN;
|
||||||
|
}
|
||||||
|
if (scrollFlags & SCROLL_Y) {
|
||||||
|
if (h == UNKNOWN)
|
||||||
|
fail("scroll-y needs a height");
|
||||||
|
innerH = UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
int main = 0, cross = 0, count = 0;
|
int main = 0, cross = 0, count = 0;
|
||||||
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) {
|
for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) {
|
||||||
measure(c, innerW, innerH);
|
measure(c, innerW, innerH);
|
||||||
@@ -373,16 +477,19 @@ private:
|
|||||||
cross = row ? s.intrinsicH : s.intrinsicW;
|
cross = row ? s.intrinsicH : s.intrinsicW;
|
||||||
}
|
}
|
||||||
|
|
||||||
Node& self = nodes[id];
|
|
||||||
int along = main + (row ? s.padL + s.padR : s.padT + s.padB);
|
int along = main + (row ? s.padL + s.padR : s.padT + s.padB);
|
||||||
int across = cross + (row ? s.padT + s.padB : s.padL + s.padR);
|
int across = cross + (row ? s.padT + s.padB : s.padL + s.padR);
|
||||||
if (row) {
|
const int contentW = row ? along : across;
|
||||||
self.w = static_cast<int16_t>(w != UNKNOWN ? w : along);
|
const int contentH = row ? across : along;
|
||||||
self.h = static_cast<int16_t>(h != UNKNOWN ? h : across);
|
if (scrollFlags) {
|
||||||
} else {
|
Scroll* state = mutableScroll(id);
|
||||||
self.w = static_cast<int16_t>(w != UNKNOWN ? w : across);
|
state->contentW = static_cast<int16_t>(contentW);
|
||||||
self.h = static_cast<int16_t>(h != UNKNOWN ? h : along);
|
state->contentH = static_cast<int16_t>(contentH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Node& self = nodes[id];
|
||||||
|
self.w = static_cast<int16_t>(w != UNKNOWN ? w : contentW);
|
||||||
|
self.h = static_cast<int16_t>(h != UNKNOWN ? h : contentH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void place(uint16_t id, int x, int y, int w, int h) {
|
void place(uint16_t id, int x, int y, int w, int h) {
|
||||||
|
|||||||
@@ -35,15 +35,6 @@ struct MemoryInfo {
|
|||||||
int32_t largestFreeBlock;
|
int32_t largestFreeBlock;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsProvider {
|
|
||||||
public:
|
|
||||||
virtual ~SettingsProvider() = default;
|
|
||||||
virtual int32_t rotation() const = 0;
|
|
||||||
virtual Status setRotation(int32_t degrees) = 0;
|
|
||||||
virtual std::string timezone() const = 0;
|
|
||||||
virtual Status setTimezone(const std::string& timezone) = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only what the firmware alone can answer. App identity, titles, data paths,
|
// Only what the firmware alone can answer. App identity, titles, data paths,
|
||||||
// feature reporting and navigation are the runtime's, because it owns app
|
// feature reporting and navigation are the runtime's, because it owns app
|
||||||
// loading and knows which providers exist.
|
// loading and knows which providers exist.
|
||||||
@@ -53,6 +44,8 @@ public:
|
|||||||
virtual int32_t millis() const = 0;
|
virtual int32_t millis() const = 0;
|
||||||
virtual MemoryInfo memory() const = 0;
|
virtual MemoryInfo memory() const = 0;
|
||||||
virtual bool isClockSynced() const = 0;
|
virtual bool isClockSynced() const = 0;
|
||||||
|
virtual std::string timezone() const = 0;
|
||||||
|
virtual Status setTimezone(const std::string& timezone) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scripts are streamed rather than slurped: a whole module in one buffer needs
|
// Scripts are streamed rather than slurped: a whole module in one buffer needs
|
||||||
@@ -111,7 +104,13 @@ public:
|
|||||||
virtual int32_t width() const = 0;
|
virtual int32_t width() const = 0;
|
||||||
virtual int32_t height() const = 0;
|
virtual int32_t height() const = 0;
|
||||||
virtual int32_t rotation() const = 0;
|
virtual int32_t rotation() const = 0;
|
||||||
virtual void setRotation(int32_t degrees) = 0;
|
// Applies the rotation and persists it, so the panel comes back the way the
|
||||||
|
// user left it with no second call to forget.
|
||||||
|
virtual Status setRotation(int32_t degrees) = 0;
|
||||||
|
// Only the name of a palette; the colours themselves live in Lua, so the
|
||||||
|
// firmware can read the saved theme before a lua_State exists.
|
||||||
|
virtual std::string theme() const = 0;
|
||||||
|
virtual Status setTheme(const std::string& theme) = 0;
|
||||||
virtual int32_t color(int32_t r, int32_t g, int32_t b) const = 0;
|
virtual int32_t color(int32_t r, int32_t g, int32_t b) const = 0;
|
||||||
virtual void clear(int32_t color) = 0;
|
virtual void clear(int32_t color) = 0;
|
||||||
virtual void fillRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
virtual void fillRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||||
@@ -132,15 +131,42 @@ public:
|
|||||||
virtual void roundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
virtual void roundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||||
int32_t radius, int32_t background, const int32_t* top,
|
int32_t radius, int32_t background, const int32_t* top,
|
||||||
const int32_t* bottom, const int32_t* border) = 0;
|
const int32_t* bottom, const int32_t* border) = 0;
|
||||||
// Hands the app the whole panel, including whatever chrome the firmware
|
|
||||||
// paints.
|
|
||||||
virtual void setFullscreen(bool on) = 0;
|
|
||||||
// Applies everything drawn since the last commit. The runtime supplies only
|
// Applies everything drawn since the last commit. The runtime supplies only
|
||||||
// the timing -- the end of a callback batch -- because that is the one fact a
|
// the timing -- the end of a callback batch -- because that is the one fact a
|
||||||
// driver cannot know; which region to touch, which waveform, and whether to
|
// driver cannot know; which region to touch, which waveform, and whether to
|
||||||
// clean up ghosting all stay here. A live LCD has nothing pending and does
|
// clean up ghosting all stay here. A live LCD has nothing pending and does
|
||||||
// nothing.
|
// nothing.
|
||||||
virtual void commit() = 0;
|
virtual void commit() = 0;
|
||||||
|
// Offscreen band for composited repaints. beginBuffer opens a RAM surface
|
||||||
|
// covering the screen rectangle (x, y, w, h); every subsequent draw keeps its
|
||||||
|
// screen coordinates (the driver translates into the band) and clips to it,
|
||||||
|
// so a caller re-walks the same tree per band without renumbering anything.
|
||||||
|
// present() blits the band back to (x, y) and returns drawing to the panel.
|
||||||
|
// Re-rasterizing into RAM and pushing once collapses a per-primitive bus
|
||||||
|
// transaction storm into a streamed write, which is what direct-to-panel
|
||||||
|
// drawing cannot make smooth. A driver with no spare RAM (or an e-ink panel
|
||||||
|
// that gains nothing) refuses the band, so the painter draws to the panel.
|
||||||
|
virtual bool beginBuffer(int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
(void)w;
|
||||||
|
(void)h;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
virtual void present() {}
|
||||||
|
// Clips every draw to this screen rectangle until clearClip(). The painter
|
||||||
|
// wraps a custom node in its own box, because a node owns its box and nothing
|
||||||
|
// else's: a band is usually taller than the node it covers, so a painter that
|
||||||
|
// draws outside itself -- a list row half scrolled off the top -- would
|
||||||
|
// otherwise land on whatever else that band covers. Not clipping is only safe
|
||||||
|
// while no custom node ever overdraws.
|
||||||
|
virtual void setClip(int32_t x, int32_t y, int32_t w, int32_t h) {
|
||||||
|
(void)x;
|
||||||
|
(void)y;
|
||||||
|
(void)w;
|
||||||
|
(void)h;
|
||||||
|
}
|
||||||
|
virtual void clearClip() {}
|
||||||
virtual void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count,
|
virtual void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count,
|
||||||
int32_t color) = 0;
|
int32_t color) = 0;
|
||||||
// Null coordinates centre the image; null bounds fall back to the panel size.
|
// Null coordinates centre the image; null bounds fall back to the panel size.
|
||||||
@@ -220,10 +246,24 @@ public:
|
|||||||
virtual Status forget() = 0;
|
virtual Status forget() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct BleDevice {
|
// One sighting of an advertising device, coalesced to its latest advert. The
|
||||||
std::string name;
|
// payload is the raw advertisement bytes; parsing the AD structures (and any
|
||||||
|
// beacon format inside them) is the caller's job, so a new sensor format never
|
||||||
|
// touches this seam.
|
||||||
|
struct BleObservation {
|
||||||
std::string address;
|
std::string address;
|
||||||
|
std::string name;
|
||||||
int32_t rssi;
|
int32_t rssi;
|
||||||
|
std::string payload;
|
||||||
|
int32_t lastSeenMs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kept adverts must carry one of these service-data UUIDs or one of these
|
||||||
|
// manufacturer ids; an empty filter keeps everything. Applied before an advert
|
||||||
|
// is buffered, so unwanted devices never occupy a slot.
|
||||||
|
struct BleFilter {
|
||||||
|
std::vector<std::string> services;
|
||||||
|
std::vector<int32_t> manufacturers;
|
||||||
};
|
};
|
||||||
|
|
||||||
class BleProvider {
|
class BleProvider {
|
||||||
@@ -232,7 +272,10 @@ public:
|
|||||||
virtual Status init(const std::string* name) = 0;
|
virtual Status init(const std::string* name) = 0;
|
||||||
virtual void deinit() = 0;
|
virtual void deinit() = 0;
|
||||||
virtual bool isInitialized() const = 0;
|
virtual bool isInitialized() const = 0;
|
||||||
virtual Status scan(int32_t durationMs, std::vector<BleDevice>& devices) = 0;
|
virtual Status observe(const BleFilter& filter) = 0;
|
||||||
|
virtual void unobserve() = 0;
|
||||||
|
virtual bool isObserving() const = 0;
|
||||||
|
virtual void observed(std::vector<BleObservation>& out) = 0;
|
||||||
virtual Status connect(const std::string& address) = 0;
|
virtual Status connect(const std::string& address) = 0;
|
||||||
virtual void disconnect() = 0;
|
virtual void disconnect() = 0;
|
||||||
virtual bool isConnected() const = 0;
|
virtual bool isConnected() const = 0;
|
||||||
|
|||||||
@@ -15,36 +15,38 @@ namespace esp32lua {
|
|||||||
// build.
|
// build.
|
||||||
constexpr int32_t API_VERSION = 1;
|
constexpr int32_t API_VERSION = 1;
|
||||||
|
|
||||||
// Where the runtime looks for apps, their data, and shared modules.
|
// The path a firmware boots. Nothing else here knows it: startApp() takes
|
||||||
struct Paths {
|
// whatever path it is given, and where apps live, where their data goes and
|
||||||
std::string apps = "/.lua/apps";
|
// what chrome surrounds them are decided by the Lua it loads.
|
||||||
std::string data = "/.lua/data";
|
constexpr const char* MAIN_PATH = "/.lua/main.lua";
|
||||||
std::string lib = "/.lua/lib";
|
|
||||||
// Where sys.back() lands once history is empty. It is an app like any other.
|
|
||||||
std::string home = "Home";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Firmware supplies every core provider; a null feature provider is how
|
// Firmware supplies every core provider; a null feature provider is how
|
||||||
// sys.hasFeature() answers false, and its namespace additions are simply never
|
// sys.hasFeature() answers false, and its namespace additions are simply never
|
||||||
// registered.
|
// registered.
|
||||||
struct Providers {
|
struct Providers {
|
||||||
LogProvider* log = nullptr;
|
LogProvider* log = nullptr;
|
||||||
SettingsProvider* settings = nullptr;
|
|
||||||
SysProvider* sys = nullptr;
|
SysProvider* sys = nullptr;
|
||||||
FsProvider* fs = nullptr;
|
FsProvider* fs = nullptr;
|
||||||
GuiProvider* gui = nullptr;
|
|
||||||
HttpProvider* http = nullptr;
|
HttpProvider* http = nullptr;
|
||||||
TimerProvider* timer = nullptr;
|
TimerProvider* timer = nullptr;
|
||||||
WifiProvider* wifi = nullptr;
|
WifiProvider* wifi = nullptr;
|
||||||
BleProvider* ble = nullptr;
|
BleProvider* ble = nullptr;
|
||||||
|
|
||||||
|
GuiProvider* gui = nullptr;
|
||||||
TouchProvider* touch = nullptr;
|
TouchProvider* touch = nullptr;
|
||||||
ButtonsProvider* buttons = nullptr;
|
ButtonsProvider* buttons = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The arguments a launch carries cross the teardown as JSON, because the table
|
||||||
|
// they came from dies with the state that built it. Encoding raises, so an app
|
||||||
|
// that passes a function sees the error at its own sys.startApp() call;
|
||||||
|
// decoding cannot, because by then there is no app to report it to.
|
||||||
|
std::string encodeJson(lua_State* state, int index);
|
||||||
|
bool decodeJson(lua_State* state, const std::string& json);
|
||||||
|
|
||||||
class Runtime {
|
class Runtime {
|
||||||
public:
|
public:
|
||||||
explicit Runtime(const Providers& providers, const Paths& paths = Paths());
|
explicit Runtime(const Providers& providers);
|
||||||
~Runtime();
|
~Runtime();
|
||||||
|
|
||||||
Runtime(const Runtime&) = delete;
|
Runtime(const Runtime&) = delete;
|
||||||
@@ -55,38 +57,27 @@ public:
|
|||||||
void close();
|
void close();
|
||||||
lua_State* state() const { return state_; }
|
lua_State* state() const { return state_; }
|
||||||
|
|
||||||
// Replaces the running app with a fresh lua_State, loads
|
// Replaces the running app with a fresh lua_State, loads the path, and hands
|
||||||
// <apps>/<path>/main.lua, and calls init(arg). A failure leaves no app
|
// the table it returns its arguments through start(args). A failure leaves no
|
||||||
// running rather than a half-built one.
|
// app running rather than a half-built one. `argsJson` is the JSON a previous
|
||||||
|
// state encoded, and is the only thing that crosses the teardown.
|
||||||
bool startApp(const std::string& path,
|
bool startApp(const std::string& path,
|
||||||
const std::string& arg = std::string());
|
const std::string& argsJson = std::string());
|
||||||
bool hasApp() const { return !appPath_.empty(); }
|
bool hasApp() const { return !appPath_.empty(); }
|
||||||
// The app-relative route, its immutable first component, and the title the
|
// The path that was loaded, which is all the runtime knows about an app.
|
||||||
// app chose.
|
|
||||||
const std::string& appPath() const { return appPath_; }
|
const std::string& appPath() const { return appPath_; }
|
||||||
std::string appId() const;
|
|
||||||
std::string appDataPath() const;
|
|
||||||
const std::string& appTitle() const { return appTitle_; }
|
|
||||||
void setAppTitle(const std::string& title) { appTitle_ = title; }
|
|
||||||
bool hasFeature(const std::string& feature) const;
|
bool hasFeature(const std::string& feature) const;
|
||||||
|
|
||||||
// sys.launch/replace/back record intent and return; swapping the lua_State
|
// sys.startApp records intent and returns; swapping the lua_State inside a
|
||||||
// inside a callback would free the VM that is still executing. The firmware
|
// callback would free the VM that is still executing. The firmware applies it
|
||||||
// applies it between batches.
|
// between batches.
|
||||||
void requestLaunch(const std::string& path, const std::string& arg,
|
void requestStart(const std::string& path, const std::string& argsJson);
|
||||||
bool replace);
|
bool hasPendingNavigation() const { return pending_.pending; }
|
||||||
void requestBack();
|
// Loads whatever was requested. False means the app failed to start, in which
|
||||||
bool hasPendingNavigation() const { return pending_.kind != Pending::None; }
|
// case no app is running.
|
||||||
// Whether sys.back() would return somewhere rather than land on the launcher,
|
|
||||||
// which is what firmware chrome needs to decide whether to offer a back
|
|
||||||
// control.
|
|
||||||
bool canGoBack() const { return !history_.empty(); }
|
|
||||||
// Loads whatever was requested. False means the app failed to start or
|
|
||||||
// history ran out at the launcher, in which case no app is running.
|
|
||||||
bool applyPendingNavigation();
|
bool applyPendingNavigation();
|
||||||
|
|
||||||
LogProvider& log() const { return *providers_.log; }
|
LogProvider& log() const { return *providers_.log; }
|
||||||
SettingsProvider& settings() const { return *providers_.settings; }
|
|
||||||
SysProvider& sys() const { return *providers_.sys; }
|
SysProvider& sys() const { return *providers_.sys; }
|
||||||
FsProvider& fs() const { return *providers_.fs; }
|
FsProvider& fs() const { return *providers_.fs; }
|
||||||
GuiProvider& gui() const { return *providers_.gui; }
|
GuiProvider& gui() const { return *providers_.gui; }
|
||||||
@@ -99,11 +90,11 @@ public:
|
|||||||
|
|
||||||
ui::Tree& tree() { return tree_; }
|
ui::Tree& tree() { return tree_; }
|
||||||
|
|
||||||
// Entry points into the app. The firmware decides whether an event reaches
|
// Entry points into main.lua, which forwards whatever the app it mounted
|
||||||
// the app at all -- jitter, chrome and debouncing are its business -- and the
|
// defines. The firmware decides whether an event happens at all -- jitter and
|
||||||
// runtime decides what the app sees. Only a failed init() stops an app; every
|
// debouncing are its business -- and main.lua decides who sees it. Only a
|
||||||
// other callback logs and carries on.
|
// failed start() stops an app; every other callback logs and carries on.
|
||||||
bool callInit(const std::string& arg);
|
bool callStart(const std::string& argsJson);
|
||||||
void callDraw(int32_t deltaMs);
|
void callDraw(int32_t deltaMs);
|
||||||
// An Up phase also fires the on_touch tap alias, in that order.
|
// An Up phase also fires the on_touch tap alias, in that order.
|
||||||
void callTouch(TouchPhase phase, int32_t x, int32_t y);
|
void callTouch(TouchPhase phase, int32_t x, int32_t y);
|
||||||
@@ -125,18 +116,17 @@ private:
|
|||||||
bool repeating;
|
bool repeating;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Route {
|
|
||||||
std::string path;
|
|
||||||
std::string arg;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Pending {
|
struct Pending {
|
||||||
enum Kind { None, Launch, Replace, Back } kind = None;
|
bool pending = false;
|
||||||
Route route;
|
std::string path;
|
||||||
|
std::string argsJson;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool loadScript(const std::string& path);
|
bool loadScript(const std::string& path);
|
||||||
void installLoader(const std::string& appDir);
|
// Runs the app's entry file and keeps the table it returns; the app is
|
||||||
|
// mounted by that table, not by the runtime.
|
||||||
|
bool loadMain(const std::string& path);
|
||||||
|
void installLoader();
|
||||||
static int searchModule(lua_State* state);
|
static int searchModule(lua_State* state);
|
||||||
static int searchEmbedded(lua_State* state);
|
static int searchEmbedded(lua_State* state);
|
||||||
static int loadFile(lua_State* state);
|
static int loadFile(lua_State* state);
|
||||||
@@ -155,9 +145,10 @@ private:
|
|||||||
Runtime& runtime_;
|
Runtime& runtime_;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pushes the named global, or returns false when the app does not define it.
|
// Pushes main.<name>, or returns false when main.lua defines no such handler.
|
||||||
bool beginCall(const char* name);
|
bool beginCall(const char* name);
|
||||||
bool finishCall(const char* name, int argc);
|
bool finishCall(const char* name, int argc);
|
||||||
|
bool finishCallValue(const char* name);
|
||||||
void cancelAllTimers();
|
void cancelAllTimers();
|
||||||
|
|
||||||
Providers providers_;
|
Providers providers_;
|
||||||
@@ -167,10 +158,10 @@ private:
|
|||||||
TimerId nextTimerId_ = 1;
|
TimerId nextTimerId_ = 1;
|
||||||
int batchDepth_ = 0;
|
int batchDepth_ = 0;
|
||||||
|
|
||||||
Paths paths_;
|
// Registry reference to the table the entry file returned, or 0 before one
|
||||||
|
// loads.
|
||||||
|
int mainRef_ = 0;
|
||||||
std::string appPath_;
|
std::string appPath_;
|
||||||
std::string appTitle_;
|
|
||||||
std::vector<Route> history_;
|
|
||||||
Pending pending_;
|
Pending pending_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
// @lua-module ble BleLib
|
// @lua-module ble BleLib
|
||||||
// @lua-preamble ---@class BleDevice
|
// @lua-preamble ---@class BleObservation
|
||||||
// @lua-preamble ---@field name string
|
|
||||||
// @lua-preamble ---@field address string
|
// @lua-preamble ---@field address string
|
||||||
|
// @lua-preamble ---@field name string
|
||||||
// @lua-preamble ---@field rssi integer
|
// @lua-preamble ---@field rssi integer
|
||||||
|
// @lua-preamble ---@field payload string Raw advertisement bytes.
|
||||||
|
// @lua-preamble ---@field lastSeenMs integer sys.getMillis() at last sighting.
|
||||||
|
// @lua-preamble
|
||||||
|
// @lua-preamble ---@class BleFilter
|
||||||
|
// @lua-preamble ---@field services? string[] Service-data UUIDs to keep.
|
||||||
|
// @lua-preamble ---@field manufacturers? integer[] Manufacturer ids to keep.
|
||||||
|
|
||||||
#include "../helpers.h"
|
#include "../helpers.h"
|
||||||
|
|
||||||
@@ -30,21 +36,67 @@ int isInitialized(lua_State* state) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int scan(lua_State* state) {
|
void readStringArray(lua_State* state, int index, const char* key,
|
||||||
const int32_t durationMs = optionalInt(state, 1, 3000);
|
std::vector<std::string>& out) {
|
||||||
luaL_argcheck(state, durationMs > 0, 1, "must be positive");
|
lua_getfield(state, index, key);
|
||||||
|
if (lua_istable(state, -1)) {
|
||||||
|
const int count = static_cast<int>(lua_rawlen(state, -1));
|
||||||
|
for (int at = 1; at <= count; at++) {
|
||||||
|
lua_rawgeti(state, -1, at);
|
||||||
|
if (lua_type(state, -1) == LUA_TSTRING)
|
||||||
|
out.push_back(lua_tostring(state, -1));
|
||||||
|
lua_pop(state, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(state, 1);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<BleDevice> devices;
|
void readIntArray(lua_State* state, int index, const char* key,
|
||||||
const Status status = Runtime::from(state)->ble().scan(durationMs, devices);
|
std::vector<int32_t>& out) {
|
||||||
if (!status.ok)
|
lua_getfield(state, index, key);
|
||||||
return pushError(state, status.error);
|
if (lua_istable(state, -1)) {
|
||||||
|
const int count = static_cast<int>(lua_rawlen(state, -1));
|
||||||
|
for (int at = 1; at <= count; at++) {
|
||||||
|
lua_rawgeti(state, -1, at);
|
||||||
|
if (lua_type(state, -1) == LUA_TNUMBER)
|
||||||
|
out.push_back(static_cast<int32_t>(lua_tointeger(state, -1)));
|
||||||
|
lua_pop(state, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lua_pop(state, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int observe(lua_State* state) {
|
||||||
|
BleFilter filter;
|
||||||
|
if (lua_istable(state, 1)) {
|
||||||
|
readStringArray(state, 1, "services", filter.services);
|
||||||
|
readIntArray(state, 1, "manufacturers", filter.manufacturers);
|
||||||
|
}
|
||||||
|
return pushStatus(state, Runtime::from(state)->ble().observe(filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
int unobserve(lua_State* state) {
|
||||||
|
Runtime::from(state)->ble().unobserve();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int isObserving(lua_State* state) {
|
||||||
|
lua_pushboolean(state, Runtime::from(state)->ble().isObserving());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int observed(lua_State* state) {
|
||||||
|
std::vector<BleObservation> devices;
|
||||||
|
Runtime::from(state)->ble().observed(devices);
|
||||||
|
|
||||||
lua_createtable(state, static_cast<int>(devices.size()), 0);
|
lua_createtable(state, static_cast<int>(devices.size()), 0);
|
||||||
for (size_t at = 0; at < devices.size(); at++) {
|
for (size_t at = 0; at < devices.size(); at++) {
|
||||||
lua_createtable(state, 0, 3);
|
lua_createtable(state, 0, 5);
|
||||||
setField(state, "name", devices[at].name);
|
|
||||||
setField(state, "address", devices[at].address);
|
setField(state, "address", devices[at].address);
|
||||||
|
setField(state, "name", devices[at].name);
|
||||||
setField(state, "rssi", devices[at].rssi);
|
setField(state, "rssi", devices[at].rssi);
|
||||||
|
setField(state, "payload", devices[at].payload);
|
||||||
|
setField(state, "lastSeenMs", devices[at].lastSeenMs);
|
||||||
lua_rawseti(state, -2, static_cast<lua_Integer>(at + 1));
|
lua_rawseti(state, -2, static_cast<lua_Integer>(at + 1));
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
@@ -108,11 +160,19 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// --- Whether the BLE stack is running.
|
// --- Whether the BLE stack is running.
|
||||||
// @return boolean
|
// @return boolean
|
||||||
{"isInitialized", isInitialized},
|
{"isInitialized", isInitialized},
|
||||||
// --- Scans for advertising devices.
|
// --- Starts passively observing advertisements, coalesced per device.
|
||||||
// @param durationMs integer|nil Defaults to 3000.
|
// @param filter BleFilter|nil Keep only matching adverts; nil keeps all.
|
||||||
// @return BleDevice[]|nil devices
|
// @return true|nil ok
|
||||||
// @return string|nil error
|
// @return string|nil error
|
||||||
{"scan", scan},
|
{"observe", observe},
|
||||||
|
// --- Stops observing and clears the snapshot.
|
||||||
|
{"unobserve", unobserve},
|
||||||
|
// --- Whether advertisement observation is running.
|
||||||
|
// @return boolean
|
||||||
|
{"isObserving", isObserving},
|
||||||
|
// --- The current snapshot of observed devices.
|
||||||
|
// @return BleObservation[] devices
|
||||||
|
{"observed", observed},
|
||||||
// --- Connects to a peripheral.
|
// --- Connects to a peripheral.
|
||||||
// @param address string
|
// @param address string
|
||||||
// @return true|nil ok
|
// @return true|nil ok
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
// @lua-module settings SettingsLib
|
|
||||||
|
|
||||||
#include <lua/runtime.h>
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
#include "lauxlib.h"
|
|
||||||
#include "lua.h"
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace esp32lua {
|
|
||||||
namespace bindings {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
int pushStatus(lua_State* state, const Status& status) {
|
|
||||||
if (status.ok) {
|
|
||||||
lua_pushboolean(state, true);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
lua_pushnil(state);
|
|
||||||
lua_pushlstring(state, status.error.data(), status.error.size());
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getRotation(lua_State* state) {
|
|
||||||
lua_pushinteger(state, Runtime::from(state)->settings().rotation());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int setRotation(lua_State* state) {
|
|
||||||
const lua_Integer degrees = luaL_checkinteger(state, 1);
|
|
||||||
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
|
|
||||||
"expected 0, 90, 180, or 270");
|
|
||||||
return pushStatus(state,
|
|
||||||
Runtime::from(state)->settings().setRotation(degrees));
|
|
||||||
}
|
|
||||||
|
|
||||||
int getTimezone(lua_State* state) {
|
|
||||||
const std::string timezone = Runtime::from(state)->settings().timezone();
|
|
||||||
lua_pushlstring(state, timezone.data(), timezone.size());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int setTimezone(lua_State* state) {
|
|
||||||
size_t length = 0;
|
|
||||||
const char* value = luaL_checklstring(state, 1, &length);
|
|
||||||
return pushStatus(
|
|
||||||
state, Runtime::from(state)->settings().setTimezone({value, length}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const luaL_Reg FUNCTIONS[] = {
|
|
||||||
// --- Returns the saved rotation in degrees clockwise.
|
|
||||||
// @return integer
|
|
||||||
{"getRotation", getRotation},
|
|
||||||
// --- Applies and persists the screen rotation.
|
|
||||||
// @param degrees integer 0, 90, 180, or 270 clockwise.
|
|
||||||
// @return true|nil ok
|
|
||||||
// @return string|nil error
|
|
||||||
{"setRotation", setRotation},
|
|
||||||
// --- Returns the active POSIX timezone rule.
|
|
||||||
// @return string
|
|
||||||
{"getTimezone", getTimezone},
|
|
||||||
// --- Applies and persists a POSIX timezone rule.
|
|
||||||
// @param timezone string
|
|
||||||
// @return true|nil ok
|
|
||||||
// @return string|nil error
|
|
||||||
{"setTimezone", setTimezone},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void registerSettings(lua_State* state) {
|
|
||||||
luaL_newlib(state, FUNCTIONS);
|
|
||||||
lua_setglobal(state, "settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace bindings
|
|
||||||
} // namespace esp32lua
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// @lua-module sys SysLib
|
// @lua-module sys SysLib
|
||||||
// @lua-preamble ---@alias Feature "touch"|"buttons"
|
// @lua-preamble ---@alias Feature "screen"|"touch"|"buttons"
|
||||||
|
|
||||||
#include "../helpers.h"
|
#include "../helpers.h"
|
||||||
|
|
||||||
@@ -20,34 +20,17 @@ int getMillis(lua_State* state) {
|
|||||||
lua_pushinteger(state, Runtime::from(state)->sys().millis());
|
lua_pushinteger(state, Runtime::from(state)->sys().millis());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
int getAppID(lua_State* state) {
|
// Encoding happens here, in the state that still holds the table, so an app
|
||||||
pushString(state, Runtime::from(state)->appId());
|
// passing something JSON cannot carry raises at its own call rather than
|
||||||
return 1;
|
// stranding the launch.
|
||||||
}
|
int startApp(lua_State* state) {
|
||||||
int getAppTitle(lua_State* state) {
|
luaL_checkstring(state, 1);
|
||||||
pushString(state, Runtime::from(state)->appTitle());
|
if (!lua_isnoneornil(state, 2))
|
||||||
return 1;
|
luaL_checktype(state, 2, LUA_TTABLE);
|
||||||
}
|
const std::string json =
|
||||||
int getAppDataPath(lua_State* state) {
|
lua_isnoneornil(state, 2) ? std::string() : encodeJson(state, 2);
|
||||||
pushString(state, Runtime::from(state)->appDataPath());
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
int setAppTitle(lua_State* state) {
|
|
||||||
Runtime::from(state)->setAppTitle(luaL_checkstring(state, 1));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int navigate(lua_State* state, bool replace) {
|
|
||||||
const std::string path = checkString(state, 1);
|
const std::string path = checkString(state, 1);
|
||||||
const std::string arg =
|
Runtime::from(state)->requestStart(path, json);
|
||||||
lua_isnoneornil(state, 2) ? std::string() : checkString(state, 2);
|
|
||||||
Runtime::from(state)->requestLaunch(path, arg, replace);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
int launch(lua_State* state) { return navigate(state, false); }
|
|
||||||
int replace(lua_State* state) { return navigate(state, true); }
|
|
||||||
int back(lua_State* state) {
|
|
||||||
Runtime::from(state)->requestBack();
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +45,14 @@ int isClockSynced(lua_State* state) {
|
|||||||
lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced());
|
lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
int getTimezone(lua_State* state) {
|
||||||
|
pushString(state, Runtime::from(state)->sys().timezone());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
int setTimezone(lua_State* state) {
|
||||||
|
const std::string timezone = checkString(state, 1);
|
||||||
|
return pushStatus(state, Runtime::from(state)->sys().setTimezone(timezone));
|
||||||
|
}
|
||||||
|
|
||||||
const luaL_Reg FUNCTIONS[] = {
|
const luaL_Reg FUNCTIONS[] = {
|
||||||
// --- Returns the implemented API contract version.
|
// --- Returns the implemented API contract version.
|
||||||
@@ -74,30 +65,14 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// --- Returns monotonic milliseconds since boot.
|
// --- Returns monotonic milliseconds since boot.
|
||||||
// @return integer
|
// @return integer
|
||||||
{"getMillis", getMillis},
|
{"getMillis", getMillis},
|
||||||
// --- Returns the immutable first path component of the running app.
|
// --- Tears the runtime down and starts over from a Lua file, which is the
|
||||||
// @return string
|
// only navigation there is: history, titles and where apps live are
|
||||||
{"getAppID", getAppID},
|
// whatever that file makes of the arguments.
|
||||||
// --- Returns the running app title, initially the app ID.
|
// @param path string Absolute path to the Lua file to load; traversal is
|
||||||
// @return string
|
// rejected.
|
||||||
{"getAppTitle", getAppTitle},
|
// @param args table|nil Plain data, carried across the teardown as JSON and
|
||||||
// --- Returns the current app's guaranteed-existing persistent data
|
// handed to start(args). Raises on anything JSON cannot represent.
|
||||||
// directory.
|
{"startApp", startApp},
|
||||||
// @return string Absolute path under /.lua/data, preserved across app
|
|
||||||
// updates.
|
|
||||||
{"getAppDataPath", getAppDataPath},
|
|
||||||
// --- Changes the running app's display title.
|
|
||||||
// @param title string
|
|
||||||
{"setAppTitle", setAppTitle},
|
|
||||||
// --- Launches /.lua/apps/<path>/main.lua and pushes the current route.
|
|
||||||
// @param path string App-relative directory path; traversal is rejected.
|
|
||||||
// @param arg string|nil Passed to init(arg).
|
|
||||||
{"launch", launch},
|
|
||||||
// --- Launches an app path without retaining the current route.
|
|
||||||
// @param path string App-relative directory path; traversal is rejected.
|
|
||||||
// @param arg string|nil Passed to init(arg).
|
|
||||||
{"replace", replace},
|
|
||||||
// --- Returns to the previous app, or the launcher when history is empty.
|
|
||||||
{"back", back},
|
|
||||||
// --- Returns heap statistics.
|
// --- Returns heap statistics.
|
||||||
// @return integer freeBytes
|
// @return integer freeBytes
|
||||||
// @return integer totalBytes
|
// @return integer totalBytes
|
||||||
@@ -106,6 +81,14 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// --- Whether network time synchronization has completed.
|
// --- Whether network time synchronization has completed.
|
||||||
// @return boolean
|
// @return boolean
|
||||||
{"isClockSynced", isClockSynced},
|
{"isClockSynced", isClockSynced},
|
||||||
|
// --- Returns the active POSIX timezone rule.
|
||||||
|
// @return string
|
||||||
|
{"getTimezone", getTimezone},
|
||||||
|
// --- Applies and persists a POSIX timezone rule.
|
||||||
|
// @param timezone string
|
||||||
|
// @return true|nil ok
|
||||||
|
// @return string|nil error
|
||||||
|
{"setTimezone", setTimezone},
|
||||||
{nullptr, nullptr},
|
{nullptr, nullptr},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,18 +32,18 @@ int wasReleased(lua_State* state) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @lua-augment input InputLib
|
// @lua-module buttons ButtonsLib
|
||||||
// @lua-preamble ---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
|
// @lua-preamble ---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
|
||||||
// @lua-preamble
|
// @lua-preamble
|
||||||
// @lua-preamble -- Roles, not physical buttons: a device maps whatever hardware
|
// @lua-preamble -- Roles, not physical buttons: a device maps whatever hardware
|
||||||
// it has onto them, and
|
// it has onto them, and
|
||||||
// @lua-preamble -- up/down/left/right are the directions node.moveFocus already
|
// @lua-preamble -- up/down/left/right are the directions tree.moveFocus already
|
||||||
// takes.
|
// takes.
|
||||||
const luaL_Reg INPUT_FUNCTIONS[] = {
|
const luaL_Reg FUNCTIONS[] = {
|
||||||
// ---Returns the roles this device reports, so an app can label only the
|
// ---Returns the roles this device reports, so an app can label only the
|
||||||
// actions it has.
|
// actions it has.
|
||||||
// @return Button[]
|
// @return Button[]
|
||||||
{"getButtons", getButtons},
|
{"getAll", getButtons},
|
||||||
// ---Whether any button is held.
|
// ---Whether any button is held.
|
||||||
// @return boolean
|
// @return boolean
|
||||||
{"isAnyPressed", isAnyPressed},
|
{"isAnyPressed", isAnyPressed},
|
||||||
@@ -65,19 +65,24 @@ const luaL_Reg INPUT_FUNCTIONS[] = {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void registerButtons(lua_State* state) {
|
void registerButtons(lua_State* state) {
|
||||||
augmentGlobal(state, "input", INPUT_FUNCTIONS);
|
luaL_newlib(state, FUNCTIONS);
|
||||||
|
lua_setglobal(state, "buttons");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace bindings
|
} // namespace bindings
|
||||||
} // namespace esp32lua
|
} // namespace esp32lua
|
||||||
|
|
||||||
// @lua-global
|
// @lua-app ButtonHandlers
|
||||||
|
// @lua-preamble -- What an app implements to see buttons, composed into its own
|
||||||
|
// class:
|
||||||
|
// @lua-preamble --
|
||||||
|
// @lua-preamble -- ---@class MenuApp : App, ButtonHandlers
|
||||||
// ---Fired when a button goes down.
|
// ---Fired when a button goes down.
|
||||||
// @param button Button
|
// @param button Button
|
||||||
// @lua-fn on_button_down
|
// @lua-fn onButtonDown?
|
||||||
// ---Fired when a button comes up.
|
// ---Fired when a button comes up.
|
||||||
// @param button Button
|
// @param button Button
|
||||||
// @lua-fn on_button_up
|
// @lua-fn onButtonUp?
|
||||||
// ---Tap alias, fired on release like a click, after on_button_up.
|
// ---Tap alias, fired on release like a click, after onButtonUp.
|
||||||
// @param button Button
|
// @param button Button
|
||||||
// @lua-fn on_button
|
// @lua-fn onButton?
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
// @lua-module gui GuiLib
|
// @lua-module screen ScreenLib
|
||||||
// @lua-preamble ---@alias GuiColor integer
|
// @lua-preamble -- The panel itself; the widget tree it paints is `tree`, and
|
||||||
// @lua-preamble ---@alias GuiFont integer
|
// @lua-preamble -- sys.hasFeature("screen") covers both.
|
||||||
// @lua-preamble ---@alias GuiTextStyle integer
|
// @lua-preamble ---@alias ScreenColor integer
|
||||||
// @lua-const FONT_SMALL GuiFont 0 Small auxiliary text.
|
// @lua-preamble ---@alias ScreenFont integer
|
||||||
// @lua-const FONT_UI GuiFont 0 Normal controls and labels.
|
// @lua-preamble ---@alias ScreenTextStyle integer
|
||||||
// @lua-const FONT_BODY GuiFont 0 Normal reading text.
|
// @lua-const FONT_SMALL ScreenFont 0 Small auxiliary text.
|
||||||
// @lua-const FONT_LARGE GuiFont 0 Headings and prominent values.
|
// @lua-const FONT_UI ScreenFont 0 Normal controls and labels.
|
||||||
// @lua-const STYLE_NORMAL GuiTextStyle 0
|
// @lua-const FONT_BODY ScreenFont 0 Normal reading text.
|
||||||
// @lua-const STYLE_BOLD GuiTextStyle 0
|
// @lua-const FONT_LARGE ScreenFont 0 Headings and prominent values.
|
||||||
|
// @lua-const STYLE_NORMAL ScreenTextStyle 0
|
||||||
|
// @lua-const STYLE_BOLD ScreenTextStyle 0
|
||||||
|
|
||||||
#include "../helpers.h"
|
#include "../../helpers.h"
|
||||||
|
|
||||||
namespace esp32lua {
|
namespace esp32lua {
|
||||||
namespace bindings {
|
namespace bindings {
|
||||||
@@ -36,8 +38,17 @@ int setRotation(lua_State* state) {
|
|||||||
const int32_t degrees = checkInt(state, 1);
|
const int32_t degrees = checkInt(state, 1);
|
||||||
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
|
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
|
||||||
"expected 0, 90, 180, or 270");
|
"expected 0, 90, 180, or 270");
|
||||||
provider(state).setRotation(degrees);
|
return pushStatus(state, provider(state).setRotation(degrees));
|
||||||
return 0;
|
}
|
||||||
|
|
||||||
|
int getTheme(lua_State* state) {
|
||||||
|
pushString(state, provider(state).theme());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int setTheme(lua_State* state) {
|
||||||
|
const std::string theme = checkString(state, 1);
|
||||||
|
return pushStatus(state, provider(state).setTheme(theme));
|
||||||
}
|
}
|
||||||
|
|
||||||
int color(lua_State* state) {
|
int color(lua_State* state) {
|
||||||
@@ -125,12 +136,6 @@ int roundRect(lua_State* state) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int setFullscreen(lua_State* state) {
|
|
||||||
luaL_checkany(state, 1);
|
|
||||||
provider(state).setFullscreen(lua_toboolean(state, 1) != 0);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void readIntegers(lua_State* state, int index, std::vector<int32_t>& out) {
|
void readIntegers(lua_State* state, int index, std::vector<int32_t>& out) {
|
||||||
const lua_Integer count = luaL_len(state, index);
|
const lua_Integer count = luaL_len(state, index);
|
||||||
for (lua_Integer at = 1; at <= count; at++) {
|
for (lua_Integer at = 1; at <= count; at++) {
|
||||||
@@ -221,62 +226,76 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// --- Returns the live frame height.
|
// --- Returns the live frame height.
|
||||||
// @return integer
|
// @return integer
|
||||||
{"getHeight", getHeight},
|
{"getHeight", getHeight},
|
||||||
// --- Rotates the live frame without changing the saved preference.
|
// --- Rotates the panel and persists the choice, so there is one rotation
|
||||||
|
// --- rather than a live one and a saved one to reconcile.
|
||||||
// @param degrees integer 0, 90, 180, or 270 clockwise.
|
// @param degrees integer 0, 90, 180, or 270 clockwise.
|
||||||
|
// @return true|nil ok
|
||||||
|
// @return string|nil error
|
||||||
{"setRotation", setRotation},
|
{"setRotation", setRotation},
|
||||||
// --- Returns the rotation of the live frame.
|
// --- Returns the rotation in degrees clockwise.
|
||||||
// @return integer Degrees clockwise for the live frame.
|
// @return integer
|
||||||
{"getRotation", getRotation},
|
{"getRotation", getRotation},
|
||||||
|
// --- Returns the saved palette name. Apps read ui.getTheme() instead; this
|
||||||
|
// --- is the stored value, which only ui.setTheme() knows how to apply.
|
||||||
|
// @return string
|
||||||
|
{"getTheme", getTheme},
|
||||||
|
// --- Persists a palette name without applying it. Call ui.setTheme(),
|
||||||
|
// which
|
||||||
|
// --- writes through here and then rebuilds the palette and repaints.
|
||||||
|
// @param theme string
|
||||||
|
// @return true|nil ok
|
||||||
|
// @return string|nil error
|
||||||
|
{"setTheme", setTheme},
|
||||||
// --- Returns an opaque native color. E-ink implementations quantize RGB to
|
// --- Returns an opaque native color. E-ink implementations quantize RGB to
|
||||||
// available grayscale.
|
// available grayscale.
|
||||||
// @param r integer 0 through 255.
|
// @param r integer 0 through 255.
|
||||||
// @param g integer 0 through 255.
|
// @param g integer 0 through 255.
|
||||||
// @param b integer 0 through 255.
|
// @param b integer 0 through 255.
|
||||||
// @return GuiColor
|
// @return ScreenColor
|
||||||
{"color", color},
|
{"color", color},
|
||||||
// --- Clears the frame.
|
// --- Clears the frame.
|
||||||
// @param color GuiColor|nil Defaults to white.
|
// @param color ScreenColor|nil Defaults to white.
|
||||||
{"clear", clear},
|
{"clear", clear},
|
||||||
// --- Fills a rectangle.
|
// --- Fills a rectangle.
|
||||||
// @param x integer
|
// @param x integer
|
||||||
// @param y integer
|
// @param y integer
|
||||||
// @param w integer
|
// @param w integer
|
||||||
// @param h integer
|
// @param h integer
|
||||||
// @param color GuiColor
|
// @param color ScreenColor
|
||||||
{"fillRect", fillRect},
|
{"fillRect", fillRect},
|
||||||
// --- Outlines a rectangle.
|
// --- Outlines a rectangle.
|
||||||
// @param x integer
|
// @param x integer
|
||||||
// @param y integer
|
// @param y integer
|
||||||
// @param w integer
|
// @param w integer
|
||||||
// @param h integer
|
// @param h integer
|
||||||
// @param color GuiColor
|
// @param color ScreenColor
|
||||||
{"drawRect", drawRect},
|
{"drawRect", drawRect},
|
||||||
// --- Draws a line.
|
// --- Draws a line.
|
||||||
// @param x1 integer
|
// @param x1 integer
|
||||||
// @param y1 integer
|
// @param y1 integer
|
||||||
// @param x2 integer
|
// @param x2 integer
|
||||||
// @param y2 integer
|
// @param y2 integer
|
||||||
// @param color GuiColor
|
// @param color ScreenColor
|
||||||
// @param width integer|nil Defaults to one pixel.
|
// @param width integer|nil Defaults to one pixel.
|
||||||
{"drawLine", drawLine},
|
{"drawLine", drawLine},
|
||||||
// --- Draws a single pixel.
|
// --- Draws a single pixel.
|
||||||
// @param x integer
|
// @param x integer
|
||||||
// @param y integer
|
// @param y integer
|
||||||
// @param color GuiColor
|
// @param color ScreenColor
|
||||||
{"drawPixel", drawPixel},
|
{"drawPixel", drawPixel},
|
||||||
// --- Outlines a circle.
|
// --- Outlines a circle.
|
||||||
// @param x integer Center.
|
// @param x integer Center.
|
||||||
// @param y integer Center.
|
// @param y integer Center.
|
||||||
// @param radius integer
|
// @param radius integer
|
||||||
// @param color GuiColor
|
// @param color ScreenColor
|
||||||
// @param width integer|nil Defaults to one pixel.
|
// @param width integer|nil Defaults to one pixel.
|
||||||
{"drawCircle", drawCircle},
|
{"drawCircle", drawCircle},
|
||||||
// --- Fills a circle.
|
// --- Fills a circle.
|
||||||
// @param x integer Center.
|
// @param x integer Center.
|
||||||
// @param y integer Center.
|
// @param y integer Center.
|
||||||
// @param radius integer
|
// @param radius integer
|
||||||
// @param color GuiColor
|
// @param color ScreenColor
|
||||||
// @param background GuiColor|nil Surface behind an anti-aliased edge.
|
// @param background ScreenColor|nil Surface behind an anti-aliased edge.
|
||||||
{"fillCircle", fillCircle},
|
{"fillCircle", fillCircle},
|
||||||
// ---Draws an anti-aliased rounded fill, optional gradient, and optional
|
// ---Draws an anti-aliased rounded fill, optional gradient, and optional
|
||||||
// border in one pass.
|
// border in one pass.
|
||||||
@@ -285,19 +304,16 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// @param w integer
|
// @param w integer
|
||||||
// @param h integer
|
// @param h integer
|
||||||
// @param radius integer
|
// @param radius integer
|
||||||
// @param background GuiColor Surface behind the anti-aliased edge.
|
// @param background ScreenColor Surface behind the anti-aliased edge.
|
||||||
// @param top GuiColor|nil Fill, or gradient top; omitted for no fill.
|
// @param top ScreenColor|nil Fill, or gradient top; omitted for no fill.
|
||||||
// @param bottom GuiColor|nil Gradient bottom; defaults to top. Panels
|
// @param bottom ScreenColor|nil Gradient bottom; defaults to top. Panels
|
||||||
// without a gradient use top.
|
// without a gradient use top.
|
||||||
// @param border GuiColor|nil Omitted for no border.
|
// @param border ScreenColor|nil Omitted for no border.
|
||||||
{"roundRect", roundRect},
|
{"roundRect", roundRect},
|
||||||
// ---Temporarily gives the app the full panel, including firmware chrome.
|
|
||||||
// @param on boolean
|
|
||||||
{"setFullscreen", setFullscreen},
|
|
||||||
// --- Fills a polygon.
|
// --- Fills a polygon.
|
||||||
// @param xs integer[]
|
// @param xs integer[]
|
||||||
// @param ys integer[]
|
// @param ys integer[]
|
||||||
// @param color GuiColor
|
// @param color ScreenColor
|
||||||
{"fillPolygon", fillPolygon},
|
{"fillPolygon", fillPolygon},
|
||||||
// --- Draws a bitmap.
|
// --- Draws a bitmap.
|
||||||
// @param path string Absolute BMP path.
|
// @param path string Absolute BMP path.
|
||||||
@@ -309,31 +325,31 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// @return string|nil error
|
// @return string|nil error
|
||||||
{"drawBmp", drawBmp},
|
{"drawBmp", drawBmp},
|
||||||
// --- Measures a text run.
|
// --- Measures a text run.
|
||||||
// @param font GuiFont Use a named gui.FONT_* role.
|
// @param font ScreenFont Use a named screen.FONT_* role.
|
||||||
// @param text string
|
// @param text string
|
||||||
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
|
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
|
||||||
// @return integer
|
// @return integer
|
||||||
{"getTextWidth", getTextWidth},
|
{"getTextWidth", getTextWidth},
|
||||||
// --- Returns the line height of a font role.
|
// --- Returns the line height of a font role.
|
||||||
// @param font GuiFont Use a named gui.FONT_* role.
|
// @param font ScreenFont Use a named screen.FONT_* role.
|
||||||
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
|
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
|
||||||
// @return integer
|
// @return integer
|
||||||
{"getFontHeight", getFontHeight},
|
{"getFontHeight", getFontHeight},
|
||||||
// --- Draws a text run with its top-left corner at x, y.
|
// --- Draws a text run with its top-left corner at x, y.
|
||||||
// @param font GuiFont Use a named gui.FONT_* role.
|
// @param font ScreenFont Use a named screen.FONT_* role.
|
||||||
// @param x integer Left edge.
|
// @param x integer Left edge.
|
||||||
// @param y integer Top edge.
|
// @param y integer Top edge.
|
||||||
// @param text string
|
// @param text string
|
||||||
// @param color GuiColor|nil Defaults to black.
|
// @param color ScreenColor|nil Defaults to black.
|
||||||
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
|
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
|
||||||
// @param background GuiColor|nil Omitted for transparent text.
|
// @param background ScreenColor|nil Omitted for transparent text.
|
||||||
{"drawText", drawText},
|
{"drawText", drawText},
|
||||||
{nullptr, nullptr},
|
{nullptr, nullptr},
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void registerGui(lua_State* state) {
|
void registerScreen(lua_State* state) {
|
||||||
luaL_newlib(state, FUNCTIONS);
|
luaL_newlib(state, FUNCTIONS);
|
||||||
const FontIds fonts = Runtime::from(state)->gui().fonts();
|
const FontIds fonts = Runtime::from(state)->gui().fonts();
|
||||||
setField(state, "FONT_SMALL", fonts.small);
|
setField(state, "FONT_SMALL", fonts.small);
|
||||||
@@ -342,7 +358,7 @@ void registerGui(lua_State* state) {
|
|||||||
setField(state, "FONT_LARGE", fonts.large);
|
setField(state, "FONT_LARGE", fonts.large);
|
||||||
setField(state, "STYLE_NORMAL", fonts.styleNormal);
|
setField(state, "STYLE_NORMAL", fonts.styleNormal);
|
||||||
setField(state, "STYLE_BOLD", fonts.styleBold);
|
setField(state, "STYLE_BOLD", fonts.styleBold);
|
||||||
lua_setglobal(state, "gui");
|
lua_setglobal(state, "screen");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace bindings
|
} // namespace bindings
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// @lua-module node NodeLib
|
// @lua-module tree TreeLib
|
||||||
// @lua-preamble ---@alias NodeId integer
|
// @lua-preamble ---@alias NodeId integer
|
||||||
// @lua-preamble ---@alias NodeType "box"|"text"|"button"|"custom"
|
// @lua-preamble ---@alias NodeType "box"|"text"|"button"|"custom"
|
||||||
// @lua-preamble ---@alias NodeDirection "up"|"down"|"left"|"right"
|
// @lua-preamble ---@alias NodeDirection "up"|"down"|"left"|"right"
|
||||||
@@ -13,30 +13,29 @@
|
|||||||
// @lua-preamble ---@field justify? "start"|"center"|"end"|"between"
|
// @lua-preamble ---@field justify? "start"|"center"|"end"|"between"
|
||||||
// @lua-preamble ---@field row? boolean
|
// @lua-preamble ---@field row? boolean
|
||||||
// @lua-preamble ---@field at? table Absolute-position fields.
|
// @lua-preamble ---@field at? table Absolute-position fields.
|
||||||
// @lua-preamble ---@field capture? boolean
|
|
||||||
// @lua-preamble ---@field interactive? boolean
|
// @lua-preamble ---@field interactive? boolean
|
||||||
// @lua-preamble ---@field label? string
|
// @lua-preamble ---@field label? string
|
||||||
// @lua-preamble ---@field font? GuiFont
|
// @lua-preamble ---@field font? ScreenFont
|
||||||
// @lua-preamble
|
// @lua-preamble
|
||||||
// @lua-preamble ---@class NodeStyle
|
// @lua-preamble ---@class NodeStyle
|
||||||
// @lua-preamble ---@field color? GuiColor
|
// @lua-preamble ---@field color? ScreenColor
|
||||||
// @lua-preamble ---@field background? GuiColor Background offered to
|
// @lua-preamble ---@field background? ScreenColor Background offered to
|
||||||
// descendants.
|
// descendants.
|
||||||
// @lua-preamble ---@field fill? GuiColor Surface painted by a box.
|
// @lua-preamble ---@field fill? ScreenColor Surface painted by a box.
|
||||||
// @lua-preamble ---@field border? GuiColor
|
// @lua-preamble ---@field border? ScreenColor
|
||||||
// @lua-preamble ---@field face? GuiColor Default button surface.
|
// @lua-preamble ---@field face? ScreenColor Default button surface.
|
||||||
// @lua-preamble ---@field pressedFace? GuiColor Pressed button surface.
|
// @lua-preamble ---@field pressedFace? ScreenColor Pressed button surface.
|
||||||
// @lua-preamble ---@field pressedColor? GuiColor Pressed button text.
|
// @lua-preamble ---@field pressedColor? ScreenColor Pressed button text.
|
||||||
// @lua-preamble ---@field focusColor? GuiColor Distinct outline for directional
|
// @lua-preamble ---@field focusColor? ScreenColor Distinct outline for
|
||||||
// focus.
|
// directional focus.
|
||||||
// @lua-preamble ---@field radius? integer
|
// @lua-preamble ---@field radius? integer
|
||||||
// @lua-preamble ---@field font? GuiFont
|
// @lua-preamble ---@field font? ScreenFont
|
||||||
// @lua-preamble ---@field textStyle? GuiTextStyle
|
// @lua-preamble ---@field textStyle? ScreenTextStyle
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "../../node/painter.h"
|
#include "../../../node/painter.h"
|
||||||
#include "../helpers.h"
|
#include "../../helpers.h"
|
||||||
|
|
||||||
namespace esp32lua {
|
namespace esp32lua {
|
||||||
namespace bindings {
|
namespace bindings {
|
||||||
@@ -173,10 +172,12 @@ int create(lua_State* state) {
|
|||||||
uint8_t flags = 0;
|
uint8_t flags = 0;
|
||||||
if (readFlag(state, 2, "row"))
|
if (readFlag(state, 2, "row"))
|
||||||
flags |= ui::ROW;
|
flags |= ui::ROW;
|
||||||
if (readFlag(state, 2, "capture"))
|
|
||||||
flags |= ui::CAPTURE;
|
|
||||||
if (readFlag(state, 2, "interactive"))
|
if (readFlag(state, 2, "interactive"))
|
||||||
flags |= ui::INTERACTIVE;
|
flags |= ui::INTERACTIVE;
|
||||||
|
if (readFlag(state, 2, "scrollX"))
|
||||||
|
flags |= ui::SCROLL_X;
|
||||||
|
if (readFlag(state, 2, "scrollY"))
|
||||||
|
flags |= ui::SCROLL_Y;
|
||||||
|
|
||||||
lua_getfield(state, 2, "font");
|
lua_getfield(state, 2, "font");
|
||||||
const int32_t font = lua_isnoneornil(state, -1)
|
const int32_t font = lua_isnoneornil(state, -1)
|
||||||
@@ -270,6 +271,27 @@ int getRect(lua_State* state) {
|
|||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int setScroll(lua_State* state) {
|
||||||
|
tree(state).setScroll(checkNode(state, 1), checkInt(state, 2),
|
||||||
|
checkInt(state, 3));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getScroll(lua_State* state) {
|
||||||
|
const ui::Scroll* scroll = tree(state).scrollOf(checkNode(state, 1));
|
||||||
|
lua_pushinteger(state, scroll ? scroll->x : 0);
|
||||||
|
lua_pushinteger(state, scroll ? scroll->y : 0);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getScrollRange(lua_State* state) {
|
||||||
|
int maxX = 0, maxY = 0;
|
||||||
|
tree(state).scrollRange(checkNode(state, 1), maxX, maxY);
|
||||||
|
lua_pushinteger(state, maxX);
|
||||||
|
lua_pushinteger(state, maxY);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
int setLabel(lua_State* state) {
|
int setLabel(lua_State* state) {
|
||||||
const uint16_t id = checkNode(state, 1);
|
const uint16_t id = checkNode(state, 1);
|
||||||
const char* text = luaL_checkstring(state, 2);
|
const char* text = luaL_checkstring(state, 2);
|
||||||
@@ -467,7 +489,8 @@ int moveFocus(lua_State* state) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void callPainter(void* context, uint16_t id, int x, int y, int w, int h) {
|
void callPainter(void* context, uint16_t id, int x, int y, int w, int h,
|
||||||
|
int clipX, int clipY, int clipW, int clipH) {
|
||||||
lua_State* state = static_cast<lua_State*>(context);
|
lua_State* state = static_cast<lua_State*>(context);
|
||||||
lua_getfield(state, LUA_REGISTRYINDEX, PAINTER_KEY);
|
lua_getfield(state, LUA_REGISTRYINDEX, PAINTER_KEY);
|
||||||
if (!lua_isfunction(state, -1)) {
|
if (!lua_isfunction(state, -1)) {
|
||||||
@@ -479,7 +502,11 @@ void callPainter(void* context, uint16_t id, int x, int y, int w, int h) {
|
|||||||
lua_pushinteger(state, y);
|
lua_pushinteger(state, y);
|
||||||
lua_pushinteger(state, w);
|
lua_pushinteger(state, w);
|
||||||
lua_pushinteger(state, h);
|
lua_pushinteger(state, h);
|
||||||
if (lua_pcall(state, 5, 0, 0) != LUA_OK) {
|
lua_pushinteger(state, clipX);
|
||||||
|
lua_pushinteger(state, clipY);
|
||||||
|
lua_pushinteger(state, clipW);
|
||||||
|
lua_pushinteger(state, clipH);
|
||||||
|
if (lua_pcall(state, 9, 0, 0) != LUA_OK) {
|
||||||
Runtime::from(state)->log().write(
|
Runtime::from(state)->log().write(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
lua_tostring(state, -1) ? lua_tostring(state, -1) : "painter");
|
lua_tostring(state, -1) ? lua_tostring(state, -1) : "painter");
|
||||||
@@ -554,6 +581,21 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// @return integer w
|
// @return integer w
|
||||||
// @return integer h
|
// @return integer h
|
||||||
{"getRect", getRect},
|
{"getRect", getRect},
|
||||||
|
// --- Pans a scrollable node's children, clamped to the content. The node
|
||||||
|
// is marked dirty, so the next draw repaints it.
|
||||||
|
// @param id NodeId
|
||||||
|
// @param x integer
|
||||||
|
// @param y integer
|
||||||
|
{"setScroll", setScroll},
|
||||||
|
// --- Returns a scrollable node's current offset, or zeroes.
|
||||||
|
// @param id NodeId
|
||||||
|
// @return integer x, integer y
|
||||||
|
{"getScroll", getScroll},
|
||||||
|
// --- Returns how far each axis can pan before the content's far edge
|
||||||
|
// reaches the box. Zero on an axis whose content fits.
|
||||||
|
// @param id NodeId
|
||||||
|
// @return integer maxX, integer maxY
|
||||||
|
{"getScrollRange", getScrollRange},
|
||||||
// --- Replaces a node's text and marks it for repaint.
|
// --- Replaces a node's text and marks it for repaint.
|
||||||
// @param id NodeId
|
// @param id NodeId
|
||||||
// @param text string
|
// @param text string
|
||||||
@@ -597,9 +639,12 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// @param direction NodeDirection
|
// @param direction NodeDirection
|
||||||
// @return NodeId|nil focused Current focus when no candidate exists.
|
// @return NodeId|nil focused Current focus when no candidate exists.
|
||||||
{"moveFocus", moveFocus},
|
{"moveFocus", moveFocus},
|
||||||
// --- Registers the painter every custom node calls.
|
// --- Registers the painter every custom node calls. The clip arguments are
|
||||||
|
// the region of the node being painted now -- one band of a composited
|
||||||
|
// repaint -- so a painter can cull to it instead of redrawing itself once
|
||||||
|
// per band.
|
||||||
// @param painter fun(id: NodeId, x: integer, y: integer, w: integer, h:
|
// @param painter fun(id: NodeId, x: integer, y: integer, w: integer, h:
|
||||||
// integer)
|
// integer, clipX: integer, clipY: integer, clipW: integer, clipH: integer)
|
||||||
{"setPainter", setPainter},
|
{"setPainter", setPainter},
|
||||||
// --- Paints dirty nodes; the firmware owns publication to the physical
|
// --- Paints dirty nodes; the firmware owns publication to the physical
|
||||||
// display.
|
// display.
|
||||||
@@ -616,9 +661,9 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void registerNode(lua_State* state) {
|
void registerTree(lua_State* state) {
|
||||||
luaL_newlib(state, FUNCTIONS);
|
luaL_newlib(state, FUNCTIONS);
|
||||||
lua_setglobal(state, "node");
|
lua_setglobal(state, "tree");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace bindings
|
} // namespace bindings
|
||||||
@@ -40,8 +40,21 @@ int isTouched(lua_State* state) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @lua-augment settings SettingsLib
|
// @lua-module touch TouchLib
|
||||||
const luaL_Reg SETTINGS_FUNCTIONS[] = {
|
const luaL_Reg FUNCTIONS[] = {
|
||||||
|
// --- Returns the calibrated touch point, or nothing when the panel is not
|
||||||
|
// touched.
|
||||||
|
// @return integer|nil x
|
||||||
|
// @return integer|nil y
|
||||||
|
{"getPoint", getTouch},
|
||||||
|
// --- Returns the uncalibrated touch reading, or nothing when the panel is
|
||||||
|
// not touched.
|
||||||
|
// @return integer|nil x
|
||||||
|
// @return integer|nil y
|
||||||
|
{"getRawPoint", getRawTouch},
|
||||||
|
// --- Whether the panel is currently touched.
|
||||||
|
// @return boolean
|
||||||
|
{"isTouched", isTouched},
|
||||||
// --- Persists the panel's touch calibration.
|
// --- Persists the panel's touch calibration.
|
||||||
// @param x0 integer Raw reading at the left edge.
|
// @param x0 integer Raw reading at the left edge.
|
||||||
// @param y0 integer Raw reading at the top edge.
|
// @param y0 integer Raw reading at the top edge.
|
||||||
@@ -53,49 +66,35 @@ const luaL_Reg SETTINGS_FUNCTIONS[] = {
|
|||||||
{nullptr, nullptr},
|
{nullptr, nullptr},
|
||||||
};
|
};
|
||||||
|
|
||||||
// @lua-augment input InputLib
|
|
||||||
const luaL_Reg INPUT_FUNCTIONS[] = {
|
|
||||||
// --- Returns the calibrated touch point, or nothing when the panel is not
|
|
||||||
// touched.
|
|
||||||
// @return integer|nil x
|
|
||||||
// @return integer|nil y
|
|
||||||
{"getTouch", getTouch},
|
|
||||||
// --- Returns the uncalibrated touch reading, or nothing when the panel is
|
|
||||||
// not touched.
|
|
||||||
// @return integer|nil x
|
|
||||||
// @return integer|nil y
|
|
||||||
{"getRawTouch", getRawTouch},
|
|
||||||
// --- Whether the panel is currently touched.
|
|
||||||
// @return boolean
|
|
||||||
{"isTouched", isTouched},
|
|
||||||
{nullptr, nullptr},
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void registerTouch(lua_State* state) {
|
void registerTouch(lua_State* state) {
|
||||||
augmentGlobal(state, "settings", SETTINGS_FUNCTIONS);
|
luaL_newlib(state, FUNCTIONS);
|
||||||
augmentGlobal(state, "input", INPUT_FUNCTIONS);
|
lua_setglobal(state, "touch");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace bindings
|
} // namespace bindings
|
||||||
} // namespace esp32lua
|
} // namespace esp32lua
|
||||||
|
|
||||||
// @lua-global
|
// @lua-app TouchHandlers
|
||||||
|
// @lua-preamble -- What an app implements to see raw touch, composed into its
|
||||||
|
// own class:
|
||||||
|
// @lua-preamble --
|
||||||
|
// @lua-preamble -- ---@class PaintApp : App, TouchHandlers
|
||||||
// ---Fired when the finger lands.
|
// ---Fired when the finger lands.
|
||||||
// @param x integer
|
// @param x integer
|
||||||
// @param y integer
|
// @param y integer
|
||||||
// @lua-fn on_touch_down
|
// @lua-fn onTouchDown?
|
||||||
// ---Fired when the finger moves while down, after the firmware's jitter
|
// ---Fired when the finger moves while down, after the firmware's jitter
|
||||||
// filter.
|
// filter.
|
||||||
// @param x integer
|
// @param x integer
|
||||||
// @param y integer
|
// @param y integer
|
||||||
// @lua-fn on_touch_move
|
// @lua-fn onTouchMove?
|
||||||
// ---Fired when the finger lifts.
|
// ---Fired when the finger lifts.
|
||||||
// @param x integer
|
// @param x integer
|
||||||
// @param y integer
|
// @param y integer
|
||||||
// @lua-fn on_touch_up
|
// @lua-fn onTouchUp?
|
||||||
// ---Tap alias, fired on release like a click, after on_touch_up.
|
// ---Tap alias, fired on release like a click, after onTouchUp.
|
||||||
// @param x integer
|
// @param x integer
|
||||||
// @param y integer
|
// @param y integer
|
||||||
// @lua-fn on_touch
|
// @lua-fn onTouch?
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// JSON for apps, provided by lua-cjson under native/src/vendor/cjson. It
|
||||||
|
// decodes straight onto the Lua stack rather than building a document first, so
|
||||||
|
// a response costs its text plus the table it becomes and nothing in between.
|
||||||
|
|
||||||
|
#include <lua/runtime.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "lauxlib.h"
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
int luaopen_cjson(lua_State* state);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace esp32lua {
|
||||||
|
namespace bindings {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// lua-cjson defaults to 1000 and decodes by recursing on the C stack, which is
|
||||||
|
// sized for a server rather than a FreeRTOS task. Setting it through the
|
||||||
|
// module's own knob keeps the vendored source unpatched.
|
||||||
|
constexpr lua_Integer MAX_DEPTH = 32;
|
||||||
|
|
||||||
|
void setDepth(lua_State* state, const char* name) {
|
||||||
|
lua_getfield(state, -1, name);
|
||||||
|
lua_pushinteger(state, MAX_DEPTH);
|
||||||
|
lua_call(state, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int open(lua_State* state) {
|
||||||
|
luaopen_cjson(state);
|
||||||
|
setDepth(state, "decode_max_depth");
|
||||||
|
setDepth(state, "encode_max_depth");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// package.preload rather than a global: the library provides this itself, so no
|
||||||
|
// firmware implements it and nothing on the SD card shadows it. An app that
|
||||||
|
// never requires it never pays for the module.
|
||||||
|
void registerJson(lua_State* state) {
|
||||||
|
luaL_getsubtable(state, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
|
||||||
|
lua_pushcfunction(state, open);
|
||||||
|
lua_setfield(state, -2, "cjson");
|
||||||
|
lua_pop(state, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace bindings
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Leaves cjson.<name> on the stack. Going through require() rather than a
|
||||||
|
// second luaopen_cjson keeps one module, and one configured depth, per state.
|
||||||
|
void pushCjson(lua_State* state, const char* name) {
|
||||||
|
lua_getglobal(state, "require");
|
||||||
|
lua_pushstring(state, "cjson");
|
||||||
|
lua_call(state, 1, 1);
|
||||||
|
lua_getfield(state, -1, name);
|
||||||
|
lua_remove(state, -2);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string encodeJson(lua_State* state, int index) {
|
||||||
|
const int value = lua_absindex(state, index);
|
||||||
|
pushCjson(state, "encode");
|
||||||
|
lua_pushvalue(state, value);
|
||||||
|
lua_call(state, 1, 1);
|
||||||
|
size_t length = 0;
|
||||||
|
const char* text = lua_tolstring(state, -1, &length);
|
||||||
|
const std::string json(text ? text : "", length);
|
||||||
|
lua_pop(state, 1);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool decodeJson(lua_State* state, const std::string& json) {
|
||||||
|
if (lua_gettop(state) + 4 > LUAI_MAXSTACK)
|
||||||
|
return false;
|
||||||
|
pushCjson(state, "decode");
|
||||||
|
lua_pushlstring(state, json.data(), json.size());
|
||||||
|
if (lua_pcall(state, 1, 1, 0) != LUA_OK) {
|
||||||
|
lua_pop(state, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esp32lua
|
||||||
File diff suppressed because one or more lines are too long
+220
-20
@@ -1,10 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// Painting the node tree through GuiProvider, so the same walk drives an LCD
|
// Painting the node tree through GuiProvider, so the same walk drives an LCD
|
||||||
// and an e-ink panel. A dirty node paints itself and dirties its children,
|
// and an e-ink panel. The dirty region is composited a band at a time into an
|
||||||
// because a parent's fill lands on top of whatever they drew; nothing tracks
|
// offscreen buffer and pushed once, so a full-pane repaint -- a scroll -- moves
|
||||||
// sub-regions, and a widget that wants to repaint part of itself is a CUSTOM
|
// smoothly instead of stalling the bus per primitive. The tree is the display
|
||||||
// node painting through the gui bindings.
|
// list: each band re-walks it and paints the nodes it touches, no recorded
|
||||||
|
// command stream. A driver that offers no buffer falls back to painting the
|
||||||
|
// dirty nodes straight to the panel, which is what an e-ink panel wants anyway.
|
||||||
|
// A widget repainting part of itself is still a CUSTOM node painting through
|
||||||
|
// the gui bindings; a custom painter is re-invoked per band it spans.
|
||||||
|
|
||||||
#include <lua/layout.h>
|
#include <lua/layout.h>
|
||||||
#include <lua/providers.h>
|
#include <lua/providers.h>
|
||||||
@@ -12,8 +16,21 @@
|
|||||||
namespace esp32lua {
|
namespace esp32lua {
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
|
// The node's own box, then the region of it being painted right now -- one band, or the
|
||||||
|
// whole node on the panel fallback. A painter that culls to the clip does its work once a
|
||||||
|
// frame instead of once per band it spans; one that ignores the extra arguments still
|
||||||
|
// paints correctly, because every write is clipped anyway.
|
||||||
typedef void (*CustomPainter)(void* context, uint16_t id, int x, int y, int w,
|
typedef void (*CustomPainter)(void* context, uint16_t id, int x, int y, int w,
|
||||||
int h);
|
int h, int clipX, int clipY, int clipW,
|
||||||
|
int clipH);
|
||||||
|
|
||||||
|
// Starting band height. How much contiguous heap exists depends on the app, the
|
||||||
|
// orientation and how fragmented the heap already is, so this is a ceiling the
|
||||||
|
// painter shrinks from rather than a size it assumes: a band that will not
|
||||||
|
// allocate is halved and retried, down to MIN_BAND, before the repaint falls
|
||||||
|
// back to the panel.
|
||||||
|
constexpr int BAND_HEIGHT = 48;
|
||||||
|
constexpr int MIN_BAND = 8;
|
||||||
|
|
||||||
class Painter {
|
class Painter {
|
||||||
public:
|
public:
|
||||||
@@ -23,22 +40,188 @@ public:
|
|||||||
void* context = nullptr;
|
void* context = nullptr;
|
||||||
|
|
||||||
void draw(uint16_t id) {
|
void draw(uint16_t id) {
|
||||||
if (tree.nodes[id].flags & DIRTY) {
|
Rect dirty;
|
||||||
paint(id);
|
collectDirty(id, false, dirty, Box::unbounded());
|
||||||
tree.nodes[id].flags &= ~DIRTY;
|
if (dirty.empty())
|
||||||
for (uint16_t c = tree.nodes[id].first; c != NONE;
|
return;
|
||||||
c = tree.nodes[c].next) {
|
if (!drawBanded(id, dirty))
|
||||||
tree.nodes[c].flags |= DIRTY;
|
drawDirect(id);
|
||||||
}
|
clearDirty(id);
|
||||||
}
|
|
||||||
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next)
|
|
||||||
draw(c);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GuiProvider& gui;
|
GuiProvider& gui;
|
||||||
Tree& tree;
|
Tree& tree;
|
||||||
|
|
||||||
|
struct Rect {
|
||||||
|
int minX = 1 << 30, minY = 1 << 30, maxX = -(1 << 30), maxY = -(1 << 30);
|
||||||
|
bool empty() const { return maxX <= minX || maxY <= minY; }
|
||||||
|
void add(int x, int y, int w, int h) {
|
||||||
|
if (x < minX)
|
||||||
|
minX = x;
|
||||||
|
if (y < minY)
|
||||||
|
minY = y;
|
||||||
|
if (x + w > maxX)
|
||||||
|
maxX = x + w;
|
||||||
|
if (y + h > maxY)
|
||||||
|
maxY = y + h;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// An edge-bounded rectangle, which a Rect built by union cannot express: the
|
||||||
|
// clip narrows as the walk descends and has to start meaning "no limit".
|
||||||
|
struct Box {
|
||||||
|
int x0, y0, x1, y1;
|
||||||
|
static Box unbounded() {
|
||||||
|
const Box b = {-(1 << 30), -(1 << 30), 1 << 30, 1 << 30};
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
bool empty() const { return x1 <= x0 || y1 <= y0; }
|
||||||
|
Box clipTo(int x, int y, int w, int h) const {
|
||||||
|
Box b = {x > x0 ? x : x0, y > y0 ? y : y0, x + w < x1 ? x + w : x1,
|
||||||
|
y + h < y1 ? y + h : y1};
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The clip in force, so a custom node inside a scroll container narrows the
|
||||||
|
// container's box instead of replacing it and painting over the chrome when
|
||||||
|
// it restores.
|
||||||
|
Box clip = Box::unbounded();
|
||||||
|
|
||||||
|
Box pushClip(const Box& next) {
|
||||||
|
const Box previous = clip;
|
||||||
|
clip = next;
|
||||||
|
gui.setClip(next.x0, next.y0, next.x1 - next.x0, next.y1 - next.y0);
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
void popClip(const Box& previous) {
|
||||||
|
clip = previous;
|
||||||
|
if (previous.x0 == -(1 << 30) && previous.y0 == -(1 << 30))
|
||||||
|
gui.clearClip();
|
||||||
|
else
|
||||||
|
gui.setClip(previous.x0, previous.y0, previous.x1 - previous.x0,
|
||||||
|
previous.y1 - previous.y0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool overlaps(const Node& n, int x, int y, int w, int h) {
|
||||||
|
return n.x < x + w && n.x + n.w > x && n.y < y + h && n.y + n.h > y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The region that will repaint: a dirty node and every descendant, because a
|
||||||
|
// dirty parent's fill lands over its children. Computed before painting so a
|
||||||
|
// band knows its extent without the dirty flags it is about to clear.
|
||||||
|
//
|
||||||
|
// Bounded by the enclosing scroll containers, because a scrolled subtree is
|
||||||
|
// as tall as its content: unclipped, dirtying a list of sixty rows would ask
|
||||||
|
// for a dirty region thousands of pixels tall and band the whole of it.
|
||||||
|
void collectDirty(uint16_t id, bool ancestorDirty, Rect& acc,
|
||||||
|
const Box& bounds) {
|
||||||
|
const Node& n = tree.nodes[id];
|
||||||
|
const bool dirty = ancestorDirty || (n.flags & DIRTY);
|
||||||
|
if (dirty) {
|
||||||
|
const Box visible = bounds.clipTo(n.x, n.y, n.w, n.h);
|
||||||
|
if (!visible.empty())
|
||||||
|
acc.add(visible.x0, visible.y0, visible.x1 - visible.x0,
|
||||||
|
visible.y1 - visible.y0);
|
||||||
|
}
|
||||||
|
const Box inner =
|
||||||
|
tree.scrolls(id) ? bounds.clipTo(n.x, n.y, n.w, n.h) : bounds;
|
||||||
|
if (inner.empty())
|
||||||
|
return;
|
||||||
|
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next)
|
||||||
|
collectDirty(c, dirty, acc, inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearDirty(uint16_t id) {
|
||||||
|
tree.nodes[id].flags &= ~DIRTY;
|
||||||
|
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next)
|
||||||
|
clearDirty(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composite the dirty region band by band. Returns false without drawing when
|
||||||
|
// the first band cannot be allocated, so the caller paints to the panel
|
||||||
|
// instead. A band is cleared to the root background because its buffer starts
|
||||||
|
// undefined; the nodes then paint their own fills over it in tree order.
|
||||||
|
bool drawBanded(uint16_t root, const Rect& dirty) {
|
||||||
|
const int x = dirty.minX;
|
||||||
|
const int w = dirty.maxX - dirty.minX;
|
||||||
|
const int32_t background = tree.inherited(root, S_BG).bg;
|
||||||
|
bool any = false;
|
||||||
|
int band = BAND_HEIGHT;
|
||||||
|
int y = dirty.minY;
|
||||||
|
while (y < dirty.maxY) {
|
||||||
|
const int h = y + band > dirty.maxY ? dirty.maxY - y : band;
|
||||||
|
if (gui.beginBuffer(x, y, w, h)) {
|
||||||
|
any = true;
|
||||||
|
gui.clear(background);
|
||||||
|
paintBand(root, x, y, w, h);
|
||||||
|
gui.present();
|
||||||
|
y += h;
|
||||||
|
} else if (band > MIN_BAND) {
|
||||||
|
band /= 2; // too big for this heap: retry the same strip, smaller
|
||||||
|
} else if (any) {
|
||||||
|
paintBand(root, x, y, w, h); // mid-frame failure: draw this strip direct
|
||||||
|
y += h;
|
||||||
|
} else {
|
||||||
|
return false; // nothing fits: caller uses the panel path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint every node overlapping the band, parent before child so fills sit
|
||||||
|
// under their contents. Unlike drawDirect this ignores the dirty flag: the
|
||||||
|
// band's buffer was just cleared, so everything visible in it must be redrawn.
|
||||||
|
void paintBand(uint16_t id, int x, int y, int w, int h) {
|
||||||
|
const Node& n = tree.nodes[id];
|
||||||
|
if (overlaps(n, x, y, w, h))
|
||||||
|
paint(id, x, y, w, h);
|
||||||
|
if (n.first == NONE)
|
||||||
|
return;
|
||||||
|
// A scroll container's children are laid out past its edges, so the subtree
|
||||||
|
// is scissored to the box on the way in. Without it a row half scrolled off
|
||||||
|
// the top paints over whatever sits above the container.
|
||||||
|
const bool scissor = tree.scrolls(id);
|
||||||
|
Box previous = clip;
|
||||||
|
if (scissor) {
|
||||||
|
const Box inner = clip.clipTo(n.x, n.y, n.w, n.h);
|
||||||
|
if (inner.empty())
|
||||||
|
return;
|
||||||
|
previous = pushClip(inner);
|
||||||
|
}
|
||||||
|
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next)
|
||||||
|
paintBand(c, x, y, w, h);
|
||||||
|
if (scissor)
|
||||||
|
popClip(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panel fallback: the original dirty-propagating walk, straight to the panel.
|
||||||
|
void drawDirect(uint16_t id) {
|
||||||
|
const Node& n = tree.nodes[id];
|
||||||
|
if (n.flags & DIRTY) {
|
||||||
|
paint(id, n.x, n.y, n.w, n.h);
|
||||||
|
for (uint16_t c = tree.nodes[id].first; c != NONE;
|
||||||
|
c = tree.nodes[c].next)
|
||||||
|
tree.nodes[c].flags |= DIRTY;
|
||||||
|
}
|
||||||
|
if (n.first == NONE)
|
||||||
|
return;
|
||||||
|
const bool scissor = tree.scrolls(id);
|
||||||
|
Box previous = clip;
|
||||||
|
if (scissor) {
|
||||||
|
const Box inner = clip.clipTo(n.x, n.y, n.w, n.h);
|
||||||
|
if (inner.empty())
|
||||||
|
return;
|
||||||
|
previous = pushClip(inner);
|
||||||
|
}
|
||||||
|
for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next)
|
||||||
|
drawDirect(c);
|
||||||
|
if (scissor)
|
||||||
|
popClip(previous);
|
||||||
|
}
|
||||||
|
|
||||||
// What a node sits on, which is not what it fills. Derived rather than
|
// What a node sits on, which is not what it fills. Derived rather than
|
||||||
// stored, because a node cannot be told what is behind it: a dialog layer
|
// stored, because a node cannot be told what is behind it: a dialog layer
|
||||||
// paints nothing, so its card blends into the dimmed content two levels up,
|
// paints nothing, so its card blends into the dimmed content two levels up,
|
||||||
@@ -56,7 +239,7 @@ private:
|
|||||||
return tree.inherited(root, S_BG).bg;
|
return tree.inherited(root, S_BG).bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
void paint(uint16_t id) {
|
void paint(uint16_t id, int clipX, int clipY, int clipW, int clipH) {
|
||||||
const Node& n = tree.nodes[id];
|
const Node& n = tree.nodes[id];
|
||||||
switch (n.type) {
|
switch (n.type) {
|
||||||
case BUTTON:
|
case BUTTON:
|
||||||
@@ -65,13 +248,29 @@ private:
|
|||||||
case TEXT:
|
case TEXT:
|
||||||
paintText(id);
|
paintText(id);
|
||||||
break;
|
break;
|
||||||
case CUSTOM:
|
case CUSTOM: {
|
||||||
// Cleared first, because a custom painter draws what it wants and nothing
|
// Cleared first, because a custom painter draws what it wants and nothing
|
||||||
// knows what it drew last time.
|
// knows what it drew last time. Only the band's slice of the node is
|
||||||
gui.fillRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_BG).bg);
|
// cleared, because each band clears its own before repainting it.
|
||||||
|
const int x0 = n.x > clipX ? n.x : clipX;
|
||||||
|
const int y0 = n.y > clipY ? n.y : clipY;
|
||||||
|
const int nx1 = n.x + n.w, ny1 = n.y + n.h;
|
||||||
|
const int x1 = nx1 < clipX + clipW ? nx1 : clipX + clipW;
|
||||||
|
const int y1 = ny1 < clipY + clipH ? ny1 : clipY + clipH;
|
||||||
|
if (x1 <= x0 || y1 <= y0)
|
||||||
|
break;
|
||||||
|
// Scissored to the node, not just to the band: a custom painter draws what
|
||||||
|
// it likes and a band is usually taller than the node, so the overflow of
|
||||||
|
// a row half scrolled off the top would otherwise paint over the chrome
|
||||||
|
// above it. Culling cannot replace this -- the row is meant to be drawn,
|
||||||
|
// just cut off at the node's edge.
|
||||||
|
const Box previous = pushClip(clip.clipTo(x0, y0, x1 - x0, y1 - y0));
|
||||||
|
gui.fillRect(x0, y0, x1 - x0, y1 - y0, tree.inherited(id, S_BG).bg);
|
||||||
if (custom)
|
if (custom)
|
||||||
custom(context, id, n.x, n.y, n.w, n.h);
|
custom(context, id, n.x, n.y, n.w, n.h, x0, y0, x1 - x0, y1 - y0);
|
||||||
|
popClip(previous);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
paintBox(id);
|
paintBox(id);
|
||||||
break;
|
break;
|
||||||
@@ -80,6 +279,7 @@ private:
|
|||||||
paintFocus(id);
|
paintFocus(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void paintBox(uint16_t id) {
|
void paintBox(uint16_t id) {
|
||||||
const Node& n = tree.nodes[id];
|
const Node& n = tree.nodes[id];
|
||||||
const Style* own = tree.styleOf(id);
|
const Style* own = tree.styleOf(id);
|
||||||
|
|||||||
@@ -123,13 +123,11 @@ int Runtime::loadFile(lua_State* state) {
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Runtime::installLoader(const std::string& appDir) {
|
// package.path is left to main.lua, which is loaded by absolute path and knows
|
||||||
|
// where its libraries and its apps are.
|
||||||
|
void Runtime::installLoader() {
|
||||||
lua_getglobal(state_, "package");
|
lua_getglobal(state_, "package");
|
||||||
|
|
||||||
const std::string path = appDir + "/?.lua;" + paths_.lib + "/?.lua";
|
|
||||||
lua_pushlstring(state_, path.data(), path.size());
|
|
||||||
lua_setfield(state_, -2, "path");
|
|
||||||
|
|
||||||
// Keep the preload searcher, drop the C loaders: they can only report
|
// Keep the preload searcher, drop the C loaders: they can only report
|
||||||
// misleading errors about shared objects that were never there.
|
// misleading errors about shared objects that were never there.
|
||||||
lua_getfield(state_, -1, "searchers");
|
lua_getfield(state_, -1, "searchers");
|
||||||
|
|||||||
+107
-99
@@ -11,12 +11,12 @@ namespace bindings {
|
|||||||
void registerBle(lua_State* state);
|
void registerBle(lua_State* state);
|
||||||
void registerButtons(lua_State* state);
|
void registerButtons(lua_State* state);
|
||||||
void registerFs(lua_State* state);
|
void registerFs(lua_State* state);
|
||||||
void registerGui(lua_State* state);
|
|
||||||
void registerHttp(lua_State* state);
|
void registerHttp(lua_State* state);
|
||||||
|
void registerJson(lua_State* state);
|
||||||
void registerLog(lua_State* state);
|
void registerLog(lua_State* state);
|
||||||
void registerNode(lua_State* state);
|
void registerScreen(lua_State* state);
|
||||||
void registerSettings(lua_State* state);
|
|
||||||
void registerSys(lua_State* state);
|
void registerSys(lua_State* state);
|
||||||
|
void registerTree(lua_State* state);
|
||||||
void registerTimer(lua_State* state);
|
void registerTimer(lua_State* state);
|
||||||
void registerTouch(lua_State* state);
|
void registerTouch(lua_State* state);
|
||||||
void registerWifi(lua_State* state);
|
void registerWifi(lua_State* state);
|
||||||
@@ -26,10 +26,13 @@ namespace {
|
|||||||
|
|
||||||
// App routes are relative and stay inside the apps root, so a traversal
|
// App routes are relative and stay inside the apps root, so a traversal
|
||||||
// component is a hard no.
|
// component is a hard no.
|
||||||
bool isSafeRoute(const std::string& path) {
|
// Absolute, and no component that could climb out of the card. Apps are
|
||||||
if (path.empty() || path[0] == '/')
|
// trusted, so this is a guard against a mistake rather than an attacker -- but
|
||||||
|
// it is the one place a path from Lua becomes a file the runtime opens.
|
||||||
|
bool isSafePath(const std::string& path) {
|
||||||
|
if (path.empty() || path[0] != '/')
|
||||||
return false;
|
return false;
|
||||||
size_t start = 0;
|
size_t start = 1;
|
||||||
while (start <= path.size()) {
|
while (start <= path.size()) {
|
||||||
const size_t end = path.find('/', start);
|
const size_t end = path.find('/', start);
|
||||||
const std::string part = path.substr(
|
const std::string part = path.substr(
|
||||||
@@ -45,17 +48,16 @@ bool isSafeRoute(const std::string& path) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Runtime::Runtime(const Providers& providers, const Paths& paths)
|
Runtime::Runtime(const Providers& providers) : providers_(providers) {}
|
||||||
: providers_(providers), paths_(paths) {}
|
|
||||||
|
|
||||||
Runtime::~Runtime() { close(); }
|
Runtime::~Runtime() { close(); }
|
||||||
|
|
||||||
bool Runtime::open() {
|
bool Runtime::open() {
|
||||||
if (state_)
|
if (state_)
|
||||||
return true;
|
return true;
|
||||||
if (!providers_.log || !providers_.settings || !providers_.sys ||
|
if (!providers_.log || !providers_.sys || !providers_.fs ||
|
||||||
!providers_.fs || !providers_.gui || !providers_.http ||
|
!providers_.http || !providers_.timer || !providers_.wifi ||
|
||||||
!providers_.timer || !providers_.wifi || !providers_.ble) {
|
!providers_.ble) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,19 +67,21 @@ bool Runtime::open() {
|
|||||||
*static_cast<Runtime**>(lua_getextraspace(state_)) = this;
|
*static_cast<Runtime**>(lua_getextraspace(state_)) = this;
|
||||||
luaL_openlibs(state_);
|
luaL_openlibs(state_);
|
||||||
|
|
||||||
|
// Primary Namespaces
|
||||||
bindings::registerBle(state_);
|
bindings::registerBle(state_);
|
||||||
bindings::registerFs(state_);
|
bindings::registerFs(state_);
|
||||||
bindings::registerGui(state_);
|
|
||||||
bindings::registerHttp(state_);
|
bindings::registerHttp(state_);
|
||||||
|
bindings::registerJson(state_);
|
||||||
bindings::registerLog(state_);
|
bindings::registerLog(state_);
|
||||||
bindings::registerNode(state_);
|
|
||||||
bindings::registerSettings(state_);
|
|
||||||
bindings::registerSys(state_);
|
bindings::registerSys(state_);
|
||||||
bindings::registerTimer(state_);
|
bindings::registerTimer(state_);
|
||||||
bindings::registerWifi(state_);
|
bindings::registerWifi(state_);
|
||||||
|
|
||||||
// Feature namespaces extend the tables the core registrations just created,
|
// Feature Namespaces
|
||||||
// so they always follow them.
|
if (providers_.gui) {
|
||||||
|
bindings::registerScreen(state_);
|
||||||
|
bindings::registerTree(state_);
|
||||||
|
}
|
||||||
if (providers_.touch)
|
if (providers_.touch)
|
||||||
bindings::registerTouch(state_);
|
bindings::registerTouch(state_);
|
||||||
if (providers_.buttons)
|
if (providers_.buttons)
|
||||||
@@ -91,11 +95,9 @@ void Runtime::close() {
|
|||||||
cancelAllTimers();
|
cancelAllTimers();
|
||||||
lua_close(state_);
|
lua_close(state_);
|
||||||
state_ = nullptr;
|
state_ = nullptr;
|
||||||
// Node handles mean nothing to the next lua_State, so an app that inherited
|
mainRef_ = 0;
|
||||||
// the previous tree would build onto its nodes.
|
|
||||||
tree_.reset();
|
tree_.reset();
|
||||||
appPath_.clear();
|
appPath_.clear();
|
||||||
appTitle_.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) {
|
Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) {
|
||||||
@@ -103,18 +105,34 @@ Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Runtime::Batch::~Batch() {
|
Runtime::Batch::~Batch() {
|
||||||
if (--runtime_.batchDepth_ == 0)
|
if (--runtime_.batchDepth_ == 0 && runtime_.providers_.gui)
|
||||||
runtime_.providers_.gui->commit();
|
runtime_.providers_.gui->commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Runtime::beginCall(const char* name) {
|
bool Runtime::beginCall(const char* name) {
|
||||||
lua_getglobal(state_, name);
|
if (!mainRef_)
|
||||||
|
return false;
|
||||||
|
lua_rawgeti(state_, LUA_REGISTRYINDEX, mainRef_);
|
||||||
|
lua_getfield(state_, -1, name);
|
||||||
|
lua_remove(state_, -2);
|
||||||
if (lua_isfunction(state_, -1))
|
if (lua_isfunction(state_, -1))
|
||||||
return true;
|
return true;
|
||||||
lua_pop(state_, 1);
|
lua_pop(state_, 1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calls a chunk or handler that leaves one value on the stack; the caller owns
|
||||||
|
// it.
|
||||||
|
bool Runtime::finishCallValue(const char* name) {
|
||||||
|
if (lua_pcall(state_, 0, 1, 0) == LUA_OK)
|
||||||
|
return true;
|
||||||
|
const char* message = lua_tostring(state_, -1);
|
||||||
|
providers_.log->write(LogLevel::Error, std::string(name) + ": " +
|
||||||
|
(message ? message : "failed"));
|
||||||
|
lua_pop(state_, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool Runtime::finishCall(const char* name, int argc) {
|
bool Runtime::finishCall(const char* name, int argc) {
|
||||||
if (lua_pcall(state_, argc, 0, 0) == LUA_OK)
|
if (lua_pcall(state_, argc, 0, 0) == LUA_OK)
|
||||||
return true;
|
return true;
|
||||||
@@ -125,18 +143,16 @@ bool Runtime::finishCall(const char* name, int argc) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @lua-global core/runtime
|
// @lua-app App core/runtime
|
||||||
// @lua-preamble -- Runtime layout:
|
// @lua-preamble -- The firmware loads the path it was booted with into every
|
||||||
// @lua-preamble -- /.lua/apps/<AppId>/main.lua application entry
|
// fresh state and calls
|
||||||
// point
|
// @lua-preamble -- these on the table it returns. Where apps live, what
|
||||||
// @lua-preamble -- /.lua/apps/<AppId>/<Subapp>/main.lua nested route,
|
// surrounds them and which of
|
||||||
// omitted from the launcher
|
// @lua-preamble -- these an app itself sees are all that file's to decide,
|
||||||
// @lua-preamble -- /.lua/data/<AppId>/ persistent app
|
// which is why an app
|
||||||
// data, preserved across updates
|
// @lua-preamble -- composes the classes for the features it handles:
|
||||||
// @lua-preamble -- /.lua/lib/<module>.lua shared require()
|
// @lua-preamble --
|
||||||
// modules
|
// @lua-preamble -- ---@class PaintApp : App, TouchHandlers
|
||||||
// @lua-preamble -- require() also searches the running application's
|
|
||||||
// directory
|
|
||||||
// @lua-preamble --
|
// @lua-preamble --
|
||||||
// @lua-preamble -- The firmware does not clear the frame before calling draw(),
|
// @lua-preamble -- The firmware does not clear the frame before calling draw(),
|
||||||
// and commits changed
|
// and commits changed
|
||||||
@@ -145,24 +161,34 @@ bool Runtime::finishCall(const char* name, int argc) {
|
|||||||
// @lua-preamble -- Timer callbacks are registered directly with
|
// @lua-preamble -- Timer callbacks are registered directly with
|
||||||
// timer.after/every.
|
// timer.after/every.
|
||||||
|
|
||||||
// ---Required. Runs once before the first draw; failing here stops the app.
|
// ---Required. Mounts whatever the arguments describe; failing here leaves no
|
||||||
// @param arg string|nil The string passed to sys.launch or sys.replace.
|
// app running.
|
||||||
// @lua-fn init
|
// @param args table|nil The table passed to sys.startApp, carried across the
|
||||||
bool Runtime::callInit(const std::string& arg) {
|
// teardown as JSON.
|
||||||
|
// @lua-fn start
|
||||||
|
bool Runtime::callStart(const std::string& argsJson) {
|
||||||
const Batch batch(*this);
|
const Batch batch(*this);
|
||||||
if (!beginCall("init")) {
|
if (!beginCall("start")) {
|
||||||
providers_.log->write(LogLevel::Error, "init: the app defines none");
|
providers_.log->write(LogLevel::Error,
|
||||||
|
"start: the entry file defines none");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
lua_pushlstring(state_, arg.data(), arg.size());
|
if (argsJson.empty()) {
|
||||||
return finishCall("init", 1);
|
lua_pushnil(state_);
|
||||||
|
} else if (!decodeJson(state_, argsJson)) {
|
||||||
|
providers_.log->write(LogLevel::Error,
|
||||||
|
"start: cannot decode arguments: " + argsJson);
|
||||||
|
lua_pop(state_, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return finishCall("start", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---Optional frame loop, called once after init and then at most 30 FPS, best
|
// ---Optional frame loop, called once after start and then at most 30 FPS, best
|
||||||
// effort.
|
// effort.
|
||||||
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero
|
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero
|
||||||
// on the first.
|
// on the first.
|
||||||
// @lua-fn draw
|
// @lua-fn draw?
|
||||||
void Runtime::callDraw(int32_t deltaMs) {
|
void Runtime::callDraw(int32_t deltaMs) {
|
||||||
const Batch batch(*this);
|
const Batch batch(*this);
|
||||||
if (!beginCall("draw"))
|
if (!beginCall("draw"))
|
||||||
@@ -180,16 +206,16 @@ void Runtime::callTouch(TouchPhase phase, int32_t x, int32_t y) {
|
|||||||
const Batch batch(*this);
|
const Batch batch(*this);
|
||||||
const char* name =
|
const char* name =
|
||||||
phase == TouchPhase::Down
|
phase == TouchPhase::Down
|
||||||
? "on_touch_down"
|
? "onTouchDown"
|
||||||
: (phase == TouchPhase::Move ? "on_touch_move" : "on_touch_up");
|
: (phase == TouchPhase::Move ? "onTouchMove" : "onTouchUp");
|
||||||
for (int pass = 0; pass < 2; pass++) {
|
for (int pass = 0; pass < 2; pass++) {
|
||||||
// The tap alias is ordering, not policy: a release always fires on_touch_up
|
// The tap alias is ordering, not policy: a release always fires onTouchUp
|
||||||
// and then on_touch, so both firmwares agree without either of them
|
// and then onTouch, so both firmwares agree without either of them
|
||||||
// deciding anything.
|
// deciding anything.
|
||||||
if (pass == 1) {
|
if (pass == 1) {
|
||||||
if (phase != TouchPhase::Up)
|
if (phase != TouchPhase::Up)
|
||||||
return;
|
return;
|
||||||
name = "on_touch";
|
name = "onTouch";
|
||||||
}
|
}
|
||||||
if (!beginCall(name))
|
if (!beginCall(name))
|
||||||
continue;
|
continue;
|
||||||
@@ -206,12 +232,12 @@ void Runtime::callButton(const std::string& button, bool pressed) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const Batch batch(*this);
|
const Batch batch(*this);
|
||||||
const char* name = pressed ? "on_button_down" : "on_button_up";
|
const char* name = pressed ? "onButtonDown" : "onButtonUp";
|
||||||
for (int pass = 0; pass < 2; pass++) {
|
for (int pass = 0; pass < 2; pass++) {
|
||||||
if (pass == 1) {
|
if (pass == 1) {
|
||||||
if (pressed)
|
if (pressed)
|
||||||
return;
|
return;
|
||||||
name = "on_button";
|
name = "onButton";
|
||||||
}
|
}
|
||||||
if (!beginCall(name))
|
if (!beginCall(name))
|
||||||
continue;
|
continue;
|
||||||
@@ -275,14 +301,9 @@ void Runtime::cancelAllTimers() {
|
|||||||
timers_.clear();
|
timers_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Runtime::appId() const {
|
|
||||||
const size_t slash = appPath_.find('/');
|
|
||||||
return slash == std::string::npos ? appPath_ : appPath_.substr(0, slash);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string Runtime::appDataPath() const { return paths_.data + "/" + appId(); }
|
|
||||||
|
|
||||||
bool Runtime::hasFeature(const std::string& feature) const {
|
bool Runtime::hasFeature(const std::string& feature) const {
|
||||||
|
if (feature == "screen")
|
||||||
|
return providers_.gui != nullptr;
|
||||||
if (feature == "touch")
|
if (feature == "touch")
|
||||||
return providers_.touch != nullptr;
|
return providers_.touch != nullptr;
|
||||||
if (feature == "buttons")
|
if (feature == "buttons")
|
||||||
@@ -290,8 +311,8 @@ bool Runtime::hasFeature(const std::string& feature) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Runtime::startApp(const std::string& path, const std::string& arg) {
|
bool Runtime::startApp(const std::string& path, const std::string& argsJson) {
|
||||||
if (!isSafeRoute(path)) {
|
if (!isSafePath(path)) {
|
||||||
providers_.log->write(LogLevel::Error, "refusing to start '" + path + "'");
|
providers_.log->write(LogLevel::Error, "refusing to start '" + path + "'");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -300,61 +321,48 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) {
|
|||||||
if (!open())
|
if (!open())
|
||||||
return false;
|
return false;
|
||||||
appPath_ = path;
|
appPath_ = path;
|
||||||
appTitle_ = appId();
|
|
||||||
|
|
||||||
const std::string directory = paths_.apps + "/" + path;
|
installLoader();
|
||||||
installLoader(directory);
|
// The entry file runs first and start() mounts whatever it decides to, so an
|
||||||
if (!loadScript(directory + "/main.lua")) {
|
// app that fails either way leaves nothing behind.
|
||||||
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1)
|
if (!loadMain(path) || !callStart(argsJson)) {
|
||||||
? lua_tostring(state_, -1)
|
|
||||||
: "load failed");
|
|
||||||
close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// The chunk body runs first, then init(), so an app that fails either way
|
|
||||||
// leaves nothing behind.
|
|
||||||
if (!finishCall("main.lua", 0) || !callInit(arg)) {
|
|
||||||
close();
|
close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Runtime::requestLaunch(const std::string& path, const std::string& arg,
|
bool Runtime::loadMain(const std::string& path) {
|
||||||
bool replace) {
|
if (!loadScript(path)) {
|
||||||
pending_.kind = replace ? Pending::Replace : Pending::Launch;
|
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1)
|
||||||
pending_.route.path = path;
|
? lua_tostring(state_, -1)
|
||||||
pending_.route.arg = arg;
|
: "cannot load " + path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!finishCallValue(path.c_str()))
|
||||||
|
return false;
|
||||||
|
if (!lua_istable(state_, -1)) {
|
||||||
|
providers_.log->write(LogLevel::Error, path + " returned no table");
|
||||||
|
lua_pop(state_, 1);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mainRef_ = luaL_ref(state_, LUA_REGISTRYINDEX);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Runtime::requestBack() {
|
void Runtime::requestStart(const std::string& path,
|
||||||
pending_.kind = Pending::Back;
|
const std::string& argsJson) {
|
||||||
pending_.route = Route();
|
pending_.pending = true;
|
||||||
|
pending_.path = path;
|
||||||
|
pending_.argsJson = argsJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Runtime::applyPendingNavigation() {
|
bool Runtime::applyPendingNavigation() {
|
||||||
const Pending pending = pending_;
|
const Pending pending = pending_;
|
||||||
pending_ = Pending();
|
pending_ = Pending();
|
||||||
if (pending.kind == Pending::None)
|
if (!pending.pending)
|
||||||
return hasApp();
|
return hasApp();
|
||||||
|
return startApp(pending.path, pending.argsJson);
|
||||||
if (pending.kind == Pending::Back) {
|
|
||||||
// An empty history means the launcher, which is an app like any other.
|
|
||||||
Route target;
|
|
||||||
target.path = paths_.home;
|
|
||||||
if (!history_.empty()) {
|
|
||||||
target = history_.back();
|
|
||||||
history_.pop_back();
|
|
||||||
}
|
|
||||||
return startApp(target.path, target.arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pending.kind == Pending::Launch && hasApp()) {
|
|
||||||
Route current;
|
|
||||||
current.path = appPath_;
|
|
||||||
history_.push_back(current);
|
|
||||||
}
|
|
||||||
return startApp(pending.route.path, pending.route.arg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Runtime* Runtime::from(lua_State* state) {
|
Runtime* Runtime::from(lua_State* state) {
|
||||||
|
|||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2010-2012 Mark Pulford <mark@kyne.com.au>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
Vendored
+211
@@ -0,0 +1,211 @@
|
|||||||
|
/* fpconv - Floating point conversion routines
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011-2012 Mark Pulford <mark@kyne.com.au>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* JSON uses a '.' decimal separator. strtod() / sprintf() under C libraries
|
||||||
|
* with locale support will break when the decimal separator is a comma.
|
||||||
|
*
|
||||||
|
* fpconv_* will around these issues with a translation buffer if required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "fpconv.h"
|
||||||
|
|
||||||
|
/* Workaround for MSVC */
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#define inline __inline
|
||||||
|
#define snprintf sprintf_s
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Lua CJSON assumes the locale is the same for all threads within a
|
||||||
|
* process and doesn't change after initialisation.
|
||||||
|
*
|
||||||
|
* This avoids the need for per thread storage or expensive checks
|
||||||
|
* for call. */
|
||||||
|
static char locale_decimal_point = '.';
|
||||||
|
|
||||||
|
/* In theory multibyte decimal_points are possible, but
|
||||||
|
* Lua CJSON only supports UTF-8 and known locales only have
|
||||||
|
* single byte decimal points ([.,]).
|
||||||
|
*
|
||||||
|
* localconv() may not be thread safe (=>crash), and nl_langinfo() is
|
||||||
|
* not supported on some platforms. Use sprintf() instead - if the
|
||||||
|
* locale does change, at least Lua CJSON won't crash. */
|
||||||
|
static void fpconv_update_locale(void)
|
||||||
|
{
|
||||||
|
char buf[8];
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%g", 0.5);
|
||||||
|
|
||||||
|
/* Failing this test might imply the platform has a buggy dtoa
|
||||||
|
* implementation or wide characters */
|
||||||
|
if (buf[0] != '0' || buf[2] != '5' || buf[3] != 0) {
|
||||||
|
fprintf(stderr, "Error: wide characters found or printf() bug.");
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
locale_decimal_point = buf[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Check for a valid number character: [-+0-9a-yA-Y.]
|
||||||
|
* Eg: -0.6e+5, infinity, 0xF0.F0pF0
|
||||||
|
*
|
||||||
|
* Used to find the probable end of a number. It doesn't matter if
|
||||||
|
* invalid characters are counted - strtod() will find the valid
|
||||||
|
* number if it exists. The risk is that slightly more memory might
|
||||||
|
* be allocated before a parse error occurs. */
|
||||||
|
static inline int valid_number_character(char ch)
|
||||||
|
{
|
||||||
|
char lower_ch;
|
||||||
|
|
||||||
|
if ('0' <= ch && ch <= '9')
|
||||||
|
return 1;
|
||||||
|
if (ch == '-' || ch == '+' || ch == '.')
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
/* Hex digits, exponent (e), base (p), "infinity",.. */
|
||||||
|
lower_ch = ch | 0x20;
|
||||||
|
if ('a' <= lower_ch && lower_ch <= 'y')
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calculate the size of the buffer required for a strtod locale
|
||||||
|
* conversion. */
|
||||||
|
static int strtod_buffer_size(const char *s)
|
||||||
|
{
|
||||||
|
const char *p = s;
|
||||||
|
|
||||||
|
while (valid_number_character(*p))
|
||||||
|
p++;
|
||||||
|
|
||||||
|
return p - s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Similar to strtod(), but must be passed the current locale's decimal point
|
||||||
|
* character. Guaranteed to be called at the start of any valid number in a string */
|
||||||
|
double fpconv_strtod(const char *nptr, char **endptr)
|
||||||
|
{
|
||||||
|
char localbuf[FPCONV_G_FMT_BUFSIZE];
|
||||||
|
char *buf, *endbuf, *dp;
|
||||||
|
int buflen;
|
||||||
|
double value;
|
||||||
|
|
||||||
|
/* System strtod() is fine when decimal point is '.' */
|
||||||
|
if (locale_decimal_point == '.')
|
||||||
|
return strtod(nptr, endptr);
|
||||||
|
|
||||||
|
buflen = strtod_buffer_size(nptr);
|
||||||
|
if (!buflen) {
|
||||||
|
/* No valid characters found, standard strtod() return */
|
||||||
|
*endptr = (char *)nptr;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Duplicate number into buffer */
|
||||||
|
if (buflen >= FPCONV_G_FMT_BUFSIZE) {
|
||||||
|
/* Handle unusually large numbers */
|
||||||
|
buf = (char *)malloc(buflen + 1);
|
||||||
|
if (!buf) {
|
||||||
|
fprintf(stderr, "Out of memory");
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* This is the common case.. */
|
||||||
|
buf = localbuf;
|
||||||
|
}
|
||||||
|
memcpy(buf, nptr, buflen);
|
||||||
|
buf[buflen] = 0;
|
||||||
|
|
||||||
|
/* Update decimal point character if found */
|
||||||
|
dp = strchr(buf, '.');
|
||||||
|
if (dp)
|
||||||
|
*dp = locale_decimal_point;
|
||||||
|
|
||||||
|
value = strtod(buf, &endbuf);
|
||||||
|
*endptr = (char *)&nptr[endbuf - buf];
|
||||||
|
if (buflen >= FPCONV_G_FMT_BUFSIZE)
|
||||||
|
free(buf);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "fmt" must point to a buffer of at least 6 characters */
|
||||||
|
static void set_number_format(char *fmt, int precision)
|
||||||
|
{
|
||||||
|
int d1, d2, i;
|
||||||
|
|
||||||
|
assert(1 <= precision && precision <= 16);
|
||||||
|
|
||||||
|
/* Create printf format (%.14g) from precision */
|
||||||
|
d1 = precision / 10;
|
||||||
|
d2 = precision % 10;
|
||||||
|
fmt[0] = '%';
|
||||||
|
fmt[1] = '.';
|
||||||
|
i = 2;
|
||||||
|
if (d1) {
|
||||||
|
fmt[i++] = '0' + d1;
|
||||||
|
}
|
||||||
|
fmt[i++] = '0' + d2;
|
||||||
|
fmt[i++] = 'g';
|
||||||
|
fmt[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assumes there is always at least 32 characters available in the target buffer */
|
||||||
|
int fpconv_g_fmt(char *str, double num, int precision)
|
||||||
|
{
|
||||||
|
char buf[FPCONV_G_FMT_BUFSIZE];
|
||||||
|
char fmt[6];
|
||||||
|
int len;
|
||||||
|
char *b;
|
||||||
|
|
||||||
|
set_number_format(fmt, precision);
|
||||||
|
|
||||||
|
/* Pass through when decimal point character is dot. */
|
||||||
|
if (locale_decimal_point == '.')
|
||||||
|
return snprintf(str, FPCONV_G_FMT_BUFSIZE, fmt, num);
|
||||||
|
|
||||||
|
/* snprintf() to a buffer then translate for other decimal point characters */
|
||||||
|
len = snprintf(buf, FPCONV_G_FMT_BUFSIZE, fmt, num);
|
||||||
|
|
||||||
|
/* Copy into target location. Translate decimal point if required */
|
||||||
|
b = buf;
|
||||||
|
do {
|
||||||
|
*str++ = (*b == locale_decimal_point ? '.' : *b);
|
||||||
|
} while(*b++);
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fpconv_init(void)
|
||||||
|
{
|
||||||
|
fpconv_update_locale();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vi:ai et sw=4 ts=4:
|
||||||
|
*/
|
||||||
Vendored
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/* Lua CJSON floating point conversion routines */
|
||||||
|
|
||||||
|
/* Buffer required to store the largest string representation of a double.
|
||||||
|
*
|
||||||
|
* Longest double printed with %.14g is 21 characters long:
|
||||||
|
* -1.7976931348623e+308 */
|
||||||
|
# define FPCONV_G_FMT_BUFSIZE 32
|
||||||
|
|
||||||
|
#ifdef USE_INTERNAL_FPCONV
|
||||||
|
#ifdef MULTIPLE_THREADS
|
||||||
|
#include "dtoa_config.h"
|
||||||
|
#include <unistd.h>
|
||||||
|
static inline void fpconv_init()
|
||||||
|
{
|
||||||
|
// Add one to try and avoid core id multiplier alignment
|
||||||
|
set_max_dtoa_threads((sysconf(_SC_NPROCESSORS_CONF) + 1) * 3);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static inline void fpconv_init()
|
||||||
|
{
|
||||||
|
/* Do nothing - not required */
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
extern void fpconv_init(void);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern int fpconv_g_fmt(char*, double, int);
|
||||||
|
extern double fpconv_strtod(const char*, char**);
|
||||||
|
|
||||||
|
/* vi:ai et sw=4 ts=4:
|
||||||
|
*/
|
||||||
Vendored
+1827
File diff suppressed because it is too large
Load Diff
Vendored
+199
@@ -0,0 +1,199 @@
|
|||||||
|
/* strbuf - String buffer routines
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010-2012 Mark Pulford <mark@kyne.com.au>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "strbuf.h"
|
||||||
|
|
||||||
|
static void die(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list arg;
|
||||||
|
|
||||||
|
va_start(arg, fmt);
|
||||||
|
vfprintf(stderr, fmt, arg);
|
||||||
|
va_end(arg);
|
||||||
|
fprintf(stderr, "\n");
|
||||||
|
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
void strbuf_init(strbuf_t *s, size_t len)
|
||||||
|
{
|
||||||
|
size_t size;
|
||||||
|
|
||||||
|
if (!len)
|
||||||
|
size = STRBUF_DEFAULT_SIZE;
|
||||||
|
else
|
||||||
|
size = len + 1;
|
||||||
|
if (size < len)
|
||||||
|
die("Overflow, len: %zu", len);
|
||||||
|
s->buf = NULL;
|
||||||
|
s->size = size;
|
||||||
|
s->length = 0;
|
||||||
|
s->dynamic = 0;
|
||||||
|
s->reallocs = 0;
|
||||||
|
s->debug = 0;
|
||||||
|
|
||||||
|
s->buf = (char *)malloc(size);
|
||||||
|
if (!s->buf)
|
||||||
|
die("Out of memory");
|
||||||
|
|
||||||
|
strbuf_ensure_null(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
strbuf_t *strbuf_new(size_t len)
|
||||||
|
{
|
||||||
|
strbuf_t *s;
|
||||||
|
|
||||||
|
s = (strbuf_t*)malloc(sizeof(strbuf_t));
|
||||||
|
if (!s)
|
||||||
|
die("Out of memory");
|
||||||
|
|
||||||
|
strbuf_init(s, len);
|
||||||
|
|
||||||
|
/* Dynamic strbuf allocation / deallocation */
|
||||||
|
s->dynamic = 1;
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void debug_stats(strbuf_t *s)
|
||||||
|
{
|
||||||
|
if (s->debug) {
|
||||||
|
fprintf(stderr, "strbuf(%p) reallocs: %d, length: %zd, size: %zd\n",
|
||||||
|
(void *) s, s->reallocs, s->length, s->size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If strbuf_t has not been dynamically allocated, strbuf_free() can
|
||||||
|
* be called any number of times strbuf_init() */
|
||||||
|
void strbuf_free(strbuf_t *s)
|
||||||
|
{
|
||||||
|
debug_stats(s);
|
||||||
|
|
||||||
|
if (s->buf) {
|
||||||
|
free(s->buf);
|
||||||
|
s->buf = NULL;
|
||||||
|
}
|
||||||
|
if (s->dynamic)
|
||||||
|
free(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
char *strbuf_free_to_string(strbuf_t *s, size_t *len)
|
||||||
|
{
|
||||||
|
char *buf;
|
||||||
|
|
||||||
|
debug_stats(s);
|
||||||
|
|
||||||
|
strbuf_ensure_null(s);
|
||||||
|
|
||||||
|
buf = s->buf;
|
||||||
|
if (len)
|
||||||
|
*len = s->length;
|
||||||
|
|
||||||
|
if (s->dynamic)
|
||||||
|
free(s);
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t calculate_new_size(strbuf_t *s, size_t len)
|
||||||
|
{
|
||||||
|
size_t reqsize, newsize;
|
||||||
|
|
||||||
|
if (len <= 0)
|
||||||
|
die("BUG: Invalid strbuf length requested");
|
||||||
|
|
||||||
|
/* Ensure there is room for optional NULL termination */
|
||||||
|
reqsize = len + 1;
|
||||||
|
if (reqsize < len)
|
||||||
|
die("Overflow, len: %zu", len);
|
||||||
|
|
||||||
|
/* If the user has requested to shrink the buffer, do it exactly */
|
||||||
|
if (s->size > reqsize)
|
||||||
|
return reqsize;
|
||||||
|
|
||||||
|
newsize = s->size;
|
||||||
|
if (reqsize >= SIZE_MAX / 2) {
|
||||||
|
newsize = reqsize;
|
||||||
|
} else {
|
||||||
|
/* Exponential sizing */
|
||||||
|
while (newsize < reqsize)
|
||||||
|
newsize *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newsize < reqsize)
|
||||||
|
die("BUG: strbuf length would overflow, len: %zu", len);
|
||||||
|
|
||||||
|
|
||||||
|
return newsize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Ensure strbuf can handle a string length bytes long (ignoring NULL
|
||||||
|
* optional termination). */
|
||||||
|
void strbuf_resize(strbuf_t *s, size_t len)
|
||||||
|
{
|
||||||
|
size_t newsize;
|
||||||
|
|
||||||
|
newsize = calculate_new_size(s, len);
|
||||||
|
|
||||||
|
if (s->debug > 1) {
|
||||||
|
fprintf(stderr, "strbuf(%p) resize: %zd => %zd\n",
|
||||||
|
(void *) s, s->size, newsize);
|
||||||
|
}
|
||||||
|
|
||||||
|
s->size = newsize;
|
||||||
|
s->buf = (char *)realloc(s->buf, s->size);
|
||||||
|
if (!s->buf)
|
||||||
|
die("Out of memory, len: %zu", len);
|
||||||
|
s->reallocs++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void strbuf_append_string(strbuf_t *s, const char *str)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
size_t space;
|
||||||
|
|
||||||
|
space = strbuf_empty_length(s);
|
||||||
|
|
||||||
|
for (i = 0; str[i]; i++) {
|
||||||
|
if (space < 1) {
|
||||||
|
strbuf_resize(s, s->length + 1);
|
||||||
|
space = strbuf_empty_length(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
s->buf[s->length] = str[i];
|
||||||
|
s->length++;
|
||||||
|
space--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* vi:ai et sw=4 ts=4:
|
||||||
|
*/
|
||||||
Vendored
+157
@@ -0,0 +1,157 @@
|
|||||||
|
/* strbuf - String buffer routines
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010-2012 Mark Pulford <mark@kyne.com.au>
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
/* Workaround for MSVC */
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#define inline __inline
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Size: Total bytes allocated to *buf
|
||||||
|
* Length: String length, excluding optional NULL terminator.
|
||||||
|
* Dynamic: True if created via strbuf_new()
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *buf;
|
||||||
|
size_t size;
|
||||||
|
size_t length;
|
||||||
|
int dynamic;
|
||||||
|
int reallocs;
|
||||||
|
int debug;
|
||||||
|
} strbuf_t;
|
||||||
|
|
||||||
|
#ifndef STRBUF_DEFAULT_SIZE
|
||||||
|
#define STRBUF_DEFAULT_SIZE 1023
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Initialise */
|
||||||
|
extern strbuf_t *strbuf_new(size_t len);
|
||||||
|
extern void strbuf_init(strbuf_t *s, size_t len);
|
||||||
|
|
||||||
|
/* Release */
|
||||||
|
extern void strbuf_free(strbuf_t *s);
|
||||||
|
extern char *strbuf_free_to_string(strbuf_t *s, size_t *len);
|
||||||
|
|
||||||
|
/* Management */
|
||||||
|
extern void strbuf_resize(strbuf_t *s, size_t len);
|
||||||
|
static size_t strbuf_empty_length(strbuf_t *s);
|
||||||
|
static size_t strbuf_length(strbuf_t *s);
|
||||||
|
static char *strbuf_string(strbuf_t *s, size_t *len);
|
||||||
|
static void strbuf_ensure_empty_length(strbuf_t *s, size_t len);
|
||||||
|
static char *strbuf_empty_ptr(strbuf_t *s);
|
||||||
|
static void strbuf_extend_length(strbuf_t *s, size_t len);
|
||||||
|
static void strbuf_set_length(strbuf_t *s, int len);
|
||||||
|
|
||||||
|
/* Update */
|
||||||
|
static void strbuf_append_mem(strbuf_t *s, const char *c, size_t len);
|
||||||
|
extern void strbuf_append_string(strbuf_t *s, const char *str);
|
||||||
|
static void strbuf_append_char(strbuf_t *s, const char c);
|
||||||
|
static void strbuf_ensure_null(strbuf_t *s);
|
||||||
|
|
||||||
|
/* Reset string for before use */
|
||||||
|
static inline void strbuf_reset(strbuf_t *s)
|
||||||
|
{
|
||||||
|
s->length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int strbuf_allocated(strbuf_t *s)
|
||||||
|
{
|
||||||
|
return s->buf != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return bytes remaining in the string buffer
|
||||||
|
* Ensure there is space for a NULL terminator. */
|
||||||
|
static inline size_t strbuf_empty_length(strbuf_t *s)
|
||||||
|
{
|
||||||
|
return s->size - s->length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_ensure_empty_length(strbuf_t *s, size_t len)
|
||||||
|
{
|
||||||
|
if (len > strbuf_empty_length(s))
|
||||||
|
strbuf_resize(s, s->length + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline char *strbuf_empty_ptr(strbuf_t *s)
|
||||||
|
{
|
||||||
|
return s->buf + s->length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_set_length(strbuf_t *s, int len)
|
||||||
|
{
|
||||||
|
s->length = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_extend_length(strbuf_t *s, size_t len)
|
||||||
|
{
|
||||||
|
s->length += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline size_t strbuf_length(strbuf_t *s)
|
||||||
|
{
|
||||||
|
return s->length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_append_char(strbuf_t *s, const char c)
|
||||||
|
{
|
||||||
|
strbuf_ensure_empty_length(s, 1);
|
||||||
|
s->buf[s->length++] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_append_char_unsafe(strbuf_t *s, const char c)
|
||||||
|
{
|
||||||
|
s->buf[s->length++] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_append_mem(strbuf_t *s, const char *c, size_t len)
|
||||||
|
{
|
||||||
|
strbuf_ensure_empty_length(s, len);
|
||||||
|
memcpy(s->buf + s->length, c, len);
|
||||||
|
s->length += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_append_mem_unsafe(strbuf_t *s, const char *c, size_t len)
|
||||||
|
{
|
||||||
|
memcpy(s->buf + s->length, c, len);
|
||||||
|
s->length += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void strbuf_ensure_null(strbuf_t *s)
|
||||||
|
{
|
||||||
|
s->buf[s->length] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline char *strbuf_string(strbuf_t *s, size_t *len)
|
||||||
|
{
|
||||||
|
if (len)
|
||||||
|
*len = s->length;
|
||||||
|
|
||||||
|
return s->buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vi:ai et sw=4 ts=4:
|
||||||
|
*/
|
||||||
@@ -23,28 +23,19 @@ struct Log : LogProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Settings : SettingsProvider {
|
|
||||||
int32_t degrees = 0;
|
|
||||||
std::string tz = "UTC0";
|
|
||||||
int32_t rotation() const override { return degrees; }
|
|
||||||
Status setRotation(int32_t value) override {
|
|
||||||
degrees = value;
|
|
||||||
return Status::success();
|
|
||||||
}
|
|
||||||
std::string timezone() const override { return tz; }
|
|
||||||
Status setTimezone(const std::string& value) override {
|
|
||||||
tz = value;
|
|
||||||
return Status::success();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Sys : SysProvider {
|
struct Sys : SysProvider {
|
||||||
|
std::string tz = "UTC0";
|
||||||
int32_t millis() const override { return 1234; }
|
int32_t millis() const override { return 1234; }
|
||||||
MemoryInfo memory() const override {
|
MemoryInfo memory() const override {
|
||||||
const MemoryInfo info = {100, 200, 50};
|
const MemoryInfo info = {100, 200, 50};
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
bool isClockSynced() const override { return true; }
|
bool isClockSynced() const override { return true; }
|
||||||
|
std::string timezone() const override { return tz; }
|
||||||
|
Status setTimezone(const std::string& value) override {
|
||||||
|
tz = value;
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Fs : FsProvider {
|
struct Fs : FsProvider {
|
||||||
@@ -148,7 +139,7 @@ struct Fs : FsProvider {
|
|||||||
struct Gui : GuiProvider {
|
struct Gui : GuiProvider {
|
||||||
std::string trace;
|
std::string trace;
|
||||||
int32_t degrees = 0;
|
int32_t degrees = 0;
|
||||||
bool fullscreen = false;
|
std::string themeName = "light";
|
||||||
bool gradient = false;
|
bool gradient = false;
|
||||||
|
|
||||||
FontIds fonts() const override {
|
FontIds fonts() const override {
|
||||||
@@ -158,11 +149,24 @@ struct Gui : GuiProvider {
|
|||||||
int32_t width() const override { return 320; }
|
int32_t width() const override { return 320; }
|
||||||
int32_t height() const override { return 240; }
|
int32_t height() const override { return 240; }
|
||||||
int32_t rotation() const override { return degrees; }
|
int32_t rotation() const override { return degrees; }
|
||||||
void setRotation(int32_t value) override { degrees = value; }
|
Status setRotation(int32_t value) override {
|
||||||
|
degrees = value;
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
|
std::string theme() const override { return themeName; }
|
||||||
|
Status setTheme(const std::string& value) override {
|
||||||
|
themeName = value;
|
||||||
|
return Status::success();
|
||||||
|
}
|
||||||
int32_t color(int32_t r, int32_t g, int32_t b) const override {
|
int32_t color(int32_t r, int32_t g, int32_t b) const override {
|
||||||
return (r << 16) | (g << 8) | b;
|
return (r << 16) | (g << 8) | b;
|
||||||
}
|
}
|
||||||
void clear(int32_t) override { trace += "clear;"; }
|
void clear(int32_t) override { trace += "clear;"; }
|
||||||
|
void setClip(int32_t x, int32_t y, int32_t w, int32_t h) override {
|
||||||
|
trace += "clip(" + std::to_string(x) + "," + std::to_string(y) + "," +
|
||||||
|
std::to_string(w) + "," + std::to_string(h) + ");";
|
||||||
|
}
|
||||||
|
void clearClip() override { trace += "unclip;"; }
|
||||||
void fillRect(int32_t, int32_t, int32_t, int32_t, int32_t) override {
|
void fillRect(int32_t, int32_t, int32_t, int32_t, int32_t) override {
|
||||||
trace += "fillRect;";
|
trace += "fillRect;";
|
||||||
}
|
}
|
||||||
@@ -189,7 +193,6 @@ struct Gui : GuiProvider {
|
|||||||
trace += border ? ",border" : ",-";
|
trace += border ? ",border" : ",-";
|
||||||
trace += ");";
|
trace += ");";
|
||||||
}
|
}
|
||||||
void setFullscreen(bool on) override { fullscreen = on; }
|
|
||||||
// Stands in for an e-ink panel, where a second commit is a second visible
|
// Stands in for an e-ink panel, where a second commit is a second visible
|
||||||
// refresh.
|
// refresh.
|
||||||
void commit() override { commits++; }
|
void commit() override { commits++; }
|
||||||
@@ -273,9 +276,10 @@ struct Wifi : WifiProvider {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct Ble : BleProvider {
|
struct Ble : BleProvider {
|
||||||
int32_t duration = 0;
|
|
||||||
std::string value;
|
std::string value;
|
||||||
bool initialized = false;
|
bool initialized = false;
|
||||||
|
bool observing = false;
|
||||||
|
BleFilter filter;
|
||||||
|
|
||||||
Status init(const std::string*) override {
|
Status init(const std::string*) override {
|
||||||
initialized = true;
|
initialized = true;
|
||||||
@@ -283,12 +287,17 @@ struct Ble : BleProvider {
|
|||||||
}
|
}
|
||||||
void deinit() override { initialized = false; }
|
void deinit() override { initialized = false; }
|
||||||
bool isInitialized() const override { return initialized; }
|
bool isInitialized() const override { return initialized; }
|
||||||
Status scan(int32_t durationMs, std::vector<BleDevice>& devices) override {
|
Status observe(const BleFilter& next) override {
|
||||||
duration = durationMs;
|
filter = next;
|
||||||
const BleDevice device = {"tag", "aa:bb", -60};
|
observing = true;
|
||||||
devices.push_back(device);
|
|
||||||
return Status::success();
|
return Status::success();
|
||||||
}
|
}
|
||||||
|
void unobserve() override { observing = false; }
|
||||||
|
bool isObserving() const override { return observing; }
|
||||||
|
void observed(std::vector<BleObservation>& out) override {
|
||||||
|
const BleObservation device = {"aa:bb", "tag", -60, "payload", 1234};
|
||||||
|
out.push_back(device);
|
||||||
|
}
|
||||||
Status connect(const std::string&) override { return Status::success(); }
|
Status connect(const std::string&) override { return Status::success(); }
|
||||||
void disconnect() override {}
|
void disconnect() override {}
|
||||||
bool isConnected() const override { return true; }
|
bool isConnected() const override { return true; }
|
||||||
@@ -353,7 +362,6 @@ struct Buttons : ButtonsProvider {
|
|||||||
// Every provider a Runtime needs, so a test names only what it asserts on.
|
// Every provider a Runtime needs, so a test names only what it asserts on.
|
||||||
struct Bench {
|
struct Bench {
|
||||||
Log log;
|
Log log;
|
||||||
Settings settings;
|
|
||||||
Sys sys;
|
Sys sys;
|
||||||
Fs fs;
|
Fs fs;
|
||||||
Gui gui;
|
Gui gui;
|
||||||
@@ -367,7 +375,6 @@ struct Bench {
|
|||||||
Providers providers() {
|
Providers providers() {
|
||||||
Providers providers;
|
Providers providers;
|
||||||
providers.log = &log;
|
providers.log = &log;
|
||||||
providers.settings = &settings;
|
|
||||||
providers.sys = &sys;
|
providers.sys = &sys;
|
||||||
providers.fs = &fs;
|
providers.fs = &fs;
|
||||||
providers.gui = &gui;
|
providers.gui = &gui;
|
||||||
|
|||||||
+256
-124
@@ -37,13 +37,17 @@ int main() {
|
|||||||
assert(bench.log.level == esp32lua::LogLevel::Info);
|
assert(bench.log.level == esp32lua::LogLevel::Info);
|
||||||
assert(bench.log.message == "shared runtime");
|
assert(bench.log.message == "shared runtime");
|
||||||
|
|
||||||
run(state, "assert(settings.getRotation() == 0)\n"
|
run(state, "assert(sys.hasFeature('screen'))\n"
|
||||||
"assert(settings.setRotation(90))\n"
|
"assert(screen.getRotation() == 0)\n"
|
||||||
"assert(settings.getTimezone() == 'UTC0')\n"
|
"assert(screen.setRotation(90))\n"
|
||||||
"assert(settings.setTimezone('EST5EDT'))");
|
"assert(screen.getTheme() == 'light')\n"
|
||||||
assert(bench.settings.degrees == 90);
|
"assert(screen.setTheme('dark'))\n"
|
||||||
assert(bench.settings.tz == "EST5EDT");
|
"assert(sys.getTimezone() == 'UTC0')\n"
|
||||||
expectError(state, "settings.setRotation(45)");
|
"assert(sys.setTimezone('EST5EDT'))");
|
||||||
|
assert(bench.gui.degrees == 90);
|
||||||
|
assert(bench.gui.themeName == "dark");
|
||||||
|
assert(bench.sys.tz == "EST5EDT");
|
||||||
|
expectError(state, "screen.setRotation(45)");
|
||||||
|
|
||||||
run(state, "assert(sys.getAPIVersion() == 1)\n"
|
run(state, "assert(sys.getAPIVersion() == 1)\n"
|
||||||
"assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n"
|
"assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n"
|
||||||
@@ -66,17 +70,17 @@ int main() {
|
|||||||
assert(bench.fs.written.size() == 3);
|
assert(bench.fs.written.size() == 3);
|
||||||
expectError(state, "fs.readFile('/notes.txt', 999999)");
|
expectError(state, "fs.readFile('/notes.txt', 999999)");
|
||||||
|
|
||||||
run(state, "assert(gui.getWidth() == 320 and gui.getHeight() == 240)\n"
|
run(state, "assert(screen.getWidth() == 320 and screen.getHeight() == 240)\n"
|
||||||
"assert(gui.FONT_UI == 2 and gui.STYLE_BOLD == 1)\n"
|
"assert(screen.FONT_UI == 2 and screen.STYLE_BOLD == 1)\n"
|
||||||
"assert(gui.color(255, 0, 0) == 0xFF0000)\n"
|
"assert(screen.color(255, 0, 0) == 0xFF0000)\n"
|
||||||
"gui.setRotation(180)\n"
|
"assert(screen.setRotation(180))\n"
|
||||||
"gui.clear()\n"
|
"screen.clear()\n"
|
||||||
"gui.fillPolygon({1, 2, 3}, {4, 5, 6}, 0)\n"
|
"screen.fillPolygon({1, 2, 3}, {4, 5, 6}, 0)\n"
|
||||||
"gui.drawText(gui.FONT_UI, 0, 0, 'hi')");
|
"screen.drawText(screen.FONT_UI, 0, 0, 'hi')");
|
||||||
assert(bench.gui.degrees == 180);
|
assert(bench.gui.degrees == 180);
|
||||||
assert(bench.gui.trace == "clear;fillPolygon3;drawText(hi);");
|
assert(bench.gui.trace == "clear;fillPolygon3;drawText(hi);");
|
||||||
expectError(state, "gui.fillPolygon({1, 2}, {3}, 0)");
|
expectError(state, "screen.fillPolygon({1, 2}, {3}, 0)");
|
||||||
expectError(state, "gui.color(300, 0, 0)");
|
expectError(state, "screen.color(300, 0, 0)");
|
||||||
|
|
||||||
run(state, "local response = http.get('https://example.test', {maxBytes = "
|
run(state, "local response = http.get('https://example.test', {maxBytes = "
|
||||||
"16, headers = {Accept = 'text/plain'}})\n"
|
"16, headers = {Accept = 'text/plain'}})\n"
|
||||||
@@ -89,6 +93,24 @@ int main() {
|
|||||||
bench.http.headers[0].name == "Accept");
|
bench.http.headers[0].name == "Accept");
|
||||||
expectError(state, "http.get('https://example.test', {maxBytes = 999999})");
|
expectError(state, "http.get('https://example.test', {maxBytes = 999999})");
|
||||||
|
|
||||||
|
// cjson is required rather than global, because the library provides it and
|
||||||
|
// no firmware implements it. Decoding is lua-cjson's; what is asserted here
|
||||||
|
// is the wiring and the depth limit the panel's C stack needs.
|
||||||
|
run(state,
|
||||||
|
"local cjson = require 'cjson'\n"
|
||||||
|
"assert(rawget(_G, 'cjson') == nil)\n"
|
||||||
|
"local value = "
|
||||||
|
"cjson.decode('{\"a\":[1,2.5,true],\"b\":\"x\\\\u00e9\"}')\n"
|
||||||
|
"assert(math.type(value.a[1]) == 'integer' and value.a[2] == 2.5)\n"
|
||||||
|
"assert(value.a[3] == true and value.b == 'x\\u{e9}')\n"
|
||||||
|
"assert(cjson.decode('null') == cjson.null)\n"
|
||||||
|
"assert(cjson.encode({1, 2, 3}) == '[1,2,3]')\n"
|
||||||
|
"assert(not pcall(cjson.decode, '{'))\n"
|
||||||
|
"assert(not pcall(cjson.encode, print))\n"
|
||||||
|
"local deep = {}; for _ = 1, 40 do deep = {deep} end\n"
|
||||||
|
"assert(not pcall(cjson.encode, deep))\n"
|
||||||
|
"assert(not pcall(cjson.decode, string.rep('[', 40)))");
|
||||||
|
|
||||||
run(state, "assert(wifi.scan()[1].ssid == 'home')\n"
|
run(state, "assert(wifi.scan()[1].ssid == 'home')\n"
|
||||||
"assert(wifi.isConnected())\n"
|
"assert(wifi.isConnected())\n"
|
||||||
"assert(wifi.getLocalIP() == '192.168.1.5')\n"
|
"assert(wifi.getLocalIP() == '192.168.1.5')\n"
|
||||||
@@ -99,10 +121,14 @@ int main() {
|
|||||||
run(state, "assert(not ble.isInitialized())\n"
|
run(state, "assert(not ble.isInitialized())\n"
|
||||||
"assert(ble.init())\n"
|
"assert(ble.init())\n"
|
||||||
"assert(ble.isInitialized())\n"
|
"assert(ble.isInitialized())\n"
|
||||||
"assert(ble.scan()[1].address == 'aa:bb')\n"
|
"assert(ble.observe({ services = { '181A' } }))\n"
|
||||||
|
"assert(ble.isObserving())\n"
|
||||||
|
"assert(ble.observed()[1].address == 'aa:bb')\n"
|
||||||
|
"assert(ble.observed()[1].payload == 'payload')\n"
|
||||||
"assert(#ble.read('svc', 'chr') == 3)\n"
|
"assert(#ble.read('svc', 'chr') == 3)\n"
|
||||||
"assert(ble.write('svc', 'chr', 'x\\0y'))");
|
"assert(ble.write('svc', 'chr', 'x\\0y'))");
|
||||||
assert(bench.ble.duration == 3000);
|
assert(bench.ble.filter.services.size() == 1);
|
||||||
|
assert(bench.ble.filter.services[0] == "181A");
|
||||||
assert(bench.ble.value.size() == 3);
|
assert(bench.ble.value.size() == 3);
|
||||||
|
|
||||||
run(state, "fired = 0\n"
|
run(state, "fired = 0\n"
|
||||||
@@ -120,92 +146,120 @@ int main() {
|
|||||||
expectError(state, "timer.after(0, function() end)");
|
expectError(state, "timer.after(0, function() end)");
|
||||||
|
|
||||||
run(state,
|
run(state,
|
||||||
"assert(settings.setCalibration(100, 200, 300, 400))\n"
|
"assert(touch.setCalibration(100, 200, 300, 400))\n"
|
||||||
"local x, y = input.getTouch()\n"
|
"local x, y = touch.getPoint()\n"
|
||||||
"assert(x == 10 and y == 20)\n"
|
"assert(x == 10 and y == 20)\n"
|
||||||
"assert(input.isTouched())\n"
|
"assert(touch.isTouched())\n"
|
||||||
"assert(input.isPressed('confirm') and not input.isPressed('back'))\n"
|
"assert(buttons.isPressed('confirm') and not buttons.isPressed('back'))\n"
|
||||||
"assert(#input.getButtons() == 4 and input.getButtons()[3] == "
|
"assert(#buttons.getAll() == 4 and buttons.getAll()[3] == "
|
||||||
"'confirm')");
|
"'confirm')");
|
||||||
assert(bench.touch.calibration[3] == 400);
|
assert(bench.touch.calibration[3] == 400);
|
||||||
|
|
||||||
// Chrome control and gradients are core: an e-ink provider flattens what it
|
// Gradients are core: an e-ink provider flattens what it cannot show.
|
||||||
// cannot show.
|
run(state, "screen.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
||||||
run(state, "gui.setFullscreen(true)\n"
|
"screen.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
|
||||||
"gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
assert(bench.gui.gradient);
|
||||||
"gui.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
|
|
||||||
assert(bench.gui.fullscreen && bench.gui.gradient);
|
|
||||||
assert(bench.gui.trace.find("roundRect(-,border);") != std::string::npos);
|
assert(bench.gui.trace.find("roundRect(-,border);") != std::string::npos);
|
||||||
|
|
||||||
bench.gui.trace.clear();
|
bench.gui.trace.clear();
|
||||||
run(state,
|
run(state,
|
||||||
"node.reset()\n"
|
"tree.reset()\n"
|
||||||
"local root = node.create(nil, {type = 'box', w = 'fill', h = 'fill', "
|
"local root = tree.create(nil, {type = 'box', w = 'fill', h = 'fill', "
|
||||||
"pad = 4, gap = 2})\n"
|
"pad = 4, gap = 2})\n"
|
||||||
"local label = node.create(root, {type = 'text', label = 'Hello'})\n"
|
"local label = tree.create(root, {type = 'text', label = 'Hello'})\n"
|
||||||
"local button = node.create(root, {type = 'button', h = 40, interactive "
|
"local button = tree.create(root, {type = 'button', h = 40, interactive "
|
||||||
"= true})\n"
|
"= true})\n"
|
||||||
"node.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, "
|
"tree.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, "
|
||||||
"face = 0xEEEEEE,\n"
|
"face = 0xEEEEEE,\n"
|
||||||
" border = 0x333333, focusColor = 0xFF0000})\n"
|
" border = 0x333333, focusColor = 0xFF0000})\n"
|
||||||
"assert(node.layout(root, 0, 0, 320, 240))\n"
|
"assert(tree.layout(root, 0, 0, 320, 240))\n"
|
||||||
"local x, y, w, h = node.getRect(label)\n"
|
"local x, y, w, h = tree.getRect(label)\n"
|
||||||
"assert(x == 4 and y == 4 and w == 312 and h == 16)\n"
|
"assert(x == 4 and y == 4 and w == 312 and h == 16)\n"
|
||||||
"assert(node.getLabel(label) == 'Hello')\n"
|
"assert(tree.getLabel(label) == 'Hello')\n"
|
||||||
"assert(node.hit(root, 10, 40) == button)\n"
|
"assert(tree.hit(root, 10, 40) == button)\n"
|
||||||
"assert(node.hit(root, 10, 200) == nil)\n"
|
// Hit is geometry: a point inside root but over no child answers root, not nil.
|
||||||
"assert(node.focusFirst(root) == button)\n"
|
"assert(tree.hit(root, 10, 200) == root)\n"
|
||||||
"assert(node.getFocus() == button)\n"
|
"assert(tree.focusFirst(root) == button)\n"
|
||||||
"assert(node.moveFocus(root, 'up') == button)\n"
|
"assert(tree.getFocus() == button)\n"
|
||||||
"node.setPressed(button, true)\n"
|
"assert(tree.moveFocus(root, 'up') == button)\n"
|
||||||
"assert(node.isPressed(button))\n"
|
"tree.setPressed(button, true)\n"
|
||||||
"assert(node.getCount() == 3 and node.getFootprint() > 0)\n"
|
"assert(tree.isPressed(button))\n"
|
||||||
"node.draw(root)\n"
|
"assert(tree.getCount() == 3 and tree.getFootprint() > 0)\n"
|
||||||
"node.dropScratch()");
|
"tree.draw(root)\n"
|
||||||
|
"tree.dropScratch()");
|
||||||
assert(bench.gui.trace.find("drawText(Hello);") != std::string::npos);
|
assert(bench.gui.trace.find("drawText(Hello);") != std::string::npos);
|
||||||
// The bordered box rounds its corners; the button fills without one.
|
// The bordered box rounds its corners; the button fills without one.
|
||||||
assert(bench.gui.trace.find("roundRect(fill,border);") != std::string::npos);
|
assert(bench.gui.trace.find("roundRect(fill,border);") != std::string::npos);
|
||||||
assert(bench.gui.trace.find("roundRect(fill,-);") != std::string::npos);
|
assert(bench.gui.trace.find("roundRect(fill,-);") != std::string::npos);
|
||||||
expectError(state, "node.create(nil, {type = 'nope'})");
|
expectError(state, "tree.create(nil, {type = 'nope'})");
|
||||||
|
|
||||||
run(state,
|
run(state,
|
||||||
"node.reset()\n"
|
"tree.reset()\n"
|
||||||
"local root = node.create(nil, {type = 'custom', w = 'fill', h = "
|
"local root = tree.create(nil, {type = 'custom', w = 'fill', h = "
|
||||||
"'fill'})\n"
|
"'fill'})\n"
|
||||||
"painted = 0\n"
|
"painted = 0\n"
|
||||||
"node.setPainter(function(id, x, y, w, h) painted = painted + w end)\n"
|
"tree.setPainter(function(id, x, y, w, h) painted = painted + w end)\n"
|
||||||
"assert(node.layout(root, 0, 0, 320, 240))\n"
|
"assert(tree.layout(root, 0, 0, 320, 240))\n"
|
||||||
"node.draw(root)\n"
|
"tree.draw(root)\n"
|
||||||
"assert(painted == 320)");
|
"assert(painted == 320)");
|
||||||
|
|
||||||
// Callbacks: only init failing stops an app, and a release fires the tap
|
// A scrolling box: children measure past its edges, panning moves them and is
|
||||||
// alias after the up.
|
// clamped to the content, and what is scrolled out of the box is neither hit
|
||||||
|
// nor painted outside it.
|
||||||
|
bench.gui.trace.clear();
|
||||||
run(state,
|
run(state,
|
||||||
"events = {}\n"
|
"tree.reset()\n"
|
||||||
"local function note(name) return function(a) events[#events + 1] = name "
|
"local list = tree.create(nil, {type = 'box', w = 'fill', h = 'fill',\n"
|
||||||
".. ':' .. tostring(a) end end\n"
|
" scrollX = true, scrollY = true})\n"
|
||||||
"function init(arg) events[#events + 1] = 'init:' .. tostring(arg) end\n"
|
"local rows = {}\n"
|
||||||
"function draw(delta) events[#events + 1] = 'draw:' .. delta end\n"
|
"for i = 1, 5 do rows[i] = tree.create(list, {type = 'box', w = 400, h = "
|
||||||
"on_touch_down = note('down')\n"
|
"100, interactive = true}) end\n"
|
||||||
"on_touch_up = note('up')\n"
|
"tree.setStyle(list, {background = 0xFFFFFF, fill = 0xFFFFFF, border = "
|
||||||
"on_touch = note('tap')\n"
|
"0x333333})\n"
|
||||||
"on_button_up = note('bup')\n"
|
// The box is the panel; the content is deliberately larger on both axes.
|
||||||
"on_button = note('btap')");
|
"assert(tree.layout(list, 0, 0, 320, 240))\n"
|
||||||
bench.gui.commits = 0;
|
"local maxX, maxY = tree.getScrollRange(list)\n"
|
||||||
assert(runtime.callInit("book.epub"));
|
"assert(maxX == 400 - 320, 'content is wider than the box')\n"
|
||||||
runtime.callDraw(33);
|
"assert(maxY == 5 * 100 - 240, 'content is taller than the box')\n"
|
||||||
runtime.callTouch(esp32lua::TouchPhase::Down, 5, 6);
|
// Panning shifts the children, and getRect keeps answering screen space.
|
||||||
runtime.callTouch(esp32lua::TouchPhase::Move, 5,
|
"tree.setScroll(list, 30, 150)\n"
|
||||||
7); // the app defines no on_touch_move
|
"local x, y = tree.getRect(rows[1])\n"
|
||||||
runtime.callTouch(esp32lua::TouchPhase::Up, 5, 8);
|
"assert(x == -30 and y == -150, 'row moved by the scroll')\n"
|
||||||
runtime.callButton("confirm", false);
|
"local sx, sy = tree.getScroll(list)\n"
|
||||||
run(state,
|
"assert(sx == 30 and sy == 150)\n"
|
||||||
"assert(table.concat(events, ' ') == "
|
// The box itself does not move, only what it holds.
|
||||||
"'init:book.epub draw:33 down:5 up:5 tap:5 bup:confirm btap:confirm')");
|
"local bx, by = tree.getRect(list)\n"
|
||||||
// One commit per visit to the app, so six calls and not seven: the release
|
"assert(bx == 0 and by == 0)\n"
|
||||||
// and its tap alias are one visible change, and the move nobody handled still
|
// Row 1 is scrolled above the box, so a tap at the top hits row 2.
|
||||||
// ends a batch.
|
"assert(tree.hit(list, 10, 10) == rows[2], 'scrolled-out row is not hit')\n"
|
||||||
assert(bench.gui.commits == 6);
|
// Hit is pure geometry: the deepest node covering the point, else the container. A
|
||||||
|
// scroll pan finds its box by bubbling up from here, not by a flag on hit.
|
||||||
|
"local bare = tree.create(nil, {type = 'box', w = 'fill', h = 'fill', scrollY = true})\n"
|
||||||
|
"local child = tree.create(bare, {type = 'box', w = 100, h = 900})\n"
|
||||||
|
"assert(tree.layout(bare, 0, 0, 320, 240))\n"
|
||||||
|
"assert(tree.hit(bare, 10, 10) == child, 'deepest node by geometry wins')\n"
|
||||||
|
"assert(tree.hit(bare, 200, 10) == bare, 'the container answers where no child covers it')\n"
|
||||||
|
"tree.dropScratch()\n"
|
||||||
|
// Clamped, never past the content's far edge or before its start.
|
||||||
|
"tree.setScroll(list, 9999, 9999)\n"
|
||||||
|
"local cx, cy = tree.getScroll(list)\n"
|
||||||
|
"assert(cx == maxX and cy == maxY, 'clamped to the content')\n"
|
||||||
|
"tree.setScroll(list, -50, -50)\n"
|
||||||
|
"local zx, zy = tree.getScroll(list)\n"
|
||||||
|
"assert(zx == 0 and zy == 0, 'clamped at the origin')\n"
|
||||||
|
"tree.draw(list)\n"
|
||||||
|
"tree.dropScratch()");
|
||||||
|
// The subtree is scissored to the box, so a row hanging past it cannot paint
|
||||||
|
// over whatever sits outside.
|
||||||
|
assert(bench.gui.trace.find("clip(0,0,320,240);") != std::string::npos);
|
||||||
|
assert(bench.gui.trace.find("unclip;") != std::string::npos);
|
||||||
|
|
||||||
|
// A scrolled axis has no size to hand down, so the box needs one of its own.
|
||||||
|
expectError(state, "tree.reset()\n"
|
||||||
|
"local l = tree.create(nil, {type = 'box', scrollY = "
|
||||||
|
"true})\n"
|
||||||
|
"tree.create(l, {type = 'box', w = 10, h = 10})\n"
|
||||||
|
"assert(tree.layout(l, 0, 0, 320, 240))");
|
||||||
|
|
||||||
// A timer firing inside draw is still one batch.
|
// A timer firing inside draw is still one batch.
|
||||||
run(state, "function draw() timer.after(1, function() end) end\n"
|
run(state, "function draw() timer.after(1, function() end) end\n"
|
||||||
@@ -214,12 +268,64 @@ int main() {
|
|||||||
runtime.callTimer(bench.timer.scheduled.back());
|
runtime.callTimer(bench.timer.scheduled.back());
|
||||||
assert(bench.gui.commits == 1);
|
assert(bench.gui.commits == 1);
|
||||||
|
|
||||||
run(state, "function init() error('boom') end");
|
// Callbacks land on the table main.lua returns, only start() failing stops an
|
||||||
assert(!runtime.callInit(""));
|
// app, and a release fires the tap alias after the up.
|
||||||
assert(bench.log.message.find("init: ") == 0);
|
{
|
||||||
run(state, "function draw() error('kaboom') end");
|
fake::Bench chrome;
|
||||||
runtime.callDraw(1); // a failed frame logs and the app keeps running
|
chrome.fs.files["/.lua/main.lua"] =
|
||||||
assert(bench.log.message.find("draw: ") == 0);
|
"events = {}\n"
|
||||||
|
"local function note(name) return function(a) events[#events + 1] = "
|
||||||
|
"name .. ':' .. tostring(a) end end\n"
|
||||||
|
"return {\n"
|
||||||
|
" start = function(args) events[#events + 1] = 'start:' .. "
|
||||||
|
"args.app .. ':' .. args.arg end,\n"
|
||||||
|
" draw = function(delta) if delta < 0 then error('kaboom') end\n"
|
||||||
|
" events[#events + 1] = 'draw:' .. delta end,\n"
|
||||||
|
" onTouchDown = note('down'),\n"
|
||||||
|
" onTouchUp = note('up'),\n"
|
||||||
|
" onTouch = note('tap'),\n"
|
||||||
|
" onButtonUp = note('bup'),\n"
|
||||||
|
" onButton = note('btap'),\n"
|
||||||
|
"}\n";
|
||||||
|
esp32lua::Runtime hosted(chrome.providers());
|
||||||
|
assert(hosted.startApp(esp32lua::MAIN_PATH,
|
||||||
|
"{\"app\":\"Reader\",\"arg\":\"book.epub\"}"));
|
||||||
|
chrome.gui.commits = 0;
|
||||||
|
hosted.callDraw(33);
|
||||||
|
hosted.callTouch(esp32lua::TouchPhase::Down, 5, 6);
|
||||||
|
hosted.callTouch(esp32lua::TouchPhase::Move, 5,
|
||||||
|
7); // main.lua defines no onTouchMove
|
||||||
|
hosted.callTouch(esp32lua::TouchPhase::Up, 5, 8);
|
||||||
|
hosted.callButton("confirm", false);
|
||||||
|
run(hosted.state(),
|
||||||
|
"assert(table.concat(events, ' ') == "
|
||||||
|
"'start:Reader:book.epub draw:33 down:5 up:5 tap:5 bup:confirm "
|
||||||
|
"btap:confirm')");
|
||||||
|
// One commit per visit, so five calls and not six: the release and its tap
|
||||||
|
// alias are one visible change, and the move nobody handled still ends a
|
||||||
|
// batch.
|
||||||
|
assert(chrome.gui.commits == 5);
|
||||||
|
|
||||||
|
hosted.callDraw(-1); // a failed frame logs and the app keeps running
|
||||||
|
assert(chrome.log.message.find("draw: ") == 0);
|
||||||
|
assert(hosted.hasApp());
|
||||||
|
|
||||||
|
chrome.fs.files["/.lua/main.lua"] =
|
||||||
|
"return { start = function() error('boom') end }";
|
||||||
|
assert(!hosted.startApp(esp32lua::MAIN_PATH));
|
||||||
|
assert(chrome.log.message.find("start: ") == 0);
|
||||||
|
assert(!hosted.hasApp());
|
||||||
|
|
||||||
|
// Arguments that are not JSON leave no app running rather than a state with
|
||||||
|
// no start() behind it.
|
||||||
|
chrome.fs.files["/.lua/main.lua"] = "return { start = function() end }";
|
||||||
|
assert(!hosted.startApp(esp32lua::MAIN_PATH, "{not json"));
|
||||||
|
assert(!hosted.hasApp());
|
||||||
|
|
||||||
|
chrome.fs.files["/.lua/main.lua"] = "return 7";
|
||||||
|
assert(!hosted.startApp(esp32lua::MAIN_PATH));
|
||||||
|
assert(chrome.log.message == "/.lua/main.lua returned no table");
|
||||||
|
}
|
||||||
|
|
||||||
// A feature callback without its provider is a wiring bug, not a silent
|
// A feature callback without its provider is a wiring bug, not a silent
|
||||||
// no-op.
|
// no-op.
|
||||||
@@ -231,66 +337,92 @@ int main() {
|
|||||||
assert(noTouch.open());
|
assert(noTouch.open());
|
||||||
noTouch.callTouch(esp32lua::TouchPhase::Down, 1, 1);
|
noTouch.callTouch(esp32lua::TouchPhase::Down, 1, 1);
|
||||||
assert(headless.log.message == "callTouch without a touch provider");
|
assert(headless.log.message == "callTouch without a touch provider");
|
||||||
run(noTouch.state(),
|
run(noTouch.state(), "assert(touch == nil and buttons.isPressed ~= nil)");
|
||||||
"assert(input.getTouch == nil and input.isPressed ~= nil)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// App loading: a fresh state per app, require reaching the app directory and
|
// A screenless firmware still runs: no gui provider, no screen/tree
|
||||||
// /.lua/lib, and navigation applied between batches rather than inside a
|
// namespaces, but a callback batch still completes.
|
||||||
|
{
|
||||||
|
fake::Bench screenless;
|
||||||
|
esp32lua::Providers providers = screenless.providers();
|
||||||
|
providers.gui = nullptr;
|
||||||
|
esp32lua::Runtime noScreen(providers);
|
||||||
|
assert(noScreen.open());
|
||||||
|
assert(!noScreen.hasFeature("screen"));
|
||||||
|
run(noScreen.state(), "assert(not sys.hasFeature('screen'))\n"
|
||||||
|
"assert(screen == nil and tree == nil)\n"
|
||||||
|
"assert(sys.getTimezone() == 'UTC0')");
|
||||||
|
noScreen.callDraw(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// App loading: a fresh state per app, main.lua deciding where apps and
|
||||||
|
// modules live, and navigation applied between batches rather than inside a
|
||||||
// callback.
|
// callback.
|
||||||
{
|
{
|
||||||
fake::Bench host;
|
fake::Bench host;
|
||||||
|
// The tree is main.lua's, so the test states it the way a card would.
|
||||||
|
// Routing, history and the launcher are all this file's, built out of the
|
||||||
|
// arguments it is handed; the runtime knows only the path it loads.
|
||||||
|
host.fs.files["/.lua/main.lua"] =
|
||||||
|
"package.path = '/.lua/lib/?.lua'\n"
|
||||||
|
"return {\n"
|
||||||
|
" start = function(args)\n"
|
||||||
|
" args = args or {app = 'Home'}\n"
|
||||||
|
" route, history = args.app, args.history or {}\n"
|
||||||
|
" local dir = '/.lua/apps/' .. route\n"
|
||||||
|
" package.path = dir .. '/?.lua;/.lua/lib/?.lua'\n"
|
||||||
|
" app = assert(loadfile(dir .. '/main.lua'))()\n"
|
||||||
|
" app.init(args.arg)\n"
|
||||||
|
" end,\n"
|
||||||
|
"}\n";
|
||||||
host.fs.files["/.lua/lib/greet.lua"] =
|
host.fs.files["/.lua/lib/greet.lua"] =
|
||||||
"return {hello = function() return 'hi' end}";
|
"return {hello = function() return 'hi' end}";
|
||||||
host.fs.files["/.lua/apps/Home/main.lua"] =
|
host.fs.files["/.lua/apps/Home/main.lua"] =
|
||||||
"local greet = require('greet')\n"
|
"local greet = require('greet')\n"
|
||||||
"function init(arg) started = greet.hello() .. ':' .. tostring(arg) "
|
"return {init = function(arg) started = greet.hello() .. ':' .. "
|
||||||
"end";
|
"tostring(arg) end}";
|
||||||
host.fs.files["/.lua/apps/Reader/main.lua"] =
|
host.fs.files["/.lua/apps/Reader/main.lua"] =
|
||||||
"local page = require('page')\n"
|
"local page = require('page')\n"
|
||||||
"function init(arg) started = page.name .. ':' .. arg end";
|
"return {init = function(arg) started = page.name .. ':' .. arg end}";
|
||||||
host.fs.files["/.lua/apps/Reader/page.lua"] = "return {name = 'page'}";
|
host.fs.files["/.lua/apps/Reader/page.lua"] = "return {name = 'page'}";
|
||||||
host.fs.files["/.lua/apps/Reader/Notes/main.lua"] = "function init() end";
|
host.fs.files["/.lua/apps/Reader/Notes/main.lua"] =
|
||||||
|
"return {init = function() end}";
|
||||||
|
|
||||||
esp32lua::Runtime app(host.providers());
|
esp32lua::Runtime app(host.providers());
|
||||||
assert(app.startApp("Home"));
|
assert(app.startApp(esp32lua::MAIN_PATH));
|
||||||
assert(app.appId() == "Home" && app.appTitle() == "Home");
|
assert(app.appPath() == esp32lua::MAIN_PATH);
|
||||||
assert(app.appDataPath() == "/.lua/data/Home");
|
run(app.state(), "assert(started == 'hi:nil' and route == 'Home')");
|
||||||
run(app.state(), "assert(started == 'hi:')");
|
|
||||||
|
|
||||||
// A subapp shares the app ID, so both routes share one data directory.
|
// The arguments cross the teardown as JSON, so a nested table survives and
|
||||||
run(app.state(), "sys.launch('Reader', 'book.epub')");
|
// the history is whatever main.lua chose to put in it.
|
||||||
|
run(app.state(),
|
||||||
|
"sys.startApp('/.lua/main.lua', "
|
||||||
|
"{app = 'Reader', arg = 'book.epub', history = {'Home'}})");
|
||||||
assert(app.hasPendingNavigation());
|
assert(app.hasPendingNavigation());
|
||||||
run(app.state(), "assert(started == 'hi:')"); // the current app keeps
|
run(app.state(), "assert(started == 'hi:nil')"); // still running until
|
||||||
// running until applied
|
// applied
|
||||||
assert(app.applyPendingNavigation());
|
assert(app.applyPendingNavigation());
|
||||||
run(app.state(), "assert(started == 'page:book.epub')");
|
run(app.state(), "assert(started == 'page:book.epub')\n"
|
||||||
run(app.state(), "sys.launch('Reader/Notes')");
|
"assert(route == 'Reader' and history[1] == 'Home')");
|
||||||
|
|
||||||
|
// Going back is the same call with the stack main.lua kept, popped by
|
||||||
|
// main.lua: nothing in C++ remembers where the app came from.
|
||||||
|
run(app.state(), "sys.startApp('/.lua/main.lua', {app = history[1]})");
|
||||||
assert(app.applyPendingNavigation());
|
assert(app.applyPendingNavigation());
|
||||||
assert(app.appPath() == "Reader/Notes" && app.appId() == "Reader");
|
run(app.state(), "assert(route == 'Home' and #history == 0)");
|
||||||
assert(app.appDataPath() == "/.lua/data/Reader");
|
|
||||||
|
|
||||||
// Back unwinds history, then lands on the launcher.
|
// Arguments JSON cannot carry raise at the call, leaving the app running.
|
||||||
run(app.state(), "sys.back()");
|
expectError(app.state(), "sys.startApp('/.lua/main.lua', {f = print})");
|
||||||
assert(app.applyPendingNavigation() && app.appPath() == "Reader");
|
assert(!app.hasPendingNavigation());
|
||||||
run(app.state(), "sys.back()");
|
run(app.state(), "assert(route == 'Home')");
|
||||||
assert(app.applyPendingNavigation() && app.appPath() == "Home");
|
expectError(app.state(), "sys.startApp('/.lua/main.lua', 'not a table')");
|
||||||
run(app.state(), "sys.back()");
|
|
||||||
assert(app.applyPendingNavigation() && app.appPath() == "Home");
|
|
||||||
|
|
||||||
// sys.replace does not grow history, so back from it still reaches the
|
// A missing file, a broken app, and a traversal all leave nothing running.
|
||||||
// launcher.
|
assert(!app.startApp("/.lua/absent.lua"));
|
||||||
run(app.state(), "sys.replace('Reader', 'other.epub')");
|
|
||||||
assert(app.applyPendingNavigation() && app.appPath() == "Reader");
|
|
||||||
run(app.state(), "sys.back()");
|
|
||||||
assert(app.applyPendingNavigation() && app.appPath() == "Home");
|
|
||||||
|
|
||||||
// A missing app, a broken app, and a traversal all leave nothing running.
|
|
||||||
assert(!app.startApp("Absent"));
|
|
||||||
assert(!app.hasApp() && app.state() == nullptr);
|
assert(!app.hasApp() && app.state() == nullptr);
|
||||||
host.fs.files["/.lua/apps/Broken/main.lua"] =
|
host.fs.files["/.lua/apps/Broken/main.lua"] =
|
||||||
"function init() error('nope') end";
|
"return {init = function() error('nope') end}";
|
||||||
assert(!app.startApp("Broken"));
|
assert(!app.startApp(esp32lua::MAIN_PATH, "{\"app\":\"Broken\"}"));
|
||||||
assert(!app.hasApp());
|
assert(!app.hasApp());
|
||||||
assert(!app.startApp("../secrets"));
|
assert(!app.startApp("../secrets"));
|
||||||
assert(host.log.message.find("refusing to start") == 0);
|
assert(host.log.message.find("refusing to start") == 0);
|
||||||
|
|||||||
+8
-4
@@ -35,7 +35,9 @@ def main():
|
|||||||
lib_dir = pathlib.Path(lib_dir)
|
lib_dir = pathlib.Path(lib_dir)
|
||||||
out_path = pathlib.Path(out_path)
|
out_path = pathlib.Path(out_path)
|
||||||
|
|
||||||
sources = sorted(lib_dir.glob("*.lua"))
|
# Recurse so ui/keyboard.lua embeds as the module require("ui.keyboard") asks for: the
|
||||||
|
# module name is the path with dots, the C array identifier the same with underscores.
|
||||||
|
sources = sorted(lib_dir.rglob("*.lua"))
|
||||||
arrays = []
|
arrays = []
|
||||||
rows = []
|
rows = []
|
||||||
for src in sources:
|
for src in sources:
|
||||||
@@ -45,10 +47,12 @@ def main():
|
|||||||
subprocess.run([luac32, str(src), tmp_path], check=True)
|
subprocess.run([luac32, str(src), tmp_path], check=True)
|
||||||
bytecode = pathlib.Path(tmp_path).read_bytes()
|
bytecode = pathlib.Path(tmp_path).read_bytes()
|
||||||
pathlib.Path(tmp_path).unlink()
|
pathlib.Path(tmp_path).unlink()
|
||||||
name = src.stem
|
parts = src.relative_to(lib_dir).with_suffix("").parts
|
||||||
|
name = ".".join(parts)
|
||||||
|
ident = "_".join(parts)
|
||||||
comma = ", ".join(f"0x{b:02x}" for b in bytecode)
|
comma = ", ".join(f"0x{b:02x}" for b in bytecode)
|
||||||
arrays.append(f"static const unsigned char {name}[] = {{{comma}}};\n")
|
arrays.append(f"static const unsigned char {ident}[] = {{{comma}}};\n")
|
||||||
rows.append(f' {{"{name}", {name}, sizeof({name})}},')
|
rows.append(f' {{"{name}", {ident}, sizeof({ident})}},')
|
||||||
|
|
||||||
footer = "const EmbeddedModule embedded_modules[] = {{\n{rows}\n}};\nconst size_t embedded_modules_count = {count};\n"
|
footer = "const EmbeddedModule embedded_modules[] = {{\n{rows}\n}};\nconst size_t embedded_modules_count = {count};\n"
|
||||||
generated = HEADER + "".join(arrays) + footer.format(rows="\n".join(rows), count=len(sources)) + FOOTER
|
generated = HEADER + "".join(arrays) + footer.format(rows="\n".join(rows), count=len(sources)) + FOOTER
|
||||||
|
|||||||
+56
-15
@@ -7,13 +7,16 @@ table that registers them:
|
|||||||
// @lua-preamble ---@alias Feature "touch"|"lcd"
|
// @lua-preamble ---@alias Feature "touch"|"lcd"
|
||||||
// @lua-module sys SysLib creates the global
|
// @lua-module sys SysLib creates the global
|
||||||
// @lua-augment gui GuiLib extends a global another file created
|
// @lua-augment gui GuiLib extends a global another file created
|
||||||
// @lua-global core/runtime app callbacks, written to an explicit path
|
// @lua-app App a class of fields on the table an app returns
|
||||||
// @lua-const MAX_READ_BYTES integer 65536 Largest portable read.
|
// @lua-const MAX_READ_BYTES integer 65536 Largest portable read.
|
||||||
|
// @lua-field home? string a plain field of a @lua-app class
|
||||||
// @lua-postamble -- notes rendered after the module
|
// @lua-postamble -- notes rendered after the module
|
||||||
|
|
||||||
Per function, `// --- text`, `// @param name type desc`, and `// @return type desc`. Functions come
|
Per function, `// --- text`, `// @param name type desc`, and `// @return type desc`. Functions come
|
||||||
from the luaL_Reg entries below the directive, or from `// @lua-fn name` for the app callbacks the
|
from the luaL_Reg entries below the directive, or from `// @lua-fn name` for the callbacks the
|
||||||
runtime calls rather than registers.
|
runtime calls on an app's table rather than registers. A `@lua-fn` name ending in `?` is one the
|
||||||
|
app may leave out; the class it lands in is what an app composes, so a device without the feature
|
||||||
|
contributes no fields rather than the runtime hiding some.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -25,9 +28,10 @@ SOURCES = [ROOT / "native/src/bindings", ROOT / "native/src/runtime"]
|
|||||||
OUTPUT = ROOT / "lua/api"
|
OUTPUT = ROOT / "lua/api"
|
||||||
|
|
||||||
MODULE = re.compile(r"^// @lua-(?P<kind>module|augment) (?P<name>\w+) (?P<class_>\w+)$")
|
MODULE = re.compile(r"^// @lua-(?P<kind>module|augment) (?P<name>\w+) (?P<class_>\w+)$")
|
||||||
GLOBAL = re.compile(r"^// @lua-global(?: (?P<path>\S+))?$")
|
APP = re.compile(r"^// @lua-app (?P<class_>\w+)(?: (?P<path>\S+))?$")
|
||||||
FUNCTION = re.compile(r"^// @lua-fn (?P<name>\w+)$")
|
FUNCTION = re.compile(r"^// @lua-fn (?P<name>\w+\??)$")
|
||||||
CONST = re.compile(r"^// @lua-const (?P<name>\w+) (?P<type>\S+) (?P<value>\S+)(?: (?P<desc>.*))?$")
|
CONST = re.compile(r"^// @lua-const (?P<name>\w+) (?P<type>\S+) (?P<value>\S+)(?: (?P<desc>.*))?$")
|
||||||
|
FIELD = re.compile(r"^// @lua-field (?P<name>\w+\??) (?P<type>\S+)(?: (?P<desc>.*))?$")
|
||||||
FIX = re.compile(r"^// @lua-(?P<where>preamble|postamble) ?(?P<text>.*)$")
|
FIX = re.compile(r"^// @lua-(?P<where>preamble|postamble) ?(?P<text>.*)$")
|
||||||
TABLE = re.compile(r"^\s*const luaL_Reg (?P<var>\w+)\[\] = \{$")
|
TABLE = re.compile(r"^\s*const luaL_Reg (?P<var>\w+)\[\] = \{$")
|
||||||
ENTRY = re.compile(r'^\s*\{"(?P<name>\w+)",\s*\w+\},?$')
|
ENTRY = re.compile(r'^\s*\{"(?P<name>\w+)",\s*\w+\},?$')
|
||||||
@@ -53,6 +57,7 @@ def new_module(kind, name, class_name, path=None):
|
|||||||
"class": class_name,
|
"class": class_name,
|
||||||
"path": path,
|
"path": path,
|
||||||
"consts": [],
|
"consts": [],
|
||||||
|
"fields": [],
|
||||||
"preamble": [],
|
"preamble": [],
|
||||||
"postamble": [],
|
"postamble": [],
|
||||||
"functions": [],
|
"functions": [],
|
||||||
@@ -92,18 +97,19 @@ def parse(path):
|
|||||||
pending_module = new_module(module.group("kind"), module.group("name"), module.group("class_"))
|
pending_module = new_module(module.group("kind"), module.group("name"), module.group("class_"))
|
||||||
modules.append(pending_module)
|
modules.append(pending_module)
|
||||||
continue
|
continue
|
||||||
glob = GLOBAL.match(stripped)
|
app = APP.match(stripped)
|
||||||
if glob:
|
if app:
|
||||||
# Callbacks are globals an app defines, so they need no table and no class.
|
# Callbacks are fields of the table an app returns, so the block is a class
|
||||||
pending_module = new_module("global", None, None, glob.group("path"))
|
# with no table of its own to register.
|
||||||
|
pending_module = new_module("app", None, app.group("class_"), app.group("path"))
|
||||||
modules.append(pending_module)
|
modules.append(pending_module)
|
||||||
current, in_table = pending_module, False
|
current, in_table = pending_module, False
|
||||||
doc = reset()
|
doc = reset()
|
||||||
continue
|
continue
|
||||||
function = FUNCTION.match(stripped)
|
function = FUNCTION.match(stripped)
|
||||||
if function:
|
if function:
|
||||||
if not current or current["kind"] != "global":
|
if not current or current["kind"] != "app":
|
||||||
raise SystemExit(f"{where}: @lua-fn outside a @lua-global block")
|
raise SystemExit(f"{where}: @lua-fn outside a @lua-app block")
|
||||||
if not doc["doc"]:
|
if not doc["doc"]:
|
||||||
raise SystemExit(f"{where}: {function.group('name')} has no description")
|
raise SystemExit(f"{where}: {function.group('name')} has no description")
|
||||||
current["functions"].append((function.group("name"), doc))
|
current["functions"].append((function.group("name"), doc))
|
||||||
@@ -120,6 +126,15 @@ def parse(path):
|
|||||||
)
|
)
|
||||||
continuation = (target["consts"], len(target["consts"]) - 1, 3)
|
continuation = (target["consts"], len(target["consts"]) - 1, 3)
|
||||||
continue
|
continue
|
||||||
|
field = FIELD.match(stripped)
|
||||||
|
if field:
|
||||||
|
if not target or target["kind"] != "app":
|
||||||
|
raise SystemExit(f"{where}: @lua-field outside a @lua-app block")
|
||||||
|
target["fields"].append(
|
||||||
|
(field.group("name"), lua_type(field.group("type")), field.group("desc") or "")
|
||||||
|
)
|
||||||
|
continuation = (target["fields"], len(target["fields"]) - 1, 2)
|
||||||
|
continue
|
||||||
fix = FIX.match(stripped)
|
fix = FIX.match(stripped)
|
||||||
if fix:
|
if fix:
|
||||||
if not target:
|
if not target:
|
||||||
@@ -135,8 +150,8 @@ def parse(path):
|
|||||||
raise SystemExit(f"{where}: {table.group('var')} has no @lua-module or @lua-augment")
|
raise SystemExit(f"{where}: {table.group('var')} has no @lua-module or @lua-augment")
|
||||||
current, pending_module, in_table, doc = pending_module, None, True, reset()
|
current, pending_module, in_table, doc = pending_module, None, True, reset()
|
||||||
continue
|
continue
|
||||||
# A @lua-global block documents callbacks the runtime calls, so it has no table to sit in.
|
# A @lua-app block documents callbacks the runtime calls, so it has no table to sit in.
|
||||||
if not in_table and not (current and current["kind"] == "global"):
|
if not in_table and not (current and current["kind"] == "app"):
|
||||||
continue
|
continue
|
||||||
if stripped == "};":
|
if stripped == "};":
|
||||||
in_table = False
|
in_table = False
|
||||||
@@ -169,13 +184,28 @@ def parse(path):
|
|||||||
doc = reset()
|
doc = reset()
|
||||||
|
|
||||||
for module in modules:
|
for module in modules:
|
||||||
if module["kind"] == "global":
|
if module["kind"] == "app":
|
||||||
continue
|
continue
|
||||||
if not module["functions"]:
|
if not module["functions"]:
|
||||||
raise SystemExit(f"{path.relative_to(ROOT)}: {module['name']} registers no annotated functions")
|
raise SystemExit(f"{path.relative_to(ROOT)}: {module['name']} registers no annotated functions")
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
def signature(doc):
|
||||||
|
"""The `fun(...)` type of one callback, for the class an app composes."""
|
||||||
|
params = ", ".join(
|
||||||
|
f"{param}{'?' if type_.endswith('?') else ''}: {type_.rstrip('?')}" for param, type_, _ in doc["params"]
|
||||||
|
)
|
||||||
|
returns = ", ".join(type_ for type_, _ in doc["returns"])
|
||||||
|
return f"fun({params})" + (f": {returns}" if returns else "")
|
||||||
|
|
||||||
|
|
||||||
|
def describe(doc):
|
||||||
|
"""A field carries one line, so a wrapped description joins back into a sentence."""
|
||||||
|
text = " ".join(doc["doc"])
|
||||||
|
return f" {text}" if text else ""
|
||||||
|
|
||||||
|
|
||||||
def render(source, modules):
|
def render(source, modules):
|
||||||
lines = ["---@meta", "", f"-- Generated from {source.relative_to(ROOT)}. Do not edit.", ""]
|
lines = ["---@meta", "", f"-- Generated from {source.relative_to(ROOT)}. Do not edit.", ""]
|
||||||
for module in modules:
|
for module in modules:
|
||||||
@@ -184,7 +214,18 @@ def render(source, modules):
|
|||||||
lines.extend(module["preamble"])
|
lines.extend(module["preamble"])
|
||||||
if module["preamble"]:
|
if module["preamble"]:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if module["kind"] != "global":
|
if module["kind"] == "app":
|
||||||
|
lines.append(f"---@class {module['class']}")
|
||||||
|
for field, type_, desc in module["fields"]:
|
||||||
|
lines.append(f"---@field {field} {type_}{(' ' + desc) if desc else ''}")
|
||||||
|
for function, doc in module["functions"]:
|
||||||
|
lines.append(f"---@field {function} {signature(doc)}{describe(doc)}")
|
||||||
|
lines.append("")
|
||||||
|
if module["postamble"]:
|
||||||
|
lines.extend(module["postamble"] + [""])
|
||||||
|
continue
|
||||||
|
|
||||||
|
if module["kind"] != "app":
|
||||||
lines.append(f"---@class {module['class']}")
|
lines.append(f"---@class {module['class']}")
|
||||||
for const, type_, _, desc in module["consts"]:
|
for const, type_, _, desc in module["consts"]:
|
||||||
lines.append(f"---@field {const} {type_}{(' ' + desc) if desc else ''}")
|
lines.append(f"---@field {const} {type_}{(' ' + desc) if desc else ''}")
|
||||||
|
|||||||
+25
-1
@@ -3,7 +3,7 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from gen_api import ROOT, parse
|
from gen_api import ROOT, parse, render
|
||||||
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(dir=ROOT) as directory:
|
with tempfile.TemporaryDirectory(dir=ROOT) as directory:
|
||||||
@@ -30,4 +30,28 @@ doc = module["functions"][0][1]
|
|||||||
assert doc["doc"] == ["Reads one complete file."]
|
assert doc["doc"] == ["Reads one complete file."]
|
||||||
assert doc["params"][0][2] == "Absolute file path."
|
assert doc["params"][0][2] == "Absolute file path."
|
||||||
assert doc["returns"][0][1] == "File contents."
|
assert doc["returns"][0][1] == "File contents."
|
||||||
|
|
||||||
|
# Callbacks render as fields of a class an app composes, never as global functions.
|
||||||
|
with tempfile.TemporaryDirectory(dir=ROOT) as directory:
|
||||||
|
source = Path(directory) / "handlers.cpp"
|
||||||
|
source.write_text(
|
||||||
|
"""// @lua-app TouchHandlers
|
||||||
|
// @lua-field home? string The route back lands on.
|
||||||
|
// ---Fired when the finger lands.
|
||||||
|
// @param x integer
|
||||||
|
// @param y integer
|
||||||
|
// @lua-fn onTouchDown?
|
||||||
|
// ---Mounts the route.
|
||||||
|
// @param route string
|
||||||
|
// @param arg string|nil
|
||||||
|
// @lua-fn start
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
output = render(source, parse(source))
|
||||||
|
|
||||||
|
assert "---@class TouchHandlers" in output
|
||||||
|
assert "---@field home? string The route back lands on." in output
|
||||||
|
assert "---@field onTouchDown? fun(x: integer, y: integer) Fired when the finger lands." in output
|
||||||
|
assert "---@field start fun(route: string, arg?: string) Mounts the route." in output
|
||||||
|
assert "function " not in output
|
||||||
print("ok")
|
print("ok")
|
||||||
|
|||||||
Reference in New Issue
Block a user