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

commitbc24779ad18b8bb652a13dc39d77ccc9b657d455
parent3f3d05c1c9
authorLucas Galante <[email protected]>
date2026-05-23 15:34
Ignore clear-status-interface in focus/tiling; support logical screen geometry scaling

 src/borders.rs |  6 +++++-
 src/ipc.rs     |  8 ++++----
 src/tiling.rs  | 20 ++++++++++----------
 src/wayland.rs | 39 +++++++++++++++++++++++++++------------
 src/wm.rs      | 51 ++++++++++++++++++++++++++++++++++++++++-----------
 5 files changed, 86 insertions(+), 38 deletions(-)

diff --git a/src/borders.rs b/src/borders.rs
index be0d493..c0f82b7 100644
--- a/src/borders.rs
+++ b/src/borders.rs
@@ -107,7 +107,7 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
             (r, g, b, ALPHA)
         };
 
-        let width = match win.tiling_mode {
+        let mut width = match win.tiling_mode {
             TilingMode::Cascade => state.layout.cascade_border_width,
             TilingMode::Fullscreen => state.layout.fullscreen_border_width,
             TilingMode::Grid => state.layout.grid_border_width,
@@ -116,6 +116,10 @@ pub fn compute_border_colors(state: &WindowManager) -> Vec<WindowBorders> {
             TilingMode::Floating => state.layout.floating_border_width,
         };
 
+        if win.app_id.as_deref() == Some("clear-status-interface") {
+            width = 0;
+        }
+
         eprintln!(
             "[borders]   -> r=#{:08x} g=#{:08x} b=#{:08x} a=#{:08x} width={}",
             r, g, b, a, width,
diff --git a/src/ipc.rs b/src/ipc.rs
index 76f2771..41d34da 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -47,7 +47,7 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) {
                 let visible_ids: Vec<u64> = state
                     .windows
                     .iter()
-                    .filter(|w| (w.tags & active_tags) != 0 && !w.closed)
+                    .filter(|w| (w.tags & active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                     .map(|w| w.id)
                     .collect();
                 if visible_ids.len() > 1 {
@@ -86,7 +86,7 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) {
                         let visible_ids: Vec<u64> = state
                             .windows
                             .iter()
-                            .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed)
+                            .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                             .map(|w| w.id)
                             .collect();
                         seat.focused_window_id = visible_ids.last().copied();
@@ -116,7 +116,7 @@ pub fn handle_ipc_command(cmd: &str, state: &mut WindowManager) {
                         let visible_ids: Vec<u64> = state
                             .windows
                             .iter()
-                            .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed)
+                            .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                             .map(|w| w.id)
                             .collect();
                         if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
@@ -504,7 +504,7 @@ fn handle_set_tag_command(rest: &str, state: &mut WindowManager) {
                     let visible_ids: Vec<u64> = state
                         .windows
                         .iter()
-                        .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed)
+                        .filter(|w| (w.tags & state.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                         .map(|w| w.id)
                         .collect();
                     if let Some(seat) = state.seats.iter_mut().find(|s| !s.removed) {
diff --git a/src/tiling.rs b/src/tiling.rs
index baf262c..47e8ebb 100644
--- a/src/tiling.rs
+++ b/src/tiling.rs
@@ -35,7 +35,7 @@ pub fn tile_cascade(
     idx: i32,
 ) -> (i32, i32, i32, i32) {
     let width = screen_w - gap_left - gap_right - bw * 2 - cascade_offset * (n_cascade - 1);
-    let height = screen_h - gap_top - gap_bottom - bw * 2 - cascade_offset * (n_cascade - 1);
+    let height = screen_h - bar_height - gap_top - gap_bottom - bw * 2 - cascade_offset * (n_cascade - 1);
     let width = if width < 1 { 1 } else { width };
     let height = if height < 1 { 1 } else { height };
     let x = gap_left + bw + idx * cascade_offset;
@@ -72,7 +72,7 @@ pub fn tile_grid(
     let col = idx % cols;
     let rows = (n_grid + cols - 1) / cols;
     let width = (screen_w - gap_left - gap_right - (cols - 1) * gap) / cols - 2 * bw;
-    let height = (screen_h - gap_top - gap_bottom - (rows - 1) * gap) / rows - 2 * bw;
+    let height = (screen_h - bar_height - gap_top - gap_bottom - (rows - 1) * gap) / rows - 2 * bw;
     let width = if width < 1 { 1 } else { width };
     let height = if height < 1 { 1 } else { height };
     let x = gap_left + bw + col * (width + 2 * bw + gap);
@@ -205,8 +205,8 @@ mod tests {
         assert_eq!(y, 64); // bar_height + gap_top + bw
                            // w = 1920 - 18 - 18 - 18*2 - 32*0 = 1920 - 72 = 1848
         assert_eq!(w, 1848);
-        // h = 1080 - 18 - 18 - 18*2 - 32*0 = 1080 - 72 = 1008
-        assert_eq!(h, 1008);
+        // h = 1080 - 28 (bar) - 18 - 18 - 18*2 - 32*0 = 1080 - 100 = 980
+        assert_eq!(h, 980);
     }
 
     #[test]
@@ -233,8 +233,8 @@ mod tests {
         assert_eq!(y, 28 + 10 + 6);   // bar_height + gap_top + bw = 44
                                        // w = 1920 - 20 - 30 - 6*2 = 1920 - 62 = 1858
         assert_eq!(w, 1858);
-        // h = 1080 - 10 - 40 - 6*2 = 1080 - 62 = 1018
-        assert_eq!(h, 1018);
+        // h = 1080 - 28 (bar) - 10 - 40 - 6*2 = 1080 - 90 = 990
+        assert_eq!(h, 990);
     }
 
     #[test]
@@ -252,8 +252,8 @@ mod tests {
 
         // Width = (1920 - 18 - 18 - 1*18) / 2 - 2*18 = (1920-54)/2 - 36 = 933 - 36 = 897
         assert_eq!(w0, 897);
-        // rows=1, Height = (1080 - 18 - 18 - 0*18) / 1 - 2*18 = 1044 - 36 = 1008
-        assert_eq!(h0, 1008);
+        // rows=1, Height = (1080 - 28 (bar) - 18 - 18 - 0*18) / 1 - 2*18 = 1016 - 36 = 980
+        assert_eq!(h0, 980);
     }
 
     #[test]
@@ -288,8 +288,8 @@ mod tests {
         assert_eq!(x0, 26);
         // y0 = bar_height + gap_top + bw = 28 + 10 + 6 = 44
         assert_eq!(y0, 44);
-        // rows=1, Height = (1080 - 10 - 40 - 0*12) / 1 - 2*6 = 1030 - 12 = 1018
-        assert_eq!(h0, 1018);
+        // rows=1, Height = (1080 - 28 (bar) - 10 - 40 - 0*12) / 1 - 2*6 = 1002 - 12 = 990
+        assert_eq!(h0, 990);
 
         // x1 = gap_left + bw + 1*(917 + 2*6 + 12) = 26 + 941 = 967
         assert_eq!(x1, 967);
diff --git a/src/wayland.rs b/src/wayland.rs
index dc16c5d..4fbf2bf 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -428,7 +428,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                                 .windows
                                 .iter()
                                 .filter(|w| {
-                                    !w.closed && (w.tags & active_tags) != 0 && w.id != *closed_id
+                                    !w.closed && (w.tags & active_tags) != 0 && w.id != *closed_id && w.app_id.as_deref() != Some("clear-status-interface")
                                 })
                                 .map(|w| w.id)
                                 .collect();
@@ -653,7 +653,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                         .wm
                         .windows
                         .iter()
-                        .filter(|w| w.is_new && !w.closed && (w.tags & active_tags) != 0)
+                        .filter(|w| w.is_new && !w.closed && (w.tags & active_tags) != 0 && w.app_id.as_deref() != Some("clear-status-interface"))
                         .map(|w| w.id)
                         .last();
                     if let Some(new_id) = new_focused_id {
@@ -798,11 +798,22 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
                                     .wm
                                     .get_window(focused_id)
                                     .and_then(|w| w.app_id.clone());
-                                eprintln!(
-                                    "[render] place_top for focused window id={} (app_id={:?})",
-                                    focused_id, app_id
-                                );
-                                node.place_top();
+                                if app_id.as_deref() != Some("clear-status-interface") {
+                                    eprintln!(
+                                        "[render] place_top for focused window id={} (app_id={:?})",
+                                        focused_id, app_id
+                                    );
+                                    node.place_top();
+                                }
+                            }
+                        }
+                    }
+
+                    // Enforce that clear-status-interface is placed at the bottom
+                    for &(wid, ref node) in &state.window_nodes {
+                        if let Some(win) = state.wm.get_window(wid) {
+                            if win.app_id.as_deref() == Some("clear-status-interface") {
+                                node.place_bottom();
                             }
                         }
                     }
@@ -1210,6 +1221,10 @@ impl Dispatch<RiverSeatV1, ()> for AppState {
             } => {
                 if let Some(wid) = state.window_id_for_proxy(&river_window) {
                     let target_app_id = state.wm.get_window(wid).and_then(|w| w.app_id.clone());
+                    if target_app_id.as_deref() == Some("clear-status-interface") {
+                        // Do not focus status bar!
+                        return;
+                    }
                     if let Some(seat) = state.wm.seats.iter_mut().find(|s| s.id == sid) {
                         eprintln!(
                             "[focus] WindowInteraction: seat={} focused_window_id={} -> {} (app_id={:?})",
@@ -1668,7 +1683,7 @@ fn execute_action(state: &mut AppState, action: &crate::types::Action, command:
                         .windows
                         .iter()
                         .filter(|w| {
-                            (w.tags & state.wm.active_tags) != 0 && !w.closed && w.id != focused_id
+                            (w.tags & state.wm.active_tags) != 0 && !w.closed && w.id != focused_id && w.app_id.as_deref() != Some("clear-status-interface")
                         })
                         .map(|w| w.id)
                         .collect();
@@ -1691,7 +1706,7 @@ fn execute_action(state: &mut AppState, action: &crate::types::Action, command:
                     .wm
                     .windows
                     .iter()
-                    .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed)
+                    .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                     .map(|w| w.id)
                     .collect();
                 if let Some(fid) = focused_id {
@@ -1907,7 +1922,7 @@ fn execute_action(state: &mut AppState, action: &crate::types::Action, command:
                 .wm
                 .windows
                 .iter()
-                .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed)
+                .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                 .map(|w| w.id)
                 .collect();
             if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
@@ -1946,7 +1961,7 @@ fn execute_action(state: &mut AppState, action: &crate::types::Action, command:
                     .wm
                     .windows
                     .iter()
-                    .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed)
+                    .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                     .map(|w| w.id)
                     .collect();
                 if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
@@ -1988,7 +2003,7 @@ fn execute_action(state: &mut AppState, action: &crate::types::Action, command:
                         .wm
                         .windows
                         .iter()
-                        .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed)
+                        .filter(|w| (w.tags & state.wm.active_tags) != 0 && !w.closed && w.app_id.as_deref() != Some("clear-status-interface"))
                         .map(|w| w.id)
                         .collect();
                     if let Some(seat) = state.wm.seats.iter_mut().find(|s| !s.removed) {
diff --git a/src/wm.rs b/src/wm.rs
index 2f97f50..7ad5c5c 100644
--- a/src/wm.rs
+++ b/src/wm.rs
@@ -19,6 +19,10 @@ use wayland_client::QueueHandle;
 /// Returns the resolved TilingMode, or None if the window's mode is locked
 /// (i.e., the user manually set it and it should not be overridden).
 pub fn get_mode_for_window(wm: &WindowManager, win: &Window) -> Option<TilingMode> {
+    if win.app_id.as_deref() == Some("clear-status-interface") {
+        return Some(TilingMode::Fullscreen);
+    }
+
     // If the user manually locked the mode (via set-mode, fullscreen toggle, etc.),
     // don't override it.
     if win.mode_locked {
@@ -73,6 +77,13 @@ pub fn get_mode_for_window(wm: &WindowManager, win: &Window) -> Option<TilingMod
 /// Assign tiling modes to all windows that aren't mode_locked.
 /// Should be called during ManageStart before compute_tiling.
 pub fn assign_window_modes(wm: &mut WindowManager) {
+    // Enforce that clear-status-interface is assigned all tags so that it is always visible
+    for win in &mut wm.windows {
+        if win.app_id.as_deref() == Some("clear-status-interface") {
+            win.tags = u32::MAX;
+        }
+    }
+
     // Collect assignments first (borrow checker: can't borrow wm mutably while iterating mode_rules)
     let assignments: Vec<(u64, TilingMode)> = wm
         .windows
@@ -146,15 +157,28 @@ fn get_screen_geometry(wm: &WindowManager) -> (i32, i32, i32, i32, i32, i32) {
         _ => return (800, 600, 800, 600, 0, 0),
     };
 
+    let scale = if wm.output_scale > 0.0 { wm.output_scale } else { 1.0 };
+
     let mut uw = output.usable_width;
     let mut uh = output.usable_height;
 
-    // Fall back to physical output dimensions if usable area not yet set
-    if uw <= 0 && output.width > 0 {
-        uw = output.width;
+    let log_width = if output.width > 0 {
+        (output.width as f64 / scale).round() as i32
+    } else {
+        0
+    };
+    let log_height = if output.height > 0 {
+        (output.height as f64 / scale).round() as i32
+    } else {
+        0
+    };
+
+    // Fall back to logical output dimensions if usable area not yet set
+    if uw <= 0 && log_width > 0 {
+        uw = log_width;
     }
-    if uh <= 0 && output.height > 0 {
-        uh = output.height;
+    if uh <= 0 && log_height > 0 {
+        uh = log_height;
     }
 
     if uw <= 0 {
@@ -164,8 +188,8 @@ fn get_screen_geometry(wm: &WindowManager) -> (i32, i32, i32, i32, i32, i32) {
         uh = 600;
     }
 
-    let pw = if output.width > 0 { output.width } else { uw };
-    let ph = if output.height > 0 { output.height } else { uh };
+    let pw = if log_width > 0 { log_width } else { uw };
+    let ph = if log_height > 0 { log_height } else { uh };
     let px = output.x;
     let py = output.y;
 
@@ -241,13 +265,13 @@ fn compute_tiling(
         .and_then(|s| s.focused_window_id)
         .filter(|&fid| {
             wm.get_window(fid).map_or(false, |w| {
-                (w.tags & wm.active_tags) != 0 && !w.closed && w.tiling_mode == TilingMode::Fullscreen
+                (w.tags & wm.active_tags) != 0 && !w.closed && w.tiling_mode == TilingMode::Fullscreen && w.app_id.as_deref() != Some("clear-status-interface")
             })
         })
         .or_else(|| {
             // Fallback: first fullscreen window if no focused window qualifies
             wm.windows.iter().find(|w| {
-                (w.tags & wm.active_tags) != 0 && !w.closed && w.tiling_mode == TilingMode::Fullscreen
+                (w.tags & wm.active_tags) != 0 && !w.closed && w.tiling_mode == TilingMode::Fullscreen && w.app_id.as_deref() != Some("clear-status-interface")
             }).map(|w| w.id)
         });
 
@@ -268,7 +292,12 @@ fn compute_tiling(
 
         let (x, y, w, h) = match mode {
             TilingMode::Fullscreen => {
-                if fullscreen_id == Some(wid) {
+                if fullscreen_id == Some(wid) || win.app_id.as_deref() == Some("clear-status-interface") {
+                    let border_w = if win.app_id.as_deref() == Some("clear-status-interface") {
+                        0
+                    } else {
+                        wm.layout.fullscreen_border_width
+                    };
                     let (tx, ty, tw, th) = tiling::tile_fullscreen(
                         phys_w,
                         phys_h,
@@ -276,7 +305,7 @@ fn compute_tiling(
                         gap_left,
                         gap_right,
                         gap_bottom,
-                        wm.layout.fullscreen_border_width,
+                        border_w,
                         bar_height,
                     );
                     (tx + phys_x, ty + phys_y, tw, th)