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.rs152
-rw-r--r--crates/core/src/manager/input_pin.rs32
-rw-r--r--crates/core/src/manager/mod.rs24
-rw-r--r--crates/core/src/manager/tests.rs102
-rw-r--r--crates/core/src/theme.rs22
-rw-r--r--crates/core/src/window.rs14
7 files changed, 340 insertions, 8 deletions
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index 066d6d2..7b728d9 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -22,7 +22,7 @@ pub use monitor::{Monitor, MonitorId};
pub use placement::{PlacementConfig, SmartPlacement, SnapZoneKind};
pub use regex::Regex;
pub use rules::{WindowMatch, WindowRule, WindowRuleActions};
-pub use theme::{parse_hex_color, ThemeConfig};
+pub use theme::{format_hex_color, parse_hex_color, ThemeConfig};
pub use window::{
classify_menu_source, parse_button_order, ButtonOrder, GlobalMenu, MenuSource, ResizeEdge, TitlebarButton, TitlebarHit, Window, WindowId,
BUTTON_CLUSTER_MARGIN, BUTTON_PITCH, RESIZE_MARGIN, TITLEBAR_HEIGHT,
diff --git a/crates/core/src/manager/dragresize.rs b/crates/core/src/manager/dragresize.rs
index 1c1b4df..7181bb3 100644
--- a/crates/core/src/manager/dragresize.rs
+++ b/crates/core/src/manager/dragresize.rs
@@ -114,21 +114,48 @@ impl WindowManager {
pub fn start_resize(&mut self, id: WindowId, edge: ResizeEdge, x: i32, y: i32) {
if let Some(w) = self.windows.get(&id) {
- self.resize = Some(ResizeState { window: id, edge, start_x: x, start_y: y, orig: w.geometry });
+ // Decided *before* `focus_window` below re-stacks `id` --
+ // see `tiling_ratio_drag`'s own doc comment for why that
+ // order is load-bearing, not stylistic.
+ let ratio_drag_ids = self.tiling_ratio_drag(id, edge);
+ let orig = w.geometry;
+ self.resize = Some(ResizeState { window: id, edge, start_x: x, start_y: y, orig, orig_master_ratio: self.tiling.master_ratio, ratio_drag_ids });
self.focus_window(id);
}
}
pub fn update_resize(&mut self, x: i32, y: i32) {
- let Some(r) = &self.resize else { return };
- let (dx, dy) = (x - r.start_x, y - r.start_y);
- let mut new_geom = r.edge.apply_delta(r.orig, dx, dy, MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
+ // Copied/cloned out rather than kept as a live `&self.resize`
+ // borrow - the tiling branch below needs `&mut self`, which
+ // can't coexist with a borrow of the field it's reading.
+ let Some((window, edge, start_x, start_y, orig, orig_master_ratio, ratio_drag_ids)) =
+ self.resize.as_ref().map(|r| (r.window, r.edge, r.start_x, r.start_y, r.orig, r.orig_master_ratio, r.ratio_drag_ids.clone()))
+ else {
+ return;
+ };
+ let (dx, dy) = (x - start_x, y - start_y);
+ // Tiling's master/stack boundary is a live *ratio* the whole
+ // column split is computed from, not one window's own rect - see
+ // `adjust_master_ratio_for_drag`'s own doc comment for why a plain
+ // geometry write here would just be silently discarded by the very
+ // next `arrange_workspace` call anyway (reported live as "tiling
+ // needs a lot of work": dragging a tiled window's border looked
+ // like it resized, then snapped back the moment anything else
+ // triggered a re-arrange). `ratio_drag_ids` was decided once, at
+ // `start_resize` time, against the pre-focus membership - see
+ // `tiling_ratio_drag`'s own doc comment for why that snapshot
+ // (not a live re-derivation) is what has to be used here.
+ if let Some(ids) = ratio_drag_ids {
+ 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);
// `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(&r.window).and_then(|w| w.aspect_ratio) {
- new_geom = r.edge.apply_aspect_ratio(new_geom, ratio, MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
+ 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);
}
// Same live `w.monitor` correction as `update_drag`'s own doc
// comment explains - a resize can cross a monitor boundary at
@@ -137,7 +164,7 @@ impl WindowManager {
// lookup doesn't care which kind of geometry change put the
// window there.
let now_on = self.monitors.iter().find(|m| m.geometry.overlaps(&new_geom)).map(|m| m.id);
- if let Some(w) = self.windows.get_mut(&r.window) {
+ if let Some(w) = self.windows.get_mut(&window) {
w.geometry = new_geom;
if let Some(now_on_id) = now_on {
w.monitor = now_on_id;
@@ -145,6 +172,117 @@ impl WindowManager {
}
}
+ /// Whether resizing `id` along `edge` should live-adjust `self.tiling.
+ /// master_ratio` instead of writing raw window geometry: `id` must be
+ /// a non-floating, non-fullscreen member of a `"tiling"`-layout
+ /// workspace's own master/stack arrangement, there must actually be a
+ /// stack column to trade width with (a lone master-only window has
+ /// nothing on the other side of the drag), and `edge` must be the
+ /// shared boundary line between the two columns - the master
+ /// column's own right edge, or any stack column window's own left
+ /// edge, since both name the same physical boundary approached from
+ /// either side. Anything else (a vertical edge, a floating window, a
+ /// window under `dynamic`) falls through to the ordinary geometry
+ /// resize unchanged.
+ ///
+ /// **Must be called before `focus_window` runs for this same
+ /// interaction** - `start_resize`'s own call site is the only correct
+ /// place, and the returned membership snapshot is what `start_resize`
+ /// caches into `ResizeState` for `adjust_master_ratio_for_drag` to
+ /// apply the layout against later, rather than that method re-deriving
+ /// membership itself from `self.order` at *its* own, later point in
+ /// time. `focus_window` raises its target to the *end* of `self.order`
+ /// (`raise_window`), the exact list this membership is read from - so
+ /// merely grabbing a master window to resize it re-stacks it into what
+ /// looks like the stack's own last slot an instant later, and anything
+ /// that re-derives membership after that point (including a first
+ /// version of this whole feature that called `WindowManager::
+ /// arrange_workspace` from inside the drag, which reads `self.order`
+ /// itself fresh every time) silently applies the resulting ratio
+ /// change to the *wrong* column: the window that's actually being
+ /// dragged shrinks while its neighbour grows, backwards from what the
+ /// mouse is doing. Caught by this method's own test coverage's fuller
+ /// assertion (checking the *other* window's width too, not just the
+ /// grabbed one), not by inspection.
+ fn tiling_ratio_drag(&self, id: WindowId, edge: ResizeEdge) -> Option<Vec<WindowId>> {
+ if !(edge.has_left() || edge.has_right()) {
+ return None;
+ }
+ let w = self.windows.get(&id)?;
+ if w.floating || w.fullscreen {
+ return None;
+ }
+ if self.workspace(w.workspace).map(|ws| ws.layout.as_str()) != Some("tiling") {
+ return None;
+ }
+ // Mirrors `arrange_workspace`'s own grouping exactly - the same
+ // window set, same order, is what decides which windows are
+ // "master" vs "stack" there, so this has to agree with it or the
+ // ratio drag would trigger (or fail to) inconsistently with what
+ // is actually on screen.
+ let ids: Vec<WindowId> = self
+ .order
+ .iter()
+ .copied()
+ .filter(|&oid| self.windows.get(&oid).is_some_and(|ow| ow.workspace == w.workspace && ow.monitor == w.monitor && !ow.minimized && !ow.floating && !ow.fullscreen))
+ .collect();
+ let pos = ids.iter().position(|&oid| oid == id)?;
+ let master_count = self.tiling.master_count.max(1).min(ids.len());
+ if ids.len() <= master_count {
+ return None;
+ }
+ ((pos < master_count && edge.has_right()) || (pos >= master_count && edge.has_left())).then_some(ids)
+ }
+
+ /// Applies a tiling ratio-drag's raw pixel delta `dx` (positive =
+ /// dragged right = master column grows) against `orig_ratio` --
+ /// `ResizeState::orig_master_ratio`, the ratio as it was when this
+ /// resize *started*, not `self.tiling.master_ratio`'s own live value --
+ /// the same "cumulative delta against a fixed starting snapshot"
+ /// shape `update_drag`/`update_resize`'s own geometry math already
+ /// uses for `orig`. Using the live value instead would compound: every
+ /// tick would add the *whole* cumulative `dx` on top of whatever the
+ /// previous tick already added, not just that tick's own incremental
+ /// motion.
+ ///
+ /// Re-arranges every window in `ids` immediately against the new
+ /// ratio, not just the grabbed one - the entire point of this being a
+ /// *ratio* rather than one window's own rect is that every master and
+ /// every stack window visibly resizes together, the same live
+ /// feedback dwm/i3/Hyprland all give while dragging this exact
+ /// boundary.
+ ///
+ /// Applies `MasterStackLayout` directly against `ids` - the frozen
+ /// pre-focus snapshot `tiling_ratio_drag` returned - rather than
+ /// calling `arrange_workspace`, which re-derives its own window list
+ /// from `self.order` fresh every time it runs. By the time this method
+ /// runs, `start_resize`'s own `focus_window` call has already raised
+ /// `id` to the end of `self.order`; re-deriving membership from that
+ /// live order here would silently apply the ratio change to
+ /// whichever window *now* occupies the position `id` used to be in,
+ /// not to `id` and its real neighbours - the exact bug this
+ /// snapshot-based approach exists to avoid (see `tiling_ratio_drag`'s
+ /// own doc comment for the full story, including how a first,
+ /// `arrange_workspace`-based version of this method got caught by
+ /// this file's own tests).
+ fn adjust_master_ratio_for_drag(&mut self, id: WindowId, ids: &[WindowId], dx: i32, orig_ratio: f32) {
+ let Some(w) = self.windows.get(&id) else { return };
+ let Some(monitor) = self.monitor_for(w.monitor).cloned() else { return };
+ let area_width = monitor.geometry.inset(self.tiling.gap_outer).width.max(1);
+ let delta_ratio = dx as f32 / area_width as f32;
+ // Clamped well short of 0.0/1.0 - either extreme would hand one
+ // column all (or none) of the width, which `MasterStackLayout`
+ // itself never guards against (a `0`-width stack column is a
+ // degenerate, not-actually-tiled state, not a valid extreme of
+ // the slider).
+ self.tiling.master_ratio = (orig_ratio + delta_ratio).clamp(0.1, 0.9);
+ for (placed_id, rect) in MasterStackLayout.arrange(ids, &monitor, &self.tiling) {
+ if let Some(w) = self.windows.get_mut(&placed_id) {
+ w.geometry = rect;
+ }
+ }
+ }
+
pub fn end_resize(&mut self) {
// Remembers this app's new size for its *next* window - see
// `remembered_sizes`' own doc comment for why this is the one
diff --git a/crates/core/src/manager/input_pin.rs b/crates/core/src/manager/input_pin.rs
index e3a7fbf..9280004 100644
--- a/crates/core/src/manager/input_pin.rs
+++ b/crates/core/src/manager/input_pin.rs
@@ -32,6 +32,38 @@ impl WindowManager {
pub fn drain_pin_input_requests(&mut self) -> Vec<(i32, Option<WindowId>)> {
std::mem::take(&mut self.pin_input_requests)
}
+
+ /// Records `pid`'s *actual current* pin state, once the Wayland
+ /// backend has genuinely applied it (`CompState::
+ /// set_virtual_pointer_pin`) - not the request queue above, which is
+ /// drained and forgotten the instant the backend picks it up. Without
+ /// this there was no readback path at all: an IPC caller could ask to
+ /// pin a window blind, but never confirm the pin actually took, or ask
+ /// "is pid X pinned to anything right now" later. Flagged directly by
+ /// the AGS peer session as exactly this gap.
+ pub fn set_pinned_window(&mut self, pid: i32, window: Option<WindowId>) {
+ match window {
+ Some(w) => {
+ self.pinned_windows.insert(pid, w);
+ }
+ None => {
+ self.pinned_windows.remove(&pid);
+ }
+ }
+ }
+
+ /// `pid`'s currently pinned window, if any - the read side of
+ /// `set_pinned_window`.
+ pub fn pinned_window(&self, pid: i32) -> Option<WindowId> {
+ self.pinned_windows.get(&pid).copied()
+ }
+
+ /// Every currently pinned pid and its window - what the IPC
+ /// `"pinned_inputs"` query lists in full, rather than requiring a
+ /// caller to already know which pids to ask about individually.
+ pub fn all_pinned_windows(&self) -> impl Iterator<Item = (i32, WindowId)> + '_ {
+ self.pinned_windows.iter().map(|(&pid, &w)| (pid, w))
+ }
}
#[cfg(test)]
diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs
index c9c77b2..e4ac1c2 100644
--- a/crates/core/src/manager/mod.rs
+++ b/crates/core/src/manager/mod.rs
@@ -49,6 +49,23 @@ struct ResizeState {
start_x: i32,
start_y: i32,
orig: Rect,
+ /// `self.tiling.master_ratio` at the moment this resize started --
+ /// unconditionally captured (cheap, one `f32` copy) even for a resize
+ /// that turns out not to touch it, the same way `orig` itself is
+ /// captured regardless of whether the drag ends up floating or tiled.
+ /// See `WindowManager::adjust_master_ratio_for_drag`'s own doc comment
+ /// for why a live tiling resize needs its own *starting* ratio, not
+ /// just the live one mutated in place tick by tick.
+ orig_master_ratio: f32,
+ /// `WindowManager::tiling_ratio_drag`'s result, decided once here,
+ /// *before* `start_resize` calls `focus_window` - `Some(membership)`
+ /// for a tiling master/stack ratio drag, `None` for an ordinary
+ /// (floating, or non-boundary-edge) resize. See that method's own doc
+ /// comment for why this has to be captured now rather than
+ /// re-derived later: focusing the target re-stacks it in `self.order`,
+ /// the exact list membership is read from, and re-deriving after that
+ /// point silently answers for the wrong window.
+ ratio_drag_ids: Option<Vec<WindowId>>,
}
/// The platform-independent core of srdwm: owns window/workspace/monitor
@@ -80,6 +97,12 @@ pub struct WindowManager {
/// ever learn) to a specific window. See `input_pin.rs`'s own doc
/// comment.
pin_input_requests: Vec<(i32, Option<WindowId>)>,
+ /// `pid`'s *actual current* pin state, as last reported by `Comp
+ /// State::set_virtual_pointer_pin` once it's genuinely applied --
+ /// distinct from `pin_input_requests` above, which is a one-shot queue
+ /// drained and forgotten the moment the backend picks it up. See
+ /// `set_pinned_window`'s own doc comment.
+ pinned_windows: HashMap<i32, WindowId>,
/// Same cross-boundary-request pattern, for fake (fully virtual, no
/// real hardware) monitors - see `fake_monitor.rs`'s own doc comment
/// and `crates/wayland/src/udev/virtual_heads.rs`'s module doc
@@ -465,6 +488,7 @@ impl WindowManager {
monitors: Vec::new(),
output_position_requests: Vec::new(),
pin_input_requests: Vec::new(),
+ pinned_windows: HashMap::new(),
create_fake_monitor_requests: Vec::new(),
remove_fake_monitor_requests: Vec::new(),
output_enable_requests: Vec::new(),
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
index 3d628a8..860af89 100644
--- a/crates/core/src/manager/tests.rs
+++ b/crates/core/src/manager/tests.rs
@@ -288,6 +288,108 @@
}
#[test]
+ fn dragging_the_master_columns_right_edge_grows_master_ratio_live() {
+ // Live report: "tiling needs a lot of work" - dragging a tiled
+ // window's border used to write raw geometry that the very next
+ // `arrange_workspace` call silently discarded. Two windows: window
+ // `a` is the sole master (index 0 < master_count 1), `b` is the
+ // whole stack.
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ wm.tiling.gap_outer = 0;
+ wm.tiling.gap_inner = 0;
+ wm.tiling.master_ratio = 0.5;
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+
+ let before = wm.window(a).unwrap().geometry.width;
+ wm.start_resize(a, ResizeEdge::Right, 0, 0);
+ wm.update_resize(200, 0); // dragged 200px to the right
+ let after = wm.window(a).unwrap().geometry.width;
+ assert!(after > before, "the master column must visibly grow while dragging its own right edge");
+ assert!(wm.tiling.master_ratio > 0.5, "master_ratio itself must have grown, not just this one window's rect");
+ // The stack window must have shrunk by the matching amount --
+ // this is a real ratio change, not a floating-style resize that
+ // only touched the grabbed window.
+ assert!(wm.window(b).unwrap().geometry.width < 960);
+ }
+
+ #[test]
+ fn dragging_a_stack_windows_left_edge_adjusts_the_same_shared_boundary() {
+ // The mirror case: grabbing the *stack* column's own left edge is
+ // the same physical boundary as the master column's right edge,
+ // approached from the other side.
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ wm.tiling.gap_outer = 0;
+ wm.tiling.gap_inner = 0;
+ wm.tiling.master_ratio = 0.5;
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+
+ wm.start_resize(b, ResizeEdge::Left, 0, 0);
+ wm.update_resize(200, 0); // dragged the shared boundary right
+ assert!(wm.tiling.master_ratio > 0.5, "master must grow when the boundary is dragged right, regardless of which side initiated it");
+ }
+
+ #[test]
+ fn dragging_a_floating_windows_edge_never_touches_master_ratio() {
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ wm.tiling.master_ratio = 0.5;
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.floating = true;
+ w.geometry = Rect::new(100, 100, 300, 200);
+ wm.add_window(w);
+
+ wm.start_resize(a, ResizeEdge::Right, 0, 0);
+ wm.update_resize(200, 0);
+ assert_eq!(wm.tiling.master_ratio, 0.5, "a floating window's own resize must behave exactly as before - no tiling ratio involved");
+ assert_eq!(wm.window(a).unwrap().geometry.width, 500, "the floating window itself must still resize normally");
+ }
+
+ #[test]
+ fn dragging_a_tiled_windows_vertical_edge_does_not_touch_master_ratio() {
+ // Only the shared master/stack *horizontal* boundary is a ratio
+ // drag - a vertical edge has no equivalent concept in this
+ // layout (stack windows split height evenly, with no per-window
+ // override), so it must fall through to the ordinary (if
+ // ultimately overwritten) geometry path rather than doing nothing
+ // useful either way.
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ wm.tiling.master_ratio = 0.5;
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+
+ wm.start_resize(a, ResizeEdge::Bottom, 0, 0);
+ wm.update_resize(0, 100);
+ assert_eq!(wm.tiling.master_ratio, 0.5);
+ }
+
+ #[test]
+ fn dragging_the_only_windows_edge_with_no_stack_does_not_touch_master_ratio() {
+ // A lone master window has nothing on the other side of any
+ // boundary - there is no stack to trade width with.
+ let mut wm = wm_with_monitor();
+ wm.set_layout(wm.current_workspace(), "tiling");
+ wm.tiling.master_ratio = 0.5;
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+
+ wm.start_resize(a, ResizeEdge::Right, 0, 0);
+ wm.update_resize(200, 0);
+ assert_eq!(wm.tiling.master_ratio, 0.5);
+ }
+
+ #[test]
fn ending_a_resize_remembers_the_new_size_for_the_apps_next_window() {
let mut wm = wm_with_monitor();
// Tiling layout, so `add_window` skips `SmartPlacement`'s grid/
diff --git a/crates/core/src/theme.rs b/crates/core/src/theme.rs
index 882326f..4dbf96d 100644
--- a/crates/core/src/theme.rs
+++ b/crates/core/src/theme.rs
@@ -194,6 +194,16 @@ pub fn parse_hex_color(s: &str) -> Option<(u8, u8, u8)> {
Some((r, g, b))
}
+/// [`parse_hex_color`]'s exact inverse - `#rrggbb`, lowercase, always six
+/// hex digits (`{:02x}` per channel, so a channel below `0x10` doesn't
+/// collapse to a five-character string). Exists for settings readback: a
+/// caller reading `border_color` back over IPC should get the identical
+/// string shape `srd set border_color` itself accepts, not a different
+/// representation of the same colour.
+pub fn format_hex_color(rgb: (u8, u8, u8)) -> String {
+ format!("#{:02x}{:02x}{:02x}", rgb.0, rgb.1, rgb.2)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -216,6 +226,18 @@ mod tests {
}
#[test]
+ fn format_hex_color_round_trips_through_parse_hex_color() {
+ for rgb in [(0x88, 0xc0, 0xd0), (0, 0, 0), (0xff, 0xff, 0xff), (0x05, 0x0a, 0x0f)] {
+ assert_eq!(parse_hex_color(&format_hex_color(rgb)), Some(rgb));
+ }
+ }
+
+ #[test]
+ fn format_hex_color_pads_low_channel_values() {
+ assert_eq!(format_hex_color((0x05, 0x0a, 0x0f)), "#050a0f");
+ }
+
+ #[test]
fn default_matches_the_legacy_hardcoded_nord_palette() {
let t = ThemeConfig::default();
assert_eq!(t.titlebar_bg, (0x2e, 0x34, 0x40));
diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs
index ae82b49..a8de352 100644
--- a/crates/core/src/window.rs
+++ b/crates/core/src/window.rs
@@ -705,6 +705,20 @@ impl ResizeEdge {
r
}
+ /// Whether this edge has a left-hand component (`Left`, `TopLeft`,
+ /// `BottomLeft`) - used by tiling's own master/stack ratio-drag
+ /// detection (`WindowManager::tiling_ratio_drag`) to recognize a stack
+ /// column window's left edge as the same shared boundary a master
+ /// column window's own right edge is.
+ pub fn has_left(self) -> bool {
+ matches!(self, ResizeEdge::Left | ResizeEdge::TopLeft | ResizeEdge::BottomLeft)
+ }
+
+ /// [`Self::has_left`]'s mirror, for `Right`/`TopRight`/`BottomRight`.
+ pub fn has_right(self) -> bool {
+ matches!(self, ResizeEdge::Right | ResizeEdge::TopRight | ResizeEdge::BottomRight)
+ }
+
/// Re-derives one dimension of `delta_applied` (the result of
/// `apply_delta`, already reflecting this drag's pointer motion) so
/// the rect holds `ratio` (`width, height`) - `Window::aspect_ratio`'s