- The child's Tick drains the network queue for HTTP and script sockets (dispatch_network_runtime_events), and that queue also carries the host socket its hosted loop reads. A host batch landing during a Tick went through dispatch_studio_msg, which drops the loop's own messages: WindowGeomChange, Swapchain, Tick. The drain now parks host-socket responses in Cx::studio_backlog once a loop owns the socket (its first read), and the loop's next read takes them first, in order. Before: 1 in 2-3 split runs lost the geometry; after: 6 of 6 runs delivered every geometry sent. Applies to the macOS and Linux X11 hosted loops too; Android hosted never drained the network in its Tick
- wm's run view cleared its bootstrap (the geometry resend) on any present, so a frame already in flight when the tile changed size cancelled the geometry before it was sent; a present settles it only after the first bootstrap beat has sent the messages
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Measured on .100 with apps/aichat and a draw-call log: the popup's rows use two
shaders created on its first draw (menu item background and text). D3D11
compiles them on the pool and skips their draw calls until they are installed;
the background's shader already existed, so the panel drew empty. The finished
compiles were only picked up inside a redraw (hlsl_compile_shaders ran under
need_redrawing), and a window at rest has none: the finished task raised the
internal signal, the loop woke and went back to sleep, and the rows stayed
missing until the next input. A startup shader that finished after the last
startup redraw waited 17 s for the click that opened the menu. Warm shader cache:
the same empty panel. Draw order is fine: the rows follow the background in the
popup's overlay list, above it in depth. Metal compiles inline and was correct.
- hlsl_adopt_shaders installs finished compiles; the Signal handler runs it
when the internal signal is raised (one wake per finished task, no polling),
and every paint tick runs it before it draws (and the stdin host's tick).
- Once the startup set is installed, new shaders compile on the UI thread
before the frame renders, as Metal does, with a 1 s budget per frame and the
rest on the pool. Startup keeps compiling on the pool. Measured first open
with a cold cache: 26 ms + 19 ms, the menu complete in its first frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
wm.exe died at startup with "thread 'main' has overflowed its stack"
(0xc00000fd) on Windows, where the main thread gets 1 MB against 8 MB on
macOS and Linux. The startup chain is shallow (~85 frames) but five of its
frames were huge: ShellIcons (41 DrawSvg, ~110 KB) sat by value in every
ShellDraw, PhoneSurface carries one ShellDraw and WmDesk carries a
PhoneSurface plus its own ShellDraw (286 KB). Each constructor layer
(WmDesk factory, WmDesk::script_new, PhoneSurface::script_new and
script_new_with_default, ShellDraw) held its whole value in its frame:
about 970 KB in five frames. Reproduced on macOS by linking wm with a
1 MB main stack (same overflow).
script gets a transparent Box<T> (ScriptNew/ScriptApply/ScriptHook forward
to T: same type id, proto, default and apply), and ShellDraw boxes its
icons. The same chain now takes ~305 KB (WmDesk 286 -> 69 KB, PhoneSurface
140 -> 30 KB, ShellDraw 117 -> 7 KB); the 1 MB-stack build starts and runs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* 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>
audio_route: the Core Audio process tap gains Windows (WASAPI process loopback; the source is ducked through its ISimpleAudioVolume) and Linux (the PulseAudio/PipeWire monitor of the source's sink; ducked through its stream volume) backends behind the same Route API. A route only monitors by default (copy for meters and visualisers; the app's own output is untouched). Route::set_processing turns processing on: the source is ducked to 2^-13 and boosted back by exactly 2^13 in float, so the processed signal is bit-exact. RouteConfig.state_dir keeps the duck state, so restore_after_crash puts a ducked volume back after a crash.
script/wgsl: an if/else whose branches end in a value nothing uses wrote that value as a bare statement (_phi_353;). Metal, GLSL and HLSL accept that, WGSL does not, so DrawMenuRow failed on Vulkan and menu rows lost their drawing (the theme chips on Linux). ShaderBackend::write_discarded_expr writes such values as `_ = expr;` for WGSL and keeps void calls as statements, in both places that emit leftovers. MAKEPAD_TRACE=shader.wgsl lifts the two-error log suppression.
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>
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>
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)
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)
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)
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)
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)
* 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.