srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/src')
-rw-r--r--crates/core/src/lib.rs2
-rw-r--r--crates/core/src/manager/dragresize.rs64
-rw-r--r--crates/core/src/manager/hittest.rs14
-rw-r--r--crates/core/src/manager/lock.rs43
-rw-r--r--crates/core/src/manager/mod.rs17
-rw-r--r--crates/core/src/manager/tests.rs154
-rw-r--r--crates/core/src/manager/windows.rs31
-rw-r--r--crates/core/src/placement.rs25
-rw-r--r--crates/core/src/theme.rs19
-rw-r--r--crates/core/src/window.rs135
10 files changed, 465 insertions, 39 deletions
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index ed24e78..30fc020 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -19,7 +19,7 @@ pub use layout::{Layout, MasterStackLayout, NoOpLayout, TilingConfig};
pub use lock_config::LockConfig;
pub use manager::{CaptureRequest, ColorFilter, Direction, WindowManager};
pub use monitor::{Monitor, MonitorId};
-pub use placement::{PlacementConfig, SmartPlacement, SnapZoneKind};
+pub use placement::{centered_in, PlacementConfig, SmartPlacement, SnapZoneKind, SNAP_FLYOUT_EDGE};
pub use regex::Regex;
pub use rules::{WindowMatch, WindowRule, WindowRuleActions};
pub use theme::{format_hex_color, parse_hex_color, ThemeConfig};
diff --git a/crates/core/src/manager/dragresize.rs b/crates/core/src/manager/dragresize.rs
index 7181bb3..2efca7c 100644
--- a/crates/core/src/manager/dragresize.rs
+++ b/crates/core/src/manager/dragresize.rs
@@ -10,12 +10,16 @@ impl WindowManager {
pub fn start_drag(&mut self, id: WindowId, x: i32, y: i32) {
if let Some(w) = self.windows.get(&id) {
- self.drag = Some(DragState { window: id, start_x: x, start_y: y, orig: w.geometry });
+ self.drag = Some(DragState { window: id, start_x: x, start_y: y, orig: w.geometry, last_x: x, last_y: y });
self.focus_window(id);
}
}
pub fn update_drag(&mut self, x: i32, y: i32) {
+ if let Some(drag) = &mut self.drag {
+ drag.last_x = x;
+ drag.last_y = y;
+ }
let Some(drag) = &self.drag else { return };
let (dx, dy) = (x - drag.start_x, y - drag.start_y);
let mut new_geom = drag.orig;
@@ -108,10 +112,68 @@ impl WindowManager {
}
}
+ /// Where the currently-dragged window would land if the button came up
+ /// right now, or `None` when the drag is not in a snap zone.
+ ///
+ /// Deliberately calls the very same `SmartPlacement::snap_zone` that
+ /// `end_drag` does, on the same inputs, rather than re-deriving the
+ /// zones: a preview that can disagree with what release actually does
+ /// is worse than no preview, and any future change to the zone
+ /// geometry updates both at once by construction.
+ ///
+ /// Reported twice as missing: "if you move the window to absolute
+ /// north it show you layout options" and "why do i still not see the
+ /// windows layout or windows change layout when moved to areas of
+ /// screen like in windows". Edge snapping itself already worked - it
+ /// just committed silently on release with nothing shown beforehand,
+ /// so there was no way to tell it was going to happen, or where.
+ pub fn drag_snap_preview(&self) -> Option<Rect> {
+ let drag = self.drag.as_ref()?;
+ let w = self.windows.get(&drag.window)?;
+ let m = self.monitor_for(w.monitor)?;
+ SmartPlacement::snap_zone(w.geometry, m, &self.placement)
+ }
+
+ /// The monitor whose top edge the drag pointer is currently within
+ /// [`SNAP_FLYOUT_EDGE`] of, or `None`.
+ ///
+ /// Measured against `full_geometry`, not `geometry`: the trigger band
+ /// is the physical top of the screen, which is exactly where a bar
+ /// usually sits. Using the exclusive-zone-shrunk rect would put the
+ /// band *below* the bar, so on a machine with a top bar the gesture
+ /// would only fire after the pointer had already travelled past it.
+ ///
+ /// The pointer, not the window's own top edge, because the two differ
+ /// by however far down the titlebar the drag grabbed - and it is the
+ /// pointer the user is actually aiming.
+ ///
+ /// Not `Rect::contains_point`: that would also reject a pointer *above*
+ /// the monitor's top edge, which is the one direction this gesture is
+ /// aimed in. A real seat clamps the cursor to the output, so `y < 0`
+ /// should not arise on hardware - but `update_drag` clamps only the
+ /// window, so nothing in this type's own API guarantees it, and
+ /// "thrown past the edge" is the strongest possible form of the intent
+ /// this is trying to detect. Horizontal containment is still required,
+ /// as is being above the monitor's bottom, so a pointer on a different
+ /// output never matches.
+ pub fn drag_top_edge_monitor(&self) -> Option<&Monitor> {
+ let drag = self.drag.as_ref()?;
+ let (x, y) = (drag.last_x, drag.last_y);
+ self.monitors.iter().find(|m| {
+ let g = m.full_geometry;
+ x >= g.x && x < g.right() && y < g.bottom() && y - g.y <= SNAP_FLYOUT_EDGE
+ })
+ }
+
pub fn is_dragging(&self) -> bool {
self.drag.is_some()
}
+ /// The window the current drag is moving, if any.
+ pub fn dragged_window(&self) -> Option<WindowId> {
+ self.drag.as_ref().map(|d| d.window)
+ }
+
pub fn start_resize(&mut self, id: WindowId, edge: ResizeEdge, x: i32, y: i32) {
if let Some(w) = self.windows.get(&id) {
// Decided *before* `focus_window` below re-stacks `id` --
diff --git a/crates/core/src/manager/hittest.rs b/crates/core/src/manager/hittest.rs
index ab19a5c..9e0a8e1 100644
--- a/crates/core/src/manager/hittest.rs
+++ b/crates/core/src/manager/hittest.rs
@@ -6,6 +6,18 @@
use super::*;
impl WindowManager {
+ /// Whether `w` gets a Maximize button, resolving `theme.dynamic_buttons`
+ /// against the window's own declared resizability.
+ ///
+ /// Lives here, next to the hit-test that consumes it, so the renderer
+ /// and the hit-test are reading one shared answer rather than each
+ /// deriving their own - these two have drifted apart before, and the
+ /// failure mode (buttons drawn in one set of slots, clicks resolved
+ /// against another) is silent.
+ pub fn show_maximize(&self, w: &Window) -> bool {
+ !self.theme.dynamic_buttons || w.resizable
+ }
+
// ---- Hit testing ------------------------------------------------------
/// Topmost window whose frame contains `(x, y)`, along with what part of
@@ -59,7 +71,7 @@ impl WindowManager {
}
let margin = w.resize_margin.unwrap_or(self.resize_margin);
let geometry = geometry_for(w.id, w.geometry);
- if let Some(hit) = ResizeEdge::hit_test(geometry, x, y, w.decorated, w.border_width, margin, self.theme.buttons_left, self.theme.button_order, w.is_dialog) {
+ if let Some(hit) = ResizeEdge::hit_test(geometry, x, y, w.decorated, w.border_width, margin, self.theme.buttons_left, self.theme.button_order, w.is_dialog, self.show_maximize(w)) {
return Some((w.id, hit));
}
// Not a titlebar/border/resize-margin hit on `w` - but if the
diff --git a/crates/core/src/manager/lock.rs b/crates/core/src/manager/lock.rs
index 07735c6..0ffe0ca 100644
--- a/crates/core/src/manager/lock.rs
+++ b/crates/core/src/manager/lock.rs
@@ -25,6 +25,32 @@ impl WindowManager {
pub fn drain_lock_request(&mut self) -> bool {
std::mem::take(&mut self.lock_requested)
}
+
+ /// Queues a request for the config layer to re-read `init.lua` and fire
+ /// the `srd.on("refresh", ...)` handler.
+ ///
+ /// Same core/backend split as `request_lock` above, for the same
+ /// reason: core owns no Lua state, so it cannot reload a config or run
+ /// a handler itself. The desktop menu's own "Refresh" row is the
+ /// caller.
+ ///
+ /// Asked for as "does refresh refresh configs in a function list in the
+ /// config ie refresh os, etc, ags/aegis/polybar/waybar". Refresh used
+ /// to re-scan the desktop icon grid and nothing else, so there was no
+ /// way to make it reload anything the user actually cared about. What
+ /// "refresh" *means* beyond srdwm's own config is deliberately the
+ /// config's decision, not a hardcoded list of other people's tools --
+ /// this compositor has no business knowing whether the user runs
+ /// waybar or AGS.
+ pub fn request_refresh(&mut self) {
+ self.refresh_requested = true;
+ }
+
+ /// Takes the current refresh request, if any. Drained once per poll,
+ /// same as `drain_lock_request`.
+ pub fn drain_refresh_request(&mut self) -> bool {
+ std::mem::take(&mut self.refresh_requested)
+ }
}
#[cfg(test)]
@@ -41,6 +67,23 @@ mod tests {
}
#[test]
+ fn drain_refresh_request_is_true_once_then_false() {
+ let mut wm = WindowManager::new();
+ assert!(!wm.drain_refresh_request(), "nothing requested yet");
+ wm.request_refresh();
+ assert!(wm.drain_refresh_request(), "must report the pending request");
+ assert!(!wm.drain_refresh_request(), "must not report the same request twice");
+ }
+
+ #[test]
+ fn a_refresh_request_is_independent_of_a_lock_request() {
+ let mut wm = WindowManager::new();
+ wm.request_refresh();
+ assert!(!wm.drain_lock_request(), "refresh must not look like a lock");
+ assert!(wm.drain_refresh_request());
+ }
+
+ #[test]
fn requesting_lock_twice_before_a_drain_is_still_just_one_pending_request() {
let mut wm = WindowManager::new();
wm.request_lock();
diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs
index 9265885..7454ce2 100644
--- a/crates/core/src/manager/mod.rs
+++ b/crates/core/src/manager/mod.rs
@@ -1,7 +1,7 @@
use crate::geometry::Rect;
use crate::layout::{Layout, MasterStackLayout, NoOpLayout, TilingConfig};
use crate::monitor::{DisabledMonitor, Monitor, MonitorId, MonitorSplit};
-use crate::placement::{PlacementConfig, SmartPlacement, SnapZoneKind, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH};
+use crate::placement::{centered_in, PlacementConfig, SmartPlacement, SnapZoneKind, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, SNAP_FLYOUT_EDGE};
use crate::rules::WindowRule;
#[cfg(test)]
use crate::rules::{WindowMatch, WindowRuleActions};
@@ -43,6 +43,17 @@ struct DragState {
start_x: i32,
start_y: i32,
orig: Rect,
+ /// Where the pointer was on the last `update_drag` tick, global space.
+ /// Seeded from the drag's own start point so it is never meaningless,
+ /// even for a drag that ends before a single motion event arrives.
+ ///
+ /// Only the *pointer* can answer "is the user reaching for the top of
+ /// the screen right now" - `orig`/`Window::geometry` answer "where is
+ /// the window", which is a different question during a drag, because
+ /// the window hangs below the grab point by however far down its
+ /// titlebar the user took hold of it. See `drag_top_edge_monitor`.
+ last_x: i32,
+ last_y: i32,
}
struct ResizeState {
@@ -423,6 +434,9 @@ pub struct WindowManager {
/// Read from `theme.lock.*`. See `LockConfig`'s own doc comment for
/// why this isn't just folded into `theme` above.
pub lock: LockConfig,
+ /// Set by `request_refresh`, drained by the main loop. Same
+ /// cross-boundary queued-request shape as `lock_requested`.
+ refresh_requested: bool,
drag: Option<DragState>,
resize: Option<ResizeState>,
rules: Vec<WindowRule>,
@@ -586,6 +600,7 @@ impl WindowManager {
auto_raise: false,
theme: ThemeConfig::default(),
lock: LockConfig::default(),
+ refresh_requested: false,
drag: None,
resize: None,
rules: Vec::new(),
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
index 6a3ba3f..fade3aa 100644
--- a/crates/core/src/manager/tests.rs
+++ b/crates/core/src/manager/tests.rs
@@ -700,6 +700,160 @@
assert_eq!(wm.window(a).unwrap().geometry, original);
}
+ /// Adds a window and drags it by exactly `(dx, dy)`, leaving the drag
+ /// open. Deltas rather than absolute targets because `add_window` runs
+ /// `SmartPlacement`, so a window's real starting rect is chosen by the
+ /// placement policy, not by whatever the test set before adding it.
+ fn drag_by(wm: &mut WindowManager, dx: i32, dy: i32) -> (WindowId, Rect) {
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let start = wm.window(a).unwrap().geometry;
+ // Grab well down the window so the pointer and the window's own
+ // top edge are never accidentally the same measurement.
+ let (px, py) = (start.x + 40, start.y + 40);
+ wm.start_drag(a, px, py);
+ wm.update_drag(px + dx, py + dy);
+ (a, start)
+ }
+
+ #[test]
+ fn dragging_to_the_left_edge_previews_the_left_half_before_release() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let start = wm.window(a).unwrap().geometry;
+ assert_eq!(wm.drag_snap_preview(), None, "no drag in progress");
+
+ let (px, py) = (start.x + 40, start.y + 40);
+ wm.start_drag(a, px, py);
+ assert_eq!(wm.drag_snap_preview(), None, "still where it started: nothing to preview");
+
+ // Exactly enough to put the window's own left edge on x=0.
+ wm.update_drag(px - start.x, py);
+ assert_eq!(wm.drag_snap_preview(), Some(Rect::new(0, 0, 960, 1080)), "left half");
+ }
+
+ #[test]
+ fn the_drag_preview_is_exactly_what_release_then_commits() {
+ // The preview and the commit must never be able to disagree --
+ // both go through SmartPlacement::snap_zone on the same inputs.
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let start = wm.window(a).unwrap().geometry;
+ let (px, py) = (start.x + 40, start.y + 40);
+ wm.start_drag(a, px, py);
+ wm.update_drag(px - start.x, py);
+ let previewed = wm.drag_snap_preview().expect("a zone was previewed");
+ wm.end_drag();
+ assert_eq!(wm.window(a).unwrap().geometry, previewed);
+ }
+
+ #[test]
+ fn a_drag_that_is_not_near_any_edge_previews_nothing() {
+ let mut wm = wm_with_monitor();
+ let (_, _) = drag_by(&mut wm, 300, 300);
+ assert_eq!(wm.drag_snap_preview(), None);
+ }
+
+ #[test]
+ fn the_flyout_trigger_follows_the_pointer_not_the_windows_own_top_edge() {
+ // A drag grabbed 40px down its titlebar holds the window's top
+ // edge 40px below the pointer, and `update_drag` clamps the window
+ // to the monitor while the pointer is free to reach y=0. Aiming
+ // the cursor at the top of the screen has to be enough on its own.
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let start = wm.window(a).unwrap().geometry;
+ let (px, py) = (start.x + 40, start.y + 40);
+ wm.start_drag(a, px, py);
+
+ wm.update_drag(px, py + 200);
+ assert!(wm.drag_top_edge_monitor().is_none(), "mid-screen");
+
+ wm.update_drag(px, 2);
+ assert_eq!(wm.drag_top_edge_monitor().map(|m| m.id), Some(0), "pointer is at the top edge");
+ }
+
+ #[test]
+ fn the_flyout_trigger_needs_the_pointer_actually_at_the_edge() {
+ let mut wm = wm_with_monitor();
+ drag_by(&mut wm, 0, -10_000);
+ // Clamped hard against the top: the pointer went with it, so this
+ // one legitimately does trigger.
+ assert!(wm.drag_top_edge_monitor().is_some());
+
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let start = wm.window(a).unwrap().geometry;
+ wm.start_drag(a, start.x + 40, start.y + 40);
+ wm.update_drag(start.x + 40, SNAP_FLYOUT_EDGE + 1);
+ assert!(wm.drag_top_edge_monitor().is_none(), "one pixel outside the band");
+ }
+
+ #[test]
+ fn there_is_no_flyout_trigger_when_nothing_is_being_dragged() {
+ let wm = wm_with_monitor();
+ assert!(wm.drag_top_edge_monitor().is_none());
+ }
+
+ #[test]
+ fn a_dialog_opens_centered_not_cascaded_into_the_corner() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ let mut d = Window::new(a, "dialog");
+ d.is_dialog = true;
+ d.geometry = Rect::new(0, 0, 400, 300);
+ wm.add_window(d);
+ // 1920x1080 monitor, 400x300 dialog.
+ assert_eq!(wm.window(a).unwrap().geometry, Rect::new(760, 390, 400, 300));
+ }
+
+ #[test]
+ fn a_dialog_ignores_the_apps_remembered_position() {
+ // `remembered_geometry` is keyed by app_id, which a dialog shares
+ // with the window that spawned it - without the dialog branch
+ // running first, a dialog lands wherever that app's last main
+ // window sat.
+ let mut wm = wm_with_monitor();
+ wm.set_remembered_geometry("someapp".to_string(), (50, 60, 900, 700));
+ let a = wm.alloc_window_id();
+ let mut d = Window::new(a, "dialog");
+ d.app_id = "someapp".into();
+ d.is_dialog = true;
+ d.geometry = Rect::new(0, 0, 400, 300);
+ wm.add_window(d);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!((g.x, g.y), (760, 390), "centred, not restored to (50,60)");
+ }
+
+ #[test]
+ fn an_ordinary_window_still_uses_its_remembered_position() {
+ // The dialog branch must not have stolen the normal path.
+ let mut wm = wm_with_monitor();
+ wm.set_remembered_geometry("someapp".to_string(), (50, 60, 900, 700));
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "main");
+ w.app_id = "someapp".into();
+ wm.add_window(w);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!((g.x, g.y), (50, 60));
+ }
+
+ #[test]
+ fn a_dialog_larger_than_the_screen_still_starts_on_screen() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ let mut d = Window::new(a, "dialog");
+ d.is_dialog = true;
+ d.geometry = Rect::new(0, 0, 4000, 3000);
+ wm.add_window(d);
+ let g = wm.window(a).unwrap().geometry;
+ assert!(g.x >= 0 && g.y >= 0, "clamped to the monitor origin, got {g:?}");
+ }
+
#[test]
fn apply_snap_zone_resizes_to_the_named_zones_rect() {
let mut wm = wm_with_monitor();
diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs
index 3271dee..bd13b0c 100644
--- a/crates/core/src/manager/windows.rs
+++ b/crates/core/src/manager/windows.rs
@@ -77,8 +77,15 @@ impl WindowManager {
// question from size: a size is always safe to reapply verbatim,
// but a *position* needs checking against the monitors that
// actually exist right now before it's safe to reuse.
+ //
+ // Skipped entirely for a dialog: `remembered_geometry` is keyed by
+ // `app_id`, which a dialog shares with the ordinary window that
+ // spawned it, so reapplying that entry would resize a small
+ // confirmation prompt to the size of the app's main window. Its
+ // own requested size is the right one. See the dialog branch
+ // further down for the matching position half.
let mut remembered_position: Option<(i32, i32)> = None;
- if !window.app_id.is_empty() {
+ if !window.app_id.is_empty() && !window.is_dialog {
if let Some((x, y, w, h)) = self.remembered_geometry.get(&window.app_id).copied() {
window.geometry.width = w.max(MIN_WINDOW_WIDTH);
window.geometry.height = h.max(MIN_WINDOW_HEIGHT);
@@ -131,7 +138,27 @@ impl WindowManager {
// shrunk usable one): a remembered position under where a bar now
// sits is still "a real monitor, just partly covered", not invalid.
let remembered_monitor = remembered_position.and_then(|(x, y)| self.monitors.iter().find(|m| m.full_geometry.contains_point(x, y)));
- if let (Some((x, y)), Some(monitor)) = (remembered_position, remembered_monitor) {
+ // A dialog is centred, and neither cascaded nor restored to a
+ // remembered spot. Reported as "even dialog/starter windows spawn
+ // that side, most times should be centered".
+ //
+ // Ahead of the remembered-position branch on purpose: `remembered_
+ // geometry` is keyed by `app_id`, which a dialog shares with the
+ // ordinary window that spawned it, so a dialog would otherwise be
+ // dropped at wherever that app's last *main* window happened to
+ // sit - and would then overwrite that memory with its own small
+ // rect on close. Centring is also what every mainstream desktop
+ // does with a transient: the user's attention is already at the
+ // middle of the screen, not at a cascade origin.
+ //
+ // `geometry`, not `full_geometry`: a dialog centred in the usable
+ // area sits clear of a bar or dock, which is where a modal belongs.
+ if window.is_dialog {
+ if let Some(monitor) = target_monitor {
+ window.monitor = monitor.id;
+ window.geometry = centered_in(monitor.geometry, window.geometry.width, window.geometry.height);
+ }
+ } else if let (Some((x, y)), Some(monitor)) = (remembered_position, remembered_monitor) {
window.monitor = monitor.id;
window.geometry.x = x;
window.geometry.y = y;
diff --git a/crates/core/src/placement.rs b/crates/core/src/placement.rs
index e2b3d04..91c317c 100644
--- a/crates/core/src/placement.rs
+++ b/crates/core/src/placement.rs
@@ -51,6 +51,19 @@ impl Default for PlacementConfig {
}
}
+/// How close to a monitor's top edge the drag pointer has to get before
+/// the Snap-Layouts flyout drops down, in logical pixels.
+///
+/// Much larger than [`PlacementConfig::snap_threshold`] (8) on purpose, and
+/// they measure different things: `snap_threshold` measures the dragged
+/// *window's* edge against the screen edge and decides whether to commit a
+/// snap, so it has to be tight or an ordinary reposition near the top
+/// silently maximizes. This measures the *pointer* and only decides whether
+/// to offer a menu, which costs nothing if ignored - the user throws the
+/// cursor at the top of the screen, the way Windows 11's own gesture works,
+/// and a tight band would just make it feel unreliable.
+pub const SNAP_FLYOUT_EDGE: i32 = 12;
+
/// The six fixed screen positions offered by the Snap-Layouts flyout
/// (`crates/wayland/src/snap_flyout.rs`, opened by right-clicking a
/// titlebar's maximize button) - the click-driven equivalent of dragging a
@@ -117,6 +130,18 @@ impl SnapZoneKind {
}
}
+/// A `width` x `height` rect centred in `area`, clamped so it never starts
+/// outside `area` even when it is larger than it.
+///
+/// Used for dialogs (see `WindowManager::add_window`). Integer division
+/// biases a one-pixel remainder toward the top-left, which is the standard
+/// convention and invisible in practice.
+pub fn centered_in(area: Rect, width: u32, height: u32) -> Rect {
+ let x = area.x + (area.width as i32 - width as i32) / 2;
+ let y = area.y + (area.height as i32 - height as i32) / 2;
+ Rect::new(x.max(area.x), y.max(area.y), width, height)
+}
+
pub struct SmartPlacement;
impl SmartPlacement {
diff --git a/crates/core/src/theme.rs b/crates/core/src/theme.rs
index 4dbf96d..026688a 100644
--- a/crates/core/src/theme.rs
+++ b/crates/core/src/theme.rs
@@ -157,6 +157,24 @@ pub struct ThemeConfig {
/// this convention's own (Windows/GNOME) maximize icon rather than
/// borrowing the other convention's.
pub traffic_light_buttons: bool,
+ /// Show only the titlebar buttons the window can actually use, rather
+ /// than a fixed three.
+ ///
+ /// Today that means one rule: a window whose client pinned its minimum
+ /// and maximum size to the same value (`Window::resizable` is `false`)
+ /// does not get a Maximize button, because pressing it can do nothing.
+ /// A dialog's Close-only titlebar is a separate, older rule that
+ /// applies regardless of this setting.
+ ///
+ /// On by default. A button that visibly does nothing when pressed is a
+ /// defect rather than a preference, and GNOME, KDE and Windows all
+ /// hide or disable maximize in exactly this case. `theme.button_mode =
+ /// "fixed"` restores the always-three behaviour for anyone who wants
+ /// the titlebar to look identical on every window.
+ ///
+ /// Asked for as titlebars able to use "decorations/buttons of the
+ /// program/dynamic".
+ pub dynamic_buttons: bool,
}
impl Default for ThemeConfig {
@@ -175,6 +193,7 @@ impl Default for ThemeConfig {
button_order: None,
button_glyph_always: false,
traffic_light_buttons: true,
+ dynamic_buttons: true,
}
}
}
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));
}