git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commitaa8b7707bbdfefa28eee8bb17ddddde80015fe83
parentc547f252b3
authorLucas Galante <[email protected]>
date2026-07-03 11:01
Support dragging and edge-snapping status windows in adjust-position-mode

 src/server/cursor.rs         |  78 ++++++++++++++++
 src/server/window.rs         |  10 ++
 src/server/window_manager.rs | 211 ++++++++++++++++++++++++++++++-------------
 3 files changed, 237 insertions(+), 62 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 0f5f01f..66769d1 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -624,6 +624,45 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
             return;
         }
 
+        if (*event).button == 0x110 && (*(*seat).server).wm.adjust_position_mode {
+            let mut clicked_status: *mut crate::window::Window = std::ptr::null_mut();
+            if let Some(result) = (*server).scene.at(lx, ly) {
+                if let SceneNodeDataVal::Window(window) = result.data {
+                    if (*window).is_status_bar() {
+                        clicked_status = window;
+                    }
+                }
+            }
+            if !clicked_status.is_null() {
+                (*server).wm.stop_panning_animation();
+                let cursor_x = (*cursor.wlr_cursor).x;
+                let cursor_y = (*cursor.wlr_cursor).y;
+                seat.op = Some(crate::seat::SeatOp {
+                    sent_release: false,
+                    input: crate::seat::SeatOpInput::Pointer,
+                    start_x: cursor_x as i32,
+                    start_y: cursor_y as i32,
+                    x: cursor_x as i32,
+                    y: cursor_y as i32,
+                    window_ptr: clicked_status,
+                    op_type: crate::seat::PointerOpType::Move,
+                    start_win_x: (*clicked_status).box_geom.x,
+                    start_win_y: (*clicked_status).box_geom.y,
+                    start_win_w: (*clicked_status).box_geom.width as u32,
+                    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_tiling_mode: (*clicked_status).tiling_mode,
+                    start_mode_locked: (*clicked_status).mode_locked,
+                    started_in_overview: false,
+                });
+                cursor.op_start_pointer();
+                cursor.pressed.insert((*event).button, None);
+                cursor.set_xcursor(b"grab\0".as_ptr() as *const _);
+                return;
+            }
+        }
+
         // --- ZOOMED OUT CLICK HANDLING ---
         if (*event).button == 0x110 && (*(*seat).server).wm.mode == crate::window_manager::WindowManagerMode::Overview {
             let mut clicked_win: *mut crate::window::Window = std::ptr::null_mut();
@@ -1025,6 +1064,45 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
             seat.op_update(cursor_x as i32, cursor_y as i32);
             
             let op = seat.op.unwrap();
+
+            if (*(*seat).server).wm.adjust_position_mode && !op.window_ptr.is_null() && (*op.window_ptr).is_status_bar() && (*event).button == 0x110 {
+                let win = op.window_ptr;
+                let mut closest_edge = crate::window::StatusEdge::Top;
+                let mut min_dist = f64::MAX;
+                
+                let outputs_list = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
+                let mut curr_out = (*outputs_list).next;
+                while curr_out != outputs_list {
+                    let output = crate::container_of!(curr_out, crate::output::Output, link);
+                    if (*output).sent.state == crate::output::OutputStateValue::Enabled {
+                        let wlr_box = (*output).sent.box_layout();
+                        let ox = wlr_box.x as f64;
+                        let oy = wlr_box.y as f64;
+                        let ow = wlr_box.width as f64;
+                        let oh = wlr_box.height as f64;
+                        
+                        if lx >= ox && lx <= ox + ow && ly >= oy && ly <= oy + oh {
+                            let dt = ly - oy;
+                            let db = (oy + oh) - ly;
+                            let dl = lx - ox;
+                            let dr = (ox + ow) - lx;
+                            
+                            if dt < min_dist { min_dist = dt; closest_edge = crate::window::StatusEdge::Top; }
+                            if db < min_dist { min_dist = db; closest_edge = crate::window::StatusEdge::Bottom; }
+                            if dl < min_dist { min_dist = dl; closest_edge = crate::window::StatusEdge::Left; }
+                            if dr < min_dist { min_dist = dr; closest_edge = crate::window::StatusEdge::Right; }
+                            break;
+                        }
+                    }
+                    curr_out = (*curr_out).next;
+                }
+                
+                (*win).status_edge = closest_edge;
+                seat.op_end();
+                cursor.pressed.remove(&(*event).button);
+                (*server).wm.dirty_windowing();
+                return;
+            }
             if op.started_in_overview && (*event).button == 0x110 {
                 let moved = (cursor_x as i32 - op.start_x).abs() > 5 || (cursor_y as i32 - op.start_y).abs() > 5;
                 if !moved && !op.window_ptr.is_null() {
diff --git a/src/server/window.rs b/src/server/window.rs
index 83ed25d..5c8e60a 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -258,6 +258,15 @@ pub struct Window {
     pub foreign_toplevel_handle: *mut ffi::wlr_ext_foreign_toplevel_handle_v1,
     pub wlr_toplevel_handle: *mut ffi::wlr_foreign_toplevel_handle_v1,
     pub csd_buffer_size_bug: bool,
+    pub status_edge: StatusEdge,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum StatusEdge {
+    Top,
+    Bottom,
+    Left,
+    Right,
 }
 
 impl Window {
@@ -467,6 +476,7 @@ impl Window {
             foreign_toplevel_handle: std::ptr::null_mut(),
             wlr_toplevel_handle: std::ptr::null_mut(),
             csd_buffer_size_bug: false,
+            status_edge: StatusEdge::Top,
         });
 
         ffi::wl_list_init(&mut window.decorations_below);
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 29eb74b..4e97fac 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -109,6 +109,7 @@ pub struct WindowManager {
     pub input_config: crate::config::InputConfig,
     pub last_status_update: std::cell::RefCell<Option<crate::status_server::StatusUpdate>>,
     pub status_hide_mode: bool,
+    pub adjust_position_mode: bool,
     pub restore_queue: Vec<SavedWindowState>,
     pub last_window_states: Vec<SavedWindowState>,
     pub shutting_down: bool,
@@ -195,6 +196,7 @@ impl WindowManager {
         self.input_config = crate::config::InputConfig::default();
         self.last_status_update = std::cell::RefCell::new(None);
         self.status_hide_mode = false;
+        self.adjust_position_mode = false;
 
         ffi::wl_list_init(&mut self.sent.outputs);
         ffi::wl_list_init(&mut self.sent.seats);
@@ -1545,16 +1547,23 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
                     b,
                     a,
                 };
-                (*win_ptr).rendering_requested.blur = self.layout.window_blur;
+(*win_ptr).rendering_requested.blur = self.layout.window_blur;
                 (*win_ptr).rendering_requested.opacity = if is_focused { 1.0f32 } else {
                     if !self.layout.window_opacity { 1.0f32 } else { 0.90f32 }
                 };
             }
-
             // Position status bar windows on this output
-            let mut left_status = Vec::new();
-            let mut right_status = Vec::new();
-            let mut full_status = Vec::new();
+            let bar_h = self.layout.bar_height as u32;
+            let spacing = 12;
+            let margin = 12;
+
+            let mut top_left = Vec::new();
+            let mut top_right = Vec::new();
+            let mut bottom_left = Vec::new();
+            let mut bottom_right = Vec::new();
+            let mut left_side = Vec::new();
+            let mut right_side = Vec::new();
+            let mut full_top = Vec::new();
 
             for &win_ptr in self.windows.iter() {
                 if win_ptr.is_null() || (*win_ptr).closed {
@@ -1564,12 +1573,49 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
                     continue;
                 }
                 if let Some(app_id) = (*win_ptr).get_app_id_string() {
-                    if app_id.starts_with("cce-status-left-") {
-                        left_status.push(win_ptr);
-                    } else if app_id.starts_with("cce-status-right-") {
-                        right_status.push(win_ptr);
-                    } else if app_id.starts_with("cce-status") {
-                        full_status.push(win_ptr);
+                    if app_id.starts_with("cce-status") {
+                        // Skip if currently being dragged interactively
+                        let mut is_being_dragged = false;
+                        let seats_list = &mut (*self.server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+                        let mut curr_seat = (*seats_list).next;
+                        while curr_seat != seats_list {
+                            let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
+                            if let Some(ref op) = (*seat).op {
+                                if op.window_ptr == win_ptr && matches!(op.op_type, crate::seat::PointerOpType::Move) {
+                                    is_being_dragged = true;
+                                    break;
+                                }
+                            }
+                            curr_seat = (*curr_seat).next;
+                        }
+                        if is_being_dragged {
+                            continue;
+                        }
+
+                        match (*win_ptr).status_edge {
+                            crate::window::StatusEdge::Top => {
+                                if app_id.starts_with("cce-status-left-") {
+                                    top_left.push(win_ptr);
+                                } else if app_id.starts_with("cce-status-right-") {
+                                    top_right.push(win_ptr);
+                                } else {
+                                    full_top.push(win_ptr);
+                                }
+                            }
+                            crate::window::StatusEdge::Bottom => {
+                                if app_id.starts_with("cce-status-left-") {
+                                    bottom_left.push(win_ptr);
+                                } else {
+                                    bottom_right.push(win_ptr);
+                                }
+                            }
+                            crate::window::StatusEdge::Left => {
+                                left_side.push(win_ptr);
+                            }
+                            crate::window::StatusEdge::Right => {
+                                right_side.push(win_ptr);
+                            }
+                        }
                     }
                 }
             }
@@ -1577,81 +1623,108 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
             const LEFT_ORDER: &[&str] = &["viewport", "window"];
             const RIGHT_ORDER: &[&str] = &["tray", "cpu", "memory", "brightness", "volume", "battery", "clock"];
 
-            left_status.sort_by_key(|&w| unsafe {
-                let app_id = (*w).get_app_id_string().unwrap_or_default();
-                let name = app_id.strip_prefix("cce-status-left-").unwrap_or(&app_id);
-                LEFT_ORDER.iter().position(|&m| m == name).unwrap_or(99)
-            });
+            let sort_left = |w_list: &mut Vec<*mut Window>| {
+                w_list.sort_by_key(|&w| unsafe {
+                    let app_id = (*w).get_app_id_string().unwrap_or_default();
+                    let name = app_id.strip_prefix("cce-status-left-").unwrap_or(&app_id);
+                    LEFT_ORDER.iter().position(|&m| m == name).unwrap_or(99)
+                });
+            };
 
-            right_status.sort_by_key(|&w| unsafe {
-                let app_id = (*w).get_app_id_string().unwrap_or_default();
-                let name = app_id.strip_prefix("cce-status-right-").unwrap_or(&app_id);
-                RIGHT_ORDER.iter().position(|&m| m == name).unwrap_or(99)
-            });
+            let sort_right = |w_list: &mut Vec<*mut Window>| {
+                w_list.sort_by_key(|&w| unsafe {
+                    let app_id = (*w).get_app_id_string().unwrap_or_default();
+                    let name = app_id.strip_prefix("cce-status-right-").unwrap_or(&app_id);
+                    RIGHT_ORDER.iter().position(|&m| m == name).unwrap_or(99)
+                });
+            };
 
-            let bar_h = self.layout.bar_height as u32;
-            let spacing = 12;
-            let margin = 12;
+            sort_left(&mut top_left);
+            sort_right(&mut top_right);
+            sort_left(&mut bottom_left);
+            sort_right(&mut bottom_right);
 
-            let status_y = if self.status_hide_mode {
+            // 1. Top Edge
+            let status_y_top = if self.status_hide_mode {
                 let preview = self.layout.status_module_hide_mode_preview as i32;
                 wlr_box.y - (bar_h as i32 - preview)
             } else {
                 wlr_box.y
             };
 
-            // Layout Left status windows
             let mut cur_left_x = wlr_box.x + margin;
-            for win_ptr in left_status {
+            for win_ptr in top_left {
                 let w = if (*win_ptr).box_geom.width > 0 { (*win_ptr).box_geom.width as u32 } else { 100 };
-                let app_id = (*win_ptr).get_app_id_string().unwrap_or_default();
-                log::info!("[ArrangeStatus] Left module app_id={} x={} y={} w={}", app_id, cur_left_x, status_y, w);
                 (*win_ptr).rendering_requested.x = cur_left_x;
-                (*win_ptr).rendering_requested.y = status_y;
-                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
-                    width: w,
-                    height: bar_h,
-                });
-                (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
-                    width: w,
-                    height: bar_h,
-                };
+                (*win_ptr).rendering_requested.y = status_y_top;
+                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: w, height: bar_h });
+                (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: w, height: bar_h };
                 cur_left_x += w as i32 + spacing;
             }
 
-            // Layout Right status windows (right-to-left)
             let mut cur_right_x = wlr_box.x + wlr_box.width - margin;
-            for win_ptr in right_status.into_iter().rev() {
+            for win_ptr in top_right.into_iter().rev() {
                 let actual_w = (*win_ptr).box_geom.width;
                 let w = if actual_w > 0 { actual_w as u32 } else { 100 };
                 let x = cur_right_x - w as i32;
-                let app_id = (*win_ptr).get_app_id_string().unwrap_or_default();
-                log::info!("[ArrangeStatus] Right module app_id={} actual_box_w={} x={} y={} w={}", app_id, actual_w, x, status_y, w);
                 (*win_ptr).rendering_requested.x = x;
-                (*win_ptr).rendering_requested.y = status_y;
-                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
-                    width: w,
-                    height: bar_h,
-                });
-                (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
-                    width: w,
-                    height: bar_h,
-                };
+                (*win_ptr).rendering_requested.y = status_y_top;
+                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: w, height: bar_h });
+                (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: w, height: bar_h };
                 cur_right_x = x - spacing;
             }
 
-            // Layout Full/Legacy status windows
-            for win_ptr in full_status {
+            for win_ptr in full_top {
                 (*win_ptr).rendering_requested.x = wlr_box.x;
-                (*win_ptr).rendering_requested.y = status_y;
-                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
-                    width: wlr_box.width as u32,
-                    height: bar_h,
-                });
-                (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
-                    width: wlr_box.width as u32,
-                    height: bar_h,
-                };
+                (*win_ptr).rendering_requested.y = status_y_top;
+                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: wlr_box.width as u32, height: bar_h });
+                (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: wlr_box.width as u32, height: bar_h };
+            }
+
+            // 2. Bottom Edge
+            let status_y_bottom = wlr_box.y + wlr_box.height - bar_h as i32;
+            let mut cur_left_x = wlr_box.x + margin;
+            for win_ptr in bottom_left {
+                let w = if (*win_ptr).box_geom.width > 0 { (*win_ptr).box_geom.width as u32 } else { 100 };
+                (*win_ptr).rendering_requested.x = cur_left_x;
+                (*win_ptr).rendering_requested.y = status_y_bottom;
+                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: w, height: bar_h });
+                (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: w, height: bar_h };
+                cur_left_x += w as i32 + spacing;
+            }
+
+            let mut cur_right_x = wlr_box.x + wlr_box.width - margin;
+            for win_ptr in bottom_right.into_iter().rev() {
+                let actual_w = (*win_ptr).box_geom.width;
+                let w = if actual_w > 0 { actual_w as u32 } else { 100 };
+                let x = cur_right_x - w as i32;
+                (*win_ptr).rendering_requested.x = x;
+                (*win_ptr).rendering_requested.y = status_y_bottom;
+                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: w, height: bar_h });
+                (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: w, height: bar_h };
+                cur_right_x = x - spacing;
+            }
+
+            // 3. Left Edge (Vertical stacking)
+            let mut cur_left_y = wlr_box.y + margin;
+            for win_ptr in left_side {
+                let actual_h = if (*win_ptr).box_geom.width > 0 { (*win_ptr).box_geom.width as u32 } else { 100 };
+                (*win_ptr).rendering_requested.x = wlr_box.x;
+                (*win_ptr).rendering_requested.y = cur_left_y;
+                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: bar_h, height: actual_h });
+                (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: bar_h, height: actual_h };
+                cur_left_y += actual_h as i32 + spacing;
+            }
+
+            // 4. Right Edge (Vertical stacking)
+            let mut cur_right_y = wlr_box.y + margin;
+            for win_ptr in right_side {
+                let actual_h = if (*win_ptr).box_geom.width > 0 { (*win_ptr).box_geom.width as u32 } else { 100 };
+                (*win_ptr).rendering_requested.x = wlr_box.x + wlr_box.width - bar_h as i32;
+                (*win_ptr).rendering_requested.y = cur_right_y;
+                (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: bar_h, height: actual_h });
+                (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: bar_h, height: actual_h };
+                cur_right_y += actual_h as i32 + spacing;
             }
         }
         // If the focused window is no longer visible, refocus
@@ -2392,6 +2465,20 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
                 self.dirty_windowing();
                 return format!("ok {}\n", enable);
             }
+            "adjust-position-mode" => {
+                let enable = if parts.len() >= 2 {
+                    match parts[1] {
+                        "true" | "on" | "enable" | "1" => true,
+                        "false" | "off" | "disable" | "0" => false,
+                        _ => !self.adjust_position_mode,
+                    }
+                } else {
+                    !self.adjust_position_mode
+                };
+                self.adjust_position_mode = enable;
+                self.dirty_windowing();
+                return format!("ok {}\n", enable);
+            }
             "view" => {
                 if parts.len() < 2 { return "error: missing tag\n".to_string(); }
                 if let Ok(tag) = parts[1].parse::<i32>() {