Commit graph

239 commits

Author SHA1 Message Date
Kevin Boos
e0a5a23f2f
Splash improvements for running untrusted mini-apps (#1139)
* fix a pile of splash script-vm bugs: newline statements, short-circuit args, tail calls

went through the script VM and parser and fixed a batch of correctness bugs
that were biting the launcher's mini-apps:

- newline-delimited statements: a `(` or `[` at the start of the next line no
  longer greedily glues onto the previous value as a call/index. leading infix
  operators and `.` still continue the expression (the shader DSL needs that),
  and the divert is suppressed inside ()/[] groupings.
- short-circuit `&&`/`||` used as a call argument no longer loses its value to
  nil when the jump skips a multi-op right-hand side.
- a call as the very last statement of a script actually executes now, in both
  end-of-parse unwind loops (also patched a zero-offset ShortCircuitEnd).
- custom widgets that deref to a base with a #[source] field now forward
  script_source, so script_apply_eval works on them instead of silently no-op'ing.

plus regression tests for the newline and short-circuit cases.

* harden splash isolates: scoped timers, net gating, effective-visibility snapshots

isolate-safety work so mini-apps can't reach outside their sandbox:

- isolate-safe script-timer dispatch hook + gc for stale timers
- gate net.socket_stream on the net runtime being present
- widget-tree snapshot reports effective visibility (a widget counts as hidden
  if any ancestor is hidden)
- macos_activate_app (plus a headless no-op) so the launcher can focus itself

* widen the host->splash surface: splash setters, view/glassbutton script calls

everything the host needs to poke into a running mini-app's script:

- Splash: call_script_fn, set_script_global, set_allow_net, and a cached body id
  so host->script calls don't rescan for the body every time
- View.set_visible and GlassButton set_text/text are callable from script now
- makepad_test learned right-click (secondary button) so the headless tests can
  exercise long-press / context menus

* fix small-size glass lens + sdf box degeneration, warn on missing glyphs

visual correctness fixes we kept tripping over:

- cap the gauss lens band at 35% of the surface's smaller side so tiny discs
  degrade gracefully instead of smearing
- clamp the Sdf2d.box (and box_x/box_y/box_all) radius so an oversized radius
  saturates at a circle instead of collapsing into a rotated diamond
- log once per codepoint when no loaded font can render it (was silently
  drawing .notdef boxes)

* guard stale rect areas in clipped_rect/abs_to_rel/set_rect against out-of-bounds panics

* add switch_finger_capture to hand a live finger capture between widgets mid-drag

* add promote_finger_capture_over: hand a child-grabbed finger up to a co-capturing container

* splash: add validate_splash_body, a dry-run eval for externally-sourced scripts

evaluates a body in a throwaway isolate with the exact prefix/limits the
Splash widget uses and returns the captured script errors instead of logging
them. lets hosts that install source from outside (downloads, AI generation,
user input) reject bad scripts with real errors to show or feed back, where
the widget's own eval silently keeps the old view.

* strip mod.res from splash isolates; document validate_splash_body caveats

the res module's handles reach both the filesystem (abs_path loads) and the
network (web_url / http resources) without going through the gated net
runtime, so a 'no-net' isolate could still fetch and exfiltrate. found by an
adversarial review of AI-generated app installs, but it applies to any
untrusted splash source.

also note on validate_splash_body that the instruction limit bounds compute,
not heap growth, and that top-level timers live until isolate reclamation --
same exposure as actually installing the source, so validation adds nothing
new.

* splash: jailed per-app file storage (mod.fs inside isolates)

mini-apps get an OS-style private data directory, like an android app's
internal storage or an iOS container: the app sees a filesystem rooted at
"/", and that root IS its host-assigned sandbox directory
(Splash::set_sandbox_dir / SplashRef forwarder). registered as mod.fs in
isolates -- deliberately shadowing the stripped real fs module, so inside
an app "the filesystem" simply is the jail:

  fs.read fs.write fs.append fs.exists fs.remove fs.mkdir fs.list

containment lives entirely in the host layer:
- lexical path resolution against the root; `..` above the root, NUL, deep
  or overlong paths are errors before any I/O
- the per-VM root is rust state keyed by the isolate's heap -- script code
  can neither read nor retarget it
- symlink defense in depth: nothing here can create links, and every
  existing component under the root is verified non-symlink before use
- quotas: 1MB/file, 16MB/jail, 256 entries
- no root assigned (previews) -> every call errors cleanly

validate_splash_body gives dry runs a throwaway jail (temp dir, removed
after) so top-level fs.read boot loads validate instead of erroring. roots
are dropped with their isolates in the gc.

unit tests cover the containment: traversal/absolute/backslash escapes,
depth/name caps, and the symlink block.

* splash: put the jailed fs module in scope as a bare name

app scripts say fs.read("/x"), but the eval prefix only used the widgets
prelude, so bare fs resolved to a not-found error value and every storage
call failed silently. bind it in the prefix (let fs = mod.fs) for both the
plain and net variants; a script reassigning fs only shadows its own name,
the jail stays host-side.

* splash storage: quota + boundary hardening from adversarial review

three confirmed jail findings:
- mkdir bypassed every quota (target + create_dir_all, no jail_usage check)
  -> unbounded inode/dir-metadata exhaustion on the shared host volume.
  now charges new dirs against MAX_ENTRIES via missing_entries(); write's
  entry check does the same so a deep write can't overshoot the cap either.
- write/append/mkdir lacked remove's root guard: fs.write("/", data)
  resolved real == root and reached create_dir_all(root.parent()) -- one
  dir above the jail (the shared app_data/). now rejected like remove does.
- validate_splash_body's scratch jail used a predictable temp name created
  with create_dir_all (would follow a planted symlink out of temp). now an
  exclusive create_dir on a per-process+vm name (EEXIST-safe against a
  planted entry), reclaimed via gc before the dir is removed so a top-level
  timer can't resurrect it.

unit tests added for missing_entries; the containment tests still pass.

* splash: empty set_text tears down the isolate instead of no-oping

set_text("") was a silent no-op (eval_body early-returns on an empty body),
so a reused Splash that goes back to empty -- the widget-gallery live preview
on Back -- left its old isolate running its timers (and holding a storage-jail
binding) behind a blank view. now an empty body reclaims the isolate: the
isolate-minted view is replaced with a fresh empty one built in the main vm
BEFORE the isolate heap is freed, then the isolate is gc'd (stopping its
timers, dropping its jail root); vm_id resets to MAIN so a later non-empty
set_text allocs a fresh isolate as before. the existing host_launcher
teardown call sites (widget picker back()/reset()) become correct unchanged.

* overlay: composite glass in draw order, not creation order

every gauss/glass surface opens its own draw list and registers it in the
window's single Overlay via store_sub_list, which hands out the first free
slot and keeps it for the life of the process. renderers walk that table in
index order, so the paint order of all glass in an app was the order the
surfaces were first *created* — permanently, with freed slots reused by
whatever registered next. draw order never came into it, so a widget rebuilt
after a layout change, or a panel opened later, could land on top of anything
drawn after it. the only workarounds available to apps were "don't draw the
thing that's winning", which looks like a bug.

the hook for fixing it was already there and unused: CxDrawList's
draw_item_reorder, honoured by every backend (metal, d3d11, opengl, vulkan,
web_gl, headless raster). so stamp each overlay sub-list with the position it
was begun in this frame (Cx2d::overlay_seq, reset in Overlay::begin) and have
Overlay::end stable-sort the table by that stamp.

this also gets parent-then-child right without special cases, which matters
because glass.GlassButton / glass.GlassSegmented call begin_overlay_reuse
unconditionally instead of checking is_drawing_overlay(), so they hold their
own slots rather than riding their parent's.

* glass.GlassSegmented: size segments to their labels, add set_selected

three things, all of them things that looked broken to a user:

- segments were width/count, so "Max" got the same room as "Default": the long
  word crowded, the short one floated. each segment is now measured (DrawText
  layout size_in_lpxs) and gets its text plus padding, with leftover width
  shared equally so every label keeps the same margin. if the labels don't fit,
  the padding shrinks (never the text) to a floor. the pill's x/width are
  computed in rust and passed as uniforms since they can't come from a segment
  count any more, and hit-testing is a boundary lookup rather than a division.

- `selected` was public but the pill is drawn from a private sel_pos that only
  followed it via the click animation, so restoring a saved value from code
  left the control showing one segment while reporting another — and a click on
  the segment it really held was then ignored as "already selected". that reads
  as the control eating your clicks. set_selected keeps both in step.

- the travel easing was 0.30, which arrived before the eye could follow it.
  0.16.

* text_input: re-layout when max_lines changes

the laidout text was cached on width alone, so flipping draw_text.max_lines
at runtime (collapsing a composer to one line) kept the old multi-row layout
and the field never shrank. make max_lines part of the cache key.

* text_input: add set_max_lines instead of making callers script it

applying script to a TextInput re-applies its #[live] fields, and text is
one of them, so toggling max_lines through script_apply_eval! silently
wiped whatever the user had typed. give it a typed setter.

* text_input: don't drop the layout in set_max_lines

clearing laidout_text there leaves the field with no layout for the rest
of the event batch, so every cursor op in that window bails out with
"can't move cursor because layout was invalidated by an earlier event".
since set_max_lines gets called from focus/blur handling, that window is
exactly when you're clicking into the field — so the click placed no
caret at all. max_lines is already part of the layout cache key, so the
next draw re-lays out on its own.

* text_input: add scroll_to_top

for a field that folds to a fixed height when it loses focus: the scroll
offset survives the blur, so a draft last edited near its end folds
showing whichever line the caret had scrolled to rather than its first.
leaves laidout_text alone — scrolling doesn't change the layout, and
dropping it would break every cursor op for the rest of the event batch,
same trap as set_max_lines.

* text_input: add set_height

for a composer that folds to one line when it loses focus. pinning the
height is the safe way to fold — unlike clamping max_lines it leaves the
laid-out text alone, and the laid-out text is what maps a click to a
caret position. fold by re-layout and the press that re-focuses the
field resolves against the folded layout while the expanded one is on
screen, so the caret and any drag-selection land on the wrong text.

* text_input: add take_key_focus, which actually shows the caret

the caret draws as (1.0 - blink) * focus, and both come from animators
that only move when the widget is dealt a Hit::KeyFocus. setting key
focus on a field that ALREADY holds it dispatches no hit — so a field
that was focused, then hidden (hiding doesn't clear Cx's key focus) and
shown again comes back typable but with no caret and no selection
highlight, animators still parked where the last focus-lost left them.

plays focus.on unconditionally rather than only when focus changed:
repairing the case where it did NOT change is the entire point.

* splash: name scripts in errors, and stop using line as an identity slot

a runtime error from a Splash app logged `:1804943384:12 - widget has
no uid`: empty file, and a "line" that is really a pointer address.
the format is {file}:{line}:{col}, and both fields were casualties of
the same hack — ScriptMod.line carried self_id so the body could be
found again (m.line == self_id && m.file.is_empty()), while ip_to_loc
adds that same field to the script's real line when reporting. so every
location came out as real_line + a pointer, and nothing said WHICH app.

identity moves to module_path, which nothing else reads for these
bodies, freeing line to be a line. file gets a real name via a new
set_debug_name the host calls with the mini-app's id.

the validator's ScriptMod gets the same treatment — its errors are
shown to the user AND fed back to the agent as repair input, so a
location offset by a vm id was actively misleading there.

* splash: document the constant offset in reported script lines

the host prefix is two lines, so a reported line is two ahead of the
app's own file. it can't be zero — a zero-line prefix would share line 1
with the app's first line, and that line is always the // name: header,
which would comment the prefix out.

* script: stop silently losing widgets emitted from branches and loops

Splash mini-apps kept rendering nothing from on_render closures with zero
errors logged. Bisected live and in pure-VM probes, this was a pile of
separate bugs in the same corner:

- an if/else whose branch emits a widget lost the taken true branch: the
  statement's POP_TO_ME got fused onto the else tail, which the IF_ELSE
  jump skips. Generalized last_short_circuit_target into last_jump_target
  and record every branch join (if/elif/else, match, try/err), so the
  commit lands standalone AT the join and every path runs it. Same disease
  as the short-circuit-argument bug, new jump sites.
- elif never patched its arm's IF_ELSE jump (relative 0), so a taken arm
  spun the interpreter until the instruction limit killed the whole entry.
  elif now desugars into else { if ... } via IfElseExpr, which also gives
  its arms the join treatment.
- for x in <non-iterable> silently skipped the body where the equivalent
  while errored; now raises "for loop source is not iterable" (nil and
  empty sources stay silent). for k v in <number> passed key/index swapped
  and lost k after the first iteration (the advance only rebound v).
- a line-leading { after a value-ending line glued onto the previous
  expression as a proto instantiation; it now starts a new statement,
  same divert rule the ( and [ newline fix added.
- int literals were second-class numbers: U40/I32/F32/F16/U32 didn't
  collapse to the number bucket in to_redux, so 6 .is_number() missed
  method dispatch entirely and an int arg against a float default failed
  with "expected number, got number". They're all just numbers now.

Regression tests in tests/on_render_emission.rs cover each shape, incl.
the exact calendar/weather patterns that were blank in the launcher.

* splash: keep an on_render closure's final widget, and say when a render fails

Two host-side halves of the emission-loss story:

- the parser turns a closure's last statement into its return value, so a
  render closure that ENDS with a widget literal built it and then threw it
  away (this is why wrapping a whole render in one extra View{} produced
  nothing). script_result now pushes a returned widget object into me as
  the last child; non-widget returns still get skipped downstream.
- a render closure that errors mid-run used to have its output discarded
  with no diagnostics at all, which is what made this whole bug family
  cost days to find. Now it logs the error before dropping the result.

* script: auto-close still-open fn and let states at end of source

Both parse drivers dropped EndFnExpr/EndFnBlock/EmitLetDyn through their
auto-close catch-all when the source ended with them still open. A module
whose FINAL statement was let c = <lambda> got a body whose jump-over
stayed 0 — FN_BODY_DYN re-ran, found its me already popped, logged
"me stack is empty" and fell straight INTO the body, running it inline
at definition time and ending the module eval early — and the let itself
never emitted, so the binding silently didn't exist (same for
let c = <call>). Now those states close the way the live handlers do:
return + jump patch for the body, LET_DYN/LET_TYPED for the binding, and
the let's own EndStmt no longer marks a statement value (LET consumed
it; the final RETURN would pop an empty stack).

Fun consequence: the old test idiom of reading a result via
  let out = r
  out
only ever worked BECAUSE the trailing let was dropped — RETURN popped
the naked value off the stack. With the let actually binding, scripts
must end on a call (echo(r)); the emission tests are updated to do that.

Regression tests in tests/auto_close_eof.rs, including the exact
deferred-boot-timer closure shape the launcher apps use (which was
already fine — it just LOOKED guilty, see the splash commit).

* splash: probe optional script hooks without spamming the error log

call_script_fn checks whether the fn exists and bails quietly — but it
probed with a trapping scope_value, which had already queued a NotFound
by the time the miss was handled. Every host broadcast of an optional
hook (on_app_resize, on_widget_resize) against a script that doesn't
define it logged
  variable 00001e93e419c77c not found in scope
— maximally misleading: the hex is just id!(on_app_resize) (Rust-side
ids aren't in the reverse-lookup table so they print raw), and the
line:col is the stale ip from the end of that script's eval, which
pointed at whatever closure happened to be compiled last. In the
launcher that was the boot-timer line of every generated app, sending
the investigation down a deferred-closure rabbit hole the pure-VM tests
then cleared. Probe with NoTrap.

* script derive: don't name the eval values vec 'v'

script_apply_eval!'s generated values block bound 'let mut v' and then
spliced #(expr) interpolations in verbatim — so a caller interpolating a
variable that happened to be called v got the macro's own half-built Vec
(borrow errors if you were lucky, the wrong value if not). Obscure name
instead.

* headless: don't compile the Apple video path

Upstream's zero-copy video work put CoreVideo/Metal code in
gpu_texture.rs (plus two consumers) behind cfg(target_os = "macos")
alone. Under cfg(headless) the apple backend isn't built at all, so
every one of those symbols — ObjcId, msg_send!, CVPixelBufferRef,
CVMetalTextureCache* — is missing and makepad-platform fails to
compile with 94 errors. That takes the headless harness down with it,
which is what host_launcher's UI tests run on.

Gate the Apple blocks on not(headless) too. Nothing is lost: headless
has no Metal device to import a CVPixelBuffer into, so the whole path
is inapplicable there.

Not caused by the rebase — pristine dev has it: its headless
CxOsTexture is an empty struct while gpu_texture.rs reads .os.texture.
2026-08-12 01:55:46 +02:00
Kevin Boos
db678bdf30
new Fit bound based on the size of the line that it actually lands on (#1164)
* Windows: fix DPI-change problems: freezing, blank windows, drag-n-drop errors

- win32: do_callback queues re-entrant events (WM_DPICHANGED pumped inside
  Present, nested WM_SIZE) instead of dropping them; the drops left dpi/geometry
  stale, the window blank, and hit-testing offset after display-scale changes.
- win32: WM_DPICHANGED applies the OS-suggested rect and returns 0; removed the
  duplicate-delivery compensations the lossless queue obsoletes.
- d3d11: no unbounded Present(1) block after a timed-out frame-latency wait
  (DO_NOT_WAIT + paced retry); DXGI errors log instead of panicking.
- dnd: Drag/Drop events carry WindowId and are remapped via dpi_override_scale,
  fixing drop-zone offset under UI zoom (Windows OLE, macOS external drops,
  Wayland internal drags).
- win32: packaged-build window icons load at native shell sizes via LoadImageW.

* TextFlow: enforce `max_lines` for inline code wraps, list items, and inline widgets

`max_lines` + `text_overflow: Ellipsis` under-counted or skipped visual lines
whenever anything but a plain text run started one, so rich content (rooms-list
previews, anything with inline `<code>` or mention pills) could overflow its
line budget or hard-clip without an ellipsis.

* TextFlow: the inline `<code>` forced wrap left `is_continuation` stale, so a
  wrapped code run was granted one extra row AND reported one fewer — a 2-line
  overflow per wrap. The wrap now refreshes the continuation state and row
  budget, is refused when no line remains, and its prediction probes measure
  an unclamped layout (a clamped probe always claimed "fits on one line").
* Layouter: a non-wrapping layout truncated by `max_lines` never reported
  `is_truncated` (detection was row-count only), so `max_lines: 1` labels
  hard-clipped mid-word instead of showing "…". Width overflow on the
  surviving last row now also triggers the ellipsis.
* TextFlow: list items charged two lines each (marker run + content run); the
  marker now shares one visual line with the item's first content run.
* Inline child widgets (mention pills, images) bypassed the budget entirely:
  Html now gates them on `is_content_truncated()`, charges the rows they open
  via `track_inline_content()`, and holds a widget that lands on the last
  allowed line in place (`Turtle::set_flow_wrap`) instead of letting the
  turtle relocate it onto a row the budget can't pay for. A held widget that
  still overruns the line is hidden behind a tracked clip and replaced with a
  drawn "…" — the same contract text truncation has.

* TextFlow: fix row alignment and spacing for wrapped text beside inline widgets

On the single-batch text path (Windows/Linux), a multi-row text run never told
the turtle where its internal row boundaries were: every walk since the last
boundary — a bold sender on one visual row, a mention pill on the next — was
centered by `finish_row` against one merged row. Text sat a few px off the
pill baselines, rows around wrapped runs squeezed or overlapped, and an
up-centered pill on a padding-less first row rose above the clip and lost the
top of its rounded corners.

* DrawText: the single-walk path now allocates per visual row and runs
  `turtle_new_line_with_spacing` at each internal boundary, keeping one glyph
  instance batch. It emits a first-row walk owning the run's align entries and
  a last-row walk with an empty range, so the run is never shifted twice.
* Turtle: `FinishedWalk` carries a `RowAlignRole`. A wrapped run's rows are
  immovable (their glyphs share one batch): rows holding its visible text are
  an `Anchor` — `finish_row_center` centers every shiftable walk on the
  anchor's line, shifting a taller pill UP onto the text — while a
  whitespace-only first row (a continuation that wrapped on a leading space)
  is `Fixed`, so it cannot anchor a row to an invisible line. Anchor shifts
  are clamped so no walk's top can rise above the turtle's clip.
* Layouter: `first_row_min_line_spacing_below_in_lpxs` is now honored (it was
  stored and hashed but never read). Resumable draws pass the current row's
  height plus wrap spacing plus the centering overhang, so a continuation's
  second row lands where centered content beside it starts exactly one wrap
  gap below the previous row — zero residual shift for uniform-height pills.
  Selection capture and the `<code>` wrap probe pass the same floor so every
  layout of a run shares one cache entry.
* Turtle: an anchored row returns its bottom forgiveness — the surplus its
  allocation extends below the risen content — and `turtle_new_line`
  subtracts it, keeping inter-row gaps uniform on both sides of anchored
  rows. A turtle's final row keeps its full extent so nothing clips at the
  bottom.

* uizoo: Html fixtures for line clamping and pill/text row alignment

Repro and regression fixtures mirroring a chat client's message surfaces:
max_lines clamping with inline <code> at several widths, atomic inline-widget
(pill) relocation/hold/ellipsis under a line budget, and timeline-style
pill+text rows at 9.3pt and 11pt metrics — including a CJK-titled pill and a
padding-0 first-row case that guards against clipped pill tops.

* Layouter: fill the last permitted row by grapheme when ellipsizing

When `max_rows` is set together with ellipsis truncation, word wrapping
would move a word that didn't fit off the final permitted row before
truncation ran, leaving that row ellipsized at the last word boundary
("@…") rather than at the last glyph that fits ("@quokka:…").

Word integrity is meaningless on a row that ends in an ellipsis, so lay
the final permitted row out by grapheme and let truncation cut at the
actual width limit.

* DrawText: account for walk margins when resolving a Fit max bound

The layout bound for a Fit-width walk with a relative max bound was the
raw resolved max, but the turtle's final width — and with it the clip
rect — gets clamped to that max minus the walk's outer margins. On top
of that, a Label passes the same margined walk to its inner DrawText,
re-applying those margins inside the turtle. Text laid out near the
bound therefore extended past the clamped clip, which sliced letters
off the end of untruncated text and cut the appended ellipsis down to
a single dot.

Subtract the walk's own horizontal margins from the resolved bound, and
when the enclosing turtle is itself an unresolved Fit with a max bound
(a Label sizing itself around this text), subtract that turtle's outer
margins too, so the layout bound matches the width the clip is actually
clamped to.

* uizoo: fixture for bounded pill labels ellipsizing at the width cap

Pill-like labels bounded to a fraction of the enclosing width, at
container widths that walk across the cap, plus one inline in an Html
flow. Truncation must always end in a visible trailing ellipsis, never
a bare mid-glyph cut.

* Layouter: keep word-boundary ellipsis on continuation rows

Grapheme-filling the last permitted row is wrong when that row is an
empty continuation: grapheme layout force-places a grapheme wider than
the row's remnant past the width limit with the truncation flag unset,
drawing overflowing text with no ellipsis. Finishing the row instead
truncates within bounds, so continuation rows keep the word-boundary
behavior.

* DrawText: scale the Fit max bound into layout units

The layouter works in unscaled units and row widths are multiplied by
font_scale on output, but the resolved Fit max bound was passed through
in physical units — the same mismatch max_layout_width_for_walk already
divides away. Any font_scale above 1 on a bounded label laid text out
past the clamped clip, reintroducing the sliced-tail bug the bound is
there to prevent.

* Turtle: add Base.Line, a Fit bound relative to the available line width

A static relative bound cannot express what an inline widget's inner
text actually has room for: that depends on the line the widget lands
on, its own leading geometry (icons, padding, spacing), and the
trailing insets after the text. Base.Line resolves a Fit max bound to
exactly that, with the enclosing line's flow selecting between two
measurements that are each final at the moment they are taken:

- A wrapping line can relocate the widget whole onto a fresh row, so
  the bound is what a fresh row offers. Content sized to it either
  fits where it is, or fits the row the widget is relocated to.
- A non-wrapping line — including one held non-wrapping by the
  inline-content clamp on the last permitted row — keeps the widget
  in place, so the bound is the remnant up to the line's right edge.

The measurement runs from the current turtle's content origin rather
than its pen, because a Fit max bound is evaluated twice: before the
content is laid out, and again in compute_final_size when the turtle
closes. A pen-relative measurement would collapse the closing clamp
to the leftover width and clip the content it just laid out.

* uizoo: fixtures for Base.Line-bounded pill labels

The four behaviors the line bound guarantees: a long name ellipsizing
at a narrow container's edge, a mid-line pill relocating whole to the
next row at full row width, a pill held on the last clamped row
squeezing visibly into the remnant, and a short name in a wide
container rendering untruncated.

* Turtle/DrawText: keep tiny Fit max bounds from slicing or inverting clips

A line-remnant bound can resolve to nearly zero when the last permitted
row is already full where an inline widget's text begins. Two guards
keep that degenerate range within the whole-glyph truncation contract:

- The Fit max clamp in compute_final_size floors at zero, so a bound
  smaller than the walk's margins cannot produce a negative width and
  an inverted clip (a pill rendering as bare chrome with no title and
  no ellipsis anywhere).
- DrawText treats a bound too narrow for the truncation ellipsis
  itself as no bound at all: the layouter appends the ellipsis glyph
  unconditionally, so a narrower clip would slice it open. Left
  unbounded, the text overflows honestly, letting an enclosing flow's
  inline-content clamp hide the widget and draw the ellipsis itself.
2026-08-10 12:30:01 +02:00
Kevin Boos
ed3eb88d4a
TextFlow: fix max_lines clamping and row alignment for wrapped text with inline widgets (#1160)
* Windows: fix DPI-change problems: freezing, blank windows, drag-n-drop errors

- win32: do_callback queues re-entrant events (WM_DPICHANGED pumped inside
  Present, nested WM_SIZE) instead of dropping them; the drops left dpi/geometry
  stale, the window blank, and hit-testing offset after display-scale changes.
- win32: WM_DPICHANGED applies the OS-suggested rect and returns 0; removed the
  duplicate-delivery compensations the lossless queue obsoletes.
- d3d11: no unbounded Present(1) block after a timed-out frame-latency wait
  (DO_NOT_WAIT + paced retry); DXGI errors log instead of panicking.
- dnd: Drag/Drop events carry WindowId and are remapped via dpi_override_scale,
  fixing drop-zone offset under UI zoom (Windows OLE, macOS external drops,
  Wayland internal drags).
- win32: packaged-build window icons load at native shell sizes via LoadImageW.

* TextFlow: enforce `max_lines` for inline code wraps, list items, and inline widgets

`max_lines` + `text_overflow: Ellipsis` under-counted or skipped visual lines
whenever anything but a plain text run started one, so rich content (rooms-list
previews, anything with inline `<code>` or mention pills) could overflow its
line budget or hard-clip without an ellipsis.

* TextFlow: the inline `<code>` forced wrap left `is_continuation` stale, so a
  wrapped code run was granted one extra row AND reported one fewer — a 2-line
  overflow per wrap. The wrap now refreshes the continuation state and row
  budget, is refused when no line remains, and its prediction probes measure
  an unclamped layout (a clamped probe always claimed "fits on one line").
* Layouter: a non-wrapping layout truncated by `max_lines` never reported
  `is_truncated` (detection was row-count only), so `max_lines: 1` labels
  hard-clipped mid-word instead of showing "…". Width overflow on the
  surviving last row now also triggers the ellipsis.
* TextFlow: list items charged two lines each (marker run + content run); the
  marker now shares one visual line with the item's first content run.
* Inline child widgets (mention pills, images) bypassed the budget entirely:
  Html now gates them on `is_content_truncated()`, charges the rows they open
  via `track_inline_content()`, and holds a widget that lands on the last
  allowed line in place (`Turtle::set_flow_wrap`) instead of letting the
  turtle relocate it onto a row the budget can't pay for. A held widget that
  still overruns the line is hidden behind a tracked clip and replaced with a
  drawn "…" — the same contract text truncation has.

* TextFlow: fix row alignment and spacing for wrapped text beside inline widgets

On the single-batch text path (Windows/Linux), a multi-row text run never told
the turtle where its internal row boundaries were: every walk since the last
boundary — a bold sender on one visual row, a mention pill on the next — was
centered by `finish_row` against one merged row. Text sat a few px off the
pill baselines, rows around wrapped runs squeezed or overlapped, and an
up-centered pill on a padding-less first row rose above the clip and lost the
top of its rounded corners.

* DrawText: the single-walk path now allocates per visual row and runs
  `turtle_new_line_with_spacing` at each internal boundary, keeping one glyph
  instance batch. It emits a first-row walk owning the run's align entries and
  a last-row walk with an empty range, so the run is never shifted twice.
* Turtle: `FinishedWalk` carries a `RowAlignRole`. A wrapped run's rows are
  immovable (their glyphs share one batch): rows holding its visible text are
  an `Anchor` — `finish_row_center` centers every shiftable walk on the
  anchor's line, shifting a taller pill UP onto the text — while a
  whitespace-only first row (a continuation that wrapped on a leading space)
  is `Fixed`, so it cannot anchor a row to an invisible line. Anchor shifts
  are clamped so no walk's top can rise above the turtle's clip.
* Layouter: `first_row_min_line_spacing_below_in_lpxs` is now honored (it was
  stored and hashed but never read). Resumable draws pass the current row's
  height plus wrap spacing plus the centering overhang, so a continuation's
  second row lands where centered content beside it starts exactly one wrap
  gap below the previous row — zero residual shift for uniform-height pills.
  Selection capture and the `<code>` wrap probe pass the same floor so every
  layout of a run shares one cache entry.
* Turtle: an anchored row returns its bottom forgiveness — the surplus its
  allocation extends below the risen content — and `turtle_new_line`
  subtracts it, keeping inter-row gaps uniform on both sides of anchored
  rows. A turtle's final row keeps its full extent so nothing clips at the
  bottom.

* uizoo: Html fixtures for line clamping and pill/text row alignment

Repro and regression fixtures mirroring a chat client's message surfaces:
max_lines clamping with inline <code> at several widths, atomic inline-widget
(pill) relocation/hold/ellipsis under a line budget, and timeline-style
pill+text rows at 9.3pt and 11pt metrics — including a CJK-titled pill and a
padding-0 first-row case that guards against clipped pill tops.

* Layouter: fill the last permitted row by grapheme when ellipsizing

When `max_rows` is set together with ellipsis truncation, word wrapping
would move a word that didn't fit off the final permitted row before
truncation ran, leaving that row ellipsized at the last word boundary
("@…") rather than at the last glyph that fits ("@quokka:…").

Word integrity is meaningless on a row that ends in an ellipsis, so lay
the final permitted row out by grapheme and let truncation cut at the
actual width limit.

* DrawText: account for walk margins when resolving a Fit max bound

The layout bound for a Fit-width walk with a relative max bound was the
raw resolved max, but the turtle's final width — and with it the clip
rect — gets clamped to that max minus the walk's outer margins. On top
of that, a Label passes the same margined walk to its inner DrawText,
re-applying those margins inside the turtle. Text laid out near the
bound therefore extended past the clamped clip, which sliced letters
off the end of untruncated text and cut the appended ellipsis down to
a single dot.

Subtract the walk's own horizontal margins from the resolved bound, and
when the enclosing turtle is itself an unresolved Fit with a max bound
(a Label sizing itself around this text), subtract that turtle's outer
margins too, so the layout bound matches the width the clip is actually
clamped to.

* uizoo: fixture for bounded pill labels ellipsizing at the width cap

Pill-like labels bounded to a fraction of the enclosing width, at
container widths that walk across the cap, plus one inline in an Html
flow. Truncation must always end in a visible trailing ellipsis, never
a bare mid-glyph cut.
2026-08-05 19:35:06 +02:00
Admin
623ee745e4 Arcade render: packed vertex formats — instance -27%, vertex -62%
Quest is vertex-bandwidth bound, so this is the measured headline:

  cube instance             176 B -> 128 B  (-27%)
  skinned character vertex   64 B ->  24 B  (-62%, re-uploaded EVERY frame)
  shadow mesh vertex         64 B ->  24 B  (-62%)

The Knight went 238 KB/frame -> 89 KB/frame: CPU skinning re-uploads the
whole buffer each frame, making it the largest recurring saving available.
Instance sizes are read from the compiled shader (RenderStats::
instance_floats), not counted by hand. The instance win was pure
duplication: sun_color/sun_sky/sun_ground/fog_color are identical for every
instance in a batch — 12 floats per cube — and moved to uniforms.
fog_density stayed per-instance because shadows switch it off individually.

Unblocked by adding geom.GameMeshVertex in draw/geometry_gen.rs and making
the existing pack_pair_f16/pack_unorm8x4 public, rather than writing a
second f16 rounding implementation that could drift from the first.

Three constraints found, worth keeping:
- Vertex attributes here are f32-ONLY. Compression means bit-packing into
  f32 lanes; unpack2f16/unpack4u8 are builtins on every backend
- Pod vertex structs need flat f32 fields, not Vec3f — std140 pads a vec3
  to 16 B and the repr(C) size assertion fails at runtime
- In the shader language `let` is immutable and helpers can't be forward-
  referenced, so the octahedral decode uses branchless step(0,v)*2-1: the
  sign() builtin returns 0 at 0, which would collapse the fold on
  axis-aligned normals

Tape BYTE_IDENTICAL; captures verified after each conversion (shadows
unchanged by packing, Knight correct with packed normals/UVs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 08:41:31 +02:00
Admin
9c514b601b Map: draw_svg last packed read (clip radius)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
3d5c54f2f3 Map: packed layout v2 — stroke_dist f32 (f16 overflow rainbowed long roads), shape_id+param0 pair, param3+clip_radius pair; draw_svg packed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
63b48a0de5 Map: packed vertex format live — 19 f32 (76B) -> 12 slots (48B) per vertex
VectorVertexPacked: f16 pairs (uv, dist+shape, param0+3, param1+2, clipr)
and unorm8x4 color bitcast into f32 slots, unpacked by the new shader
intrinsics; positions, stroke_mult sentinels, param4 icon composite,
param5 depth ladder and zbias stay f32. Emitters unchanged — one packer
runs at upload (map tiles + generic vector picture path). -37% vertex
fetch bandwidth across every map buffer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
fea68f8c22 Map: labels pan/zoom on the GPU — cam_scale/cam_shift uniforms, glyphs emit in cached space
The cache-hit path re-emitted every glyph CPU-side with the pan shift
baked into vertices; under tilt that visibly trailed the tile geometry
(which pans purely by uniform). The pan/zoom delta now rides two new
uniforms applied before the existing camera-delta matrix, in the same
frame as map_offset — labels physically cannot lag the map. Pin-interior
text keeps constant px via a billboard instance flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
a7a11c670b Map: bake deck shadows into the shadow section — the last unbaked boolean
The deck-shadow dissolve ran after the bake sink's early return, so no
cut ever contained it and every dz-covered street tile paid ~59ms at
runtime (found via emit-section fencing: ebuild=59). Extracted
dissolve_deck_shadows; the sink concatenates it into the baked shadow
shapes (concat == today's two separate emits exactly); a shadow HIT now
gates the runtime block off. Also: face-arm/stroke-arm/events laps, icon
template cache, batched expanded writer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:43 +02:00
Admin
3aa81131b7 Map: way-bbox grid for endpoint join passes + batched 19-float vertex appends
Replaces the remaining linear bbox scans in grade-through/flush-ends with
a 16-unit cell grid (candidates stay in ascending way order, so
first-match semantics survive); append helpers now extend_from_slice one
19-float record with reserved capacity. 7-tile rz16 sweep + z12 byte-equal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 17:48:42 +02:00
Kevin Boos
8e4573ee3b TextFlow: fix inline <code> spans that are too low (beneath text baseline) (#1156)
* TextFlow: fix inline `<code>` spans sagging below the baseline

finish_row_center centered every walk by its own height, so a code run's
shorter walk got a larger downward shift than the surrounding prose under
`RowAlign.Center`. That undid TextFlow's baseline_shift: inline `<code>`
spans sat a few px below the line's baseline, and their descenders poked
out of the bottom of the code box.

* FinishedWalk now carries an optional `align_height` that text runs set
  to their line style's height, so every text run on a row receives the
  same centering shift and stays on the baseline. Also fixes sub- and
  superscripts drifting under `RowAlign.Center`.
* The per-style metrics probe now caches descenders too, since we need
  the full line height (ascender + descender) to compute `align_height`.

* Html/Markdown: make the fixed/code font size scale configurable

Replaces the hardcoded 0.85 `FIXED_FONT_SIZE_SCALE` consts with a
`fixed_font_size_scale` live property on TextFlow (default 0.85), so
apps can tune how much smaller `<code>` text renders than the prose.
2026-08-01 08:25:00 +02:00
Admin
1f6b12fe67 Add clickable themes: Circuit City preset + emissive roads + night trees
Circuit City (shiny.md showcase) ships as a built-in third theme slot on
MapView (style_circuit, theme_select 0/1/2, set_theme with the
keep-stale restyle so switching cross-fades per tile as rebakes land):
near-black ground, charcoal volumes with gloss 1.1 sheen, dark
reflective water, and every road an emissive amber filament weighted by
class. Emissive plumbing: MapRoadRule.emissive -> StrokePassStyle ->
PaintGroup/PaintFace -> road-union center faces carry MAT_ROUTE_GLOW +
strength in param1, and the shader pushes the line color white-hot
behind the route_glow gate. Gloss is now a theme value (ShinyConfig).

Street-tree canopy/trunk colors become theme-driven (tree_canopy /
tree_trunk fill rules) so dark themes get night trees. The example's
Layers panel grows Night/Circuit City rows behaving as radios.

Shadow geometry hardening from live review: silhouette simplification
floored at 0.35 units, and a needle filter drops hair-thin boolean
slivers (area/perimeter < 0.05) that read as dark pins at building
corners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:10:52 +02:00
Admin
3d25a9afa0 Add SceneSun + material channels + baked AO (shiny.md phase 1)
One global light (draw/src/scene_sun.rs): SceneSun POD shared by the
tile bake, ball lighting and terrain hillshade, replacing three
hardcoded sun rigs. ShinyConfig carries every shiny.md toggle; it rides
CompiledMapTheme so bake-flag flips reuse the style-epoch restyle and
stale tiles stay drawable (MapView::update_shiny).

T1: shape-0 geometry now carries its surface normal in param1/2 and a
material id in param3 (walls/roofs/water/canopy/green) — channels were
free, no vertex-format growth. DrawMapVector dispatches per-material
pixel effects behind uniform gates that default off (legacy frame when
off): T4 water noise sheen + sun glint, T4b building specular sheen
with heading-rotation highlight sweep, T5 canopy clump noise + rim and
green-area patchiness.

T2 (first slice): wall vertical AO gradient (ground-contact darkening)
and roof-edge parapet AO strips, baked at zero GPU cost behind bake_ao.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 00:10:52 +02:00
Admin
a37db4228c Fix unified road elevation and antialiasing 2026-07-30 10:42:24 +02:00
Admin
241e5d2e2b map: geometric vertex-alpha AA fringes, strict grounded cover, boot-view + cam readout
- AA fringe coverage now rides per-vertex premultiplied color (inner =
  face color, outer = transparent) with full shader coverage: the ramp
  interpolates across the whole skirt at any magnification, where the
  fwidth-normalized shader ramp pinned to one device pixel and left
  magnified tilt-foreground stairs visible. Skirt sits half a ladder step
  above its face so the up-screen half wins its depth tie.
- Straddling (partially lifted) rings stay visible in both level parts
  but no longer join the grounded cover: a ramp segment spanning the
  threshold was cutting the road underneath along its whole length —
  the under-the-overpass hole at the benchmark interchange.
- /tmp/mp_start_cam boots the app at a saved viewport; the map draws its
  own '@cam lon lat zoom rot tilt' readout bottom-right so any screenshot
  is reproducible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:42:24 +02:00
Admin
fff629bb3f map: bridge dz solver+AHN bake, base_dz transcode, road overlay unifier
Bridge elevation (bridge.md M1+M2): tools/map_tiles bridge-bake solves
per-vertex road/rail dz over the OSM graph (crossing clearance, grade
ramps, deck holds, junction consensus) with AHN DSM-DTM measured deck
profiles (BigTIFF reader in geodata, WGS84->RD), baked as bridge_dz +
per-base-tile base_dz (L/F/P join to exact renderer geometry). Renderer
joins dz through parse into TileWay.dz; strokes/arrows/fills lift off
their own profiles; tunnels never deck.

Road overlay unifier: painter's algorithm as geometry — per-way segment
rects + vertex discs, i_overlay top-down subtraction cascade into
DISJOINT faces (overlay_paint_groups), triangulated once; flat render is
pixel-equal to paint order (proven 0-diff on the abstract unit case) and
tilt cannot reorder it. Legacy strokes interleave by rank; plazas join
the cascade at true alpha. @roads 0/1 A/B toggle, @abtest headless
CPU-raster diff harness (1.50% vs 2D reference on the Raampoort tile),
@cam debug camera, bridge_eval.sh aerial-vs-render loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:42:24 +02:00
Admin
85515cea35 map: 3D terrain displacement, Europe-wide DEM, landcover drape, wind layer
Terrain gets real z in tilt mode: a displaced 288x216 surface mesh plus
per-vertex ground lift for all tile geometry (roads/fills/buildings/icons)
via vertex-stage sample_lod of a terrarium-packed elevation texture, with
labels riding the same ground through a lift-aware camera delta. Depth
domains rescale per frame to stay inside the -24 budget; road passes get
a relief-scaled clearance over the surface so cities keep correct
street-vs-building order. Regional zooms (<z14) drape landcover colors
into the hillshade (full-res MVT rasterization, treeline fade) instead of
lifting km-scale polygons that cannot follow relief.

Terrain build now covers EV-trip Europe (lat 43..58, lon -5..17, 8.8 GB
GLO-30 z6-12) with sign-safe Copernicus stems, transparent no-data, and
an Alpine rock/snow ramp. Terrain requests render 4096x3072 over ~3
viewports and only re-render on real movement.

Also: NOMADS GFS wind particle layer synced to the weather clock with 3D
altitude, rain cloud deck lift, stuck-tile fixes (expiring missing set,
decode-failure backoff, restyle placeholder purge), 78-degree tilt
ceiling, and park fill z-fight fix via widened micro-depth ranks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:42:24 +02:00
Admin
1b0decc6c9 map: charger pin redesign, upright/billboard labels, 3D trees, exits
- Tesla-style droplet pins anchored at the tail tip (rotate around the
  exact site point in tilted views); red exclusively Tesla, amber DC,
  blue AC; Tesla pins show stall count, others peak kW
- in-pin text through the normal text renderer: billboard-anchored
  glyphs (anchor scales with the map, glyph size constant), exempt from
  label collision/repeat culling, no halo
- upright camera-delta labels: straightened labels (place names, POIs,
  brands, pin text) translate with the rotation gesture but stay
  horizontal — no more rotate-then-snap on regen
- per-icon zoom floors baked in vertex param4 + live icon_zoom uniform:
  stale deeper-bucket tiles never flash markers on zoom-out (param4 is
  lift-height only for non-icon shapes — icons stay on the ground)
- motorway exit labels (street_labels_points): carto-red name + ref
- 2D/3D mode: tilt gesture syncs app state (TiltChanged action), tilt
  release near-flat settles to exact 0, mode flip re-bakes tiles
  (extrusions appear/disappear without a zoom nudge)
- Simple 3D Buildings: building:part volumes with min_height bases;
  outlines containing parts flatten to footprints; famous buildings
  with tourism=attraction extrude instead of dying as attraction fills
- little 3D trees in tilt mode: crossed trunk quads + sphere-slice
  ball canopy (architecture-model proportions)
- searchdb/mbtiles support work: reach-based distance ranking, direct
  tile lookup + writer ordering

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:02:44 +02:00
Admin
1b1d908d2a map: exact camera-delta matrix — labels land where the regen lands
The snap: tilt does not commute with rotation. The placement maps world
points rotate-then-y-compress, so the true delta between the cached
camera (r0,t0) and now (r1,t1) is S(t1)*R(d)*S(1/t0) — a general 2x2
matrix, not the rotate+scale previously approximated. The shader takes
the full matrix; the pan shift pre-inverts through it so pans during
rotation land correctly. Between-frame tracking now converges on the
exact re-place result — no more rotate-snap in 2.5D.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:13:58 +02:00
Admin
f8273f52d1 map: GPU camera-delta for labels; brand text draws over the pins
- DrawRotatedText gains cam_cs/cam_sn/cam_tilt/cam_pivot uniforms: while
  the camera rotates or tilts between re-places, ALL placed glyphs spin
  and squash about the view pivot on the GPU — whole-quad transform, so
  orientation follows position for free; the CPU per-frame rotated copy
  is gone and the async re-place trues up with identity uniforms
- label plans carry a post-icon flag: in-pin digits' phase now also
  hosts the charger brand text, which was drawn before the symbol pass
  and hid BEHIND its own pin

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:10:03 +02:00
Admin
9e7b3b8576 map: GPU re-expandable strokes — zoom-invariant widths on stale tiles
Stroke geometry now bakes the centerline anchor per vertex (svg
tessellator emits anchors; offsets ride in param1/2, width-growth
class in param3, shape_id+100 marks expand mode — the shared vertex
format is unchanged). The map vertex shader re-expands each stroke
with a per-class width correction (regular roads, thin paths/rails,
waterways, constant-px building outlines) computed from the tile's
styled bucket vs the live fractional view zoom.

Tiles rendered at a stale zoom bucket keep the exact widths a fresh
restyle would produce, through the whole gesture: no fat roads while
zooming in, no width snap when the rebuild lands, dash dots and
building outlines stay crisp. Fading outgoing generations carry their
own bucket so they correct too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 20:39:23 +02:00
Admin
65814d02dc map: labels track zoom via affine cached draw; no more vanishing
Cached label placements were redrawn shifted by the raw map-offset
delta; during a zoom the offsets change by world-scale amounts, flinging
the cache thousands of px off-screen (labels vanished just off max zoom
and stayed gone while idle because the re-place rate limit counted
frames). Now: cached glyphs draw through the exact affine screen
transform s*k + (off_new - off_old*k) with glyph size scaled, so labels
track the map mid-gesture; the re-place limit is wall-time based; and
the settle timer re-arms until a budget-truncated label pass completes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:34:57 +02:00
Admin
364b4fd575 map: frame-time instrumentation + label scan/draw cost fixes
- perf log: draw_walk aggregates total/geometry/label ms, max frame,
  inter-frame gap and full-placement count per 240 frames into
  local/map_perf.log
- labels precompute name_key + tile-local bbox at tile build; the
  per-frame scan does no allocation and rejects offscreen labels with
  two point transforms before any path work (an overzoomed tile holds
  ~16K labels, mostly offscreen)
- batched glyph path no longer runs update_draw_vars per call (the open
  batch already did); halo underdraw 8 -> 4 diagonal offsets, halving
  glyph volume
- fill clip overlap shrunk to ~1 screen px: the wider double-drawn
  strip let the later tile's land paint over the earlier tile's
  buildings (pale band at tile edges at high zoom)
- point local mbtiles back at noord-holland-shortbread-1.0.mbtiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 16:57:59 +02:00
Admin
ad22ee7ba1 map: label performance pass — batch, pan-cache, shape-cache
- one glyph instance batch per frame (begin/end_glyph_batch on
  DrawRotatedText): every glyph previously opened and closed its own
  aligned-instance batch, x9 with the halo underdraw
- label placement cache: pan-only frames redraw the previous placement
  shifted by the pan delta (like the tile geometry) instead of
  re-scanning up to 16K labels and re-shaping/re-colliding ~1-2K; full
  re-placement only after 48px of pan, zoom change, or tile updates
- shaped-run cache keyed by (text, quantized font_scale) so periodic
  re-placements stop re-shaping every string every time

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 16:47:40 +02:00
Admin
cba9e6b851 map: zoom-constant vector POI symbols + green-area ranking fixes
- openstreetmap-carto symbol SVGs (CC0) tessellated once at final screen
  size and baked into a 4th tile-cached vector buffer; vertices encode
  anchor + screen-px offset, the map vertex shader (shape id 20) adds the
  offset after the map transform so symbols keep constant pixel size at
  every zoom — pure GPU vector, no raster atlas
- POI symbols colored by class (orange food, purple shops, brown culture,
  red health incl. pharmacies), labels shifted below the symbol
- symbols gated to z17+ and hidden immediately on zoom-out even while
  stale higher-bucket tiles are still displayed
- green areas (leisure/grass/forest/gardens) rank above generic landuse:
  they share the shortbread land layer with huge residential polygons and
  lost to protobuf feature order (Bellamyplein rendered gray)
- cache-only fallback when the local mbtiles source is missing (e.g.
  while a larger extract downloads)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 16:34:57 +02:00
Admin
d266571ab6 map: tile-seam overpaint fixes, label halos/colors/stability, z19
- per-end line caps (Tessellator::stroke_ends): butt at tile-clip cuts,
  round at true ends — road cap discs no longer stamp over neighbor
  tiles' tram tracks and roads at seams
- fill polygons clipped to their own tile square (Sutherland-Hodgman);
  a tile's MVT buffer fragments no longer overpaint the neighbor;
  building outlines skip segments running along the tile cut
- stroke clip padding 3px (under the generator buffer) so boundary
  cuts are detectable
- white label halos (8-offset underdraw, theme label_halo color)
- carto POI label colors (orange food, purple shops, brown culture,
  muted house numbers) from shortbread poi attributes
- label placement hysteresis across frames (no flicker while panning)
- street_polygons fill ranked below sites/parks (Bellamyplein-class
  plazas no longer cover the park inside them)
- max zoom 19 with width stops for z18/19

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:53:36 +02:00
Admin
691021129b map: rebuild MapView to openstreetmap-carto quality
- semantic fill paint order (land/sites/water/buildings/street areas);
  raw MVT layer order was painting land over 6,880 buildings per tile
- tile-local f64 coordinates + per-tile offsets; fixes f32 web-mercator
  quantization (0.25px vertex, 2px shader ULP at z17)
- casing/center stroke buffers split; fills -> all casings -> all centers
  across tiles (carto roads-casing/roads-fill order)
- per-view-zoom-bucket restyling with carto width stops; stale buckets
  stay drawable during rebuild; screen-space AA/tolerance
- carto palette, building outlines z15+, bridge decks, tram/rail from
  streets layer above road centers, thin-path slow width growth
- house numbers (addresses) and shop names (pois) as point labels z16+
- MVT-correct absolute ring winding for multipolygon classification
- visible_tile_keys divides viewport by overzoom (was 64x over-request)
- text: prepare_single_line_run scales glyph size+raster with font_scale;
  draw_path_glyphs pins ambient font_scale (letter-spaced labels bug)
- script derive: cast numeric field defaults instead of .into() fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 15:34:44 +02:00
Patrik Husfloen
04a8811618 feat(draw): add sdf.arc_to circular-arc path segment (#1142)
Replace the abandoned arc2 colour-stub with a real path primitive: the
distance to a bare circular-arc CENTERLINE (no baked thickness), so it
chains after move_to/line_to and is covered by a single stroke(w) -- unlike
arc_round_caps/arc_flat_caps which bake their own width. Angles in radians,
0 = +x axis, counter-clockwise positive; last_pos advances to the arc end so
a following line_to connects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

feat(draw): add sdf.arc_to circular-arc path segment
2026-07-23 12:14:24 +02:00
Kevin Boos
f40cb7b319 Honor X & Y align values for deferred Fill child widgets (#1143)
A `Fill` child under Flow::Right (or `height: Fill` under Flow::Down) is
deferred and given the row's full slack up front, so end_turtle's align
step takes the deferred branch, which distributes width/height to fills
and drops main-axis align entirely. That's correct when a fill consumes
its whole slot, but a fill that draws narrower than its slot (e.g. an
Image that aspect-fits, or `Fill{max: N}` capped below the container)
leaves real slack that then anchors to the start regardless of align.

Add row_align_x_shift / col_align_y_shift: reclaim align * (inner -
actually_drawn) in each deferred branch. They early-return 0 when align
is 0, the inner size is unknown, or the content fills, so the only
behavior change is deferred-flow containers that set main-axis align and
hold an under-filling Fill child.

Add examples to the uizoo "Layout Demos" tab that proves they boht work.
2026-07-23 12:14:04 +02:00
Kevin Boos
d1efac1892 New unified kinetic scrolling. Vastly improve draw shaprness on low-res screens. Fix rendering and text perf issues on CPU+GPU (#1127)
* Fix rendering, gradient, sampling, etc issues on older GPUs

* Fix bug in `box_y` sdf function, which caused gradients to be split
  into two bands incorrectly. Mostly a problem on lower-res screens.
* Use per-texture filtering instead of GL sampler objects on Linux,
  especially for Mesa drivers that ignore min filter samplers.

This should help prevent blocky/pixellated things like emoji/avatars

* Improve rendering sharpness on low-DPI screens (icons, emoji, images)

On 1.0-DPI screens, emoji/SVG-icons/avatars were minified without
adequate sampling and SVG AA was sub-pixel, producing blocky/aliased
output. This reworks each path and adds optional full-window SSAA.

SVG icons (device-aware AA + round caps):
- draw_svg.rs/draw_vector.rs/render.rs: size the fill & stroke AA
  fringe and the curve-flatten tolerance in DEVICE pixels (≈constant
  regardless of icon size), so edges resolve via the analytic
  d/fwidth coverage and curves stay smooth at any scale; re-tessellate
  on scale change.
- triangulate.rs: thread the flatten tolerance through path fill/stroke.
- tessellate.rs: emit round caps as a solid disc (u=0.5) instead of a
  radial fade that collapsed to a square at small sizes.
- widgets/icon.rs: don't clip the Icon to its Fit bounds, so round
  caps that extend past the box render fully instead of being sheared.

Emoji:
- glyph_raster_image.rs/rasterizer.rs: rasterize color emoji near the
  on-screen size with an alpha-weighted box downscale (geometric-mean
  scale factor) instead of the font's native PNG strike.

Images / avatars (mipmaps):
- image_cache.rs/texture.rs/draw_list.rs/lib.rs: optionally emit a CPU
  mipmap chain (VecMipBGRAu8_32) for non-animated images so minified
  avatars/thumbnails sample cleanly. Env-gated MAKEPAD_IMAGE_MIPMAPS;
  default on for GL on Linux.
- metal.rs: real per-level mip upload. d3d11.rs/vulkan.rs/web_gl.rs:
  safe single-level fallback (no crash; real mips TODO).

Full-window supersampling (optional):
- window.rs: render the whole UI into an offscreen target at
  MAKEPAD_SUPERSAMPLE× device resolution and downscale-resolve into the
  window. Default 2×, env-tunable, 1× disables. Modeled on the existing
  GaussStack render-to-texture path.

* don't unconditionally enable supersampling SSAA of 2x by default

it's too expensive and too slow for most older devices

* cleanup, reduce comment verbosity

* improve SVG anti aliasing

* Windows: fix laggy/juddery scroll performance

- Pace the render loop to the display refresh using a DXGI frame-latency
  waitable object and present with vsync, replacing the free-spinning,
  uncapped Poll loop that caused uneven scroll cadence.
- Coalesce consecutive WM_MOUSEMOVE messages and paint once per loop pass
  to stop the judder when moving the mouse during fling deceleration.
- Cache get_dpi_factor() and the WM_NCHITTEST WindowDragQuery result to
  avoid per-mouse-move GetDeviceCaps syscalls and widget-tree hit-tests.
- Throttle XInput/DirectInput polling of empty/disconnected controller
  slots, which was stalling the UI thread.
- Rework the momentum fling to a native exponential model with a
  frame-interval EMA, and stop the tail auto-scroll from fighting an
  active fling/drag.
- D3D11: update the glyph atlas and image textures in place via
  UpdateSubresource instead of recreating them on every change, and
  spread D3D11 shader-object creation across frames.
- Slug atlas: only force a full re-layout on a width change; append rows
  on height growth.

* Windows: correctness fixes from review (off the scroll hot path)

- d3d11: close the DXGI frame-latency waitable HANDLE in Drop (it was
  leaked once per main-window lifecycle and the field comment was wrong);
  keep popup swap chains at frame-latency 1; track the waitable-swapchain
  flag for ResizeBuffers instead of inferring it from the handle; present
  without the vsync interval during a live resize.
- windows.rs: poll game input on the idle signal tick so a gamepad button
  can be serviced while the app is otherwise idle.
- win32_window / window: invalidate the WM_NCHITTEST / WindowDragQuery
  caches on window move and on a caption relayout, with a generation
  counter guarding against a reentrant invalidation being clobbered.
- windows_game_input: detect a controller already plugged in at launch via
  a one-shot full scan on the first poll, probe slot 0 (Player 1) first,
  and offset the DirectInput enumeration so it never stacks with the
  XInput probe.
- comment/doc corrections.

* Image cache: accept any Arc<D: AsRef<[u8]> + ?Sized> for async image data

The load_image_from_data_async family required Arc<Vec<u8>>, forcing callers
that already hold the bytes as Arc<[u8]> (e.g. a content-addressed media cache)
to copy the whole buffer via .to_vec() just to satisfy the type. Generalize the
data parameter to Arc<D> where D: AsRef<[u8]> + ?Sized, so those callers can pass
their existing Arc by refcount-clone with no byte copy. The decode path only ever
borrowed the bytes (&[u8]), so this is purely a signature relaxation; existing
Arc<Vec<u8>> callers are unaffected (D = Vec<u8>).

* Linux: GL glyph-atlas in-place texture update + X11/Wayland mouse-move coalescing

* Scroll: unified fling model + native trackpad momentum deceleration

Share one kinetic-scroll model between PortalList and ScrollBar (and thus
ScrollXView/ScrollYView/ScrollXYView) via a new widgets/src/scroll_motion.rs:

- Touch-drag flicks use an iOS-style exponential self-decay, frame-rate
  independent via a per-frame integrator with dt smoothing.
- Trackpad scrolling applies the OS momentum directly while fast (responsive,
  full native speed), then hands off to a gentler self-decaying tail once it
  slows past a threshold, so the deceleration is longer and smoother than the
  OS's short, choppy tail. Handoff is seeded at the current speed for a
  continuous transition; the seed is clamped against degenerate event timing.
- Add ScrollPhase to scroll events, mapped from NSEventPhase/momentumPhase on
  macOS and wl_pointer AxisStop on Wayland; None elsewhere (wheels/X11/Windows
  behave as before). MAKEPAD_RAW_TRACKPAD_MOMENTUM=1 bypasses the smoothed tail.
- A press catches an in-progress fling (stops the scroll, consumes the press so
  it doesn't also activate a child), matching iOS/Android/macOS.

* Shader codegen: prefix Metal/WGSL locals to avoid reserved-word collisions

The Metal/WGSL backends emitted user-declared shader locals verbatim, so a
local named after a reserved type keyword (e.g. `half`) produced invalid
shader source and failed to compile at runtime. Prefix them with `l_` like the
HLSL/GLSL backends already do.

* TextFlow: don't panic on unbalanced HTML close tags

end_code/end_quote unwrapped the area stack, so a stray `</pre>` or
`</blockquote>` in untrusted content (e.g. a chat message) panicked. Return
early instead, and drop a vestigial per-list-item area-stack push that leaked
an entry and could hand a stray close tag the wrong block's area.

* Scroll: expose fling decel, handoff threshold, & tail-decel as `#[live]` fields

This allows app devs to override the scroll feel per-widget in the DSL,
or globally by overriding the base widget's defaults — verified that a DSL
override takes effect.

* Windows: fix frame pacing, paste crash, and wheel input backlog

- Wait on the frame-latency waitable right before each window's vsync
  present instead of on every Paint, so input no longer stalls behind
  waits that have no matching present. Drain leftover credits after a
  live resize.
- Pasting when the clipboard has no text no longer panics.
- The poll loop now handles up to 32 messages (2 ms) per frame and
  merges consecutive mouse-wheel messages, so fast wheels can't build a
  backlog that keeps scrolling after the gesture ends. Sleep 1 ms when a
  frame presents nothing so animation polling doesn't spin a core.

* Image: don't build unused mip chains; fix stale images in recycled widgets

- Only build the CPU mip chain when a backend actually uploads it
  (Metal, behind its env var), and build it on the decode thread instead
  of the UI thread. Linux GL still gets its mipmaps via glGenerateMipmap
  and now retains less CPU memory per image.
- Recycled Image widgets no longer show the previous item's image or
  apply an old decode result. Placeholder textures set via set_texture
  (like blurhashes) stay visible while the real image decodes, and a
  failed load clears the widget instead of leaving old content up.

* Widgets: avoid needless caption redraws; cheaper PortalList height tracking

- Label::set_text does nothing when the text hasn't changed, and the
  window caption title is only synced when it actually changes, so mouse
  moves and animation ticks no longer redraw the whole window every
  event. The caption centering padding requests its own redraw now.
- PortalList records item heights only when new or changed, and only
  re-applies the default height after it drifts by half a pixel, so big
  lists don't walk every unmeasured item on every scroll frame.

* Text: cache layouts of long texts; stop cloning glyph outlines every frame

- The layout cache now accepts texts of any length (long messages and
  code blocks used to re-layout on every scroll frame). It is a real LRU
  with a byte budget on top of the entry cap, and texts drawn in the
  current frame are never evicted, so one heavy frame can't thrash the
  cache into a permanent miss cycle. The shaper cache is LRU now too.
- Glyph outlines are shared via Rc, so drawing a cached glyph no longer
  copies its command list, and outline complexity is computed once when
  the outline is built instead of every frame.

* Html: fix stale links/spans in recycled widgets and <details> renumbering

- set_text only rebuilds when the content actually changed, so a
  recycled link can't open the previous message's URL, and re-setting
  identical content keeps the user's <details> open/closed state.
- Custom widgets and <details> are keyed by their node index instead of
  a visit-order counter, so toggling a collapsed <details> can't
  renumber the widgets after it and rebind them to the wrong nodes.
  item_with_scope also recreates its widget when the template changes.

* Linux: fixed-distance wheel scrolling; Wayland frame-callback pacing

- Wheel scrolling moves a fixed 60 px per detent on X11 and Wayland
  instead of a timing-based guess that flipped between 12 px and 240 px
  depending on how events batched. Wayland reads real detent counts via
  AxisValue120 (wl_seat v9, with AxisDiscrete as the older fallback) and
  maps keymaps MAP_PRIVATE as v7+ requires. Touchpads are unchanged.
- Wayland frames are paced with wl_surface frame callbacks and swap
  interval 0, so redrawing a hidden or minimized window can no longer
  hang the whole app inside eglSwapBuffers (compositors withhold frame
  callbacks for hidden windows). Windows with a callback in flight skip
  presenting and stay dirty; X11 keeps vsync exactly as before.

* Text: bigger layout cache budget, reclaimed at the end of each frame

A maximal ~60 KB message lays out to roughly 4 MB of glyphs, so the 4 MB
budget couldn't hold even one alongside a normal screen. Raise it to
16 MB, and run eviction at the end of every frame so memory over the
budget is freed one frame after its content leaves the screen, instead
of lingering until some later layout happens to insert a new entry.

* Fix oversized uniform slices: the array lengths were in bytes, not f32 elements

* Wayland: flush buffered mouse motion before scroll events, and drop motion for closed windows

* GL: fall back to non-mipmapped filtering when glGenerateMipmap fails on strict GLES3 drivers

* Text: bucket emoji raster scales so zooming reuses atlas slots instead of re-decoding every step

* Image: add has_content() and record texture provenance on cache-hit loads too

* Scroll: native trackpad momentum, Chrome-model bounce; presses catch motion, never click children

* Dock: redraw the newly selected tab immediately when the active tab is closed

* Html: standard link colors with pressed precedence; skip re-parsing unchanged text; color setters

* Image: don't redraw on cache-hit loads of the already-bound image (per-draw reloaders looped forever)

* Scroll: log macOS momentum-end phase bits to check Cancelled (touch-cut) vs Ended (natural fade)

* Scroll: momentum state machine; flicks survive pagination; edge sentinels & once-per-frame actions

* Scroll: remove the MAKEPAD_SCROLL_DEBUG diagnostics

* Scroll: time-based fling velocity window; pointer fan-out guard; parked flings survive pagination

* PortalList: optional reached-start/end margins (Some(0) default); repositioning re-announces the edges

* Linux/Wayland: honor UI zoom across window resizes; scale caption bar with zoom

Keep the wayland-side window geom in native units so the zoomed dpi isn't
read back as "native" on the next Configure — UI zoom no longer resets or
flickers on maximize/tile. Also let the caption bar height scale with the
zoom (pin to native only on macOS, where the buttons are OS traffic lights).

* Windows: D3D11 fixes for drawlist mgmt

trying to help with the `new_batch` bugs

* draw_list.rs — `set_zbias` now returns whether it changed
* d3d11.rs — uploads draw_call_uniforms on the given condition:
  `uniforms_dirty || zbias_changed || buffer.is_none()`
  and the zbias advance is hoisted above the early-continues

* Scroll: hard flicks carry farther and boost on re-flick; iOS-style touch rubber band with per-edge bounce gating; Android fling spline behind a flag

* Splitter: redraw both pane subtrees on drag; cached views keep fresh fixed sizes in the dirty check

* iOS: re-deliver window-geometry changes dropped by re-entrant UIKit callbacks; Init only ever from the first draw

* final cleanup fixes. Ensure all examples, experiments, `studio` all work

* Image: skip re-parsing an SVG that is already shown, keyed on the caller's shared bytes

* CachedView: fix upside-down offscreen texture on GL/GL-ES

GL/GL-ES store offscreen FBOs bottom-up, unlike Metal/D3D/WGSL.
The DSL->script-shader migration switched the CachedView composite to plain
.sample() (non-flipping sample2d on GL), so cached views rendered
vertically mirrored on GL/GL-ES; Metal was fine on macOS.

Add a `sample_rt` script sampler (emits the V-flipping sample2d_rt on
GLSL, plain no-flip sample elsewhere) and use it in the CachedView and
CachedRoundedView composites.

* Fix `AdaptiveView::redraw()` to actually do something
2026-07-21 09:59:12 +02:00
Admin
ef0514a9aa hypothetical heap access fix 2026-07-01 14:04:23 +02:00
Admin
56cf3bc2e6 aichat 2026-06-25 18:52:10 +02:00
Kevin Boos
e444804344 parse and use orientation info for all supported image formats (#1130) 2026-06-23 09:08:45 +02:00
Kevin Boos
662545be44 Limit cache growth for text/script, reclaim memory after gc (#1123)
* Run script-VM gc in the desktop and mobile event loops, not just macOS

Only macOS was calling the script VM's garbage collector.
Now we call it everywhere, based on the original implementation in macOS.

* Limit cache growth for text/script, reclaim memory after gc

This PR includes several misc improvements to reduce memory usage and/or
return unused memory to the OS properly.

- slug atlas: reset the append-only curve buffer past a cap (mirrors the raster
  atlas reset: cleared at the prepare_textures boundary, forcing a rebuild), so
  it no longer accumulates every distinct large glyph ever rendered.
- script heap: in gc(), truncate the String reuse pool and shrink over-allocated
  free-list/slot capacity (never moves a live slot, so all refs stay valid).
- font outline cache: cap per-font distinct-glyph entries (clear-on-exceed).
- image cache: evict Loaded entries past a cap; widgets keep their own texture
  clones so displayed images are unaffected, and in-flight loads are preserved.

The string intern table is intentionally left unbounded: it backs stable
pointer-based FontId/FontFamilyId, and is bounded by the few distinct font names.
2026-06-16 09:07:53 +02:00
Kevin Boos
5ae5e65a7f Fix modal behavior: prevent scroll behind it, make Fit{max} scrollable (#1117)
* Ensure that a view that specifies Fit with a max value can be scrolled.
* Turtle: include a view's outer maring in the size calc for a `Fit{max}` bound.
* Modal: dismiss on Escape KeyUp (not KeyDown) so the release can't leak to a
  background widget behind the modal.
* Modal: allow scrolling, and reset the scroll to the top when showing it.
* Touch-baased dragging for views (ScrollBar) and PortalList now respect
  the blocked scrolling areas, not just the mouse wheel / trackpad scroll.
* Forward the `set_scroll_pos()` through the widget derive traits so that
  we don't have to hook it up for each specific widget.
2026-06-11 22:12:09 +02:00
Kevin Boos
f389197f92 SVG: allow replacing and re-loading the SVG "doc" (#1116)
Without this, once you load an SVG for the first time,
you can never change it. This meant that you couldn't change
a buttton's icon, for example, at runtime, even using a script apply.

Now that works, at no cost too, since we track which SVG body/"doc"
has been loaded to ensure we're not re-loading it on every draw
(which was already there, it was just too strict).
2026-06-09 21:04:52 +02:00
Kevin Boos
f25f54230e Audit and harden image decoding stuff against huge inputs (DoS) (#1110)
* Image support: add bmp/qoi,ico, webp, SVG in `Image` widget, 16-bit png

Generally, this commit makes improvements to image decoding and rendering.

Added a bunch of functions for image discovery / metadata gathering:
`decode_image_from_data()`, `image_size_by_data()`, `looks_like_svg()`

Added more `Image[Ref]` functions for other image formats:
`ImageRef::load_{bmp,qoi,ico,gif,webp,svg}_from_data()`, plus a nice
convenience fn for auto-detec+load: `load_image_from_data()`.

Added cheap, lazily-init'd support for SVGs within the `Image` widget.

Fixed some issues with aspect ratio being clobbered during image rotation.

* Audit and harden image decoding stuff against huge inputs (DoS)

Bound the size of the decoded image, pixel count, frame counts (for animated),
range of SVG sniffing, and encoded file size.
Only once we run those checks do we actually alloc a buffer for the decoded image. before allocating decode buffers. Validate

Add various other checks within the vendored image decoding libraries too.
2026-06-09 00:31:36 +02:00
Kevin Boos
ea63bc580e Layout/turtle fixes: right-wrap flows could cut off the right side of widgets (#1114)
Especially in right-aligned view rows (`Align: {x: 1.0}`), the turtle logic
wasn't accounting for spacing nor alignment when deciding to wrap.
Now those are taken into account, so we don't get weird cut-off views.
2026-06-09 00:10:47 +02:00
Kevin Boos
8551b949f8 Image support: add bmp/qoi,ico, webp, SVG in Image widget, 16-bit png (#1108)
Generally, this commit makes improvements to image decoding and rendering.

Added a bunch of functions for image discovery / metadata gathering:
`decode_image_from_data()`, `image_size_by_data()`, `looks_like_svg()`

Added more `Image[Ref]` functions for other image formats:
`ImageRef::load_{bmp,qoi,ico,gif,webp,svg}_from_data()`, plus a nice
convenience fn for auto-detec+load: `load_image_from_data()`.

Added cheap, lazily-init'd support for SVGs within the `Image` widget.

Fixed some issues with aspect ratio being clobbered during image rotation.
2026-06-09 00:09:29 +02:00
Kevin Boos
93c905722d DrawText: avoid redrawing slug stuff on EVERY frame (#1104)
* Fix CPU core locked to 100% on Linux X11/Wayland when idle

The desktop event loop's `select()` call was watching stdin (file descriptor 0)
which acts as ALWAYS readable whenever stdin is redirected to /dev/null
or similar.
So taht was causing the loop to falsely run every time that the select
call was made, even if nothing actually was readable on any of those FDs.

The fix is to just ... not do that, haha. Only makepad-studio uses
something like that, but no longer. It now uses websockets only.

Also, harden vsync behavior by setting `eglSwapInterval` explicitly
(vsync on by default; MAKEPAD_NO_VSYNC opt-out) to ensure that things
that get continuously drawn are capped at the display's refresh rate.

* DrawText: avoid redrawing slug stuff on EVERY frame

On Linux/Windows, the slug text path redrew a draw item's "old area" whenever
that path wasn't drawn in the current draw_text call and the area wasn't Empty.

THis was causing an entire CPU core to be pinned to 100% due to an
infinite loop of repaints.

Now, we only clear a draw item when its area holds genuinely stale content
(instance_count > 0 and a stale redraw_id).
2026-06-05 07:19:06 +02:00
Admin
f50ee44a0f update zune 2026-06-02 18:47:28 +02:00
Kevin Boos
de6f45eda2 Fix subtle layout bug that ignored padding for text wrapping calc (#1094) 2026-05-24 06:34:08 +02:00
Edward Tan
baee7d9922 Fix Android rendering, loading issues (#1090)
* Disable SLUG text band acceleration

* Load Android optional APIs dynamically

* Fixed the android-only Gauss-pane vertical flip by correcting render-target Y sampling in:

  - widgets/src/window.rs:110
  - widgets/src/gauss_view.rs:151

  Root cause: Gauss captures the scene into render-target textures, then samples them back
  into the UI. Those render-target textures need a Y flip when displayed, matching the
  existing Image widget behavior.
2026-05-21 09:08:13 +02:00
alanpoon
dabcdca5de Added gif (#1083)
* added giphy

* added gif

* remove unnecessary file changes

* animated_image_git
2026-05-18 22:47:53 +02:00
Admin
759a5a386c gauss blurs 2026-05-03 23:45:59 +02:00
Kevin Boos
2e6726f4c3 Add full app lifecycle event support, and Ctrl+C/signal catch (#1074)
* WIP: adding full app lifecycle event support, and Ctrl+C/signal catch

* Added fuller lifecycle event support, plus ability to catch sigquit

Tested working well on every platform except web/wasm, as I don't have
a working setup able to test that.
2026-05-01 22:30:24 +02:00
Kevin Boos
4d6bf1d462 Misc fixes for PortalList alignment and inline <code> tags (#1071)
* Fix portallist alignment handling so centering actually works

Now, alignment on PortalLists now only applies to the "cross-axis"
of each item, not the main axis.
So when you center a portallist itself, it won't add weird space
on the leading side of the list

* Fix text not drawing on top of a background, e.g., `<code>` tags

This worked in *most* but not all cases, e.g., if you had a ton of
inline code tags, some of them would rarely but deterministically
not show the actual text glyphs, but just an empty background.

* Avoid large margin on the left of `<code>` if it's on a new line
2026-04-30 08:32:51 +02:00
Admin
4a106111b8 ai manager otw 2026-04-28 12:24:40 +02:00
Kevin Boos
c52b353c52 Support runtime changes to script-level heap objects (#1063)
* Support runtime-reassigned module templates and app-wide events

- Dock/PortalList: add `refresh_widgets_mod_template()` so callers can
  re-capture a content template from `mod.widgets.*` after reassigning
  it at runtime via `script_eval!`. Dock's variant takes a separate
  template_key and mod_widgets_name since local DSL names (e.g.
  `room_screen`) don't always match the module entry (`RoomScreen`).
- PortalList: add `all_items_and_pool()` iterator so callers can walk
  every live and pooled item (e.g. to push a new property across the
  whole list on a preference change).
- StackNavigation: forward non-visibility events (`Event::Actions`) to
  all child stack views, not just visible ones. Inactive views need
  global state updates too; `View::handle_event` still gates on each
  child's own `visible` flag for events that require visibility.
- TextInput: add `submit_on_enter` so callers can opt into Cmd/Ctrl+
  Enter submit semantics, plus a `key_focus_lost` helper for commit-
  on-blur inputs.
- Image: honor `Size::Fit { max }` when `peek_walk_turtle` returns NaN
  so `Fit{max: Abs(..)}` caps image height without clipping.
- FlatList: derive `Default` on the shared `WidgetItem` struct.
- draw: re-export `Base` and `FitBound` from turtle.

* Add Event::ScriptReapply + preserve Dock state on reload

- `Event::ScriptReapply`: new event signalling a widget-tree Apply::Reload
  that does NOT re-run `script_mod!`. Fires from `run_live_edit_if_needed`
  when `Cx::pending_script_reapply` is set (previously the flag was never
  observed on desktop). The AppMain macro caches the app's script root as
  a rooted `ScriptObjectRef` and re-applies the tree with it — so runtime
  heap mutations (e.g. `script_eval!` overriding a user preference) stay
  intact. If a file-driven `LiveEdit` handler then sets the flag again, a
  bounded follow-up `ScriptReapply` pass runs in the same tick.

- Dock: on `Apply::Reload`, preserve existing runtime `dock_items` (open
  tabs, selected indices, splitter positions). Only insert DSL-defined
  items for IDs that don't already exist — so a source hot-reload no
  longer wipes the user's opened tabs.

* cleanup, remove unnecessary crap from prior approaches

the whole `refresh_widgets_mod_template` was a misguided approach,
and now that we have script reload/re-apply working and we have fixed
LiveEdit for most widgets, we just don't need it

* cleanup, remove more unused functions
2026-04-22 23:36:56 +02:00
Kevin Boos
7dbae63eff Add RowAlign::Center, per-row FinishedWalk support, and inline widget alignment (#1053)
* Add RowAlign::Center, per-row FinishedWalk support, and inline widget alignment

Closes #712

turtle.rs:
- Add RowAlign::Center variant for vertically centering walks within a row
- Add finish_row_center() that shifts shorter walks to the row's vertical midline
- Fix finish_row's current_row_walks_start() to use last finished row (not first)
- Add Cx2d::align_list_len(), shift_align_entries(), emit_turtle_walk_with_metrics()

draw_text.rs (draw_walk_resumable_with):
- Per-row path: multi-row wrapped text now emits one FinishedWalk per visual row
  with separate glyph instance batches, enabling RowAlign::Center per row
- Between rows: call turtle_new_line_with_spacing to trigger finish_row at each
  visual-row boundary
- Wrapped rows draw glyphs at turtle position (not layouter position) so glyph
  positions stay in sync with turtle tracking when pills inflate row height
- Remove shift_extra_height from allocation (caused turtle/glyph position divergence)

text_flow.rs:
- Fix wrap check to use matches!(Flow::Right { wrap: true, .. }) instead of
  equality against Flow::right_wrap() (broke wrapping with non-Top RowAlign)
- Account for inline_code padding in turtle allocation (fixes overlap bug)

* fix build for Linux / Windows
2026-04-18 10:30:04 +02:00