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.rs7
-rw-r--r--crates/core/src/manager/mod.rs6
-rw-r--r--crates/core/src/manager/tests.rs115
-rw-r--r--crates/core/src/manager/windows.rs44
-rw-r--r--crates/core/src/manager/winops.rs4
-rw-r--r--crates/core/src/rules.rs5
-rw-r--r--crates/core/src/window.rs21
8 files changed, 194 insertions, 10 deletions
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index 30fc020..e9d31a5 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::{centered_in, PlacementConfig, SmartPlacement, SnapZoneKind, SNAP_FLYOUT_EDGE};
+pub use placement::{centered_in, PlacementConfig, SmartPlacement, SnapZoneKind, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, 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 2efca7c..906e5db 100644
--- a/crates/core/src/manager/dragresize.rs
+++ b/crates/core/src/manager/dragresize.rs
@@ -211,13 +211,16 @@ impl WindowManager {
self.adjust_master_ratio_for_drag(window, &ids, dx, orig_master_ratio);
return;
}
- let mut new_geom = edge.apply_delta(orig, dx, dy, MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
+ // This window's own minimum, not the one global floor - see
+ // `Window::min_size`.
+ let (min_w, min_h) = self.windows.get(&window).map(|w| w.min_size).unwrap_or((MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT));
+ let mut new_geom = edge.apply_delta(orig, dx, dy, min_w, min_h);
// `Window::aspect_ratio`'s own doc comment: a locked-ratio window
// (the "phone monitor" case, concretely) re-derives one dimension
// from the other here, on top of the ordinary delta above, rather
// than needing a second, separate resize code path.
if let Some(ratio) = self.windows.get(&window).and_then(|w| w.aspect_ratio) {
- new_geom = edge.apply_aspect_ratio(new_geom, ratio, MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
+ new_geom = edge.apply_aspect_ratio(new_geom, ratio, min_w, min_h);
}
// Same live `w.monitor` correction as `update_drag`'s own doc
// comment explains - a resize can cross a monitor boundary at
diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs
index e778a32..2ca2bf9 100644
--- a/crates/core/src/manager/mod.rs
+++ b/crates/core/src/manager/mod.rs
@@ -434,6 +434,11 @@ 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,
+ /// `general.maximize_covers_dock`: whether maximize runs to the bottom
+ /// of the screen, under a bottom-anchored dock, rather than stopping
+ /// above it. A top bar's own reservation is always honoured either
+ /// way. Default `true` - see `input::layers::maximize_geometry_for`.
+ pub maximize_covers_dock: bool,
/// Set by `request_refresh`, drained by the main loop. Same
/// cross-boundary queued-request shape as `lock_requested`.
refresh_requested: bool,
@@ -623,6 +628,7 @@ impl WindowManager {
auto_raise: false,
theme: ThemeConfig::default(),
lock: LockConfig::default(),
+ maximize_covers_dock: true,
refresh_requested: false,
live_settings: std::collections::BTreeMap::new(),
drag: None,
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
index fade3aa..152c71d 100644
--- a/crates/core/src/manager/tests.rs
+++ b/crates/core/src/manager/tests.rs
@@ -799,6 +799,121 @@
assert!(wm.drag_top_edge_monitor().is_none());
}
+ /// A monitor with a 30px top bar reserved, like the real session.
+ fn wm_with_top_bar() -> WindowManager {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(vec![{
+ let mut m = Monitor::new(0, "primary", Rect::new(0, 30, 1920, 1050));
+ m.full_geometry = Rect::new(0, 0, 1920, 1080);
+ m.primary = true;
+ m
+ }]);
+ wm
+ }
+
+ #[test]
+ fn a_remembered_position_under_the_top_bar_is_pushed_clear_of_it() {
+ // The reported bug: an app whose remembered y predates the bar (or
+ // was saved on a monitor without one) reopened with its titlebar
+ // tucked underneath, unreachable.
+ let mut wm = wm_with_top_bar();
+ wm.set_remembered_geometry("someapp".to_string(), (100, 5, 800, 600));
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "w");
+ w.app_id = "someapp".into();
+ wm.add_window(w);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!(g.x, 100, "x was fine and must be left alone");
+ assert_eq!(g.y, 30, "pushed down to the usable area's own top edge");
+ }
+
+ #[test]
+ fn a_remembered_position_that_already_clears_the_bar_is_untouched() {
+ let mut wm = wm_with_top_bar();
+ wm.set_remembered_geometry("someapp".to_string(), (100, 200, 800, 600));
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "w");
+ w.app_id = "someapp".into();
+ wm.add_window(w);
+ assert_eq!((wm.window(a).unwrap().geometry.x, wm.window(a).unwrap().geometry.y), (100, 200));
+ }
+
+ #[test]
+ fn a_position_remembered_on_a_now_disconnected_monitor_is_pulled_back_on_screen() {
+ // Five of the eleven entries in the real store were saved with a
+ // second monitor attached. They used to be discarded outright, so
+ // those apps stopped remembering anything at all.
+ let mut wm = wm_with_top_bar();
+ wm.set_remembered_geometry("someapp".to_string(), (2400, 300, 800, 600));
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "w");
+ w.app_id = "someapp".into();
+ wm.add_window(w);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!(g.x, 1120, "clamped so its right edge sits on the monitor's own right edge");
+ assert_eq!(g.y, 300, "y was already valid and is kept");
+ assert!(g.x >= 0 && g.x + g.width as i32 <= 1920, "fully on screen: {g:?}");
+ }
+
+ #[test]
+ fn a_remembered_window_larger_than_the_screen_still_starts_at_the_usable_origin() {
+ let mut wm = wm_with_top_bar();
+ wm.set_remembered_geometry("someapp".to_string(), (500, 500, 4000, 3000));
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "w");
+ w.app_id = "someapp".into();
+ wm.add_window(w);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!((g.x, g.y), (0, 30), "clamped to the usable origin, not left off-screen");
+ }
+
+ #[test]
+ fn a_min_size_rule_raises_the_floor_an_interactive_resize_stops_at() {
+ let mut wm = wm_with_monitor();
+ wm.add_rule(WindowRule {
+ matcher: WindowMatch { class: Some("bigapp".into()), ..Default::default() },
+ actions: WindowRuleActions { min_size: Some((600, 400)), ..Default::default() },
+ });
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "w");
+ w.app_id = "bigapp".into();
+ w.geometry = Rect::new(100, 100, 900, 700);
+ wm.add_window(w);
+ assert_eq!(wm.window(a).unwrap().min_size, (600, 400));
+
+ // Drag the bottom-right corner far past the minimum.
+ wm.start_resize(a, ResizeEdge::BottomRight, 1000, 800);
+ wm.update_resize(-5000, -5000);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!((g.width, g.height), (600, 400), "must stop at the rule's own minimum");
+ }
+
+ #[test]
+ fn without_a_rule_a_window_keeps_the_global_minimum() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "w"));
+ assert_eq!(wm.window(a).unwrap().min_size, (MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT));
+ }
+
+ #[test]
+ fn a_min_size_rule_also_floors_a_remembered_size() {
+ // A size remembered from before the rule existed must not reopen
+ // the window below its new minimum.
+ let mut wm = wm_with_monitor();
+ wm.add_rule(WindowRule {
+ matcher: WindowMatch { class: Some("bigapp".into()), ..Default::default() },
+ actions: WindowRuleActions { min_size: Some((700, 500)), ..Default::default() },
+ });
+ wm.set_remembered_geometry("bigapp".to_string(), (100, 100, 300, 200));
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "w");
+ w.app_id = "bigapp".into();
+ wm.add_window(w);
+ let g = wm.window(a).unwrap().geometry;
+ assert_eq!((g.width, g.height), (700, 500));
+ }
+
#[test]
fn a_dialog_opens_centered_not_cascaded_into_the_corner() {
let mut wm = wm_with_monitor();
diff --git a/crates/core/src/manager/windows.rs b/crates/core/src/manager/windows.rs
index bd13b0c..0186679 100644
--- a/crates/core/src/manager/windows.rs
+++ b/crates/core/src/manager/windows.rs
@@ -60,6 +60,10 @@ impl WindowManager {
if let Some(margin) = a.resize_margin {
window.resize_margin = Some(margin);
}
+ if let Some(min) = a.min_size {
+ window.min_size = min;
+ window.min_size_from_rule = true;
+ }
if let Some(ratio) = a.aspect_ratio {
window.aspect_ratio = Some(ratio);
}
@@ -87,8 +91,8 @@ impl WindowManager {
let mut remembered_position: Option<(i32, i32)> = None;
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);
+ window.geometry.width = w.max(window.min_size.0);
+ window.geometry.height = h.max(window.min_size.1);
remembered_position = Some((x, y));
}
}
@@ -137,7 +141,16 @@ impl WindowManager {
// current monitor's own *full* geometry (not the exclusive-zone-
// 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)));
+ // Whichever monitor a remembered position lands on, else the one
+ // the window would have been placed on anyway - a position saved
+ // while a second monitor was connected must not be thrown away
+ // just because that monitor is gone. Clamped onto the surviving
+ // monitor below instead, which keeps "reopen where I left it"
+ // meaningful across a docking change rather than silently falling
+ // back to a fresh cascade.
+ let remembered_monitor = remembered_position
+ .and_then(|(x, y)| self.monitors.iter().find(|m| m.full_geometry.contains_point(x, y)))
+ .or(target_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".
@@ -160,8 +173,25 @@ impl WindowManager {
}
} else if let (Some((x, y)), Some(monitor)) = (remembered_position, remembered_monitor) {
window.monitor = monitor.id;
- window.geometry.x = x;
- window.geometry.y = y;
+ // Clamped into the monitor's *usable* area, not its full one.
+ //
+ // The check above only asked whether the remembered point was
+ // on some monitor at all, and `full_geometry` includes the
+ // strip a top bar reserves - so an app whose remembered `y`
+ // was small reopened with its titlebar tucked under the bar,
+ // unreachable. Reported as windows spawning "so close to top
+ // bar", and "sometimes" precisely because it depended on the
+ // remembered value: this store currently holds a window at
+ // y=44 with a 30px bar above it.
+ //
+ // The same clamp is what makes a position from a
+ // now-disconnected monitor usable rather than discarded: it is
+ // pulled back onto a monitor that exists, keeping as much of
+ // the remembered placement as still fits.
+ let area = monitor.geometry;
+ let (w, h) = (window.geometry.width as i32, window.geometry.height as i32);
+ window.geometry.x = x.clamp(area.x, (area.right() - w).max(area.x));
+ window.geometry.y = y.clamp(area.y, (area.bottom() - h).max(area.y));
} else if let Some(monitor) = target_monitor {
window.monitor = monitor.id;
let layout_name = self.workspace(workspace).map(|w| w.layout.clone()).unwrap_or_default();
@@ -280,6 +310,10 @@ impl WindowManager {
if let Some(margin) = actions.resize_margin {
window.resize_margin = Some(margin);
}
+ if let Some(min) = actions.min_size {
+ window.min_size = min;
+ window.min_size_from_rule = true;
+ }
if let Some(ratio) = actions.aspect_ratio {
window.aspect_ratio = Some(ratio);
}
diff --git a/crates/core/src/manager/winops.rs b/crates/core/src/manager/winops.rs
index 1deed3e..d145e97 100644
--- a/crates/core/src/manager/winops.rs
+++ b/crates/core/src/manager/winops.rs
@@ -258,8 +258,8 @@ impl WindowManager {
pub fn resize_window(&mut self, id: WindowId, width: u32, height: u32) {
if let Some(w) = self.windows.get_mut(&id) {
- w.geometry.width = width.max(MIN_WINDOW_WIDTH);
- w.geometry.height = height.max(MIN_WINDOW_HEIGHT);
+ w.geometry.width = width.max(w.min_size.0);
+ w.geometry.height = height.max(w.min_size.1);
}
}
diff --git a/crates/core/src/rules.rs b/crates/core/src/rules.rs
index ea83501..d44f5a4 100644
--- a/crates/core/src/rules.rs
+++ b/crates/core/src/rules.rs
@@ -100,6 +100,11 @@ pub struct WindowRuleActions {
/// binding (`crates/config`), not here - this crate has no config
/// engine of its own to own that parsing.
pub aspect_ratio: Option<(u32, u32)>,
+ /// `(width, height)` this window may never be resized below, in
+ /// physical pixels - see `Window::min_size`. Overrides whatever the
+ /// client itself declared, so a rule can both raise a minimum the app
+ /// set too low and lower one it set too high.
+ pub min_size: Option<(u32, u32)>,
}
#[derive(Debug, Clone, Default)]
diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs
index ee9fd23..272d772 100644
--- a/crates/core/src/window.rs
+++ b/crates/core/src/window.rs
@@ -197,6 +197,25 @@ pub struct Window {
/// (GNOME, KDE, Windows) hides or disables it in exactly this case.
/// Asked for as titlebars with "buttons of the program/dynamic".
pub resizable: bool,
+ /// This window's own minimum size, in physical pixels. Defaults to the
+ /// global [`crate::placement::MIN_WINDOW_WIDTH`]/`MIN_WINDOW_HEIGHT`.
+ ///
+ /// Two sources, in order of precedence: a `min_size` window rule, and
+ /// the client's own declared minimum (`xdg_toplevel.set_min_size`, or
+ /// an XWayland window's ICCCM size hints), which the backend fills in
+ /// the same way it fills `resizable`.
+ ///
+ /// One global minimum for every window is wrong in both directions: it
+ /// is far too small for an application that needs room to lay out at
+ /// all, and too large for a small utility or palette window. Asked for
+ /// as "different windows should have minimum sizes depending on what
+ /// window is".
+ pub min_size: (u32, u32),
+ /// Set when `min_size` came from a `min_size` window rule rather than
+ /// from the client or the default. The backend refreshes a client's
+ /// declared minimum on every decoration redraw, and must not overwrite
+ /// a deliberate rule with it.
+ pub min_size_from_rule: 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.
@@ -302,6 +321,8 @@ impl Window {
decorated: true,
is_dialog: false,
resizable: true,
+ min_size: (crate::placement::MIN_WINDOW_WIDTH, crate::placement::MIN_WINDOW_HEIGHT),
+ min_size_from_rule: false,
restore_decorated: None,
floating: false,
minimized: false,