Commit graph

2,973 commits

Author SHA1 Message Date
Admin
ccadef613e widgets: touch lists scroll with Android's physics -- Mail's inbox on the Pixel felt indirect and its flings wrong. Touch scrolling in PortalList and ScrollBar-driven views now shares widgets/src/scroll_motion.rs on Android: release velocity is estimated like VelocityTracker (LSQ2 over 100 ms, zero after a 40 ms rest), flings follow OverScroller's spline to its end within 50-8000 dp/s, the 8 dp slop is subtracted so the content does not jump when the drag takes over, and edges stretch briefly and firmly. Mouse, trackpad and every other platform are unchanged. Mail re-binds a list row only when its row, the selection or the layout changes instead of re-running script evaluation for every visible row each frame (4.2 -> 3.05 ms per child frame)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
6487c41d3a wm on Android: gestures and surfaces settle like the phone's -- the home wallpaper's motion slows over about 5 s and stops instead of animating forever; the status and navigation bands are a flat colour fading 16 pt into the app instead of the app's stretched edge rows (Weather's clouds smeared into warped bars); swiping an app up keeps its bottom edge under the finger (Quickstep's rule), a pause opens Recents and 36 dp up goes Home; on Home a swipe from the bottom strip opens Recents and a swipe anywhere above opens the drawer; opening an app from Recents only grows, no bounce. The Clock tile no longer redraws every second
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
3e01d10828 android: hosted children hand frames over with GPU fences and draw only when they have work -- Mail as a WM child ran at 20-26 ms per frame (3% of frames at 120 Hz): the child CPU-waited its GPU before announcing each frame and the WM paced it one frame per WM frame on a fixed 125 Hz beat. The child now exports a SYNC_FD semaphore per frame over the socket that carries its AHardwareBuffers and the WM waits on it GPU-side; the WM returns a release semaphore so the child never renders into an image being sampled (VK_KHR_external_semaphore_fd; children announce that they fence, and anything else falls back to the CPU wait). The WM ticks a child on its display frame only when it has work (a requested frame, input, a due timer, startup, a bootstrap), and host messages handled outside a tick ask for a frame. Idle wakeups are gone: host messages no longer wake the child's UI loop, the 2 s heartbeat, the 125 Hz beat on empty views and the 20 Hz warm-pool timer are removed, the bar no longer redraws every second, and touch times are mapped to the app clock (the phone shell treated every touch as just now and never came to rest). Mail landscape fling: 20.5/29.2 ms -> 8.48/11.6 ms (p50/p95), 86% of frames at 120 Hz; the WM home at rest repaints once in 10 s instead of ~1220 times
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
801a07e884 vulkan (android): a window released on suspend is not released again -- switching away from a Makepad Android app and back crashed it (SIGSEGV in ANativeWindow_release from CxVulkan::update_surface): suspend_surface had already released the window and set it to NULL, and update_surface released it again. Only a non-null window is released now
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
dedc447e68 android: rotated Vulkan windows stop rebuilding their swapchain every frame, and hosted children get touch -- on the Pixel, Mail as a WM child process could not be scrolled in portrait and ran at about 40 fps in landscape. In landscape the WM's swapchain (IDENTITY pre-transform on a rotated display) reported VK_SUBOPTIMAL_KHR on every present and vulkan.rs rebuilt the whole swapchain each frame: present went from 34-35 ms to about 1 ms and repaint from 52-66 ms to 2-7 ms once that one known Android case (extent and transform unchanged since creation) no longer rebuilds; every other SUBOPTIMAL, and desktop Wayland/X11, rebuild as before. The WM forwards the finger to a child as mouse events, so a press on a row captured the mouse and the list refused to drag; an Android hosted child now dispatches it as one touch like the Activity build (Cx::dispatch_hosted_touch), cancels included, and reports its focused text field on a cancel. Mail's toolbar buttons are flat discs with hover/press states instead of glass (the child drew its window twice per frame: 12.8 -> 7.1 ms). The WM's bottom band draws a smooth app edge stretched (Weather's sky) and a busy one in its dominant colour, so text scrolling under it no longer smears into streaks
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
0c83b8c73f wm on Android: app transitions and gestures feel like the phone's -- on the Pixel, opening Clock from its live tile showed a streaked frame, the swipe up to Recents moved the app about 0.28 pt per pt of finger travel, the bottom gesture was hard to find, fading cards went grey and a closed app landed on a white card then "Loading". The WM now sizes a child's shared images for full screen before it opens (MpRunView::prepare_size), the child refuses to draw into a smaller image and the WM rejects such frames; the lifted card's centre follows the finger 1:1 while it shrinks and on release travels straight to its target; the WM draws its own 32 pt bottom-gesture strip and pill above the OS gesture band; rounded captures use fill_premul (opacity was applied twice); closing into a tile keeps the tile's last face until the child confirms a new one, holding the old capture so a live tile never draws over the foreground app. On Android a child renders one frame per WM frame into three shared images, with tick counters that resync on each acknowledgement; the warm pool is off there. Calculator's portrait toolbar uses a flat button (the glass one refracted its neighbour). A debug hook setprop debug.makepad.grab burst-<tag>-<n> saves a child's next frames
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
e603d6ae84 wm on Android: apps build on the phone -- the multi-process WM's APK can now carry the toolchain, source tree and prebuilt target/ (proc-pack --proc-toolchain=<tc>, written as wm-proc-ondevice.apk), like wm-dyn's super-app, but without the engine dylib: each app is a small generated wrapper crate linking the engine statically, so an on-device cargo build reuses the shipped engine rlibs and compiles only the app. With debug.makepad.wm.ondevice=1 the launcher (--build) builds the app before loading it; builds take turns on a lock, each finished library is copied to its own file in files/run/ and loaded from there, and a build or lock wait past its deadline (300 s, 600 s for the first proc-macro bootstrap) is killed with its process group and the APK's prebuilt library runs instead. Provisioning, unpacking and the proc-macro bootstrap moved out of the WM's dylib host into libs/ondevice_build, shared with wm-dyn (streaming LZ4 parts, same stamps and paths); dyn-pack's staging was factored so proc-pack reuses it. The debuggable switch is opt-in and only for the on-device APK. Proven on a Pixel 11 Pro XL: calculator's source edited in the phone's copy rebuilt in 12.7 s and showed the change; four apps built in turn in about 6 s each
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
9baf6d6854 platform, wm: hosted apps ask the WM for what they cannot do themselves -- a hosted child has no OS window and, on Android, no JVM, so the clipboard menu, opening a URL, permission prompts, file pickers and HTTP were unavailable to it (Weather sat on "Loading" as a phone child process). AppToStudio::Relay / StudioToApp::Relay (appended, tags 26/27, existing ordinals pinned) carry these requests (platform/studio/src/relay.rs); the child side routes them automatically from the ordinary Cx APIs (platform/src/hosted_relay.rs), the WM executes them and answers by a host request id mapped back to the child's. HTTP for Android children is relayed through the WM's own Java HTTPS (NDK code has no TLS and the repo takes no external TLS crate), arriving as the NetworkResponses apps already handle; a relayed request fails after 60 s without an answer. The WM serves relays only to clients it holds a live connection to; open_url allows http, https, mailto, tel and geo; picked documents are copied on a worker into a per-pick cache folder (older than a day deleted); Android save/folder pickers answer cancelled until children can write through SAF. open_url, which was unimplemented on Android, now opens the URL. linux_direct gets a clipboard of its own (platform/src/direct_clipboard.rs): the WM owns it, Ctrl/Logo+C/X/V work as on X11, and children reach it through the existing copy/paste messages
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
3495ce41fa clock: a cancelled release neither opens an alarm nor switches tab -- the alarm list and the phone tab bar acted on any FingerUp; with the touch-cancel contract (237470eef) a press a scroller took away arrives as a cancelled release, which now only clears the press
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
a8bf0e4dcf wm on Android: every app is its own process -- the plain makepad-wm now ships as an Android APK (cargo makepad android ... proc-pack -p makepad-wm-android) whose phone apps run as real child processes instead of dylibs loaded into the WM. The interface between WM and app shrinks to what the desktop WM uses: shared GPU buffers and the StudioToApp/AppToStudio protocol. The WM allocates each app's swapchain as AHardwareBuffers and hands them over a unix socket (AHardwareBuffer_sendHandleToUnixSocket, API 26); the child, started through the libmakepad_launch.so launcher in nativeLibraryDir (W^X allows it there), imports them into a windowless Vulkan device and renders its window pass into them (android_hosted.rs). Each app library links the engine statically, so the engine-dylib identity handling of wm-dyn is not needed. Children have no JVM: assets are read from the APK file, audio device lookup no longer goes through Java, and the startup/resize extra draw of the Activity build is repeated so the first frame carries its text. On a phone the WM asks for the soft keyboard only while the child reports a focused text field. Proven on a Pixel 11 Pro XL: all twelve phone apps launch as processes (first frame 1.6-2.4 s cold), taps reach them. Not yet: HTTP in children (the Activity build borrows Java's), clipboard/permission/file-picker relays, idle-app reclaim, and on-device builds of app libraries
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
f9a8f12a00 wm: the phone shell's surfaces match the app and the grid -- in the Android skin a fixed light status band sat over dark apps, corner radii were drawn at twice their size (tiles 44 instead of 28, Recents cards 52), wallpaper seams showed at the screen edges and the home, drawer and Recents spacing drifted. The status and navigation bands now take the open app's own edge colours from a small WM-owned strip read back only after the app really changed (26 KB instead of the 5.2 MB full capture, off for good where readback is unsupported, nothing at rest), with ink chosen by contrast ratio and a wash where neither ink reaches 4.5:1; iOS gets the same bands. Android geometry follows the Material spec: the date under the 48 pt clock, 16 pt tile gutters, a 120x48 All apps target, 96 pt landscape tiles with one row of seven favourites, a 56 pt drawer search with centred Clear/Cancel, fixed row pitch, and Recents with a tonal backdrop, 24 pt icon + 14 pt title header and a 24 pt card gap. iOS keeps its previous radii, gaps and typography through skin-specific metrics
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
1e44574d45 files: the phone layout tidies up -- in the WM's Android skin Files ran Browse into its view switch, showed no selected view, clipped long names and let its footer cover the last row. Browse is its own 48 pt action, Icons/List/Storage is a segmented switch that shows the current view (List stays lit after List->narrow), the toolbar is 56 pt with 48 pt targets, names are 14 pt on two whole lines, and the footer reserves 24 pt. The segment row fits the narrowest phones
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:44 +02:00
Admin
3fed1ffce7 weather, notes, finance, photos, route: the phone layouts tidy up -- in the WM's Android skin Weather drew a dark box behind its city and temperature and ran its list controls over the forecast, Notes had a 56 pt empty spacer and small targets, Finance clipped negative amounts and squeezed four stats into narrow capsules, Photos showed a filesystem path in its footer and had no way back from a deep zoom, and Route's Layers button was a missing-glyph box. Weather's scrims feather instead of boxing, its controls sit on a solid 64 pt bar, the landscape hero is compact and the header fade reaches the regrouped labels. Notes gets 56 pt folder rows, a 20 pt chevron and 48 pt toolbar targets. Finance cards use 16 pt radius and spacing, amounts fit by measuring the whole formatted value, and narrow landscape panes show the stats in two columns. Photos gets a 64/56 pt search row, a Fit action that frames the visible picked or centred picture, and a readable "N pictures . library" footer. Route draws Layers as an SVG, shortens the location button to "Locate me" and aligns its three controls 16 pt from the edges
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:43 +02:00
Admin
6c6ebff2ec platform, widgets, wm: a touch that is taken away is cancelled, never released -- in the WM's phone skin the mouse now reaches the foreground app as a touch, and scrolling Mail then lifting opened the message under the finger: a list that took over a drag had no way to tell the pressed row it lost the press, and the same happened on real phones. Cancellation is now a contract. Event::FingerCancel and FingerUpEvent.cancelled carry it; was_tap() rejects it and no long press fires. claim_finger_gesture makes one owner per finger: PortalList and ScrollBar claim only with touch travel along their own axis (mouse drags scroll as before), and every loser, ancestors included, gets its terminal cancel in the same dispatch and never moves again. Pressed rows are cancelled before a list recycles them, cancels reach hidden widgets, and captures retire only after every consumer sharing the area has seen the cancel. iOS touchesCancelled and Android ACTION_CANCEL arrive as cancels and end an internal drag without a Drop. The WM relays StudioToApp::MouseCancel (appended, existing tags pinned) only to children that advertise it; inside a child it dispatches as FingerCancel. Widgets and apps that activated, committed, dropped, flung or resampled on any release now clear their state without acting (buttons, radios, menus, DataGrid, Kanban, Carousel, WheelPicker, RadialMenu, Modal, Dialog, Popover, colour controls, video hold-to-pause, maps, the Files treemap and tiles, Weather, Clock, AIChat, fab, flowgraph and more).
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>
2026-09-23 22:34:43 +02:00
Admin
8329e1fd11 calendar, reminders, calculator, sheets: the phone layouts fit their space -- in the WM's Android skin Reminders drew a blank glass "New Reminder" pill and clipped its "Today" title, Calendar drew month rules the phone layout meant to hide and set today's circle low, Calculator left an empty band under a landscape keypad, and Sheets clipped its toolbar and showed "Your text here" in the name box. Reminders gets solid 80x48 New/Done buttons, baseline-aligned title and count, 56/80/104 pt rows, readable secondary ink and per-tile ink, and a solid priority selector. Calendar honours hidden rules, centres the today mark, snaps week dividers to one physical pixel and gives landscape a 56 pt toolbar with a solid Month/Week/Day selector whose title shrinks near 700 pt. Calculator's landscape header and five 48 pt key rows fill the 332 pt height, results fit from 32 down to 24 pt and pan beyond that. Sheets' portrait toolbar keeps name, Undo and a scrolling More sheet that holds every moved command and Insert function, the name box shows the active cell, tabs scroll with "+" and rename on screen, and a rotation no longer discards an unfinished formula
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:43 +02:00
Admin
fde31df7e3 clock: the phone layout fits both orientations -- in the WM's Android skin the landscape dial ran under the tab bar and the tab bar drew grey square corners from its blur backing. Clock now has a 56/48 pt heading row with an overflow menu holding the "Digital home tile" preference, a two-column landscape (192 pt dial beside time, date and alarm card) and side-by-side Stopwatch/Timer in landscape with widths derived from the screen, and a solid 370x64 tab bar with 16 pt corners whose height the pages reserve. Tab, date and caption ink reach 4.5:1 in light and dark. The alarm editor slides again: margin: Inset{..} failed at runtime with two errors a frame, so the sheet is translated with paired top/bottom margins
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:43 +02:00
Admin
9a12bcf7ea mail: the phone layout lines up -- in the WM's Android skin the portrait inbox had its back button on the screen edge, titles, icons and cards on three different left edges and a bottom bar over the last row; landscape stacked shadowed toolbars and a second Inbox heading. Portrait now has one 64 pt app bar (48 pt back at the 16 pt margin, title at 72), a 48 pt search, 56 pt mailbox rows with a right-aligned count slot and separators, and a solid 56 pt bottom bar the list reserves. Landscape has one solid 56 pt toolbar, a 320 pt list of 84 pt two-line rows with 24 pt trailing clearance, and the reader actions appear only once a message is selected. The desktop three-pane layout keeps its structure
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 22:34:43 +02:00
Admin
deb66b4fef draw text: the CoreText outline fallback is only ever resolved for a face whose outlines live solely in hvgl, and font-family diagnostics are the font trace topic -- a space has no outline in any font, so every face on macOS reached the fallback and would have resolved a CTFont for it (PostScript-name lookup, a cascade-list scan, then the face written to a temp file and loaded through the font manager) on the UI thread to outline nothing; the gate checks for an hvgl table with no glyf, CFF or CFF2, so bitmap faces stay out too; MAKEPAD_FONT_DEBUG read the environment on every update_font_definitions and is now MAKEPAD_TRACE=font
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 22:34:43 +02:00
ychen
09ff817679 feat(text): CoreText outline fallback for hvgl-only fonts (macOS)
Apple ships system fonts whose only outline table is the proprietary hvgl
format (PingFangUI.ttc on macOS 26+ has no glyf/CFF at all). They parse
fine — cmap, metrics and shaping all work — but ttf_parser can outline
nothing, so text silently renders blank.

When ttf_parser yields no outline, Font::glyph_outline now asks CoreText
(whose in-OS decoder reads hvgl) for the glyph path and converts the CGPath
into the same GlyphOutline commands the SDF rasterizer already consumes.
Shaping, metrics and the atlas are untouched; the fallback only fires for
glyphs that render nothing today.

CTFont resolution is layered because CoreGraphics and CTFontManager both
refuse hvgl-only fonts loaded from data or URL, and CTFontCreateWithName
silently substitutes a fallback for dot-prefixed hidden names: try the
PostScript name, then scan the system UI font's cascade lists for CJK
languages (where hidden fonts like .PingFangUITextSC-Default are
reachable), then a temp-file font-manager load. Every candidate is
validated against the ttf_parser view of the face (glyph count + cmap
probes) so a substituted font can never smuggle mismatched glyph IDs into
the atlas. Variation coordinates (e.g. wght for bold) are carried onto the
CTFont via kCTFontVariationAttribute, consistent with the HVAR-adjusted
metrics rustybuzz produces.

Also: env-gated MAKEPAD_FONT_DEBUG=1 logs font family member resolution
(resource path + byte count) while families load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 4c59a370834189b87ee6d151b9971defb860e812)
2026-09-23 22:34:43 +02:00
ychen
6db127c82e fix(text): enable ttf-parser gvar-alloc for many-tuple variable fonts
Apple system fonts carry more than 32 gvar variation tuples per glyph
(SFNS.ttf: 54). Without the gvar-alloc heap spill, VariationTuples::reserve
fails past its stack capacity and outline_glyph returns None for most
glyphs — variable-font text renders completely blank.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit f401d4d567245809f4ede9aa2d4a4b54157367fd)
2026-09-23 22:34:43 +02:00
Admin
6941c0e86b script: the Octoscript hardening keeps a Makepad host's semantics and its speed -- an uncaught error ends an evaluation only when the host sets bail_on_uncaught_error, so by default a module keeps evaluating past one bad statement and scripts can inspect returned error values; an error raised while no script ran is reported at the Rust->script boundary instead of ending, or being caught by, the next run; cross-call try unwinding stops at the nearest root frame, so an error inside a native's callback (array.retain) never pops frames an outer run_core still executes; the equality work ceiling applies to bounded evaluations only and a scalar == allocates nothing; run_core keeps its cached opcode pointer, invalidated by an epoch every bodies.borrow_mut() advances; take_allocation_error and charge_allocation are one flag read when nothing is limited, cast_to_f64 keeps its number path inlinable and checked_index is one round-trip compare
splash_bench geomean against work, best of alternating runs: +12.9% with the series as submitted (the per-instruction Option<String>::take in take_allocation_error alone was +9.8%), -1.1% with this commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 22:34:43 +02:00
ymote
a4c31be0b5 script: keep silenced streaming evals running past uncaught errors
The ws1 equality/fuel port made an uncaught script error Bail the whole
evaluation (vendor semantics). Splash's incremental eval_with_append_source
sets silence_errors because incomplete source inevitably raises errors that
are meaningless until the rest arrives, and its live widget tree relies on
evaluation continuing past them; with the Bail, `field := TextInput{...}`
never produced its child and
splash::style_tests::embedded_splash_restyles_its_isolate_without_replacing_edits
regressed (bisected to vm-port/ws1-equality-fuel alone).

Gate the Bail on !silence_errors: streaming evals keep drain-and-continue,
every other eval (including Octoscript's captured-sink evals) terminates on
the first uncaught error. Regression test added in vm.rs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw
(cherry picked from commit a4347332b38d0d511287006dc19f0c079604f78a)
(cherry picked from commit 6ee2aecfcb7d9296b501be5ea481caef1a4d024a)
2026-09-23 22:34:43 +02:00
ymote
ba33084d9c script: port orphaned Octoscript VM hardening (execution caps, clear_type_methods, parser diagnostics)
Items no workstream branch owned, ported from the Octoscript vendor tree:

- Operand-stack and call-frame caps: ScriptVm::with_stack_value_limit,
  with_call_frame_limit, clear_execution_limit_failures; ScriptThread
  call_frame_limit / *_limit_exceeded flags, push_call_frame,
  has_execution_limit_exceeded; run_core, CALL_EXEC/RETURN/RETURN_IF_ERR
  and handle_return skip pop_to_me and raise an uncatchable
  "script operand stack limit exceeded" / "script call frame limit
  exceeded" bail. The root evaluation frame counts toward the cap.
- ScriptNative::clear_type_methods for restricted embeddings.
- ScriptParser structured diagnostics: ScriptParserDiagnostic,
  MAX_PARSER_DIAGNOSTICS, diagnostics()/diagnostics_truncated(),
  set_emit_errors(); the existing parse_errors sink is kept in lockstep.

Tests: tests/execution_limits.rs, vm.rs
return_does_not_pop_to_me_after_an_operand_stack_limit, parser.rs
diagnostics_* tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw
(cherry picked from commit bfa4087b59abf64d30c385bea53120e0adf2f64b)
(cherry picked from commit 6a349506dd58b4f150dfa12f447385def3f1f469)
2026-09-23 22:34:43 +02:00
ymote
1f481016f4 script: port Octoscript heap/string/array/object hardening onto upstream's allocation budget
Workstream 3 of the Octoscript VM patch port (heap, strings, arrays, objects).

- Array opcode reads and writes validate the index (finite, non-negative,
  integral, representable) before touching storage: ScriptValue::checked_index
  and ScriptVm::checked_array_index, applied at every array/pod index site in
  opcodes_vars.rs and opcodes_assign.rs.
- Updating an existing untracked object field keeps its insertion order
  (ScriptObjectData::map_insert), matching the tracked path.
- Numeric string conversion handles inline and heap strings alike and yields
  a traced NaN for text that is not a number (ScriptHeap::cast_to_f64).
- Heap accounting: Octoscript's retained-heap cap is expressed over upstream's
  ScriptAllocationBudget instead of a second parallel accounting system. A
  persistent budget (heap_cap) is charged by the same charge_allocation calls
  as the scoped with_heap_allocation_limit budget, its headroom re-derived
  from a retained-capacity estimate by reconcile_heap_bytes (setup, GC sweep,
  shrink_to_fit, host boundaries). Public API preserved for octoscript-core:
  set_max_heap_bytes / max_heap_bytes / accounted_heap_bytes /
  reconcile_heap_bytes / take_heap_limit_exceeded. Refusals surface through
  take_allocation_error, so run_core bails uncatchably as before.
- Per-string ceiling: set_max_string_bytes / max_string_bytes /
  take_string_limit_exceeded, enforced on exact lengths at the store choke
  points (new_string_from_str, new_string_concat, intern_or_store_string and
  its preflighted variant, check_intern_string), plus ScriptStringSink,
  ScriptStringBuffer, new_bounded_string_with and temp_bounded_string_with
  (bounded by the string ceiling and the remaining allocation budget) for
  hosts that build strings incrementally. cast_to_string is generic over the
  sink. Byte-array parse_json builds its lossy text through the bounded buffer.
- Pod creation is charged; ValueMap/ScriptArrayStorage/ScriptObjectData expose
  retained_bytes for the estimate.

Retired in favor of upstream: per-path capacity preflights in array_heap.rs /
object_heap.rs (charge_allocation already meters sparse growth), the sink
generalization of to_json/percent/regex builders (upstream preflights exact
lengths), array_mut_with in vec_prims.rs (host conversions are covered by
reconcile_heap_bytes at the host boundary).

Tests: invalid index reads/writes leave storage untouched, numeric conversion
variants, insertion order, capped sparse growth (array, object vec, object
map) refused before mutation, string ceiling at the store paths and in the
bounded buffer, byte-array to_string/parse_json limits, lossy UTF-8 parity,
scoped budget nesting inside the cap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw
(cherry picked from commit ce4bbe5980f38fd0206f06fe27568528bd49a394)
(cherry picked from commit b21de9ff271d523f52b6154df3c7ef84048772d7)
2026-09-23 22:34:43 +02:00
ymote
2f9565e08a script: port Octoscript parser/tokenizer/control-flow VM patches (ws2)
Ports the parser, tokenizer and control-flow part of the Octoscript
makepad-script patch set (PATCHES.md, grammar v0.2) onto the September
`work` revision, by hand, area by area.

Decisions per PATCHES.md item:

1. Logical/comparison precedence, streaming, `!`: ADAPT. Upstream already
   patches a pending ShortCircuitEnd during auto-close but forgot the
   operator: ShortCircuitEnd now retains `what_op` so a tighter logical
   operator keeps a looser left jump open (`a || b && c`), the checkpoint
   restores the original TEST opcodes on continuation, comparisons (14)
   bind tighter than equality (15), and NOT always negates truth
   conversion instead of doing a bitwise NOT on f64-stored numbers.
   Upstream's `last_jump_target` bookkeeping is kept at every patch site.
2. Canonical `try protected catch fallback`: PORT. `catch` is a one-shot
   contextual separator carried in TryErrBlockOrExpr checkpoint state
   (allow_catch/canonical_catch/protected_was_block); block branches keep
   their tail value by removing the inherited pop-to-me marker before
   recomputing the jump; TRY_ERR now uses its encoded relative distance
   and the parser adds the extra TRY_OK skip only when legacy `ok`
   follows. Legacy catch-less `try a b [ok c]` still parses (the checker
   in Octoscript's own crates restricts it to the compatibility entry).
3. Cross-call unwinding: PORT. handle_errors searches all call frames
   (call_stack_has_try), pops younger script calls restoring slot_base
   and the return ip's body, then applies the try-frame cleanup/jump.
   Hard bails stay on ScriptTrapOn::Bail.
4. Loop back-edges: ADAPT onto upstream's reset_iteration_scope fast
   path: truncate_loop_iteration_bases discards iteration-local tries,
   operand values and mes before the scope reset; plain loop/while keep
   their iteration scope and free nested ones. Hard time-budget bails
   drain their diagnostic; OK_END with no try frame bails.
5. Field-assignment reverse-pair walk: PORT (stop at a 1-opcode chunk).
6. Prototype-field `:` rewrite: PORT (chain must begin with an id,
   paired insert through insert_code_with_source keeps opcode/source-map
   lockstep); unavailable rust-value index is a parse error.
7. Numeric-boundary tokenizer: PORT (`_` stays in the pending number
   instead of moving to Whitespace with stale text) plus the char-count
   token length so a multibyte identifier cannot underflow `pos`.

Tests: inline parser/tokenizer/vm regressions and tests/try_catch.rs
(legacy syntax, block/expression branches, nested and cross-function
recovery, contextual `catch`, checkpoint restoration, loop cleanup,
streaming appends, precedence and effects, tokenizer boundaries).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw
(cherry picked from commit 8231a13906cfb339b496ff78b687e43a48e4e11b)
(cherry picked from commit a1f7afb543c8a737f9c56936bfcba605097f88df)
2026-09-23 22:34:43 +02:00
ymote
4a00885f26 script: port Octoscript WS1 — worklist equality with fuel/deadline/work bail, uncaught-error bail, allow_debug_output
Port of the equality / fuel / error-bail slice of Octoscript's makepad-script
patch set onto the September `work` base.

equality (PORT, string compare ADAPTED): `deep_eq` moves from heap.rs into
the new equality.rs as an iterative worklist that visits each container pair
once (cycles and shared DAGs terminate), keeps NaN unequal to itself, and
charges one work unit per processed pair, queued edge and typed-array
element, capped by MAX_EQUALITY_WORK = 65,536 per comparison. Upstream's
69d78873e string early-out is kept instead of the patch's chunked byte
compare: every heap string is interned (string_heap.rs) and short strings
are inline, so string equality is exactly bit equality and a string pair
costs one unit. The raw host `ScriptHeap::deep_eq` is iterative and
unbounded and consumes no VM fuel. The `==`/`!=` opcodes go through
`deep_eq_bounded`: each unit charges one instruction of
`instruction_limit_remaining`, the hard deadline (now an f64 on the
platform clock) is sampled every 256 units so trivial comparisons never
touch the clock, and exhaustion drains diagnostics and raises an
uncatchable Bail, like the instruction limit.

uncaught errors (PORT): `handle_errors` without an active try frame drains
the diagnostic once and sets `ScriptTrapOn::Bail(error)`, so no later
instruction or host effect runs and `eval` returns the error value; active
`try` handlers recover exactly as before. The hard time-budget bail drains
its diagnostic before unwinding, as the instruction-limit bail already did.

allow_debug_output (PORT): new host-controlled `ScriptVmBase` flag,
default true so raw makepad debugging is unchanged. When false the `~` LOG
opcode raises a catchable not-allowed error and `ScriptVm::log` is a no-op.
Incidental VM prints are removed: run_core's `log!` traces, the undefined
opcode `eprintln!` (now a bail) and the loop "unknown state" `println!`
(now a bail). mod_std.rs needs no change: std.log already routes through
the gated `ScriptVm::log`.

Tests: platform/script/tests/equality_fuel_bail.rs covers cycles, shared
DAGs, NaN, string/number semantics, the host deep_eq on typed arrays and
beyond the ceiling, instruction fuel charging, the work ceiling being
uncatchable, the in-comparison hard-deadline check, the hard-budget drain,
uncaught-error bail with try recovery, and the debug-output flag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw
(cherry picked from commit a44f8678ed68b20a0c413d607c07a37d55186fbe)
(cherry picked from commit 353fa369faa5672e075dd874a431dc928420bafb)
2026-09-23 22:34:43 +02:00
ymote
6c3414f087 script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix
Port of Octoscript's re-entrancy and hardening items onto the new VM host contract.

- vm.rs: run_core no longer caches a raw pointer into the active body's opcode
  vector across native calls. Each opcode is copied through a scoped
  `bodies.borrow()` that ends before dispatch, so a native handler that
  re-enters `eval` and replaces the body's parser cannot leave a dangling
  pointer. Regression: reentrant_reload_of_the_active_body_does_not_keep_an_opcode_pointer.
- thread.rs: ScriptThreads::set_current validates the index before updating
  the cached pointer (set_current_thread_id routes through it); update_ptr is
  bounds-checked via get_mut; cur/cur_ref/trap use release-mode assert!.
  Regressions: selecting_an_unknown_current_thread_panics_before_pointer_update,
  empty_threads_reject_current_access_in_release_builds.
- handle.rs: ScriptHandleGc: Any; is/downcast_ref/downcast_mut compare
  Any::type_id, removing the overridable ref_cast_type_id hook.
  Regression: handle_downcasts_use_the_concrete_any_type.
- suggest.rs: value previews truncate at character boundaries.
  Regression: preview_truncation_preserves_utf8_boundaries.
- libs/regex utf8.rs: iterator entry uses `self.range_stack.pop()?` instead of
  a never-advancing `while let`, replacing upstream's #[allow(clippy::never_loop)].

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgSir4cQyaakzPju4h6smw
(cherry picked from commit ee7729e71c1408f61ec8f9083a79bdf9117d31a7)
(cherry picked from commit cdbc33e8ac1683fcdeb6e9dcb076e26ce5d23360)
2026-09-23 22:34:43 +02:00
Kevin Boos
1f60033ed3
RadioButton: take key focus on the click, not on the press (#1259)
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`.
2026-09-23 07:38:46 +02:00
Kevin Boos
d14a991085
HtmlLink: fix its hover and pressed states (#1257)
* 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.
2026-09-22 21:28:44 +02:00
Kevin Boos
2b1de986e9
RadioButton: fix its touch hover state and click-off behavior (#1258)
* 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.
2026-09-22 21:28:29 +02:00
Admin
76627b6f19 ci: a driven app takes every key once, and the storage module builds for the browser -- the mini's wm script went red one run in three at the browser launch, and the shell menu's new log lines said why: the filter read "bbrowser" and the menu had opened twice. The remote bridge applied every wait=1 input first and, when the frame after it could not be sealed (the window was busy presenting the warm browsers), answered "requested input frame could not be submitted; retry"; the CI driver took that at its word and sent the input again, so a busy app took the Cmd+Space and the first letter twice. The bridge now keeps the waiters of an applied input and asks for the frame on the next beat, as it already did for a drawable still being acquired; the driver never asks an input route twice, whatever the answer, and still retries a grab it could not place. The workspace row was orange and apps/scope red on wasm32 since 8d7246231: the public volume_available_bytes had been put between the not(wasm32) guard and the native module it guarded, so the module compiled in the browser with nothing using it (17 warnings) and the function it exported was missing there; the guard is back on the module, and the browser has a volume_available_bytes that says the free space is not known 2026-09-22 19:04:04 +02:00
Admin
3cc56cf9b5 git: the memory ledger can take what it is asked for -- MemoryAccount::reserve takes the bytes whether or not the account has room, keeps the excess in overcommitted and leaves available at zero while it stands, so callers that can wait hold back; try_reserve stays for them. The ledger paces work, it is not a wall: a preparation in Scope that could not do without a lease stopped with a capacity error where the machine could have swapped (the user: "analysis ram cant just fail either")
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 17:40:41 +02:00
Admin
2e926a70f4 platform: the storage module says how much a volume has free -- volume_available_bytes(path) was the private figure behind the storage estimate; an application that sizes a store by its disk has to ask the disk, not carry a figure of its own (Scope prepared into a fixed 32 GB per namespace and stopped at "disk capacity" on a volume with room to spare), so it is public now
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 17:11:05 +02:00
Admin
0663a2be6c wm: the shell menu says what it did, and the CI waits for that -- the mini's wm run went red once with 'log did not contain "browser client * first frame" within 240s' and nothing in the log between the Return and the timeout: no launch, no adoption, no menu line at all, so there was no telling whether the launcher never opened, lost the typed name or picked nothing. The menu now logs each open with its number and path, every activation with its target, its close, and a Return that activated nothing with the filter and row count it had; the terminal and browser steps wait for "shell menu activate apps.<name>" right after Return, so a launcher that went wrong fails there by name instead of at the frame wait
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 14:16:57 +02:00
Admin
da8a29869b widgets: the pooled test context's resets replace the globals -- Cx::set_global keeps a global that is already there, so the reset landed in 83228f9e5 never touched the Escape claim or the drawn root once a case had installed them, and the mini still failed escape_belongs_to_an_overlay_locked_above on the next run; the resets now assign through global(). A test checks the contract on the pool itself: a claim in one checkout does not refuse the next checkout's Escape
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 14:12:48 +02:00
Admin
83228f9e54 widgets: a pooled test context forgets the last case's Escape claim -- the pooled contexts the widget tests run on since 90c9a7b01 are handed from case to case without an event loop, so their event id never moves; the first case on a thread that claimed an Escape left its claim behind, and every later case on that thread whose menu needed the key was refused it (radial_menu's the_keyboard_walks_the_rings and escape_belongs_to_an_overlay_locked_above failed about one run in four, depending on which cases shared a thread). A checkout now resets the claim, the orphaned-lock list and the drawn root the last case left; twelve release runs in a row pass
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 13:48:16 +02:00
ymote
a08ef4ebd6 widgets: let the host install script mods into every Splash isolate
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)
2026-09-22 13:41:13 +02:00
Admin
90c9a7b01a tests: the three binaries over ten seconds in release come under it -- the user's law is the whole suite in about two minutes, in release, and the CI now runs every test binary of its selection several at a time with a cap on each, so one long binary is the run's floor. Grok measured every binary the debug-era table named (release harness time): fourteen were already under 10 s and are untouched; three were not. Widgets --lib (1822 tests) 17.4 s -> 8.0 s: no single slow test; every test built a fresh Cx and registered the widget library on it (45 ms each, in release), and libtest runs each test on its own thread, so nothing could be kept. Test modules that build widgets now run their cases through on_test_cx on four threads that stay up and keep one registered Cx each (checkout_test_cx); finger, key, pass, draw-list and window state is cleared on every checkout so a case that locks a sweep or draws a menu leaves nothing for the next. The style-reload tests keep their own contexts: a reload would change the next case. The isolate cost table measures batches of 1 and 2 (the four sizes measured 30.06, 29.92, 29.63 and 29.49 ms per isolate: flat), and the two sheet-contrast tests read one walk of every sheet from a OnceLock instead of building the library twelve times each. Director mcp_battery 10.2 s -> 0.4 s: the slowloris test waited out the production head deadline of 10 s; McpServer::start_with_head_deadline takes the budget, production start still passes HEAD_DEADLINE_MS (asserted), and the test proves the socket is held until an 80 ms budget and closed after it. Piano acoustic_reference 11.6 s -> 8.6 s: the promotion renders are 2.5 s instead of 4.0 s, which covers every sample measure and onset read (2.0 s after an onset found in the first 0.5 s). No test is ignored, deleted or loosened. The review dropped the lane's short-circuit in script_mod (a library change for a test's sake: the one test helper that re-registered on a pooled context no longer does) and its per-module pool thread-locals that the checkout never read.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 12:29:54 +02:00
Admin
f7ac37f690 ci: a card is as high as its content -- with the time beside the count there is no bottom line to keep room for, so the tile height is capped at the name and two lines under it with the padding around them, and the name takes the size the width allows; the layout test asserts what matters (the whole name fits) instead of an exact column count
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 10:26:13 +02:00
Admin
14bcadec89 cef: a hosted browser survives its host's exit -- the window manager quits, its pipes close, and the browser it hosted keeps painting for a moment; the CEF crate's next diagnostic line went through eprintln!, which panics when stderr's reader is gone ("failed printing to stderr"), and that panic aborted the browser with a crash report on every quit of the desk (the CI box showed the dialog after each window manager run). Its diagnostics now go through a writer that drops the line when nobody reads it, as the platform's log sink already did
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 10:26:13 +02:00
Kevin Boos
88639f79a2
Tooltip: position anchored tooltips in the same draw (#1256)
* 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.
2026-09-22 09:51:33 +02:00
Admin
611a4e2035 ci: the tests are the platform's own, in release, in two minutes -- the user's law: "we mostly only care about the platform tests", "a max of about 2 minutes of tests", "test everything the CI does in release builds; debug builds are uselessly slow for this". ci.test now builds every test binary of its selection ONCE in release and runs them several at a time (half the cores, at least two), each one timed and capped, so a hung test costs its own cap and never the hour, and the step's detail names the slowest binaries with their counts; the doc tests follow through cargo for the packages that have a library. A selection is dirs (the packages whose manifests live under those directories, by cargo metadata), packages, package or workspace: true; a run over budget_secs (120 by default) turns the block orange and says so. The root script runs platform, draw, widgets and tools/ci by default: 49 binaries, 2535 tests, 22 s on a laptop. Every other crate's tests are the deep run, opt-in with deep_tests = true in ci.toml or ci --deep (ci.deep in scripts). The wall's cards lose a line: the time sits beside the count, 1 passed · 12s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 09:50:38 +02:00
Admin
68aed71902 gif: the crate's doc examples compile -- they were the upstream crate's, naming it gif, and failed as doc tests in every workspace test run; they alias the crate under that name and are compile-only, since they open sample files the repository does not carry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 09:36:13 +02:00
Admin
ab64a64e7e tools: makepad-screen is makepad-agents, and its binary is agents -- the crate is the agent session manager (start, attach, list, the TUI); "screen" was a nod to GNU screen, and its other job, the pty trampoline a terminal starts its shell through, does not need the old name either. The package is makepad-agents in tools/agents, the executable is agents, so it is the command on the PATH itself and the shell wrapper that existed only to give it that name is gone (the binary already resolves the workspace's session directory the wrapper used to export); the terminal's pty spawn looks for agents beside the app, Director looks for and pins the sibling agents with the makepad-agents-v1 record and the agents version line, the session environment is MAKEPAD_AGENTS_SESSION and MAKEPAD_AGENTS_STATE_DIR, the CI scripts build makepad-agents, and Director's notes say how to build and run it
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 09:36:13 +02:00
Admin
06a708dc3f ci: the header names the tip being tested -- while a run was on, the branch line still showed the tip of the last finished run beside "testing now", since the live state only learned the tip when the run ended; the record a run keeps as it goes now carries the tip under test from its first update
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 08:57:50 +02:00
Kevin Boos
71f3b84a87
Linux: fix window chrome button hovers and how maximized/fullscreen windows work (#1255)
* 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.
2026-09-22 08:47:38 +02:00
Admin
0770a8c8b8 tests: the last three failures of the CI box's night -- the dock clips a reported tab header on the tab bar's scroll axis only, since a tab's own height is its own and clipping it to the bar's box moved the tab's reported centre by a logical pixel and a half (the text centring test measured the "H" tab against that rect and failed everywhere); the Files cancel test walks a made-up home under the temp dir instead of the real one, whose Desktop, Documents and Downloads block on a macOS permission dialog that nobody at a CI box answers (the whole suite hung on it for an hour, and that dialog was the "release wants access to Downloads" the person at the box saw); and the tour generation's "feels instant" budget stays two seconds for the release build it describes while a debug build, where the same work takes 1.7 s on a fast laptop, gets a proportionate eight and the best of three runs, so a busy CI box does not fail it by a few percent
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 08:46:45 +02:00
Admin
c20cf99c89 ci: a judgement is never lost to the way it was phrased, and every "retry" of the bridge is retried -- the small vision model, asked to describe the desk's terminal, locked onto one phrase and repeated it to the token cap, so its answer had no verdict line and the window manager tile went red over a terminal that was fine; the decoder now stops a looping answer (the tail of the text is one short pattern over and over), and when the free answer has no verdict line the conversation is continued with "VERDICT:" already written and the model finishes that line, so the judgement is asked for outright instead of being lost. The harness retries every answer of the bridge that ends in "; retry" (a grab it could not arm as well as an input frame it could not place), where it matched one wording only and failed the browser step on the other. The workspace test suite gets three hours instead of one: it runs beside the app scripts now, and a hung test was worth a whole hour before
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 08:46:45 +02:00
Admin
863ca812a3 ci: the workspace script gives the wall back after warming -- after a push that touches a core crate the workspace script spent ten minutes and more alone (fifteen target checks, a release build of every app, the workspace check and the whole test suite) while every other tile sat grey, which read as a hang. It still goes first and alone for the part only it may do alone, warming the cache every other script reads, and then calls ci.shared(): its permit turns from exclusive into an ordinary one and the waiting scripts start beside its checks and tests. The runner sends it through the same pool as the rest and hands out nothing else until it holds the wall, instead of running it to the end before the pool even started. It also builds the pty helper while it is alone, so the terminal, director and window manager scripts get a cache answer instead of a cargo run that would queue behind the test build
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 00:07:23 +02:00
Admin
86cd0604ef libs: no warnings in the workspace check on any row -- the CI box checks every package on every target, and the library crates, which have no tile of their own, warned on the rows nobody builds by hand. On wasm and mobile, things are gated with their users: the workspace document worker (its commands, payloads, retry constants and Drop are native; the wasm API stays, behind an uninhabited worker whose constructors answer that the browser has no document worker), the hub's unused non-Unix available_bytes and its ram facts parser, the loader's ArenaPtr, system_speech's bcp47. On the tvOS rows, the only ones built on nightly, the legacy numeric constants and functions become the associated constants they have been since Rust 1.43 (micro_serde, bytemuck, rustybuzz, unicode-script, smallvec, weezl); fetch_update keeps its name under allow(deprecated) in stitch, git and the map archive, since try_update does not exist on older stable toolchains; objc-sys declares free with the signature the standard library uses; rustybuzz calls its own method through a path that a future std method of the same name cannot shadow; and the Script derive's unused helper attribute splat, which nightly now reserves as a built-in, is spelled script_splat (nothing in makepad, Scope or Stage uses it)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 00:02:28 +02:00