diff --git a/platform/script/derive/src/derive_scriptable.rs b/platform/script/derive/src/derive_scriptable.rs index e92adae4f..40e4fcd16 100644 --- a/platform/script/derive/src/derive_scriptable.rs +++ b/platform/script/derive/src/derive_scriptable.rs @@ -140,21 +140,6 @@ fn derive_script_impl_inner( .iter() .any(|a| a.name == "live" || a.name == "apply_default") { - // A field whose canonical mutation path is an imperative - // setter (`#[visible]` → `set_visible`, `#[imperative]` for - // the rest) shares its storage with the DSL value, so a - // re-walk that carries no authored change must leave it alone. - // Otherwise `script_mod` re-runs that exist only to re-bake - // heap primitives — every safe-area inset change, i.e. every - // Android system-bar hide and every rotation — silently put - // the DSL default back over the runtime state. - let imperative = field - .attrs - .iter() - .any(|a| a.name == "imperative" || a.name == "visible"); - if imperative { - tb.add("if !apply.preserves_runtime_state() {"); - } tb.add("{ let mut __field_value = vm.bx.heap.value_for_apply(value, id!(") .ident(&field.name) .add(").into(), apply);"); @@ -174,9 +159,6 @@ fn derive_script_impl_inner( .add(",vm, apply, scope, v);"); tb.add("}"); tb.add("}"); - if imperative { - tb.add("}"); - } } if field .attrs diff --git a/platform/script/derive/src/lib.rs b/platform/script/derive/src/lib.rs index 67c090605..808e0ef6f 100644 --- a/platform/script/derive/src/lib.rs +++ b/platform/script/derive/src/lib.rs @@ -36,7 +36,6 @@ pub fn script_err_gen(input: TokenStream) -> TokenStream { source, new, live, - imperative, rust, pick, splat, diff --git a/platform/script/src/apply.rs b/platform/script/src/apply.rs index 8375d8d07..a39e18425 100644 --- a/platform/script/src/apply.rs +++ b/platform/script/src/apply.rs @@ -132,21 +132,6 @@ pub enum Apply { /// is the new source of truth and template values should override /// any prior runtime state. Reload, - /// `script_mod` was re-run and the fresh template re-walked, but the DSL - /// source did NOT change — the re-run exists only to re-bake heap - /// primitives that are evaluated at `script_mod` time and are therefore - /// unreachable to `ScriptReapply` (canonical case: - /// `mod.widgets.SAFE_INSET_PAD_*` after a safe-area inset change, which - /// `cx.request_live_edit()` triggers on every Android system-bar hide and - /// every rotation). - /// - /// Structurally this is a reload — children lists are rebuilt and the - /// `#[source]` object is re-bound, because the template really is a new - /// object. Semantically it is a `ScriptReapply` — nothing the developer - /// wrote changed, so imperative runtime state must survive. Routing it - /// through `Reload` is what used to wipe every `set_text`, every - /// `set_visible`, and every animator state on each inset change. - Rebake, /// Heap-mutation broadcast triggered by `cx.request_script_reapply()` /// (e.g. preference change, safe-area inset change). The template has /// NOT changed — the same cached `app_value` is being re-walked so @@ -165,7 +150,6 @@ impl Apply { match self { Self::New => true, Self::Reload => true, - Self::Rebake => true, Self::ScriptReapply => true, Self::Eval => true, _ => false, @@ -177,13 +161,11 @@ impl Apply { /// creates temporary objects that would become dangling after GC. /// Excludes ScriptReapply because the template hasn't changed — /// the same source object is being re-walked, so re-binding it is - /// unnecessary work. Includes `Rebake`: `script_mod` re-ran, so the - /// source object really is new even though the DSL text is unchanged. + /// unnecessary work. pub fn is_template_apply(&self) -> bool { match self { Self::New => true, Self::Reload => true, - Self::Rebake => true, _ => false, } } @@ -210,15 +192,13 @@ impl Apply { pub fn is_reload(&self) -> bool { match self { Self::Reload => true, - Self::Rebake => true, Self::ScriptReapply => true, _ => false, } } /// True only for `Apply::Reload` — a LiveEdit-driven hot-reload where - /// the DSL itself changed. Excludes `Apply::ScriptReapply` and - /// `Apply::Rebake` (a `script_mod` re-run with unchanged source). Use this + /// the DSL itself changed. Excludes `Apply::ScriptReapply`. Use this /// (rather than `is_reload`) when behavior should fire only when the /// template source has actually changed (e.g. re-running script_mod /// scaffolding, invalidating template-derived caches). @@ -230,11 +210,10 @@ impl Apply { } /// True only for `Apply::ScriptReapply` — a `request_script_reapply`-driven - /// re-walk where the template has NOT changed. - /// - /// Prefer `preserves_runtime_state()` when the question is "may I clobber - /// the runtime value here?"; this predicate exists for the narrower cases - /// that care about the specific trigger. + /// re-walk where the template has NOT changed. Field impls whose value + /// should not be clobbered by template defaults on this kind of re-walk + /// should early-return when this is true (canonical example: + /// `ArcStringMut::script_apply`). pub fn is_script_reapply(&self) -> bool { match self { Self::ScriptReapply => true, @@ -242,40 +221,6 @@ impl Apply { } } - /// True for the walks that follow a `script_mod` re-run (`Reload`, - /// `Rebake`). The re-run builds a fresh object heap, so any script object - /// reference a widget cached during an earlier walk is now dangling — - /// `Animator`'s cached state objects are the canonical example. Code - /// holding such references must re-resolve them from the incoming value - /// rather than reuse what it stored. - /// - /// `ScriptReapply` is excluded: it re-walks the same cached `app_value`, - /// so cached references stay alive. - pub fn follows_script_rerun(&self) -> bool { - match self { - Self::Reload => true, - Self::Rebake => true, - _ => false, - } - } - - /// True for the re-apply walks where nothing the developer wrote changed, - /// so template values must NOT overwrite state that was set through an - /// imperative setter (`Label::set_text`, `set_visible`, animator state). - /// Field impls whose canonical mutation path is such a setter should - /// early-return when this is true (canonical example: - /// `ArcStringMut::script_apply`). - /// - /// Excludes `Apply::Reload`, where the DSL genuinely changed and the new - /// template is meant to win. - pub fn preserves_runtime_state(&self) -> bool { - match self { - Self::ScriptReapply => true, - Self::Rebake => true, - _ => false, - } - } - pub fn is_animate(&self) -> bool { match self { Self::Animate => true, diff --git a/platform/script/src/opcodes_control.rs b/platform/script/src/opcodes_control.rs index 97fac4f03..41bd0925a 100644 --- a/platform/script/src/opcodes_control.rs +++ b/platform/script/src/opcodes_control.rs @@ -44,12 +44,6 @@ impl<'a> ScriptVm<'a> { return; }; self.bx.threads.cur().slot_base = call.prev_slot_base; - // A root frame's last scope holds everything the body defined after - // a shadowing let; keep it for host->script lookups. - if call.return_ip.is_none() { - let end = self.bx.threads.cur_ref().scopes.last().copied(); - self.bx.threads.cur().root_end_scope = end.map(|s| self.bx.heap.new_object_ref(s)); - } self.bx .threads .cur() diff --git a/platform/script/src/prims.rs b/platform/script/src/prims.rs index 649f1dc6c..2bdb90108 100644 --- a/platform/script/src/prims.rs +++ b/platform/script/src/prims.rs @@ -340,14 +340,13 @@ script_primitive!( ) { // Same rationale as `ArcStringMut::script_apply`: text-bearing fields // (`TextInput.text`, `TextInput.empty_text`, etc.) are canonically - // mutated through imperative setters at runtime. A re-walk that - // carries no authored change (`cx.request_script_reapply()` preference - // broadcast, or the `script_mod` re-run a safe-area inset change - // forces) would otherwise clobber that runtime value with the stale - // DSL literal. Strings are not part of the shared-heap-object - // propagation pipeline (which uses `Size`/numerics), so bailing here - // is safe. - if apply.preserves_runtime_state() { + // mutated through imperative setters at runtime. A ScriptReapply + // walk fired by `cx.request_script_reapply()` (preference broadcast, + // safe-area inset change) would otherwise clobber that runtime value + // with the stale DSL literal. Strings are not part of the shared- + // heap-object propagation pipeline (which uses `Size`/numerics), so + // bailing here is safe. + if apply.is_script_reapply() { return; } self.clear(); diff --git a/platform/script/src/thread.rs b/platform/script/src/thread.rs index 466049725..afe9479b8 100644 --- a/platform/script/src/thread.rs +++ b/platform/script/src/thread.rs @@ -111,9 +111,6 @@ pub struct ScriptThread { /// Base of the CURRENT slot frame (see CallFrame::prev_slot_base). pub(crate) slot_base: usize, pub(crate) instruction_limit_remaining: Option, - /// The innermost scope when the root frame returned, kept alive so - /// the body can hand it to host->script calls. - pub(crate) root_end_scope: Option, pub trap: ScriptTrapInner, //pub(crate) last_err: ScriptValue, pub(crate) json_parser: JsonParserThread, @@ -125,7 +122,6 @@ impl ScriptThread { Self { thread_id, is_paused: false, - root_end_scope: None, //last_err: NIL, // pre-reserve the hot stacks so steady-state execution never // pays Vec growth in the interpreter loop diff --git a/platform/script/src/traits.rs b/platform/script/src/traits.rs index 6d4f1621b..cce1f9b54 100644 --- a/platform/script/src/traits.rs +++ b/platform/script/src/traits.rs @@ -39,9 +39,7 @@ pub trait ScriptHook { // Widgets that need to differentiate can branch on // `apply.is_live_edit_reload()` or `apply.is_script_reapply()` // inside the hook. - Apply::Reload | Apply::Rebake | Apply::ScriptReapply => { - self.on_before_reload_scoped(vm, scope) - } + Apply::Reload | Apply::ScriptReapply => self.on_before_reload_scoped(vm, scope), _ => (), } } @@ -64,9 +62,7 @@ pub trait ScriptHook { ) { match apply { Apply::New => self.on_after_new_scoped(vm, scope), - Apply::Reload | Apply::Rebake | Apply::ScriptReapply => { - self.on_after_reload_scoped(vm, scope) - } + Apply::Reload | Apply::ScriptReapply => self.on_after_reload_scoped(vm, scope), _ => (), } self.on_alive() diff --git a/platform/script/src/vm.rs b/platform/script/src/vm.rs index 78efcf838..5628070d9 100644 --- a/platform/script/src/vm.rs +++ b/platform/script/src/vm.rs @@ -64,9 +64,6 @@ pub struct ScriptBody { pub parser: ScriptParser, pub scope: ScriptObjectRef, pub me: ScriptObjectRef, - /// The scope the body ended in, once it has run: `let`/`fn` that - /// shadow a name open child scopes below `scope`. - pub end_scope: Option, pub checkpoint: Option, pub source_len: usize, } @@ -975,11 +972,7 @@ impl<'a> ScriptVm<'a> { self.bx.threads.cur().trap.ip.index = 0; // the main interpreter loop - let value = self.run_core(); - if let Some(end) = self.bx.threads.cur().root_end_scope.take() { - self.bx.code.bodies.borrow_mut()[body_id as usize].end_scope = Some(end); - } - value + self.run_core() } /// Checks if the value has an apply transform and calls it, returning the transformed value. @@ -1312,7 +1305,6 @@ impl<'a> ScriptVm<'a> { parser: ScriptParser::default(), scope, me, - end_scope: None, checkpoint: None, source_len: 0, }; diff --git a/platform/script/tests/hook_scope.rs b/platform/script/tests/hook_scope.rs deleted file mode 100644 index 76d5cc578..000000000 --- a/platform/script/tests/hook_scope.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Host->script hook lookups against a Splash-shaped body: the script is -//! wrapped in an auto-closed object literal, and a `let`/`fn` that shadows -//! an existing name opens a child scope the module scope cannot see into. -//! The body remembers the scope it ended in for exactly that. - -use makepad_script::*; - -fn test_vm() -> ScriptVm<'static> { - let host = Box::leak(Box::new(0i32)); - let std = Box::leak(Box::new(0i32)); - ScriptVm { host, std, bx: Box::new(ScriptVmBase::new()) } -} - -fn scopes(vm: &mut ScriptVm, file: &str) -> (ScriptObject, Option) { - let bodies = vm.bx.code.bodies.borrow(); - bodies.iter().find_map(|body| match &body.source { - ScriptSource::Mod(m) if m.file == file => { - Some((body.scope.as_object(), body.end_scope.as_ref().map(|s| s.as_object()))) - } - _ => None, - }).expect("body") -} - -#[test] -fn hooks_resolve_in_the_scope_the_body_ended_in() { - let mut vm = test_vm(); - vm.bx.captured_errors = Some(Vec::new()); - // `fs` is defined twice: the second `let` shadows and opens a child scope. - let code = "let fs = 1\n{height: 1, let fs = 2\nfn before(){ 7 }\nfn on_x(){ 42 }\n{}\n"; - vm.with_instruction_limit(500_000, |vm| { - vm.eval(ScriptMod { - cargo_manifest_path: String::new(), - module_path: String::new(), - file: "hook_scope".to_string(), - line: 0, column: 0, - code: code.to_string(), - values: vec![], - }) - }); - let errors = vm.take_errors(); - assert!(errors.is_empty(), "eval errored: {errors:?}"); - - let (module, end) = scopes(&mut vm, "hook_scope"); - let end = end.expect("the body recorded the scope it ended in"); - let from_module = vm.bx.heap.scope_value(module, id!(on_x), NoTrap); - assert!(from_module.is_nil() || from_module.is_err(), "the module scope should not see past the shadowing let"); - let on_x = vm.bx.heap.scope_value(end, id!(on_x), NoTrap); - assert!(!on_x.is_nil() && !on_x.is_err(), "on_x not found in the end scope"); - let result = vm.with_instruction_limit(500_000, |vm| vm.call(on_x, &[])); - assert_eq!(format!("{result:?}"), "42", "on_x returned {result:?}"); - let before = vm.bx.heap.scope_value(end, id!(before), NoTrap); - assert!(!before.is_nil() && !before.is_err(), "before not found in the end scope"); -} diff --git a/platform/src/app_main.rs b/platform/src/app_main.rs index 8743718bc..064ed5d28 100644 --- a/platform/src/app_main.rs +++ b/platform/src/app_main.rs @@ -287,9 +287,6 @@ macro_rules! _app_main_event_closure { if let Event::LiveEdit = event { let mut app_ref = app.borrow_mut(); if let Some(app) = app_ref.as_mut() { - // `Reload` when the DSL changed on disk, `Rebake` when - // `script_mod` only re-ran to pick up new heap primitives. - let live_edit_apply = cx.live_edit_apply(); cx.with_vm(|vm| { let value = vm.with_reload(|vm| <$app as AppMain>::script_mod(vm)); if let Some(obj) = value.as_object() { @@ -298,7 +295,7 @@ macro_rules! _app_main_event_closure { <$app as $crate::ScriptApply>::script_apply( app, vm, - &live_edit_apply, + &$crate::Apply::Reload, &mut $crate::Scope::empty(), value, ); diff --git a/platform/src/arc_string_mut.rs b/platform/src/arc_string_mut.rs index 1b8beafce..52825eb55 100644 --- a/platform/src/arc_string_mut.rs +++ b/platform/src/arc_string_mut.rs @@ -92,11 +92,11 @@ impl ScriptApply for ArcStringMut { ) { // Same rationale as `String::script_apply` (see `prims.rs`): // text-bearing fields are canonically mutated by imperative setters - // (`Label::set_text`, `Button::set_text`, etc.), so a re-walk that - // carries no authored change should not clobber the runtime value with - // the stale DSL literal. Only `Apply::Reload` gets to win, because - // there the DSL itself changed. - if apply.preserves_runtime_state() { + // (`Label::set_text`, `Button::set_text`, etc.), so a ScriptReapply + // walk should not clobber the runtime value with the stale DSL + // literal. `Apply::Reload` (LiveEdit) still applies — DSL just + // changed, so the new template wins. + if apply.is_script_reapply() { return; } // Convert to owned String using the heap's cast method diff --git a/platform/src/cx.rs b/platform/src/cx.rs index a98f9067e..16c25404c 100644 --- a/platform/src/cx.rs +++ b/platform/src/cx.rs @@ -145,25 +145,16 @@ pub struct Cx { pub pending_script_reapply: bool, /// When true, the next event-loop iteration will fire `Event::LiveEdit`, - /// which re-runs `script_mod` and re-applies with `Apply::Rebake`. Use + /// which re-runs `script_mod` and re-applies with `Apply::Reload`. Use /// this when a primitive heap value (e.g. `mod.widgets.SAFE_INSET_PAD_TOP`) /// has changed and needs to be re-baked into widget definitions that /// reference it via expressions like `top: (mod.widgets.SAFE_INSET_PAD_TOP)` /// — those expressions are only re-evaluated when `script_mod` re-runs. - /// The re-run is still a full-tree walk, so prefer - /// `pending_script_reapply` whenever the change can be modeled as a - /// shared-heap-object mutation instead. + /// `Apply::Reload` clobbers runtime widget state (animator values, etc.), + /// so prefer `pending_script_reapply` whenever the change can be modeled + /// as a shared-heap-object mutation instead. pub pending_live_edit_request: bool, - /// Which `Apply` variant the pending `Event::LiveEdit` should re-apply - /// the freshly re-run `script_mod` value with. A file-change hot reload - /// means the DSL actually changed, so the new template wins - /// (`Apply::Reload`). A `request_live_edit()` re-bake did not change the - /// DSL, so imperative runtime state must survive (`Apply::Rebake`) — - /// otherwise every safe-area inset change wipes each `set_text`, - /// `set_visible` and animator state in the tree. - pub(crate) live_edit_apply: Apply, - /// `WindowGeomChange` events queued up during an event dispatch. pub(crate) pending_window_geom_changes: Vec, pub(crate) clear_hover_queued: bool, @@ -534,7 +525,6 @@ impl Cx { display_context: Default::default(), pending_script_reapply: false, pending_live_edit_request: false, - live_edit_apply: Apply::Reload, pending_window_geom_changes: Default::default(), clear_hover_queued: false, diff --git a/platform/src/cx_api.rs b/platform/src/cx_api.rs index 55480621b..2e8c6936b 100644 --- a/platform/src/cx_api.rs +++ b/platform/src/cx_api.rs @@ -576,26 +576,19 @@ impl Cx { /// Requests a deferred `Event::LiveEdit` on the next event-loop iteration. /// The handler re-runs `script_mod` (re-evaluating any expressions that /// reference primitive heap values like `mod.widgets.SAFE_INSET_PAD_TOP`) - /// and then re-applies the widget tree with `Apply::Rebake`, which - /// preserves imperative runtime state since the DSL itself is unchanged. + /// and then re-applies the widget tree with `Apply::Reload`. /// /// Use this only when a primitive heap value has changed and that value /// is consumed by `script_mod!` block expressions — those expressions are - /// not re-evaluated by `Apply::ScriptReapply`. The re-run is still a full - /// tree walk, so prefer `request_script_reapply` when the change can be + /// not re-evaluated by `Apply::ScriptReapply`. `Apply::Reload` walks + /// clobber runtime widget state (animator values, dynamic instance + /// buffers, user-typed text in widgets that don't early-return on + /// LiveEdit), so prefer `request_script_reapply` when the change can be /// modeled as a shared-heap-object mutation instead. pub fn request_live_edit(&mut self) { self.pending_live_edit_request = true; } - /// The `Apply` variant the currently dispatching `Event::LiveEdit` should - /// be re-applied with — `Reload` for a file-change hot reload, `Rebake` - /// for a `request_live_edit()` re-bake. `app_main!` reads this; app code - /// has no reason to. - pub fn live_edit_apply(&self) -> crate::makepad_script::Apply { - self.live_edit_apply.clone() - } - /// Remap an absolute coordinate from the OS-reported logical-point space /// into the layout's logical-point space when a `dpi_override` is active /// on the given window. No-op if no override is set or `os_dpi_factor` diff --git a/platform/src/event/finger.rs b/platform/src/event/finger.rs index 0fb1eabaf..c4c4099e7 100644 --- a/platform/src/event/finger.rs +++ b/platform/src/event/finger.rs @@ -487,12 +487,6 @@ impl CxFingers { self.captures.iter().find(|v| v.area == area).is_some() } - /// Whether `digit_id` is held by an area other than `area`, so a - /// capture-overload hit can tell a press a child already owns. - pub fn is_digit_captured_elsewhere(&self, digit_id: DigitId, area: Area) -> bool { - self.captures.iter().any(|v| v.digit_id == digit_id && v.area != area) - } - /// The area that captured the touch with the given uid, if any. /// Lets a raw `Event::LongPress` handler check which widget owns the press. pub fn touch_capture_area(&self, uid: u64) -> Option { diff --git a/platform/src/lib.rs b/platform/src/lib.rs index dcbb9ab8a..43762968d 100644 --- a/platform/src/lib.rs +++ b/platform/src/lib.rs @@ -245,8 +245,8 @@ pub use { web_socket::{WebSocket, WebSocketMessage}, window::{ CxWindowPool, MacosWindowChrome, MacosWindowConfig, MacosWindowKind, MacosWindowLevel, - ScriptWindowHandle, WaylandDecorationPreference, WindowBackdrop, WindowHandle, - WindowIcon, WindowIconBuffer, WindowId, WindowVisuals, + ScriptWindowHandle, WindowBackdrop, WindowHandle, WindowIcon, WindowIconBuffer, + WindowId, WindowVisuals, }, xr_tsdf::{ ChunkKey, SparseTsdGridReadSnapshot, SparseTsdReadChunk, TsdfPublishedSnapshot, diff --git a/platform/src/os/cx_shared.rs b/platform/src/os/cx_shared.rs index a87bb6d46..e65406649 100644 --- a/platform/src/os/cx_shared.rs +++ b/platform/src/os/cx_shared.rs @@ -890,7 +890,6 @@ impl Cx { LiveEditTrigger::FileChange => { self.draw_shaders.reset_for_live_reload(); self.pending_script_reapply = false; - self.live_edit_apply = crate::makepad_script::Apply::Reload; self.call_event_handler(&Event::LiveEdit); self.redraw_all(); if self.pending_script_reapply { @@ -905,10 +904,6 @@ impl Cx { // app-level handler that re-broadcasts sets a fresh flag // that lands on the next tick. self.pending_script_reapply = false; - // The DSL did not change, so re-apply with `Rebake`: the - // re-run only exists to pick up new `SAFE_INSET_PAD_*` - // values, and imperative runtime state must survive it. - self.live_edit_apply = crate::makepad_script::Apply::Rebake; self.call_event_handler(&Event::LiveEdit); self.redraw_all(); } diff --git a/platform/src/os/linux/opengl_cx.rs b/platform/src/os/linux/opengl_cx.rs index ece36edb4..030098712 100644 --- a/platform/src/os/linux/opengl_cx.rs +++ b/platform/src/os/linux/opengl_cx.rs @@ -281,31 +281,6 @@ impl OpenglCx { } } - /// Makes this context current on `egl_surface` for both drawing and reading. - /// - /// Wayland must bind the target surface before resizing its `wl_egl_window`: - /// some EGL implementations defer a resize of a non-current surface until - /// the next swap, which would render one frame with mismatched buffer geometry. - pub(crate) fn make_current_with_surface(&self, egl_surface: egl_sys::EGLSurface) -> bool { - unsafe { - let ok = (self.libegl.eglMakeCurrent.unwrap())( - self.egl_display, - egl_surface, - egl_surface, - self.egl_context, - ); - if ok == 0 { - // `eglGetError` is called outside the latch: it clears EGL's per-thread - // error, and skipping it would leak a stale code into the next report. - let egl_error = (self.libegl.eglGetError.unwrap())(); - self.report_egl_error("eglMakeCurrent(window surface)", egl_error); - return false; - } - self.make_current_error_logged.set(false); - true - } - } - /// Logs an `eglMakeCurrent` failure once per outage, naming a lost context explicitly /// so a report of "the window went black" arrives with its cause attached. fn report_egl_error(&self, what: &str, egl_error: egl_sys::EGLint) { @@ -355,9 +330,20 @@ impl Cx { unsafe { let gl = self.os.gl(); let opengl_cx = self.os.opengl_cx.as_ref().unwrap(); - if !opengl_cx.make_current_with_surface(egl_surface) { + let make_current_ok = (opengl_cx.libegl.eglMakeCurrent.unwrap())( + opengl_cx.egl_display, + egl_surface, + egl_surface, + opengl_cx.egl_context, + ); + if make_current_ok == 0 { + // `eglGetError` is called outside the latch: it clears EGL's per-thread + // error, and skipping it would leak a stale code into the next report. + let egl_error = (opengl_cx.libegl.eglGetError.unwrap())(); + opengl_cx.report_egl_error("eglMakeCurrent", egl_error); return false; } + opengl_cx.make_current_error_logged.set(false); // Apply the configured swap interval (vsync) on the now-current window surface. // Re-applied per frame because it is surface-scoped and surfaces are recreated // on resize; the call is cheap and idempotent. diff --git a/platform/src/os/linux/wayland/linux_wayland.rs b/platform/src/os/linux/wayland/linux_wayland.rs index cc44846bd..e20132132 100644 --- a/platform/src/os/linux/wayland/linux_wayland.rs +++ b/platform/src/os/linux/wayland/linux_wayland.rs @@ -37,8 +37,7 @@ use crate::{ gpu_info::GpuPerformance, texture::TextureFormat, Area, Cx, CxDrawPassParent, CxOsOp, CxWindowPool, Event, KeyModifiers, MouseButton, - MouseMoveEvent, MouseUpEvent, SignalToUI, WaylandDecorationPreference, WindowClosedEvent, - WindowGeomChangeEvent, + MouseMoveEvent, MouseUpEvent, SignalToUI, WindowClosedEvent, WindowGeomChangeEvent, }; use wayland_client::protocol::{wl_keyboard, wl_pointer}; use wayland_client::{Connection, Proxy}; @@ -51,41 +50,6 @@ fn log_linux_backdrop_unsupported_once() { }); } -fn parse_decoration_preference(value: &str) -> Option { - match value { - "server" | "server-side" => Some(WaylandDecorationPreference::ServerSide), - "client" | "client-side" => Some(WaylandDecorationPreference::ClientSide), - _ => None, - } -} - -fn decoration_preference_override() -> Option { - std::env::args_os() - .find_map(|arg| { - arg.to_str() - .and_then(|arg| arg.strip_prefix("--wayland-decoration=")) - .and_then(parse_decoration_preference) - }) - .or_else(|| { - std::env::var("MAKEPAD_WAYLAND_DECORATION") - .ok() - .as_deref() - .and_then(parse_decoration_preference) - }) -} - -/// Whether a window's surface may be promised opaque to the compositor. -/// -/// This is the same decision the macOS backend makes from the same two fields for its -/// layer's `opaque` flag, and the one the Windows backend makes before enabling -/// composition, so a window that is opaque on one platform is opaque on all of them. -/// `transparent` is the documented opt-in for a see-through window; a backdrop effect -/// needs the surface translucent to sample what is behind it, so it forfeits the -/// promise as well, whether or not this platform implements it yet. -fn window_is_opaque(transparent: bool, backdrop: crate::window::WindowBackdrop) -> bool { - !transparent && backdrop == crate::window::WindowBackdrop::None -} - pub fn wayland_event_loop(cx: Rc>) { WaylandCx::event_loop_impl(cx); } @@ -100,7 +64,6 @@ pub(crate) struct WaylandCx { /// callbacks the compositor withholds) from wedging the event loop. Disabled by /// `MAKEPAD_NO_VSYNC` for uncapped benchmarking. frame_pacing: bool, - decoration_preference_override: Option, } impl WaylandCx { @@ -116,7 +79,6 @@ impl WaylandCx { cx: cx.clone(), qhandle: None, frame_pacing: std::env::var_os("MAKEPAD_NO_VSYNC").is_none(), - decoration_preference_override: decoration_preference_override(), })); let conn = Connection::connect_to_env().unwrap(); let display = conn.display(); @@ -225,31 +187,39 @@ impl WaylandCx { // do this here because mac let mut cx = self.cx.borrow_mut(); - let window_index = state - .windows - .iter() - .position(|window| window.window_id == re.window_id); - // Wayland's native surface state does not know the geometry of - // Makepad-drawn buttons. Populate it after DPI conversion below. - re.new_geom.window_chrome_buttons = Rect::default(); - - if let Some(window_index) = window_index { - // compare in native units, before new_geom is converted below - let window = &mut state.windows[window_index]; - let uses_csd = window.uses_client_side_decorations; - let is_fullscreen = window.is_fullscreen; - let old_chrome_buttons = - cx.windows[re.window_id].window_geom.window_chrome_buttons; - let decoration_changed = { - let cx_window = &cx.windows[re.window_id]; - cx_window.uses_client_side_decorations != uses_csd - || cx_window.wayland_is_fullscreen != is_fullscreen + // When drawing our own window chrome (no server-side decorations), + // populate the chrome buttons bounding box: three buttons right-aligned + // at the top of the caption bar, matching the Makepad widget layout. + if matches!( + cx.os_type(), + OsType::LinuxWindow(LinuxWindowParams { + custom_window_chrome: true, + .. + }) + ) { + const BUTTONS_W: f64 = 46.0 * 3.0; + const BUTTONS_H: f64 = 29.0; + let w = re.new_geom.inner_size.x; + re.new_geom.window_chrome_buttons = Rect { + pos: Vec2d { + x: w - BUTTONS_W, + y: 0.0, + }, + size: Vec2d { + x: BUTTONS_W, + y: BUTTONS_H, + }, }; - let geom_changed = decoration_changed - || window.csd_shadow_needs_update() - || re.old_geom.inner_size != re.new_geom.inner_size - || re.old_geom.dpi_factor != re.new_geom.dpi_factor - || re.old_geom.is_fullscreen != re.new_geom.is_fullscreen; + } + + if let Some(window) = state + .windows + .iter_mut() + .find(|w| w.window_id == re.window_id) + { + // compare in native units, before new_geom is converted below + let geom_changed = re.old_geom.inner_size != re.new_geom.inner_size + || re.old_geom.dpi_factor != re.new_geom.dpi_factor; // Keep the wayland geom native (buffer/viewport size + the next resize's dpi come // from it). Store the zoomed geom here and the next resize reads its dpi back as @@ -258,19 +228,9 @@ impl WaylandCx { window.window_geom = re.new_geom.clone(); { let cx_window = &mut cx.windows[re.window_id]; - cx_window.uses_client_side_decorations = uses_csd; - cx_window.wayland_is_fullscreen = is_fullscreen; cx_window.os_dpi_factor = Some(re.new_geom.dpi_factor); re.new_geom = cx_window.native_window_geom_to_layout(re.new_geom); } - if uses_csd && !is_fullscreen { - const BUTTONS_SIZE: Vec2d = Vec2d { x: 138.0, y: 29.0 }; - re.new_geom.window_chrome_buttons = Rect { - pos: dvec2(re.new_geom.inner_size.x - BUTTONS_SIZE.x, 0.0), - size: BUTTONS_SIZE, - }; - } - re.old_geom.window_chrome_buttons = old_chrome_buttons; cx.windows[re.window_id].window_geom = re.new_geom.clone(); // redraw when the size or scale changed if geom_changed { @@ -587,17 +547,8 @@ impl WaylandCx { } cx.call_event_handler(&Event::WindowClosed(WindowClosedEvent { window_id })); cx.windows[window_id].is_created = false; - // The pointer may have been over the window's shadow gutter rather than the - // window, which leaves no `pointer_window` to match on. - if state.pointer_window == Some(window_id) - || state - .pointer_shadow - .is_some_and(|(shadow_window, _)| shadow_window == window_id) - { + if state.pointer_window == Some(window_id) { state.pointer_window = None; - state.pointer_shadow = None; - state.pointer_enter_serial = None; - state.last_resize_edge = None; } if state.keyboard_window == Some(window_id) { state.keyboard_window = None; @@ -625,17 +576,8 @@ impl WaylandCx { let mut cx = self.cx.borrow_mut(); cx.call_event_handler(&Event::WindowClosed(event)); cx.windows[window_id].is_created = false; - // The pointer may have been over the window's shadow gutter rather than the - // window, which leaves no `pointer_window` to match on. - if state.pointer_window == Some(window_id) - || state - .pointer_shadow - .is_some_and(|(shadow_window, _)| shadow_window == window_id) - { + if state.pointer_window == Some(window_id) { state.pointer_window = None; - state.pointer_shadow = None; - state.pointer_enter_serial = None; - state.last_resize_edge = None; } if state.keyboard_window == Some(window_id) { state.keyboard_window = None; @@ -698,13 +640,9 @@ impl WaylandCx { } else { &window.create_app_id }; - let decoration_preference = self - .decoration_preference_override - .unwrap_or(window.wayland_decorations); let window = WaylandWindow::new( window_id, compositor, - state.subcompositor.as_ref(), wm_base, state.decoration_manager.as_ref(), state.scale_manager.as_ref(), @@ -718,7 +656,6 @@ impl WaylandCx { &window.create_title, app_id, window.is_fullscreen, - decoration_preference, ); if cx.windows[window_id].backdrop != crate::window::WindowBackdrop::None { log_linux_backdrop_unsupported_once(); @@ -731,12 +668,8 @@ impl WaylandCx { // Seed the geom too: the default `dpi_factor` is 0.0, which would make // `get_pass_rect()` produce NaN once the flag is on. let native_geom = window.window_geom.clone(); - let uses_client_side_decorations = window.uses_client_side_decorations; - let is_fullscreen = window.is_fullscreen; state.windows.push(window); let cx_window = &mut cx.windows[window_id]; - cx_window.uses_client_side_decorations = uses_client_side_decorations; - cx_window.wayland_is_fullscreen = is_fullscreen; cx_window.os_dpi_factor = Some(native_geom.dpi_factor); let layout_geom = cx_window.native_window_geom_to_layout(native_geom); cx_window.window_geom = layout_geom; @@ -833,20 +766,25 @@ impl WaylandCx { } } CxOsOp::ResizeWindow(window_id, size) => { - // A Wayland client has no "set my size" request. A self-resize changes the - // next EGL buffer, viewport destination, and explicit xdg window geometry; - // the latter excludes any CSD shadow subsurfaces from placement and snapping. + // A Wayland client has no "set my size" request. Window geometry is by + // default whatever the surface commits -- `xdg_surface.set_window_geometry`: + // "If never set, the value is the full bounds of the surface ... This + // updates dynamically on every commit" -- and this backend never sets it, + // so a self-resize is just the next frame committed at a different extent. + // The paint path derives the EGL extent and the viewport destination from + // `window_geom.inner_size`, so writing it here is the whole operation. // // Only a floating toplevel may choose its own size. Under xdg_toplevel's // `maximized` state the configured window geometry must be obeyed "or the // xdg_wm_base.invalid_surface_state error is raised", which disconnects the - // client; under `fullscreen` the configured geometry is a maximum. Popups - // take their extent from their positioner and are deliberately not matched - // here. + // client; under `fullscreen` the configured geometry is a maximum. The + // configure handler folds both states into `is_fullscreen`, so that one + // flag gates the operation. Popups take their extent from their positioner + // and are deliberately not matched here. if let Some(window) = state.windows.iter_mut().find(|w| w.window_id == window_id) { - if window.is_maximized || window.is_fullscreen { + if window.window_geom.is_fullscreen { crate::error!( "ResizeWindow ignored: a maximized or fullscreen Wayland toplevel \ must keep the size the compositor configured." @@ -880,9 +818,9 @@ impl WaylandCx { // by a restored position the way it can be on Windows, macOS and X11. // xdg_toplevel exposes no absolute-positioning request: `move` is // interactive and serial-gated ("This request must be used in response to - // some sort of user action"). `xdg_popup.reposition` only moves a popup - // relative to its parent using a new positioner; it cannot place a toplevel at - // absolute screen coordinates. This arm is therefore a permanent no-op. + // some sort of user action"), and `reposition` is an xdg_popup request + // requiring xdg_wm_base v3, which `wayland_state.rs` does not bind. This arm + // is correct as a permanent no-op. CxOsOp::RepositionWindow(_window_id, _size) => {} CxOsOp::SetWindowTitle(window_id, title) => { if let Some(window) = state.windows.iter().find(|w| w.window_id == window_id) { @@ -927,15 +865,9 @@ impl WaylandCx { cx.call_event_handler(&Event::DragEnd); } CxOsOp::SetCursor(cursor) => { - state.requested_cursor = cursor; - // Native CSD resize hit-testing owns the cursor at an edge, and in the - // shadow gutter the pointer is outside the window entirely, so the app - // has no say over it there either. - if state.last_resize_edge.is_none() && state.pointer_shadow.is_none() { - if let Some(cursor_shape) = state.cursor_shape.as_ref() { - if let Some(serial) = state.pointer_enter_serial.as_ref() { - cursor_shape.set_shape(*serial, cursor.into()); - } + if let Some(cursor_shape) = state.cursor_shape.as_ref() { + if let Some(serial) = state.pointer_serial.as_ref() { + cursor_shape.set_shape(*serial, cursor.into()); } } } @@ -1244,26 +1176,13 @@ impl WaylandCx { continue; } let mut presented = false; - let opaque = { - let cx_window = &cx.windows[window_id]; - window_is_opaque(cx_window.transparent, cx_window.backdrop) - }; - let compositor = state.compositor.clone(); if let Some(window) = state.windows.iter_mut().find(|w| w.window_id == window_id) { if !window.configured { continue; } - if !window.prepare_buffer_size(cx.os.opengl_cx.as_ref().unwrap()) { - continue; - } - window.prepare_csd_shadow(); - if let (Some(compositor), Some(qhandle)) = - (compositor.as_ref(), self.qhandle.as_ref()) - { - window.sync_opaque_region(compositor, qhandle, opaque); - } + window.resize_buffers(); if std::env::var_os("MAKEPAD_WAYLAND_TRACE").is_some() { crate::log!( "Wayland paint window={:?} inner=({}, {}) dpi={} pix=({}, {})", @@ -1280,7 +1199,8 @@ impl WaylandCx { // `wp_viewport.set_destination` raises the `bad_value` protocol // error, which disconnects the client, on a zero or negative // extent, and a float-to-int cast turns both a negative and a NaN - // into zero. Floor the destination the same way as the EGL extent. + // into zero. Floor the destination the way `resize_buffers` floors + // the EGL extent. viewport.set_destination( window.window_geom.inner_size.x.max(1.0) as i32, window.window_geom.inner_size.y.max(1.0) as i32, @@ -1310,15 +1230,14 @@ impl WaylandCx { if !window.configured { continue; } - if !window.prepare_buffer_size(cx.os.opengl_cx.as_ref().unwrap()) { - continue; - } + window.resize_buffers(); if let Some(viewport) = window.viewport.as_ref() { viewport.set_source(-1., -1., -1., -1.); // `wp_viewport.set_destination` raises the `bad_value` protocol // error, which disconnects the client, on a zero or negative // extent, and a float-to-int cast turns both a negative and a NaN - // into zero. Floor the destination the same way as the EGL extent. + // into zero. Floor the destination the way `resize_buffers` floors + // the EGL extent. viewport.set_destination( window.window_geom.inner_size.x.max(1.0) as i32, window.window_geom.inner_size.y.max(1.0) as i32, @@ -1357,33 +1276,3 @@ impl WaylandCx { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn only_a_window_that_asked_for_translucency_gives_up_the_opaque_promise() { - use crate::window::WindowBackdrop; - assert!(window_is_opaque(false, WindowBackdrop::None)); - // Promising opacity for a window that wanted to be see-through would leave - // whatever is behind it on screen, so both opt-ins must veto it. - assert!(!window_is_opaque(true, WindowBackdrop::None)); - assert!(!window_is_opaque(false, WindowBackdrop::Blur)); - assert!(!window_is_opaque(false, WindowBackdrop::Acrylic)); - assert!(!window_is_opaque(true, WindowBackdrop::Blur)); - } - - #[test] - fn parses_wayland_decoration_override_values() { - assert_eq!( - parse_decoration_preference("server"), - Some(WaylandDecorationPreference::ServerSide) - ); - assert_eq!( - parse_decoration_preference("client-side"), - Some(WaylandDecorationPreference::ClientSide) - ); - assert_eq!(parse_decoration_preference("invalid"), None); - } -} diff --git a/platform/src/os/linux/wayland/opengl_wayland.rs b/platform/src/os/linux/wayland/opengl_wayland.rs index 0bbd62509..b9c76fb76 100644 --- a/platform/src/os/linux/wayland/opengl_wayland.rs +++ b/platform/src/os/linux/wayland/opengl_wayland.rs @@ -1,21 +1,16 @@ #![allow(unused_imports)] use std::fs::File; use std::os::fd::{AsFd, AsRawFd, FromRawFd}; -use std::sync::Once; use crate::egl_sys::{EGLNativeWindowType, EGLSurface, NativeWindowType}; use crate::makepad_math::Vec2d; use wayland_client::protocol::__interfaces::WL_OUTPUT_INTERFACE; -use wayland_client::protocol::{ - wl_buffer, wl_compositor, wl_region, wl_shm, wl_shm_pool, wl_subcompositor, wl_subsurface, - wl_surface, -}; +use wayland_client::protocol::{wl_buffer, wl_compositor, wl_shm, wl_shm_pool, wl_surface}; use wayland_client::{Proxy, QueueHandle}; use wayland_egl::WlEglSurface; use wayland_protocols::wp::fractional_scale::v1::client::{ wp_fractional_scale_manager_v1, wp_fractional_scale_v1, }; -use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1; use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter}; use wayland_protocols::xdg::decoration::zv1::client::{ zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, @@ -31,9 +26,7 @@ use wayland_protocols::xdg::toplevel_icon::v1::client::{ use crate::opengl_cx::OpenglCx; use crate::screen::DEFAULT_WINDOW_SIZE; use crate::wayland::wayland_state::WaylandState; -use crate::{ - egl_sys, event::WindowGeom, WaylandDecorationPreference, WindowId, -}; +use crate::{egl_sys, event::WindowGeom, WindowId}; /// Wraps a `wl_egl_window` in an `EGLSurface`, or returns null when the driver refuses it — /// which it does for an extent it cannot allocate a buffer for, however willingly @@ -49,540 +42,11 @@ fn create_egl_window_surface(opengl_cx: &OpenglCx, wl_egl_surface: &WlEglSurface } } -fn initially_uses_client_side_decorations( - decoration_manager_available: bool, - preference: WaylandDecorationPreference, -) -> bool { - !decoration_manager_available || preference == WaylandDecorationPreference::ClientSide -} - -// libadwaita 1.9's light-theme CSD profile. The 25 px margin is also what -// native GNOME applications include around their xdg window geometry. -const CSD_SHADOW_MARGIN: i32 = 25; -// How far a corner tile reaches along each edge before the straight strips take -// over. A square corner's influence dies out where the widest layer's coverage -// reaches 1 within half a quantization step: 0.15 * (1 - phi((5 + k) / 7)) < 1/510 -// at k = 10.5, so 16 px leaves better than five px of margin on the seam. -const CSD_SHADOW_CORNER_INSET: i32 = 16; -const CSD_SHADOW_CORNER_SIZE: i32 = CSD_SHADOW_MARGIN + CSD_SHADOW_CORNER_INSET; -// How far the resize grab reaches outward from the window edge into the gutter. -// libadwaita sets its toplevel input region to the window rect grown by exactly -// this much; the rest of the gutter stays click-through so a window's shadow -// never steals a click from whatever is behind it. -const CSD_SHADOW_GRAB: i32 = 12; -static CSD_SHADOW_UNAVAILABLE_LOGGED: Once = Once::new(); - -/// One CSS `box-shadow` layer: the window rectangle grown by `spread`, blurred by a -/// Gaussian of standard deviation `sigma` (half the CSS blur radius), painted at -/// `alpha`. A zero `sigma` is an unblurred hard edge. -struct CsdShadowLayer { - sigma: f64, - spread: f64, - alpha: f64, -} - -/// libadwaita 1.9 `window.csd`, the profile every GNOME 50 app on a stock desktop casts: -/// `box-shadow: 0 0 14px 5px rgb(0 0 0/15%), 0 0 5px 2px rgb(0 0 0/10%), 0 0 0 1px rgb(0 0 0/5%)` -const CSD_SHADOW_ACTIVE_LAYERS: &[CsdShadowLayer] = &[ - CsdShadowLayer { sigma: 7.0, spread: 5.0, alpha: 0.15 }, - CsdShadowLayer { sigma: 2.5, spread: 2.0, alpha: 0.10 }, - CsdShadowLayer { sigma: 0.0, spread: 1.0, alpha: 0.05 }, -]; - -/// libadwaita 1.9 `window.csd:backdrop`. Its first term is `0 0 14px 5px transparent`, -/// which exists only to keep the shadow's extent identical to the focused profile, so -/// losing focus never changes any geometry. Being transparent it is omitted here. -const CSD_SHADOW_INACTIVE_LAYERS: &[CsdShadowLayer] = &[ - CsdShadowLayer { sigma: 5.0, spread: 5.0, alpha: 0.08 }, - CsdShadowLayer { sigma: 0.0, spread: 1.0, alpha: 0.05 }, -]; - -/// Abramowitz & Stegun 7.1.26, whose 1.5e-7 worst-case error is three orders of -/// magnitude below the 1/255 the result is quantized to. -fn csd_erf(x: f64) -> f64 { - const P: f64 = 0.3275911; - const A: [f64; 5] = [ - 0.254829592, - -0.284496736, - 1.421413741, - -1.453152027, - 1.061405429, - ]; - let sign = if x < 0.0 { -1.0 } else { 1.0 }; - let x = x.abs(); - let t = 1.0 / (1.0 + P * x); - let poly = A.iter().rev().fold(0.0, |acc, a| (acc + a) * t); - sign * (1.0 - poly * (-x * x).exp()) -} - -/// The fraction of one blurred half-plane covering a point `t` px outside its edge. -/// `t` is signed, so a negative value is inside the shadow rectangle. -fn csd_shadow_coverage(layer: &CsdShadowLayer, t: f64) -> f64 { - if layer.sigma <= 0.0 { - return if t < layer.spread { 1.0 } else { 0.0 }; - } - let z = (layer.spread - t) / (layer.sigma * std::f64::consts::SQRT_2); - 0.5 * (1.0 + csd_erf(z)) -} - -/// Composited shadow alpha at a point `tx` px outside the window's nearer vertical -/// edge and `ty` px outside its nearer horizontal edge, both signed. A rectangle's -/// Gaussian shadow is separable, so each layer's 2-D coverage is the product of its -/// two 1-D coverages; `f64::NEG_INFINITY` means "far enough inside that this axis -/// contributes full coverage", which is what an edge strip passes for the axis it -/// is constant along. -fn csd_shadow_alpha(layers: &[CsdShadowLayer], tx: f64, ty: f64) -> u32 { - let transmission = layers.iter().fold(1.0, |acc, layer| { - let coverage = csd_shadow_coverage(layer, tx) * csd_shadow_coverage(layer, ty); - acc * (1.0 - layer.alpha * coverage) - }); - ((1.0 - transmission) * 255.0).round() as u32 -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum CsdShadowPieceKind { - TopLeft, - Top, - TopRight, - Left, - Right, - BottomLeft, - Bottom, - BottomRight, -} - -const CSD_SHADOW_PIECES: [CsdShadowPieceKind; 8] = [ - CsdShadowPieceKind::TopLeft, - CsdShadowPieceKind::Top, - CsdShadowPieceKind::TopRight, - CsdShadowPieceKind::Left, - CsdShadowPieceKind::Right, - CsdShadowPieceKind::BottomLeft, - CsdShadowPieceKind::Bottom, - CsdShadowPieceKind::BottomRight, -]; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct CsdShadowRect { - x: i32, - y: i32, - width: i32, - height: i32, -} - -fn csd_shadow_rect(kind: CsdShadowPieceKind, width: i32, height: i32) -> CsdShadowRect { - let m = CSD_SHADOW_MARGIN; - let i = CSD_SHADOW_CORNER_INSET; - let c = CSD_SHADOW_CORNER_SIZE; - match kind { - CsdShadowPieceKind::TopLeft => CsdShadowRect { - x: -m, - y: -m, - width: c, - height: c, - }, - CsdShadowPieceKind::Top => CsdShadowRect { - x: i, - y: -m, - width: width - 2 * i, - height: m, - }, - CsdShadowPieceKind::TopRight => CsdShadowRect { - x: width - i, - y: -m, - width: c, - height: c, - }, - CsdShadowPieceKind::Left => CsdShadowRect { - x: -m, - y: i, - width: m, - height: height - 2 * i, - }, - CsdShadowPieceKind::Right => CsdShadowRect { - x: width, - y: i, - width: m, - height: height - 2 * i, - }, - CsdShadowPieceKind::BottomLeft => CsdShadowRect { - x: -m, - y: height - i, - width: c, - height: c, - }, - CsdShadowPieceKind::Bottom => CsdShadowRect { - x: i, - y: height, - width: width - 2 * i, - height: m, - }, - CsdShadowPieceKind::BottomRight => CsdShadowRect { - x: width - i, - y: height - i, - width: c, - height: c, - }, - } -} - -fn csd_shadow_buffer_size(kind: CsdShadowPieceKind) -> (i32, i32) { - match kind { - CsdShadowPieceKind::TopLeft - | CsdShadowPieceKind::TopRight - | CsdShadowPieceKind::BottomLeft - | CsdShadowPieceKind::BottomRight => (CSD_SHADOW_CORNER_SIZE, CSD_SHADOW_CORNER_SIZE), - CsdShadowPieceKind::Top | CsdShadowPieceKind::Bottom => (1, CSD_SHADOW_MARGIN), - CsdShadowPieceKind::Left | CsdShadowPieceKind::Right => (CSD_SHADOW_MARGIN, 1), - } -} - -/// Where buffer pixel `(x, y)` of a piece sits relative to the window, as the signed -/// distance outside the nearer vertical edge and the nearer horizontal edge. Sampling -/// at pixel centres keeps the tiles seam-free against the strips they abut. -/// -/// Makepad's window is a plain rectangle, so both distances are measured from a square -/// corner. Sampling a rounded window's shadow here instead would leave the corners -/// visibly washed out, because a rounded corner's geometry recedes from the square -/// corner the shadow has to hug. -fn csd_shadow_offsets(kind: CsdShadowPieceKind, x: i32, y: i32) -> (f64, f64) { - const INSIDE: f64 = f64::NEG_INFINITY; - let margin = CSD_SHADOW_MARGIN as f64; - let inset = CSD_SHADOW_CORNER_INSET as f64; - let x = x as f64 + 0.5; - let y = y as f64 + 0.5; - // Left-hand and top pieces start `margin` px before the window; right-hand and - // bottom corner pieces start `inset` px inside it, and their strips start on it. - let past_left = margin - x; - let past_top = margin - y; - let past_right_corner = x - inset; - let past_bottom_corner = y - inset; - match kind { - CsdShadowPieceKind::TopLeft => (past_left, past_top), - CsdShadowPieceKind::Top => (INSIDE, past_top), - CsdShadowPieceKind::TopRight => (past_right_corner, past_top), - CsdShadowPieceKind::Left => (past_left, INSIDE), - CsdShadowPieceKind::Right => (x, INSIDE), - CsdShadowPieceKind::BottomLeft => (past_left, past_bottom_corner), - CsdShadowPieceKind::Bottom => (INSIDE, y), - CsdShadowPieceKind::BottomRight => (past_right_corner, past_bottom_corner), - } -} - -fn csd_shadow_pixel(kind: CsdShadowPieceKind, x: i32, y: i32, active: bool) -> u32 { - let layers = if active { - CSD_SHADOW_ACTIVE_LAYERS - } else { - CSD_SHADOW_INACTIVE_LAYERS - }; - let (tx, ty) = csd_shadow_offsets(kind, x, y); - // Premultiplied ARGB8888 black: with all three colour channels at zero the alpha - // is already the premultiplied value wl_shm requires. - csd_shadow_alpha(layers, tx, ty) << 24 -} - -/// The part of a piece that grabs the pointer for a resize, in its own surface-local -/// coordinates. The union of these across the eight pieces is the window rectangle -/// grown by [`CSD_SHADOW_GRAB`], which is exactly the input region libadwaita gives -/// its toplevels, minus the window itself (which the parent surface covers anyway). -/// -/// Each piece maps to one resize edge, so where the pointer landed is enough to know -/// which edge it grabbed and no coordinate hit-testing is needed. -fn csd_shadow_grab_rect(kind: CsdShadowPieceKind) -> CsdShadowRect { - // `wl_surface.set_input_region` ignores whatever falls outside the surface, so a - // span longer than any window keeps the stretched axis correct without ever - // needing to be re-sent on resize. - const SPAN: i32 = 1 << 20; - let outer = CSD_SHADOW_MARGIN - CSD_SHADOW_GRAB; - let inner = CSD_SHADOW_CORNER_INSET + CSD_SHADOW_GRAB; - let grab = CSD_SHADOW_GRAB; - let rect = |x, y, width, height| CsdShadowRect { x, y, width, height }; - match kind { - CsdShadowPieceKind::TopLeft => rect(outer, outer, SPAN, SPAN), - CsdShadowPieceKind::Top => rect(0, outer, SPAN, grab), - CsdShadowPieceKind::TopRight => rect(0, outer, inner, SPAN), - CsdShadowPieceKind::Left => rect(outer, 0, grab, SPAN), - CsdShadowPieceKind::Right => rect(0, 0, grab, SPAN), - CsdShadowPieceKind::BottomLeft => rect(outer, 0, SPAN, inner), - CsdShadowPieceKind::Bottom => rect(0, 0, SPAN, grab), - CsdShadowPieceKind::BottomRight => rect(0, 0, inner, inner), - } -} - -/// The edge a grab on this piece resizes. -fn csd_shadow_resize_edge(kind: CsdShadowPieceKind) -> xdg_toplevel::ResizeEdge { - use xdg_toplevel::ResizeEdge; - match kind { - CsdShadowPieceKind::TopLeft => ResizeEdge::TopLeft, - CsdShadowPieceKind::Top => ResizeEdge::Top, - CsdShadowPieceKind::TopRight => ResizeEdge::TopRight, - CsdShadowPieceKind::Left => ResizeEdge::Left, - CsdShadowPieceKind::Right => ResizeEdge::Right, - CsdShadowPieceKind::BottomLeft => ResizeEdge::BottomLeft, - CsdShadowPieceKind::Bottom => ResizeEdge::Bottom, - CsdShadowPieceKind::BottomRight => ResizeEdge::BottomRight, - } -} - -struct CsdShadowPiece { - kind: CsdShadowPieceKind, - surface: wl_surface::WlSurface, - subsurface: wl_subsurface::WlSubsurface, - viewport: wp_viewport::WpViewport, - active_buffer: wl_buffer::WlBuffer, - inactive_buffer: wl_buffer::WlBuffer, -} - -struct WaylandCsdShadow { - pieces: Vec, - state: Option<(i32, i32, bool, bool)>, -} - -fn csd_shadow_visible_at_size(visible: bool, width: i32, height: i32) -> bool { - visible - && width > 2 * CSD_SHADOW_CORNER_INSET - && height > 2 * CSD_SHADOW_CORNER_INSET -} - -impl WaylandCsdShadow { - fn new( - compositor: &wl_compositor::WlCompositor, - subcompositor: Option<&wl_subcompositor::WlSubcompositor>, - shm: Option<&wl_shm::WlShm>, - viewporter: Option<&wp_viewporter::WpViewporter>, - parent: &wl_surface::WlSurface, - qhandle: &QueueHandle, - ) -> Option { - let (Some(subcompositor), Some(shm), Some(viewporter)) = - (subcompositor, shm, viewporter) - else { - CSD_SHADOW_UNAVAILABLE_LOGGED.call_once(|| { - crate::warning!( - "Wayland client-side shadow unavailable: wl_subcompositor, wl_shm, and \ - wp_viewporter are required; continuing with client-side window controls" - ); - }); - return None; - }; - let Some(buffers) = Self::create_buffers(shm, qhandle) else { - CSD_SHADOW_UNAVAILABLE_LOGGED.call_once(|| { - crate::warning!( - "Wayland client-side shadow unavailable: could not allocate shared-memory \ - buffers; continuing with client-side window controls" - ); - }); - return None; - }; - let mut pieces = Vec::with_capacity(CSD_SHADOW_PIECES.len()); - for (kind, (active_buffer, inactive_buffer)) in - CSD_SHADOW_PIECES.into_iter().zip(buffers) - { - let surface = compositor.create_surface(qhandle, ()); - // The gutter is where this window is resized from, the way it is for every - // native app on the desktop: the pointer never has to compete with a widget - // for the edge, so the close button no longer swallows the top-right corner. - // Input regions are copied by the compositor at request time, and these are - // expressed so they survive any resize, so this is the only time they are set. - let grab = csd_shadow_grab_rect(kind); - let region = compositor.create_region(qhandle, ()); - region.add(grab.x, grab.y, grab.width, grab.height); - surface.set_input_region(Some(®ion)); - region.destroy(); - let subsurface = subcompositor.get_subsurface(&surface, parent, qhandle, ()); - subsurface.set_sync(); - subsurface.place_below(parent); - let viewport = viewporter.get_viewport(&surface, qhandle, ()); - pieces.push(CsdShadowPiece { - kind, - surface, - subsurface, - viewport, - active_buffer, - inactive_buffer, - }); - } - Some(Self { - pieces, - state: None, - }) - } - - /// The piece owning `surface`, if any. The pointer entering one means the pointer is - /// in this window's gutter rather than in the window. - fn piece_kind_for_surface( - &self, - surface_id: &wayland_client::backend::ObjectId, - ) -> Option { - self.pieces - .iter() - .find(|piece| piece.surface.id() == *surface_id) - .map(|piece| piece.kind) - } - - fn create_buffers( - shm: &wl_shm::WlShm, - qhandle: &QueueHandle, - ) -> Option> { - let mut offset = 0; - let layouts: Vec<_> = CSD_SHADOW_PIECES - .into_iter() - .map(|kind| { - let (width, height) = csd_shadow_buffer_size(kind); - let layout = (kind, width, height, offset); - offset += (width * height * 4) as usize; - layout - }) - .collect(); - let style_bytes = offset; - let total_bytes = style_bytes * 2; - let name = std::ffi::CString::new("makepad-csd-shadow").ok()?; - let raw_fd = - unsafe { crate::libc_sys::memfd_create(name.as_ptr(), crate::libc_sys::MFD_CLOEXEC) }; - if raw_fd < 0 { - return None; - } - let fd = unsafe { std::os::fd::OwnedFd::from_raw_fd(raw_fd) }; - if unsafe { crate::libc_sys::ftruncate(fd.as_raw_fd(), total_bytes as i64) } != 0 { - return None; - } - let map = unsafe { - crate::libc_sys::mmap( - std::ptr::null_mut(), - total_bytes, - crate::libc_sys::PROT_READ | crate::libc_sys::PROT_WRITE, - crate::libc_sys::MAP_SHARED, - fd.as_raw_fd(), - 0, - ) - }; - if map == crate::libc_sys::MAP_FAILED { - return None; - } - let bytes = unsafe { std::slice::from_raw_parts_mut(map.cast::(), total_bytes) }; - for (style_index, active) in [true, false].into_iter().enumerate() { - for &(kind, width, height, offset) in &layouts { - let piece_offset = style_index * style_bytes + offset; - for y in 0..height { - for x in 0..width { - let pixel_offset = piece_offset - + ((y * width + x) * 4) as usize; - bytes[pixel_offset..pixel_offset + 4] - // wl_shm's ARGB8888 is defined as little endian regardless of - // the host, so the byte order cannot follow the CPU's. - .copy_from_slice(&csd_shadow_pixel(kind, x, y, active).to_le_bytes()); - } - } - } - } - unsafe { - crate::libc_sys::munmap(map, total_bytes); - } - - let pool = shm.create_pool(fd.as_fd(), total_bytes as i32, qhandle, ()); - let create_buffer = |offset: usize, width: i32, height: i32| { - pool.create_buffer( - offset as i32, - width, - height, - width * 4, - wl_shm::Format::Argb8888, - qhandle, - (), - ) - }; - let buffers = layouts - .into_iter() - .map(|(_, width, height, offset)| { - ( - create_buffer(offset, width, height), - create_buffer(style_bytes + offset, width, height), - ) - }) - .collect(); - pool.destroy(); - Some(buffers) - } - - fn is_visible(&self) -> bool { - self.state.is_some_and(|state| state.2) - } - - fn needs_update(&self, width: i32, height: i32, visible: bool, active: bool) -> bool { - let visible = csd_shadow_visible_at_size(visible, width, height); - let active = visible && active; - self.state != Some((width, height, visible, active)) - } - - fn update(&mut self, width: i32, height: i32, visible: bool, active: bool) -> bool { - // Nine-patch edge destinations must be positive. Makepad's 200x120 minimum is - // well above this; suppress the visual-only shadow for pathological sizes. - let visible = csd_shadow_visible_at_size(visible, width, height); - // Focus has no visual effect while detached. Ignoring it here also avoids - // repainting maximized, tiled, fullscreen, and server-decorated windows. - let active = visible && active; - let size_changed = self.state.map(|state| (state.0, state.1)) != Some((width, height)); - if !self.needs_update(width, height, visible, active) { - return false; - } - let was_visible = self.state.is_some_and(|state| state.2); - let style_changed = self.state.map_or(true, |state| state.3 != active); - for (kind, piece) in CSD_SHADOW_PIECES.into_iter().zip(&self.pieces) { - if visible { - let rect = csd_shadow_rect(kind, width, height); - // Both are sticky surface state, so a focus change — which swaps buffers - // at an unchanged size — has no reason to re-send them. - if !was_visible || size_changed { - piece.subsurface.set_position(rect.x, rect.y); - piece.viewport.set_destination(rect.width, rect.height); - } - if !was_visible || style_changed { - let buffer = if active { - &piece.active_buffer - } else { - &piece.inactive_buffer - }; - piece.surface.attach(Some(buffer), 0, 0); - } - if !was_visible || style_changed || size_changed { - piece.surface.damage(0, 0, rect.width, rect.height); - } - piece.surface.commit(); - } else if was_visible { - piece.surface.attach(None, 0, 0); - piece.surface.commit(); - } - } - self.state = Some((width, height, visible, active)); - size_changed - } - - fn destroy(self) { - for piece in self.pieces { - piece.viewport.destroy(); - piece.subsurface.destroy(); - piece.surface.destroy(); - piece.active_buffer.destroy(); - piece.inactive_buffer.destroy(); - } - } -} - -fn should_show_csd_shadow(uses_csd: bool, maximized: bool, fullscreen: bool, tiled: bool) -> bool { - uses_csd && !maximized && !fullscreen && !tiled -} - pub(crate) struct WaylandWindow { pub window_id: WindowId, pub base_surface: wl_surface::WlSurface, pub toplevel: xdg_toplevel::XdgToplevel, pub decoration: Option, - pub uses_client_side_decorations: bool, - pub pending_client_side_decorations: Option, - pub is_maximized: bool, - pub is_fullscreen: bool, - pub is_tiled: bool, - pub is_active: bool, - pub unavailable_resize_edges: u8, pub xdg_surface: xdg_surface::XdgSurface, pub viewport: Option, pub fractional_scale: Option, @@ -591,17 +55,12 @@ pub(crate) struct WaylandWindow { pub cal_size: Vec2d, pub wl_egl_surface: WlEglSurface, pub egl_surface: EGLSurface, - csd_shadow: Option, - /// The `(width, height, opaque)` the surface's opaque region was last set from, so a - /// steady-state frame re-sends nothing. - opaque_region_state: Option<(i32, i32, bool)>, } impl WaylandWindow { pub fn new( window_id: WindowId, compositer: &wl_compositor::WlCompositor, - subcompositor: Option<&wl_subcompositor::WlSubcompositor>, wm_base: &xdg_wm_base::XdgWmBase, decoration_manager: Option<&zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>, scale_manager: Option<&wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>, @@ -615,7 +74,6 @@ impl WaylandWindow { title: &str, app_id: &str, is_fullscreen: bool, - decoration_preference: WaylandDecorationPreference, ) -> WaylandWindow { // Checked "downcast" of the EGL platform display to a X11 display. assert_eq!(opengl_cx.egl_platform, egl_sys::EGL_PLATFORM_WAYLAND_KHR); @@ -633,53 +91,12 @@ impl WaylandWindow { // Set window icon via xdg-toplevel-icon-v1 if compositor supports it Self::set_wayland_icon(icon_manager, shm, &toplevel, qhandle); - let uses_client_side_decorations = initially_uses_client_side_decorations( - decoration_manager.is_some(), - decoration_preference, - ); - let decoration = decoration_manager.and_then(|manager| { - if decoration_preference == WaylandDecorationPreference::ClientSide { - // Without negotiation the protocol requires clients to self-decorate; - // set_mode(ClientSide) would only be a preference the compositor may reject. - return None; - } - let decoration = manager.get_toplevel_decoration(&toplevel, qhandle, window_id); - decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ServerSide); - Some(decoration) + let decoration = decoration_manager.map(|manager| { + let decoration = manager.get_toplevel_decoration(&toplevel, qhandle, ()); + decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide); + decoration }); - let surface_width = (inner_size.x as i32).max(1); - let surface_height = (inner_size.y as i32).max(1); - // Do not allocate eight shadow buffers and subsurfaces for a window that - // the compositor decorates. If negotiation later selects CSD, the - // configure handler creates them before the first client-decorated frame. - let mut csd_shadow = uses_client_side_decorations - .then(|| { - WaylandCsdShadow::new( - compositer, - subcompositor, - shm, - viewporter, - &base_surface, - qhandle, - ) - }) - .flatten(); - if let Some(shadow) = csd_shadow.as_mut() { - shell_surface.set_window_geometry(0, 0, surface_width, surface_height); - shadow.update( - surface_width, - surface_height, - should_show_csd_shadow( - uses_client_side_decorations, - false, - is_fullscreen, - false, - ), - false, - ); - } - if is_fullscreen { toplevel.set_fullscreen(None); } @@ -688,8 +105,8 @@ impl WaylandWindow { // `wl_egl_window_create` rejects a non-positive extent, and a float-to-int cast turns // both a negative and a NaN into zero, so the requested size is floored before the // call rather than allowed to panic an app at startup over a bad saved size. - let egl_w = surface_width; - let egl_h = surface_height; + let egl_w = (inner_size.x as i32).max(1); + let egl_h = (inner_size.y as i32).max(1); let mut wl_egl_surface = match WlEglSurface::new(base_surface.id(), egl_w, egl_h) { Ok(surface) => surface, Err(e) => { @@ -738,13 +155,6 @@ impl WaylandWindow { base_surface, toplevel, decoration, - uses_client_side_decorations, - pending_client_side_decorations: None, - is_maximized: false, - is_fullscreen, - is_tiled: false, - is_active: false, - unavailable_resize_edges: 0, viewport, fractional_scale, configured: false, @@ -754,8 +164,6 @@ impl WaylandWindow { window_geom: geom, wl_egl_surface, egl_surface, - csd_shadow, - opaque_region_state: None, } } /// Set the toplevel icon via xdg-toplevel-icon-v1 protocol using shm pixel data. @@ -846,152 +254,27 @@ impl WaylandWindow { // wl_buf kept alive until compositor reads it (destroyed on drop) } - pub fn prepare_buffer_size(&mut self, opengl_cx: &OpenglCx) -> bool { + pub fn resize_buffers(&mut self) -> bool { let cal_size = Vec2d { x: self.window_geom.inner_size.x * self.window_geom.dpi_factor, y: self.window_geom.inner_size.y * self.window_geom.dpi_factor, }; if self.cal_size != cal_size { - // NVIDIA's Wayland EGL platform may defer resizing a non-current - // EGLSurface until its next swap. Bind this exact surface first so - // the next frame cannot mix the old buffer with the new viewport. - if !opengl_cx.make_current_with_surface(self.egl_surface) { - return false; - } + self.cal_size = cal_size; let pix_width = cal_size.x.max(1.0) as i32; let pix_height = cal_size.y.max(1.0) as i32; self.wl_egl_surface.resize(pix_width, pix_height, 0, 0); - // Cache only a resize that was actually issued. A failed bind is - // retried on the next paint rather than leaving stale buffers. - self.cal_size = cal_size; - } - true - } - - /// Promises the compositor that the whole window rectangle is solid, so it can skip - /// blending this surface and cull everything the window covers, and can hand a - /// fullscreen buffer straight to the display controller instead of compositing it. - /// - /// The buffer is ARGB8888 — every EGL config Makepad accepts asks for 8 alpha bits — - /// so without this promise the compositor has no way to learn that the alpha channel - /// is uniformly opaque short of reading every pixel, and must assume it is not. - /// - /// `opaque` must be false for a window that actually wants translucency, or the - /// compositor will happily leave whatever was behind it on screen. The region covers - /// the base surface only: the shadow subsurfaces are genuinely translucent, and are - /// separate surfaces that keep their own (empty) opaque regions. - pub fn sync_opaque_region( - &mut self, - compositor: &wl_compositor::WlCompositor, - qhandle: &QueueHandle, - opaque: bool, - ) { - let width = (self.window_geom.inner_size.x as i32).max(1); - let height = (self.window_geom.inner_size.y as i32).max(1); - if self.opaque_region_state == Some((width, height, opaque)) { - return; - } - self.opaque_region_state = Some((width, height, opaque)); - if opaque { - let region = compositor.create_region(qhandle, ()); - region.add(0, 0, width, height); - self.base_surface.set_opaque_region(Some(®ion)); - region.destroy(); + true } else { - self.base_surface.set_opaque_region(None); + false } } - - /// Whether the shadow gutter is currently mapped, and therefore carrying this window's - /// resize grabs. While it is, the interior bands are redundant: they would only compete - /// with the app's own widgets for the pointer, which is what let the close button - /// swallow the top-right corner. A tiled window has no gutter and falls back to them. - pub fn csd_shadow_gutter_active(&self) -> bool { - self.csd_shadow - .as_ref() - .is_some_and(|shadow| shadow.is_visible()) - } - - /// The resize edge and cursor for a pointer that has entered one of this window's - /// shadow surfaces, or `None` if `surface` is not part of this window's shadow. - /// Edges the compositor has declared unavailable — a tiled window's shared borders — - /// are narrowed to the components that remain resizable, so a half-tiled window - /// keeps the corner grabs on its free axis. - pub fn csd_shadow_resize_for_surface( - &self, - surface_id: &wayland_client::backend::ObjectId, - ) -> Option<(xdg_toplevel::ResizeEdge, wp_cursor_shape_device_v1::Shape)> { - let kind = self - .csd_shadow - .as_ref()? - .piece_kind_for_surface(surface_id)?; - let edge = crate::wayland::wayland_state::available_resize_edge( - csd_shadow_resize_edge(kind), - self.unavailable_resize_edges, - )?; - Some((edge, crate::wayland::wayland_state::resize_edge_cursor(edge))) - } - - pub fn csd_shadow_needs_update(&self) -> bool { - let width = (self.window_geom.inner_size.x as i32).max(1); - let height = (self.window_geom.inner_size.y as i32).max(1); - let visible = should_show_csd_shadow( - self.uses_client_side_decorations, - self.is_maximized, - self.is_fullscreen, - self.is_tiled, - ); - self.csd_shadow - .as_ref() - .is_some_and(|shadow| shadow.needs_update(width, height, visible, self.is_active)) - } - - pub(crate) fn ensure_csd_shadow( - &mut self, - compositor: &wl_compositor::WlCompositor, - subcompositor: Option<&wl_subcompositor::WlSubcompositor>, - shm: Option<&wl_shm::WlShm>, - viewporter: Option<&wp_viewporter::WpViewporter>, - qhandle: &QueueHandle, - ) { - if self.csd_shadow.is_none() { - self.csd_shadow = WaylandCsdShadow::new( - compositor, - subcompositor, - shm, - viewporter, - &self.base_surface, - qhandle, - ); - } - } - - pub fn prepare_csd_shadow(&mut self) { - let width = (self.window_geom.inner_size.x as i32).max(1); - let height = (self.window_geom.inner_size.y as i32).max(1); - let visible = should_show_csd_shadow( - self.uses_client_side_decorations, - self.is_maximized, - self.is_fullscreen, - self.is_tiled, - ); - if let Some(shadow) = self.csd_shadow.as_mut() { - if shadow.update(width, height, visible, self.is_active) { - self.xdg_surface - .set_window_geometry(0, 0, width, height); - } - } - } - pub fn close_window(&mut self) { // Destroy in protocol order: role-specific objects first, base // surface last. if let Some(decoration) = self.decoration.take() { decoration.destroy(); } - if let Some(shadow) = self.csd_shadow.take() { - shadow.destroy(); - } self.toplevel.destroy(); self.xdg_surface.destroy(); if let Some(viewport) = self.viewport.take() { @@ -1122,21 +405,20 @@ impl WaylandPopupWindow { } } - pub fn prepare_buffer_size(&mut self, opengl_cx: &OpenglCx) -> bool { + pub fn resize_buffers(&mut self) -> bool { let cal_size = Vec2d { x: self.window_geom.inner_size.x * self.window_geom.dpi_factor, y: self.window_geom.inner_size.y * self.window_geom.dpi_factor, }; if self.cal_size != cal_size { - if !opengl_cx.make_current_with_surface(self.egl_surface) { - return false; - } + self.cal_size = cal_size; if let Some(ref wl_egl_surface) = self.wl_egl_surface { wl_egl_surface.resize(cal_size.x.max(1.0) as i32, cal_size.y.max(1.0) as i32, 0, 0); } - self.cal_size = cal_size; + true + } else { + false } - true } pub fn close_window(&mut self) { @@ -1175,272 +457,3 @@ impl Drop for WaylandPopupWindow { self.close_window(); } } - -#[cfg(test)] -mod tests { - use super::*; - - fn alpha(pixel: u32) -> u8 { - (pixel >> 24) as u8 - } - - #[test] - fn decoration_initial_state_prefers_server_and_falls_back_without_protocol() { - assert!(!initially_uses_client_side_decorations( - true, - WaylandDecorationPreference::ServerSide - )); - assert!(initially_uses_client_side_decorations( - false, - WaylandDecorationPreference::ServerSide - )); - assert!(initially_uses_client_side_decorations( - true, - WaylandDecorationPreference::ClientSide - )); - } - - #[test] - fn shadow_layout_tiles_the_gutter_without_gaps_or_overlap() { - let expected = [ - (-25, -25, 41, 41), - (16, -25, 608, 25), - (624, -25, 41, 41), - (-25, 16, 25, 448), - (640, 16, 25, 448), - (-25, 464, 41, 41), - (16, 480, 608, 25), - (624, 464, 41, 41), - ]; - for (kind, expected) in CSD_SHADOW_PIECES.into_iter().zip(expected) { - let rect = csd_shadow_rect(kind, 640, 480); - assert_eq!((rect.x, rect.y, rect.width, rect.height), expected); - } - - // Every pixel of the ring around the window is covered exactly once. The pieces - // are translucent, so an overlap would double-blend into a visible seam. - // The smallest size `csd_shadow_visible_at_size` admits is checked too, since - // that is where the corner tiles come closest to colliding. - let smallest = 2 * CSD_SHADOW_CORNER_INSET + 1; - for (width, height) in [(640, 480), (smallest, smallest), (smallest, 480)] { - let rects: Vec<_> = CSD_SHADOW_PIECES - .into_iter() - .map(|kind| csd_shadow_rect(kind, width, height)) - .collect(); - for rect in &rects { - assert!( - rect.width > 0 && rect.height > 0, - "empty destination at {width}x{height}" - ); - } - for y in -CSD_SHADOW_MARGIN..height + CSD_SHADOW_MARGIN { - for x in -CSD_SHADOW_MARGIN..width + CSD_SHADOW_MARGIN { - let covers = rects - .iter() - .filter(|rect| { - x >= rect.x - && x < rect.x + rect.width - && y >= rect.y - && y < rect.y + rect.height - }) - .count(); - let inside_window = (0..width).contains(&x) && (0..height).contains(&y); - // The corner tiles reach `CSD_SHADOW_CORNER_INSET` px into the window, - // where the parent surface covers them; everywhere else in the ring is - // covered exactly once and nothing spills past the margin. - if inside_window { - assert!( - covers <= 1, - "overlap inside the {width}x{height} window at ({x}, {y})" - ); - } else { - assert_eq!( - covers, 1, - "gutter of {width}x{height} not covered once at ({x}, {y})" - ); - } - } - } - } - } - - #[test] - fn shadow_raster_is_monotonic_symmetric_and_seam_free() { - for i in 1..CSD_SHADOW_MARGIN { - let previous = alpha(csd_shadow_pixel(CsdShadowPieceKind::Top, 0, i - 1, true)); - let current = alpha(csd_shadow_pixel(CsdShadowPieceKind::Top, 0, i, true)); - assert!(current >= previous); - } - for i in 0..CSD_SHADOW_MARGIN { - assert_eq!( - alpha(csd_shadow_pixel(CsdShadowPieceKind::Top, 0, i, true)), - alpha(csd_shadow_pixel( - CsdShadowPieceKind::Bottom, - 0, - CSD_SHADOW_MARGIN - 1 - i, - true, - )) - ); - } - // The outermost row of the margin has faded to nothing, so the shadow does not - // end in a visible step. - assert_eq!( - alpha(csd_shadow_pixel(CsdShadowPieceKind::Top, 0, 0, true)), - 0 - ); - - // A square corner is where two half-covered edges meet, so it is lighter than a - // straight edge at the same distance but nowhere near the washed-out 14/255 that - // sampling a 15 px-rounded window's shadow here would give. - let corner = alpha(csd_shadow_pixel(CsdShadowPieceKind::TopLeft, 24, 24, true)); - let straight = alpha(csd_shadow_pixel(CsdShadowPieceKind::Top, 0, 24, true)); - assert_eq!((corner, straight), (44, 55)); - - let inner = csd_shadow_pixel(CsdShadowPieceKind::Top, 0, 24, true); - assert_eq!(inner & 0x00ff_ffff, 0, "must stay premultiplied black"); - assert_eq!(alpha(inner), straight); - } - - #[test] - fn shadow_corner_tiles_join_the_straight_edges_exactly() { - let last = CSD_SHADOW_CORNER_SIZE - 1; - for active in [false, true] { - // CSD_SHADOW_CORNER_INSET is chosen so the corner's second axis has reached - // full coverage by the time the strips take over: the join is not merely - // close, it is bit-identical, so no seam can appear at any scale. - for y in 0..CSD_SHADOW_MARGIN { - assert_eq!( - csd_shadow_pixel(CsdShadowPieceKind::TopLeft, last, y, active), - csd_shadow_pixel(CsdShadowPieceKind::Top, 0, y, active), - "top join differs at y={y}" - ); - } - for x in 0..CSD_SHADOW_MARGIN { - assert_eq!( - csd_shadow_pixel(CsdShadowPieceKind::TopLeft, x, last, active), - csd_shadow_pixel(CsdShadowPieceKind::Left, x, 0, active), - "left join differs at x={x}" - ); - } - - for y in 0..CSD_SHADOW_CORNER_SIZE { - for x in 0..CSD_SHADOW_CORNER_SIZE { - let mirror_x = last - x; - let mirror_y = last - y; - assert_eq!( - csd_shadow_pixel(CsdShadowPieceKind::TopLeft, x, y, active), - csd_shadow_pixel(CsdShadowPieceKind::TopRight, mirror_x, y, active) - ); - assert_eq!( - csd_shadow_pixel(CsdShadowPieceKind::TopLeft, x, y, active), - csd_shadow_pixel(CsdShadowPieceKind::BottomLeft, x, mirror_y, active) - ); - assert_eq!( - csd_shadow_pixel(CsdShadowPieceKind::TopLeft, x, y, active), - csd_shadow_pixel( - CsdShadowPieceKind::BottomRight, - mirror_x, - mirror_y, - active, - ) - ); - } - } - } - } - - #[test] - fn shadow_grab_rects_reach_exactly_the_libadwaita_input_region() { - // libadwaita grows its toplevel input region by 12 px on every side. The union of - // the eight grab rects must be the same ring, with each piece owning the side it - // resizes and none of them claiming the outer half of the gutter. - let (width, height) = (640, 480); - for kind in CSD_SHADOW_PIECES { - let rect = csd_shadow_rect(kind, width, height); - let grab = csd_shadow_grab_rect(kind); - // Clip the grab rect to the piece the way the compositor does, then place it - // in window coordinates. - let x0 = rect.x + grab.x.max(0); - let y0 = rect.y + grab.y.max(0); - let x1 = rect.x + (grab.x + grab.width).min(rect.width); - let y1 = rect.y + (grab.y + grab.height).min(rect.height); - assert!(x0 < x1 && y0 < y1, "{kind:?} has an empty grab region"); - assert!( - x0 >= -CSD_SHADOW_GRAB - && y0 >= -CSD_SHADOW_GRAB - && x1 <= width + CSD_SHADOW_GRAB - && y1 <= height + CSD_SHADOW_GRAB, - "{kind:?} grabs outside the 12 px halo: ({x0},{y0})..({x1},{y1})" - ); - } - - // The top-right corner is grabbable strictly outside the window, which is what - // keeps the close button from swallowing it. - let rect = csd_shadow_rect(CsdShadowPieceKind::TopRight, width, height); - let grab = csd_shadow_grab_rect(CsdShadowPieceKind::TopRight); - assert!(rect.x + grab.x + grab.width > width); - assert!(rect.y + grab.y < 0); - } - - #[test] - fn shadow_pieces_map_to_the_edge_they_sit_on() { - use xdg_toplevel::ResizeEdge; - for (kind, edge) in [ - (CsdShadowPieceKind::TopLeft, ResizeEdge::TopLeft), - (CsdShadowPieceKind::Top, ResizeEdge::Top), - (CsdShadowPieceKind::TopRight, ResizeEdge::TopRight), - (CsdShadowPieceKind::Left, ResizeEdge::Left), - (CsdShadowPieceKind::Right, ResizeEdge::Right), - (CsdShadowPieceKind::BottomLeft, ResizeEdge::BottomLeft), - (CsdShadowPieceKind::Bottom, ResizeEdge::Bottom), - (CsdShadowPieceKind::BottomRight, ResizeEdge::BottomRight), - ] { - assert_eq!(csd_shadow_resize_edge(kind), edge); - } - } - - #[test] - fn shadow_alpha_matches_the_installed_libadwaita_profile() { - let active = [54, 39, 34, 28, 24, 20, 17, 14, 12, 10, 8, 6, 5, 4, 3, 2, 1, 1, 1, 0]; - let inactive = [28, 15, 14, 12, 11, 9, 8, 6, 5, 3, 2, 1, 1, 1, 0]; - for (distance, expected) in active.into_iter().enumerate() { - let actual = alpha(csd_shadow_pixel( - CsdShadowPieceKind::Top, - 0, - CSD_SHADOW_MARGIN - 1 - distance as i32, - true, - )); - assert!(actual.abs_diff(expected) <= 1); - } - for (distance, expected) in inactive.into_iter().enumerate() { - let actual = alpha(csd_shadow_pixel( - CsdShadowPieceKind::Top, - 0, - CSD_SHADOW_MARGIN - 1 - distance as i32, - false, - )); - assert!(actual.abs_diff(expected) <= 1); - } - assert!( - alpha(csd_shadow_pixel(CsdShadowPieceKind::Top, 0, 24, true)) - > alpha(csd_shadow_pixel(CsdShadowPieceKind::Top, 0, 24, false)) - ); - } - - #[test] - fn shadow_avoids_invalid_nine_patch_destinations_for_tiny_windows() { - assert!(!csd_shadow_visible_at_size(true, 32, 480)); - assert!(!csd_shadow_visible_at_size(true, 640, 32)); - assert!(csd_shadow_visible_at_size(true, 33, 33)); - assert!(!csd_shadow_visible_at_size(false, 640, 480)); - } - - #[test] - fn shadow_only_appears_on_floating_client_decorated_windows() { - assert!(should_show_csd_shadow(true, false, false, false)); - assert!(!should_show_csd_shadow(false, false, false, false)); - assert!(!should_show_csd_shadow(true, true, false, false)); - assert!(!should_show_csd_shadow(true, false, true, false)); - assert!(!should_show_csd_shadow(true, false, false, true)); - } -} diff --git a/platform/src/os/linux/wayland/wayland_state.rs b/platform/src/os/linux/wayland/wayland_state.rs index 5b6a5a9e0..1e8483540 100644 --- a/platform/src/os/linux/wayland/wayland_state.rs +++ b/platform/src/os/linux/wayland/wayland_state.rs @@ -4,7 +4,7 @@ use crate::{ makepad_math::{dvec2, Vec2d}, wayland::{wayland_type, xkb_sys}, Area, DragEvent, DragItem, DragResponse, DropEvent, KeyEvent, KeyModifiers, MouseButton, - MouseCursor, MouseDownEvent, MouseMoveEvent, MouseUpEvent, TextClipboardEvent, TextInputEvent, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, TextClipboardEvent, TextInputEvent, WindowClosedEvent, WindowDragQueryEvent, WindowDragQueryResponse, }; use std::{ @@ -21,8 +21,7 @@ use wayland_client::{ wl_buffer, wl_callback, wl_compositor, wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_keyboard, wl_output, wl_pointer::{self, ButtonState}, - wl_region, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_subcompositor, wl_subsurface, - wl_surface, + wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface, }, Connection, Dispatch, Proxy, QueueHandle, WEnum, }; @@ -50,10 +49,7 @@ use wayland_protocols::{ use crate::{ cx_native::EventFlow, - event::{ - PopupDismissReason, PopupDismissedEvent, ScrollEvent, ScrollPhase, WindowGeom, - TAP_COUNT_DISTANCE, TAP_COUNT_TIME, - }, + event::{PopupDismissReason, PopupDismissedEvent, ScrollEvent, ScrollPhase, WindowGeom}, select_timer::SelectTimers, wayland::wayland_app::WaylandApp, x11::xlib_event::XlibEvent, @@ -66,244 +62,6 @@ use super::opengl_wayland::{WaylandPopupWindow, WaylandWindow}; /// Reserved timer ID for keyboard repeat. Uses a high value to avoid conflicts with app timers. const KEY_REPEAT_TIMER_ID: u64 = u64::MAX - 1; -/// Whether a pointer frame's scroll came from a wheel-like source: one that ratchets in -/// coarse steps, so its detents drive the delta, it carries no gesture phase, and it is -/// reported to widgets as mouse input. -/// -/// `source` is the frame's `wl_pointer::AxisSource`, or `None` when the compositor sent -/// none — the event is optional and only sent when the source is known. With no source, -/// the detents settle it: a device without discrete steps does not generate them, which -/// the spec spells out for `axis_discrete`. Guessing wheel-like is in any case the safe -/// guess, being the one classification that cannot strand a stretched rubber band waiting -/// for a terminator the spec does not promise. -fn scroll_is_wheel_like(source: Option, has_detents: bool) -> bool { - match source { - Some(wl_pointer::AxisSource::Wheel) | Some(wl_pointer::AxisSource::WheelTilt) => true, - // A trackpoint or button-held scroll is smooth, so it takes the raw pixel path even - // though it is not a gesture. - Some(wl_pointer::AxisSource::Finger) | Some(wl_pointer::AxisSource::Continuous) => false, - _ => has_detents, - } -} - -/// What one pointer frame's axis events add up to. -struct FrameScroll { - /// The delta in logical pixels. - delta: Vec2d, - phase: ScrollPhase, - /// Reported as `ScrollEvent::is_mouse`: a wheel that ratchets in steps, which widgets may - /// ease between. False for every smooth source, which needs no easing. - is_mouse: bool, -} - -/// Resolve a pointer frame's accumulated axis events into one scroll, or `None` when the -/// frame carries nothing worth dispatching. -/// -/// `source` is the frame's `wl_pointer::AxisSource` (`None` if the compositor sent none), -/// `gesture_active` whether the previous frame was a live touchpad gesture, and `stopped` -/// whether an `AxisStop` arrived in this frame. -fn frame_scroll( - source: Option, - gesture_active: bool, - stopped: bool, - acc: Vec2d, - detents: Vec2d, -) -> Option { - let has_detents = detents.x != 0.0 || detents.y != 0.0; - let has_delta = acc.x != 0.0 || acc.y != 0.0 || has_detents; - let is_wheel_like = scroll_is_wheel_like(source, has_detents); - // `axis_source` is per-frame and optional, so a compositor may name the source on a - // gesture's motion frames and omit it on the lift-off frame. Treating that frame as - // sourceless would drop the terminator and leave a stretched rubber band with nothing - // to release it, so a gesture already in flight carries its classification forward -- - // but never over a frame whose detents say it is a wheel. - let is_finger = match source { - Some(wl_pointer::AxisSource::Finger) => true, - None => gesture_active && !is_wheel_like, - _ => false, - }; - // A stop alongside live motion is not the end of the gesture. Per the `frame` event: - // "When a wl_pointer.axis and a wl_pointer.axis_stop event occur within the same frame, - // this indicates that axis movement in one axis has stopped but continues in the other - // axis." The lift-off frame that does end the gesture carries its stops alone. - let gesture_ended = is_finger && stopped && !has_delta; - if !has_delta && !gesture_ended { - // Only `Finger` is guaranteed an `AxisStop`; the spec tells clients to treat wheel, - // wheel_tilt and continuous sequences "as unterminated by default". A bare stop from - // one of those says nothing, and dispatching a zero-delta `ScrollPhase::None` for it - // would clear a widget's overscroll and cut short a running bounce. - return None; - } - // Scale wheel detents to a fixed distance each, so slow deliberate clicks and fast spins - // both move proportionally. Decided per axis: a frame can carry detents on one axis and - // only a smooth value on the other, and scaling that second axis by a zero detent count - // would silently drop it. - // - // An axis with no detents keeps its raw value. Compositors pair a detent event with every - // wheel-source axis event — `axis_discrete` is documented as absent only for continuous - // devices — and the seat binds above the v5 that introduced it, so a physical wheel - // always brings one. The fallback is for virtual pointers: `zwlr_virtual_pointer_v1` lets - // a client send a wheel-source axis value with no discrete step, and `wl_pointer.axis` - // defines that value as a "length of vector in surface-local coordinate space" — already - // a distance, with no detent count to recover and no units-per-detent constant that could - // recover one (compositors disagree, and hwdb ships wheels from 10 to 30 degrees a click). - let axis_scroll = |detent: f64, raw: f64| { - if detent != 0.0 { - detent * PIXELS_PER_WHEEL_DETENT - } else { - raw - } - }; - // Finger-driven (touchpad) scrolling reports `Changed` per frame and `Ended` when the - // fingers lift, which is what drives the rubber band at a scroll limit. Every other - // source is a plain delta with no gesture. - // - // Note this yields no kinetic scrolling for Wayland touchpads: widgets start their fling - // on `ScrollPhase::Momentum`, which only macOS emits — there the OS synthesizes that - // stream, while Wayland compositors do not and neither Linux backend fabricates one. - let phase = if !is_finger { - ScrollPhase::None - } else if gesture_ended { - ScrollPhase::Ended - } else { - ScrollPhase::Changed - }; - Some(FrameScroll { - delta: if is_wheel_like { - dvec2(axis_scroll(detents.x, acc.x), axis_scroll(detents.y, acc.y)) - } else { - acc - }, - phase, - is_mouse: is_wheel_like, - }) -} - -fn is_caption_double_click( - previous: Option<(WindowId, Vec2d, u32)>, - window_id: WindowId, - pos: Vec2d, - time: u32, -) -> bool { - previous.is_some_and(|(last_window_id, last_pos, last_time)| { - last_window_id == window_id - && time.wrapping_sub(last_time) <= (TAP_COUNT_TIME * 1000.0) as u32 - && (pos - last_pos).length() < TAP_COUNT_DISTANCE - }) -} - -#[derive(Clone, Copy, Debug)] -struct CaptionPress { - window_id: WindowId, - pos: Vec2d, - time: u32, - serial: u32, - drag_started: bool, -} - -impl CaptionPress { - fn start_drag_if_needed(&mut self, window_id: WindowId, pos: Vec2d) -> Option<(WindowId, u32)> { - if self.drag_started { - return None; - } - if self.window_id != window_id { - self.drag_started = true; - return None; - } - if (pos - self.pos).length() < TAP_COUNT_DISTANCE { - return None; - } - self.drag_started = true; - Some((self.window_id, self.serial)) - } - - fn completed_click(self, window_id: WindowId, pos: Vec2d) -> Option<(WindowId, Vec2d, u32)> { - (self.window_id == window_id - && !self.drag_started - && (pos - self.pos).length() < TAP_COUNT_DISTANCE) - .then_some((self.window_id, self.pos, self.time)) - } -} - -const RESIZE_EDGE_LEFT: u8 = 1 << 0; -const RESIZE_EDGE_RIGHT: u8 = 1 << 1; -const RESIZE_EDGE_TOP: u8 = 1 << 2; -const RESIZE_EDGE_BOTTOM: u8 = 1 << 3; - -fn xdg_toplevel_edge_mask(states: &[u8], first_state: u32) -> u8 { - [ - (first_state, RESIZE_EDGE_LEFT), - (first_state + 1, RESIZE_EDGE_RIGHT), - (first_state + 2, RESIZE_EDGE_TOP), - (first_state + 3, RESIZE_EDGE_BOTTOM), - ] - .into_iter() - .filter_map(|(state, edge)| WaylandState::xdg_toplevel_has_state(states, state).then_some(edge)) - .fold(0, |mask, edge| mask | edge) -} - -fn resize_edge_mask(edge: xdg_toplevel::ResizeEdge) -> u8 { - match edge { - xdg_toplevel::ResizeEdge::Top => RESIZE_EDGE_TOP, - xdg_toplevel::ResizeEdge::Bottom => RESIZE_EDGE_BOTTOM, - xdg_toplevel::ResizeEdge::Left => RESIZE_EDGE_LEFT, - xdg_toplevel::ResizeEdge::TopLeft => RESIZE_EDGE_TOP | RESIZE_EDGE_LEFT, - xdg_toplevel::ResizeEdge::BottomLeft => RESIZE_EDGE_BOTTOM | RESIZE_EDGE_LEFT, - xdg_toplevel::ResizeEdge::Right => RESIZE_EDGE_RIGHT, - xdg_toplevel::ResizeEdge::TopRight => RESIZE_EDGE_TOP | RESIZE_EDGE_RIGHT, - xdg_toplevel::ResizeEdge::BottomRight => RESIZE_EDGE_BOTTOM | RESIZE_EDGE_RIGHT, - _ => 0, - } -} - -fn resize_edge_from_mask(mask: u8) -> Option { - use xdg_toplevel::ResizeEdge; - Some( - match ( - mask & (RESIZE_EDGE_LEFT | RESIZE_EDGE_RIGHT), - mask & (RESIZE_EDGE_TOP | RESIZE_EDGE_BOTTOM), - ) { - (RESIZE_EDGE_LEFT, RESIZE_EDGE_TOP) => ResizeEdge::TopLeft, - (RESIZE_EDGE_LEFT, RESIZE_EDGE_BOTTOM) => ResizeEdge::BottomLeft, - (RESIZE_EDGE_RIGHT, RESIZE_EDGE_TOP) => ResizeEdge::TopRight, - (RESIZE_EDGE_RIGHT, RESIZE_EDGE_BOTTOM) => ResizeEdge::BottomRight, - (RESIZE_EDGE_LEFT, 0) => ResizeEdge::Left, - (RESIZE_EDGE_RIGHT, 0) => ResizeEdge::Right, - (0, RESIZE_EDGE_TOP) => ResizeEdge::Top, - (0, RESIZE_EDGE_BOTTOM) => ResizeEdge::Bottom, - _ => return None, - }, - ) -} - -/// Narrows `edge` to the components the compositor still allows. A tiled window shares -/// its inner borders with a neighbour and cannot resize them, but its outer ones stay -/// free; dropping the whole corner in that case would cost the user a grab they still -/// have, so a corner degrades to whichever of its two edges survives. -pub(crate) fn available_resize_edge( - edge: xdg_toplevel::ResizeEdge, - unavailable: u8, -) -> Option { - resize_edge_from_mask(resize_edge_mask(edge) & !unavailable) -} - -pub(crate) fn resize_edge_cursor( - edge: xdg_toplevel::ResizeEdge, -) -> wp_cursor_shape_device_v1::Shape { - use wp_cursor_shape_device_v1::Shape; - match edge { - xdg_toplevel::ResizeEdge::Top => Shape::NResize, - xdg_toplevel::ResizeEdge::Bottom => Shape::SResize, - xdg_toplevel::ResizeEdge::Left => Shape::WResize, - xdg_toplevel::ResizeEdge::Right => Shape::EResize, - xdg_toplevel::ResizeEdge::TopLeft => Shape::NwResize, - xdg_toplevel::ResizeEdge::TopRight => Shape::NeResize, - xdg_toplevel::ResizeEdge::BottomLeft => Shape::SwResize, - xdg_toplevel::ResizeEdge::BottomRight => Shape::SeResize, - _ => Shape::Default, - } -} - /// State for tracking keyboard key repeat. struct KeyRepeatState { key_code: KeyCode, @@ -324,7 +82,6 @@ struct PendingClipboardRead { pub(crate) struct WaylandState { pub(crate) compositor: Option, - pub(crate) subcompositor: Option, pub(crate) wm_base: Option, pub(crate) seat: Option, pub(crate) shm: Option, @@ -344,19 +101,12 @@ pub(crate) struct WaylandState { pub(crate) pointer: Option, pub(crate) last_mouse_pos: Vec2d, pub(crate) pointer_serial: Option, - pub(crate) pointer_enter_serial: Option, - pub(crate) requested_cursor: MouseCursor, pub(crate) keyboard_serial: Option, pub(crate) decoration_manager: Option, pub(crate) icon_manager: Option, pub(crate) windows: Vec, pub(crate) popups: Vec, pub(crate) pointer_window: Option, - /// Set while the pointer is over a window's shadow gutter rather than the window - /// itself, together with the edge a press there would resize. Kept apart from - /// [`Self::pointer_window`] because the gutter is outside the window: the app must - /// not see hover or clicks at coordinates that fall outside its own surface. - pub(crate) pointer_shadow: Option<(WindowId, xdg_toplevel::ResizeEdge)>, /// The latest un-dispatched pointer motion `(window_id, pos)`, coalesced across a whole /// `dispatch_pending` batch. A high-Hz mouse queues many `wl_pointer` motion+frame pairs between /// paints; dispatching each as a `MouseMove` runs a redundant hover hit-test across the whole @@ -387,9 +137,6 @@ pub(crate) struct WaylandState { Option, pub(crate) primary_selection_text: String, pub(crate) last_resize_edge: Option, - caption_press: Option, - last_caption_click: Option<(WindowId, Vec2d, u32)>, - consumed_pointer_buttons: MouseButton, event_callback: Option>, pub(crate) scroll_accumulator: Vec2d, @@ -397,25 +144,12 @@ pub(crate) struct WaylandState { /// (fractional detents on high-resolution wheels) or `AxisDiscrete` on pre-v8 /// compositors. Same sign convention as `scroll_accumulator`. pub(crate) scroll_detents: Vec2d, - /// The `wl_pointer::AxisSource` reported for the current pointer frame, or `None` - /// when the compositor sent no `AxisSource` event — the event is optional ("If the - /// source is unknown for a particular axis event sequence, no event is sent") and a - /// source value newer than this protocol copy is likewise recorded as `None`. Scoped - /// to one frame, so it resets on every `Frame` and must never be assumed to carry - /// over. See [`scroll_is_wheel_like`]. - pub(crate) scroll_source: Option, - /// Set when `wl_pointer::AxisStop` arrives in the current pointer frame. It ends the - /// gesture — sending that frame's Scroll event with `ScrollPhase::Ended`, which springs - /// a stretched rubber band back and releases the widget's gesture ownership — only on a - /// finger frame that carries no motion of its own. A stop alongside live motion means - /// that one axis stopped while the other continues (see the `frame` event), not lift-off, - /// and a stop from a source that is not a gesture says nothing at all. See - /// [`frame_scroll`]. + pub(crate) scroll_is_wheel: bool, + /// Set when `wl_pointer::AxisStop` arrives in the current pointer frame: the fingers + /// lifted off the touchpad. The frame's Scroll event is then sent with + /// `ScrollPhase::Ended` (even if its delta is zero) so widgets can start their own + /// fling — Wayland compositors do not synthesize momentum scrolling for clients. pub(crate) scroll_stopped: bool, - /// Whether the last dispatched pointer frame was a live touchpad gesture, so that a - /// lift-off frame on which the compositor omitted its `AxisSource` is still recognised - /// as the end of that gesture rather than as an unclassified scroll. - pub(crate) scroll_gesture_active: bool, /// Windows whose last presented frame's `wl_surface::frame` callback has not fired /// yet. While a window is listed here the compositor is not ready for a new frame /// on that surface, so presenting it is skipped (its pass stays dirty). See the @@ -436,7 +170,6 @@ impl WaylandState { pub fn new(event_callback: Box) -> Self { Self { compositor: None, - subcompositor: None, wm_base: None, seat: None, shm: None, @@ -460,12 +193,9 @@ impl WaylandState { windows: Vec::new(), popups: Vec::new(), pointer_window: None, - pointer_shadow: None, pending_motion: None, keyboard_window: None, pointer_serial: None, - pointer_enter_serial: None, - requested_cursor: MouseCursor::Default, keyboard_serial: None, modifiers: KeyModifiers::default(), xkb_state: None, @@ -481,15 +211,11 @@ impl WaylandState { primary_selection_text: String::new(), last_mouse_pos: dvec2(0., 0.), last_resize_edge: None, - caption_press: None, - last_caption_click: None, - consumed_pointer_buttons: MouseButton::empty(), timers: SelectTimers::new(), event_callback: Some(event_callback), scroll_accumulator: dvec2(0.0, 0.0), scroll_detents: dvec2(0.0, 0.0), - scroll_source: None, - scroll_gesture_active: false, + scroll_is_wheel: false, scroll_stopped: false, frame_callbacks_pending: Vec::new(), event_flow: EventFlow::Wait, @@ -529,114 +255,6 @@ impl WaylandState { .map(|win| win.xdg_surface.clone()) }) } - - fn clear_resize_edge(&mut self, force_cursor_update: bool) { - if self.last_resize_edge.take().is_some() || force_cursor_update { - if let (Some(cursor), Some(serial)) = - (self.cursor_shape.as_ref(), self.pointer_enter_serial) - { - cursor.set_shape(serial, self.requested_cursor.into()); - } - } - } - - fn update_resize_edge( - &mut self, - window_id: WindowId, - pos: Vec2d, - force_cursor_update: bool, - ) { - self.last_mouse_pos = pos; - let window_state = self - .windows - .iter() - .find(|window| window.window_id == window_id) - .filter(|window| { - window.uses_client_side_decorations - && !window.is_maximized - && !window.is_fullscreen - // The gutter already owns the grabs, and hit-testing here as well would - // charge every pointer motion near an edge for a whole-widget-tree - // `WindowDragQuery` dispatch that cannot change the answer. - && !window.csd_shadow_gutter_active() - }) - .map(|window| { - ( - window.window_geom.inner_size, - window.unavailable_resize_edges, - ) - }); - // The gutter outside the window is the primary way to resize, so these interior - // bands only need to cover the case where the shadow could not be created and - // there is no gutter to aim at. They stay narrow because every pixel they claim - // is a pixel the app's own widgets do not get. - let mut edge = window_state.and_then(|(size, unavailable)| { - let mut mask = 0; - if pos.x < 10.0 { - mask |= RESIZE_EDGE_LEFT; - } else if pos.x >= size.x - 10.0 { - mask |= RESIZE_EDGE_RIGHT; - } - if pos.y < 10.0 { - mask |= RESIZE_EDGE_TOP; - } else if pos.y >= size.y - 10.0 { - mask |= RESIZE_EDGE_BOTTOM; - } - // Away from a corner the band narrows to 5 px, so a single-axis hit outside - // that has to fall through to the app. - if mask.count_ones() == 1 - && pos.x >= 5.0 - && pos.x < size.x - 5.0 - && pos.y >= 5.0 - && pos.y < size.y - 5.0 - { - return None; - } - resize_edge_from_mask(mask & !unavailable) - }); - if edge.is_some() { - let response = Rc::new(Cell::new(WindowDragQueryResponse::NoAnswer)); - self.do_callback(XlibEvent::WindowDragQuery(WindowDragQueryEvent { - window_id, - abs: pos, - response: response.clone(), - })); - if matches!(response.get(), WindowDragQueryResponse::Client) { - edge = None; - } - } - if let Some(resize_edge) = edge { - self.last_resize_edge = Some(resize_edge); - if let (Some(cursor), Some(serial)) = - (self.cursor_shape.as_ref(), self.pointer_enter_serial) - { - cursor.set_shape(serial, resize_edge_cursor(resize_edge)); - } - } else { - self.clear_resize_edge(force_cursor_update); - } - } - - /// Handles the pointer entering one of a window's shadow surfaces: the pointer is in - /// the gutter, outside the window proper, where the only gesture is a resize. Returns - /// false when `surface` belongs to no shadow, leaving the caller's normal path intact. - fn enter_shadow_gutter(&mut self, surface: &wl_surface::WlSurface) -> bool { - let surface_id = surface.id(); - let Some((window_id, edge, shape)) = self.windows.iter().find_map(|window| { - window - .csd_shadow_resize_for_surface(&surface_id) - .map(|(edge, shape)| (window.window_id, edge, shape)) - }) else { - return false; - }; - self.pointer_shadow = Some((window_id, edge)); - if let (Some(cursor), Some(serial)) = - (self.cursor_shape.as_ref(), self.pointer_enter_serial) - { - cursor.set_shape(serial, shape); - } - true - } } impl Dispatch for WaylandState { @@ -660,19 +278,9 @@ impl Dispatch for WaylandState { wl_registry.bind::(name, 1, qhandle, ()); state.compositor = Some(compositor); } - "wl_subcompositor" => { - let subcompositor = wl_registry - .bind::(name, 1, qhandle, ()); - state.subcompositor = Some(subcompositor); - } "xdg_wm_base" => { let wm_base = - wl_registry.bind::( - name, - version.min(7), - qhandle, - (), - ); + wl_registry.bind::(name, 1, qhandle, ()); state.wm_base = Some(wm_base); } "wl_seat" => { @@ -845,14 +453,7 @@ impl Dispatch for WaylandState { height, states, } => { - let mut geom_change = None; - let mut disable_client_resize = false; - let mut refresh_client_resize = false; - if let Some(window) = state - .windows - .iter_mut() - .find(|win| win.window_id == *window_id) - { + if let Some(window) = state.windows.iter().find(|win| win.window_id == *window_id) { let inner_size = if width > 0 && height > 0 { dvec2(width as f64, height as f64) } else { @@ -862,37 +463,13 @@ impl Dispatch for WaylandState { WaylandState::xdg_toplevel_has_state(&states, 1 /* maximized */); let is_fullscreen = WaylandState::xdg_toplevel_has_state(&states, 2 /* fullscreen */); - let is_active = - WaylandState::xdg_toplevel_has_state(&states, 4 /* activated */); - let tiled_edges = xdg_toplevel_edge_mask(&states, 5 /* tiled_left */); - let constrained_edges = - xdg_toplevel_edge_mask(&states, 10 /* constrained_left */); - let unavailable_resize_edges = tiled_edges | constrained_edges; - let resize_was_disabled = window.is_maximized || window.is_fullscreen; - let resize_edges_changed = - window.unavailable_resize_edges != unavailable_resize_edges; - window.is_maximized = is_maximized; - window.is_fullscreen = is_fullscreen; - window.is_tiled = tiled_edges != 0; - window.is_active = is_active; - window.unavailable_resize_edges = unavailable_resize_edges; - disable_client_resize = is_maximized || is_fullscreen; - // A size change moves the right and bottom bands out from under a - // stationary pointer. Without re-running the hit test the window keeps - // a resize cursor it no longer has an edge for, and the next click is - // swallowed starting a resize from nowhere. - refresh_client_resize = resize_edges_changed - || resize_was_disabled != disable_client_resize - || window.window_geom.inner_size != inner_size; - geom_change = Some(WindowGeomChangeEvent { + state.do_callback(XlibEvent::WindowGeomChange(WindowGeomChangeEvent { window_id: *window_id, old_geom: window.window_geom.clone(), new_geom: WindowGeom { dpi_factor: window.window_geom.dpi_factor, can_fullscreen: false, xr_is_presenting: false, - // Preserve the established Makepad API: on Wayland this - // flag has always represented maximized or fullscreen. is_fullscreen: is_fullscreen || is_maximized, is_topmost: false, position: dvec2(0., 0.), @@ -900,17 +477,7 @@ impl Dispatch for WaylandState { outer_size: inner_size, ..Default::default() }, - }); - } - if let Some(event) = geom_change { - state.do_callback(XlibEvent::WindowGeomChange(event)); - } - if state.pointer_window == Some(*window_id) { - if disable_client_resize { - state.clear_resize_edge(false); - } else if refresh_client_resize { - state.update_resize_edge(*window_id, state.last_mouse_pos, false); - } + })); } } xdg_toplevel::Event::Close => { @@ -924,38 +491,6 @@ impl Dispatch for WaylandState { } } } - -impl Dispatch - for WaylandState -{ - fn event( - state: &mut Self, - _decoration: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, - event: zxdg_toplevel_decoration_v1::Event, - window_id: &WindowId, - _conn: &Connection, - _qhandle: &QueueHandle, - ) { - let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event else { - return; - }; - let uses_client_side_decorations = match mode { - WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => false, - WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) - | WEnum::Value(_) - | WEnum::Unknown(_) => true, - }; - if let Some(window) = state - .windows - .iter_mut() - .find(|window| window.window_id == *window_id) - { - // Decoration state is double-buffered with xdg_surface state. Keep - // only the latest mode and apply it at the matching surface configure. - window.pending_client_side_decorations = Some(uses_client_side_decorations); - } - } -} impl Dispatch for WaylandState { fn event( state: &mut Self, @@ -967,59 +502,21 @@ impl Dispatch for WaylandState { ) { if let xdg_surface::Event::Configure { serial, .. } = event { xdg_surface.ack_configure(serial); - let mut configure_event = None; - let mut clear_resize_edge = false; - let mut update_resize_edge = false; - // Proxy clones are cheap handles and let us initialize CSD shadow - // resources while mutably borrowing the matching window. - let compositor = state.compositor.clone(); - let subcompositor = state.subcompositor.clone(); - let shm = state.shm.clone(); - let viewporter = state.viewporter.clone(); + let mut first_configure_event = None; if let Some(window) = state .windows .iter_mut() .find(|win| win.window_id == *window_id) { - let decoration_changed = - if let Some(uses_csd) = window.pending_client_side_decorations.take() { - if uses_csd == window.uses_client_side_decorations { - false - } else { - if uses_csd { - if let Some(compositor) = compositor.as_ref() { - window.ensure_csd_shadow( - compositor, - subcompositor.as_ref(), - shm.as_ref(), - viewporter.as_ref(), - qhandle, - ); - } - } - clear_resize_edge = !uses_csd; - update_resize_edge = uses_csd; - window.uses_client_side_decorations = uses_csd; - true - } - } else { - false - }; if !window.configured { let mut old_geom = window.window_geom.clone(); old_geom.inner_size = dvec2(0., 0.); old_geom.outer_size = dvec2(0., 0.); - configure_event = Some(WindowGeomChangeEvent { + first_configure_event = Some(WindowGeomChangeEvent { window_id: *window_id, old_geom, new_geom: window.window_geom.clone(), }); - } else if decoration_changed { - configure_event = Some(WindowGeomChangeEvent { - window_id: *window_id, - old_geom: window.window_geom.clone(), - new_geom: window.window_geom.clone(), - }); } window.configured = true; } else if let Some(window) = state @@ -1031,7 +528,7 @@ impl Dispatch for WaylandState { let mut old_geom = window.window_geom.clone(); old_geom.inner_size = dvec2(0., 0.); old_geom.outer_size = dvec2(0., 0.); - configure_event = Some(WindowGeomChangeEvent { + first_configure_event = Some(WindowGeomChangeEvent { window_id: *window_id, old_geom, new_geom: window.window_geom.clone(), @@ -1039,14 +536,9 @@ impl Dispatch for WaylandState { } window.configured = true; } - if let Some(event) = configure_event { + if let Some(event) = first_configure_event { state.do_callback(XlibEvent::WindowGeomChange(event)); } - if clear_resize_edge && state.pointer_window == Some(*window_id) { - state.clear_resize_edge(false); - } else if update_resize_edge && state.pointer_window == Some(*window_id) { - state.update_resize_edge(*window_id, state.last_mouse_pos, false); - } } } } @@ -1633,26 +1125,12 @@ impl Dispatch for WaylandState { wl_pointer::Event::Enter { serial, surface, - surface_x, - surface_y, + surface_x: _, + surface_y: _, } => { state.pointer_serial = Some(serial); - state.pointer_enter_serial = Some(serial); state.flush_pending_clipboard_copy(qhandle, serial); - state.clear_resize_edge(true); - state.pointer_shadow = None; - state.pointer_window = None; - if state.enter_shadow_gutter(&surface) { - return; - } state.pointer_window = state.window_id_for_surface(&surface); - if let Some(window_id) = state.pointer_window { - let pos = dvec2(surface_x as f64, surface_y as f64); - state.last_mouse_pos = pos; - // Deliver the enter position through the normal coalesced motion path so - // stationary pointers establish hover state and the right app cursor. - state.pending_motion = Some((window_id, pos)); - } } wl_pointer::Event::Leave { serial, surface: _ } => { // Dispatch any buffered motion before the pointer leaves, so the final hover @@ -1661,41 +1139,93 @@ impl Dispatch for WaylandState { state.pointer_serial = Some(serial); state.flush_pending_clipboard_copy(qhandle, serial); state.pointer_window = None; - state.pointer_shadow = None; - state.scroll_gesture_active = false; - state.pointer_enter_serial = None; state.last_resize_edge = None; - state.caption_press = None; - state.last_caption_click = None; } wl_pointer::Event::Motion { - time: _, + time, surface_x, surface_y, } => { if let Some(window_id) = state.pointer_window { let pos = dvec2(surface_x as f64, surface_y as f64); state.last_mouse_pos = pos; - let drag_request = state - .caption_press - .as_mut() - .and_then(|press| press.start_drag_if_needed(window_id, pos)); - if let Some((press_window, press_serial)) = drag_request { - state.last_caption_click = None; - if let (Some(seat), Some(window)) = ( - state.seat.as_ref(), - state - .windows - .iter() - .find(|window| window.window_id == press_window), - ) { - window.toplevel._move(seat, press_serial); + + // Edge-resize detection (matches X11 backend thresholds) + let window_size = state + .windows + .iter() + .find(|w| w.window_id == window_id) + .map(|w| w.window_geom.inner_size); + if let Some(ws) = window_size { + let edge = if pos.x < 10.0 && pos.y < 10.0 { + Some(( + xdg_toplevel::ResizeEdge::TopLeft, + wp_cursor_shape_device_v1::Shape::NwResize, + )) + } else if pos.x < 10.0 && pos.y >= ws.y - 10.0 { + Some(( + xdg_toplevel::ResizeEdge::BottomLeft, + wp_cursor_shape_device_v1::Shape::SwResize, + )) + } else if pos.x < 5.0 { + Some(( + xdg_toplevel::ResizeEdge::Left, + wp_cursor_shape_device_v1::Shape::WResize, + )) + } else if pos.x >= ws.x - 10.0 && pos.y < 10.0 { + Some(( + xdg_toplevel::ResizeEdge::TopRight, + wp_cursor_shape_device_v1::Shape::NeResize, + )) + } else if pos.x >= ws.x - 10.0 && pos.y >= ws.y - 10.0 { + Some(( + xdg_toplevel::ResizeEdge::BottomRight, + wp_cursor_shape_device_v1::Shape::SeResize, + )) + } else if pos.x >= ws.x - 5.0 { + Some(( + xdg_toplevel::ResizeEdge::Right, + wp_cursor_shape_device_v1::Shape::EResize, + )) + } else if pos.y < 5.0 { + Some(( + xdg_toplevel::ResizeEdge::Top, + wp_cursor_shape_device_v1::Shape::NResize, + )) + } else if pos.y >= ws.y - 5.0 { + Some(( + xdg_toplevel::ResizeEdge::Bottom, + wp_cursor_shape_device_v1::Shape::SResize, + )) + } else { + None + }; + if let Some((resize_edge, cursor_shape)) = edge { + state.last_resize_edge = Some(resize_edge); + if let (Some(cursor_dev), Some(serial)) = + (state.cursor_shape.as_ref(), state.pointer_serial) + { + cursor_dev.set_shape(serial, cursor_shape); + } + } else { + if state.last_resize_edge.is_some() { + if let (Some(cursor_dev), Some(serial)) = + (state.cursor_shape.as_ref(), state.pointer_serial) + { + cursor_dev.set_shape( + serial, + wp_cursor_shape_device_v1::Shape::Default, + ); + } + } + state.last_resize_edge = None; } } // Buffer this motion instead of dispatching immediately; the latest one is // flushed as a single MouseMove once the whole event batch is drained (or before - // an intervening button/leave). See `flush_pending_motion`. + // an intervening button/leave). The edge-resize cursor above still updates per + // motion so the resize cursor stays responsive. See `flush_pending_motion`. state.pending_motion = Some((window_id, pos)); } } @@ -1713,12 +1243,7 @@ impl Dispatch for WaylandState { // Outside-click popup dismissal: if press lands on a // regular window while popups are open, fire dismiss. if let WEnum::Value(ButtonState::Pressed) = key_state { - // A press in the shadow gutter is as much "outside" as one on the - // window, so it dismisses popups too. - if let Some(win_id) = state.pointer_window.or(state - .pointer_shadow - .map(|(window_id, _)| window_id)) - { + if let Some(win_id) = state.pointer_window { if state.windows.iter().any(|w| w.window_id == win_id) && !state.popups.is_empty() { @@ -1733,47 +1258,26 @@ impl Dispatch for WaylandState { } } } - // In the gutter the only gesture is a resize, and the app is not told about - // it: these coordinates are outside its surface, and the compositor takes - // the pointer grab for the duration of the drag. - if let Some((window_id, resize_edge)) = state.pointer_shadow { - if let (WEnum::Value(ButtonState::Pressed), Some(MouseButton::PRIMARY)) = - (key_state, wayland_type::from_mouse(button)) - { - if let (Some(seat), Some(window)) = ( - state.seat.as_ref(), - state.windows.iter().find(|win| win.window_id == window_id), - ) { - window.toplevel.resize(seat, serial, resize_edge); - } - } - return; - } if let Some(btn) = wayland_type::from_mouse(button) { if let Some(window_id) = state.pointer_window { match key_state { WEnum::Value(ButtonState::Pressed) => { - // A surface can disappear before delivering a release. Do not let - // that stale bit consume the next independent press/release pair. - state.consumed_pointer_buttons.remove(btn); - let previous_caption_click = if btn == MouseButton::PRIMARY { - state.last_caption_click.take() - } else { - state.last_caption_click = None; - None - }; - state.caption_press = None; - if btn == MouseButton::PRIMARY - || btn == MouseButton::SECONDARY - { - let uses_client_side_decorations = state - .windows - .iter() - .find(|win| win.window_id == window_id) - .is_some_and(|win| { - win.uses_client_side_decorations && !win.is_fullscreen - }); - if uses_client_side_decorations { + if btn == MouseButton::PRIMARY { + if state.windows.iter().any(|win| win.window_id == window_id) { + // Edge resize takes priority + if let Some(resize_edge) = state.last_resize_edge.take() { + if let (Some(seat), Some(window)) = ( + state.seat.as_ref(), + state + .windows + .iter() + .find(|win| win.window_id == window_id), + ) { + window.toplevel.resize(seat, serial, resize_edge); + return; + } + } + let response = Rc::new(Cell::new(WindowDragQueryResponse::NoAnswer)); state.do_callback(XlibEvent::WindowDragQuery( @@ -1783,41 +1287,10 @@ impl Dispatch for WaylandState { response: response.clone(), }, )); - let response = response.get(); - // The top resize zone overlaps the caption vertically, but - // caption buttons must keep their full-height click target. - if btn == MouseButton::PRIMARY - && !matches!( - response, - WindowDragQueryResponse::Client - ) - { - if let Some(resize_edge) = state.last_resize_edge { - if let (Some(seat), Some(window)) = ( - state.seat.as_ref(), - state - .windows - .iter() - .find(|win| win.window_id == window_id), - ) { - window - .toplevel - .resize(seat, serial, resize_edge); - state.consumed_pointer_buttons.insert(btn); - return; - } - } - } if matches!( - response, + response.get(), WindowDragQueryResponse::Caption ) { - let is_double_click = is_caption_double_click( - previous_caption_click, - window_id, - state.last_mouse_pos, - time, - ); if let (Some(seat), Some(window)) = ( state.seat.as_ref(), state @@ -1825,33 +1298,7 @@ impl Dispatch for WaylandState { .iter() .find(|win| win.window_id == window_id), ) { - if btn == MouseButton::SECONDARY { - window.toplevel.show_window_menu( - seat, - serial, - state.last_mouse_pos.x as i32, - state.last_mouse_pos.y as i32, - ); - state.consumed_pointer_buttons.insert(btn); - return; - } - if is_double_click { - if window.is_maximized { - window.toplevel.unset_maximized(); - } else { - window.toplevel.set_maximized(); - } - state.consumed_pointer_buttons.insert(btn); - return; - } - state.caption_press = Some(CaptionPress { - window_id, - pos: state.last_mouse_pos, - time, - serial, - drag_started: false, - }); - state.consumed_pointer_buttons.insert(btn); + window.toplevel._move(seat, serial); return; } } @@ -1867,20 +1314,6 @@ impl Dispatch for WaylandState { })) } WEnum::Value(ButtonState::Released) => { - let consumed = state.consumed_pointer_buttons.contains(btn); - if consumed { - state.consumed_pointer_buttons.remove(btn); - } - if btn == MouseButton::PRIMARY { - if let Some(press) = state.caption_press.take() { - state.last_caption_click = - press.completed_click(window_id, state.last_mouse_pos); - return; - } - } - if consumed { - return; - } state.do_callback(XlibEvent::MouseUp(MouseUpEvent { abs: state.last_mouse_pos, button: btn, @@ -1908,116 +1341,118 @@ impl Dispatch for WaylandState { } } } - // Wayland axis values already match Makepad's convention: positive vertical = - // scroll down = viewport moves DOWN. The spec pins the sign in - // wl_pointer::axis_relative_direction, whose `identical` case is fingers moving - // down producing a "vertical_scroll down" axis event; libinput documents the - // same ("the positive direction being down or right"). So pass the values - // through untouched — the compositor has already applied the user's - // natural-scrolling preference to the sign, and negating here would invert both - // settings. Toolkits that do negate (winit, SDL, Chromium) only do so because - // their own convention is inverted; GTK, which shares Makepad's, does not. + // Wayland axis values use motion-event coordinates: positive + // vertical = downward on screen = content slides down = viewport + // moves UP. Makepad's internal convention is positive = viewport + // moves DOWN (matching X11 button mapping and macOS after its + // negation of scrollingDeltaY). Negate to align conventions, + // same as winit does for the same reason. wl_pointer::Event::Axis { time: _, axis, value, } => match axis { WEnum::Value(wl_pointer::Axis::VerticalScroll) => { - state.scroll_accumulator.y += value; + state.scroll_accumulator.y -= value; } WEnum::Value(wl_pointer::Axis::HorizontalScroll) => { - state.scroll_accumulator.x += value; + state.scroll_accumulator.x -= value; } _ => {} }, wl_pointer::Event::AxisSource { axis_source } => { - // A source this protocol copy predates (`AxisSource` is `#[non_exhaustive]`) - // is as good as no source: record `None` rather than letting it fall through - // to the finger branch, which is the one classification that can strand a - // stretched rubber band. - state.scroll_source = match axis_source { - WEnum::Value(source) => Some(source), - WEnum::Unknown(_) => None, - }; + state.scroll_is_wheel = axis_source == WEnum::Value(wl_pointer::AxisSource::Wheel); } wl_pointer::Event::Frame => { - let frame = frame_scroll( - state.scroll_source, - state.scroll_gesture_active, - state.scroll_stopped, - state.scroll_accumulator, - state.scroll_detents, - ); - if let Some(frame) = &frame { - // Tracked whether or not a window is under the pointer, so a gesture that - // starts over one window and lifts over another still terminates. - state.scroll_gesture_active = frame.phase == ScrollPhase::Changed; - } - if let (Some(frame), Some(window_id)) = (frame, state.pointer_window) { - // Deliver any buffered motion first so the Scroll event's hover - // position is current (Button and Leave already do this). - state.flush_pending_motion(); - let time_now = state.time_now(); - state.do_callback(XlibEvent::Scroll(ScrollEvent { - window_id, - scroll: frame.delta, - abs: state.last_mouse_pos, - modifiers: state.modifiers, - is_mouse: frame.is_mouse, - handled_x: Cell::new(false), - handled_y: Cell::new(false), - time: time_now, - phase: frame.phase, - })); + let acc = state.scroll_accumulator; + let detents = state.scroll_detents; + // Dispatch when there is a scroll delta, or when the touchpad gesture just + // ended (AxisStop): the `Ended` event may carry a zero delta but is what lets + // widgets start their fling animation at finger lift-off. + if acc.x != 0.0 + || acc.y != 0.0 + || detents.x != 0.0 + || detents.y != 0.0 + || state.scroll_stopped + { + if let Some(window_id) = state.pointer_window { + // Deliver any buffered motion first so the Scroll event's hover + // position is current (Button and Leave already do this). + state.flush_pending_motion(); + let time_now = state.time_now(); + let scroll = if state.scroll_is_wheel { + if detents.x != 0.0 || detents.y != 0.0 { + // Scale wheel detents to a fixed distance each so slow, + // deliberate clicks and fast spins both move proportionally. + dvec2( + detents.x * PIXELS_PER_WHEEL_DETENT, + detents.y * PIXELS_PER_WHEEL_DETENT, + ) + } else { + // Some compositors send wheel frames without discrete or + // value120 information; the accumulated axis value is + // already a real distance in pixels. + acc + } + } else { + acc + }; + // Wheels have no gesture phases. Finger-driven (touchpad) scrolling + // reports `Changed` per frame and `Ended` when the fingers lift + // (AxisStop), letting widgets run their own momentum fling — + // Wayland compositors do not synthesize momentum for clients. + let phase = if state.scroll_is_wheel { + ScrollPhase::None + } else if state.scroll_stopped { + ScrollPhase::Ended + } else { + ScrollPhase::Changed + }; + state.do_callback(XlibEvent::Scroll(ScrollEvent { + window_id, + scroll, + abs: state.last_mouse_pos, + modifiers: state.modifiers, + is_mouse: state.scroll_is_wheel, + handled_x: Cell::new(false), + handled_y: Cell::new(false), + time: time_now, + phase, + })); + } } state.scroll_accumulator = dvec2(0.0, 0.0); state.scroll_detents = dvec2(0.0, 0.0); - state.scroll_source = None; + state.scroll_is_wheel = false; state.scroll_stopped = false; } - wl_pointer::Event::AxisStop { time: _, axis } => { - // An axis stopped. One flag for the whole frame rather than one per axis: - // `ScrollEvent` carries a single phase for both axes, so a per-axis mask - // could not be expressed anyway. `frame_scroll` separates the two cases the - // protocol defines — "this axis stopped, the other continues" from a real - // lift-off — by whether the frame also carries motion. - if matches!( - axis, - WEnum::Value(wl_pointer::Axis::VerticalScroll) - | WEnum::Value(wl_pointer::Axis::HorizontalScroll) - ) { - state.scroll_stopped = true; - } + wl_pointer::Event::AxisStop { time: _, axis: _ } => { + // Fingers lifted off the touchpad: mark the gesture ended so this pointer + // frame's Scroll event goes out with `ScrollPhase::Ended`. + state.scroll_stopped = true; } - // Wheel detent counts, carrying the same sign convention as the Axis event - // above: the spec states each expresses its direction in terms of the positive - // or negative direction of the same axis, never inverted relative to it. + // Wheel detent counts, negated to match the Axis sign convention above. // AxisDiscrete is only sent by compositors below seat v8; v8+ compositors // send AxisValue120 instead (120 units per detent, fractional detents // allowed for high-resolution wheels), so the two never double-count. wl_pointer::Event::AxisDiscrete { axis, discrete } => match axis { WEnum::Value(wl_pointer::Axis::VerticalScroll) => { - state.scroll_detents.y += discrete as f64; + state.scroll_detents.y -= discrete as f64; } WEnum::Value(wl_pointer::Axis::HorizontalScroll) => { - state.scroll_detents.x += discrete as f64; + state.scroll_detents.x -= discrete as f64; } _ => {} }, wl_pointer::Event::AxisValue120 { axis, value120 } => match axis { WEnum::Value(wl_pointer::Axis::VerticalScroll) => { - state.scroll_detents.y += value120 as f64 / 120.0; + state.scroll_detents.y -= value120 as f64 / 120.0; } WEnum::Value(wl_pointer::Axis::HorizontalScroll) => { - state.scroll_detents.x += value120 as f64 / 120.0; + state.scroll_detents.x -= value120 as f64 / 120.0; } _ => {} }, - // Purely informational: the physical direction of the entity that caused the - // axis event. The axis value itself already reflects the user's natural-scrolling - // setting, so scrolling content must ignore this. It exists for widgets that - // should follow the physical wheel regardless of that setting — the spec's - // example is a volume slider — which Makepad has no plumbing for, so drop it. wl_pointer::Event::AxisRelativeDirection { axis: _, direction: _, @@ -2067,10 +1502,8 @@ delegate_noop!(WaylandState: ignore wl_surface::WlSurface); delegate_noop!(WaylandState: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1); delegate_noop!(WaylandState: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); delegate_noop!(WaylandState: ignore wl_compositor::WlCompositor); -delegate_noop!(WaylandState: ignore wl_region::WlRegion); -delegate_noop!(WaylandState: ignore wl_subcompositor::WlSubcompositor); -delegate_noop!(WaylandState: ignore wl_subsurface::WlSubsurface); delegate_noop!(WaylandState: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); +delegate_noop!(WaylandState: ignore zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1); delegate_noop!(WaylandState: ignore xdg_toplevel_icon_v1::XdgToplevelIconV1); delegate_noop!(WaylandState: ignore wl_shm::WlShm); delegate_noop!(WaylandState: ignore wl_shm_pool::WlShmPool); @@ -2333,7 +1766,6 @@ impl WaylandState { { return; } - self.update_resize_edge(window_id, pos, false); self.do_callback(XlibEvent::MouseMove(MouseMoveEvent { lock_delta: Default::default(), abs: pos, @@ -2429,307 +1861,3 @@ impl WaylandState { self.timers.time_now() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn wheel_like_sources_take_the_detent_path() { - // Wheels and wheel tilts ratchet, whether or not this frame carried detents. - for source in [ - wl_pointer::AxisSource::Wheel, - wl_pointer::AxisSource::WheelTilt, - ] { - assert!(scroll_is_wheel_like(Some(source), true)); - assert!(scroll_is_wheel_like(Some(source), false)); - } - // A touchpad gesture and a trackpoint / button-held scroll are both smooth. - for source in [ - wl_pointer::AxisSource::Finger, - wl_pointer::AxisSource::Continuous, - ] { - assert!(!scroll_is_wheel_like(Some(source), false)); - assert!(!scroll_is_wheel_like(Some(source), true)); - } - } - - #[test] - fn a_frame_without_an_axis_source_is_classified_by_its_detents() { - // `axis_source` is optional, and an unknown value is recorded as `None`. Detents - // then decide, and the sourceless default must not be the finger path. - assert!(scroll_is_wheel_like(None, true)); - assert!(!scroll_is_wheel_like(None, false)); - } - - /// A frame carrying no stop, from a source with no gesture in flight. - fn plain_frame( - source: Option, - acc: Vec2d, - detents: Vec2d, - ) -> Option { - frame_scroll(source, false, false, acc, detents) - } - - #[test] - fn each_axis_chooses_detents_or_raw_pixels_on_its_own() { - // A wheel frame with a detented vertical axis and a smooth horizontal one: scaling - // the horizontal by its zero detent count would drop it entirely. - let frame = plain_frame( - Some(wl_pointer::AxisSource::Wheel), - dvec2(7.5, 15.0), - dvec2(0.0, 1.0), - ) - .expect("a frame with a delta dispatches"); - assert_eq!(frame.delta, dvec2(7.5, PIXELS_PER_WHEEL_DETENT)); - assert!(frame.is_mouse); - assert_eq!(frame.phase, ScrollPhase::None); - } - - #[test] - fn a_wheel_frame_without_detents_keeps_its_raw_distance_unscaled() { - let frame = plain_frame( - Some(wl_pointer::AxisSource::Wheel), - dvec2(0.0, 15.0), - dvec2(0.0, 0.0), - ) - .expect("a frame with a delta dispatches"); - assert_eq!(frame.delta, dvec2(0.0, 15.0)); - } - - #[test] - fn a_sourceless_frame_with_detents_takes_the_wheel_path() { - let frame = plain_frame(None, dvec2(0.0, 15.0), dvec2(0.0, 1.0)) - .expect("a frame with a delta dispatches"); - assert_eq!(frame.delta, dvec2(0.0, PIXELS_PER_WHEEL_DETENT)); - assert!(frame.is_mouse); - assert_eq!(frame.phase, ScrollPhase::None); - } - - #[test] - fn a_touchpad_gesture_reports_changed_then_ended_at_lift_off() { - let moving = plain_frame( - Some(wl_pointer::AxisSource::Finger), - dvec2(0.0, 12.0), - dvec2(0.0, 0.0), - ) - .expect("a frame with a delta dispatches"); - assert_eq!(moving.phase, ScrollPhase::Changed); - assert_eq!(moving.delta, dvec2(0.0, 12.0)); - assert!(!moving.is_mouse); - - // Lift-off: the stops arrive alone, and the zero-delta event is what springs a - // stretched rubber band back. - let lifted = frame_scroll( - Some(wl_pointer::AxisSource::Finger), - true, - true, - dvec2(0.0, 0.0), - dvec2(0.0, 0.0), - ) - .expect("a bare stop ends the gesture"); - assert_eq!(lifted.phase, ScrollPhase::Ended); - assert_eq!(lifted.delta, dvec2(0.0, 0.0)); - } - - #[test] - fn a_stop_alongside_live_motion_is_one_axis_stopping_not_lift_off() { - // The `frame` event defines axis + axis_stop in one frame as "movement in one axis - // has stopped but continues in the other axis". - let frame = frame_scroll( - Some(wl_pointer::AxisSource::Finger), - true, - true, - dvec2(0.0, 12.0), - dvec2(0.0, 0.0), - ) - .expect("a frame with a delta dispatches"); - assert_eq!(frame.phase, ScrollPhase::Changed); - } - - #[test] - fn a_gesture_in_flight_still_ends_when_the_compositor_drops_the_axis_source() { - // `axis_source` is per-frame and optional, so the lift-off frame may carry none. - // Losing the terminator would strand a stretched rubber band. - let frame = frame_scroll(None, true, true, dvec2(0.0, 0.0), dvec2(0.0, 0.0)) - .expect("the in-flight gesture recognises its own lift-off"); - assert_eq!(frame.phase, ScrollPhase::Ended); - - // With no gesture in flight the same frame says nothing and must not dispatch. - assert!(frame_scroll(None, false, true, dvec2(0.0, 0.0), dvec2(0.0, 0.0)).is_none()); - } - - #[test] - fn a_bare_stop_from_a_source_with_no_gesture_dispatches_nothing() { - // Only `Finger` is guaranteed an AxisStop. A zero-delta `ScrollPhase::None` from one - // of the others would clear a widget's overscroll and cut short a running bounce. - for source in [ - wl_pointer::AxisSource::Wheel, - wl_pointer::AxisSource::WheelTilt, - wl_pointer::AxisSource::Continuous, - ] { - assert!( - frame_scroll(Some(source), true, true, dvec2(0.0, 0.0), dvec2(0.0, 0.0)) - .is_none(), - "{source:?} has no gesture to end" - ); - } - } - - #[test] - fn a_trackpoint_scroll_is_a_plain_delta_that_skips_wheel_easing() { - let frame = plain_frame( - Some(wl_pointer::AxisSource::Continuous), - dvec2(0.0, 9.0), - dvec2(0.0, 0.0), - ) - .expect("a frame with a delta dispatches"); - assert_eq!(frame.phase, ScrollPhase::None); - assert_eq!(frame.delta, dvec2(0.0, 9.0)); - assert!(!frame.is_mouse); - } - - #[test] - fn an_empty_frame_dispatches_nothing() { - assert!(plain_frame(None, dvec2(0.0, 0.0), dvec2(0.0, 0.0)).is_none()); - } - - fn encoded_states(states: &[u32]) -> Vec { - states - .iter() - .flat_map(|state| state.to_ne_bytes()) - .collect() - } - - #[test] - fn caption_double_click_requires_matching_window_time_and_position() { - let window = WindowId(2, 1); - let previous = Some((window, dvec2(10.0, 10.0), u32::MAX - 100)); - assert!(is_caption_double_click( - previous, - window, - dvec2(12.0, 11.0), - 50 - )); - assert!(!is_caption_double_click( - previous, - WindowId(3, 1), - dvec2(12.0, 11.0), - 50 - )); - assert!(!is_caption_double_click( - previous, - window, - dvec2(20.0, 10.0), - 50 - )); - assert!(!is_caption_double_click( - previous, - window, - dvec2(12.0, 11.0), - 600 - )); - } - - #[test] - fn caption_press_waits_for_drag_threshold_before_requesting_move() { - let window = WindowId(2, 1); - let mut press = CaptionPress { - window_id: window, - pos: dvec2(10.0, 10.0), - time: 100, - serial: 77, - drag_started: false, - }; - - assert_eq!( - press.start_drag_if_needed(window, dvec2(13.0, 13.0)), - None - ); - assert!(!press.drag_started); - assert_eq!( - press.completed_click(window, dvec2(13.0, 13.0)), - Some((window, dvec2(10.0, 10.0), 100)) - ); - } - - #[test] - fn caption_drag_requests_move_once_and_cannot_complete_as_click() { - let window = WindowId(2, 1); - let mut press = CaptionPress { - window_id: window, - pos: dvec2(10.0, 10.0), - time: 100, - serial: 77, - drag_started: false, - }; - - assert_eq!( - press.start_drag_if_needed(window, dvec2(15.0, 10.0)), - Some((window, 77)) - ); - assert!(press.drag_started); - assert_eq!( - press.start_drag_if_needed(window, dvec2(20.0, 10.0)), - None - ); - assert_eq!(press.completed_click(window, dvec2(10.0, 10.0)), None); - } - - #[test] - fn tiled_and_constrained_states_disable_only_their_resize_edges() { - let states = encoded_states(&[5, 7, 11, 13]); - let tiled = xdg_toplevel_edge_mask(&states, 5); - let constrained = xdg_toplevel_edge_mask(&states, 10); - assert_eq!(tiled, RESIZE_EDGE_LEFT | RESIZE_EDGE_TOP); - assert_eq!(constrained, RESIZE_EDGE_RIGHT | RESIZE_EDGE_BOTTOM); - // A corner whose edges are both free is kept whole. - assert_eq!( - available_resize_edge(xdg_toplevel::ResizeEdge::BottomRight, tiled), - Some(xdg_toplevel::ResizeEdge::BottomRight) - ); - // A corner with one tiled edge degrades to the edge that is still free, - // rather than losing the grab entirely. - assert_eq!( - available_resize_edge(xdg_toplevel::ResizeEdge::TopLeft, constrained), - Some(xdg_toplevel::ResizeEdge::TopLeft) - ); - assert_eq!( - available_resize_edge(xdg_toplevel::ResizeEdge::TopRight, tiled), - Some(xdg_toplevel::ResizeEdge::Right) - ); - assert_eq!( - available_resize_edge(xdg_toplevel::ResizeEdge::BottomLeft, constrained), - Some(xdg_toplevel::ResizeEdge::Left) - ); - // Both components gone means no grab at all. - assert_eq!( - available_resize_edge(xdg_toplevel::ResizeEdge::TopLeft, tiled), - None - ); - assert_eq!( - available_resize_edge(xdg_toplevel::ResizeEdge::Top, tiled), - None - ); - } - - #[test] - fn resize_edge_cursor_matches_every_edge() { - use wp_cursor_shape_device_v1::Shape; - use xdg_toplevel::ResizeEdge; - for (edge, shape) in [ - (ResizeEdge::Top, Shape::NResize), - (ResizeEdge::Bottom, Shape::SResize), - (ResizeEdge::Left, Shape::WResize), - (ResizeEdge::Right, Shape::EResize), - (ResizeEdge::TopLeft, Shape::NwResize), - (ResizeEdge::TopRight, Shape::NeResize), - (ResizeEdge::BottomLeft, Shape::SwResize), - (ResizeEdge::BottomRight, Shape::SeResize), - ] { - assert_eq!(resize_edge_cursor(edge), shape); - // Every edge must survive a round trip through the mask it degrades with. - assert_eq!(available_resize_edge(edge, 0), Some(edge)); - } - } -} diff --git a/platform/src/script/draw.rs b/platform/src/script/draw.rs index 5a0f264ff..054fdf74f 100644 --- a/platform/src/script/draw.rs +++ b/platform/src/script/draw.rs @@ -9,7 +9,6 @@ use crate::window::MacosWindowKind; use crate::window::MacosWindowLevel; use crate::window::ScriptWindowHandle; use crate::window::WindowBackdrop; -use crate::window::WaylandDecorationPreference; use crate::*; pub fn script_mod(vm: &mut ScriptVm) -> ScriptValue { @@ -21,7 +20,6 @@ pub fn script_mod(vm: &mut ScriptVm) -> ScriptValue { set_script_value_to_pod!(vm, draw.DrawPassUniforms); set_script_value_to_api!(vm, draw.MouseCursor); set_script_value_to_api!(vm, draw.WindowBackdrop); - set_script_value_to_api!(vm, draw.WaylandDecorationPreference); set_script_value_to_api!(vm, draw.MacosWindowKind); set_script_value_to_api!(vm, draw.MacosWindowChrome); set_script_value_to_api!(vm, draw.MacosWindowLevel); diff --git a/platform/src/window.rs b/platform/src/window.rs index c5391a44c..922e98792 100644 --- a/platform/src/window.rs +++ b/platform/src/window.rs @@ -50,21 +50,6 @@ pub enum MacosWindowLevel { StatusBar, } -/// The decoration mode a Wayland toplevel asks the compositor to use. -/// -/// `ServerSide` requests compositor decorations, but the compositor may select -/// client-side mode instead. `ClientSide` explicitly uses Makepad's frame. If -/// xdg-decoration is unavailable, Makepad always uses client-side decorations. -/// `MAKEPAD_WAYLAND_DECORATION=client|server` can override it for all windows -/// in a process. `--wayland-decoration=client|server` is also recognized when -/// the application's own argument parser accepts framework arguments. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Script, ScriptHook, Default)] -pub enum WaylandDecorationPreference { - #[default] - ServerSide, - ClientSide, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, Script)] pub struct MacosWindowConfig { #[live] @@ -368,9 +353,6 @@ impl WindowHandle { cxwindow.popup_position = None; cxwindow.popup_size = None; cxwindow.popup_grab_keyboard = true; - cxwindow.wayland_decorations = WaylandDecorationPreference::default(); - cxwindow.uses_client_side_decorations = false; - cxwindow.wayland_is_fullscreen = false; cx.platform_ops .push_back(CxOsOp::CreateWindow(window.window_id())); window @@ -397,9 +379,6 @@ impl WindowHandle { cxwindow.popup_position = Some(position); cxwindow.popup_size = Some(size); cxwindow.popup_grab_keyboard = true; - cxwindow.wayland_decorations = WaylandDecorationPreference::default(); - cxwindow.uses_client_side_decorations = false; - cxwindow.wayland_is_fullscreen = false; cxwindow.popup_grab_keyboard }; cx.platform_ops.push_back(CxOsOp::CreatePopupWindow { @@ -441,10 +420,6 @@ pub struct ScriptWindowHandle { pub backdrop_intensity: f32, #[live(MacosWindowConfig::default())] pub macos: MacosWindowConfig, - /// Wayland only. Server-side decorations are requested by default; set - /// this to `ClientSide` to explicitly use Makepad's window frame. - #[live(WaylandDecorationPreference::ServerSide)] - pub wayland_decorations: WaylandDecorationPreference, /// Optionally override the caption bar height. /// * If `None` (the default), the caption bar's height is based on a system-calculated height /// derived from window chrome button geometry, which will make the window chrome buttons @@ -495,9 +470,6 @@ impl ScriptHook for ScriptWindowHandle { .normalized(); let macos = self.macos.normalized(); cx.windows[window_id].macos = macos; - if !cx.windows[window_id].is_created { - cx.windows[window_id].wayland_decorations = self.wayland_decorations; - } if cx.windows[window_id].window_visuals() != visuals { cx.windows[window_id].transparent = visuals.transparent; cx.windows[window_id].backdrop = visuals.backdrop; @@ -549,19 +521,6 @@ impl WindowHandle { pub fn configure_macos_window(&mut self, cx: &mut Cx, config: MacosWindowConfig) { cx.windows[self.window_id()].macos = config.normalized(); } - - /// Selects the decoration mode requested when a Wayland window is created. - /// This has no effect after the native window has been created. - pub fn configure_wayland_decorations( - &mut self, - cx: &mut Cx, - preference: WaylandDecorationPreference, - ) { - let window = &mut cx.windows[self.window_id()]; - if !window.is_created { - window.wayland_decorations = preference; - } - } pub fn get_inner_size(&self, cx: &Cx) -> Vec2d { cx.windows[self.window_id()].get_inner_size() } @@ -604,17 +563,6 @@ impl WindowHandle { cx.windows[self.window_id()].window_geom.is_fullscreen } - /// Whether this Wayland window is using Makepad-drawn decorations. - pub fn uses_wayland_client_side_decorations(&self, cx: &Cx) -> bool { - cx.windows[self.window_id()].uses_client_side_decorations - } - - /// Whether this Wayland window is in true compositor fullscreen, as - /// distinct from the legacy `is_fullscreen` maximize-or-fullscreen flag. - pub fn is_wayland_fullscreen(&self, cx: &Cx) -> bool { - cx.windows[self.window_id()].wayland_is_fullscreen - } - pub fn xr_is_presenting(&mut self, cx: &mut Cx) -> bool { cx.windows[self.window_id()].window_geom.xr_is_presenting } @@ -735,11 +683,6 @@ pub struct CxWindow { pub backdrop: WindowBackdrop, pub backdrop_intensity: f32, pub macos: MacosWindowConfig, - pub wayland_decorations: WaylandDecorationPreference, - /// Effective Wayland decoration mode selected by the compositor. - pub(crate) uses_client_side_decorations: bool, - /// True compositor fullscreen, kept separate so CSD remains visible when maximized. - pub(crate) wayland_is_fullscreen: bool, } impl Default for CxWindow { @@ -766,9 +709,6 @@ impl Default for CxWindow { backdrop: WindowBackdrop::None, backdrop_intensity: 1.0, macos: MacosWindowConfig::default(), - wayland_decorations: WaylandDecorationPreference::default(), - uses_client_side_decorations: false, - wayland_is_fullscreen: false, } } } @@ -984,32 +924,6 @@ mod tests { assert!(!cx_window.transparent); assert_eq!(cx_window.backdrop, WindowBackdrop::None); assert_eq!(cx_window.backdrop_intensity, 1.0); - assert_eq!( - cx_window.wayland_decorations, - WaylandDecorationPreference::ServerSide - ); - } - - #[test] - fn reused_window_slot_resets_wayland_decoration_state() { - let mut cx = test_cx(); - let first = WindowHandle::new(&mut cx); - let first_id = first.window_id(); - cx.windows[first_id].wayland_decorations = WaylandDecorationPreference::ClientSide; - cx.windows[first_id].uses_client_side_decorations = true; - cx.windows[first_id].wayland_is_fullscreen = true; - drop(first); - - let second = WindowHandle::new(&mut cx); - let second_id = second.window_id(); - assert_eq!(second_id.0, first_id.0); - assert_ne!(second_id.1, first_id.1); - assert_eq!( - cx.windows[second_id].wayland_decorations, - WaylandDecorationPreference::ServerSide - ); - assert!(!cx.windows[second_id].uses_client_side_decorations); - assert!(!cx.windows[second_id].wayland_is_fullscreen); } #[test] @@ -1170,7 +1084,6 @@ mod tests { backdrop: WindowBackdrop::Blur, backdrop_intensity: 0.5, macos: MacosWindowConfig::floating_panel(), - wayland_decorations: WaylandDecorationPreference::ClientSide, caption_bar_height_override: None, }; @@ -1193,10 +1106,6 @@ mod tests { } ); assert_eq!(cx_window.macos, MacosWindowConfig::floating_panel()); - assert_eq!( - cx_window.wayland_decorations, - WaylandDecorationPreference::ClientSide - ); } #[test] diff --git a/tools/cargo_makepad/src/apple/compile.rs b/tools/cargo_makepad/src/apple/compile.rs index a1413a60b..cb9f3ad7e 100644 --- a/tools/cargo_makepad/src/apple/compile.rs +++ b/tools/cargo_makepad/src/apple/compile.rs @@ -236,7 +236,7 @@ pub fn list_profiles()->Result<(), String>{ } */ impl PlistValues { - pub(super) fn to_plist_file(&self, os: AppleOs) -> String { + fn to_plist_file(&self, os: AppleOs) -> String { match os { AppleOs::Tvos => self.to_tvos_plist_file(), AppleOs::Ios => self.to_ios_plist_file(), @@ -701,35 +701,10 @@ pub fn build( apple_target: AppleTarget, ) -> Result { let build_crate = get_build_crate_from_args(args)?; - let cwd = std::env::current_dir().unwrap(); - let info_plist = crate::apple::info_plist::load(&cwd, build_crate, apple_target.os())?; let binary_name = get_package_binary_name(build_crate).unwrap_or_else(|| build_crate.to_string()); - // Capitalize the first letter for the user-visible name (CFBundleDisplayName / - // CFBundleName) so the iOS home-screen icon doesn't show a lowercased crate name, - // while keeping the bundle identifier lowercase so existing provisioning profiles - // still match. - let display_name = { - let mut chars = product.chars(); - match chars.next() { - Some(c) => c.to_uppercase().collect::() + chars.as_str(), - None => String::new(), - } - }; - let plist = PlistValues { - identifier: format!("{org}.{product}").to_string(), - display_name: display_name.clone(), - name: display_name, - executable: binary_name.clone(), - version: "1.0.0".to_string(), - }; - let generated_plist = plist.to_plist_file(apple_target.os()); - let plist_contents = match info_plist { - Some(overrides) => overrides.merge(&generated_plist)?, - None => generated_plist, - }; - + let cwd = std::env::current_dir().unwrap(); let target_dir = cargo_target_dir(&cwd); let target_dir_str = target_dir.to_string_lossy().to_string(); let target_dir_arg = format!("--target-dir={target_dir_str}"); @@ -770,6 +745,25 @@ pub fn build( } shell_env(&rust_env, &cwd, "rustup", &args_out)?; + // alright lets make the .app file with manifest + // Capitalize the first letter for the user-visible name (CFBundleDisplayName / + // CFBundleName) so the iOS home-screen icon doesn't show a lowercased crate name, + // while keeping the bundle identifier lowercase so existing provisioning profiles + // still match. + let display_name = { + let mut chars = product.chars(); + match chars.next() { + Some(c) => c.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } + }; + let plist = PlistValues { + identifier: format!("{org}.{product}").to_string(), + display_name: display_name.clone(), + name: display_name, + executable: binary_name.clone(), + version: "1.0.0".to_string(), + }; let profile = get_profile_from_args(args); let app_dir = target_dir.join(format!( @@ -779,7 +773,7 @@ pub fn build( mkdir(&app_dir)?; let plist_file = app_dir.join("Info.plist"); - write_text(&plist_file, &plist_contents)?; + write_text(&plist_file, &plist.to_plist_file(apple_target.os()))?; if matches!(apple_target.os(), AppleOs::Ios) { match generate_app_icon_xcassets(&app_dir, build_crate) { diff --git a/tools/cargo_makepad/src/apple/info_plist.rs b/tools/cargo_makepad/src/apple/info_plist.rs deleted file mode 100644 index fa11a398b..000000000 --- a/tools/cargo_makepad/src/apple/info_plist.rs +++ /dev/null @@ -1,161 +0,0 @@ -use super::AppleOs; -use makepad_micro_serde::{DeJson, DeJsonErr, DeJsonState, JsonValue}; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; - -#[cfg(test)] -mod tests; - -pub(super) struct InfoPlist { - path: PathBuf, - xml: String, -} - -#[derive(DeJson)] -struct CargoMetadata { - packages: Vec, -} - -#[derive(DeJson)] -struct CargoPackage { - name: String, - version: String, - id: String, - manifest_path: String, - metadata: JsonValue, -} - -/// Load an optional app-owned plist relative to the selected Cargo package, -/// never the invoking workspace directory. iOS and tvOS configure it separately. -pub(super) fn load(cwd: &Path, build_crate: &str, os: AppleOs) -> Result, String> { - // Let Cargo parse the manifest and resolve the selected workspace member. - // A file URL from `cargo pkgid` is not a filesystem path (e.g. `%20`). - let output = Command::new("cargo") - .args(["metadata", "--no-deps", "--format-version", "1"]) - .current_dir(cwd) - .output() - .map_err(|e| format!("Cannot read Cargo metadata for {}: {e}", cwd.join("Cargo.toml").display()))?; - if !output.status.success() { - return Err(format!("Cannot read Cargo metadata for {}: {}", - cwd.join("Cargo.toml").display(), String::from_utf8_lossy(&output.stderr))); - } - let json = std::str::from_utf8(&output.stdout) - .map_err(|e| format!("Cargo metadata is not UTF-8: {e}"))?; - let metadata = CargoMetadata::deserialize_json_lenient(json) - .map_err(|e| format!("Cannot parse Cargo metadata: {e:?}"))?; - let package = metadata.packages.iter().find(|package| { - package.name == build_crate || package.id == build_crate - || format!("{}@{}", package.name, package.version) == build_crate - }).ok_or_else(|| format!("Cargo metadata does not contain package {build_crate}"))?; - let manifest = Path::new(&package.manifest_path); - let crate_dir = manifest.parent() - .ok_or_else(|| format!("Cargo returned an invalid manifest path: {}", manifest.display()))?; - let platform = match os { - AppleOs::Ios => "ios", - AppleOs::Tvos => "tvos", - }; - let key = format!("package.metadata.makepad.{platform}.info_plist"); - let configured = package.metadata.key("makepad") - .and_then(|value| value.key(platform)) - .and_then(|value| value.key("info_plist")); - let path = match configured { - None => return Ok(None), - Some(JsonValue::String(path)) if !path.is_empty() => crate_dir.join(path), - _ => return Err(format!("{key} in {} must be a nonempty path string", manifest.display())), - }; - let xml = read_dictionary(&path) - .map_err(|e| format!("Cannot read custom Info.plist {}: {e}", path.display()))?; - Ok(Some(InfoPlist { path, xml })) -} - -impl InfoPlist { - /// Replace whole top-level values, preserving types and unspecified defaults. - /// Bundle identity and executable stay controlled by the build's CLI inputs: - /// changing them here would disagree with signing and simulator launch paths. - pub(super) fn merge(&self, generated: &str) -> Result { - self.merge_inner(generated) - .map_err(|e| format!("Cannot merge custom Info.plist {}: {e}", self.path.display())) - } - - fn merge_inner(&self, generated: &str) -> Result { - let temporary = TemporaryDirectory::new()?; - let defaults = temporary.0.join("defaults.plist"); - let merged = temporary.0.join("merged.plist"); - fs::write(&defaults, generated.trim_start()) - .map_err(|e| format!("Cannot write generated Info.plist: {e}"))?; - read_dictionary(&defaults)?; - fs::write(&merged, &self.xml) - .map_err(|e| format!("Cannot write temporary Info.plist: {e}"))?; - - // PlistBuddy's Merge adds only missing top-level keys. Starting with the - // app's dictionary therefore preserves whole custom values, including - // nested dictionaries and arrays. Capture its duplicate-key notices. - // Fixed filenames in a private directory avoid interpolating app paths - // into PlistBuddy's command language; the source file is never modified. - run(Command::new("/usr/libexec/PlistBuddy") - .current_dir(&temporary.0) - .args(["-c", "Merge defaults.plist", "merged.plist"]))?; - - for key in ["CFBundleIdentifier", "CFBundleExecutable"] { - // Compare typed, canonical XML so e.g. a boolean cannot pass as a - // string. Check after merging so absent custom keys need no special - // handling, and all command failures remain errors. - let extract = |path: &Path| run(Command::new("/usr/bin/plutil") - .args(["-extract", key, "xml1", "-o", "-", "--"]) - .arg(path)); - if extract(&merged)? != extract(&defaults)? { - return Err(format!( - "cannot change {key}; use the Apple build options and Cargo binary target instead", - )); - } - } - read_dictionary(&merged) - } -} - -/// Apple builds already require macOS and its plist utilities. Let plutil parse -/// both XML and binary input; only inspect its normalized XML to check the root -/// type, without implementing a general XML or binary plist parser. -fn read_dictionary(path: &Path) -> Result { - let xml = run(Command::new("/usr/bin/plutil") - .args(["-convert", "xml1", "-o", "-", "--"]) - .arg(path))?; - let root = xml.split_once("')) - .map(|(_, rest)| rest.trim_start()) - .ok_or_else(|| "plutil did not return a plist document".to_string())?; - if !root.starts_with("") && !root.starts_with("") { - return Err("Info.plist must contain a dictionary".to_string()); - } - Ok(xml) -} - -fn run(command: &mut Command) -> Result { - let output = command.output() - .map_err(|e| format!("Cannot run {}: {e}", command.get_program().to_string_lossy()))?; - if !output.status.success() { - return Err(format!("{} failed ({}): {}{}", - command.get_program().to_string_lossy(), output.status, - String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr))); - } - String::from_utf8(output.stdout).map_err(|e| format!("Tool output is not UTF-8: {e}")) -} - -struct TemporaryDirectory(PathBuf); - -impl TemporaryDirectory { - fn new() -> Result { - // mktemp creates a private directory atomically, including for parallel - // builds. Drop cleans it up on both successful and failed merges. - let path = run(Command::new("/usr/bin/mktemp") - .args(["-d", "-t", "makepad-info-plist"]))?; - Ok(Self(PathBuf::from(path.trim_end_matches('\n')))) - } -} - -impl Drop for TemporaryDirectory { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.0); - } -} diff --git a/tools/cargo_makepad/src/apple/info_plist/tests.rs b/tools/cargo_makepad/src/apple/info_plist/tests.rs deleted file mode 100644 index 8c4118f76..000000000 --- a/tools/cargo_makepad/src/apple/info_plist/tests.rs +++ /dev/null @@ -1,300 +0,0 @@ -#![cfg(target_os = "macos")] - -use super::AppleOs; -use super::super::compile::PlistValues; -use std::{ - fs, - path::{Path, PathBuf}, - process::{Command, Output}, - sync::atomic::{AtomicU64, Ordering}, -}; - -static NEXT_DIRECTORY: AtomicU64 = AtomicU64::new(0); - -struct Package { - root: PathBuf, - path: PathBuf, -} - -impl Package { - fn new(metadata: &str) -> Self { - let root = std::env::temp_dir().join(format!( - "makepad-info-plist-{}-{}-{}", - std::process::id(), - std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos(), - NEXT_DIRECTORY.fetch_add(1, Ordering::Relaxed), - )); - fs::create_dir_all(root.join("workspace/member package/src")).unwrap(); - let root = fs::canonicalize(root).unwrap(); - let package = Self { path: root.join("workspace/member package"), root }; - fs::write(package.path.join("src/main.rs"), "fn main() {}\n").unwrap(); - package.manifest(metadata); - package - } - - fn manifest(&self, metadata: &str) { - fs::write(self.path.join("Cargo.toml"), format!( - "[package]\nname = \"plist-test\"\nversion = \"0.1.0\"\n{metadata}\n" - )).unwrap(); - } - - fn plist(&self, relative: &str, xml: &str, binary: bool) -> PathBuf { - let path = self.path.join(relative); - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(&path, xml.trim_start()).unwrap(); - if binary { - checked_plutil(&path, &["-convert", "binary1"]); - } - path - } -} - -impl Drop for Package { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.root); - } -} - -fn load(path: &Path, os: AppleOs) -> Result, String> { - super::load(path, "plist-test", os) -} - -fn generated(os: AppleOs) -> String { - PlistValues { - identifier: "org.example.plist-test".into(), - display_name: "Plist Test".into(), - name: "Plist Test".into(), - executable: "plist-test".into(), - version: "1.2.3".into(), - }.to_plist_file(os) -} - -fn plist_xml(root: &str) -> String { - format!("\n{root}\n") -} - -fn one_entry(key: &str, value_xml: &str) -> String { - plist_xml(&format!("{key}{value_xml}")) -} - -fn plutil(path: &Path, args: &[&str]) -> Output { - Command::new("/usr/bin/plutil").args(args).arg("--").arg(path).output().unwrap() -} - -fn checked_plutil(path: &Path, args: &[&str]) -> String { - let output = plutil(path, args); - assert!(output.status.success(), "plutil {args:?} {} failed: {}{}", - path.display(), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)); - String::from_utf8(output.stdout).unwrap() -} - -fn raw(path: &Path, key: &str, expected_type: &str) -> String { - checked_plutil(path, &["-extract", key, "raw", "-expect", expected_type, "-o", "-", "-n"]) -} - -fn extracted_xml(path: &Path, key: &str) -> String { - checked_plutil(path, &["-extract", key, "xml1", "-o", "-"]) -} - -fn assert_missing(path: &Path, key: &str) { - assert!(!plutil(path, &["-extract", key, "raw", "-o", "-"]).status.success(), "unexpected key {key}"); -} - -fn assert_path(error: &str, path: &Path) { - assert!(error.contains(path.to_str().unwrap()), "missing path {}: {error}", path.display()); -} - -#[test] -fn no_opt_in_preserves_defaults_without_adding_speech_permission() { - let package = Package::new(""); - for os in [AppleOs::Ios, AppleOs::Tvos] { - assert!(load(&package.path, os).unwrap().is_none()); - let defaults = package.plist("Generated.plist", &generated(os), false); - checked_plutil(&defaults, &["-lint"]); - assert_missing(&defaults, "NSSpeechRecognitionUsageDescription"); - assert_eq!(raw(&defaults, "CFBundleIdentifier", "string"), "org.example.plist-test"); - } -} - -#[test] -fn unrelated_hex_and_exponent_metadata_works_with_and_without_overlay() { - let unrelated = "[package.metadata.unrelated]\nhex_value = 0xdead_beef\nscale = 1.5e+4\n"; - let package = Package::new(unrelated); - assert!(load(&package.path, AppleOs::Ios).unwrap().is_none()); - package.manifest(&format!( - "{unrelated}\n[package.metadata.makepad.ios]\ninfo_plist = \"App Info.plist\"" - )); - package.plist("App Info.plist", &one_entry("CFBundleDisplayName", "Custom App"), false); - let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap(); - let merged = package.plist("Merged.plist", &overlay.merge(&generated(AppleOs::Ios)).unwrap(), false); - assert_eq!(raw(&merged, "CFBundleDisplayName", "string"), "Custom App"); -} - -#[test] -fn selects_each_platform_relative_to_package_with_spaces_in_paths() { - let package = Package::new( - r#"[package.metadata.makepad.ios] -info_plist = '''app metadata/iOS "Bob's".plist''' -[package.metadata.makepad.tvos] -info_plist = '''app metadata/tvOS "Bob's".plist'''"# - ); - for (os, filename, label) in [ - (AppleOs::Ios, "app metadata/iOS \"Bob's\".plist", "Phone App"), - (AppleOs::Tvos, "app metadata/tvOS \"Bob's\".plist", "TV App"), - ] { - let path = package.plist(filename, &one_entry("CFBundleDisplayName", &format!("{label}")), false); - let custom = load(&package.path, os).unwrap().unwrap(); - assert_eq!(custom.path, path); - let merged = package.plist("Merged.plist", &custom.merge(&generated(os)).unwrap(), false); - assert_eq!(raw(&merged, "CFBundleDisplayName", "string"), label); - } -} - -#[test] -fn configuring_one_platform_does_not_opt_in_the_other() { - let package = Package::new( - "[package.metadata.makepad.ios]\ninfo_plist = \"missing-ios.plist\"" - ); - assert!(load(&package.path, AppleOs::Tvos).unwrap().is_none()); - assert!(load(&package.path, AppleOs::Ios).is_err()); - package.manifest("[package.metadata.makepad.tvos]\ninfo_plist = \"missing-tvos.plist\""); - assert!(load(&package.path, AppleOs::Ios).unwrap().is_none()); - assert!(load(&package.path, AppleOs::Tvos).is_err()); -} - -#[test] -fn xml_and_binary_overlays_preserve_types_and_unspecified_defaults() { - let package = Package::new( - "[package.metadata.makepad.ios]\ninfo_plist = \"metadata/Info.plist\"" - ); - let custom = plist_xml(r#" - NSMicrophoneUsageDescriptionRecord the user's "voice" & review <text>. - NSSpeechRecognitionUsageDescriptionTranscribe a message. - LSEnvironmentAPP_SETTINGcustom - AppConfiguration - Enabled - SignedCount-42 - UnsignedCount18446744073709551615 - PayloadAAF/gP8= - Created2026-09-09T12:34:56Z - Itemsone & <two>1.5 - odd: key & <tag>unusual key - - "#); - let source = generated(AppleOs::Ios); - let defaults = package.plist("Generated.plist", &source, false); - assert_eq!(raw(&defaults, "LSEnvironment.RUST_BACKTRACE", "string"), "1"); - for binary in [false, true] { - let path = package.plist("metadata/Info.plist", &custom, binary); - let original = fs::read(&path).unwrap(); - let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap(); - let merged = package.plist("Merged.plist", &overlay.merge(&source).unwrap(), false); - assert_eq!(fs::read(&path).unwrap(), original, "successful merge modified the source plist, binary={binary}"); - checked_plutil(&merged, &["-lint"]); - for (key, value_type, expected) in [ - ("NSMicrophoneUsageDescription", "string", "Record the user's \"voice\" & review ."), - ("NSSpeechRecognitionUsageDescription", "string", "Transcribe a message."), - ("LSEnvironment.APP_SETTING", "string", "custom"), - ("AppConfiguration.Enabled", "bool", "false"), - ("AppConfiguration.SignedCount", "integer", "-42"), - ("AppConfiguration.UnsignedCount", "integer", "18446744073709551615"), - ("AppConfiguration.Payload", "data", "AAF/gP8="), - ("AppConfiguration.Created", "date", "2026-09-09T12:34:56Z"), - ("AppConfiguration.Items", "array", "2"), - ("AppConfiguration.Items.0", "string", "one & "), - ("AppConfiguration.odd: key & ", "string", "unusual key"), - ] { - assert_eq!(raw(&merged, key, value_type), expected, "{key}, binary={binary}"); - } - assert_eq!(raw(&merged, "AppConfiguration.Items.1", "float").parse::().unwrap(), 1.5); - for key in ["CFBundleIdentifier", "CFBundleName", "CFBundleExecutable", "UIDeviceFamily", - "UISupportedInterfaceOrientations~ipad", "UIRequiredDeviceCapabilities", "MinimumOSVersion", - "NSLocationWhenInUseUsageDescription"] { - assert_eq!(extracted_xml(&merged, key), extracted_xml(&defaults, key), "default changed: {key}, binary={binary}"); - } - assert_missing(&merged, "LSEnvironment.RUST_BACKTRACE"); - } -} - -#[test] -fn missing_or_malformed_custom_file_reports_its_path() { - let package = Package::new( - "[package.metadata.makepad.ios]\ninfo_plist = \"missing Info.plist\"" - ); - let path = package.path.join("missing Info.plist"); - let missing = load(&package.path, AppleOs::Ios).err().expect("missing plist must fail"); - assert_path(&missing, &path); - fs::write(&path, b"Broken").unwrap(); - let malformed = load(&package.path, AppleOs::Ios).err().expect("malformed plist must fail"); - assert_path(&malformed, &path); -} - -#[test] -fn non_dictionary_custom_root_is_rejected_in_xml_and_binary() { - let package = Package::new( - "[package.metadata.makepad.ios]\ninfo_plist = \"Info.plist\"" - ); - for binary in [false, true] { - let path = package.plist("Info.plist", &plist_xml("not a dictionary"), binary); - let error = load(&package.path, AppleOs::Ios).err().expect("array root must fail"); - assert_path(&error, &path); - assert!(error.contains("dictionary"), "{error}"); - } -} - -#[test] -fn invalid_metadata_reports_manifest_and_key() { - let package = Package::new(""); - for invalid in ["\"\"", "false", "42", "[]"] { - package.manifest(&format!("[package.metadata.makepad.ios]\ninfo_plist = {invalid}")); - let error = load(&package.path, AppleOs::Ios).err().expect("invalid path metadata must fail"); - assert_path(&error, &package.path.join("Cargo.toml")); - assert!(error.contains("package.metadata.makepad.ios.info_plist"), "{error}"); - } -} - -#[test] -fn missing_or_malformed_manifest_reports_its_path() { - let package = Package::new(""); - let path = package.path.join("Cargo.toml"); - fs::remove_file(&path).unwrap(); - let missing = load(&package.path, AppleOs::Ios).err().expect("missing manifest must fail"); - assert_path(&missing, &path); - fs::write(&path, "[package\n").unwrap(); - let malformed = load(&package.path, AppleOs::Ios).err().expect("malformed manifest must fail"); - assert_path(&malformed, &path); -} - -#[test] -fn rejects_changed_bundle_identity_or_executable_with_path_context() { - let package = Package::new( - "[package.metadata.makepad.ios]\ninfo_plist = \"Info.plist\"" - ); - for key in ["CFBundleIdentifier", "CFBundleExecutable"] { - for replacement in ["different", ""] { - let path = package.plist("Info.plist", &one_entry(key, replacement), false); - let original = fs::read(&path).unwrap(); - let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap(); - let error = overlay.merge(&generated(AppleOs::Ios)).unwrap_err(); - assert_eq!(fs::read(&path).unwrap(), original, "rejected merge modified the source plist"); - assert_path(&error, &path); - assert!(error.contains(key), "{error}"); - } - } -} - -#[test] -fn accepts_unchanged_bundle_identity_and_executable() { - let package = Package::new( - "[package.metadata.makepad.ios]\ninfo_plist = \"Info.plist\"" - ); - let custom = plist_xml("CFBundleIdentifierorg.example.plist-test\ - CFBundleExecutableplist-test\ - CFBundleDisplayNameCustom App"); - package.plist("Info.plist", &custom, false); - let overlay = load(&package.path, AppleOs::Ios).unwrap().unwrap(); - let merged = package.plist("Merged.plist", &overlay.merge(&generated(AppleOs::Ios)).unwrap(), false); - assert_eq!(raw(&merged, "CFBundleIdentifier", "string"), "org.example.plist-test"); - assert_eq!(raw(&merged, "CFBundleExecutable", "string"), "plist-test"); - assert_eq!(raw(&merged, "CFBundleDisplayName", "string"), "Custom App"); -} diff --git a/tools/cargo_makepad/src/apple/mod.rs b/tools/cargo_makepad/src/apple/mod.rs index 056841893..b5cfd6bf4 100644 --- a/tools/cargo_makepad/src/apple/mod.rs +++ b/tools/cargo_makepad/src/apple/mod.rs @@ -1,5 +1,4 @@ mod compile; -mod info_plist; mod sdk; use crate::utils::{get_build_crate_from_args, get_package_binary_name}; use compile::*; diff --git a/tools/cargo_makepad/src/main.rs b/tools/cargo_makepad/src/main.rs index 5d691bcf6..7852e8b52 100644 --- a/tools/cargo_makepad/src/main.rs +++ b/tools/cargo_makepad/src/main.rs @@ -143,12 +143,6 @@ fn show_help() { " --device= The device name to use for signing/provisioning" ); println!(); - println!(" [package.metadata.makepad.ios] (or .tvos) in Cargo.toml:"); - println!(" info_plist = \"packaging/ios/Info.plist\""); - println!(" Optional XML/binary plist dictionary, relative to the package directory."); - println!(" Its top-level keys replace generated defaults before icons and signing."); - println!(" CFBundleIdentifier and CFBundleExecutable must match the generated values."); - println!(); println!("Android commands:"); println!(); println!( diff --git a/widgets/derive_widget/src/lib.rs b/widgets/derive_widget/src/lib.rs index 07880d5e7..c159086df 100644 --- a/widgets/derive_widget/src/lib.rs +++ b/widgets/derive_widget/src/lib.rs @@ -23,7 +23,6 @@ pub fn derive_widget(input: TokenStream) -> TokenStream { area, event, visible, - imperative, action_data, uid, cast, diff --git a/widgets/src/animator.rs b/widgets/src/animator.rs index d78de3e64..a3a8a5fc2 100644 --- a/widgets/src/animator.rs +++ b/widgets/src/animator.rs @@ -254,12 +254,8 @@ struct AnimatorTrack { play: Play, /// The ease function ease: Ease, - /// The target apply object (what we're animating to). - /// Held as a `ScriptObjectRef` for the same reason as `from_snapshot`: a - /// bare `ScriptObject` into the template dangles as soon as `script_mod` - /// re-runs (safe-area inset change, hot reload), and `interpolate_object` - /// walks it every frame. - target_apply: ScriptObjectRef, + /// The target apply object (what we're animating to) + target_apply: ScriptObject, /// The starting values SNAPSHOT (captured/copied when animation begins) /// This is a SEPARATE object from state_object - it must not be mutated during animation /// Uses ScriptObjectRef to prevent GC from freeing it @@ -322,16 +318,6 @@ impl ScriptHook for Animator { let Some(obj) = value.as_object() else { return false; }; - // A `script_mod` re-run replaces every template object an in-flight - // track is animating against. The track's refs keep those objects - // alive, so walking them is safe, but they now describe the previous - // template — and a `Play::Loop` track would pin them for good. Drop - // the tracks instead. `current_states` is kept: the logical state is - // still meaningful, and the next `cut`/`play` re-resolves its objects - // from the new heap. - if apply.follows_script_rerun() { - self.tracks.clear(); - } let obj_ref = vm.bx.heap.new_object_ref(obj); // Minted from the VM we are running in, so this always resolves; the // fallback only exists because the lookup is fallible in general. @@ -376,11 +362,7 @@ impl ScriptApplyDefault for Animator { _scope: &mut Scope, _value: ScriptValue, ) -> Option { - // `follows_script_rerun` rather than `is_live_edit_reload`: what makes - // injecting the current state unsafe is not that the DSL changed, it - // is that `script_mod` re-ran and freed the objects `state_object` and - // `groups` point at. Both `Reload` and `Rebake` re-run it. - if apply.follows_script_rerun() || apply.is_animate() || apply.is_eval() { + if apply.is_live_edit_reload() || apply.is_animate() || apply.is_eval() { return None; } @@ -560,7 +542,7 @@ impl Animator { // The snapshot must be a separate object that won't be mutated during animation. // We sample from state_object (current animated values) or fall back to static state apply. let vm_id = self.vm_id; - let (from_snapshot, target_apply_ref) = cx.with_script_vm_id(vm_id, |vm| { + let from_snapshot = cx.with_script_vm_id(vm_id, |vm| { let snapshot = vm.bx.heap.new_object(); // Get the default state's apply for fallback values @@ -604,12 +586,8 @@ impl Animator { }, ); - // Create ScriptObjectRefs to prevent GC from freeing either object - // out from under the running animation. - ( - vm.bx.heap.new_object_ref(snapshot), - vm.bx.heap.new_object_ref(target_apply), - ) + // Create a ScriptObjectRef to prevent GC from freeing the snapshot + vm.bx.heap.new_object_ref(snapshot) }); // Get the object before moving into track (for return value) @@ -623,7 +601,7 @@ impl Animator { start_time: f64::NEG_INFINITY, play, ease, - target_apply: target_apply_ref, + target_apply, from_snapshot, redraw: target_state.redraw, }; @@ -874,7 +852,7 @@ impl Animator { vm, state_obj, track.from_snapshot.as_object(), - track.target_apply.as_object(), + track.target_apply, mix, ); } diff --git a/widgets/src/desktop_button.rs b/widgets/src/desktop_button.rs index abf16c0a2..fa0c11f8f 100644 --- a/widgets/src/desktop_button.rs +++ b/widgets/src/desktop_button.rs @@ -60,7 +60,10 @@ script_mod! { return sdf.result } DesktopButtonType.WindowsMaxToggled => { - sdf.rect(c.x - sz, c.y - sz, 2. * sz, 2. * sz) + let sz = 5. + sdf.rect(c.x - sz + 1., c.y - sz - 1., 2. * sz, 2. * sz) + sdf.stroke(#f, 0.5 + 0.5 * self.draw_pass.dpi_dilate) + sdf.rect(c.x - sz - 1., c.y - sz + 1., 2. * sz, 2. * sz) sdf.stroke(color, 0.5 + 0.5 * self.draw_pass.dpi_dilate) return sdf.result } diff --git a/widgets/src/drop_down.rs b/widgets/src/drop_down.rs index 87e3ec83f..e3959e2d3 100644 --- a/widgets/src/drop_down.rs +++ b/widgets/src/drop_down.rs @@ -420,7 +420,6 @@ pub struct DropDown { #[rust] popup_global: PopupMenuGlobal, - #[imperative] #[live] selected_item: usize, diff --git a/widgets/src/lib.rs b/widgets/src/lib.rs index 26f637afa..eff4334b6 100644 --- a/widgets/src/lib.rs +++ b/widgets/src/lib.rs @@ -221,7 +221,7 @@ pub use crate::{ WidgetSet, WidgetSetIterator, WidgetUid, }, widget_async::{ - set_splash_theme, set_widget_async_trace, CxSplashVmExt, CxWidgetToScriptCallExt, ScriptAsyncCalls, SplashTheme, + set_widget_async_trace, CxSplashVmExt, CxWidgetToScriptCallExt, ScriptAsyncCalls, ScriptAsyncId, ScriptAsyncResult, SplashVmId, MAIN_SPLASH_VM_ID, }, widget_match_event::WidgetMatchEvent, diff --git a/widgets/src/page_flip.rs b/widgets/src/page_flip.rs index 6f898761b..b61f95ea2 100644 --- a/widgets/src/page_flip.rs +++ b/widgets/src/page_flip.rs @@ -22,7 +22,6 @@ pub struct PageFlip { layout: Layout, #[live(false)] lazy_init: bool, - #[imperative] #[live] active_page: LiveId, #[rust] diff --git a/widgets/src/splash.rs b/widgets/src/splash.rs index b7230f97b..ea4d376a3 100644 --- a/widgets/src/splash.rs +++ b/widgets/src/splash.rs @@ -552,9 +552,6 @@ impl Splash { /// The scope object holding this Splash body's top-level definitions, via /// the body id cached at eval time (with a pointer-identity fallback for /// robustness). - /// The scope the script's top-level names live in: the one its body - /// ended in, since a `let`/`fn` that shadows a prelude name opens a - /// child scope the module scope cannot see into. fn body_scope(&mut self, cx: &mut Cx) -> Option { if self.vm_id == MAIN_SPLASH_VM_ID { return None; @@ -563,14 +560,13 @@ impl Splash { let body_id = self.body_id; cx.with_script_vm_id(self.vm_id, |vm| { let bodies = vm.bx.code.bodies.borrow(); - let ended_in = |body: &ScriptBody| { - body.end_scope.as_ref().unwrap_or(&body.scope).as_object() - }; if let Some(body) = body_id.and_then(|i| bodies.get(i as usize)) { - return Some(ended_in(body)); + return Some(body.scope.as_object()); } bodies.iter().find_map(|body| match &body.source { - ScriptSource::Mod(m) if m.module_path == body_key => Some(ended_in(body)), + ScriptSource::Mod(m) if m.module_path == body_key => { + Some(body.scope.as_object()) + } _ => None, }) }) diff --git a/widgets/src/view.rs b/widgets/src/view.rs index c3e65d8cc..e9f4c5ee4 100644 --- a/widgets/src/view.rs +++ b/widgets/src/view.rs @@ -99,7 +99,6 @@ pub struct View { #[live] event_order: EventOrder, - #[imperative] #[live(true)] pub visible: bool, #[live(false)] @@ -124,15 +123,9 @@ pub struct View { #[live] on_render: ScriptFnRef, - /// `|index|` of the direct child a tap landed on. Lives on the container - /// so `on_render` rows stay closure-free and drag scrolling keeps working. - #[live] - on_item_tap: ScriptFnRef, #[rust] script_async: ScriptAsyncCalls, - #[rust] - item_tap_live: bool, #[rust] scroll_bars_obj: Option>, @@ -927,35 +920,6 @@ impl Widget for View { if let Some(scroll_bars) = &mut self.scroll_bars_obj { scroll_bars.handle_scroll_event(cx, event, scope, &mut Vec::new()); } - - // After the scroll bars so their drag capture comes first; the overload - // lets this view capture the same press alongside it. - if fling_caught { - self.item_tap_live = false; - } else if self.visible && self.on_item_tap.as_object() != ScriptObject::ZERO { - match event.hits_with_capture_overload(cx, self.area(), true) { - Hit::FingerDown(e) => { - // A child that already owns this press (a Button) gets the tap. - self.item_tap_live = !cx.fingers.is_digit_captured_elsewhere(e.digit_id, self.area()); - } - Hit::FingerUp(e) if e.was_tap() && self.item_tap_live => { - self.item_tap_live = false; - // clipped_rect: scrolled and clipped, like the hit test itself. - let index = self.children.iter() - .position(|(_, child)| child.area().clipped_rect(cx).contains(e.abs)); - if let Some(index) = index { - cx.widget_to_script_call( - uid, - NIL, - self.source.clone(), - self.on_item_tap.clone(), - &[(index as f64).into()], - ); - } - } - _ => (), - } - } } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { diff --git a/widgets/src/widget_async.rs b/widgets/src/widget_async.rs index 4ddc094b9..07b2abec6 100644 --- a/widgets/src/widget_async.rs +++ b/widgets/src/widget_async.rs @@ -8,7 +8,7 @@ use { std::any::Any, std::cell::RefCell, std::collections::{HashMap, VecDeque}, - std::sync::atomic::{AtomicU64, AtomicU8, Ordering}, + std::sync::atomic::{AtomicU64, Ordering}, }; static SCRIPT_ASYNC_COUNTER: AtomicU64 = AtomicU64::new(1); @@ -32,30 +32,6 @@ pub struct SplashVmId(pub u64); pub const MAIN_SPLASH_VM_ID: SplashVmId = SplashVmId(0); -/// The widget theme a Splash isolate boots with. A host picks its own theme -/// after `theme_mod`, which an isolate's prelude never sees, so it says here. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -pub enum SplashTheme { - #[default] - Dark, - Light, - Skeleton, -} - -static SPLASH_THEME: AtomicU8 = AtomicU8::new(0); - -pub fn set_splash_theme(theme: SplashTheme) { - SPLASH_THEME.store(theme as u8, Ordering::Relaxed); -} - -fn splash_theme() -> SplashTheme { - match SPLASH_THEME.load(Ordering::Relaxed) { - 1 => SplashTheme::Light, - 2 => SplashTheme::Skeleton, - _ => SplashTheme::Dark, - } -} - thread_local! { /// Splash isolate VMs whose owning `Splash` widget has been dropped, awaiting /// reclamation. `Drop` can't reach `Cx`, so it only records the id here; the @@ -418,17 +394,7 @@ impl CxSplashVmExt for Cx { bx: Box::new(ScriptVmBase::new()), }; crate::makepad_draw::makepad_platform::script::script_mod(&mut vm); - crate::theme_mod(&mut vm); - match splash_theme() { - SplashTheme::Light => { - vm.eval(crate::makepad_script::script! { mod.theme = mod.themes.light }); - } - SplashTheme::Skeleton => { - vm.eval(crate::makepad_script::script! { mod.theme = mod.themes.skeleton }); - } - SplashTheme::Dark => {} - } - crate::widgets_mod(&mut vm); + crate::script_mod(&mut vm); // Splash isolates run untrusted-ish mini-app script; strip the // ambient-authority modules from the isolate's namespace entirely: // filesystem access (`fs`), child processes (`run`), and the resource diff --git a/widgets/src/window.rs b/widgets/src/window.rs index 7bbd660ae..61c25195d 100644 --- a/widgets/src/window.rs +++ b/widgets/src/window.rs @@ -163,7 +163,7 @@ script_mod! { draw_bg.button_type: DesktopButtonType.WindowsMin width: 46 height: 29 draw_bg +: { - color: theme.color_label_inner, color_hover: #000, color_down: #000 + color: #000, color_hover: #000, color_down: #000 bg_color_hover: #E9E9E9, bg_color_down: #CCCCCC } } @@ -171,7 +171,7 @@ script_mod! { draw_bg.button_type: DesktopButtonType.WindowsMax width: 46 height: 29 draw_bg +: { - color: theme.color_label_inner, color_hover: #000, color_down: #000 + color: #000, color_hover: #000, color_down: #000 bg_color_hover: #E9E9E9, bg_color_down: #CCCCCC } } @@ -179,7 +179,7 @@ script_mod! { draw_bg.button_type: DesktopButtonType.WindowsClose width: 46 height: 29 draw_bg +: { - color: theme.color_label_inner, color_hover: #FFF, color_down: #FFF + color: #000, color_hover: #FFF, color_down: #FFF bg_color_hover: #E81123, bg_color_down: #F1707A } } @@ -374,17 +374,13 @@ pub struct Window { /// Used to only emit a platform op when the resolved value actually changes. #[rust] system_bar_dark_icons: Option, - /// Cached `(caption_bar visible, caption rect, buttons rect)` for `WindowDragQuery`. It is - /// refreshed only after layout finishes, so a synchronous native hit-test between configure - /// and redraw cannot preserve rectangles from the previous window size. + /// Cached `(caption_bar visible, caption rect, buttons rect)` for `WindowDragQuery`. That event + /// fires once per `WM_NCHITTEST` — i.e. on every mouse move on Windows — and resolving the + /// views + their areas each time runs widget-tree lookups, a real source of scroll jitter when + /// the mouse is moved during a fling. These only change on relayout, so we recompute lazily and + /// invalidate on `WindowGeomChange`. #[rust] drag_query_cache: Option<(bool, Rect, Rect)>, - /// Whether a completed draw has made this frame's areas authoritative, so a geometry - /// computed now may be cached. Between a configure and the redraw that answers it the - /// areas still describe the previous size, and a query in that window is answered live - /// without being stored. - #[rust] - drag_query_layout_valid: bool, /// The caption-layout inputs (show_caption_bar, height override, system caption height) that /// `drag_query_cache` was last computed against. When they change without a platform /// `WindowGeomChange` (e.g. a live/DSL reload toggling the caption), we drop the cache in @@ -510,54 +506,6 @@ fn gauss_render_texture_y_flip_for_os(os_type: &OsType) -> f32 { } } -fn classify_window_drag_query( - visible: bool, - caption_rect: Rect, - buttons_rect: Rect, - transitional_buttons_rect: Rect, - point: Vec2d, -) -> WindowDragQueryResponse { - if !visible { - return WindowDragQueryResponse::NoAnswer; - } - let hits_buttons = (buttons_rect.size != Vec2d::default() && buttons_rect.contains(point)) - || (transitional_buttons_rect.size != Vec2d::default() - && transitional_buttons_rect.contains(point)); - if hits_buttons { - WindowDragQueryResponse::Client - } else if caption_rect.contains(point) { - WindowDragQueryResponse::Caption - } else { - WindowDragQueryResponse::NoAnswer - } -} - -fn configured_window_buttons_rect(buttons_rect: Rect, configured_rect: Rect) -> Rect { - if buttons_rect.size == Vec2d::default() || configured_rect.size == Vec2d::default() { - return configured_rect; - } - Rect { - pos: dvec2( - configured_rect.pos.x + configured_rect.size.x - buttons_rect.size.x, - buttons_rect.pos.y, - ), - size: buttons_rect.size, - } -} - -fn configured_window_caption_rect(caption_rect: Rect, configured_size: Vec2d) -> Rect { - if caption_rect.size == Vec2d::default() || configured_size == Vec2d::default() { - return caption_rect; - } - Rect { - pos: caption_rect.pos, - size: dvec2( - (configured_size.x - caption_rect.pos.x).max(0.0), - caption_rect.size.y, - ), - } -} - impl GaussStack { fn new(cx: &mut Cx) -> Self { let scene_pass = DrawPass::new_with_name(cx, "gauss_scene"); @@ -1036,20 +984,15 @@ impl Window { .set_visible(cx, self.show_caption_bar && !is_fullscreen); } OsType::LinuxWindow(params) => { - // X11 uses WM decorations. Wayland decides per window from the - // compositor's xdg-decoration configure event. - let custom_chrome = params.custom_window_chrome - && self - .window - .handle - .uses_wayland_client_side_decorations(cx); - let visible = self.show_caption_bar - && custom_chrome - && !self.window.handle.is_wayland_fullscreen(cx); + // Only show the caption bar if we're drawing our own window chrome + // (e.g. Wayland without server-side decorations). On X11 the WM + // provides native decorations, so we hide the in-app caption bar. + let custom_chrome = params.custom_window_chrome; self.view(cx, ids!(caption_bar)) - .set_visible(cx, visible); - self.view(cx, ids!(windows_buttons)) - .set_visible(cx, visible); + .set_visible(cx, self.show_caption_bar && custom_chrome); + if custom_chrome { + self.view(cx, ids!(windows_buttons)).set_visible(cx, true); + } } OsType::LinuxDirect | OsType::Android(_) => { //self.frame.get_view(ids!(caption_bar)).set_visible(false); @@ -1061,21 +1004,6 @@ impl Window { } } - fn caption_drag_geometry(&self, cx: &mut Cx) -> (bool, Rect, Rect) { - // Each `self.view` is a widget-tree walk, so the caption bar is resolved once - // rather than once per field read. - let caption = self.view(cx, ids!(caption_bar)); - let visible = caption.visible(); - let caption_rect = caption.area().rect(cx); - let buttons = self.view(cx, ids!(windows_buttons)); - let buttons_rect = if buttons.visible() { - buttons.area().rect(cx) - } else { - Rect::default() - }; - (visible, caption_rect, buttons_rect) - } - fn sync_caption_bar_height(&mut self, cx: &mut Cx) { // Explicit DSL override takes priority, then system-calculated. let height = self @@ -1189,7 +1117,6 @@ impl Window { if self.caption_query_sig != Some(caption_sig) { self.caption_query_sig = Some(caption_sig); self.drag_query_cache = None; - self.drag_query_layout_valid = false; } self.sync_caption_bar_state(cx); @@ -1386,14 +1313,6 @@ impl Window { cx.end_pass_sized_turtle(); - // Areas are authoritative only after this frame's layout has completed, so this is - // where a cached answer becomes allowed. Computing it here instead would charge - // every frame for three widget-tree walks that only a drag query ever reads, and - // most frames never see one. Dropping last frame's answer rather than keeping it - // means the first query after any relayout still recomputes, whether or not the - // relayout was one of the two that invalidate explicitly. - self.drag_query_cache = None; - self.drag_query_layout_valid = true; self.main_draw_list.end(cx); cx.end_pass(&self.pass.handle); } @@ -1423,16 +1342,6 @@ impl Window { self.window.handle.configure_macos_window(cx, config); } - pub fn configure_wayland_decorations( - &mut self, - cx: &mut Cx, - preference: WaylandDecorationPreference, - ) { - self.window - .handle - .configure_wayland_decorations(cx, preference); - } - pub fn window_index(&self) -> usize { self.window.handle.window_id().id() } @@ -1454,66 +1363,6 @@ mod tests { 1.0 ); } - - #[test] - fn native_button_geometry_wins_during_configure_to_draw_transition() { - let stale_caption = Rect { - pos: dvec2(0.0, 0.0), - size: dvec2(800.0, 29.0), - }; - let stale_buttons = Rect { - pos: dvec2(662.0, 0.0), - size: dvec2(138.0, 29.0), - }; - let configured_buttons = Rect { - pos: dvec2(1782.0, 0.0), - size: dvec2(138.0, 29.0), - }; - assert!(matches!( - classify_window_drag_query( - true, - stale_caption, - stale_buttons, - configured_buttons, - dvec2(1851.0, 14.0), - ), - WindowDragQueryResponse::Client - )); - assert!(matches!( - classify_window_drag_query( - true, - stale_caption, - stale_buttons, - Rect::default(), - dvec2(400.0, 14.0), - ), - WindowDragQueryResponse::Caption - )); - - let zoomed_buttons = Rect { - pos: dvec2(708.0, 0.0), - size: dvec2(92.0, 19.0), - }; - assert_eq!( - configured_window_buttons_rect(zoomed_buttons, configured_buttons), - Rect { - pos: dvec2(1828.0, 0.0), - size: dvec2(92.0, 19.0), - } - ); - let configured_caption = - configured_window_caption_rect(stale_caption, dvec2(1920.0, 1080.0)); - assert!(matches!( - classify_window_drag_query( - true, - configured_caption, - stale_buttons, - configured_buttons, - dvec2(1200.0, 14.0), - ), - WindowDragQueryResponse::Caption - )); - } } impl WindowRef { @@ -1629,16 +1478,6 @@ impl WindowRef { inner.configure_macos_window(cx, config); } } - - pub fn configure_wayland_decorations( - &self, - cx: &mut Cx, - preference: WaylandDecorationPreference, - ) { - if let Some(mut inner) = self.borrow_mut() { - inner.configure_wayland_decorations(cx, preference); - } - } } impl Widget for Window { @@ -1699,10 +1538,8 @@ impl Widget for Window { Event::WindowGeomChange(ev) => { if ev.window_id == self.window.window_id() { // The caption / buttons may have been re-laid-out; drop the WindowDragQuery - // geometry cache so it is recomputed on the next hit-test, and mark the - // areas non-authoritative until the redraw that answers this configure. + // geometry cache so it is recomputed on the next hit-test. self.drag_query_cache = None; - self.drag_query_layout_valid = false; match cx.os_type() { OsType::Windows | OsType::Macos => { if self.hide_caption_on_fullscreen && !cx.in_makepad_studio() { @@ -1763,51 +1600,42 @@ impl Widget for Window { } Event::WindowDragQuery(dq) => { if dq.window_id == self.window.window_id() { - // A native query can arrive synchronously after configure but before redraw. - // Use live areas for that one query, but only a completed draw may cache them. - let cache_ready = self.drag_query_cache.is_some() || self.drag_query_layout_valid; - let geometry = match self.drag_query_cache { - Some(cached) => cached, + // Resolve the caption / buttons geometry at most once per relayout; this event + // arrives per mouse-move (per WM_NCHITTEST) and the view lookups are not free. + let (visible, caption_rect, buttons_rect) = match self.drag_query_cache { + Some(c) => c, None => { - let live = self.caption_drag_geometry(cx); - if self.drag_query_layout_valid { - self.drag_query_cache = Some(live); + let visible = self.view(cx, ids!(caption_bar)).visible(); + let caption_rect = self.view(cx, ids!(caption_bar)).area().rect(cx); + let buttons_view = self.view(cx, ids!(windows_buttons)); + let buttons_visible = buttons_view.visible(); + let buttons_rect = buttons_view.area().rect(cx); + // Only cache once the caption bar AND its (visible) buttons have actually + // been laid out, so an early query doesn't pin a stale rect. Pinning a + // zero buttons_rect while the buttons are visible-but-not-yet-laid-out + // would make the min/max/close strip respond as draggable Caption (a + // click on Close would drag the window) until the next geometry change. A + // window with no (hidden) buttons keeps a zero buttons_rect, which is fine. + let caption_ready = caption_rect.size != Vec2d::default(); + let buttons_ready = + !buttons_visible || buttons_rect.size != Vec2d::default(); + if !visible || (caption_ready && buttons_ready) { + self.drag_query_cache = Some((visible, caption_rect, buttons_rect)); } - live + (visible, caption_rect, buttons_rect) } }; - let (visible, mut caption_rect, buttons_rect) = geometry; - if !cache_ready { - caption_rect = configured_window_caption_rect( - caption_rect, - cx.windows[dq.window_id].window_geom.inner_size, - ); - } - let transitional_buttons = if cache_ready { - Rect::default() - } else { - configured_window_buttons_rect( - buttons_rect, - cx.windows[dq.window_id].window_geom.window_chrome_buttons, - ) - }; - match classify_window_drag_query( - visible, - caption_rect, - buttons_rect, - transitional_buttons, - dq.abs, - ) { - WindowDragQueryResponse::Client => { - // Button geometry wins even if the stale caption rect still has the - // previous width, and therefore also blocks native top-edge resize. - dq.response.set(WindowDragQueryResponse::Client); - } - WindowDragQueryResponse::Caption => { - dq.response.set(WindowDragQueryResponse::Caption); + if visible { + if caption_rect.contains(dq.abs) { + if buttons_rect.size != Vec2d::default() + && buttons_rect.contains(dq.abs) + { + dq.response.set(WindowDragQueryResponse::Client); + } else { + dq.response.set(WindowDragQueryResponse::Caption); + } cx.set_cursor(MouseCursor::Default); } - WindowDragQueryResponse::NoAnswer | WindowDragQueryResponse::SysMenu => {} } } true