Bound drawable, geometry, texture, image, map and radar work; validate WebGL submissions and retire GPU resources safely. Treat context loss as terminal without re-entering Wasm after worker termination.
Add explicit Route location consent and regression coverage, including software-WebGL release probes for six deployed demos.
A round cap was a hard disc fan of up to 32 vertices per road end with no
anti-aliasing; a Butt/Square start pushed a feather pair it never
indexed. The cap is now carried by the last body pair extended by half a
width, with uv.y = 1 + cap_axis, and the fragment computes the circle's
coverage with the same derivative-based AA as the band edge. Amsterdam
start view: casing 42.5 -> 40.4 MiB (shape 100 17.1 -> 16.5, shape 110
15.2 -> 14.5), tilted total 116.4 -> 114.3 MiB, flat 313.9 -> 311.7.
Cap edges gain AA; Metal hidden grab vs the head without it 1.23 % /
0.27 % start (label placement in the crops), web GPU gate clean
(CAP1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Shape-0 Boolean union faces (plazas, road bodies, tunnel and bridge
faces) leave the 28-byte road vertex for FaceVertexTyped: i16 anchor,
UNorm8x4 colour, F16x2 class/material + depth tick, F16x2 deck lift +
coverage. The road shader draws them through a second vertex format on
the same pass; face streams use u16 indices. Amsterdam start view:
casing 73.6 -> 58.7 MiB (42.5 casing + 16.2 face), tilted total
158.6 -> 143.7 MiB, flat 328.8 -> 313.9, 433 ms/tile. Metal hidden grab
within the noise floor of the head without it (1.26 % / 0.23 %, label
placement); web GPU gate clean. Faces now draw as one batch per tile
before the stroke bands, so a translucent shadowed face over a crossing
casing blends ground instead of stroke at that pixel — not visible in
the crops (FACE1 lane).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Roads move from eight f32 lanes to a 28-byte RoadVertexTyped (i16 anchor
at 1/64 tile unit, f16 offset / depth / uv pairs, unorm8 colour, f16
params, exact f32 deck), ground fills and roofs to 16 bytes (i16 anchor,
unorm8 colour, f16 params; u16 depth ticks for fills, exact f32 height for
roofs); indices are u16 whenever a stream has fewer than 65,536 vertices.
The three shaders read the typed POD fields directly (the fetch converts),
uploads go through Geometry::update_typed with the shader's layout, the
space-warp subdivision decodes / interpolates / re-encodes the typed
records, and a tile that lands before its shader has drawn waits in the
pending queue instead of panicking. The shader compiler types a packed
field access as vec2f / vec4f, and the Metal name table gains the packed
short / ushort / uchar / char vector names.
Harness: 249.6 -> 215.5 MiB for the 25 start-view tiles, 384 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Draw shaders can declare geometry POD fields as F16x2/F16x4, U16x2/I16x2
(plain or normalised) and UNorm8x4/SNorm8x4; the fetch converts them to
vec2f/vec4f on Metal and WebGL2 (per-attribute vertexAttribPointer with
type, normalised flag, byte stride and offset), the headless JIT decodes
the same formats, and Vulkan/OpenGL/D3D11 refuse compact layouts with a
logged skip rather than drawing garbage. Attribute packing computes
physical offsets with natural alignment for the whole record; the all-F32
path stays byte-identical (stride = slots * 4, packed_geometry_N on GL).
Geometry::update_typed / update_typed_with_recycled_buffers take
IndexData::{U16,U32} plus a byte vector and the shader's input layout;
updates are validated (stride, whole vertices, index range) and every
geometry carries its index width and a layout signature that survive
releasing the CPU staging, so a draw checks layout and index type against
the resident buffers, never the staging enum. Compact fields are rejected
in instance PODs and in nested aggregates at shader validation; SNORM
minima clamp to -1 like WebGL2.
On Metal the decoded (logical) vertex is reached through the shader
context like the instance is, so every shader function that reads
geometry compiles.
No map stream is converted yet; that is the next cut.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Lifted shape-0 roofs leave the 48-byte universal vertex for a five-slot
RoofVertexPacked (anchor, unorm8 colour, exact metre height, f16
material | zbias ticks); DrawMapRoof reconstructs the fixed vector
channels and shares the colour pass, the fade, the LOD band and the
shadow-mask projection / footprint cut-out. Roof records that need more
(parapet AO depth variants, marker stalks) stay on the generic layout in
a fill_3d_misc stream. The tree and signal contact shadows are no longer
tessellated ten-segment discs in a vertex stream but four floats per disc
drawn as an instanced unit quad into the shadow mask, with the same
radial ramp.
Harness: 281.5 -> 249.6 MiB for the 25 start-view tiles, 387 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Casing, stroke and fringe records leave the 48-byte universal vertex for
an 8-slot road layout: full-precision anchor and deck height, f16 offset /
depth / tessellator uv pairs, unorm8 colour, and one f16 integer that
packs class, material, dash id, kind (stroke / union face / fringe) and an
explicit expanded flag. DrawMapRoad reproduces DrawMapVector's expansion,
terrain, tilt, space-warp and shadow-mask behaviour for those streams;
casing/stroke draw through it in the colour passes, the fade cross-fade
and the shadow-mask projection, and the space-warp subdivision has a
road-stride midpoint.
Harness: 339.0 -> 281.5 MiB for the 25 start-view tiles, 437 ms/tile.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
libs/app_module (makepad-app-module) is the contract (aicontrol §3):
AppModule (id, label, register into an isolate, an OpenSchema, create
into InstanceParts, capabilities), InstanceHandles (an InstanceScope
owner token, the storage jail as a cx.storage namespace, the viewport,
a ReplySink for calls that finish later), a ServiceExecutor addressed
apart from the root, and an OpenSchema that refuses raw paths — a file
is a handle the host issued or nothing. Only types live there.
The WM hosts one (module_host.rs): allocate the isolate, retint its
theme from the palette, register, create — one trusted entry into the
isolate, never a second &mut Cx beside the VM; teardown drops the root
first, runs shutdown in the isolate, frees it. MpModuleView is the tile
for an instance's root, drawing and dispatching with the isolate
INSTALLED on Cx (enter_isolate/leave_isolate, new in widgets) and
gating keys on the WM's focus; a TileHost trait covers both tile kinds
so the desk and the focus logic never ask which. The registry overlay
(apps.rs) links modules per app-* feature; the desktop default stays
Process, switched per app in ~/.makepad/wm/apps.splash or by a dev
--module flag. The bus gains its in-process leg: an instance is an
m<id> endpoint the pane addresses like any other — the leg the web
superbuild runs everything on.
sheets is lib + bin: SheetsModule mints MpSheets{} in the isolate and
answers sheets.summary on its executor; the standalone binary keeps its
Window and F10 overlay behind the standalone feature.
The drive found a platform bug: a shader compiled on a widget minted in
an isolate hit the draw-shader object cache by object index alone, so
an isolate's Button reused the main heap's entry and drew with zero
instance slots. The cache is keyed by (heap, object) in every backend,
and add_instance refuses a zero-slot draw call instead of dividing by
it. Isolate cost, measured (the P2 entry gate): 12 ms and ~8 MB per
isolate, flat to 32 — no pre-warming needed.
Verified by hidden grabs under the WM: /os.launch sheets → an
in-process Sheets tile (no child process), the apps row Desktop ·
Sheets, /sheets.summary answered through the in-process leg, a warm
terminal beside it, F10, close → the instance torn down and its isolate
freed. wm 145, sheets 84, app-module 3, services 33; platform and draw
check for windows-msvc, linux-gnu and wasm32.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Latin (IBM Plex) is the web default, International (adds the CJK and emoji faces) the
desktop default; app_main! records the choice before widgets register, the theme only
registers the selected chains, a glyph miss never starts a load, and the resolved font
resources are emitted as the makepad.font-assets.v1 section for the packager.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WV6BzHQiJEvvK9EPc1d4ks
libs/score_view carries the engraving, spacing, font and document code out of the score app so any app can show notation: a ScoreView widget with width/page/content fits, pan and zoom, a playhead, a dark palette, drum-key labels, lyrics under melody notes, and builders for drum, pitched and bass-tab scores. The score app uses it. Vector glyphs no longer write depth for transparent fragments, and the Metal screenshot staging sizes itself from the source texture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squashed from work:
- map: overlay and label rendering
- map: bake a runnable Amsterdam test map from inside the route app
- map: gate the @cam readout; a .mkmap archive is a directory
- map: the view's own draw list carries its clip
- the map's centre is clamped to the world, not wrapped past it
- a country is drawn in hairlines: carto's road ladder joins by zoom
Squashed from work:
- draw: DrawVector reused one geometry slot for every session in a frame
- draw, platform: overlays now composite above content that uses draw_depth
- DrawVars::set_uniform_on_draw_list — one uniform into every retained call of a shader in a list, pass repainted
Squashed from work:
- glass: a glass button can carry an icon
- widgets: the fab palette cell's colour is a field, not an object
- widgets: the glass stops reading draw_pass.time, so glass apps idle again
- live-with-parent passes: the glass blurs the world in realtime again
- fold_header: visually open is open
- fold_header: the ease snaps to its ends
- fold_header: settled state is the truth, the pane edge is the law, the area is the fold
Squashed from work; the fine-grained history is under tag archive/work-2026-08-29:
- mp* wave: mpwm window manager + the mp app family, WM API, theme bridge, PDF engine fix
- mpwm polish wave: terminal key focus, focus-history close order, pop-back-to-origin, occupied-workspace cycling, demo
- mpwm: warm-instance pool, flat-luminance opens, flicker-free CEF resize
- work: land the sources the last commits reference
- kenney: catalogue all 50 free 3D kits; Modal dismissed() never fired
- platform: windows check green again — SetWindowTextW binding
- map: exact warp-aware inverse projections — pointer ops work folded
- mpwm: quick-look gap fixes; image cache eviction on preview unload
- tweaker: material thumbnails + vibecode popup + ctrl-space notes, undo/redo over the edit ledger, capture-semantics pi
- tweaker: vibe popup card chrome + dispatch order, ctrl-space notes verified, sploded design v2 chapter
- tweaker: tabbed side panel (Props/Shader/Tree) - shader tab with checkerboard material well + prompt, complete widget-
- sploded v2: nesting-depth z, hairline scope frames, body pass
- sploded: pin the depth convention with a test, kill the draw_depth residue
- sploded: real body-pass split (scene-pass capture, panel flat) + y-convention source of truth with anti-flip gate test
- sploded: hollow outlines, flat-band input, ray-pick unprojection
- tweaker: shader tab defaults to the selection's first draw layer, stale hint trimmed
- sploded: outlines become clipped, antialiased strips; tighter deck
- sploded: merge the lane's v2 (nesting-depth z, clipped AA strip outlines, flat-band input, unproject, SplodedStack bod
- sploded: the exploded view is a LIVE view — pointer events route through the inverse explode transform (ray -> plane -
- tweaker: tabs are real widgets (uid, tree node under the dock, own plane in 3D) and pickable; navigation-class clicks
- sploded: pinned/hover outlines render on the widget's own plane in 3D — per-widget nesting depth lives on the platform
- tweaker: the material well renders the pinned widget's actual shader — the swatch byte-copies the widget's live draw c
- tweaker: the Shader tab shows the shader as written — the layer's pixel/vertex fn source (nearest definition up the co
- sploded: I = true isometric preset (yaw 45°, pitch atan(1/√2))
- tweaker: eyedropper — the colour popover's pick button arms a pixel probe; the next press in the app samples that devi
- tweaker: the shader loop closes — /tweak/apply resolves the pinned widget by uid (anonymous path segments never round-
- vj: responsive DJ mixer + Windows drag-and-drop, cherry-picked from PR #1199 (vjroger)
- tweaker: the material well is a magnifier — the mirrored instance draws at the widget's native size in the well's own
- tweaker: per-layer material thumbnails — the Widget derive emits WidgetNode::layer_areas() (every #[live] Draw… field
- tweaker: the shader source view is the real CodeView (syntax highlighting, selection, editing) when the app registers
- tweaker: Ctrl+Enter sends on every platform (TextInput treated only Cmd as primary on macOS, so Ctrl+Enter inserted a
- Modal claims no layout slot: the DJ page fills its window again
- tweaker: every fn apply recompiles (eval_chunk ran every chunk under ONE synthetic callsite, so the script body — and
- tweaker: an apply whose draw shader fails to compile is rejected — the layer goes back (last live fns / the fn as writ
- tweaker: the Shader tab's source view owns its scrolling (the ScrollYView around the CodeView double-scrolled the care
- widgets: set_visible belongs to every widget, not just View (#1194)
- script: a dead heap's resource handles must not outlive it (#1195)
- Resources: search the executable's directory, not only the working directory (#1196)
- Windows: fit a restored window to the displays that are actually attached (#1197)
- d3d11: a failing GPU call reports the loss instead of killing the process (#1198)
Co-authored-by: Kevin Boos <1139460+kevinaboos@users.noreply.github.com>
Squashed from work; the fine-grained history is under tag archive/work-2026-08-26:
- vj: the console grows real transports, and the deck stops lying about reverse
- vj: video goes NV12 end to end, and the GPU does the unpacking
- widgets: boxed labels center on their ink, not on the font's line box
- metal: a fresh texture forgets nothing it never had — reallocated vec textures upload whole
- fab: a 3D creation shell and the viewer built on it
- widgets: a dock redraws its panels, not just its own frame
- draw: hsv2rgb takes vector bounds, so it compiles
- texcomp: the block codec and the container every texture will travel in
- widgets: a Splash keeps the walk its host declared across a body rebuild, and logs a body that fails to evaluate instead of drawing nothing
- vj: DJ-tab scroll knobs, title-click reset, crossfade fix, stem headroom — plus the pre-Ampere CUDA fix (#1193)
Co-authored-by: Rogier de Leeuw <vjroger@gmail.com>
An app built with `--remote` now serves a localhost HTTP control surface:
window list, per-window PNG grabs, real mouse/key/text injection, widget
rects, a log ring buffer, and `/gq` (grab every window, then quit). It exists
so a test or an agent can DRIVE a running app instead of reasoning about it
from source — the protocol is documented in AGENTS.md. Grabs are targeted per
window (`/g?w=N`), so a multi-window app is captured window by window rather
than whichever pass happens to present first, and `log!` mirrors into the ring
buffer without anyone owning the app's stdout.
platform/video grows a streaming half beside the file half. StreamEncoder /
StreamDecoder with Apple VideoToolbox and Windows Media Foundation backends,
Annex-B framing, and all-intra bound through pEncodingParameters on Windows —
the only control that MFT actually honors, as the readbacks claim success for
everything else. The file decoder can now be asked for a SPECIFIC frame rather
than only the next one, which is what frame-exact seek and bounce playback
need. Tests cover file seek and the stream round trip.
Draw shaders gain `Rgba16F` and `Rgba32F` color formats to pair with the
float render textures: blending off, whole-texel writes, meant for GPU
simulation state (particle position/velocity, fluid fields) rather than
pictures.
Windowing and dialogs:
- `CxOsOp::SetChromelessWhenMaximized` drops the native maximized border
strip on Windows, so a maximized window reads as a clean picture.
- `Cx::open_select_folder_dialog` opens the native folder picker with a
title and start location, answered by a `FileDialogAction` in the actions
pass; cancelling is a first-class outcome, not an error.
- Windows reports a user close the way macos.rs already did.
- macOS swaps the titlebar container so WindowDragQuery alone decides window
drags, and the delegates carry a panic shield.
- `Windows::id_iter()` enumerates window slots generation-correctly.
Headless: the virtual GPU and its rasterizer are substantially rebuilt around
the shader runtime preamble, making `MAKEPAD=headless` render-to-PNG a real
test surface rather than a smoke check. `PerfMonitor::frames_painted()` lets a
scripted driver pace itself to PRESENTED frames instead of queueing passes
faster than the GPU retires them.
Drop the private sandbox clone from required workspace members
and Studio runnables. Finish the platform_ops VecDeque merge
(push_back / Option remove), land the mip-repeat texture API
the renderer already calls, and unbreak the Q3 importer plus
the godot example template that .gitignore had hidden.
* 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.
* 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.
* 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.
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>
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>
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>
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>
* 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.
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>