Skip to content

fbui v0.3.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 05:17
· 26 commits to main since this release
ba8b42e

Added

  • crates.io publishing. All five crates are now publishable (publish = false is gone) and carry the metadata crates.io and docs.rs want: a root
    README.md, keywords/categories, LICENSE-MIT/LICENSE-APACHE texts, and
    [package.metadata.docs.rs] feature sets. Sibling crates pin each other
    exactly (=x.y.z, via [workspace.dependencies]), matching the lockstep
    policy. Pushing a v<version> tag runs .github/workflows/release.yml:
    consistency check, tests, cargo package --workspace, an ordered
    cargo publish --workspace (trusted publishing, or a CARGO_REGISTRY_TOKEN
    for the first upload), and a GitHub release from this changelog.
    scripts/release-check.sh guards the pins/changelog/tag, and CI now
    packages and verifies every crate on each push. See RELEASING.md.

  • fbui-ctl and the remote console's text endpoints — the third flow
    executor, and the field-support loop.

    • fbui-ctl (a binary in fbui, --features remote, std::net only):
      tree, json, shot, tap/press/release/move/wheel, type,
      key, trace [--follow], metrics, and run <flow.txt> — which resolves
      a flow's references against the live device via GET /tree and injects
      through POST /input. FBUI_CTL picks the console; FBUI_REMOTE_TOKEN
      authenticates.
    • GET /tree.txt serves Ui::inspect_text(), and GET /trace the
      last 500 trace lines — which the console records whenever it is enabled,
      even without FBUI_TRACE.
    • fbui::remote::parse_tree reads the /tree document back into an
      InspectNode, pinned to the writer by a round-trip test.
    • docs/tooling.md documents the whole track; PHASE-TOOLING.md records
      verified-vs-pending status and every deviation from the design;
      CLAUDE.md gains the no-screen workflow.
    • CI gains a headless job: every committed flow, every example lint-checked,
      and a monkey session on three examples — no sudo, no modprobe.
  • 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: no msg after the input means the callback is missing; a
      msg with no mutate means update matched the wrong arm.
    • App::describe_message (default None) 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_lint suppresses
      one rule on one widget and Ui::set_touch_target sets the tappable
      minimum (default 24 logical px; a touch-only kiosk should set 44).
      FBUI_LINT=1 runs the pass after every layout and reports each finding
      once; expect no-lints makes it a flow failure.
    • All ten shipped examples are lint-clean. Getting there fixed two real
      bugs the pass found: the showcase's panels row pushed its content off a
      1024x600 screen, and custom_widget's Dot described nothing.

Changed

  • Container::shrink() — the flexbox min-size: 0 idiom, so a growing
    container bounded by its parent wins over an oversized child. Without it a
    windowing child (a List of 50 rows) demands its full content height and
    pushes the page off the screen, which is what the new overflow lint found
    in the showcase example.

  • Flow scripts (fbui-rec 2) and two executors — a UI interaction
    written as steps and expectations, meaning the same thing in a cargo test
    and under the runner.

    fbui-rec 2
    tap #inc
    type "milk"
    key Enter
    wait settle
    expect #count text "1 item"
    
    • fbui_widgets::script holds 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::harness runs a flow in-process against a Ui
      (run_text, assert_flow), feeding widget events through the same path
      the behavior tests use and calling the caller's update for every
      message. A failure panics with the failing line, the actual value, and
      the tree.
    • The runner plays v2 flows wherever FBUI_REPLAY takes 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.txt and .fail.png
      beside the flow. FBUI_REPLAY_STEP (default 50 ms) paces the steps.
      Raw @ms lines 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
      on Ui::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=path writes that dump at the end of a replay,
      beside FBUI_REPLAY_SHOT. An empty replay file is now a valid empty
      recording, so FBUI_REPLAY=/dev/null renders the first screen, writes
      the artifacts and exits.

Changed

  • InspectNode.name is now InspectNode.kind (the widget type), and
    name holds the app-assigned name from Ui::name (Option<String>). The
    node also grows text, props and visible. The remote console's
    GET /tree JSON follows: kind replaces name, and nodes gain optional
    name, text, props and a visible flag. 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, feature headless, on by
    default) — FBUI_BACKEND=headless runs 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=WxH sets the surface (default 1024x600); a
      malformed value is a hard error like the other FBUI_* knobs.
    • Buffer ages follow the DRM double-buffered sequence exactly, so the
      partial redraw path runs headless rather than the age = 0
      repaint-everything path a single buffer would force.
    • Rows are padded to 64 bytes, so stride != width * bpp and 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 (or display::headless::request_mode) simulates a hotplug /
      mode change, giving on_display_changed an 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 fuller TextInput —
    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/Down keep the
      goal column across shorter lines, Home/End are per line,
      Ctrl+Home/End document-wide, PageUp/PageDown by 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 enclosing ScrollView still moves. rows(n) fixes the height,
      grow(f) fills a column.
    • The process clipboard — Ui::clipboard() / Ui::set_clipboard(..)
      and EventCtx::clipboard() / EventCtx::set_clipboard(..). There is no
      system clipboard without a display server, so the Ui owns 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).
    • TextInput now does Ctrl+A select-all, Ctrl+C/X/V (paste
      flattens line breaks to spaces), Ctrl+Left/Right word jumps,
      Ctrl+Backspace/Delete word 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²)). Enter is left unhandled so a form
      can act on it. New selection() / select(..) / select_all() /
      cursor() / scroll_offset() accessors. Both widgets share one
      unit-tested editing core so the key table can't drift between them.
    • TextLayout geometry (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 \x03 for Ctrl+C, the terminal parser reports
      nothing), so Key::Char('c') + mods.ctrl is what a field sees
      everywhere. Behavior tests in fbui-widgets/tests/text_editing.rs
      drive both widgets through the real event path with the bundled font.
      See docs/text-editing.md; the form example gained a notes box.
  • Bootable kiosk ISO tooling (scripts/iso/, docs/linux-iso.md) —
    build-iso.sh packages a static-musl fbui app (default: the showcase
    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 /init loads 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.sh boot-tests the ISO headlessly under
    QEMU (BIOS and --uefi OVMF, --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 in docs/linux-iso.md.

  • Display rotation (fbui-render + fbui runner) — 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. See docs/rotation.md.

  • VideoView (fbui-widgets) — damage-aware presentation for externally
    produced frames (camera preview, decoded video): the producer thread
    converts with the new fbui_render::yuv helpers (yuyv_to_rgba,
    nv12_to_rgba — BT.601 fixed point), builds an Image via the new
    Image::from_rgba_bytes, and ships it through Proxy + Ui::stream;
    VideoView::push_frame reports precise StreamDamage::Repaint, so a
    60 fps feed repaints only the video rect with no relayout while the rest
    of the UI stays idle. Contain/Cover/Fill object-fit (pure, tested
    fit_rect), letterbox color, and a new Painter::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 (new Widget::position_child hook) 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
    (new Widget::active_children hook); popped screens are reaped after the
    exit slide settles (new Widget::take_child_removals hook). Push remembers
    focus, pop restores it (new Ui::focus/Ui::child_ids,
    EventCtx::focus_widget), and a bubbling Escape pops with an optional
    on_back message.

  • Idle power management (fbui-platform + fbui runner) — 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 sysfs Backlight control, and a
    PlatformHandler::take_power_request hook so the handler can drive power
    without owning the display. Apps opt in via App::idle_policy
    (IdlePolicy: dim/blank timings, dim level, on_idle/on_wake
    messages); operators override with FBUI_IDLE_DIM / FBUI_IDLE_BLANK /
    FBUI_IDLE_DIM_LEVEL. The tracker is a pure, unit-tested state machine
    riding the loop's next_timeout, so idle management adds no ticking and a
    blanked screen sleeps purely on fds. See docs/power-management.md.

  • TreeView (fbui-widgets) — the hierarchical counterpart to List:
    expand/collapse branches with disclosure triangles, per-depth indentation,
    and single selection. Nodes are plain data (TreeNode::branch/leaf)
    addressed by stable depth-first NodeIds; the tree is flattened to its
    visible rows and painted with List'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_toggle message hooks, set_nodes/set_selected/set_expanded for
    Ui::with mutation.

  • 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-Gregorian Date
    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
    retarget mid-flight bends the trajectory smoothly instead of restarting
    it, and set_velocity hands off a fling's release speed. Feel is described
    by response (undamped period; default 0.35 s) and damping_ratio (1.0 =
    critical, below overshoots) rather than raw spring constants. Same
    frame-clock contract as Tween (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, feature platform) — 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 .rec file before
    the first event fires, so any panic the monkey finds ships with its own
    FBUI_REPLAY reproducer — 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 at FBUI_REPLAY_SPEED=max. FBUI_MONKEY_EVENTS sets the budget
    (default 1000), FBUI_MONKEY_OUT the script path; FBUI_REPLAY_SHOT /
    FBUI_REPLAY_EXIT work as in replay. See docs/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, or fixed_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-dt Tween contract,
    numeric readout). Both headless, deterministic, and snapshot-tested. See
    docs/instruments.md and the telemetry example.

  • Ui::stream + StreamDamage (fbui-widgets) — the high-rate mutation
    path: unlike Ui::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), or Repaint (whole box, no relayout). A steady-state
    chart push costs a memmove plus a few re-rasterized columns; the
    chart_stream_blit_matches_a_full_repaint test 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
    overlapping memmove; single-axis only, diagonals fall back to full
    repaint), and the new defaulted Widget::scroll_blit_xy hook lets a widget
    shift a sub-rect of its box (the chart shifts its plot, never the axis
    gutter). Existing scroll_region/scroll_blit are 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) and Path::circle; what the
    gauge dials are drawn with.

  • The remote console (fbui feature remote) — every fbui app becomes
    remotely observable and operable over HTTP: set FBUI_REMOTE=8433 and 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_RECORD captures it, so a remotely reproduced field
    issue becomes a replayable regression test), a widget-tree inspector
    with on-screen highlighting, and a Prometheus /metrics endpoint
    (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 over std::net; nothing runs unless FBUI_REMOTE is
    set, a bare port binds loopback only, and FBUI_REMOTE_TOKEN gates every
    request when set. Headless-tested end to end (fbui/tests/remote_http.rs).
    See docs/remote-console.md.

  • Ui::inspect (fbui-widgets) — a diagnostics snapshot of the live
    tree: every node's widget name (new defaulted Widget::debug_name hook),
    laid-out bounds, focusable/focused/hovered state, floating overlay rect,
    and children, as plain owned data (InspectNode). What the remote
    console's /tree serves; equally useful to a test harness or a custom
    embedder (fbui::remote::tree_json emits 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 a Surface (Surface::encode_png now delegates to it);
    the form a frame snapshot shipped to another thread needs.

  • Terminal backend (fbui-platform feature term, 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=term forces it; when DRM
    and fbdev both fail and the process is attached to a capable terminal
    ($TERM not empty/dumb/linux), the platform now falls back to it
    instead of dying. Two pixel protocols, auto-detected
    (FBUI_TERM_PROTOCOL=kitty|cells overrides): 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). See docs/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 (fbui runner) — set FBUI_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), and FBUI_REPLAY=path to play it back through exactly the live
    input path (gestures, focus, kinetic scrolling, App::update).
    FBUI_REPLAY_SPEED=n|max scales the clock; FBUI_REPLAY_SHOT=end.png
    captures the settled end state and exits (FBUI_REPLAY_EXIT overrides),
    with a bounded settle window so perpetual animations cannot hang CI.
    The format is hand-editable line-oriented text (fbui-rec v1, timestamps
    clamped monotonic, unknown lines skipped). Together with the terminal
    backend this makes a recorded kiosk flow a headless CI regression test:
    replay at max speed on FBUI_BACKEND=term, screenshot, compare — end
    states verified byte-identical across runs. See docs/record-replay.md.

  • The popup layer — floating overlays can now be interactive.
    Ui::open_popup(owner, PopupOptions) (or EventCtx::open_popup from 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 new Event::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 new Widget::prepare_overlay hook (defaulted) gives overlay owners font
    access to measure themselves before first placement. See DESIGN.md §5.

  • place_anchored (fbui_widgets::popup) — the shared placement rule for
    anything floating: preferred side (Below/Above/Right/Left +
    Start/Center/End alignment), 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 (the Toasts pattern) armed via Ui::with (open_at/open_below)
    then registered with Ui::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 fire on_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::update once after delay; Proxy::send_every(period, msg) repeats
    (fixed-delay: a stalled loop catches up with one message, never a burst).
    Both return a cancellable, Send fbui::Timer handle (dropping the handle
    detaches; the delivery still happens) and work from any thread. No threads,
    no ticking: the event loop sleeps in poll until the earliest deadline.
    See the new timer example.

  • 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.
    Returning None lets the loop block until fd activity (bounded by the
    ~1 s hotplug-poll backstop). The fbui runner 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 via set_running. Driven by the frame dt (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, Ui now arms one conservative
    animation tick when a widget is inserted (cleared on the next animate if
    nothing is actually running), the same arm Ui::with already used.

  • SVG icons (feature svg, off by default) — Image::from_svg /
    Image::from_svg_file rasterize 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 existing ImageView.
    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, and Surface::write_png export what's on screen;
    Ui::request_screenshot(path) + Ui::take_screenshot_request let an app
    ask for a capture from App::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 ?123 symbols layer. It deliberately never takes focus (so the
    text field being edited keeps it) and emits each tapped Key via on_key.
    Shift is one-shot (reverts after the next character) and Backspace
    auto-repeats while held, driven by the frame dt — never a wall clock.
    Keyboard::key_rect exposes the key geometry paint and hit-testing use.

  • Ui::send_key — replay a Key to the focused widget as a synthetic
    pressed key event, the routing for an on-screen Keyboard's taps from
    App::update: it drives the same event path as hardware input, so
    on_change fires and Tab moves focus. See the osk example.

  • TextInput::apply_key — apply a Key at 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; use Ui::send_key when it should.

  • Widget::animate_with + AnimCtx — a defaulted variant of
    Widget::animate that can emit application messages from an animation tick
    (what Ui::animate now calls); Keyboard's Backspace auto-repeat rides on
    it. Existing animate implementations are unaffected.

Changed

  • BackendKind is now #[non_exhaustive] and gained the Terminal
    variant
    (breaking): downstream matches need a wildcard arm (once — the
    planned GPU backend then arrives without another break).
  • PlatformConfig gained prefer_term (breaking for struct literals not
    using ..Default::default()), and PlatformConfig::default() now reads
    FBUI_BACKEND
    (drm/fbdev/term; unknown values are warned about and
    ignored), so existing binaries gain backend selection without a rebuild.
    Requesting term on a build without the feature is a
    FeatureDisabled error, never a silent fall-through to a device backend.
  • fbui-platform's default feature set now includes term, and
    Platform::new falls 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.
  • Event is now #[non_exhaustive] (breaking): downstream matches need a
    wildcard arm. Added the Event::PopupDismissed variant, delivered to a
    popup's owner when the Ui dismisses 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-button PointerDown now bubble from
    the hit widget to its ancestors like Scroll and Key always did.
    Left-button presses/releases stay direct. In-tree audit: Dialog already
    swallowed the gesture set (pinned by a regression test); a fling landing on
    a child of a ScrollView now correctly coasts the view.
  • Select sits on the popup layer: the open menu no longer captures the
    pointer or implements its own click-away/scroll-swallow — the Ui routes,
    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::Message now requires Send (breaking): the timer queue shares
    messages across threads, as Proxy always did in practice (Proxy<M> was
    only ever Send when M was). UI message enums are Send in any
    realistic app; the bound just states it.

Fixed

  • A headless cargo doc --no-deps --workspace failed on fbui's intra-doc
    links to platform-gated runner items (run, Proxy); those links are now
    exempt only when the platform feature is off — CI still documents with
    the feature on, where they're fully checked.
  • A stray debug eprintln! in Ui::paint printed a line on every
    scroll-blit frame; removed.
  • Keyboard no 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, and Proxy wakes — a message queued from inside
    App::update itself (e.g. an on_change triggered via Ui::send_key) was
    previously stranded until the next input event arrived.