Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: edge auto-pan during interactive move/resize
Dragging or resizing against a screen edge now scrolls the infinite
desktop underneath the pinned cursor. The op tracks the cursor in
virtual space as start + cursor_delta/zoom + (pan − start_pan): SeatOp
records the desk pan at op start, and the new pan-delta term (both in
the seat op arms and in get_active_resize_dimensions, which must not
diverge) turns desktop scroll into window/edge motion, so the grab
sticks to the cursor while the desk slides. Seat::update_edge_pan runs
on every op motion: inside a band at the output edge it sets a velocity
ramping from 0 at the band's inner rim to full speed at the screen edge
(screen px/s, zoom-compensated), arming a 16ms tick that pans and
re-runs the op at its last cursor position — the tick's own op_update
re-derives the velocity and re-arms, so the scroll sustains itself
while the cursor rests at the edge and stops when it leaves the band or
the op ends. Status-bar drags are exempt. Config:
desktop { edge_pan=#true edge_pan_band=32 edge_pan_speed=1000 }.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/config.rs | 53 +++++++++++++++++++++++++++++++
src/server/cursor.rs | 14 +++++++--
src/server/seat.rs | 59 ++++++++++++++++++++++++++++++++---
src/server/window_manager.rs | 74 ++++++++++++++++++++++++++++++++++++++++++--
src/server/xdg_toplevel.rs | 4 +++
5 files changed, 195 insertions(+), 9 deletions(-)
diff --git a/src/server/config.rs b/src/server/config.rs
index 2518142..8da85b4 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -71,6 +71,14 @@ pub struct Layout {
pub desktop_snap: bool,
/// Snap radius in virtual units.
pub desktop_snap_threshold: f64,
+ /// Edge auto-pan: dragging/resizing against a screen edge scrolls the
+ /// desktop underneath the pinned cursor.
+ pub desktop_edge_pan: bool,
+ /// Width of the trigger band inside each output edge, in layout px.
+ pub desktop_edge_pan_band: f64,
+ /// Full-tilt pan speed at the screen edge, in screen px/s (the tick
+ /// divides by zoom; speed ramps linearly across the band).
+ pub desktop_edge_pan_speed: f64,
pub scenefx_optimized_blur: bool,
pub status_backdrop_blur_ignore_transparent: bool,
pub window_backdrop_blur_ignore_transparent: bool,
@@ -144,6 +152,9 @@ impl Default for Layout {
shadow_offset_y: 7,
desktop_snap: true,
desktop_snap_threshold: 24.0,
+ desktop_edge_pan: true,
+ desktop_edge_pan_band: 32.0,
+ desktop_edge_pan_speed: 1000.0,
scenefx_optimized_blur: true,
status_backdrop_blur_ignore_transparent: true,
window_backdrop_blur_ignore_transparent: true,
@@ -327,6 +338,12 @@ pub struct SurfaceConfig {
pub desktop_snap: bool,
#[serde(default = "default_desktop_snap_threshold")]
pub desktop_snap_threshold: i64,
+ #[serde(default = "default_desktop_edge_pan")]
+ pub desktop_edge_pan: bool,
+ #[serde(default = "default_desktop_edge_pan_band")]
+ pub desktop_edge_pan_band: i64,
+ #[serde(default = "default_desktop_edge_pan_speed")]
+ pub desktop_edge_pan_speed: i64,
#[serde(default = "default_backplate_color")]
pub backplate_color: String,
#[serde(default = "default_backplate_blur")]
@@ -382,6 +399,9 @@ impl Default for SurfaceConfig {
desktop_grid_fade_mode: default_desktop_grid_fade_mode(),
desktop_snap: default_desktop_snap(),
desktop_snap_threshold: default_desktop_snap_threshold(),
+ desktop_edge_pan: default_desktop_edge_pan(),
+ desktop_edge_pan_band: default_desktop_edge_pan_band(),
+ desktop_edge_pan_speed: default_desktop_edge_pan_speed(),
backplate_color: default_backplate_color(),
backplate_blur: default_backplate_blur(),
backplate_corner_radius: default_backplate_corner_radius(),
@@ -443,6 +463,18 @@ fn default_desktop_snap_threshold() -> i64 {
24
}
+fn default_desktop_edge_pan() -> bool {
+ true
+}
+
+fn default_desktop_edge_pan_band() -> i64 {
+ 32
+}
+
+fn default_desktop_edge_pan_speed() -> i64 {
+ 1000
+}
+
fn default_backplate_color() -> String {
"#151520e6".to_string()
}
@@ -1562,6 +1594,21 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.desktop_snap_threshold = val;
}
}
+ "edge_pan" => {
+ if let Some(val) = entry.value().as_bool() {
+ surface.desktop_edge_pan = val;
+ }
+ }
+ "edge_pan_band" => {
+ if let Some(val) = entry.value().as_i64() {
+ surface.desktop_edge_pan_band = val;
+ }
+ }
+ "edge_pan_speed" => {
+ if let Some(val) = entry.value().as_i64() {
+ surface.desktop_edge_pan_speed = val;
+ }
+ }
_ => {}
}
}
@@ -1709,6 +1756,9 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.desktop_grid_fade_mode = get_child_arg_string(node, "grid_fade_mode", &default_desktop_grid_fade_mode());
surface.desktop_snap = get_child_arg_bool(node, "desktop_snap", default_desktop_snap());
surface.desktop_snap_threshold = get_child_arg_i64(node, "desktop_snap_threshold", default_desktop_snap_threshold());
+ surface.desktop_edge_pan = get_child_arg_bool(node, "desktop_edge_pan", default_desktop_edge_pan());
+ surface.desktop_edge_pan_band = get_child_arg_i64(node, "desktop_edge_pan_band", default_desktop_edge_pan_band());
+ surface.desktop_edge_pan_speed = get_child_arg_i64(node, "desktop_edge_pan_speed", default_desktop_edge_pan_speed());
surface.backplate_color = get_child_arg_string(node, "backplate_color", &default_backplate_color());
surface.backplate_blur = get_child_arg_f64(node, "backplate_blur", default_backplate_blur());
surface.backplate_corner_radius = get_child_arg_i64(node, "backplate_corner_radius", default_backplate_corner_radius());
@@ -1857,6 +1907,9 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
state.layout.desktop_grid_scale = config.surface.desktop_grid_scale as f64;
state.layout.desktop_snap = config.surface.desktop_snap;
state.layout.desktop_snap_threshold = config.surface.desktop_snap_threshold.max(0) as f64;
+ state.layout.desktop_edge_pan = config.surface.desktop_edge_pan;
+ state.layout.desktop_edge_pan_band = config.surface.desktop_edge_pan_band.max(1) as f64;
+ state.layout.desktop_edge_pan_speed = config.surface.desktop_edge_pan_speed.max(0) as f64;
state.layout.desktop_gap_width = config.surface.desktop_gap_width as i32;
state.layout.desktop_cell_corner_radius = config.surface.desktop_cell_corner_radius as i32;
state.layout.desktop_cell_fade_inset = config.surface.desktop_cell_fade_inset;
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 956a648..ade4be9 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -828,6 +828,8 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
start_win_h: (*clicked_status).box_geom.height as u32,
start_win_virtual_x: (*clicked_status).virtual_x,
start_win_virtual_y: (*clicked_status).virtual_y,
+ start_pan_x: (*server).wm.desk_pan_x,
+ start_pan_y: (*server).wm.desk_pan_y,
start_tiling_mode: (*clicked_status).tiling_mode,
start_mode_locked: (*clicked_status).mode_locked,
started_in_overview: false,
@@ -867,6 +869,8 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
start_win_h: (*clicked_win).box_geom.height as u32,
start_win_virtual_x: (*clicked_win).virtual_x,
start_win_virtual_y: (*clicked_win).virtual_y,
+ start_pan_x: (*server).wm.desk_pan_x,
+ start_pan_y: (*server).wm.desk_pan_y,
start_tiling_mode: (*clicked_win).tiling_mode,
start_mode_locked: (*clicked_win).mode_locked,
started_in_overview: true,
@@ -995,6 +999,8 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
start_win_virtual_y: (*target_win).virtual_y,
start_tiling_mode: (*target_win).tiling_mode,
start_mode_locked: (*target_win).mode_locked,
+ start_pan_x: (*(*seat).server).wm.desk_pan_x,
+ start_pan_y: (*(*seat).server).wm.desk_pan_y,
started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
});
cursor.op_start_pointer();
@@ -1088,7 +1094,9 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
start_win_virtual_y: (*border_target_win).virtual_y,
start_tiling_mode: (*border_target_win).tiling_mode,
start_mode_locked: (*border_target_win).mode_locked,
- started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
+ start_pan_x: (*(*seat).server).wm.desk_pan_x,
+ start_pan_y: (*(*seat).server).wm.desk_pan_y,
+ started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
});
cursor.op_start_pointer();
cursor.pressed.insert((*event).button, None);
@@ -1168,7 +1176,9 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
start_win_virtual_y: (*border_target_win).virtual_y,
start_tiling_mode: (*border_target_win).tiling_mode,
start_mode_locked: (*border_target_win).mode_locked,
- started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
+ start_pan_x: (*(*seat).server).wm.desk_pan_x,
+ start_pan_y: (*(*seat).server).wm.desk_pan_y,
+ started_in_overview: (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
});
cursor.op_start_pointer();
cursor.pressed.insert((*event).button, None);
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 0465fb4..1b4bc5c 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -29,6 +29,12 @@ pub struct SeatOp {
pub start_win_h: u32,
pub start_win_virtual_x: f64,
pub start_win_virtual_y: f64,
+ /// Desk pan at op start. The op tracks the cursor in virtual space as
+ /// `start + cursor_delta/zoom + (pan - start_pan)`: the pan-delta term
+ /// keeps the dragged window/edge pinned to the cursor when edge auto-pan
+ /// (or anything else) scrolls the desktop mid-drag.
+ pub start_pan_x: f64,
+ pub start_pan_y: f64,
pub start_tiling_mode: crate::tiling::TilingMode,
pub start_mode_locked: bool,
pub started_in_overview: bool,
@@ -1130,9 +1136,9 @@ impl Seat {
let scale = (*(*self.server).wm.server).wm.desk_zoom;
let pan_x = (*(*self.server).wm.server).wm.desk_pan_x;
let pan_y = (*(*self.server).wm.server).wm.desk_pan_y;
- let virtual_dx = dx as f64 / scale;
- let virtual_dy = dy as f64 / scale;
-
+ let virtual_dx = dx as f64 / scale + (pan_x - op.start_pan_x);
+ let virtual_dy = dy as f64 / scale + (pan_y - op.start_pan_y);
+
let vx = op.start_win_virtual_x + virtual_dx;
let vy = op.start_win_virtual_y + virtual_dy;
let (vx, vy) = crate::policy::snap::snap_move(
@@ -1172,8 +1178,8 @@ impl Seat {
let scale = (*(*self.server).wm.server).wm.desk_zoom;
let pan_x = (*(*self.server).wm.server).wm.desk_pan_x;
let pan_y = (*(*self.server).wm.server).wm.desk_pan_y;
- let virtual_dx = dx as f64 / scale;
- let virtual_dy = dy as f64 / scale;
+ let virtual_dx = dx as f64 / scale + (pan_x - op.start_pan_x);
+ let virtual_dy = dy as f64 / scale + (pan_y - op.start_pan_y);
let mut vx = op.start_win_virtual_x;
let mut vy = op.start_win_virtual_y;
@@ -1249,11 +1255,52 @@ impl Seat {
}
(*self.server).wm.dirty_windowing();
}
+ self.update_edge_pan(x as f64, y as f64);
+ }
+
+ /// Edge auto-pan eligibility + velocity for the current op: while an
+ /// interactive move/resize holds the cursor inside the band at an output
+ /// edge, the desktop scrolls that way, ramping from 0 at the band's inner
+ /// rim to full speed at the screen edge. Called on every op motion AND
+ /// from the edge-pan tick's op_update, which is what re-arms the timer —
+ /// so the scroll continues while the cursor rests pinned at the edge.
+ unsafe fn update_edge_pan(&mut self, lx: f64, ly: f64) {
+ let wm = &mut (*self.server).wm;
+ let mut vx = 0.0;
+ let mut vy = 0.0;
+ let eligible = wm.layout.desktop_edge_pan
+ && match self.op {
+ Some(ref op) => {
+ !op.window_ptr.is_null()
+ && !(*op.window_ptr).closed
+ && !(*op.window_ptr).is_status_bar()
+ }
+ None => false,
+ };
+ if eligible {
+ let wlr_output = (*self.server).om.output_at(lx, ly);
+ if !wlr_output.is_null() {
+ let mut ob = ffi::wlr_box { x: 0, y: 0, width: 0, height: 0 };
+ ffi::wlr_output_layout_get_box((*self.server).om.output_layout, wlr_output, &mut ob);
+ let band = wm.layout.desktop_edge_pan_band.max(1.0);
+ let speed = wm.layout.desktop_edge_pan_speed.max(0.0);
+ // 0 outside the band, 1 at (or past) the screen edge.
+ let ramp = |dist_to_edge: f64| ((band - dist_to_edge) / band).clamp(0.0, 1.0);
+ vx = speed
+ * (ramp((ob.x + ob.width) as f64 - lx) - ramp(lx - ob.x as f64));
+ vy = speed
+ * (ramp((ob.y + ob.height) as f64 - ly) - ramp(ly - ob.y as f64));
+ }
+ }
+ wm.set_edge_pan_velocity(vx, vy);
}
pub unsafe fn op_end(&mut self) {
if let Some(op) = self.op.take() {
log::debug!("end seat op");
+ let wm = &mut (*self.server).wm;
+ wm.edge_pan_vx = 0.0;
+ wm.edge_pan_vy = 0.0;
let win = op.window_ptr;
if !win.is_null() && !(*win).closed {
if let PointerOpType::Resize { .. } = op.op_type {
@@ -1498,6 +1545,8 @@ unsafe extern "C" fn seat_op_start_pointer(
start_win_h: 0,
start_win_virtual_x: 0.0,
start_win_virtual_y: 0.0,
+ start_pan_x: (*(*seat).server).wm.desk_pan_x,
+ start_pan_y: (*(*seat).server).wm.desk_pan_y,
start_tiling_mode: crate::tiling::TilingMode::Floating,
start_mode_locked: false,
started_in_overview: false,
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 610c7f5..c590a44 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -116,6 +116,12 @@ pub struct WindowManager {
pub target_desk_pan_x: Option<f64>,
pub target_desk_pan_y: Option<f64>,
pub animation_timer: *mut ffi::wl_event_source,
+ /// Edge auto-pan velocity during an interactive move/resize, in SCREEN
+ /// px/s (the tick divides by zoom). Written by `Seat::update_edge_pan`
+ /// on every op motion; both zero when the cursor is outside the bands.
+ pub edge_pan_vx: f64,
+ pub edge_pan_vy: f64,
+ pub edge_pan_timer: *mut ffi::wl_event_source,
pub has_restored_focused_window: bool,
pub restored_focused_window_mapped: bool,
pub last_viewport_zoom: f64,
@@ -185,6 +191,9 @@ impl WindowManager {
self.target_desk_pan_x = None;
self.target_desk_pan_y = None;
self.animation_timer = std::ptr::null_mut();
+ self.edge_pan_vx = 0.0;
+ self.edge_pan_vy = 0.0;
+ self.edge_pan_timer = std::ptr::null_mut();
self.viewport_is_active = false;
self.viewport_settle_timer = std::ptr::null_mut();
self.desk_zoom = 1.0;
@@ -639,6 +648,10 @@ impl WindowManager {
ffi::wl_event_source_remove(self.animation_timer);
self.animation_timer = std::ptr::null_mut();
}
+ if !self.edge_pan_timer.is_null() {
+ ffi::wl_event_source_remove(self.edge_pan_timer);
+ self.edge_pan_timer = std::ptr::null_mut();
+ }
if !self.viewport_settle_timer.is_null() {
ffi::wl_event_source_remove(self.viewport_settle_timer);
self.viewport_settle_timer = std::ptr::null_mut();
@@ -651,6 +664,28 @@ impl WindowManager {
self.target_desk_pan_y = None;
}
+ /// Record the edge auto-pan velocity (screen px/s) and arm its 16ms tick
+ /// when nonzero. A zero velocity just parks: the armed tick sees it and
+ /// stops itself without re-arming.
+ pub unsafe fn set_edge_pan_velocity(&mut self, vx: f64, vy: f64) {
+ self.edge_pan_vx = vx;
+ self.edge_pan_vy = vy;
+ if vx == 0.0 && vy == 0.0 {
+ return;
+ }
+ if self.edge_pan_timer.is_null() {
+ let event_loop = ffi::wl_display_get_event_loop((*self.server).wl_server);
+ self.edge_pan_timer = ffi::wl_event_loop_add_timer(
+ event_loop,
+ Some(handle_edge_pan_tick),
+ self as *mut WindowManager as *mut _,
+ );
+ }
+ if !self.edge_pan_timer.is_null() {
+ ffi::wl_event_source_timer_update(self.edge_pan_timer, 16);
+ }
+ }
+
/// Arm the pan animation timer (creating it on first use): every 16ms
/// `handle_panning_animation_tick` eases `desk_pan_x/y` toward
/// `target_desk_pan_x/y`. Callers set the targets first.
@@ -1152,8 +1187,8 @@ impl WindowManager {
let scale = self.desk_zoom;
let dx = op.x - op.start_x;
let dy = op.y - op.start_y;
- let virtual_dx = dx as f64 / scale;
- let virtual_dy = dy as f64 / scale;
+ let virtual_dx = dx as f64 / scale + (self.desk_pan_x - op.start_pan_x);
+ let virtual_dy = dy as f64 / scale + (self.desk_pan_y - op.start_pan_y);
// Same math (and snapping) as the seat op's Resize
// arm — this recomputation feeds the arrange
// snapshot and must not diverge from it.
@@ -3966,6 +4001,41 @@ pub(crate) unsafe extern "C" fn handle_viewport_settle_tick(data: *mut std::ffi:
0
}
+/// 16ms edge auto-pan tick: scroll the desktop by the current velocity, then
+/// re-run the seat op at its last cursor position so the dragged window keeps
+/// tracking the (pinned) cursor — the op's pan-delta term turns the scroll
+/// into window motion. op_update re-derives the velocity and re-arms this
+/// timer, so the loop sustains itself until the op ends or the cursor leaves
+/// the edge bands; then it stops without re-arming.
+pub(crate) unsafe extern "C" fn handle_edge_pan_tick(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
+ let wm = data as *mut WindowManager;
+ if wm.is_null() {
+ return 0;
+ }
+ let (vx, vy) = ((*wm).edge_pan_vx, (*wm).edge_pan_vy);
+ if vx == 0.0 && vy == 0.0 {
+ return 0;
+ }
+ let seat = match (*wm).first_seat() {
+ Some(seat) if (*seat).op.is_some() => seat,
+ _ => {
+ (*wm).edge_pan_vx = 0.0;
+ (*wm).edge_pan_vy = 0.0;
+ return 0;
+ }
+ };
+ let (ox, oy) = {
+ let op = (*seat).op.as_ref().unwrap();
+ (op.x, op.y)
+ };
+ let dt = 0.016;
+ let zoom = (*wm).desk_zoom.max(0.01);
+ (*wm).desk_pan_x += vx * dt / zoom;
+ (*wm).desk_pan_y += vy * dt / zoom;
+ (*seat).op_update(ox, oy);
+ 0
+}
+
pub(crate) unsafe extern "C" fn handle_panning_animation_tick(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
let wm = data as *mut WindowManager;
if wm.is_null() {
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index 56a085f..0782a7f 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -826,6 +826,8 @@ unsafe extern "C" fn handle_request_move(
start_win_virtual_y: (*window).virtual_y,
start_tiling_mode: (*window).tiling_mode,
start_mode_locked: (*window).mode_locked,
+ start_pan_x: (*(*window).server).wm.desk_pan_x,
+ start_pan_y: (*(*window).server).wm.desk_pan_y,
started_in_overview: (*(*window).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
});
cursor.op_start_pointer();
@@ -881,6 +883,8 @@ unsafe extern "C" fn handle_request_resize(
start_win_virtual_y: (*window).virtual_y,
start_tiling_mode: (*window).tiling_mode,
start_mode_locked: (*window).mode_locked,
+ start_pan_x: (*(*window).server).wm.desk_pan_x,
+ start_pan_y: (*(*window).server).wm.desk_pan_y,
started_in_overview: (*(*window).server).wm.mode == crate::window_manager::WindowManagerMode::Overview,
});
cursor.op_start_pointer();