Repository navigation
fbui v0.3.0
Added
-
crates.io publishing. All five crates are now publishable (
publish = falseis gone) and carry the metadata crates.io and docs.rs want: a root
README.md, keywords/categories,LICENSE-MIT/LICENSE-APACHEtexts, and
[package.metadata.docs.rs]feature sets. Sibling crates pin each other
exactly (=x.y.z, via[workspace.dependencies]), matching the lockstep
policy. Pushing av<version>tag runs.github/workflows/release.yml:
consistency check, tests,cargo package --workspace, an ordered
cargo publish --workspace(trusted publishing, or aCARGO_REGISTRY_TOKEN
for the first upload), and a GitHub release from this changelog.
scripts/release-check.shguards the pins/changelog/tag, and CI now
packages and verifies every crate on each push. SeeRELEASING.md. -
fbui-ctland the remote console's text endpoints — the third flow
executor, and the field-support loop.fbui-ctl(a binary infbui,--features remote,std::netonly):
tree,json,shot,tap/press/release/move/wheel,type,
key,trace [--follow],metrics, andrun <flow.txt>— which resolves
a flow's references against the live device viaGET /treeand injects
throughPOST /input.FBUI_CTLpicks the console;FBUI_REMOTE_TOKEN
authenticates.GET /tree.txtservesUi::inspect_text(), andGET /tracethe
last 500 trace lines — which the console records whenever it is enabled,
even withoutFBUI_TRACE.fbui::remote::parse_treereads the/treedocument back into an
InspectNode, pinned to the writer by a round-trip test.docs/tooling.mddocuments the whole track;PHASE-TOOLING.mdrecords
verified-vs-pending status and every deviation from the design;
CLAUDE.mdgains the no-screen workflow.- CI gains a
headlessjob: every committed flow, every example lint-checked,
and a monkey session on three examples — nosudo, nomodprobe.
-
Traces, diagnostics and lints — reading why, and catching what an eye
would catch.FBUI_TRACE=path(or-) writes one line per notable event on the
replay/wall clock:start,input(with the named widget under the
pointer),msg,mutate(ops and damage),frame,timer,proxy,
expect,lint. That is the causal chain input → message → mutation →
damage → frame, which makes "the button did nothing" a one-line
diagnosis: nomsgafter theinputmeans the callback is missing; a
msgwith nomutatemeansupdatematched the wrong arm.App::describe_message(defaultNone) gives the trace the app's own
vocabulary; a#[derive(Debug)]message type is a one-line impl.Ui::diagnostics()/take_diagnostics()— mutations, damage rects
and area, layouts, paints, messages, events. A few integer increments per
operation, always on, so a test can assert cost rather than only pixels.Ui::lint()reports what a tree dump never will:touch-target,
truncated-text,unreachable-focus,off-surface,overflow,
empty-scroll,stacked-modals,duplicate-name,undescribed, and
(from the script resolver)ambiguous-ref.Ui::allow_lintsuppresses
one rule on one widget andUi::set_touch_targetsets the tappable
minimum (default 24 logical px; a touch-only kiosk should set 44).
FBUI_LINT=1runs the pass after every layout and reports each finding
once;expect no-lintsmakes it a flow failure.- All ten shipped examples are lint-clean. Getting there fixed two real
bugs the pass found: theshowcase's panels row pushed its content off a
1024x600 screen, andcustom_widget'sDotdescribed nothing.
Changed
-
Container::shrink()— the flexboxmin-size: 0idiom, so a growing
container bounded by its parent wins over an oversized child. Without it a
windowing child (aListof 50 rows) demands its full content height and
pushes the page off the screen, which is what the newoverflowlint found
in theshowcaseexample. -
Flow scripts (
fbui-rec 2) and two executors — a UI interaction
written as steps and expectations, meaning the same thing in acargo test
and under the runner.fbui-rec 2 tap #inc type "milk" key Enter wait settle expect #count text "1 item"fbui_widgets::scriptholds everything that decides meaning: the
parser, the reference resolver (#name,#screen/field,Kind "text",
@x,y), and expectation evaluation. References resolve against the live
tree at the moment their step runs, so a flow follows the layout; a
reference that matches nothing — or matches a widget that is not on
screen — fails the step instead of tapping the void.fbui_widgets::harnessruns a flow in-process against aUi
(run_text,assert_flow), feeding widget events through the same path
the behavior tests use and calling the caller'supdatefor every
message. A failure panics with the failing line, the actual value, and
the tree.- The runner plays v2 flows wherever
FBUI_REPLAYtakes a recording;
the header picks the format. Input goes through the real gesture
recognizer, a flow exits 0 when every expectation held and non-zero when
one did not, and a failure writes<flow>.fail.txtand.fail.png
beside the flow.FBUI_REPLAY_STEP(default 50 ms) paces the steps.
Raw@mslines may be mixed in and keep their own timestamps. - Committed flows for three shipped examples under
fbui/flows/, which run
headless with no display, tty or privileges. Ui::name-based addressing means the examples no longer keep
Option<WidgetId>fields for their own labels.- Recordings are annotated: a live press is written with the named
widget under it (@120 b l p # tap #inc), so a recorded session can be
read — and hand-converted into a flow — without replaying it.
-
Widget names,
Widget::describe, and the tree as text — the widget
tree becomes readable without looking at the screen.Widget::describe(fbui-widgets) reports a widget's user-visible
content and state as ordered key/value pairs — a label's words, a text
field's content and caret,checked/open/selected, a slider's value
and range, a scroll offset. Every built-in widget implements it; the
default is empty, so third-party widgets keep compiling. It runs only
onUi::inspect, never on the paint or event path.- Names:
Ui::name,Ui::add_named,Ui::find,Ui::name_of,
Ui::names. A name is tree-unique, optional, and dies with its widget;
find("form/name")scopes a name by its named ancestors. Ui::inspect_text()renders the laid-out tree one widget per line —
Kind #name [x,y wxh] "text" prop=value focused— which is what an
author reads instead of a screenshot, and what a review diff shows when
the screen changes.FBUI_REPLAY_TREE=pathwrites that dump at the end of a replay,
besideFBUI_REPLAY_SHOT. An empty replay file is now a valid empty
recording, soFBUI_REPLAY=/dev/nullrenders the first screen, writes
the artifacts and exits.
Changed
-
InspectNode.nameis nowInspectNode.kind(the widget type), and
nameholds the app-assigned name fromUi::name(Option<String>). The
node also growstext,propsandvisible. The remote console's
GET /treeJSON follows:kindreplacesname, and nodes gain optional
name,text,propsand avisibleflag. The built-in console UI shows
all of them. Pre-1.0 rename, in one change, with the only in-repo consumer
updated. -
A headless display backend (
fbui-platform, featureheadless, on by
default) —FBUI_BACKEND=headlessruns the unmodified runner against two
RAM back buffers that present nowhere: no display device, no tty, no seat,
no input devices, no privileges. This is what lets CI and an author who
cannot see the screen drive a real app (FBUI_REPLAY,FBUI_MONKEY,
FBUI_REPLAY_SHOT, the remote console) and get artifacts back. It is a
backend rather than a test runner on purpose: the frame clock, gestures,
timers,Proxy, power policy and record/replay are the real ones, so a
headless result is evidence about the real app.FBUI_HEADLESS_SIZE=WxHsets the surface (default1024x600); a
malformed value is a hard error like the otherFBUI_*knobs.- Buffer ages follow the DRM double-buffered sequence exactly, so the
partial redraw path runs headless rather than theage = 0
repaint-everything path a single buffer would force. - Rows are padded to 64 bytes, so
stride != width * bppand any code that
recomputes the stride breaks loudly in CI instead of quietly on a device. - Presents complete synchronously, so the event loop needs no pacing timer
and an idle headless app still burns ~0% CPU. SIGUSR1(ordisplay::headless::request_mode) simulates a hotplug /
mode change, givingon_display_changedan off-device test for the first
time.NullSeat(fbui-platform) is the shared device-less seat behind the
terminal and headless backends.
-
Text editing:
TextArea, the clipboard, and a fullerTextInput—
closing the "clipboard-less, single-line" v1 scope from Phase 3.TextArea(fbui-widgets): a multi-line text box with word-wrapped
paragraphs, a caret that moves by visual line (Up/Downkeep the
goal column across shorter lines,Home/Endare per line,
Ctrl+Home/Enddocument-wide,PageUp/PageDownby viewport),Enter
inserting a line break, selection across lines (drawn per line with a
line-end tail), and a vertical scroll that follows the caret with a thumb
when content overflows — the wheel scrolls it and bubbles at its bounds
so an enclosingScrollViewstill moves.rows(n)fixes the height,
grow(f)fills a column.- The process clipboard —
Ui::clipboard()/Ui::set_clipboard(..)
andEventCtx::clipboard()/EventCtx::set_clipboard(..). There is no
system clipboard without a display server, so theUiowns the one the
process has; text widgets cut/copy/paste through it and an app bridges
it wherever it likes (the remote console, a serial link, a file). TextInputnow doesCtrl+Aselect-all,Ctrl+C/X/V(paste
flattens line breaks to spaces),Ctrl+Left/Rightword jumps,
Ctrl+Backspace/Deleteword deletion,Shift-extended moves that
collapse correctly, drag selection (pointer captured), a
long-press selects the word under it (the touch stand-in for
double-click), and a horizontal scroll that keeps the caret visible
in a value wider than the box. Caret placement and selection painting
now use the shaped layout's geometry rather than measuring substrings
(O(n) per click instead of O(n²)).Enteris left unhandled so a form
can act on it. Newselection()/select(..)/select_all()/
cursor()/scroll_offset()accessors. Both widgets share one
unit-tested editing core so the key table can't drift between them.TextLayoutgeometry (fbui-render):hit(x, y)→ byte offset,
caret(idx)→ caret box,selection_rects(a, b),line_height(),
line_count()— what any text-editing widget needs from a shaped
paragraph, pinned by a caret↔hit round-trip test on the bundled Inter
face across explicit and wrapped lines.- Ctrl chords reach widgets on every backend: the runner now falls back
to the character keysym when a key arrives with no printable text
(xkbcommon reports\x03for Ctrl+C, the terminal parser reports
nothing), soKey::Char('c')+mods.ctrlis what a field sees
everywhere. Behavior tests infbui-widgets/tests/text_editing.rs
drive both widgets through the real event path with the bundled font.
Seedocs/text-editing.md; theformexample gained a notes box.
-
Bootable kiosk ISO tooling (
scripts/iso/,docs/linux-iso.md) —
build-iso.shpackages a static-musl fbui app (default: theshowcase
example with the bundled font) into a 31 MB hybrid BIOS+UEFI ISO: GRUB, a
stock Ubuntu kernel from the build host's apt archive, and a ~3 MB busybox
initramfs whose/initloads a dep-ordered display/input module set and
execs the app — no distro, no init system, no display server. simpledrm
(built into the stock kernel) is the zero-module display baseline; bundled
virtio-gpu/bochs/virtio_input/USB-HID/PS-2 modules cover VMs and
common input hardware.test-iso.shboot-tests the ISO headlessly under
QEMU (BIOS and--uefiOVMF,--gpu std|virtio), waiting for fbui's
display line on serial and QMP-screendumping the rendered UI; verified
end-to-end including injected keyboard/pointer input. The assessment —
design trade-offs, alternatives, and what remains unverified (real
hardware, Secure Boot) — is indocs/linux-iso.md. -
Display rotation (
fbui-render+fbuirunner) —FBUI_ROTATE=90|180|270
turns the UI clockwise on the panel for portrait-mounted kiosk screens, in
software, identically on DRM/fbdev/terminal. The surface renders in UI
orientation (panel dims swapped) and each pixel is written to its rotated
position in the damage-bounded copy-out — destination rows still written
forward and sequentially per the write-combined-memory rule. Pointer/touch
input maps back through the inverse rotation; the remote console keeps UI
orientation for frames and injected clicks. Public pieces for custom
embedders:Rotation(surface/panel point, rect, and delta mappings),
Surface::set_rotation. Seedocs/rotation.md. -
VideoView(fbui-widgets) — damage-aware presentation for externally
produced frames (camera preview, decoded video): the producer thread
converts with the newfbui_render::yuvhelpers (yuyv_to_rgba,
nv12_to_rgba— BT.601 fixed point), builds anImagevia the new
Image::from_rgba_bytes, and ships it throughProxy+Ui::stream;
VideoView::push_framereports preciseStreamDamage::Repaint, so a
60 fps feed repaints only the video rect with no relayout while the rest
of the UI stays idle.Contain/Cover/Fillobject-fit (pure, tested
fit_rect), letterbox color, and a newPainter::draw_image_scaled
bilinear blit primitive. -
Navigator(fbui-widgets) — a retained screen stack with slide
transitions for multi-page kiosk flows. Screens are ordinary subtrees laid
out side by side (newWidget::position_childhook) and slid with a
device-snapped tweened content offset, so transitions ride the scroll-blit
fast path (equivalence with a full repaint pinned by a behavior test).
Covered screens keep their state but leave hit-testing and the Tab order
(newWidget::active_childrenhook); popped screens are reaped after the
exit slide settles (newWidget::take_child_removalshook). Push remembers
focus, pop restores it (newUi::focus/Ui::child_ids,
EventCtx::focus_widget), and a bubbling Escape pops with an optional
on_backmessage. -
Idle power management (
fbui-platform+fbuirunner) — dim the
backlight after a period with no input, power the panel off a while later,
wake (and swallow the waking gesture) on the next input. New
Display::set_power(DRM: connector DPMS property; fbdev:FBIOBLANK;
best-effort by contract), new sysfsBacklightcontrol, and a
PlatformHandler::take_power_requesthook so the handler can drive power
without owning the display. Apps opt in viaApp::idle_policy
(IdlePolicy: dim/blank timings, dim level,on_idle/on_wake
messages); operators override withFBUI_IDLE_DIM/FBUI_IDLE_BLANK/
FBUI_IDLE_DIM_LEVEL. The tracker is a pure, unit-tested state machine
riding the loop'snext_timeout, so idle management adds no ticking and a
blanked screen sleeps purely on fds. Seedocs/power-management.md. -
TreeView(fbui-widgets) — the hierarchical counterpart toList:
expand/collapse branches with disclosure triangles, per-depth indentation,
and single selection. Nodes are plain data (TreeNode::branch/leaf)
addressed by stable depth-firstNodeIds; the tree is flattened to its
visible rows and painted withList's windowing, drag/kinetic scrolling,
and scroll-blit fast path, so a huge collapsed tree costs what its visible
rows cost. Tap the triangle to toggle, the row body to select; with focus,
Up/Down walk visible rows, Right expands then descends, Left collapses then
ascends, and Enter/Space select (Space toggling branches).on_select/
on_togglemessage hooks,set_nodes/set_selected/set_expandedfor
Ui::withmutation. -
Calendar(fbui-widgets) — a month-grid date picker: header with
previous/next month arrows, weekday row, and a fixed 6×7 grid including the
adjacent months' muted edge days. Ships its own proleptic-GregorianDate
type (validated construction, leap years, day/month arithmetic with
end-of-month clamping — no external date crate) and, per the no-wall-clock
rule, never reads "today" from the system: the app passes it in
(Calendar::today) to get the marker ring. Tap a day to pick
(on_pick(Date)); with focus the arrows move by day/week across month
boundaries, PageUp/PageDown by month, Home/End inside it, Enter/Space
re-emit the pick. Monday or Sunday week start. -
anim::Spring(fbui-widgets) — spring-physics animation beside
Tween, for motion that must absorb interruption: it carries velocity, so
retargetmid-flight bends the trajectory smoothly instead of restarting
it, andset_velocityhands off a fling's release speed. Feel is described
byresponse(undamped period; default 0.35 s) anddamping_ratio(1.0 =
critical, below overshoots) rather than raw spring constants. Same
frame-clock contract asTween(advance(dt) -> bool, snaps exactly onto
the target on settle), integrated with substepped semi-implicit Euler so a
stalled frame can't destabilize it — deterministic and unit-tested
headless. -
Monkey testing (
fbui, featureplatform) —FBUI_MONKEY=<seed>
synthesizes a seed-deterministic pseudo-random input session (taps, drags,
flings, long-presses, cancelled touches, mouse clicks, wheel scrolls,
navigation keys, multibyte text) and drives it through the same replay path
as live input. The whole session is saved as a normal.recfile before
the first event fires, so any panic the monkey finds ships with its own
FBUI_REPLAYreproducer — hand-trimmable to a minimal test, since the
format is plain text. Every gesture is physically plausible and balanced
(a run ends with nothing held; Escape is never pressed), positions are
always on-screen, and long-press/fling timing rides the replay clock so it
holds atFBUI_REPLAY_SPEED=max.FBUI_MONKEY_EVENTSsets the budget
(default 1000),FBUI_MONKEY_OUTthe script path;FBUI_REPLAY_SHOT/
FBUI_REPLAY_EXITwork as in replay. Seedocs/monkey-testing.md. -
Instrument widgets (
fbui-widgets) — live-data widgets for the HMI /
dashboard niche:Chart, a multi-series streaming strip chart (newest
sample pinned right, traces scroll left; auto y-range quantized to nice
1-2-5 bounds with escape/shrink hysteresis, orfixed_range; horizontal
gridlines, sample-locked scrolling time-gridlines, area fill, NaN gaps,
y-axis gutter labels;Chart::sparkline()chrome-free preset) and
Gauge, a 270° radial dial (colored zone bands, 1-2-5 tick ladder,
accent value arc, needle animated on the frame-dtTweencontract,
numeric readout). Both headless, deterministic, and snapshot-tested. See
docs/instruments.mdand thetelemetryexample. -
Ui::stream+StreamDamage(fbui-widgets) — the high-rate mutation
path: unlikeUi::with(full-bounds damage + relayout, correct-but-
pessimistic), the widget reports the cheapest honest damage —Quiet
(nothing visible),Shifted(pixels scroll-blitted; repaint only the
exposed strip + seam), orRepaint(whole box, no relayout). A steady-state
chart push costs amemmoveplus a few re-rasterized columns; the
chart_stream_blit_matches_a_full_repainttest pins the fast path against
a forced full repaint frame-by-frame at two scales, per the fast-path
invariant. -
2-axis scroll-blit —
Surface::scroll_region_xy(fbui-render)
generalizes the Phase 5 scroll-blit to horizontal shifts (per-row
overlappingmemmove; single-axis only, diagonals fall back to full
repaint), and the new defaultedWidget::scroll_blit_xyhook lets a widget
shift a sub-rect of its box (the chart shifts its plot, never the axis
gutter). Existingscroll_region/scroll_blitare unchanged wrappers. -
Widget::placed(fbui-widgets) — defaulted post-layout hook handing
every widget its placed bounds and the surface scale, so widgets that need
geometry outside paint (the chart's device-quantized sample step) can cache
it without a paint context. -
Arc/circle paths (
fbui-render) —PathBuilder::arc(cubic-Bézier
circular arcs that chain with other segments) andPath::circle; what the
gauge dials are drawn with. -
The remote console (
fbuifeatureremote) — every fbui app becomes
remotely observable and operable over HTTP: setFBUI_REMOTE=8433and the
runner serves an embedded single-file web console with a live view of the
screen (multipart PNG stream, damage-driven), remote control (clicks,
scrolls, and typing injected through the exact live input path — gestures
fire, focus moves,FBUI_RECORDcaptures it, so a remotely reproduced field
issue becomes a replayable regression test), a widget-tree inspector
with on-screen highlighting, and a Prometheus/metricsendpoint
(frame cost, input counts, uptime) for fleet monitoring. Also scriptable
headless:curl -X POST '…/input?type=tap&x=240&y=112',GET /screen.png
(works while idle),GET /tree. No X11, no VNC, no new dependencies — the
server is hand-rolled overstd::net; nothing runs unlessFBUI_REMOTEis
set, a bare port binds loopback only, andFBUI_REMOTE_TOKENgates every
request when set. Headless-tested end to end (fbui/tests/remote_http.rs).
Seedocs/remote-console.md. -
Ui::inspect(fbui-widgets) — a diagnostics snapshot of the live
tree: every node's widget name (new defaultedWidget::debug_namehook),
laid-out bounds, focusable/focused/hovered state, floating overlay rect,
and children, as plain owned data (InspectNode). What the remote
console's/treeserves; equally useful to a test harness or a custom
embedder (fbui::remote::tree_jsonemits the same JSON document). -
Debug HUD (
FBUI_HUD=1) — an fps / paint-cost readout composited into
the top-right corner of every presented frame, drawn like the software
cursor (after copy-out, never into the shadow) with a built-in 3×5 pixel
font, so it works even when text rendering is what's broken. Freezes with
an idle app — the idle-burns-0% rule is untouched. -
fbui_render::encode_png_rgba— PNG-encode straight-alpha RGBA8 rows
that already left aSurface(Surface::encode_pngnow delegates to it);
the form a frame snapshot shipped to another thread needs. -
Terminal backend (
fbui-platformfeatureterm, in the default set) —
run any unmodified fbui app inside a terminal: over SSH, in a terminal
emulator on a dev machine, in CI.FBUI_BACKEND=termforces it; when DRM
and fbdev both fail and the process is attached to a capable terminal
($TERMnot empty/dumb/linux), the platform now falls back to it
instead of dying. Two pixel protocols, auto-detected
(FBUI_TERM_PROTOCOL=kitty|cellsoverrides): the kitty graphics
protocol (kitty/Ghostty/WezTerm) shows real pixels at full resolution
with damage sent as small patch images over a ping-ponged base frame — a
button repaint costs bytes proportional to the button, which is what makes
it usable over SSH; half-block cells (▀+ 24-bit SGR, 2 px per cell)
works in any truecolor terminal as a preview. Input parses the terminal
byte stream into the normal normalized events: UTF-8 keys and CSI/SS3
sequences with modifiers (each key synthesized as press+release — terminals
report no release), and SGR mouse buttons/motion/wheel with
pixel-precision coordinates (mode 1016) in kitty mode. Terminal resize
rides the existing hotplug path (reconfigure→on_display_changed).
The raw-mode/alt-screen/mouse/image state is restored on every exit
path — drop, panic, fatal signals — mirroring the VT guard. Pure Rust, no
new dependencies; tested headless against pty pairs
(fbui-platform/tests/term_pty.rs). Seedocs/terminal-backend.md. -
Named keysyms for PageUp/PageDown/Insert/F1–F12 in
fbui_platform::keysym(the terminal parser emits them; evdev apps can now
match them by name too). -
Input session record & replay (
fbuirunner) — setFBUI_RECORD=path
to capture the normalized input stream of a live session (flushed per
event, so a crashed session's recording survives — that's the artifact you
wanted), andFBUI_REPLAY=pathto play it back through exactly the live
input path (gestures, focus, kinetic scrolling,App::update).
FBUI_REPLAY_SPEED=n|maxscales the clock;FBUI_REPLAY_SHOT=end.png
captures the settled end state and exits (FBUI_REPLAY_EXIToverrides),
with a bounded settle window so perpetual animations cannot hang CI.
The format is hand-editable line-oriented text (fbui-recv1, timestamps
clamped monotonic, unknown lines skipped). Together with the terminal
backend this makes a recorded kiosk flow a headless CI regression test:
replay atmaxspeed onFBUI_BACKEND=term, screenshot, compare — end
states verified byte-identical across runs. Seedocs/record-replay.md. -
The popup layer — floating overlays can now be interactive.
Ui::open_popup(owner, PopupOptions)(orEventCtx::open_popupfrom a
handler) promotes a widget's overlay into a popup: pointer events inside the
overlay rect route to the owner ahead of capture and tree hit-testing,
presses outside dismiss it (delivering the newEvent::PopupDismissed) and
are consumed, scrolls outside are swallowed, Tab is confined to the topmost
popup owner's subtree, and grabbed focus is restored on close. Popups stack;
entries are pruned when the owner vanishes or stops reporting an overlay.
The newWidget::prepare_overlayhook (defaulted) gives overlay owners font
access to measure themselves before first placement. SeeDESIGN.md§5. -
place_anchored(fbui_widgets::popup) — the shared placement rule for
anything floating: preferred side (Below/Above/Right/Left+
Start/Center/Endalignment), flipped to the opposite side when it
doesn't fit and the opposite has more room, clamped (and shrunk as a last
resort) to the surface.Select's menu flip is now this function. -
Menu— a floating action menu on the popup layer: a zero-size host
widget (theToastspattern) armed viaUi::with(open_at/open_below)
then registered withUi::open_popup. Items activate on release or Enter;
separators and disabled items are skipped by pointer and arrow keys
(Up/Down/Home/End); Esc and click-away fireon_close. Submenus and
scrolling menus are out of scope for v1. -
ContextMenu— a transparent wrapper widget: children lay out and paint
normally; a right-click or long-press anywhere inside its bounds (bubbled,
so gestures on nested interactive children count) opens the shared menu
engine at the pointer.on_select(index)on activation,on_close
otherwise. -
Tooltips —
Ui::set_tooltip(id, Tooltip::new("…"))shows a tip after a
hover dwell (default 0.6 s,delay(..)) or immediately on a long-press
(touch), hidden on hover change, any press/release, or a key. The dwell
counts down on the deterministic frame clock and the clock runs only
during the dwell — a shown tip costs nothing (the idle-burns-0% rule).
Placement prefers above (placement(..)), flipping at screen edges.
(Deliberately not built on the app timer API below: tooltips stay inside
the deterministic, headless-testable widget layer.) -
App timers —
Proxy::send_after(delay, msg)delivers a message to
App::updateonce afterdelay;Proxy::send_every(period, msg)repeats
(fixed-delay: a stalled loop catches up with one message, never a burst).
Both return a cancellable,Sendfbui::Timerhandle (dropping the handle
detaches; the delivery still happens) and work from any thread. No threads,
no ticking: the event loop sleeps inpolluntil the earliest deadline.
See the newtimerexample. -
PlatformHandler::next_timeout(fbui-platform, defaulted — existing
handlers unaffected) — the handler now bounds the event loop's poll
timeout with its next deadline instead of the loop hard-coding 16 ms.
ReturningNonelets the loop block until fd activity (bounded by the
~1 s hotplug-poll backstop). Thefbuirunner uses it: 16 ms only while
animating or mid-gesture, the next timer deadline otherwise, else no
time-based wakeups at all — a truly idle app now sleeps ~1 s per wakeup
instead of spinning at 60 Hz. -
TabBar— a segmented tab strip for switching between views: equal-width
segments in one tree node (self-painted, self-hit-tested), a single tab stop
with Left/Right/Home/End moving the selection while focused. Emits
on_select(index)only when the selection changes;TabBar::tab_rect
exposes the segment geometry paint and hit-testing use. -
Spinner— an indeterminate activity indicator: a ring of dots with a
rotating brightness head, spinning from the moment it's added and stopped /
restarted viaset_running. Driven by the framedt(never a wall clock),
damage-quantized to head steps, and free when stopped — the idle-burns-0%
rule. To let a widget animate from birth,Uinow arms one conservative
animation tick when a widget is inserted (cleared on the nextanimateif
nothing is actually running), the same armUi::withalready used. -
SVG icons (feature
svg, off by default) —Image::from_svg/
Image::from_svg_filerasterize a vector icon at any device-pixel size
through resvg (which draws with the same tiny-skia the painter uses):
one asset serves every scale, displayed via the existingImageView.
The drawing is fit to the requested box, aspect preserved, centered.
Text and embedded raster images inside SVGs are deliberately not enabled. -
Screenshot API —
Surface::to_rgba(straight-alpha RGBA8 rows),
Surface::encode_png, andSurface::write_pngexport what's on screen;
Ui::request_screenshot(path)+Ui::take_screenshot_requestlet an app
ask for a capture fromApp::update(remote diagnostics for a device with
no second screen). The runner fulfills a request after the next paint — the
shot always includes what the requesting update changed. -
Keyboard— an on-screen (virtual) keyboard for touch kiosks with no
physical keyboard. A docked, non-focusable key grid that paints all its keys
itself and hit-tests taps internally (one tree node), with QWERTY, a Shift
layer, and a?123symbols layer. It deliberately never takes focus (so the
text field being edited keeps it) and emits each tappedKeyviaon_key.
Shift is one-shot (reverts after the next character) and Backspace
auto-repeats while held, driven by the framedt— never a wall clock.
Keyboard::key_rectexposes the key geometry paint and hit-testing use. -
Ui::send_key— replay aKeyto the focused widget as a synthetic
pressed key event, the routing for an on-screenKeyboard's taps from
App::update: it drives the same event path as hardware input, so
on_changefires andTabmoves focus. See theoskexample. -
TextInput::apply_key— apply aKeyat the caret as if typed
(insert/backspace/delete/cursor), for direct programmatic edits. The
hardware-key path shares this same implementation. It does not fire
on_change; useUi::send_keywhen it should. -
Widget::animate_with+AnimCtx— a defaulted variant of
Widget::animatethat can emit application messages from an animation tick
(whatUi::animatenow calls);Keyboard's Backspace auto-repeat rides on
it. Existinganimateimplementations are unaffected.
Changed
BackendKindis now#[non_exhaustive]and gained theTerminal
variant (breaking): downstream matches need a wildcard arm (once — the
planned GPU backend then arrives without another break).PlatformConfiggainedprefer_term(breaking for struct literals not
using..Default::default()), andPlatformConfig::default()now reads
FBUI_BACKEND(drm/fbdev/term; unknown values are warned about and
ignored), so existing binaries gain backend selection without a rebuild.
Requestingtermon a build without the feature is a
FeatureDisablederror, never a silent fall-through to a device backend.fbui-platform's default feature set now includesterm, and
Platform::newfalls back to the terminal backend when DRM and fbdev both
fail and the process is attached to a capable terminal emulator. Builds
that must never touch the controlling terminal can omit the feature or set
FBUI_BACKEND=drm/fbdev, which disables the fallback.Eventis now#[non_exhaustive](breaking): downstream matches need a
wildcard arm. Added theEvent::PopupDismissedvariant, delivered to a
popup's owner when theUidismisses it (click-away, or a press landing in
a popup stacked below it).- Gesture bubbling (breaking for custom widgets relying on non-bubbling):
Tap,LongPress,Fling, and right-buttonPointerDownnow bubble from
the hit widget to its ancestors likeScrollandKeyalways did.
Left-button presses/releases stay direct. In-tree audit:Dialogalready
swallowed the gesture set (pinned by a regression test); a fling landing on
a child of aScrollViewnow correctly coasts the view. Selectsits on the popup layer: the open menu no longer captures the
pointer or implements its own click-away/scroll-swallow — theUiroutes,
dismisses, and consumes. Behavior is unchanged (the existing Select tests
pass as-is); Tab while the menu is open now stays on the field instead of
leaking to the page behind it.App::Messagenow requiresSend(breaking): the timer queue shares
messages across threads, asProxyalways did in practice (Proxy<M>was
only everSendwhenMwas). UI message enums areSendin any
realistic app; the bound just states it.
Fixed
- A headless
cargo doc --no-deps --workspacefailed onfbui's intra-doc
links to platform-gated runner items (run,Proxy); those links are now
exempt only when theplatformfeature is off — CI still documents with
the feature on, where they're fully checked. - A stray debug
eprintln!inUi::paintprinted a line on every
scroll-blit frame; removed. Keyboardno longer leaks pointer capture when a held (auto-repeating)
Backspace is cancelled by sliding off the key: the slide-off cleared the
pressed state, and the release of the capture taken on press was gated on
it, so the keyboard kept routing all later pointer input to itself —
buttons and fields elsewhere on screen stopped responding until a key tap
completed on the keyboard. The capture is now released on every pointer-up.- The runner now drains widget messages until the queue is empty after
events, animation ticks, andProxywakes — a message queued from inside
App::updateitself (e.g. anon_changetriggered viaUi::send_key) was
previously stranded until the next input event arrived.