* 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.
- deep_eq returns false immediately when both sides are string-like and
the bit-compare failed (strings are interned, so bit-equality IS string
equality) — trims the type-check chain on the hot failed-compare path
of string-tag dispatch (a.kind == "...")
- pre-reserve thread stacks (value stack, scopes, calls, mes, loops)
- splash_bench: string_cmp workload, arith_hostmode (instruction limit +
run budget installed, the game-host configuration; measured: the two
per-instruction counters cost ~nothing, branch prediction hides them),
BENCH_ONLY profiler filter
Tried and rejected: batching consecutive value-pushes in run_core into
one dispatch iteration — the scan overhead outweighed the per-value
limit/budget checks it skipped (they were already free); reverted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Interpreter perf pass on platform/script, semantics-preserving:
- ValueMap hybrid small-map: object maps linear-scan a vec below 16
entries and spill to a HashMap above (scopes/call-args/small objects
never hash)
- compound scope assigns (+=, -=, ...) locate their slot with ONE
proto-chain walk instead of a read walk plus a write walk
- for-loop iterations clear and reuse the iteration scope in place when
nothing captured it (closure capture -> REFFED -> fresh scope); loop
scopes no longer copy the parent scope vec
- 'for i in a..b' captures end/step at loop entry when the range object
is un-REFFED; stored (mutable) ranges keep live per-iteration lookups
- trap error-queue + trap.on polled via one Cell<u8> bitfield per
instruction instead of a RefCell borrow + Option<enum> Cell read
- ADD checks the number fast path before the string check
Measured (splash_bench, medians of 3 A/B runs): array_iter -27%,
method_call -21%, array_index -21%, for-range loops -19%, object_churn
and field r/w -8..9%, fib -7%, sandbox-style composite tick -6%.
Adds platform/script/test/src/bin/splash_bench.rs (run with
cargo run -p makepad-script-test --bin splash_bench --release,
BENCH_ONLY=<name> loops one workload for profilers) and parity asserts
for the touched edges: mid-loop range end/step mutation stays live on
stored ranges, closures pin their iteration's scope, selective capture
does not leak across reused scopes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* android oes video zero-copy and gles shader fixes
* regenerate windows-rs by windows-strip
* fix windows video freezes with MF on an MTA worker
* regenerate windows-rs by windows-strip
* MTA MF video, COM notify, YUV texture reuse
* add local video file playback support to video-player example
* NV12 Metal present, seek warm-up gate, YUV full range
* fix Linux GStreamer video: A/V mute, HLS prepare, async teardown
* no-op SelectVideoTrack/SelectAudioTrack on non-Linux backends
* Linux ALSA/Pulse: bigger buffers, fix resample setup, report device rate
* fix some warnings
* Move XInput/DirectInput device discovery off the UI thread
* fix GLSL unpack4u8 for GLES 3.0 (#version 300 es)
* Linux GStreamer: DMA-Buf NV12 OES zero-copy, optional GLMemory path
* Linux MediaPlugin: DMA-Buf NV12 and GLMemory zero-copy present APIs
* Add Windows SourceReader DXGI NV12 zero-copy video path
* fix build error
* fix GLES: add highp precision for sampler2DArray in Linux shaders
* playback speed support for android
---------
Co-authored-by: jasonqiu <jasonqiuchen@outlook.com>
Co-authored-by: jasonqiu <jasonqiu@futunn.com>
CPU launches shells; the GPU animates every spark from a closed form. Once
a shell bursts nothing in the world can influence a spark — it flies from a
known point, at a known time, along a fixed arc — so position is a function
of (spark index, seed, age) and there is no state to step and nothing to
upload per frame. Twelve shells of 320 sparks is 3,840 particles for TWELVE
instances of CPU work, against 3,840 simulation updates and a 3,840-instance
upload for the stepped path.
**The style seam is the point.** Rust owns structure — trajectory, lifetime,
billboarding — and exposes four hooks a splash script overrides by
inheritance: `spark_motion` (swirl, fizzle, drift), `spark_size`,
`spark_color`, and `spark_pixel` (the sprite program). Arcade's own styling
lives in its `script_mod!`, not in Rust: a three-stage temperature burn,
hotter on the fast outer shell than the slow core, with sparse glitter
strobing. A generated game can restyle the sky without being able to reach
the simulation.
Two real bugs fixed on the way:
- **Vec3f instance fields silently misalign.** A `Vec3f` is tightly packed in
Rust but a `vec3` obeys 16-byte alignment in the shader ABI, so the burst
origin read back as zero and every shell rendered at the world origin, on
the ground. The other shaders here get away with `Vec3f` because theirs are
`uniform`, not instance. Everything is packed into `vec4`s now, which is
the shape the hardware wants anyway and removes the class of bug.
- **Eight bare `panic!()`s in the shader backend** now name the backend, the
stage and the IO type, and the commonest case — reading a geometry
attribute from `pixel:` — gets a message saying so and showing the varying
that fixes it. Previously a shader that tripped it aborted with no text at
all, which is not a barrier, it is a wall in the dark.
**Gated off by default.** The spark SIZE instance is not reaching the shader:
scaling it 25x changes nothing on screen, so every spark draws as a
screen-filling blob and the sky whites out. That is the same class as the
Vec3f bug above and I have not found the second instance of it. The launcher,
the trajectory, the placement annulus and the whole hook surface are finished
and tested; this is one plumbing bug from working. ARCADE_FIREWORKS=1 turns
it on to work on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The compiler is where hostile shaders get stopped, because this engine lowers
the script shader language to Metal/HLSL/GLSL/WGSL rather than passing source
to a driver. Two genuinely exploitable holes found and closed:
- Loop bounds were arbitrary EXPRESSION STRINGS written straight into the
emitted shader, so `for i in 0..some_uniform` compiled to an unbounded GPU
loop and `loop{}` emitted a bare `while(true)`. A hang triggers a driver
device reset that kills the app — ugly anywhere, worst in a headset. Rather
than reject (which would break legitimate code), the bound is now EMITTED: a
provably-small integer literal compiles unchanged, anything else gets a hard
65536-iteration cap. literal_bound() is deliberately conservative — a
uniform, arithmetic, a call, hex or a negative all count as unprovable
- compile_fn INLINES at every call site, so a branching call graph expands
exponentially with depth (recur_block stops self-recursion, but not f1
calling f2 twice calling f3 twice). MAX_EMITTED_BYTES (1 MB) bounds what was
an unbounded compile-time DoS
Recursion was already safe (recur_block errors); nothing added there.
Honest remaining gap: the cap bounds ITERATIONS, not cost per iteration — a
shader doing 65536 heavy texture samples is legal and slow. Bounding real GPU
time needs a cost model or driver watchdog; neither exists here and this does
not claim otherwise.
Verified empirically, not just by compiling: all 24 loop{} constructs in the
built-in shaders are guarded (48 emitted lines, 4 unique guard names per
shader confirming no shadowing), zero for( loops exist in any built-in, and
the capture shows rendering intact — including text, which is exactly where
those guarded loops live.
MEASURED FIRST, then declined to build: 34 shaders compile at Arcade boot in
~15 ms total on Metal (first 4.47 ms cold, rest 0.10-0.74 ms). A persistent
shader cache is NOT worth its invalidation-and-staleness surface to save 15 ms
of one-time boot, so it wasn't built. Vulkan/GLES on Quest may differ — that
needs on-device measurement before anyone builds speculatively. Benchmark
retained behind MAKEPAD_SHADER_BENCH (it was logging every boot);
MAKEPAD_SHADER_DUMP=<dir> writes generated source, which is what an AI has to
debug and was otherwise invisible.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Entity lookup O(log n): shared entity_index_sorted helper, single
push_entity spawn path with ascending-id assert + per-tick sorted
debug_assert — the sorted-Vec invariant is now enforced, not hoped
- WorldSnapshot captures next_id: failed-eval rollback can no longer mint
colliding ids under surviving entities (review defect fixed)
- game.log buffered (1s/16KiB/eval-boundary flush), agent RPC poll gated
on one .agent dir-mtime stat — no per-tick file I/O left
- Camera rig authoritative on GameWorld (orbit/chase state); widget keeps
only device-input accumulation; mailbox drain order unchanged
- Per-tick cumulative script budget: with_instruction_limit reports
consumption (incl. trap-wipe subtlety), gamemaker tick holds ONE 500k
pool across on_tick + timers + touch events
- Tape probe verified BYTE-IDENTICAL vs pre-cleanup reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Escape barrier: every checked store funnel tags stored objects REFFED
(stored => REFFED structural), ret==args guards at native completion,
fn-arg bind-time barrier (closure-captured scopes retained args untagged
-> latent use-after-free under eager release)
- vm.release_transient() for host per-call objects; *_unchecked pushes are
the releasable-container path; call_with_scope native branch no longer
leaks one scope object per Rust->native vm.call()
- pump_widget_async: dead-isolate reclaim every pump + needs_gc-gated
round-robin mark/sweep — isolate heaps were never collected before
- res.rs: resource path cache was Cx-global and cached handle VALUES across
VMs, so isolate heaps held main-heap handle indices (exposed by the first
isolate mark pass ever to run). Now: data shared globally, handles minted
per-heap, cache keyed (heap_key, path), per-heap detach on GC
- vm.gc() no longer shrink_to_fits every run; explicit gc_and_compact()
- script-test: GC campaign suite (flat-heap tick loops, retained-input
survival, escaped-scope capture, isolate churn collection)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wrapper names printed as hex LiveId hashes on non-Metal backends
(identifiers starting with digits — GLSL compile failure at startup on
Linux/GLES). Metal had the lut entries; Glsl/Wgsl now do too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GLSL emits _mp_ wrappers over unpackHalf2x16/unpackUnorm4x8 (WebGL2
baseline has them native); WGSL names mapped for the future backend;
the headless runtime gets bit-exact CPU decodes. The packed map vertex
format is now portable to the wasm target.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One f32 geometry slot carries two f16s or four unorm8s; the vertex fn
unpacks via _mp_ helpers in the MSL preamble. Type checker: float ->
vec2f/vec4f. GLSL/WGSL mappings + headless Rust-backend impl to follow
with the packed layout itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deep gauss pyramid levels (3-5) are re-homed to 1/8 resolution via
progressive half-texel tent upsamples so no on-screen sample comes from
a texture coarser than 8 device px/texel; half-texel offsets keep the
exposed sigma ladder at ratio-2 per level (full-texel tents inflate deep
levels ~35% and open a visible blur band at the raw->re-homed boundary).
Per-pixel sampling replaces the 13-tap cross cascade with 4-tap bicubic
B-spline reconstruction: C2-smooth (kills the bilinear texel lattice)
and cheaper (glass 26->8 taps, chromatic variant 78->24).
Headless shader JIT fixes found while verifying: RenderCx always emits
vtx_pos (vertex fns that return the position have no VertexPosition io),
vec*vec MulAssign/DivAssign impls, and discard returns a value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* 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
* 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.
* Support standard keyboard navg shortcuts/keys in TextInput
Implement platform-standard TextInput navigation and deletion behavior,
including Home, End, PageUp, PageDown, word movement, line/document
boundaries, and Shift-based selection.
* Use Apple Option/Cmd conventions on Apple targets
* Use Ctrl conventions on non-Apple targets
* Web accepts both shortcut styles for now, since we don't have a way
to query the host OS from within a makepad web env.
Also, be extremely careful to ensure that we respect Unicode grapheme boundaries
when doing all the selection/navigation logic.
Fix `Delete`, which was erroneously handled before.
Add lots of missing keys in Linux X11 & Wayland backends, e.g.,
Home, End, Delete, Insert, PageUp/PageDown, and arrow keys
* Add `CropToFill` image fit variant, improve ImageFit docs
This allows you to easily achieve the "centered cropped fit" that most apps
want for things like avatars or small thubmnails that get masked.
* Detect and support hardware keyboards, distinguish from soft/virtual kbd
Mimic desktop behavior on mobile systems as much as possible.
This is esp important for tablets like iPad OS where you're more likely
to have a real physical keyboard attached.
For iOS:
* Arrow keys and Home/End/PageUp/PageDown navigate and auto-repeat
at the system-defined rate (connected via `UIKeyCommand`)
* Cmd+Enter to submit a `TextInput` and Cmd+C/X/V clipboard shortcuts now work.
* Ensure the pop-up diacritic/accent menu is properly placed using a hidden
`UITextInput` native widget, which acts as a sort of "proxy"
* Proactively drain `ShowTextIME` after each draw so the IME position will be
properly updated after each keystroke.
* Importnatly, don't mark the IME dismissed when a hardware keyboard is attached.
For both iOS & Android:
* Add a `has_physical_keyboard()` detection mechanism across both backends,
and fix platform-specific key repeat behavior
For Android:
* Ensure clipboard cut/copy works using the same Ctrl shortcuts (API 26+)
Soft/virtual keyboard/IME changes:
* For multiline TExtInputs, a soft keyboard Enter/Return key will always just
insert a new line, to avoid complexity with keyboard shortcut cfgs.
* CJK keyboard character selection should also be properly positioned now
* minor optimization to avoid re-setting IME pos if it didn't change
* Ensure inline composition is still shown in TextInput on all platforms
This is mostly relevant when using CJK and other similar IMEs.
Previously Makepad didn't shown any "echoes" of the single latin chars
that the user would type, but it would correctly input the selected CJK
glyph. So if you typed `nihao` and then selected `你哈`, then you would
see the proper chinese characters but not the latin "nihao".
Tha't s a bit confusing while typing.
Summary of the fixes per platform:
macOS:
- `set_marked_text` now forwards the marked (composition) text to the
focused TextInput with `replace_last = true`; previously it only stored
it in an ivar. `unmark_text` clears the preview, and both it and
`insert_text` share a new `clear_marked_text_ivar` helper so a commit
doesn't emit a second, destructive text-input event.
Windows:
- Add `WM_IME_COMPOSITION` handling: commit `GCS_RESULTSTR`
(`replace_last = false`) and show `GCS_COMPSTR` inline
(`replace_last = true`), and clear on `WM_IME_ENDCOMPOSITION`. The
message is consumed so DefWindowProc neither draws its own composition
window nor synthesizes a duplicate WM_CHAR for the result.
- Extend the vendored `windows` binding with `ImmGetCompositionStringW`,
`GCS_COMPSTR`/`GCS_RESULTSTR`, and `WM_IME_COMPOSITION`/
`WM_IME_ENDCOMPOSITION`, which it didn't previously generate.
Linux (Wayland):
- Handle the `zwp_text_input_v3` `PreeditString` event (previously empty)
and apply the double-buffered preedit/commit state on `Done`, in the
protocol-mandated order (commit, then preedit), clearing the preview
when a cycle carries no preedit.
Linux (X11):
- Create the input context with XIM on-the-spot (`XIMPreeditCallbacks`)
and forward the preedit string from the draw/start/done callbacks,
falling back to `XIMPreeditNothing` if the IM server doesn't support
callbacks. Callbacks run inside `XFilterEvent`, so they only mutate a
thread-local that the event loop drains into the widget afterward,
avoiding re-entrant access to the app.
Android:
- Don't mark the IME dismissed when a physical keyboard is attached
(mirrors the iOS guard). Android was unconditionally calling
`text_ime_was_dismissed()` on soft-keyboard hide, which tore down the
IME connection that hardware-key composition relies on.
* iOS: replace custom `UITextInput` with a native `UITextView`
`UITextView` is a full system-native keyboard client, so we get all the
major features for free: language HUD pill and the complete globe/Ctrl+Space
shortcut to cycle between IMEs/languages.
Makepad basically just mirrors the state of the system native text view,
via the `full_state_sync`, but the actual native text view is kept invisible
so it doesn't interfere with what we render in Makepad's TextInput.
Notably, the Full Keyboard Accessibility setting now does work properly,
whereas it did not before with our UITextInput-based approach.
We also make sure that arrow keys, nav keys, auto-repeat, and modifiers
are properly hanlded so we can retain the expected kbd shortcuts,
like other desktop platforms.
* iOS: remove the old `UITextInput` connection with the Makepad TextInput
We've now switched to the native UITextview, so we don't need this any more.
* iOS: fix desync during fast typing
Ensure there's no race between the native UITextView and
Makepad's TextInput, as the Enter/REturn key needs special handling
w.r.t. how `pressesBegan` gets it (From a real hardware kbd).
* TextInput: more iOS integration, and text input types
more native integration for things like username/password,
new password fields, email, address, URLs, etc.
These tell iOS to change the keyboard layout/type for the text input.
* iOS: don't let Full Keyboard Access focus on our hidden native cursor
* cleanup
* iOS/TextInput: fix perf issues
* iOS TextInput: more fixes for read-only efficiency, and filtered input
Also port some of these fixes to Android's IME integration layer
* iOS/TextInput: hide the native caret iOS draws during autocorrect
but still allow the "decline autocorrect" bubble to popup where that
hidden caret is located (and the CJK candidate window in the same spot)
* Avoid script VM re-entrant panic: defer animator_cut/play if script VM is held
`animator_cut` / `animator_play` call `cx.with_vm`, which panics
(*"Script VM swapped off"*) when invoked during an apply walk — e.g. a
widget's `on_after_apply` on `ScriptReapply` / `Reload` — because the VM
is already taken for the duration of that walk's enclosing `cx.with_vm`.
- The derive macro's `animator_cut_scoped` / `animator_play_scoped` now
check `cx.is_script_vm_held()`; when held, they queue the op
(`defer_cut` / `defer_play`) and return instead of re-entering the VM.
- `animator_handle_event_scoped` replays the queue via `flush_deferred`
on the next frame, once the VM is free.
The defer path runs **only** in the formerly-panicking case, so VM-free
animations are byte-for-byte unchanged.
Also adds a re-entrancy-naming panic (`VmHolderGuard`) plus
`Cx::try_with_vm` / `Cx::is_script_vm_held` for diagnosing and handling
this class of bug.
* Better spacing/positioning for IME popups like the CJK candidate menu
applied to all platforms, but primarily an issue on macOS/iOS.
The candidate/conversion window (e.g. CJK pinyin) was covering the line of
text being composed. Carry the caret-line rect (not just a point) through
ShowTextIME and feed each backend its native "keep clear of this line" API,
so the OS places the candidate directly above/below the line with a small gap:
- macOS: firstRectForCharacterRange returns the line rect via AppKit
convertRect:toView:nil + convertRectToScreen (drops the hand-rolled
screen-coord math + fudge offsets); invalidate on caret move.
- Windows: ImmSetCandidateWindow with a CFS_EXCLUDE line rect.
- Wayland: set_cursor_rectangle with the real line rect.
- X11: XNSpotLocation/XNArea at the line.
- iOS: return the true composing-line box from firstRectForRange so iOS flips
around the real edges (consistent at any screen position) instead of a
degenerate point; only while marked text is active, to avoid an oversized
autocorrect highlight when typing normally.
* Fix Linux X11 behavior: Ctrl-based kbd shortcuts didn't work in TextInput
also trying to fix X11 behavior for positioning the CJK candidate window,
turns out there was an X11 bug for Ubuntu 22 and older so it's not always
possible, but we can attempt a workaround if errors occur (based on that,
we try to auto-detect the version of X11)
* fix X11 event loop latency by draining only a max of 64 events before redrawing
still working on X11 CJK candidate window positioning...
* add logs to X11 ime to figure out wtf is going on
* more robust fallbacks for X11 CJK candidate window positioning... grr
* maybe try to set the XFontSet attribute? for CJK candidate positioning
* positioning works now but there is a bit of overlap still
* now that X11 CJK candidate positioning works in some cases,
we need to pass the full rectangle containing the current line of text
to the X11 library so that it can position the window both on top
and beneath the current line of text, if needed.
* tweaking X11 CJK candidate positioning
* abandon the screen-positioning heuristic
Instead, we just send the bounding rect of the current text character
and hopefully let the X11 platform libs decide where to put the
CJK candidate popup
* add more spacing to the bounding rect on X11
* tweak for a bit more space between CJK candidate window
* more tweaks, rect height isn't being respected for some reason...
* attempting to add more instrumentation to figure out wtf is going on with X11 CJK positioning
* remove bad instrumentation that was causing freezes. ugh
* different approach for IME placement on X11
* previous positioning attempts for X11 didn't work.
New strategy: let it be positioned, and then try to move it
* still trying to fix X11 CJK candidate window positoining...
* trying to find CJK candidate window with X11 queries (To move it)
* abandon window scanning approach
* better approach, now just tweaking it
* fix one case where the candidate window was flipped but it pointing too low
* tweaking more
* trying to fix above-text line positioning
* still trying to tweak CJK candidates ABOVE the text line
* be more conservative when guessing whether X11 will show the CJK candidate above or below
* improve size heuristic for CJK candidate height
* calling X11 as complete now. jfc. Cleanup, remove debug logs, etc
* Simplify tooltip logic, fix positioning to respect safe inset areas
And other misc positioning/formatting fixes, like wrapping
after a hard line break, as well as ensuring that the callout
arrow thing itself is centered (to the fullest extent possible)
w.r.t. the rest of the tooltip body.
Basically, now it looks good again.
* Fix CheckBox/Toggle `set_active` to animate like others
All other widgets allow you to pass an `animate` arg when setting
them as active, except CheckBox (and by proxy, its wrapper Toggle).
This is necessary for proper non-animated thigns like restoring the
state of a toggle from persistent storage, or other similar examples
where you don't really want the animation to occur (because that'll
look like the user did it accidentally or some kind of phantom movement).
* PortalList: pass "touch stop" (FingerUp) events to children, always
I had recently implemented a feature where PortalList would not pass
events down to its children if those events were being direclty handled
by the PortalList as part of its scroll-action. That was generally correct,
but it missed one rare case where a child widget was waiting on a
finger up (touch stop/release) for something like stopping a hover/down
animation.
So the child, like a button, could capture the initial FingerDown
but never the FingerUp, so they'd get stuck on the hover or down
animation. This fixes that issue by passing FingerUp-causing events
down to the child widgets.
Note that the children must use `was_tap()` on the FingerUp in order
to handle a regular click -- but they should have already been doing that.
So this doesn't break anything, it's just strictly a proper fix
for something that i should've covered previously.
* Separate apply-reload and script-reapply into different concepts
The goal here is to differentiate between "applies" that change the actual
Splash script "DSL" (i.e., the template) from a "re-apply" that doesn't
change the DSL template but does change runtime heap objects.
Mostly, we want to ensure that LiveEdit (the former) is different from
things that require heap updates (the latter), such as changing a theme
value or doing screen rotation that changes safe inset areas on mobile.
I don't know that I love this approach as a permanent solution,
but it's a good stepping stone until we can redesign LiveEdit/Apply
to funnel all of these various events through the same singular system.
We probably want to use different attributes on widget fields in order
to have more fine-grained control over what happens on an Apply action.
Generated list of brief details here:
* `Apply::ScriptReapply` variant + `is_script_reapply` /
`is_live_edit_reload` predicates; `Event::ScriptReapply` now applies
via `Apply::ScriptReapply`.
* `String` / `ArcStringMut` `script_apply` early-return on
`ScriptReapply`.
* Codegen: `#[deref]` runs before `#[apply_default]`'s recursive call
so animator state wins over template defaults.
* `Animator::script_apply_default` returns `state_object` on
`ScriptReapply`; new `current_state_apply()` helper for
`on_after_apply` hooks.
* `Cx::request_live_edit()` + `pending_live_edit_request` for primitive
heap mutations (safe-area insets baked into `script_mod!`
expressions).
* `handle_live_edit()` returns `LiveEditTrigger {None, FileChange,
Manual}`; `run_live_edit_if_needed` skips shader-cache reset and
same-tick `ScriptReapply` follow-up for `Manual` (fixes ~1s rotation
lag).
* iOS/Android post-event hook now drains both flags via
`run_live_edit_if_needed` (was firing `LiveEdit` indiscriminately).
* `Window` `WindowGeomChange` uses `request_live_edit()` for
safe-area.
* `StackNavigationView` gains `runtime_title` field re-asserted in
`on_after_apply`; new `StackNavigation::set_title` API.
* `app_main!` collapses 4 duplicate platform branches into a shared
`_app_main_event_closure!` macro.
* Switch to SLUG/DrawGlyph font drawing stack (via Codex)
* Improve same-frame new glyph caching and SLUG packed instances
* avoid performance regression by batch updating slug atlas cache
* TextFlow: separate SLUG glyph batches to avoid interleaving HTML text drawing
* SLUG optimization: append instead of a full clone each generation
* trying out codex perf fix for linux wayland
* Disable SLUG glyps on Linux for now
* Fix Linux SLUG rendering, warmup, and promotion behavior
- re-enable Linux SLUG through a separate Linux-only DrawText helper
instead of bloating the normal DrawText shader path
- preserve widget text styling on Linux SLUG by syncing common DrawText
state into the helper, including base colors, gradients, and interactive
states such as hover, focus, down, active, pressed, drag, empty, and
disabled
- keep normal Linux UI text on the raster/MSDF path and only switch to
SLUG above the Linux cutoff, while still falling back cleanly when SLUG
data is unavailable
- fix Linux SLUG glyph placement so promoted text uses the correct glyph
origin, layout position, and atlas packing
- add progressive SLUG warmup on Linux by budgeting glyph generation and
uploads across redraws and falling back to raster/MSDF until SLUG data
is actually ready
- lazily register and prewarm the shared Linux SLUG helper so app startup
and first-use latency stay low
- opt only the Linux SLUG helper into async GL shader compilation and use
parallel shader compile support when available, avoiding the large
startup and first-tab stalls seen before
- fix Linux runtime shader issues caused by copied widget text shaders and
custom get_color logic by using a shared helper shader with the expected
text-state inputs
- stabilize Linux SLUG promotion by preventing stale retained areas,
cleaning up raster/helper ownership correctly during draw, and
shadow-promoting the first ready SLUG frame before making it visible
- eliminate the visible SLUG handoff flicker so Linux text now switches
from raster/MSDF to SLUG without freezes or noticeable visual artifacts
- add a dedicated UIZoo SLUG tab with side-by-side below-cutoff and
above-cutoff examples, plus diagnostic cases for plain labels,
gradients, custom text shaders, and glyph/color probes
- keep the final diff focused by removing unrelated formatting-only churn
from the worktree during cleanup
* Tighten Linux SLUG promotion and helper sync
- make Linux SLUG promotion state local to each DrawText instance
instead of using a global per-redraw gate
- cache Linux SLUG helper shader field intersections so helper state
syncing avoids repeated per-draw allocations and linear membership checks
- harden the shadow-promotion fallback path so failed helper batching
only takes a single raster fallback path
- preserve DrawText memory alignment after adding Linux SLUG bookkeeping
* try to fix emoji on Android
* emoji fix take 2
* uizoo example: allow touch/drag scroll. Don't panic in FileTree demo
* Fix emoji on Android
* Windows: async HLSL shader compile + extend SLUG helper path to Windows
Fixes two major performance issues on Windows that made uizoo unusable on
first launch:
1. **60-75s startup stall** caused by synchronous `D3DCompile` of ~30+
SLUG-bearing text shader variants on the UI thread before the window
could present.
2. **3-4s hang when opening the SLUG tab** caused by synchronous compile
of the fat DrawTextSlug helper shader the first time a SLUG glyph was
needed.
Also fixes a latent HLSL-only `CreateInputLayout` E_INVALIDARG crash
triggered by shaders with >26 instance inputs (exposed by DrawTextSlug).
`DrawTextLinuxSlug` → `DrawTextSlug` and all `linux_slug_*` /
`LinuxSlug*` symbols dropped their `Linux` prefix. Cfg guards expanded
from `target_os = "linux"` to `any(target_os = "linux", target_os = "windows")`
so Windows now:
- uses the same lean base `DrawText` shader (SDF/MSDF only, no SLUG
curve-solver HLSL inlined)
- uses a separate `DrawTextSlug` helper shader for the SLUG path
- has the same progressive glyph-build budget and DPI cutoff
(`default_slug_new_glyphs_per_redraw`, `default_slug_min_dpxs_per_em`
in `fonts.rs` now match `OsType::Windows` alongside the Linux variants)
- falls back to raster/MSDF while the SLUG helper shader or its glyph
data isn't yet ready
macOS/iOS/Android/WASM paths are untouched — they still use the fat
all-in-one `DrawText` shader via `cfg(not(any(linux, windows)))`.
Added `AsyncHlslCompile` in `d3d11.rs` and an `async_hlsl_compile` field
on `CxOs`. Shaders flagged `async_compile: true` (the SLUG helper) now
dispatch to a background thread per shader via `std:🧵:Builder`
(named `hlsl-compile-<id>` for debugging). Workers call `D3DCompile`
off the UI thread, write the resulting DXBC to the on-disk cache, and
send only a status result (not the bytes themselves — SLUG is ~240 KB
and ferrying it through the channel is wasteful) back via an mpsc
channel guarded by a `Mutex`.
`hlsl_compile_shaders` drains completed results at the top of each
call, constructs `CxOsDrawShader` objects on the main thread (the
bytes come from the cache-hit path in `CxOsDrawShader::new`), and
triggers `redraw_all()` so widgets whose shaders just became ready
get re-rendered. The existing `sh.os_shader_id.is_none()` guard in
`render_view` handles skipping the draw call while a shader is
pending.
Added `Cx::is_draw_shader_window_ready()` on Windows for the SLUG
helper's readiness check; on Windows it's simply
`os_shader_id.is_some()` since HLSL compile is either synchronous
(cache hit) or tracked via the async path.
Cold-start still compiled 30+ shaders synchronously (parallelized via
`std:🧵:scope`) which took ~5-10s because FXC's per-call speed is
the bottleneck and parallelism helps less than expected. Now
`hlsl_compile_shaders` partitions queued shaders by cache state:
- cache hit → sync path: disk read + D3D11 object creation, a few ms
- cache miss OR `async_compile: true` → async path: worker thread
On a fully cold cache, every shader is a cache miss → the window
presents on the first frame with no compile work on the UI thread.
Widgets fill in over the next ~1-2s as their shaders become ready.
On a warm cache every shader is a hit → instant startup as before.
Added `shader_bytes_cached()` for cheap existence checking of the
cache entries.
`d3d_compile_hlsl` now passes `D3DCOMPILE_SKIP_OPTIMIZATION`. FXC's
optimizer is what makes individual compiles burn hundreds of ms to
seconds on text shaders with loops; UI shaders don't benefit enough
from it to justify the cold-cache cost. If a specific shader is later
shown to be a runtime hotspot, the fix is to recompile it optimized
on a background thread and hot-swap, not to pay the cost upfront for
every shader.
Bumped `CACHE_KEY_VERSION` to 2 so pre-existing `.dxbc` blobs compiled
with the old flags are invalidated cleanly on upgrade.
`d3d11.rs` used its own `index_to_char(i) = i + 'A'` which produced
invalid HLSL semantic names (`[`, `\`, `]`, …) past 26 inputs, while
the HLSL generator in `shader_hlsl.rs` already used a correct
multi-character scheme (`A..Z, AA..AZ, BA..`). `CreateInputLayout`
returned E_INVALIDARG because the names didn't match. Replaced with
`makepad_script::shader_hlsl::index_to_semantic` so both sides of
the binding agree.
This was a latent bug — no existing shader had >26 instance inputs
until `DrawTextSlug` (which inherits many interactive-state fields
from Label-derived shaders). Linux/GLSL is unaffected because GLSL
binds by identifier, not semantic name.
- Hoisted `d3d_compile_hlsl`, `hlsl_cache_key`, and
`get_or_compile_shader_bytes` out of `CxOsDrawShader::new` to module
scope so they can be shared with the async worker.
- Compute `hlsl_cache_key` once per shader during partition and reuse
at dispatch instead of recomputing.
- Scoped borrows in the async drain loop eliminate mapping/bindings
clones.
- `platform/src/os/windows/d3d11.rs` — compile pipeline, async infra,
semantic-name fix
- `platform/src/os/windows/windows.rs` — `async_hlsl_compile` field on
`CxOs`
- `draw/src/shader/draw_text.rs` — rename `LinuxSlug*` → `Slug*`,
expand cfg gates
- `draw/src/text/fonts.rs` — extend SLUG cutoff/budget defaults to
Windows
No changes to macOS, iOS, Android, or WASM paths.
TL;DR: the shader compiler needed explicit casts in for loop bounds,
and the window positioning was messed up, causing the app-level title bar
to overlap with the native OS-level title bar (which means you couldn't
see or press the window chrome buttons)
--------
here's an AI-generated summary of the changes, for more details:
On OpenGL ES 3.0 targets (Linux/EGL, Android), shaders containing `for` loops
over `uint` variables failed to compile with a GLSL type-error. The shader
compiler emitted the loop header as:
```glsl
for(uint i = 0; i < 4; i++) { … }
```
The integer literals `0` and `4` are of type `int` in GLSL. GLSL ES 3.0 forbids
implicit casts between `int` and `uint`, so the initialiser and the comparison
both produce a compile error. The other shader backends (Metal/WGSL/Rust) do not
have this restriction, so no corresponding arm existed for GLSL.
**`platform/script/src/shader_control.rs`** — Add a `ShaderBackend::Glsl` arm to
`handle_for_1` that wraps both loop bounds in an explicit constructor call for the
loop variable's type:
```glsl
for(uint i = uint(0); i < uint(4); i++) { … }
```
This satisfies GLSL ES 3.0's strict no-implicit-cast rule and matches the
behavior already implemented for the WGSL backend (which uses typed variable
declarations for the same reason).
**`platform/src/os/linux/opengl.rs`** — Gate the helper functions
`shader_source_hash` and `shader_source_preview` (and their call-sites) behind
`#[cfg(target_os = "android")]`. These functions are only referenced from
Android-specific shader-cache code paths; without the attribute the compiler
emits dead-code warnings on every other Linux/OpenGL build.
- `platform/script/src/shader_control.rs`
- `platform/src/os/linux/opengl.rs`
----------------------------------------------------------------------------
On GNOME (and likely other modern WMs), restoring a saved window position would
consistently produce two visual artifacts:
1. **No WM title bar visible** — the client area was rendered where the title bar
should appear.
2. **Black bar at the bottom** — the bottom portion of the window surface was not
covered by rendered content.
The root cause was two related issues in `xlib_window.rs`:
**Issue 1 — `XMoveWindow` called after `XMapWindow` (races with WM reparenting)**
After `XMapWindow`, the window manager asynchronously reparents the client window
into a decoration frame. If `XMoveWindow` is called after reparenting has occurred,
the coordinates are interpreted relative to the WM frame rather than the root
window. For example, calling `XMoveWindow(client, 23, 89)` after GNOME reparents
places the client 89 px from the top of the WM frame. Since the title bar is only
~37 px tall, the client ends up 52 px below the title bar, and its bottom edge
extends 52 px *beyond* the bottom of the WM frame. GNOME responds by resizing or
repositioning the client, producing the rendering artifacts described above.
**Issue 2 — No `USPosition` hint set**
Without the `USPosition` flag in `WM_NORMAL_HINTS`, GNOME ignores the position
provided to `XCreateWindow` and applies its own smart-placement algorithm. This
meant the application relied entirely on the post-map `XMoveWindow` call described
above, which was itself broken.
**`platform/src/os/linux/x11/x11_sys.rs`** — Add the standard `XSizeHints` flag
constants:
- `USPosition` (`1 << 0`) — user-specified x, y
- `USSize` (`1 << 1`) — user-specified width, height
- `PPosition` (`1 << 2`) — program-specified position
- `PSize` (`1 << 3`) — program-specified size
**`platform/src/os/linux/x11/xlib_window.rs`** — Two changes in `XlibWindow::init()`:
1. Before calling `Xutf8SetWMProperties`, populate an `XSizeHints` struct with
`flags = USPosition | PPosition` (and `x`/`y` set to the requested coordinates)
when a position was provided. Pass this struct as the `WM_NORMAL_HINTS` argument
instead of the previous `ptr::null_mut()`. This tells GNOME/Mutter to honor the
requested position rather than running its own placement heuristic.
2. Move the `XMoveWindow` call to *before* `XMapWindow`. At that point the window is
still a direct child of the root window, so the coordinates are unambiguously
root-relative. This eliminates the race with WM reparenting entirely.
- Android log levels: map log! macro levels to Android log priorities
(ERROR=6, WARN=5, INFO=4). Some devices suppress DEBUG by default.
- cargo-makepad android: show help text instead of panicking on missing
or invalid subcommand.
- Android build: discover .class files dynamically instead of hardcoding
~25 individual paths. Add Java 8 source/target flags.
- Remove deprecated AsyncTask import from MakepadNetwork.java.
- Studio stdout: add newline after JSON messages for JSON-lines parsing.
- Studio stdout: skip profiler timing in stdout mode to avoid overhead.
- Script thread: fix panic on empty call stack in call_has_me/call_has_try.
- Cursor: reset to Default on FingerHoverOut in TextFlow and TextInput.
Co-authored-by: ant <ant@offline.click>