diff options
Diffstat (limited to 'crates/core/src/window.rs')
| -rw-r--r-- | crates/core/src/window.rs | 135 |
1 files changed, 102 insertions, 33 deletions
diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs index a3d48a2..ee9fd23 100644 --- a/crates/core/src/window.rs +++ b/crates/core/src/window.rs @@ -182,6 +182,21 @@ pub struct Window { /// lights - see `hit_test`'s and `decoration::render_titlebar`'s own /// use of this for what actually changes. pub is_dialog: bool, + /// Whether the client says it can actually be resized - `false` when + /// it pinned its minimum and maximum size to the same value (a native + /// `xdg_toplevel`'s `set_min_size`/`set_max_size`, or an XWayland + /// window's ICCCM size hints). Backend-set on every decoration redraw, + /// same as `is_dialog` above, for the same reason: `core` has no + /// protocol of its own to read it from. Defaults to `true`, so a + /// client that never declares limits - the common case - is treated + /// as resizable. + /// + /// Consumed by the `dynamic` titlebar button mode: a window that + /// cannot be resized cannot meaningfully be maximized either, so + /// offering the button is offering a no-op. Every mainstream desktop + /// (GNOME, KDE, Windows) hides or disables it in exactly this case. + /// Asked for as titlebars with "buttons of the program/dynamic". + pub resizable: bool, /// `decorated`'s value from just before entering fullscreen, restored /// on exit - see `WindowManager::toggle_fullscreen`'s doc comment on /// why this can't just hardcode `true` back. @@ -286,6 +301,7 @@ impl Window { restore_geometry: None, decorated: true, is_dialog: false, + resizable: true, restore_decorated: None, floating: false, minimized: false, @@ -503,6 +519,12 @@ impl ResizeEdge { // other" trap every other button-geometry value here already has // to avoid. is_dialog: bool, + // Whether a Maximize button is shown at all - `theme. + // dynamic_buttons && !window.resizable` resolves to `false`. Same + // "must stay in exact agreement with `decoration::render_titlebar`" + // contract as `is_dialog` directly above, and for the same reason: + // the two sides compute button slots independently. + show_maximize: bool, ) -> Option<TitlebarHit> { // Border strips render *outside* `frame` (`decoration:: // border_strips`, `border_width` pixels past each edge) - without @@ -579,10 +601,20 @@ impl ResizeEdge { [TitlebarButton::Close, TitlebarButton::Maximize, TitlebarButton::Minimize] }) }; + // Maximize dropped from the slot list entirely rather than + // left in place and ignored: leaving a hole would put a dead + // gap between the two remaining buttons, and the renderer + // closes the gap, so hit-testing has to close it identically + // or every button after it is offset by one slot. + let order: Vec<TitlebarButton> = if show_maximize { + order.to_vec() + } else { + order.iter().copied().filter(|b| *b != TitlebarButton::Maximize).collect() + }; // A dialog only ever recognizes the first slot, matching // `decoration::render_titlebar` only ever drawing the one // button there too. - let button_count = if is_dialog { 1 } else { 3 }; + let button_count = if is_dialog { 1 } else { order.len() }; if buttons_left { let left = frame.x + BUTTON_CLUSTER_MARGIN as i32; // `x >= left` excludes the dead `BUTTON_CLUSTER_MARGIN` @@ -1000,14 +1032,14 @@ mod tests { // margin is a real dead strip now (see its own doc comment) - a // point only `5` in from the raw edge landed inside it, not on the // button. - let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Close)); } #[test] fn maximize_is_left_of_close() { let f = frame(); - let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Maximize)); } @@ -1023,12 +1055,12 @@ mod tests { // undecorated windows. let f = frame(); let maximize_spot = f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 - 5; - let hit = ResizeEdge::hit_test(f, maximize_spot, f.y + 5, true, 0, RESIZE_MARGIN, false, None, true); + let hit = ResizeEdge::hit_test(f, maximize_spot, f.y + 5, true, 0, RESIZE_MARGIN, false, None, true, true); assert_ne!(hit, Some(TitlebarHit::Maximize), "a dialog must not have a Maximize hit zone at all"); assert_ne!(hit, Some(TitlebarHit::Minimize), "a dialog must not have a Minimize hit zone at all"); // The one real button (Close) must still be exactly where it always // is - `is_dialog` removes the other two, not shifts this one. - let close_hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, None, true); + let close_hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, None, true, true); assert_eq!(close_hit, Some(TitlebarHit::Close)); } @@ -1041,7 +1073,7 @@ mod tests { // it get used. let f = frame(); let order = [TitlebarButton::Maximize, TitlebarButton::Minimize, TitlebarButton::Close]; - let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, Some(order), true); + let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, Some(order), true, true); assert_eq!(hit, Some(TitlebarHit::Close)); } @@ -1053,7 +1085,7 @@ mod tests { // not just what gets drawn. let f = frame(); let order = [TitlebarButton::Close, TitlebarButton::Minimize, TitlebarButton::Maximize]; - let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, Some(order), false); + let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, Some(order), false, true); assert_eq!(hit, Some(TitlebarHit::Minimize)); } @@ -1066,8 +1098,8 @@ mod tests { // on. let f = frame(); let order = [TitlebarButton::Maximize, TitlebarButton::Minimize, TitlebarButton::Close]; - let left_hit = ResizeEdge::hit_test(f, f.x + BUTTON_CLUSTER_MARGIN as i32 + 5, f.y + 5, true, 0, RESIZE_MARGIN, true, Some(order), false); - let right_hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, Some(order), false); + let left_hit = ResizeEdge::hit_test(f, f.x + BUTTON_CLUSTER_MARGIN as i32 + 5, f.y + 5, true, 0, RESIZE_MARGIN, true, Some(order), false, true); + let right_hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 5, f.y + 5, true, 0, RESIZE_MARGIN, false, Some(order), false, true); assert_eq!(left_hit, Some(TitlebarHit::Maximize)); assert_eq!(right_hit, Some(TitlebarHit::Maximize)); } @@ -1081,17 +1113,54 @@ mod tests { // of_titlebar_resizes_not_drags` for that zone's own coverage. let f = frame(); let (cx, _) = f.center(); - let hit = ResizeEdge::hit_test(f, cx, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, cx, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Drag)); } #[test] fn bottom_right_corner_is_resize() { let f = frame(); - let hit = ResizeEdge::hit_test(f, f.right() - 1, f.bottom() - 1, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - 1, f.bottom() - 1, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Resize(ResizeEdge::BottomRight))); } + #[test] + fn a_non_resizable_window_has_no_maximize_button_to_click() { + // `show_maximize = false` - the dynamic button mode's one rule. + let f = Rect::new(0, 0, 400, 300); + // Buttons on the right, default order Close, Maximize, Minimize. + // Slot 0 is Close either way. + let slot = |i: i32| f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 * i - 1; + let hit = |x: i32, show_max: bool| ResizeEdge::hit_test(f, x, f.y + TITLEBAR_HEIGHT as i32 / 2, true, 0, RESIZE_MARGIN, false, None, false, show_max); + + assert_eq!(hit(slot(1), true), Some(TitlebarHit::Maximize), "resizable: slot 1 is Maximize"); + // With Maximize dropped, Minimize moves up into slot 1 - it must + // not leave a dead gap there. + assert_eq!(hit(slot(1), false), Some(TitlebarHit::Minimize), "non-resizable: Minimize closes the gap"); + assert_eq!(hit(slot(0), false), Some(TitlebarHit::Close), "Close stays put"); + } + + #[test] + fn a_non_resizable_window_still_has_exactly_two_buttons() { + let f = Rect::new(0, 0, 400, 300); + let slot = |i: i32| f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 * i - 1; + let hit = |x: i32| ResizeEdge::hit_test(f, x, f.y + TITLEBAR_HEIGHT as i32 / 2, true, 0, RESIZE_MARGIN, false, None, false, false); + // Slot 2 held Minimize when there were three; with two it is past + // the cluster and must be a drag, not a phantom third button. + assert_eq!(hit(slot(2)), Some(TitlebarHit::Drag), "no third button exists any more"); + } + + #[test] + fn a_dialog_is_close_only_regardless_of_the_dynamic_button_mode() { + let f = Rect::new(0, 0, 400, 300); + let slot = |i: i32| f.right() - BUTTON_CLUSTER_MARGIN as i32 - BUTTON_PITCH as i32 * i - 1; + for show_max in [true, false] { + let hit = |x: i32| ResizeEdge::hit_test(f, x, f.y + TITLEBAR_HEIGHT as i32 / 2, true, 0, RESIZE_MARGIN, false, None, true, show_max); + assert_eq!(hit(slot(0)), Some(TitlebarHit::Close), "show_maximize={show_max}"); + assert_eq!(hit(slot(1)), Some(TitlebarHit::Drag), "a dialog has no second button, show_maximize={show_max}"); + } + } + /// The bug this guards against: an undecorated window's own content in /// its top `TITLEBAR_HEIGHT` band (Firefox's tab strip/URL bar, once /// `decorated = false` actually applies to it) was silently swallowed @@ -1105,7 +1174,7 @@ mod tests { // outside RESIZE_MARGIN, so a real resize edge can't also explain a // `None` here - undecorated, this must not be treated as // decoration (or a resize edge) at all, just plain content. - let hit = ResizeEdge::hit_test(f, cx, f.y + 20, false, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, cx, f.y + 20, false, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, None); } @@ -1113,7 +1182,7 @@ mod tests { fn undecorated_window_still_resizes_from_every_edge_including_top() { let f = frame(); let (cx, _) = f.center(); - let hit = ResizeEdge::hit_test(f, cx, f.y + 1, false, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, cx, f.y + 1, false, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Resize(ResizeEdge::Top))); } @@ -1133,9 +1202,9 @@ mod tests { // resize zone at all, which is no longer true. let f = frame(); let (cx, _) = f.center(); - assert_eq!(ResizeEdge::hit_test(f, cx, f.y + 5, false, 0, RESIZE_MARGIN, false, None, false), None, "5px in: past the narrow undecorated band, must reach the client"); + assert_eq!(ResizeEdge::hit_test(f, cx, f.y + 5, false, 0, RESIZE_MARGIN, false, None, false, true), None, "5px in: past the narrow undecorated band, must reach the client"); assert_eq!( - ResizeEdge::hit_test(f, cx, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false), + ResizeEdge::hit_test(f, cx, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false, true), Some(TitlebarHit::Drag), "decorated: past its own (wider) top resize margin, still plain drag" ); @@ -1151,7 +1220,7 @@ mod tests { // within the old, decorated-window-sized corner zone // (`CORNER_MARGIN * RESIZE_MARGIN` = 18px) before this fix. let f = frame(); - let hit = ResizeEdge::hit_test(f, f.right() - 10, f.y + 10, false, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - 10, f.y + 10, false, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, None, "past the narrow undecorated corner margin on both axes, must reach the client"); } @@ -1161,7 +1230,7 @@ mod tests { // content near - nothing about `CORNER_MARGIN`'s widening should // survive for an undecorated window at any corner. let f = frame(); - let hit = ResizeEdge::hit_test(f, f.right() - 10, f.bottom() - 10, false, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - 10, f.bottom() - 10, false, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, None, "past the narrow undecorated corner margin on both axes, must reach the client"); } @@ -1174,14 +1243,14 @@ mod tests { let f = frame(); let (_, cy) = f.center(); let (cx, _) = f.center(); - assert_eq!(ResizeEdge::hit_test(f, f.right() - 1, cy, false, 0, RESIZE_MARGIN, false, None, false), Some(TitlebarHit::Resize(ResizeEdge::Right))); - assert_eq!(ResizeEdge::hit_test(f, cx, f.bottom() - 1, false, 0, RESIZE_MARGIN, false, None, false), Some(TitlebarHit::Resize(ResizeEdge::Bottom))); + assert_eq!(ResizeEdge::hit_test(f, f.right() - 1, cy, false, 0, RESIZE_MARGIN, false, None, false, true), Some(TitlebarHit::Resize(ResizeEdge::Right))); + assert_eq!(ResizeEdge::hit_test(f, cx, f.bottom() - 1, false, 0, RESIZE_MARGIN, false, None, false, true), Some(TitlebarHit::Resize(ResizeEdge::Bottom))); } #[test] fn outside_frame_is_none() { let f = frame(); - assert_eq!(ResizeEdge::hit_test(f, 0, 0, true, 0, RESIZE_MARGIN, false, None, false), None); + assert_eq!(ResizeEdge::hit_test(f, 0, 0, true, 0, RESIZE_MARGIN, false, None, false, true), None); } #[test] @@ -1196,16 +1265,16 @@ mod tests { let border_width = 2; // One pixel into the border strip, past the left edge. let x = f.x - 1; - assert_eq!(ResizeEdge::hit_test(f, x, cy, true, 0, RESIZE_MARGIN, false, None, false), None, "sanity check: with no border, this point really is outside the window"); + assert_eq!(ResizeEdge::hit_test(f, x, cy, true, 0, RESIZE_MARGIN, false, None, false, true), None, "sanity check: with no border, this point really is outside the window"); assert_eq!( - ResizeEdge::hit_test(f, x, cy, true, border_width, RESIZE_MARGIN, false, None, false), + ResizeEdge::hit_test(f, x, cy, true, border_width, RESIZE_MARGIN, false, None, false, true), Some(TitlebarHit::Resize(ResizeEdge::Left)), "one pixel into the actual drawn border must still register as the left edge" ); // Just past the border entirely (border_width + 1 outside frame) is // still nothing - the fix widens the dead zone's boundary, it // doesn't remove it. - assert_eq!(ResizeEdge::hit_test(f, f.x - border_width as i32 - 1, cy, true, border_width, RESIZE_MARGIN, false, None, false), None); + assert_eq!(ResizeEdge::hit_test(f, f.x - border_width as i32 - 1, cy, true, border_width, RESIZE_MARGIN, false, None, false, true), None); } #[test] @@ -1222,7 +1291,7 @@ mod tests { assert!(corner_reach > RESIZE_MARGIN, "the whole point of this test is that corner reach exceeds a plain edge's"); let x = f.x + corner_reach - 1; let y = f.bottom() - corner_reach + 1; - assert_eq!(ResizeEdge::hit_test(f, x, y, true, 0, RESIZE_MARGIN, false, None, false), Some(TitlebarHit::Resize(ResizeEdge::BottomLeft))); + assert_eq!(ResizeEdge::hit_test(f, x, y, true, 0, RESIZE_MARGIN, false, None, false, true), Some(TitlebarHit::Resize(ResizeEdge::BottomLeft))); } #[test] @@ -1233,7 +1302,7 @@ mod tests { // - must read as a plain bottom edge, not a corner. let x = f.x + corner_reach + 5; let y = f.bottom() - 1; - assert_eq!(ResizeEdge::hit_test(f, x, y, true, 0, RESIZE_MARGIN, false, None, false), Some(TitlebarHit::Resize(ResizeEdge::Bottom))); + assert_eq!(ResizeEdge::hit_test(f, x, y, true, 0, RESIZE_MARGIN, false, None, false, true), Some(TitlebarHit::Resize(ResizeEdge::Bottom))); } #[test] @@ -1244,7 +1313,7 @@ mod tests { // `resize_edge_at` never even ran for a y inside the titlebar. let f = frame(); let corner_reach = CORNER_MARGIN * RESIZE_MARGIN; - let hit = ResizeEdge::hit_test(f, f.x + corner_reach - 1, f.y + corner_reach - 1, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.x + corner_reach - 1, f.y + corner_reach - 1, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Resize(ResizeEdge::TopLeft))); } @@ -1259,7 +1328,7 @@ mod tests { // the raw corner itself now sits in that real dead strip (see its // own doc comment), which correctly falls through to drag/resize, // not Close. - let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 1, f.y + 1, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - BUTTON_CLUSTER_MARGIN as i32 - 1, f.y + 1, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Close)); } @@ -1276,7 +1345,7 @@ mod tests { // frame's own edge, nowhere near where Close's hitbox starts) and // within `DECORATED_TOP_RESIZE_MARGIN` vertically. let f = frame(); - let hit = ResizeEdge::hit_test(f, f.right() - 1, f.y + 1, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - 1, f.y + 1, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Resize(ResizeEdge::TopRight))); } @@ -1289,7 +1358,7 @@ mod tests { // this only ever claims the true corner, not the whole column // beside Close. let f = frame(); - let hit = ResizeEdge::hit_test(f, f.right() - 1, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - 1, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Drag)); } @@ -1300,7 +1369,7 @@ mod tests { // corner, so the dead strip and its own corner-resize target move // to the frame's left edge instead. let f = frame(); - let hit = ResizeEdge::hit_test(f, f.x + 1, f.y + 1, true, 0, RESIZE_MARGIN, true, None, false); + let hit = ResizeEdge::hit_test(f, f.x + 1, f.y + 1, true, 0, RESIZE_MARGIN, true, None, false, true); assert_eq!(hit, Some(TitlebarHit::Resize(ResizeEdge::TopLeft))); } @@ -1316,7 +1385,7 @@ mod tests { // boxes, so this is testing the plain top edge specifically. let f = frame(); let x = f.x + f.width as i32 / 2; - let hit = ResizeEdge::hit_test(f, x, f.y + DECORATED_TOP_RESIZE_MARGIN - 1, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, x, f.y + DECORATED_TOP_RESIZE_MARGIN - 1, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Resize(ResizeEdge::Top))); } @@ -1328,7 +1397,7 @@ mod tests { // silently grown a resize zone everywhere. let f = frame(); let x = f.x + f.width as i32 / 2; - let hit = ResizeEdge::hit_test(f, x, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, x, f.y + DECORATED_TOP_RESIZE_MARGIN + 5, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Drag)); } @@ -1341,7 +1410,7 @@ mod tests { // avoid. Right-aligned close button's box starts at `right - 30`; // well inside it, at the very top row. let f = frame(); - let hit = ResizeEdge::hit_test(f, f.right() - 15, f.y + 1, true, 0, RESIZE_MARGIN, false, None, false); + let hit = ResizeEdge::hit_test(f, f.right() - 15, f.y + 1, true, 0, RESIZE_MARGIN, false, None, false, true); assert_eq!(hit, Some(TitlebarHit::Close)); } |