* macOS: re-arm the display links when a window leaves the Dock
`pause_display_link` only sets `setPaused: YES` and keeps the links, so
`display_link_needs_rearm` stays false and `ensure_timer0_started` never
clears `timer0_armed`. A work beat while minimized arms the NSTimer with
that flag set, so deminiaturize early-outs before anything unpauses the
links and the restored window paces on the timer, not its own panel,
until the next idle downshift.
Clear the flag first, like the pointer-capture release does.
* Vulkan: stop printing the loader's startup narration by default
`vulkan_debug_messenger_create_info` asked for `INFO` severity on the
`GENERAL` message type, which is the channel the Vulkan loader narrates
itself on. Every run dumped around ninety lines naming each directory it
searched for layer and ICD manifests, each manifest it found, and the
layer callstack it assembled, before the app had drawn anything. None of
it comes from the validation layer -- that only loads under
`MAKEPAD_VULKAN_VALIDATION` -- so it was noise on every Linux desktop,
Android and OpenXR run, on every machine.
* Subscribe to `ERROR | WARNING`, adding `INFO | VERBOSE` only when
`MAKEPAD_TRACE=vulkan.debug` is set. The driver skips the callback for a
severity we did not ask for, so the loader no longer formats the lines
either. Validation errors and warnings still print unconditionally.
* Route the informational branch of the callback through `trace!`, so it
carries the topic that enabled it like the `gl.*` and `shader.*` ones.
* `devices` logged a line per software device it stepped over, which fires
on any machine carrying lavapipe -- that is most Mesa systems. Move it to
`MAKEPAD_TRACE=vulkan.device`, and instead say so once when software
rasterizers were the *only* devices found, since every caller then
reports no usable device, which reads as if the machine had no Vulkan at
all rather than no accelerated one. What happens next is left to the
caller that decides it: `linux_wayland` already logs its OpenGL ES
fallback.
* Two Android camera-import sites used `warning!` for a plain dump of
image size and format on the success path; they become
`MAKEPAD_TRACE=vulkan.camera`.
A desktop Linux run now prints the one line that says what it got:
Vulkan: NVIDIA GeForce GTX 1070, graphics/present queue 0
`MAKEPAD_TRACE=vulkan` brings all of it back; the topics are hierarchical,
so `vulkan.debug`, `vulkan.device` and `vulkan.camera` also work on their own.
iOS: VideoFileDecoder::open_audio guarded its Apple path with macOS alone, so iOS fell through to UNSUPPORTED, a constant no Apple target has. It takes the same split as every other entry point in that file.
tvOS: libs/apple_sys opened with a crate guard of macOS or iOS, so on tvOS the crate compiled to nothing and every msg_send! user lost the macro; the crate guard and its 27 inner guards of that shape now name tvOS, and MTLCopyAllDevices is macOS only, which is where it exists. The platform's Apple video playback, player and YUV modules, the 17 guards of the Metal NV12 video path and the texture-pool imports follow, and the tvOS app gains try_metal_device, the lookup the texture adopt path already calls on iOS.
wasm: the window manager's dylib host needs a process, a linker and a loader, so it is native only; the web gets a stand-in with the same surface that refuses every compile through the queue the native host answers on, and libloading is a non-wasm dependency.
Windows: three Unix-only uses in apps/wm/src/clients.rs (Cx, CancellationToken, the grace argument) are guarded to match where they are used, tests included.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`pause_display_link` only sets `setPaused: YES` and keeps the links, so
`display_link_needs_rearm` stays false and `ensure_timer0_started` never
clears `timer0_armed`. A work beat while minimized arms the NSTimer with
that flag set, so deminiaturize early-outs before anything unpauses the
links and the restored window paces on the timer, not its own panel,
until the next idle downshift.
Clear the flag first, like the pointer-capture release does.
The CEF follow-up (libs/cef, widgets/src/browser.rs, apps/browser): BrowserOptions with software frames, evaluate_javascript answered as JSON or the exception text, console messages taken by the embedder, editable_focus, with_cef_browser on the widget, the profile flushed on Event::Shutdown, and a repr(C) mismatch in the FFI fixed.
platform/video: VideoFileEncoder::new_fragmented lays the container down in movie fragments (AVAssetWriter's movieFragmentInterval), proven by tests/fragmented_growing.rs: 90 of 120 frames readable before finish, all 120 after. Windows and Linux write one movie as before. The Apple backend's plain constructor went with it: nothing called it once the fragment-aware one existed.
platform: the audio output fence is seated in the one seam (media_api.rs), so a panic in an app's output closure costs that closure and its buffer, not the device thread, and the taps are fed the silence so a recording keeps its place; its tests adapted to this tree's tap registry. /midi routes inject a message as if a device sent it, declare ports for the app to adopt, read back what the app sent, and reset (platform/src/midi.rs, remote.rs).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The child draws its frame through a texture pass, which on GL renders through an inverted projection, so its glReadPixels rows come in picture order already; the host keeps them as they are, and the two run views sample the texture as stored on every path. The old shader flip on the software path inverted it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A custom-camera pass used to keep GL's bottom-up storage while the 2D passes were inverted, so a 3D scene rendered to a texture came out upside down on Linux and Android GL and nowhere else; it now uploads an inverted copy of its projection as the web backend does. Backface culling follows with a clockwise front face on those passes, and a target allocated taller than its pass keeps the pass at row 0 instead of the far end.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The 2D camera is GL-style: the top of a pass rect lands at clip y = +1 on every backend. Vulkan's clip space points down, so a pass drawn with that camera has to go through a negative-height viewport, window and capture alike; the window comes out upright and a capture's rows are stored top-left like Metal's. fbfec26ad made both viewports positive to cure an inverted phone desk, and every desktop Vulkan window stood on its head (the Scope report of 2026-09-20). The desk was inverted by its own OS-keyed consumer flips, not by the viewport, so those go: the gauss stack's per-OS flip and its callers, the phone shell's three Android flips and its y_flip shader term, the dock warp's capture_y_flip and its term. The direct display's letterbox blit keeps its positive viewport: its own vertex shader maps uv.y = 0 to clip -1.
Seen right side up by eye on the Arch RTX 5090 box: apps/wm as a Wayland client under sway, the hosted apps inside it, and the linux_direct WM on the panel; and on the Pixel 11 Pro XL the wm-dyn super-app (Vulkan, no GL fallback). Codex review in the session's notes endorses the restores and removals and leaves two flips for a later look: the SSAA resolve's 1.0 and the map shadow mask's Metal flip, both untouched here.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Taken from vjroger/work. A full merge of that fork fights this work branch
in widgets; this is the stems crate, vocal mel-band, span-cache lanes,
log_ring, output fence, midi inject, effect_doc, mp3 sniff, mp4 audio
edit, and audio-only file open. Stage on origin/main cargo-checks again.
Every manifest `app_main!` emitted carried three fonts no theme role uses:
`NewCMMath-Regular.otf` for `MathView`, and `Inter.ttf` / `RobotoFlex.ttf` for the opt-in
iOS and Android platform styles. That was 3.6 MB in every package, and it only existed
because a font an app forgot to declare failed silently: `FontFamily::update_font_definitions`
skipped a member whose bytes never arrived, so the text showed as boxes with no log line.
* Drop the forced extras. The manifest is now the font set's fallback chain plus whatever
the app puts in `font_assets`, which is what makepad's own apps already did for `Inter`.
* `font_assets` takes expressions, and `INTER_FONT_ASSET` / `ROBOTO_FLEX_FONT_ASSET` join
`MATH_VIEW_FONT_ASSET`, so an app declares a font by name instead of by path.
* A font that never loads now logs one `error!` naming the path and the fix. A missing
member still degrades gracefully: the family keeps its remaining members.
* The apps that use those fonts declare them: the `wm` family binds both platform faces,
`clock`, `weather` and `director` draw with Inter and can select any style, `terminal`
and the builder use Inter for symbols, `splash` and `aichat` use `MathView`.
`drm_sys` carries `#[link(name = "drm")]` but was compiled for every non-Android
Linux target, so an ordinary desktop build fails to link on a machine without
libdrm, even though nothing outside the DRM/KMS backend calls into it.
* Gate the module on `linux_direct`, the same cfg its only callers already
carry: `drm_native_resolution` in `vulkan_linux` and the `direct` module.
`NSView.displayLink` never fires for a window in the Dock, and the 0.2s
NSTimer fallback only starts from inside the timer 0 branch that the link
drives. So minimizing froze the UI thread, and Ctrl+C / SIGTERM / SIGHUP
sat in `REQUESTED` until the window came back.
* swap the paint clock on `windowDidMiniaturize:`/`windowDidDeminiaturize:`
* skip link pacing while every window is miniaturized
* termination worker restores `SIG_DFL` if it gives up, so the process
can't end up unkillable
Measured on the Pixel 11 Pro XL (PowerVR): a pane swipe presented at
~84 fps on the 120 Hz panel, a vsync dropped every three to six frames.
simpleperf showed 22% of the CPU in the driver's shader compiler and 24%
in its render-target teardown: the Vulkan backend created a VkRenderPass
and a VkFramebuffer for every offscreen pass on every frame and destroyed
them after the fence, and on this driver each render pass compiles a
load-op shader. Offscreen draw render passes now live for the device
(keyed by formats and load/store ops) and framebuffers are cached per
render pass, attachment views and storage extent, invalidated through
texture retirement so they die after the frame that used them.
The app icons were re-tessellated from SVG every frame: one DrawSvg kept
one scale and the desk draws each icon at two or three sizes. A DrawSvg
keeps up to four meshes per device scale; the geometry pool defers frees
and releases them once per frame against the geometry ids the live draw
lists still name, so a retained draw call never sees its slot reused.
A font member whose resource can never load (the WM referenced Inter and
its other faces through `self:../../widgets/...`, unmapped in a package)
kept its family incomplete, and an incomplete family is redefined every
frame: the layout cache cleared, every label laid out again, the asset
reopened. Such a member drops out of its family once, logged, keyed on
the resource registry's generation so a resource that appears later is
asked for again. The WM names its fonts through `makepad_widgets:` and
reads the clock in-process on the UI thread instead of forking `date`
twice a second.
Android gains a `frame.cpu` trace (events, next-frame, draw and repaint
milliseconds per drawn frame) and a profileable manifest so simpleperf
can sample a release build. The dyn-pack tile proof tolerates the app's
own Dirty line after an engine rebuild.
After: SurfaceFlinger presents every swipe frame at 8.3 ms, the render
thread runs at ~45% instead of 85–97%, and the frame is paced by the GPU.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squashed from vjroger/makepad `storybook-pr` at 086d25452 (1,332 commits on top of
6f1e44649; his last commit restored every path outside the contribution to upstream).
- apps/storybook: every component organised, documented and previewed live.
- widgets: about eighty new widgets (accordion, alert, avatar, badge, breadcrumb, calendar,
card, carousel, chat, chip, colour, command palette, date and time pickers, dialog,
dropzone, floating action and panel, form, hamburger, line and radial menus, kanban,
masonry, menu, nav list, pagination, pill nav, popover, progress, property inspector,
range slider, rich text, select, spinner, table, tabs, tag field, timeline, toast,
toolbar, tour, transfer, tree, waveform, wheel picker and more); theme tokens and a
theme store, themes mixed by weight with a legibility check, a twelfth style sheet in
black and orange; the data grid gains row selection, drag and reorder, heading tips and
alignment; the glass button is a water lens; the portal list keeps the wheel it uses,
stands down from a press another control holds, can keep a row on screen and rule the
gap under a short list.
- platform: sweep locks and scroll blocks nest, `is_mouse_held_outside`, per-axis
scroll-handled flags, `next_frame_is_pending`, owner-scoped scroll unblocking, a
hands-off marker for the remote bridge, exploded-view projection and focus.
- draw: the interior distance of square-cornered boxes, a pointer shape, and
`turtle_ancestor_clip`.
- wm: the style tween carries an eighth weight for the new sheet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The APK carries rustc, the checkout and a packed target tree as LZ4
frames. First compile streams them out of the asset manager into files/
and shows progress on the desk; later launches skip unpack when the
stamp matches. App crates keep a desktop Cargo.toml — dynamic-module is
empty — and rustc `--extern force:` binds makepad_wm_engine already in
the process so widgets stay the host dylib.
libs/lz4 grows a streaming frame codec and a makepad-lz4 CLI; libs/tar
unpacks those frames without buffering the archive. Android Vulkan
records two in-flight repaints instead of waiting every pass, and the
capture Y-flip applies only on the OpenGL fallback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review of the retained renderer on top of the runtime GPU choice: a
refused GL retained upload skips the draw and dyn-uniform writes are
bounds checked; Vulkan draws retained publications whose CPU-side data
is empty and records a fresh retained transfer command buffer after
each submission; Metal stamps consumption for every encoded retained
item, empty ranges included; a refused WebGL retained upload is final
for that content.
TaskPool::new_with_priority sets the heavy workers' thread priority.
Remote control: keys no longer hold the gate, if_user_seq is optional,
and status waits through a stall; `--focus` brings the app to the front
as its macOS window opens. The package map, the remote activity ledger
and the GPU choice are owned Cx state, not globals.
Squashed from work:
- platform: a refused GL retained upload skips the draw; dyn-uniform writes are bounds checked
- platform: TaskPool::new_with_priority sets the heavy workers' thread priority
- platform: Vulkan draws retained publications whose CPU-side data is empty
- platform: MAKEPAD_FOCUS activates the app when its window opens on macOS
- platform: `--focus` brings the app to the front as its macOS window opens
- platform: Vulkan records a fresh retained transfer command buffer after each submission
- platform: Metal stamps consumption for every encoded retained item, empty ranges included
- remote: keys no longer hold the gate, if_user_seq is optional, status waits through a stall
- platform: a refused WebGL retained upload is final for that content
- platform: the package map, the remote activity ledger and the GPU choice are owned state, not globals
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Choose the Linux GPU backend at runtime and pace Wayland frames adaptively
A Vulkan-capable desktop Linux build (the `vulkan` feature, or
`MAKEPAD=vulkan`) now carries OpenGL ES as well and picks between them
when its event loop starts: Vulkan on Wayland when a hardware device
answers, OpenGL ES when none does (no driver, only a software rasterizer,
or an X11 session). `MAKEPAD_GPU=auto|gl|vulkan` overrides the choice, and
`MAKEPAD=gl` still produces an OpenGL-only binary. The feature stays
opt-in: the hosted (`--stdin-loop`) and direct renderers of such a build
are Vulkan-only, and Vulkan has no video texture import yet.
Wayland frame pacing
- Pace presents by what the backend and the session can actually do,
rather than by a fixed number (new `wayland/frame_pacer.rs`). Vulkan
runs two presents in flight only when the compositor offers `fifo-v1`
and the driver uses it; otherwise a second present would block inside
`vkQueuePresentKHR` on a callback an occluded window never receives.
OpenGL starts the next frame early only when its measured cost says the
swap would land after the outstanding callback is due, so cheap frames
are not committed twice per refresh. One present in flight, which is
what this did before, left a heavy scene at half the display rate.
- Bound the pacing gate at 250 ms so an occluded window cannot freeze the
app's clocks, and let pending screenshot requests through it.
- Treat WouldBlock on the display flush as transient.
Vulkan
- Bound the frame fence wait and the swapchain acquire on Linux instead of
waiting forever.
- Keep the per-frame packet arena mapped, recycle completed frame
resources on the window path, and ask for one more swapchain image on
Linux, where the pacing can keep two presents queued.
- Skip CPU devices unless `MAKEPAD_GPU=vulkan` asks for Vulkan explicitly.
OpenGL
- Stop repainting forever at rest: poll the texture lifetime fence once
per frame, and check for time-driven shaders only after the
zero-instance skip, as Vulkan does. The explicit
`Cx::frame_completion_serial` poll still always arms a fence.
- Upload draw-call uniforms only when they changed; they were uploaded
twice per draw call per frame. A zbias shift now marks them dirty, so a
call skipped that frame still uploads when it next draws.
- Compute the retained-instance upload plan once per buffer per frame; it
was computed three times.
- Target remote screenshot requests at the presenting window. Every
`--remote` grab timed out on OpenGL before this.
- Emit the `gpu.present` trace with render and swap timings.
Retained instances
- `upload_plan` settles segments that kept their slot and offset by `Arc`
identity, scans for the first few that moved, and only then builds a
pointer-keyed map. On a large map this took a plan from 0.3-1.5 ms to
about 0.07 ms. Results are identical to the previous planner.
- Add `collect_backlog` so a renderer can drain retirements once a frame.
Runtime backend consistency
- `CxOs::vulkan_active()` replaces the compile-time branches that decided
between the two renderers, so a build that fell back to OpenGL releases
its uniform buffers, shares host swapchains and retires textures the way
an OpenGL build does.
Wayland teardown
- Drop windows before the `Connection`, and destroy a window's EGL surface
and `wl_egl_window` before its `wl_surface`. Every OpenGL exit on
Wayland segfaulted inside NVIDIA's egl-wayland.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Make Vulkan the default on desktop Linux, with mipmaps and hosted fallback
Every desktop Linux binary now carries both renderers and picks at startup,
instead of only the apps that asked for Vulkan by name. Three things had to
be true first.
Gate the feature where the fallback exists. build.rs derived `use_vulkan`
from `target_os == "linux"` alone, which also matches OpenHarmony and every
other Linux triple, none of which carry `naga`, and it ignored
`MAKEPAD=linux_direct`, whose DRM/KMS renderer has no OpenGL fallback of its
own. The feature now only reaches x86_64/aarch64 gnu windowed builds;
`MAKEPAD=linux_direct+vulkan` remains the way to ask for direct Vulkan.
Give Vulkan a mip chain. `image_cache_use_mipmaps` was off for Vulkan
because the uploader only ever filled level 0, so every minified image
aliased. Images now allocate their full chain and fill levels below the
first with `vkCmdBlitImage`, the way `glGenerateMipmap` does, skipping
formats the device cannot linearly blit. On Robrix's sign-in icons this
takes Vulkan from 2153 pixels differing from the OpenGL render by more than
8, to 400.
Choose the hosted renderer at runtime too. `--stdin-loop` mode was
Vulkan-only in a Vulkan-capable build and panicked when no device answered,
while its host, on an X11 session, had already fallen back to OpenGL: with
the feature on by default that combination would have killed every child the
wm launches. The hosted path now selects the way the windowed one does, its
import follows the renderer the process actually started, and a hosted child
rejects software devices for the same reason a window does.
Video and `Texture::read_back` are still OpenGL-only; the video error now
names `MAKEPAD_GPU=gl`, and the feature comment says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* workspace: drop the renderer-routing argument and its build-time wording
One desktop Linux binary now carries both GPU backends and picks at startup,
so an app honouring a saved renderer choice passes it to the platform as
MAKEPAD_GPU and restarts itself. Nothing produces `--renderer-routed` any
more; an argument this parser does not know was already ignored, so dropping
its arm changes nothing for anyone still passing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Keep the shared Android and direct-display paths as they were
An audit of what this branch reaches on platforms that share these files
found five places where it changed behaviour it was never meant to touch.
All of them come from code this branch made runtime-selected or relaxed.
Mipmaps are desktop Linux only, matching `image_cache_use_mipmaps`, which
is what asks for the format. An Android or Quest Vulkan build shared the
new chain code and would have allocated levels and recorded blits that
nothing there requests and nothing measured.
The mip chain also needs more of the format than it asked for. It checked
only that the format samples linearly, while `record_mip_chain` blits
between levels, so it now requires BLIT_SRC and BLIT_DST too and keeps a
single level otherwise.
Shader compilation stays a compile-time answer off desktop Linux. Whether
a draw shader is compiled to SPIR-V became a runtime `vulkan_active()`
test, which on Quest would follow an Android Vulkan init failure instead
of the build. Only desktop Linux has that fallback.
The hosted loop compiles GLSL only when OpenGL is the renderer. Losing its
cfg left it calling `gl()` in a Vulkan hosted child, which has no EGL
context, so it panicked. Its Wayland sibling already guards this way.
The direct display build keeps its software-buffer upload. `texture_for_draw`
gained a `not(linux_direct)` that was never needed: `MAKEPAD=linux_direct`
without Vulkan has its own `upload_presentable_image_software_buffer` in
os/linux/presentable.rs, and the outer gate already excludes the direct
Vulkan build.
Also: `gpu_preference` is now gated exactly where its caller is compiled,
since `vulkan_linux.rs` builds for every `target_os = "linux"` under
`use_vulkan`, and the hosted loop's imports follow the block that uses
them, which the direct Vulkan build does not compile.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retained renderer support is back for Scope on Metal, Vulkan, OpenGL,
WebGL and the simulated GPU. A `vulkan` cargo feature picks Vulkan on
desktop Linux, Cx::gpu_backend() names the compiled GPU API, and the
direct WM builds again. Settings.renderer in libs/workspace keeps the
saved GPU API choice (Vulkan | OpenGL) behind the --renderer-routed
argument. The Android build keeps the texture alloc types imported for
OES adoption, and that import stays off the web build. The simulated
GPU builds on Linux again.
Squashed from work, without the cargo vendor snapshot the retained
renderer commit carried there:
- platform: a `vulkan` cargo feature picks Vulkan on desktop Linux; Cx::gpu_backend() names the compiled GPU API; the direct WM builds again
- workspace: Settings.renderer — the saved GPU API choice (Vulkan | OpenGL) and the --renderer-routed argument
- Restore retained renderer support for Scope
- platform: Android builds again — the texture alloc types stay imported for OES adoption
- platform: the Android texture-adoption import stays off the web build
- platform: the simulated GPU builds on Linux again
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Button: add `label_align` to center a wrapped label
`Button::draw_walk` passed `Align::default()` to `draw_text.draw_walk`, so
a label that wrapped onto more rows left-aligned them under each other even
when the button itself centered its content, and no script property could
reach that argument.
* New `#[live] label_align: Align`, mirroring the one `TextInput` already has.
* Defaults to left, so every existing button draws exactly as before.
* Pair it with a `Fill`-width `label_walk` to give the rows room to move.
* macOS: don't paint into a drawable the layer has since outgrown
The prefetching `DrawableWorker` hands back a drawable that `nextDrawable`
acquired on the previous beat. A frame that straddles a resize therefore
attaches a texture of the old size to a viewport derived from the new one.
* moving a window between displays of different DPI drew the whole UI at
the old scale, and it stuck: a dpi change dirties the pass exactly once
* dragging a border fast left the strip the old texture didn't cover
unpainted, which macOS shows as magenta
Check the invariant where the drawable is consumed: a texture whose size
isn't the layer's `cal_size` is dropped and one is acquired on this beat,
the way the pre-worker path did every beat. The pool was just rebuilt for
the new size, so that acquire doesn't block. Traced on the `present` topic.
* Window: don't put an app icon in the caption bar by default
`AppIcon` falls back to a generic placeholder tile for any app id makepad
ships no artwork for, so every third-party app got a meaningless icon next
to its title, and an off-centre title with it.
* `AppIcon` gains `visible`, which it had no way to express before
* the caption icon defaults to hidden; apps opt in with
`caption_icon +: {visible: true}`
* Quiet the startup and per-decode log spam
A plain run printed ~1500 lines before anything happened.
* `zune-qoi` was the only zune crate with `log` in its default features,
and cargo unifies that onto `zune-core`, so every zune decoder logged
per image. zune-core's macros became real logging in this tree, so what
used to be inert now floods the log
* memory budget, task-pool priority/summary and the Metal retained-upload
budgets move onto the `MAKEPAD_TRACE` topics this tree added
(`memory`, `pool`, `gpu.upload`)
* drop the studio-websocket line, which only says a disabled thing is off
* Make the UI-hang sampler opt-in
It started with every `Cx`, so a shipped app carried a thread waking ~16
times a second forever, and any stall over 250ms got the UI thread
suspended once per sample while its stack was walked.
`MAKEPAD_UI_HANG_MS` is now the switch as well as the threshold. Unset, no
thread starts and the phase guards see a null registration, which is a TLS
read and a null check.
* Wake the event loop from render workers without raising the UI signal
`Event::Signal` means "a worker has something for you" and is dispatched to
the whole widget tree. The submitter, the instance allocator and the
drawable worker raised it after every commit, so an app painting at 120fps
walked its tree 120 extra times a second. Measured in robrix: 119 signals
against 111 repaints, down to ~0.3 per frame.
They only ever wanted the loop awake, so give them `wake_ui_loop()`, which
is what `set_ui_signal` already called underneath. A dirty pass is what
keeps the paint clock armed, so nothing depends on the flag to get painted.
* Compare the resident instance bytes instead of hashing them
`immediate_payload_hash` FNV'd every byte of every dirty draw call to skip
the upload when nothing changed. That suits a few big payloads, not a 2D
frame: robrix scrolls ~1090 draw calls of ~110 bytes, and the hash cost
2.3ms a frame in a debug build to skip ~12% of 110KB of uploads.
Instance buffers are StorageModeShared, so the resident copy can just be
compared. `memcmp` stays fast in an unoptimized build, and an exact
comparison can't collide into a stale frame the way a hash can.
* Install a platform stylesheet only when an app asks for one
`current()` picked "ios"/"android" straight off `OsType`, so any app built
for a phone was silently restyled: ~270 theme tokens including the fonts,
over whatever the app had already set.
Worse, it only half-landed. `apply_theme` runs from `widgets_mod` but
`apply_widgets` runs from `script_mod`, so an app that calls the
`theme_mod` + `widgets_mod` pair got the mobile palette with desktop
metrics. Every in-tree user already calls `install` or sets
`MAKEPAD_WIDGET_STYLE`, so that variable is now the only implicit route.
* Let an internal drag deliver its pointer events on Linux and wasm
Moving internal drag handling into shared code changed it in two ways that
the macOS and old X11 paths never had, and a dock tab shows both.
The pointer event was replaced by the drag event rather than followed by
it, so no widget saw the `MouseUp`. `Tab` sets `is_dragging` on FingerMove
and clears it only on FingerUp, so a tab could be reordered once and then
never dragged again. Dispatch the pointer event first and append the drag
one, with a flag so that dispatch doesn't produce the drag event again.
A release that never moved after `start_dragging` also produced no Drop and
no DragEnd at all, so `Dock` never cleared `dragging_tab` and kept painting
the ghost. `Tab::min_drag_dist` has no default, so a press, one motion and a
release reaches it. Every other backend ends the drag unconditionally.
* Split the UI signal so makepad's own queues don't broadcast Event::Signal
`set_ui_signal` was the one wake for everything, and every platform loop
answered it by running makepad's handlers AND broadcasting `Event::Signal`
to the whole tree. So termination, the network runtime, live reload and
every pool completion (label shaping, per frame) woke every widget.
* `set_internal_signal`: the loops run their handlers and don't broadcast
* `TaskPool::submit_internal`: a job whose result makepad polls at draw
* `Event::Signal` is documented, and the loops treat the app flag as a
superset of the internal one, so nothing left on it can regress
The scheduler keeps the app signal on purpose: `service_scheduler` re-arms
the platform timer from `call_event_handler`, which only the app half runs.
Media device changes still go through `SignalToUI::set`, whose instance API
is app-facing; they are hotplug-rare, so splitting that is left alone.
* Harden the drawable re-acquire, the internal drag and the opt-in sampler
Follow-ups from reviewing the five commits above.
* the resize re-acquire only runs while the drawable pool has a free slot.
Exhausted, `nextDrawable` blocks the UI thread on the compositor, which is
what the worker exists to avoid; skip the beat and stay dirty instead
* the byte compare no longer skips an item the GPU has evicted, which would
leave it invisible in a pass that then repaints forever
* the internal drag suspends its items across the pointer dispatch instead
of holding a flag. An unwound dispatch now ends the drag rather than
wedging it for the life of the process, and a widget that starts a new
drag from that dispatch keeps it instead of tripping "start drag twice"
* `tests/ui_hang.rs` opts the sampler in, since it is the thing under test
Squash of 1 work commits (Sep 12–12):
e74b919 platform: the CPU simulated-GPU backend is `gpusim` — the word "headless" now means only window-less
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 2 work commits (Sep 9–12):
8d851ef platform: per-draw alpha blend, macOS waker, texture lifetime, file drop
cc2fa8f platform: the Metal display link is gone; the hang line prints its tick deficit; hidden instances refuse App Nap
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 27 work commits (Sep 3–12):
aa907ca platform: a frame trace on both desktop backends, and a window that cannot present stops ticking at 600 Hz
e3abf4d map: the hosted-tile cache keeps the same ~2x-visible margin as the local one — a budget pinned at the visible set evicted the trailing edge of every pan on the next frame and refetched it a moment later
8d542fe platform: a child pass orphaned by its attaching draw list is no longer painted — the window's gauss_scene pass stayed a live_with_parent child after the map went flat and re-encoded a frozen 400-item list every pan frame with stale geometry ids (new tile meshes × old instance counts, tens of millions of triangles into a texture nobody read); make_child_pass records the recording list + redraw id, stale passes are skipped, a cached View re-attaches on a cache hit; tests for the orphan gate and the pool generation contract
bb49fe6 map + platform: retained per-tile draw lists — each resident tile owns one DrawList2d per carto pass (fill, casing, stroke, icon, icon-high, shadow) and the label glyph batches are retained the same way, recorded when the bake, LOD ring, fringe/icon gates, flat/tilted or clip change and re-attached otherwise; a pan/zoom/tilt frame pushes this frame's uniforms onto the retained calls (DrawVars::update_uniforms_on_area, resolved slot table) and uploads zero instance bytes; the tilted per-pass depth is a pass_depth uniform; a held list's zbias resolves at entry (zbias_hold in every backend); the shimmer heartbeat patches shiny_time in place without a redraw; a freed/reused sub-list id is skipped by every draw-tree walker and the mask list re-records empty on the flat transition (contract test). Web pan tail 1,098 → 20 MiB/s, flat pan 2.4 → 0.43 MiB/frame; Metal grabs within the run-to-run noise floor
313265d Studio code atlas: geometry code views, live filter, 3D size lens, lanes as terminals
9c4e0cc Studio code atlas: performance round — retained uploads, worker labels, exact search, no forks
7235d7e Studio code atlas: stall fix, GPU working set, lens hard switch, filter masks, parallel index
56a6da5 platform: per-pass GPU counter timing on Metal; retained publications replace in place
72e2443 platform: present-path trace (1 Hz cause histograms), bounded drawable wait and retirement on macOS
1c5d583 platform: bounded retained maintenance on empty paint beats; republish actual backend debt
edfed73 platform: retained residency high/low water and hysteresis; no distance eviction without pressure
acab6a0 platform: critical upload class serves present-blocking items first; identical immediate re-records upload nothing
906c774 platform: uniform_range on DrawVars and patch_retained_uniforms on retained draw lists
c29031c platform/draw/widgets: heap-keyed script resources and RecordingBuffer draw items — the files today's commits depend on
cd0964f platform headless: homogeneous near-plane clipping before the perspective divide
61d424d platform: release retained bindings of released textures so pool evictions complete
a00287a platform: texture-tile cache support — per-item instance ranges, painted pass receipts, display-dpi pass uniform, retained render targets, present gate on the drawable pool
74b63be platform: retained upload floor reverted, unconfirmed presents counted
c03fcc5 platform: tile-cache round 3 support — O(1) demand on re-recorded lists, evictions counter, allocated_size, lost-button release, Vec2d::round
e515d75 platform: shared instance publications — the DL-0/DL-1 contract, additive beside the retained path
142a337 platform: shared instance publications — close the seven review items (DL-1b)
6811a19 platform: Debug for SharedInstances, WeakSharedInstances and PublishReceipt
c061e47 platform: Metal draws are resident by construction — the hole path and its gates are gone (DL-2)
0bdbb29 platform: drop the unreachable InstancesNotResident present cause
de3c868 platform: `drawlist` trace names the holder of a stale draw-list id; the per-frame upload line moves to `gpu.upload`
721c3d8 platform: publication backends — Metal per-publication backings, lease-keyed uniform ring, receipts on every backend, Vulkan draws attached items (DL-3)
393de54 platform: integrated deletion — the draw-list system is generic again (DL-5)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 7 work commits (Sep 2–12):
95fd7d6 platform: a hosted window with a dpi override lays out in its own points and gets the host's pointer remapped
c2c7f51 platform: a hosted window on Windows draws again — the depth buffer matches the shared target's allocation
3b1a8c2 platform: an in-app drag works without an OS drag session (web, Linux) — effect tiles drop into the channels on the web platform: an in-app drag works without an OS drag session, so effect tiles drop into the channels on the web
5ff7397 platform: typed geometry uploads on OpenGL; the typed gate names only the backend that still lacks it
80bf3d3 platform: Linux hosted GPU transport and routing, hosted tick pacing, WGSL packed vertex members, Vulkan typed geometry
e2d8a0a platform: the Linux GL and gpusim cfgs build warning-free again
c8c757a vulkan: honor tile draw inputs and retire submitted frames correctly
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 2 work commits (Sep 2–2):
a6d0338 widgets: popup menu items run in the popup owner's script VM; app_main! releases its borrow after a trap
232909d script: the VM reaches std and its slot through one host — no aliased references
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 10 work commits (Sep 2–12):
9633ded web server: site static serving plus the first nav backends
ba1010d asset-client: review fixes — additive web feature, shared transport contract, guarded cache and base URL
81ef71c network: native backends honour the response body cap
4d75e65 web server: hardening after security review — worker-only parsing, FD-relative static opens, strict framing, bounded work, panic recovery, Cloudflare-aware limits
dfe2087 web-server: O(1) report admission, one connection deadline, shared route sampler, static fallbacks, cache policy
9c9c72b web-server: bodies land before workers, client keys normalized, verbs fail closed
6c32c88 web-server: ETags from size and mtime, deadlines from size, Allow per resource
7613f3e web-server: one deadline per response from its size, 404 before 405 for unknown API paths, one Earth radius
7deb052 web-server: ETags from size and mtime, never from content; upgrades only where a socket route exists
f407342 libs, network: in-tree shims replace crates.io libc/log in the Wayland and zune crates; deterministic math; the websocket flushes control messages at once
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 10 work commits (Sep 2–12):
3389475 trace: one switch — MAKEPAD_TRACE=<topics>, trace!(topic, …), and /trace on the bridge
c55a315 platform: monotonic clock beside the wall clock, trap-safe dispatch, studio worker only with a studio
89fe453 platform: wheels and flight sticks are game inputs, with an output-report handle
e5d37e0 platform: a web file picker and file drop that hand apps bytes
19cf377 platform: the thread runtime contract — spawner, tasks, pools, scheduler, UI waker
cf2b8ca platform: no std::time on web — clippy guard and the platform clock everywhere a web build runs
c527cd8 vj: the web thumbnail pipeline never blocks the main thread
2a064d3 workspace: add loader, haptics, voice, and runtime fixes
541c886 platform: name heavy pool jobs over 250 ms; headless Startup sent once per Cx
d476e52 Fix Linux worker sizing and screen recording defaults
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 3 work commits (Sep 2–12):
debd8c1 rename: the mp prefix goes — apps/wm, files, terminal, browser, task, sheets, image, video, pdf; libs/wm_api and wm_theme
6dcca00 workspace: no timed std waits on web-reachable paths — the clippy gate covers every web demo's dependency set
9d8e314 platform: drop two in-band coordination notes that were committed with the tree
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 39 work commits (Sep 2–3):
ad24534 map: persistent asynchronous tile archive over a completion-based byte source
5d28ed5 map: tile archive review fixes — legacy MBTiles path restored, decode off the UI thread, retry, cancellation, blob dedup, validation
a7eae82 map: the tile build keeps time with the platform clock
d931f9d map: tile archive third pass — per-pass pruning, validated watcher metadata, shared blob bytes, timer watchdog, atomic cancellation
4b529a1 map: tile ranges fetch once, centre-out, and never time out while queued
04a36de map-build: the bake produces only what the renderer reads; a bad tile is skipped and logged
383b83e map: the Amsterdam bake report — the real archive through the real bake path, bytes per stream and milliseconds per tile
2d4ff16 map: POI symbols and building walls are instances — one shared mesh per symbol slot, one record per footprint edge, extruded and placed in the vertex shader
72b60b6 map: the memory diet folded in — CPU staging freed after upload, budgets from one platform number, bakes capped by bytes in flight, per-zoom bake profiles, a memory report; instance records count as retained CPU
a6f495d map: street trees are instances of one template per tile; the CPU staging of every uploaded stream is dropped; the memory diet keeps its platform half only
9047600 map: the bake report can dump the raw and decoded tiles it measured
57995b6 map: uploaded staging is freed on a pool worker — a large free on the UI thread of the threaded web build contends the allocator lock, and a contended lock there is an Atomics.wait the main thread may not make
02b58fc map: ground fills on a 20-byte vertex — position, colour, variant + AA coverage, depth ticks; DrawMapFill is the fill path of the map shader
ef25495 map: building shadows derived at draw time in a screen-space mask pass
9d664ca map: roads on a 32-byte vertex
5b49fee platform, map: the UI thread never futex-waits on wasm
99d2180 map: roofs on a 20-byte vertex, contact shadows as instances
5a5eda2 map: road, fill and roof streams on the typed vertex formats, u16 indices
f987651 map: the analytic road fringe is baked only when the view is flat
7e66991 map: finished bakes reach the screen on the next frame
d8539d8 map: the detail parse and merge stop allocating
7a2c09a map: road-union faces on a 16-byte vertex
a2cbe38 map, platform: the tile budgets follow the platform's one memory number
fef8058 map: marker stalks and stoplights are instances
662d141 map: building wall instances on a 20-byte record
d6d3241 map: round road caps are a fragment SDF, dead cap rows gone
8ae7c3c map: every typed stream index stays u16 — streams chunk under 65,536 vertices
4e61e44 route + map: the desktop app streams the makepad.nl archive through a persistent on-disk range cache, chosen in settings
a8b0382 map: the water/foliage shimmer read draw_pass.time, which flags every DrawMap* shader as animated and repaints the whole map at display rate forever — a Rust-stamped shiny_time uniform instead; the 20 Hz heartbeat drives it
0d3a9b8 map: nothing animates at rest — the water/foliage shimmer heartbeat runs only during interaction or a camera animation, plus a 1 s settle tail, then freezes at its last phase; flat and tilted views alike
1f24428 map bake: a face-band triangle is a face only when all three records pack (a mixed triangle stays on the road path); a panicking pool job no longer takes the worker down — caught, reported once, the tile fails; the bake panic that blacked the native map and killed the web workers
199be9e map + route: the six overlays (EV chargers, transit, nature, districts, building age, population) read through the same archive plane as the base tiles — OverlaySource {name, TileSourceConfig}, hosted .mkmap archives on makepad.nl fetched by range through the shared archive reader and disk cache, a local .mbtiles only as a dev override; one layer table (apps/route/src/overlays.rs) for the native and demo builds, the demo checkboxes now set overlays like native
01e77d9 map: the shimmer clock is one pass-level uniform (draw_pass.shiny_time, a map-owned slot, never draw_pass.time) written once per heartbeat tick instead of patched into every retained draw call's block — the settle tail stops re-uploading 600 uniform blocks per tick
d641cbf map: labels no longer vanish — the gesture label budget is charged from the placement loop, not from candidate collection (4-7 ms on the web at z15-16, up to 6000 candidates in space warp), so a place can no longer commit an empty cache; a truncated pass is never a strict cache hit, arms its own settle wake, and the at-rest follow-up chain is capped at 4
635c3a3 map: ready tiles are inserted at most two per frame (byte budget kept), queued visible ring first then margin ring, the stale tile drawn until its replacement lands — a restyle burst of 4-9 refined tiles no longer stalls a frame for 70-120 ms
ddc4709 map: touch gestures — one-finger pan and double-tap zoom, two-finger pinch zoom around the midpoint, rotate with a 5° dead zone, and a parallel vertical slide for tilt, one state machine native and web; the web page keeps browser pinch-zoom out (non-passive touch listeners, touch-action none, maximum-scale 1)
8eaa6ec map: two-finger tilt follows the phone convention — fingers up tilt into 3D, down flattens
018ce73 map: labels hold still through a gesture and never pop — the settled placement rides the camera delta while anything moves (pan now part of the motion signature; re-place only beyond the pan/zoom law), every draw uses the rect-centre fold pivot so CPU placement and the GPU warp agree (a fresh place drew about the screen corner: the giant space-warp labels), and a re-place cross-fades: survivors keep their birth, newcomers fade in, dropped labels retire from their own camera over 250 ms
c79e84e map: a tilt is two fingers moving together up or down — same vertical direction for both, the pair's stroke within a fifth of vertical; spread and angle no longer matter, so real fingers trigger it
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 38 work commits (Sep 2–6):
ed5b2fa web: wasm32 portability in libs and web startup geometry deferral
0ccd384 platform web: focus dispatch, window-zero geometry and generation-correct ids after the startup deferral
a7af3ea platform + mpfiles: platform clock instead of std::time on the web, unwind-safe event dispatch
52251b7 platform: a namespaced async key/value storage API on Cx — files natively, IndexedDB on the web
84b46c1 mbtile reader: file-backed readers are native-only so the map stack builds for wasm
6b661bf web: crashes report themselves — panic text, breadcrumbs, memory, workers; a dead instance stops pumping
372bfd7 asset-store: browser durability — generation extents over cx.storage, chunked CAS, quota and GC
0f967ce web path: the stream trace and the sqlite pager never wait on a clock the browser does not have
4e58ab9 vj: the output window exists only once opened — never on the web
93232a2 platform: two pool workers on wasm until the allocator is per-thread
c7fe851 vj: effect thumbnails render, encode and persist in browser storage on the web
5b33781 platform: thread-caching allocator for the threaded wasm build
463de64 web: shaders compile per draw list, link in parallel
e7be0e3 vj + platform: the browser UI thread never waits on a lock, and the web hot paths log only errors and summaries
7b33f7b vj + platform: a loaded deck actually plays on the web vj + platform: a loaded deck actually plays on the web
dc85eb0 web + vj: render-to-texture passes keep their 3D camera on WebGL, thumbnails wait for shader compile, bundled tiles re-ask — effect thumbnails match native
b39d301 web audio: the worklet links the whole module — every platform import the audio thread does not serve is stubbed, clocks and the UI wake are real
1e2dcd0 web: a pass without a draw list is skipped instead of taking the app down (F12 layers overlay)
0ef0311 web audio: a throw inside the worklet's process() is reported with its real text instead of a bare ErrorEvent
97e0701 vj + platform: the web audio thread never waits on a lock — a loaded deck plays
58d3fd5 web audio: the output is created inside the first gesture, and a stalled worklet module load is retried on a fresh context
410acd0 web + vj + route: the console carries failures and one-line summaries, nothing per request, per tile or per hiccup
a3248d1 web audio: the worklet gets the audio access pointer as its context — the thread-stack call gained a request id and the audio start was still passing the pointer in its place
9e2d65b pdf + photos + task + wm + video + automate + widgets: no per-job threads — pool jobs and start-up workers
639887d webgl: texture passes get their depth target — the tilted map (and every 3D scene drawn into a texture) was draw-order only: hollow buildings, no roofs, landmarks buried
2aa0780 webgl: BGRA uploads become RGBA at upload and sample_as_bgra is a plain sample on the web, as on every native backend — the tilt-shift's sharp band showed red/blue-swapped water
de9aba3 vj + web: the Layer button works on the web — a second Window is a queried capability (OsType::is_single_window; the web creates none and reports it once), so the output becomes an in-page full-canvas layer with browser fullscreen (Esc, the browser's own fullscreen exit, or a double-click leave it); a pass without a draw list settles its dirty flag instead of erroring every frame; exitFullscreen fixed; fullscreenchange feeds the window geometry
f615054 tweaker + webgl: click-to-climb continues only from the widget the climb started on, so a press on a sibling picks that sibling (the empty draw_bg was a bare View being pinned); the Shader tab says when a layer has no live draw call; the WebGL paint walk no longer resets every draw list's view_transform — the magnified material well drew at the window origin on the web
0d7ddee webgl: a uniform block is uploaded only when its generation moved — per draw call (uniforms_gen), per draw list (uniforms_gen, recording_gen), per pass (pass_uniforms_gen) and per shader scope block; the JS side caches (ptr, len, gen) per uniform buffer and re-records/recompiles reset it; direct camera writers (vj effects, render scene, the web flipped copy) bump the pass generation
e48b056 Revert "webgl: a uniform block is uploaded only when its generation moved — per draw call (uniforms_gen), per draw list (uniforms_gen, recording_gen), per pass (pass_uniforms_gen) and per shader scope block; the JS side caches (ptr, len, gen) per uniform buffer and re-records/recompiles reset it; direct camera writers (vj effects, render scene, the web flipped copy) bump the pass generation"
b043332 webgl: uniform blocks upload only when their generation moved — one global monotonic counter on Cx hands out every generation (draw call create/dirty/zbias, draw list allocate/transform/re-record, pass allocate/time/dpi/ortho/camera and the web flipped copy, shader scope writes), so a reused pool slot can never match a cached generation; clear_draw_items, pool reuse and VAO recreation reset the caches; the JS caches key on the generation alone. Proven on a local build: the pan screenshot keeps every tile, the settle tail drops 20.5 → 12.8 MiB/s
3c2530d web memory diet: the 805 MiB at load was the ocean-high archive's 13 M-entry leaf directory decoded to 407 MiB per lookup round and evicted at once — leaves now parse streaming into a window around the waiting tile ids (LeafParseLimits); a phone policy on the web (deviceMemory, UA, touch + short side) caps wasm at 512 MiB with a 320 MiB budget; archive leaf/range caches, reads and bakes in flight follow the budget; packed tile bytes stay packed until the bake decodes them; terrain scratch sized to the viewport and dropped with the layer; Cx::memory_report by owner. Phone viewport 1334 → 308 MiB after a minute of pans, desktop 1208 → 588
303458d webgl: a 2D texture pass builds its camera through set_ortho_matrix like every other backend, so the exploded z-layer view's camera reaches the GPU — the hand-built ortho branch uploaded an identity view and clipped every exploded draw, leaving the tweaker's layers view a bare window on the web; the Y flip for render-to-texture is one helper shared with the keep-camera branch
1801e38 Harden web renderer and make Route location opt-in
09fa1f0 Restore WebGL text with complete fallback samplers
9e61553 Keep Route 3D buildings under bounded web memory pressure
b005c6e Preserve complete Route geometry within measured web memory budgets
0832b35 platform: support float GI targets and retained mesh snapshots
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 10 work commits (Sep 2–11):
120b7e2 score view: the engraver as a shared library, with drum and pitched score builders
09b41a8 fonts: FontSet and FontPolicy — one application choice, selected-only loading, a font-assets manifest
6981e90 fonts: manifest generated from the chains, app-level font assets, a symbol fallback, deprecated i18n aliases
77d9138 wm: the module contract and the first in-process app — sheets in a tile, in an isolate of its own
55a3810 platform: typed compact vertex formats and u16 indices
83a8d4f fonts: the web demos start with the Latin set — CJK and emoji faces load on the first glyph that needs them
1980c24 platform: a draw call whose geometry id went stale is skipped, not drawn with whatever mesh now sits in the reused slot — the runaway triangle count that took a web map pan to 1 fps; reported with a power-of-ten backoff, never per frame
a75fe91 layout: extend turtle sizing and add Grid
4429551 draw, widgets: text clips by the list clip; GaussChain; map colour roles
6e02468 draw: restore Cx2d::set_current_pass_dpi_factor (raster density) beside the display-dpi setter
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 9 work commits (Sep 1–5):
929f822 video: the single-frame mp4 encode exists on every platform
294cb9e video: the single-frame mp4 is written on every platform, not stubbed
730b5b8 video: the Windows H.264 stream decoder pulls output before it knows the format
c1febc7 windows h264 stream decoder: low-latency mode, output type before first ProcessOutput, real MF_E codes, trace file
d6cc49a windows mft: PROVIDES_SAMPLES is bit 0x100, not bit 0
dcf7d21 windows h264 decoder: ICodecAPI low-latency, per-pump trace, access-unit dumps, and the stream tests run on Windows
d1e7a6f windows h264 decoder: AVLowLatencyMode is a VT_UI4; the round-trip test tolerates the MF encoder's access unit delimiters
aa816ed windows h264 decoder: pictures come out one access unit later — rewrite the SPS level so the DPB is one picture deep
2f44d20 apple: avoid blocking video clocks and release native players once
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Squash of 36 work commits (Sep 1–12):
176433a tweaker: click-again climbs the pick — the container under the children is reachable
97e9572 speech: one STT/TTS API on every platform, through the ai-hub
8ea3684 widgets: EventOrder reachable from the DSL; DataGrid hit-tests where it was drawn
0fd1ea2 route: demo profile — native/demo features, side-panel and provisioning seams, hosted tiles, HTTP nav client
cf4c84c keys: F10 is the assistant — the exploded-view debugger moves to Shift+F10, the screen recorder to Ctrl+F10
126d8c9 route: the demo profile runs in the browser — merged panel draw, platform clock, unavailable backends tolerated
e21db96 route: demo review fixes — route origin, rain lifecycle, hosted route validation, request context, provisioning
c24c206 aichat: the Window overlay — F10 in every standalone app, the in-process port, the /ai bridge routes, sheets as the pilot
dbe43bd wm + widgets: the AI panel on the left, pushing the body in
5184340 platform + vj + map + files + widgets + image_tiles: the runtime owns one warm two-lane task pool — jobs never spawn threads
697215e route + converse + example-map: every worker comes from the runtime pool or a start-up worker
862f3bd asset widgets + chat ui + render: fan-outs and jobs on the runtime pool, the transcript read without a lock on draw
976ea0e tweaker: Shift+F10 toggles it on every platform (KeyEvent::is_tweaker_toggle, one call site; the web page swallows exactly Shift+F10 so the browser never sees it); the exploded z-layer view has no keyboard shortcut any more — it is a button in the tweaker
4e8e4cd flow-ui: the design pass — menu bar and toolbar with the total run bar, continuous zoom through the draw-list view transform with pointer remapping, dark checker canvas with grid steps, shadowed cards with icon labels and port icons, glowing wires, per-node progress bars, full-bleed image cards, palette cards you drag out, the fab edit controls in the inspector, a template picker behind New, model pickers from the hub; MenuBar widget in the shared crate
075e1a7 flow-ui: pickers filled from the hub for image and text nodes, popups anchored through the canvas transform, labelled face controls, add_style explains itself, cards resize from a grip with size: vec2 kept in the file, full-bleed pictures, a click anywhere on a card selects it and still reaches the face, no remount on layout-only edits, panels float over the canvas
a50750f flow-ui: Flows, Running and Palette as their own rounded panels with splitters, the inspector and source pane likewise, columns resizable; gaussian frame shadows; keys and IME reach the focused face field through the canvas transform
43302a6 flow-ui: every card owns a draw list and draws in z order, selection brings it to the front; and the design review's findings — every event kind remapped through the camera, run events keyed by run id, no remount mid-run, an input journal that survives a failed PUT, terminal states reconciled, the total bar from the planned node set, isolate ownership on instance change, popups retired before an isolate is freed, the Ask face answers on a button, the menu bar navigates by keyboard, no per-frame allocation in the canvas draw
4ee4f4c flow-ui: the resize path sets walks and fits through typed setters, never a script apply from the main VM on an isolate's widget — a failed apply had left a freed script object behind and wedged every frame; a resized card fills its picture box, clips its face and lets the last flexible element take the height
cff15ac widgets: a fab number field drops a label that cannot fit instead of crushing it to a dot
24c927a flow-ui + widgets: every dropdown is the searchable ComboBox
a6c5f15 widgets: FabValueInput honours visible, so the seed picker's random mode hides the number field
1181a3b flow + flow-ui + widgets: every creator pipeline is a template — 55 templates in six groups (Image, Video, Audio, 3D, Vision & text, Utilities), all evaluated and engine-exercised in tests; the New picker, the flows.templates tool and the palette group the same way; the Templates menu shows them under group headings, and a menu taller than the window scrolls
f8b9a67 widgets: preserve numeric edit completion and menu focus
ed5f2e2 Add hotloadable OS themes and preserve widget state across style changes
f1d3b39 Center resized app recordings on a fixed black canvas
915fcae Remove icon rim highlights and align compact home tile contents
bfa7805 Keep terminal palettes theme-aware and resize above mobile keyboards
7535ce8 Fix workspace build regressions (#1220)
2233cbc Replace legacy Studio with docked and canvas agent workspace
708aa9f code_editor: range views, anchors, prepared documents, read-only, tab stops
0eae276 widgets: capture Studio evaluation feedback and recordings
487c602 widgets: let Studio pump dock bodies across presentations
c5adb93 Studio code atlas: settle-line diagnostics, index progress, chrome fades, exact search budget
ee9ab46 widgets: every screen capture lands in the repo's local/screencap, named by app
dcc9673 draw, widgets: the phone shell's glass, hosted-view and overlay support, app icons for the new apps
2fbc679 wm: preserve app caption controls and add Scope to the launcher
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Event: trace cancel scopes, and gate StackNavigationView's Back on ownership
A scope held by a widget that has stopped being the active thing wedges
Escape and the back gesture for everything behind it, and the only symptom
is that the gesture silently stops working -- which is indistinguishable
from there being nothing to cancel. MAKEPAD_CANCEL_TRACE=1 now logs every
scope begun and ended, and which one each press was stamped to, each named
by the call site that began it.
That location comes from #[track_caller] on both Cx::begin_cancel_scope and
CxCancelScopes::begin: the attribute propagates through the chain, so
Location::caller() names the widget rather than either of makepad's own
frames. No signature changes, no caller passes anything new, and the
existing tests needed no edits. Releases are logged from Drop rather than
end(), so giving a scope up by dropping it -- including a widget being torn
down, the case most likely to leak -- is reported exactly once on either
route.
StackNavigationView called the consuming back_pressed() whenever it was
Active, with no ownership check. A modal or pane opened over a pushed stack
view owns that press, but the view could consume it first and pop: the
wrong thing acts and the owner is starved, on one gesture. It worked only
because children are dispatched before the closure request, which is
precedence by traversal order -- the thing cancel scopes exist to replace.
A pushed view genuinely is what Back should pop when nothing is in front,
so it now holds a scope while Active and acts only when it owns the press.
Its five state writes route through a single set_nav_state that moves the
state and the scope together, acquired at the transition because ownership
is stamped before dispatch. The left_button and mouse-back-button paths
stay ungated: those are unambiguous clicks on this view, matching Modal,
which gates only back_pressed().
* Fix Escape and Back ownership across widget lifecycles
Allow gesture-specific scopes, preserve held Escape ownership across Back and focus changes, and suppress repeated Android Back dispatch without invoking Activity fallback first.
Release popup, modal, drag, and navigation scopes on every exit; support suspended navigation, isolate Pop actions, and finalize wide-window hide animations. Add focused ownership and lifecycle regressions.
Validated with 14 platform cancellation tests, 14 widget cancellation tests, Android Rust and Java checks, and a release modal Escape smoke test.
* Resolve cancel ownership from the active widget hierarchy
Bind widget scopes to their owners and resolve visibility and descendant priority only when Escape or Back begins. Retained inactive pages, collapsed controls, and unfocused windows no longer require application activation callbacks.
Preserve press ownership through repeats and release, suppress scoped or repeated TextInput Escape actions, and remove the StackNavigation cancellation activation API. Cover hierarchy, container, wrapper, focus, and gesture ownership regressions.
* Simplify cancel traversal and remove unsafe root lookup
* Reuse validated widget paths for repeated activity queries
* Remove PR-added cancellation tests and tracing
* Arbitrate the mouse back button with cancel scopes
The mouse's back button is the same navigation gesture as Android Back, but it
never received a cancel owner: handle_event clears press_owner for every event
and only restores it for Escape and BackPressed. owns_cancel was therefore false
for every scope while a MouseUp was delivered, so a widget could not gate that
button on ownership at all. The ones that tried had to fall back on ad-hoc
conditions -- "is my tab the visible one" -- which cannot express the thing that
actually decides it, namely that something else is in front.
Stamp a Back press for Event::MouseUp with the back button: in
resolve_widget_owner so widget-bound scopes are resolved against the hierarchy,
and in handle_event so ownership is settled before dispatch, exactly as for the
gesture itself.
StackNavigationView's mouse-back path is gated on that ownership to match its
back_pressed(). A pane or modal opened over a pushed view now takes the first
click and the view stays put; the second pops it. The left_button path stays
ungated, being an explicit click on the view's own header rather than a gesture
something in front of it could have a better claim to.
* Close a Modal on the mouse's back button
The back button is the desktop equivalent of the back gesture, and is arbitrated
by the same cancel scope, but Modal acted only on Escape, BackPressed, and a
click on its backdrop. A back-click inside the content did nothing at all, and
one outside it closed the modal only incidentally, as a background click.
Gated on ownership like the other two, so a modal opened over another one keeps
its place, and left inside can_dismiss so a non-dismissible modal still ignores
it. This is what lets a full-screen modal's content -- an image viewer, say --
respond to the back button without handling the gesture itself.
* Fold Modal's Escape and mouse-back checks under one ownership test
Same behaviour with one ownership test instead of two, matching how the other
cancel-gesture handlers read. Back consumption stays outside can_dismiss, so a
non-dismissible modal still blocks back-navigation for the widgets behind it.
* Event: let the foreground widget own a cancel gesture
Several widgets act on Escape, and today more than one can act on a single
press: a modal closes and background dictation stops; a popup closes and the
microphone keeps recording. Dispatch order cannot arbitrate this. Siblings are
handled in reverse declaration order, a parent runs before its children, and
declaration order doubles as the z-order knob, so dispatch order is not
foreground order and cannot be made into it.
Add a stack of cancel scopes on Cx. A widget begins a scope when it becomes the
active thing -- a modal opens, a drag starts, a dictation session begins -- and
ends it when it stops being; the most recently begun live scope is in front. On
a fresh Escape key-down or a back gesture, call_event_handler records which
scope is in front, and that scope owns the whole press, its repeats and its
release included. A widget asks owns_cancel() and acts only if the press is its
own, so exclusivity needs no consumption primitive: only one scope is in front.
A scope that ends part-way through a press does not hand the rest of it to
whatever was behind, so an Escape that stops dictation cannot also close the
modal it was running in front of. Dropping a scope gives it up, so a widget torn
down without a tidy close cannot wedge the key for everything behind it.
Nothing changes for a widget that never begins a scope, so adoption is
incremental.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Widgets: adopt cancel scopes for Escape and the back gesture
Every widget that treats Escape or the back gesture as "cancel" now holds
a CancelScope while it is active, and acts on a press only when it owns
it. Foreground order decides who cancels, not dispatch order: a modal
opened in front of another modal takes the press, and nothing behind it
acts on the same one.
Widgets whose active state can end by several routes reconcile their
scope from that state on each event rather than trusting a single close
path. That also fixes the tweaker holding its drag state open after the
panel is toggled off with F12.
* Fix cancel-scope timing and Back gesture ownership
Acquire drag and color-popup scopes at activation and release them on each exit path, before the next cancel event chooses its owner. Gate Back consumption on scope ownership across modals and their popup/drag controls; non-dismissible foreground modals consume Back without closing.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Admin <info@makepad.nl>
Makepad's log! is its own printer rather than `tracing`, so an app cannot
filter it: anything logged this way is printed unconditionally, and an app
developer reading their own output has to scroll past it.
Six calls on always-taken success paths are commented out rather than
deleted, so they are one uncomment away for anyone debugging that area:
macos_window.rs titlebar container swapped, once per window
macos_app.rs display link pinned, paint pacing, once per window
macos_app.rs PIN stats, on every pointer-lock release
audio_unit.rs voice input native format and ducking level, on every
microphone open, which a dictating app does repeatedly
None of them report a problem or anything the app developer can act on;
they describe internal decisions in makepad's own vocabulary. Error and
warning paths beside them are untouched, as are logs already gated behind
an env var, a feature, or a once-per-process flag.
* macOS: don't deliver IME-consumed keys as KeyDown
`process_ns_event` hands each NSEvent to AppKit via `sendEvent:` before
emitting Makepad's own KeyDown. When an IME has marked (composition)
text, that dispatch lets the IME consume the key: Return/Space commit
the candidate, digits pick one, arrows navigate, Escape discards,
Backspace edits the preedit. A committing key clears the marked text
during dispatch, so the old post-dispatch `hasMarkedText` check (which
only covered Backspace) could not see it, and the Return that merely
committed a pinyin candidate was also delivered as KeyDown(ReturnKey).
TextInput then treated it as a submit.
Snapshot `hasMarkedText` before `sendEvent:` and skip the KeyDown
callback when the IME was composing. This subsumes the Backspace check
and also fixes the case where Backspace deleted the last preedit
character and then fell through to delete committed text.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HLgZDz8vuuqqMya1nCWKf
* macOS: preserve IME commands and pair consumed key releases
---------
Co-authored-by: ymote <151983+ymote@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Html: keep collapsed details with void tags from swallowing table closures
* Html: harden the parser, the walker and the widget against malformed input
Follows the void-tag fix in the `<details>` skip loop by auditing the rest of
the HTML code for the same class of defect. Robrix renders `formatted_body`
straight from Matrix events, so every parser crash here is reachable from a
message any stranger can send.
Crashes, all reachable from a chat message:
- Numeric character references were parsed as `i64` and cast to `u32`, then
handed to `char::from_u32(..).unwrap()`. `�`, `�`, `&#-1;`
and `�` aborted the process. They are validated now, and a
reference that names no scalar value stays literal text.
- An unterminated `&` stayed pending across a tag boundary or a closing
attribute quote, so a later `;` could fire `decoded.truncate()` and
retroactively invalidate byte ranges of nodes already emitted —
`<p>&am<b>p;</b></p>` produced out-of-bounds and mid-character ranges.
The pending entity is dropped at each of those boundaries.
- An unquoted attribute value beginning with a multi-byte character recorded
`decoded.len() - 1` as its start, splitting the character.
- `</summary>` with no `<summary>` popped an empty tracker stack, and stray
`</td>`, `</tr>`, `</li>` and friends reached `cx.end_turtle()` with nothing
to end. The widget now tracks what it opened and ignores unmatched closes.
- `('A' as u8 + count as u8 - 1)` overflowed on an attacker-controlled `start`
or `value`; alphabetic list markers now number a..z, aa, ab, ...
Content silently lost or mangled:
- `jump_to_close` counted every open tag toward depth, but a void element
written without a slash emits no close tag, so it overshot and swallowed the
rest of the document. `<a href=u>x<br>y</a>` hid everything after the link.
Only tags with the same id affect depth now, and an element with no close tag
leaves the walker where it is. `mod_html::find_close_tag` had it too.
- A `<` that cannot start a tag is literal text, the way a browser reads it.
`5<10 and 6<12` used to parse `<10` as an element and drop the rest.
- `?` mid-tag-name and `<!-->` / `<!--->` ran to end of input.
- `/` in an unquoted value ended it, truncating `href=http://host/path` at the
first slash; only a slash immediately before `>` closes the tag now.
- `<a href=>text</a>` took `>` as the value's first character, so the tag never
closed and its content leaked out as text.
- Unquoted values never decoded entities at all, unlike quoted ones.
- `<pre>`/`<code>` whitespace preservation was a single flag that any nested
tag cancelled, so a syntax-highlighted code block lost its indentation. It is
a depth counter now.
- HTML's whitespace set is five ASCII characters, not Unicode's;
`char::is_whitespace` collapsed ` ` runs and ate the full-width spaces
in CJK text.
- `find_text` returned the zero-length node the parser emits before every tag,
so `<a href=x><b>label</b></a>` rendered an empty link. `find_tag_text`
matched the case-sensitive id and missed any tag carrying an attribute.
- Duplicate `id` attributes bound two elements to one cached sub-widget, so a
second link could render its own text over the first link's href.
- `<li>a<li>b` and `<td>a<td>b` now implicitly close the previous item, and
anything a document leaves open is unwound before `TextFlow::end`.
Entity table, which had been generated by folding names case-insensitively:
- 146 names took their case-twin's code point. `é` rendered `É`,
`α` rendered `Α`, `→` rendered `⇒`, `𝕔` rendered `ℂ`.
- `Igrave`/`Icirc`/`Iuml` had been transcribed as `Lgrave`/`Lcirc`/`Luml`, and
`Iacute` was missing outright; the invented l-spellings are removed.
- `permil` mapped to the Windows-1252 byte 0x89 rather than U+2030, and an
empty-string key sat where it belonged, so `&;` decoded to `‰`.
- `tilde`, `lang` and `rang` were wrong.
The ALL-CAPS aliases the table also carries are left as they were.
Also: dropped the `unwrap` in `ElementSelfClose`, memoised table column counts
(quadratic in the number of `<table>` tags), and replaced the backward node
scan on every tag close with the depth counter.
Adds 18 tests covering each of the above. Verified by exhaustive enumeration of
all 12.2M inputs up to length 6 over a markup-heavy alphabet, and 6M randomized
structured cases, both checking that no input panics and that every node's byte
range is ordered, in bounds, on a character boundary, non-overlapping, and
agrees with its `all_ws` flag.
* Html: recover from malformed tags without leaking them into the text
A second pass over the same code, after the first round of fixes changed what
the edge cases look like.
- `</` followed by something that cannot name an element is literal text, the
rule `<` already follows. `i </3 u` used to emit a close tag named `3` and
drop the rest of the line.
- Junk inside a tag is discarded up to its `>` rather than resuming text in the
middle of it, which leaked the tag's own `>` into the output: `a</p x>b` and
`a<br/x>b` rendered `>b`.
- A custom widget with no close tag of its own is void, so it has no text.
Reading ahead picked up the *following* sibling's text, and now that
`jump_to_close` correctly stays put, the main loop drew that text a second
time: `<img src=x>caption` showed `caption` twice.
- `table_columns_cache` is keyed by node index, so it has to be cleared per
draw or a recycled widget lays a table out with a previous document's column
count.
- `<ol start="2147483647">` overflowed the item counter.
* Html: bound jump_to_close's scan and cut the measured hot spots
Benchmarked against the branch point (best-of-7, black_box'd, release).
- `jump_to_close` stops at the first close tag belonging to an enclosing
element instead of reading to the end of the node vector. It tracks the
elements opened inside this one so a descendant's close tag is still
matched correctly, and allocates nothing for the common case of an element
whose content is plain text.
- Numeric character references were compared against all ~1500 named-entity
arms before reaching the catch-all. Dispatching on the leading `#` first
makes them 2.9x faster (991us -> 342us for 3000 references).
- `process_entity` is `#[inline]`; it is called once per character.
- `decoded` is reserved up front, worth ~4% on text-heavy input. `nodes`
deliberately is not: its length tracks tag count rather than byte count, and
sizing it from `body.len()` cost a tag-sparse document a large pointless
allocation — that made the numeric-entity case 3x *slower* before it was
measured and removed.
- The widget rejects an unmatched close tag from a tally instead of scanning
the whole open-element stack, which was quadratic on a message combining
deep nesting with stray close tags.
- `align_keyword_to_x` compares in place rather than lowercasing into a fresh
String for every aligned cell on every draw.
Tag-heavy parsing is ~2-3% slower than the branch point, which is the standing
cost of the `<pre>` depth tracking, the literal-`<` guard and the entity state
carried across characters. Plain text is ~4% faster.
* Html: follow the tokenizer's recovery rules and resolve element ends at parse time
The parser's states now mirror the WHATWG tokenizer's, so malformed input
produces the tokens a browser would build from it rather than a guess:
- `</` followed by anything but a letter opens a bogus comment that runs to
the next `>`, `</>` is dropped, and `<?...>` is a bogus comment too. `<`
or `</` at the very end of input is text.
- `<a/b>` reads as `<a b>`: the slash was not a self-closing marker, so no
close tag is synthesized. `<x/>` still emits one — the SVG parser is built
on this walker and XML needs it — which is the one deliberate departure.
- In an unquoted attribute value a `/` is just another character, so
`href=http://host/path` keeps its path and `<img src=x/>` is `src="x/"`.
- `<!--x--!>` closes a comment, `<!-x>` is a bogus comment, and a tag cut
off by the end of input is dropped whole.
- Numeric character references follow the tokenizer's end state: zero, a
surrogate, or anything past U+10FFFF becomes U+FFFD, and the C1 range is
read as Windows-1252, so `—` is an em dash as legacy content intends.
Digits are accumulated with saturation so a forty-digit reference lands on
U+FFFD rather than an error. A decoded space collapses like a literal one.
- `<pre>`/`<code>` are tracked as a stack: a stray `</code>` cannot cancel an
enclosing `<pre>`, and `</pre>` closes a `<code>` left open inside it.
Every element's end is now resolved once at parse time (`HtmlDoc::closes`),
with the recovery a browser applies: a close tag ends the innermost open
element of its name and everything still open inside it, and a close tag
that matches nothing is ignored. `jump_to_close` and the new
`HtmlWalker::close_index` are lookups, which removes the last quadratic
case — a paragraph of thousands of `<img>` tags cost 3.4ms a frame — and a
stray `</span>` no longer stops a link's `</a>` from being found. The tally
that rejects stray close tags hashes `LiveId` through an identity hasher,
since it is already a 64-bit hash; with SipHash the pass cost 20%.
Widget:
- A `<summary>` left open is closed by `</details>` or the end of the
document, so its bold run and glyph tracker no longer leak into everything
drawn after it.
- Implicit closes follow the tree builder's scope rules — `<li>` closes an
open item up to its list, a cell up to its row, a row with its cells, a
heading directly following a heading, and any block element an open `<p>`
— rather than only the innermost element.
- A custom widget's label is all the text inside it, so
`<a href=x><b>Click</b> me</a>` reads "Click me", and a void one has none.
- Sub-widgets are keyed only by node index. Keying by the `id` attribute let
a document choose cache keys, and a repeated id bound two links to one
widget.
- `TrimWhitespaceInText`, `combine_spaces` and `ignore_newlines` are gone:
all three were written at every site and read at none.
- List markers are borrowed rather than allocated per item per draw, table
cell alignment compares in place, and link hit-testing no longer clones
its area list on every event.
Script module: `.html` printed raw hex for every tag and attribute name,
because the document was parsed without interning; it is interned now and
text and attribute values are escaped on the way out, so the output parses
back to the same document. `find_elements` counted every open tag toward
depth, the void-element bug again; it steps by resolved close index.
23 parser tests, exhaustive enumeration of all inputs up to length 6 over a
markup-heavy alphabet, and 6M randomized structured cases, checking that no
input panics and that every node range and close index is consistent.
* Html: resolve every element's end in the tokenizer, and close the review's findings
An adversarial review of the previous commit against the WHATWG tokenizer,
the branch point and a reference parser found the gaps below. All fixed.
The parser now keeps the open-element stack as tags stream past, so each
element's end is resolved in the same pass that tokenizes it — the recovery a
browser's tree builder applies: a close tag ends the innermost open element
of its name and everything still open inside it; a close tag that matches
nothing is ignored; the spec's void elements are whole at their open tag;
what is still open at end of input ends there. `HtmlDoc` records both the
element's own close tag (`close_index`) and where it ends (`end_index`).
That distinction was missing: an element ended by an ancestor looked the
same as a void one, so the script module gave `<li>a<li>b` items empty
ranges — no `.text`, no `.html`, children promoted to siblings — and the
widget dropped the label of a link ended by `</td>`. Both read correctly now.
Because the whitespace-preserving stack is the same stack, a `<pre>` ended
by an enclosing element's close tag stops preserving at that tag, which it
did not before.
Tokenizer fixes, each per the spec's state machine:
- `<!>` and `<!->` are complete bogus comments; they used to swallow text up
to the next `>`.
- A numeric character reference ends at the first non-digit whether or not
`;` follows (`& b` reads `& b`), and has no length limit: forty digits
saturate to U+FFFD as the previous commit claimed but did not do.
- An end tag followed by junk and then end of input is dropped like any
other tag cut off there; it used to emit its close tag anyway.
- `\r\n` and lone `\r` become `\n`, as the input stream preprocessing says.
- A repeated attribute name on one tag is dropped, so a consumer iterating
attributes sees the first `data-mx-color` rather than the last.
- A comment is not content, so `a <!-- c --> b` collapses to one space.
- `find_tag_text` answers for the first matching element and does not fall
through to a later one.
The maps that reject stray close tags and duplicate attributes are keyed
with a per-parse random seed and a multiply-fold hash: the previous identity
hasher let crafted tag names collide and made the pass quadratic, and the
standard SipHash cost a quarter of the parse time.
Widget:
- A `<summary>` is tied to the `<details>` that owns it. A `<details>` opened
inside a summary was taken for the owner, and `</details>` then popped an
empty tracker stack — a panic reachable from a chat message.
- `</summary>` and `</details>` end whatever was opened inside them, so an
`<li>` or a table cell opened in a summary no longer swallows the content
that follows.
- A collapsed body is skipped to the element's resolved end, so a
`<details>` ended by an ancestor no longer hides everything after it.
- `count_table_columns` ends the first row at the next `<tr>` as well as
`</tr>`; a table written without `</tr>` had every column halved.
- The `<p>` rule runs before the heading rule, as the tree builder orders
them, so `<h1><p>a<h2>` no longer nests the second heading in the first.
Script module: ranges are `(open, end)` with an exclusive end; `parse_query`
no longer panics on `a]b[`.
30 parser tests, exhaustive enumeration of all inputs up to length 6 over a
markup-heavy alphabet, and 6M randomized structured cases, checking every
node range, every `close_index`/`end_index`, nesting consistency, and
determinism.
* Html: build the tree builder's implicit closes into the parser, and end every element where it says
Two verification rounds against the previous commit — a spec-conformance
review, a stack-based reference for element ends, a simulation of the widget's
draw loop over exhaustive and random tag soups, and a round-trip check of the
script module — found the gaps below. All fixed.
The parser now applies the tree builder's implicit closes as it builds the
element stack: a block start tag closes an open `<p>`; a heading closes a
heading that is the current node; `<li>` closes an open item up to its list,
`<dd>`/`<dt>` likewise; a cell closes an open cell up to its row; `<tr>` closes
a row and its cells; a table section closes section, row and cells; a second
`<a>` closes the first. Every consumer therefore sees the tree a browser
builds: `<li>a<li>b` is two items, `<a href=1>x<a href=2>y</a>` two links,
and `<li><a href=u>one<li>two` gives the first link the label "one" rather
than "onetwo". The widget's own copy of these rules is gone; it closes each
element at the index the parser resolved, before that node is handled, and
`<details>`/`<summary>` without a close tag of their own are ended the same
way. Two bugs that fell out of them being special:
- a `<details>` ended by an enclosing close tag stayed on the stack, a later
`<summary>` bound to it, and the collapse-skip resumed *behind* the walker.
One stale level drew the text twice; N of them re-walked the document 2^N
times — a 380-byte message hung the UI. A resume is now never behind the
walker, and no level is left behind to be claimed.
- a `<summary>` ended by an enclosing close tag never popped its bold run and
glyph tracker, which leaked into everything drawn after it.
Per-name depth stacks replace the per-name counts, so finding the innermost
open element of a name, or the outermost one above a scope boundary, is a
lookup; scanning the stack made a document of nested `<div>`s quadratic.
A tag with thousands of attributes no longer makes every later tag pay to
clear the attribute-name set. Every nesting shape measured is linear.
Tokenizer and tree builder, per the spec: `</br>` is read as `<br>`, so
`x</br>y` breaks the line; the newline immediately after `<pre>` is not
content; NUL is dropped from text and replaced in attribute values.
Widget: a table whose first row is empty is sized by the first row that has
cells rather than falling back to 100px columns.
Script module: `.html` always writes `=""` and doubles a newline that starts
a `<pre>`, so its output parses back to the same document; `.text` is the
decoded text verbatim, no longer inventing a space inside a word split by a
comment or an inline tag; a query on a selection searches inside it, as
`querySelectorAll` does; descendant steps skip ranges already scanned, which
made `b b` on deeply nested `<b>` quadratic; `parse_query`'s grammar is
documented as implemented.
Deliberately unchanged: the entity table's omissions (`€`, ...), named
references without `;`, and an unquoted attribute value ending in `/` before
`>` (per the tokenizer the slash is part of the value; XML requires quotes).
33 parser tests; exhaustive enumeration of all inputs up to length 6 over a
markup-heavy alphabet and 6M randomized structured cases, checking every node
range, every `close_index`/`end_index`, nesting consistency, attribute
dedupe and determinism.
* cargo-makepad: declare native speech recognition metadata
* cargo-makepad: make Apple plist customization opt-in
* cargo-makepad: use macOS tools for plist overlays
* TextInput: add replace_range for edits that aren't typing
Anything that writes into a text field without being the keyboard —
dictation, autocomplete, a paste button — had only set_text and
restore_state to work with. Both clear the undo history, neither emits
Changed, and both end an IME composition that the platform keyboard still
thinks is in progress, so the next commit from the keyboard lands on text
the widget no longer agrees about.
replace_range(range, text, UndoGroup) goes through the same edit path as
typing: the input filter applies (text it rejects outright is refused
rather than deleting the range), the edit lands in the undo history,
Changed is emitted, and the selection is carried across it (anything that
was inside the range ends up after the replacement). UndoGroup::Extend
joins consecutive external edits into one undo step, so every revision of
a dictated phrase undoes together, while any typing in between splits
them. The IME composition belongs to the keyboard: an edit beside it moves
it and pushes the new text to the platform, and an edit overlapping it is
refused with ReplaceRangeError::Composing. is_composing() lets callers
wait for it to clear, and force_new_edit_group is now reachable from the
ref as well.
Undo and redo now end a composition, since the text the IME was composing
is gone with the rewind; the widget used to keep pointing at it, which
also stalled its IME syncing until focus was lost.
* ios: report the keyboard's marked text as the IME composition
The UITextView bridge forwarded marked text (kana awaiting conversion and
the like) to the widget as plain text with no composition range, so
TextInput never knew the keyboard was mid-composition on iOS:
is_composing() stayed false, replace_range's Composing refusal never
engaged, and the widget's next push did a whole-text setText: that
detached the keyboard's composition from the buffer.
forward_state_to_makepad now reads markedTextRange and carries it through
full_state_sync, so the widget records it exactly as it does for Android.
When the widget pushes text while it still holds a composition (an edit
beside it through replace_range), set_ime_text writes the text around the
composition and marks the composed part again with setMarkedText: instead
of committing it.
* macos: complete the handshake with a development launcher
`cargo run` starts a bare executable, which macOS gives no bundle identity.
Microphone, speech and location prompts are then attributed to the terminal
or editor that spawned it, and denied outright when that process has no
matching usage description, so a permission-using app cannot be developed
with plain `cargo run` at all. The way around it is a cargo runner that
launches a real .app through LaunchServices.
Three things are then lost, because LaunchServices forks the process itself
and starts it in `/`: the runner never learns the app's pid, so it has
nothing to forward a Ctrl-C to; it cannot pass on the terminal's working
directory; and it never sees the app's exit code, so `cargo run` always
reports success. All three are only knowable in-process.
The macOS event loop now reports them through the directory named by
MAKEPAD_DEV_LAUNCH_DIR, adopting MAKEPAD_DEV_WORKING_DIR before any
resource is loaded. Apps launched any other way see neither variable and
do nothing, so this replaces the same handshake being hand-written in every
app's main() that wants to develop against a permission-gated API.
* cargo-makepad: declare native speech recognition metadata
* cargo-makepad: make Apple plist customization opt-in
* cargo-makepad: use macOS tools for plist overlays
* TextInput: add replace_range for edits that aren't typing
Anything that writes into a text field without being the keyboard —
dictation, autocomplete, a paste button — had only set_text and
restore_state to work with. Both clear the undo history, neither emits
Changed, and both end an IME composition that the platform keyboard still
thinks is in progress, so the next commit from the keyboard lands on text
the widget no longer agrees about.
replace_range(range, text, UndoGroup) goes through the same edit path as
typing: the input filter applies (text it rejects outright is refused
rather than deleting the range), the edit lands in the undo history,
Changed is emitted, and the selection is carried across it (anything that
was inside the range ends up after the replacement). UndoGroup::Extend
joins consecutive external edits into one undo step, so every revision of
a dictated phrase undoes together, while any typing in between splits
them. The IME composition belongs to the keyboard: an edit beside it moves
it and pushes the new text to the platform, and an edit overlapping it is
refused with ReplaceRangeError::Composing. is_composing() lets callers
wait for it to clear, and force_new_edit_group is now reachable from the
ref as well.
Undo and redo now end a composition, since the text the IME was composing
is gone with the rewind; the widget used to keep pointing at it, which
also stalled its IME syncing until focus was lost.
* ios: report the keyboard's marked text as the IME composition
The UITextView bridge forwarded marked text (kana awaiting conversion and
the like) to the widget as plain text with no composition range, so
TextInput never knew the keyboard was mid-composition on iOS:
is_composing() stayed false, replace_range's Composing refusal never
engaged, and the widget's next push did a whole-text setText: that
detached the keyboard's composition from the buffer.
forward_state_to_makepad now reads markedTextRange and carries it through
full_state_sync, so the widget records it exactly as it does for Android.
When the widget pushes text while it still holds a composition (an edit
beside it through replace_range), set_ime_text writes the text around the
composition and marks the composed part again with setMarkedText: instead
of committing it.
`process_ns_event` hands each NSEvent to AppKit via `sendEvent:` before
emitting Makepad's own KeyDown. When an IME has marked (composition)
text, that dispatch lets the IME consume the key: Return/Space commit
the candidate, digits pick one, arrows navigate, Escape discards,
Backspace edits the preedit. A committing key clears the marked text
during dispatch, so the old post-dispatch `hasMarkedText` check (which
only covered Backspace) could not see it, and the Return that merely
committed a pinyin candidate was also delivered as KeyDown(ReturnKey).
TextInput then treated it as a submit.
Snapshot `hasMarkedText` before `sendEvent:` and skip the KeyDown
callback when the IME was composing. This subsumes the Backspace check
and also fixes the case where Backspace deleted the last preedit
character and then fell through to delete committed text.
Claude-Session: https://claude.ai/code/session_017HLgZDz8vuuqqMya1nCWKf
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Use cached transparent 2D and cube textures for unallocated optional samplers without bypassing invalid-resource, target, or framebuffer-feedback guards.
Require actual glyph draws in release smoke tests and keep shader compilation asynchronous in probe instrumentation.
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.
Use output/presentation timestamps instead of AVPlayerItem.currentTime on the UI thread. Consume only fresh frames and stop polling paused players after their poster arrives. Balance Objective-C ownership and make native cleanup idempotent.
Validation: release Flow playback smoke and process sampling; the previous currentTime mutex hotspot is absent in the updated sample.
* wayland: stop inverting the scroll direction
Wayland's wl_pointer axis values already carry Makepad's scroll
convention -- positive vertical means scroll down, i.e. the viewport
moves down. The backend negated them, so wheel and touchpad both
scrolled backwards relative to X11, macOS, Windows and web.
The spec pins the sign in wl_pointer::axis_relative_direction, whose
`identical` case is a user's fingers moving down producing a
"vertical_scroll down" axis event. libinput, which produces the values
compositors forward, documents the same: "the positive direction being
down or right". Makepad's own convention matches -- ScrollBar applies
`scroll_pos + e.scroll.y` against a position clamped to
[0, view_total - view_visible], and the turtle draws content at
`origin - layout.scroll` inside a clip rect fixed at the unshifted
origin, so a positive delta moves the viewport down.
The negation came from #875, which read a positive axis value as content
sliding down and cited winit's negation as precedent. But winit's
MouseScrollDelta is documented as positive = content moves down, the
inverse of Makepad's convention -- winit's own comment reads "Wayland
sign convention is the inverse of winit" -- so copying it was a double
negation. Whether a toolkit negates is decided by its own convention,
not by anything about Wayland: GTK, which shares Makepad's convention,
passes the values through; SDL and Chromium negate because theirs are
inverted, and SDL negates vertical only, which is self-consistent just
in case Wayland's +y is down and +x is right. #875 also cited the web
backend as agreeing, but web forwards DOM deltaY unnegated, and deltaY
is positive when scrolling down.
The AxisDiscrete and AxisValue120 handlers added later inherited the
sign, so all six sites flip together; the spec states each expresses its
direction along the same axis as the coupled axis event.
Natural scrolling needs no client-side handling. libinput applies it in
evdev_notify_axis_*, below the compositor, so the delivered axis value
already reflects the user's setting -- the negation was not implementing
that, it inverted both settings equally. AxisRelativeDirection stays
ignored, which is correct for scrolling content; it exists so widgets
that should track the physical wheel regardless of the setting (the
spec's example is a volume slider) can recover the direction.
Fixes#1173
* wayland: classify the scroll source, and choose each axis's delta on its own
Five defects in the wl_pointer frame handler, adjacent to the sign fix in
the previous commit but independent of it.
The detent-vs-pixel choice was made once for both axes, so a frame
carrying detents on one axis and only a smooth value on the other scaled
that second axis by a zero detent count and silently dropped it. Each
axis now chooses on its own.
`scroll_is_wheel` collapsed a five-valued classification into "Wheel vs
everything else", and its false default meant "finger gesture". So a
wheel tilt discarded its detents, a continuous source — a trackpoint, or
button-held scrolling — was reported as a touchpad gesture, and so was a
frame from a compositor that sent no axis_source at all, the event being
optional and sent only when the source is known. That default is the one
classification that can strand a widget: ScrollPhase::Ended is what
springs a stretched rubber band back, only a finger source is guaranteed
an AxisStop, and the spec tells clients to treat every other source as
unterminated by default. The bool gives way to the source itself, and a
sourceless frame is classified by whether it carried detents.
A bare AxisStop no longer dispatches for a source with no gesture to end.
Compositors stop an axis whenever its value reaches zero, whatever the
source, and a zero-delta ScrollPhase::None clears a widget's overscroll
and cuts short a running bounce.
Nor is a stop arriving alongside live motion treated as lift-off. Per the
frame event: "When a wl_pointer.axis and a wl_pointer.axis_stop event
occur within the same frame, this indicates that axis movement in one
axis has stopped but continues in the other axis." And because
axis_source is per-frame and optional, a gesture in flight now carries
its classification forward, so a lift-off frame that omits the source
still ends the gesture instead of losing the terminator.
The raw-pixel fallback for an axis without detents stays unscaled, which
is a deliberate non-change rather than an oversight. No units-per-detent
constant exists to scale it by — compositors disagree, and hwdb ships
wheels from 10 to 30 degrees per click — and a physical wheel never
reaches it: the fallback is for virtual pointers, whose axis value the
protocol already defines as a distance.
Finally, the claim that ScrollPhase::Ended lets widgets run their own
momentum fling was wrong. Widgets start their fling on
ScrollPhase::Momentum, which only macOS emits, so Wayland touchpads have
no kinetic scrolling at all; the comment now says that rather than its
opposite.
The frame decision moves into `frame_scroll`, which puts every case above
under a unit test instead of leaving it to be re-derived by reading.
Makepad windows on Wayland had no drop shadow, which on GNOME reads as
broken next to everything else on the desktop. Mutter implements no
server-side decoration protocol at all -- it advertises neither
zxdg_decoration_manager_v1 nor any KDE equivalent, and its shadow code
(MetaShadowFactory) lives in src/x11/ and isn't even in the
introspection surface. Every shadow on that desktop is drawn by the app
that owns the window.
So draw one, out of eight wl_subsurfaces hung outside the toplevel: four
corner tiles and four edge strips, backed by one memfd wl_shm pool and
sized with wp_viewport, with xdg_surface.set_window_geometry keeping them
out of the window's logical bounds. GTK instead oversizes its own surface
and paints the shadow into a transparent margin. Subsurfaces keep the GL
surface exactly window-sized, so the shadow costs no per-frame GPU fill,
and no margin ever crosses the platform/widget boundary -- which is the
entire class of off-by-a-margin bugs the other approach invites.
The profile is libadwaita 1.9's, computed rather than sampled. A
rectangle's Gaussian shadow is separable, so each box-shadow layer's 2-D
coverage is the product of two 1-D normal CDFs, and evaluating that for a
*square* rectangle is what makes the corners hug the window: sampling a
rounded window's shadow gives 14/255 where a square corner needs 44/255,
and fades the edge out over the last 20px before every corner. The
straight-edge profile this produces matches a capture of the real
libadwaita output to within 1/255, which is what the test pins. Corner
tiles reach 16px along each edge, far enough that they join the strips
bit-identically at any scale.
Resizing happens in the gutter, the way it does for every native app.
The shadow surfaces carry input regions whose union is the window rect
grown by 12px -- the same halo libadwaita gives its toplevels -- and each
piece maps to exactly one edge, so landing on a surface is the hit test.
Window controls no longer compete with the corner grabs for the pointer,
which is what let the close button swallow the top-right corner.
Server-side decorations are requested wherever a compositor offers them,
overridable per process with --wayland-decoration= or
MAKEPAD_WAYLAND_DECORATION, and fall back to the frame above. KWin and
wlroots grant them; GNOME cannot.
Alongside, the caption bar gains double-click-to-maximize, a right-click
window menu, resize cursors keyed off the wl_pointer.enter serial the
protocol actually asks for, and tiled/constrained edges that suppress the
grabs they cannot service -- degrading a corner to its free axis rather
than dropping it.
Finally, declare the toplevel's opaque region, under the same
`!transparent && backdrop == None` condition macOS already uses for its
layer's opaque flag. The buffer is ARGB8888, so without that promise a
compositor cannot learn the alpha is uniformly solid short of reading
every pixel: it must blend the whole window, cannot cull what the window
covers, and cannot scan a fullscreen buffer out directly.
Verified against a WAYLAND_DEBUG trace: over 67 committed frames the
shadow issues no protocol traffic at all, and set_window_geometry,
set_opaque_region and the nine wl_regions are each sent and destroyed
exactly once.