`smooth_scroll_to()` never cleared `tail_range`, so on a list that was following
its end (e.g., a chat timeline restored at the bottom), each draw pulled the list
back down while the scroll animation moved it up, and the target was never reached.
Now scrolling to any item but the last one stops following the end and drops any
pending tail adjustment. Scrolling to the last item still resumes tailing once it lands.
* Constrain Splash external I/O to the host service bridge
* Validate untrusted Splash source with the host I/O restriction
* Give strict Splash validation a disposable storage jail
* Close the remaining ways out of the Splash host I/O restriction
- Keep CachedWidget singletons per heap, and don't register CachedWidget
or WindowMenu in restricted isolates.
- Don't emit Html/Markdown link URLs as actions from a restricted isolate,
and ignore its menu bar updates.
- Suppress clipboard copy/cut hits for any restricted isolate, not only
within Splash.
- Give script calls made while an isolate is installed the same budget.
- On web, let package resource fetches skip the guest I/O guard.
- Name the validation jail without the wall clock, and skip it on wasm.
- Make the host I/O tests fail when their guards are removed.
- The loopback MCP server moves from Director into libs/ai/services (mcp::server); Director re-exports it, its lane tokens and tests unchanged. The dispatcher now names the server and its instructions; a TokenStore can live in memory only (ephemeral).
- mcp::host: while Claude Desktop is the provider the panel serves its registry's tools (service__tool names) on an ephemeral 127.0.0.1 port with a fresh bearer token, and writes ~/.makepad/mcp/<exe-stem>.json {pid, port, token, title} (0600 in a 0700 dir), removed when the provider changes or the panel goes. Calls queue to the UI thread and run through EngineCore::call_external: a card in the transcript, destructive calls held for the person's confirm (the pane opens for it), the result sent back when the card lands.
- mcp::mcpb: "Connect to Claude Desktop" writes <exe-stem>.mcpb (a stored zip, manifest_version 0.3, binary server = this executable with --mcp) and opens it so Claude Desktop shows its install dialog.
- platform mcp_relay: `<app> --mcp`, checked first in app_main before any Cx, window, GPU or audio, relays newline JSON-RPC on stdio to the running app's endpoint. It answers initialize/ping itself and tools/list from the app's last list while the app is down, starts the app (detached, MAKEPAD_AI_PROVIDER=claude-desktop, engine up with the pane closed) on the first call that needs it, waits up to 20 s for its file, and exits when stdin closes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- task: a right-click column chooser (sections as flyouts, Default Columns), every figure and graph its own sortable column, dragged order and widths saved; per-process network bytes/packets from the kernel's ntstat control socket (matches nettop, no root), disk bytes, footprint and idle wake-ups every tick; history journal v4
- widgets: data_grid_columns, one column helper (chooser, reorder, resize, fit, sort cycle, layout text) that task uses and other tables can reuse; the menu engine refreshes marks inside an open flyout; the segmented control centres its labels on the line height and no longer glides after a moved row
- svg: a stroke join never connects to the previous subpath (the diagonal through outline icons)
- platform: home::app_data_dir; script: ScriptIp body ids widened to 14 bits (16384 bodies, was 4096) with an index of 26 bits, and a clear stop instead of aliasing past the limit
- audio_route (new): tap an app's audio output through the Core Audio process tap into a host processor (equalizer, gain, limiter, analyzer) and play it; audio_picture owns the one FFT; audio_decode probes tags and length from a file's head and tail; search::fold_words; zip_file reads archives with a trailing comment
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The WM phone shell rides on it: the simulated finger in the desktop skin, time-based release velocity (80 ms window, stale after a 120 ms rest), one critically damped spring (k 900, c 60) seeded with that velocity for paging, drawer, recents and app open/close, an 8 pt / 1.2x axis lock latched through release, a drawer that tracks the finger 1:1 and draws opaque over the home tiles, hold-to-Recents precedence, launches that zoom from the icon actually drawn with an opaque launch card, no ghost card on close, and one cancel/reset path for rotation, resize, focus loss, style switch and keyboard navigation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pressing a radio grabbed key focus immediately, and the theme draws the focus
ring in the same color as hover, so a press dragged off the button left what
looks like a stuck hover highlight behind.
Move `set_key_focus` to the completed click, so a cancelled press leaves no
mark. Keyboard focus still arrives through `Hit::KeyFocus`.
* HtmlLink: don't keep the hover color after a touch release
`Hit::FingerUp` played `hover.on` whenever the release was over the link, and
that state sets `pressed` as well as `hovered`. Touch never sends a
`FingerHoverOut` afterwards, so a tapped or long-pressed link kept the red
pressed color until its list item was recycled.
Guard that branch with `has_hovers()`, the way `TextFlowLink` and `Button`
already do.
* HtmlLink: let `hover_color` show while hovering
`hover.on` snapped `pressed` to 1.0, and `draw_walk` checks `pressed` before
`hovered`, so a hovered link always drew `pressed_color` and any `hover_color`
was dead. `TextFlowLink` carries the same animator but checks `hovered` first,
which is why it never showed there.
Clear `pressed` in that state, like `Button`, `CheckBox` and `Markdown` do.
Its `from` clause already fades `pressed` over 0.01s, which only makes sense
fading to zero.
* RadioButton: don't keep the hover tint after a touch release
`Hit::FingerUp` played `hover.on` unconditionally, and touch never sends a
`FingerHoverOut` afterwards, so a tapped radio kept its hover tint until
something else redrew it. Releasing the mouse away from the button left it
tinted too, since the arm never checked `is_over`.
Guard that branch with `is_over` and `has_hovers()`, the way `Button` and
`TextFlowLink` already do.
* RadioButton: only select when the release is over the button
`Hit::FingerUp` selected and emitted `Clicked` without checking `is_over`, so
pressing a radio and releasing anywhere else still selected it. `Button` gates
its click on `is_over`; do the same here.
A Splash isolate receives makepad's own mods and nothing else, so a widget
type defined in host code is unnameable from a mounted body. Octoscript-Makepad
hit this with OctoscriptTap: unable to name it, the generated body had to
target a Button, whose handle_event captures the finger on touch-down, so
every tappable row starved the scroll it sat in.
register_splash_isolate_mod(fn(&mut ScriptVm)) records an installer; each
isolate runs the registered installers as the last step of its allocation.
Last matters: it is after the ambient-authority strip (fs, run, res,
cx.quit) and after the jailed fs and brokered host re-registrations, so a
host mod cannot be removed by that pass and sees the isolate's final
namespace. Host code is trusted and already chooses what it installs.
The registry is a thread-local because the caller has no Cx in hand, and is
collected before running so an installer may register another. Mods are
taken at allocation, so a registration only reaches later isolates.
Test covers the contract in both directions: an isolate allocated before
registration does not resolve the probe, one allocated after does, each
allocation installs again, and the earlier isolate stays unchanged.
makepad-widgets: 207 passed, 5 failed — the same 5 that fail on the
unmodified branch (desktop_style, grid x2, widget_tree x2).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR1KyERPDtrF9FgZ9HvWqt
(cherry picked from commit 227aa3625b8e53ab1ff3f153f63933564849640d)
(cherry picked from commit 9addb746b2497abd9fe530aebb68b3c3d99b0ccf)
* Tooltip: position anchored tooltips in the same draw
`CalloutTooltip` drew itself invisibly, waited on a 5ms timer, then read the
drawn size back and re-applied margins/widths until the height stopped
changing. When the timer beat the paint it read a zero rect, gave up, and left
the tooltip opened but invisible.
* base `Tooltip` gains `show_anchored`: it draws `content`, measures it, and
shifts its align range into place (`Tooltip::place` flips to the roomier
side, clamps to the safe area, and keeps `gap` from the anchor and edges)
* `CalloutTooltip` now derefs `Tooltip`, measures its label each draw to pick
the wrap width, and writes the callout edge/offset into the shader post-draw
* Tooltip: ignore zero-delta scroll events
macOS sends a zero-delta `Scroll` whenever fingers touch or rest on the
trackpad (`ScrollPhase::Touched`, and `Began` for a two-finger rest). That hid
every open tooltip, and since the pointer never left the target, nothing showed
it again. Only a scroll that actually moves counts as an interaction now.
* Wayland: restore a maximized window maximized, not fullscreen
`configure_window`'s `is_fullscreen` is the legacy maximize-or-fullscreen
flag, and on Wayland the reporting side says so out loud: `wayland_state`
builds the geom with `is_fullscreen: is_fullscreen || is_maximized`. But
`WaylandWindow::new` took that same bool and called
`toplevel.set_fullscreen(None)`. So an app that persists `is_fullscreen()`
on exit and feeds it back through `configure_window` on the next launch --
Robrix does exactly this -- turns a window the user had merely maximized
into true compositor fullscreen, and the escalation sticks: the next save
records the monitor size rather than the work area.
Wayland was alone in reading the flag that way. x11 creates with
`self.maximize()` (`_NET_WM_STATE_MAXIMIZED_HORZ/VERT`) and reports
`get_is_maximized()` back; win32 creates with `ShowWindow(SW_MAXIMIZE)`
and reports the `WS_MAXIMIZE` style bit. Both round-trip. Only macOS takes
the flag literally, and AppKit keeps a menu bar and traffic lights there.
* Create with `set_maximized()`.
* Seed `is_maximized` from the request rather than `is_fullscreen`.
`wayland_is_fullscreen` is copied out of that field before the first
configure arrives, so the old seeding claimed fullscreen from frame one,
before the compositor had confirmed anything, while `window_geom` still
said `is_fullscreen: false`. The two signals no longer disagree.
* `should_show_csd_shadow` gets the request as `maximized`; same answer as
before, now for the right reason.
An app that wants to come up genuinely fullscreen still can:
`WindowHandle::fullscreen()` during `Event::Startup` queues
`FullscreenWindow` behind `CreateWindow` in the same FIFO drain.
Also corrects the docs this contradicted. `configure_window` claimed
`inner_size` and `position` are ignored when fullscreen and that the
window is sized to the monitor, which is true on no backend now, and
`maximize()` claimed macOS zooms when it calls `toggleFullScreen:`.
* Wayland: keep our window chrome up in fullscreen
Under client-side decorations we draw the title bar and the min/max/close
cluster ourselves, and we were hiding both the moment the compositor put
the toplevel in fullscreen. The compositor draws nothing in their place,
so the window ends up with no chrome at all -- and the max button is the
only path to `RestoreWindow`, so there is no way back out. There is no
F11, no Escape, `Window::handle_event` has no `KeyDown` arm, and the View
menu is inert outside macOS. That leaves the compositor's own keybinding,
if the desktop happens to have one bound.
Hiding chrome in fullscreen is the right call when the OS supplies its own
-- macOS has an auto-hiding menu bar and traffic lights there, which is
why that arm stays as it is. Wayland supplies nothing, so ours stays up.
* `sync_caption_bar_state` drops the `wayland_fullscreen` terms: the
caption bar and the buttons now follow `custom_chrome` alone.
* Fill `window_chrome_buttons` in the geom in fullscreen too. It is the
transitional hit-test rect `WindowDragQuery` falls back on before the
widget layout is known, so leaving it empty made the first clicks after
entering fullscreen read as a caption drag instead of a button press.
* Let the caption's own gestures through in fullscreen -- they were gated
behind `!is_fullscreen` on a bar that could not be visible then anyway.
Double-click now unsets fullscreen first: `set_maximized` under it does
nothing, so the bar would have looked dead.
`is_wayland_fullscreen()` loses its only caller but stays public: it is
the only way to tell true fullscreen from maximize, which the conflated
`is_fullscreen()` cannot. Say so on `WindowGeom::is_fullscreen` too, since
reading it as real fullscreen is what started this.
* Window: drop hide_caption_on_fullscreen, a dead trap
The `WindowGeomChange` arm hid the caption bar whenever the geom flipped
to `is_fullscreen`, on `Windows | Macos`. On Windows that flag is literally
`get_is_maximized()` (`win32_window.rs`: `is_fullscreen:
self.get_is_maximized()`), and Windows draws its own chrome, so this would
have stripped the close button on a plain maximize -- the same trap just
fixed on Wayland, one `#[live]` default away from firing.
It never fired, and could not have: `hide_caption_on_fullscreen` is set
nowhere in makepad or in any app (grep finds no other mention, DSL
included), and `sync_caption_bar_state` re-decides caption visibility on
every event through `ensure_initialized()`, so it overwrites whatever this
arm set. The macOS half it duplicated lives there already.
Deleted rather than repaired: the whole caption policy belongs in
`sync_caption_bar_state`, and a second copy that keys off a flag meaning
different things per platform is what produced the bug in the first place.
* macOS: restore() no longer enters fullscreen
`restore()` and `maximize()` were the same call, `toggleFullScreen:`, so
`CxOsOp::RestoreWindow` on a window that was not fullscreen put it *into*
fullscreen. The Window widget's max button hands `restore()` whatever
`is_fullscreen()` reports, which on macOS is the real NSWindow fullscreen
state -- so this only misfires when something else pushes `RestoreWindow`
on its own, but then it does the exact opposite of its name.
Guard on `is_fullscreen`, the field the fullscreen delegates maintain.
* x11: implement FullscreenWindow and NormalizeWindow
Both fell through to the catch-all `Not implemented on this platform`, so
`WindowRef::fullscreen()` and `disable_fullscreen()` silently did nothing
on x11. `_NET_WM_STATE_FULLSCREEN` was never even interned -- the atom
table only carried the two maximize atoms.
* Intern `_NET_WM_STATE_FULLSCREEN`, and split the `_NET_WM_STATE` client
message out of `restore_or_maximize` so the fullscreen requests can
reuse it instead of copying the send.
* `get_is_maximized` becomes a thin caller of `has_net_wm_state`, which
`get_is_fullscreen` shares.
* `get_window_geom` reports `maximized || fullscreen`, the same union
Wayland reports and the meaning the flag already had. Creation still
maps the flag to `maximize()` alone, so a persisted `true` cannot come
back as fullscreen -- the bug this branch opened with.
* `RestoreWindow` drops both states, matching the Wayland arm: a caller
restoring off `is_fullscreen()` means "make it small again", and the
union does not say which of the two is set.
* Wayland: go back to the floating size when leaving maximize
An xdg_toplevel configure of 0x0 means "pick your own size", which is what
the compositor sends on the way out of maximize or fullscreen. We fell back
to `window_geom.inner_size` -- the size we were maximized at -- so the
window came out of maximize still covering the work area, with nothing to
bring it back down. Creating a window maximized made it permanent: the
floating size was never recorded anywhere.
Track the last size the window actually floated at, and use that for the
0x0 case. `is_floating` excludes tiled as well as maximized and fullscreen,
so a half-snapped window does not get recorded as the floating size.
A configure that does carry a size is still obeyed exactly as before, which
is both what the protocol requires and what a user drag-resize produces.
Seen with a window created maximized and then normalized: 3383x1408 before,
the requested 900x600 after. Note this only covers the case where the
compositor defers to us -- GNOME sends a concrete size after unfullscreening
a window that was maximized first, and we honor it.
* Linux: tell the app when the pointer leaves the window
Neither Linux backend ever sent `Event::MouseLeave`. Windows has sent one
since forever, and `Hit` handles it -- `finger.rs` returns `FingerHoverOut`
for whatever area still holds the hover -- but on Wayland and x11 nothing
produced it, so the last hovered widget kept its hover the entire time the
pointer was outside the window.
That is what makes the window chrome buttons flicker. Hover close, move the
pointer off the top of the window, and the button stays lit; the hover only
clears on the first motion after the pointer comes back, so returning to the
window makes the button flash off. It is most obvious on the chrome buttons
because they sit against the window edge, where leaving the button and
leaving the window are the same gesture.
It also broke re-entry. `FingerHoverOut` does not clear the stored hover
area -- `cycle_hover_area` does, once per event -- so with no leave event
`hover_last` still named the button, and coming back over it returned
`FingerHoverOver` instead of `FingerHoverIn`. Widgets act on HoverIn, so a
button could fail to light up at all on the second hover.
* Wayland: emit it from `wl_pointer.leave`, guarded on `pointer_window`
being set, which it is only for the toplevel surface -- leaving a CSD
shadow gutter has no hover to drop.
* x11: finish the `LeaveNotify` TODO that has been commented out in
`xlib_app`. The condition it had, `detail == 4`, would not have fired
anyway: a real pointer-out-of-window here reports detail 3
(NotifyNonlinear). Take any detail except NotifyInferior (the pointer
only moved into a child), and only mode NotifyNormal, so a grab or
ungrab does not drop the hover mid-drag.
* Both event loops then `cycle_hover_area` + `switch_captures`, the same
pair the MouseMove arm uses and the same thing the Win32 arm does.
Verified on both backends with a probe on the hover in/out arms: before,
leaving the surface logged nothing; after, HoverOut fires on leave and a
fresh HoverIn on re-entry.
* DesktopButton: cross-fade the hover background premultiplied
The chrome buttons flash dark for an instant when the pointer leaves them.
Not a hover-state bug -- the `hover` instance really does fall 1.0 -> 0.0
monotonically over the 100ms fade. The dip is in the shader.
`bg_color` is `#00000000`: transparent BLACK. Mixing it toward an opaque
`bg_color_hover` in straight-alpha space ramps rgb up from black as well as
alpha, and `sdf.fill` then multiplies rgb by that same alpha again, so what
reaches the premultiplied blend is `rgb * h^2` against coverage `h`. Over a
caption bar of luminance C the composite is quadratic in h and sags well
below both endpoints in the middle.
Measured on a #F3F3F3 bar with the `#E9E9E9` hover face this file's callers
use (widgets/src/window.rs), sweeping hover across the button's width:
hover 0% 30% 52% 80% 100%
before 246 207 199 212 238
after 246 244 242 240 239
The endpoints are 8 levels apart, so the intended highlight is nearly
invisible -- and the 47-level excursion between them is the only thing the
eye catches. It is symmetric, but on the way in it reads as arrival feedback
and on the way out it is a dark flash left behind where the pointer just
was, which is why it gets reported as flicker on hover-out.
Premultiply each face before mixing and fill with `fill_premul`, so the
cross-fade is linear in `hover` and anti-aliasing blends in premultiplied
space too. `DesktopButton` is the only widget with a fully transparent base
colour feeding this pattern.
* ScrollBar: add `show_handle` for a view that scrolls without a grabbable bar
The handle is both the visual and the hit target, and `show_scroll_x`
gates wheel/trackpad input too, so there was no way to keep a view
scrollable while dropping the bar a user can click.
* `show_handle: false` skips drawing the handle and its hit test.
* Wheel, trackpad, finger drag and the scroll API are untouched.
* ScrollBarTabs: stop the invisible handle from eating presses on tabs
The tab-bar handle is transparent until hovered, and it runs along the
bottom of a strip whose tabs are exactly as tall as it, so it sits over
the lower edge of every tab. `TabBar` hands presses to the scroll bars
before the tabs, so while the strip overflows, a press near a tab's
bottom grabs a bar nobody can see instead of selecting the tab.
* Default `show_handle: false`, so `TabBar`/`TabBarFlat` scroll only by
wheel, trackpad and drag.
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 resolve flipped V and mirrored the whole window on Metal, grab-verified the wrong way round; the mask flipped on Metal and iOS alone. Both textures are ordinary passes drawn with the 2D camera, top-left on every backend.
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>
`Button` matched only `KeyFocus` and `KeyFocusLost`, so a button reached with
Tab could take focus and animate, but no key press ever activated it.
* `Space`, `Enter` and numpad enter now emit `Pressed` and then `Clicked`, and
make the same `on_press`/`on_click` script calls a tap does.
* Key repeat is ignored, so holding the key down doesn't re-press.
* Both arms are gated on `enabled`, like the finger paths.
The contribution widened `StyleTween` to eight weights but left its two initialisers at
seven, so the window manager did not compile. It also declared `BlackOrange` second in
`DesktopStyle`, while the window manager reads the tween's weights by discriminant (1 is
macOS, 3 Windows 2000, 4 NeXTSTEP): every style after Omarchy would have driven the chrome
of the one before it. The new style is declared last, `ALL` keeps the order the sheets are
shown in, and `next()` walks `ALL` by place rather than by discriminant. An unused import
in a storybook story goes.
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>
apps/mail indexes local Apple Mail through the Envelope Index: Gmail
Inbox, Sent, Starred and Important are label rows, attachments open
from the message view, and a reimport button wipes the cache. apps/wm
hosts apps as on-demand dylibs on Android (apps/wm-dyn, with the module
apps' manifests and module.rs following). apps/terminal polls the child
with MpTerm::process_exited and resolves widget fonts through
makepad_widgets. Widgets: double-click selects a word in TextFlow,
links keep their hand cursor, and three stale tests follow the tree's
root rule and the mobile font policy.
Squashed from work (the apps and widgets parts of each):
- Index local Apple Mail with Gmail labels, attachments and reimport
- terminal: MpTerm::process_exited polls the child, and widget fonts resolve through makepad_widgets
- widgets: double-click selects a word in TextFlow; links keep their hand cursor
- wm: the Android super-app hosts apps as on-demand dylibs (apps/wm-dyn)
- widgets: three stale tests follow the tree's root rule and the mobile font policy
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>
* 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
`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.
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 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 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):
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>