Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
refactor: extract pure window-management policy layer from arrange_views
First step of the compositor/window-manager split: new src/server/policy/
module holds FFI-free policy code, seeded with the Policy/Compositor trait
boundary (api.rs) and the pure logic carved out of window_manager.rs:
- tiling.rs moved into policy/ (re-exported as crate::tiling)
- SavedState/SavedWindowState moved to policy/state.rs (same serde format)
- policy/arrange.rs: status-bar nine-region layout engine, usable-area
computation, window classification, overlay placement, the Maximized
enter/exit state machine, per-mode placement (popup/fullscreen/
maximized grid-snap/pannable), offscreen culling, opacity policy
- WindowRole::from_app_id centralizes the "cce-wallpaper"/"cce-status"
app_id conventions; Window::is_wallpaper/is_status_bar delegate to it
- StatusEdge moved to policy (re-exported via window.rs)
arrange_views drops from ~870 to ~460 lines and now only builds plain-data
snapshots and applies returned plans to the scene graph. Behavior-identical
by construction: persistent state writes (box_geom, virtual pos, saved
maximized geometry) are explicit plan fields, and loop order (including
multi-output last-wins and overlay demotion) is preserved. 14 new unit
tests pin grid snapping, bar layout, transitions, and culling.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_014qnNdSob3VnZC9yGyiuGMA
src/lib.rs | 5 +-
src/server/policy/api.rs | 142 ++++++
src/server/policy/arrange.rs | 1014 +++++++++++++++++++++++++++++++++++++
src/server/policy/mod.rs | 20 +
src/server/policy/state.rs | 32 ++
src/server/{ => policy}/tiling.rs | 0
src/server/window.rs | 21 +-
src/server/window_manager.rs | 930 +++++++++-------------------------
8 files changed, 1466 insertions(+), 698 deletions(-)
diff --git a/src/lib.rs b/src/lib.rs
index 1a98484..e2cb64d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,8 +21,9 @@ pub mod xkb_bindings;
pub mod layer_shell;
#[path = "server/scene.rs"]
pub mod scene;
-#[path = "server/tiling.rs"]
-pub mod tiling;
+#[path = "server/policy/mod.rs"]
+pub mod policy;
+pub use policy::tiling;
#[path = "server/config.rs"]
pub mod config;
#[path = "server/ipc_server.rs"]
diff --git a/src/server/policy/api.rs b/src/server/policy/api.rs
new file mode 100644
index 0000000..99e222d
--- /dev/null
+++ b/src/server/policy/api.rs
@@ -0,0 +1,142 @@
+// The Policy / Compositor trait boundary.
+//
+// `Policy` is implemented by the window-management side: it receives events
+// and decides placement, focus, decoration, and background — using only the
+// plain-data types in this file, never FFI. `Compositor` is implemented by
+// the mechanism side (`window_manager.rs` and friends): it executes those
+// decisions against the wlroots/scenefx scene graph.
+//
+// Skeleton status: nothing implements these traits yet. The migration plan is
+// to split `arrange_views()` into a policy half (compute placements) and a
+// mechanism half (apply to scene), then route window lifecycle, input actions,
+// and the animation tick through `Policy`. Effects are declarative on purpose:
+// new scenefx capabilities extend `EffectSpec` without changing either trait.
+
+use super::state::SavedState;
+
+/// Opaque handle to a window. Wraps the `SlotMap` key that the mechanism side
+/// uses internally; policy code never sees a pointer.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct WindowId(pub crate::slotmap::Key);
+
+/// What a surface is for. Assigned once at map time, this replaces scattered
+/// app_id string-matching (`"cce-wallpaper"`, `"cce-status*"`) in mechanism code.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum WindowRole {
+ Normal,
+ StatusBar,
+ Background,
+ Overlay,
+}
+
+impl WindowRole {
+ /// The single place the special app_id conventions are interpreted.
+ /// `Overlay` is never derived from an app_id — it comes from tiling mode.
+ pub fn from_app_id(app_id: Option<&str>) -> Self {
+ match app_id {
+ Some("cce-wallpaper") => WindowRole::Background,
+ Some(id) if id.starts_with("cce-status") => WindowRole::StatusBar,
+ _ => WindowRole::Normal,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct WindowInfo {
+ pub app_id: String,
+ pub title: String,
+ pub role: WindowRole,
+ pub cmdline: String,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Rect {
+ pub x: i32,
+ pub y: i32,
+ pub width: i32,
+ pub height: i32,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Rgba(pub [f32; 4]);
+
+/// Server-side decoration for one window: borders now, titlebars later.
+#[derive(Debug, Clone, PartialEq)]
+pub struct DecorationSpec {
+ pub border_width: i32,
+ pub border_color: Rgba,
+ pub corner_radius: i32,
+}
+
+/// Declarative per-window scenefx effects.
+#[derive(Debug, Clone, PartialEq)]
+pub struct EffectSpec {
+ pub opacity: f32,
+ pub blur: bool,
+ pub shadow: Option<ShadowSpec>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct ShadowSpec {
+ pub color: Rgba,
+ pub blur_sigma: f32,
+}
+
+/// What the background layer shows. Absorbs cce-wallpaper (Solid) and the
+/// grid_tree drawing in `output.rs` (Grid).
+#[derive(Debug, Clone, PartialEq)]
+pub enum BackgroundSpec {
+ Solid(Rgba),
+ Grid(GridSpec),
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct GridSpec {
+ pub background: Rgba,
+ pub line_color: Rgba,
+ pub cell_size: i32,
+ pub line_width: i32,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct OutputInfo {
+ pub width: i32,
+ pub height: i32,
+ pub scale: f32,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum PointerEvent {
+ Press { window: Option<WindowId>, x: f64, y: f64, button: u32 },
+ Release { window: Option<WindowId>, x: f64, y: f64, button: u32 },
+ Motion { x: f64, y: f64 },
+}
+
+/// Commands from policy to mechanism. Implemented by the compositor side;
+/// every method maps onto existing `WindowManager` / scene operations.
+pub trait Compositor {
+ fn place(&mut self, window: WindowId, rect: Rect);
+ fn focus(&mut self, window: Option<WindowId>);
+ fn raise(&mut self, window: WindowId);
+ fn close(&mut self, window: WindowId);
+ /// Pans/zooms windows and the background in the same frame.
+ fn set_viewport(&mut self, pan_x: f64, pan_y: f64, zoom: f64);
+ fn set_decoration(&mut self, window: WindowId, spec: DecorationSpec);
+ fn set_effects(&mut self, window: WindowId, spec: EffectSpec);
+ fn set_background(&mut self, spec: BackgroundSpec);
+ fn spawn(&mut self, cmdline: &str);
+}
+
+/// Events from mechanism to policy. Implemented by the window-management side.
+pub trait Policy {
+ fn window_mapped(&mut self, c: &mut dyn Compositor, window: WindowId, info: &WindowInfo);
+ fn window_unmapped(&mut self, c: &mut dyn Compositor, window: WindowId);
+ fn window_meta_changed(&mut self, c: &mut dyn Compositor, window: WindowId, info: &WindowInfo);
+ fn action(&mut self, c: &mut dyn Compositor, action: &crate::config::Action, arg: Option<&str>);
+ fn pointer(&mut self, c: &mut dyn Compositor, event: PointerEvent);
+ fn output_changed(&mut self, c: &mut dyn Compositor, outputs: &[OutputInfo]);
+ /// Animation driver: easing for pan/zoom targets, effect transitions.
+ fn tick(&mut self, c: &mut dyn Compositor, dt: f64);
+ fn save_state(&self) -> SavedState;
+ fn restore_state(&mut self, c: &mut dyn Compositor, state: SavedState);
+}
diff --git a/src/server/policy/arrange.rs b/src/server/policy/arrange.rs
new file mode 100644
index 0000000..67dd7f9
--- /dev/null
+++ b/src/server/policy/arrange.rs
@@ -0,0 +1,1014 @@
+// Pure layout computation extracted from `WindowManager::arrange_views()`.
+//
+// Functions here take plain-data snapshots and return placement plans; the
+// mechanism side (`window_manager.rs`) builds the snapshots from FFI state and
+// applies the plans to the scene graph. Extraction happens section by section;
+// currently covers the status-bar layout engine.
+
+use super::api::{Rect, WindowRole};
+use super::tiling::TilingMode;
+
+/// Which screen edge/region a status-bar window docks to.
+/// Set from the app_id suffix or config; `Unspecified` resolves to `TopLeft`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum StatusEdge {
+ Unspecified,
+ TopLeft,
+ TopCenter,
+ TopRight,
+ BottomLeft,
+ BottomCenter,
+ BottomRight,
+ Left,
+ Right,
+}
+
+/// Snapshot of one status-bar window, in `self.windows` iteration order.
+/// Windows being interactively dragged are excluded before layout.
+pub struct StatusBarItem {
+ pub app_id: String,
+ pub edge: StatusEdge,
+ /// max(box_geom.width, box_geom.height) — the bar's previous major length.
+ pub prev_len: i32,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct StatusBarPlacement {
+ pub x: i32,
+ pub y: i32,
+ pub width: u32,
+ pub height: u32,
+}
+
+pub struct StatusBarLayoutParams {
+ /// The output's layout box.
+ pub output: Rect,
+ pub bar_height: u32,
+ pub hide_mode: bool,
+ /// Pixels of bar left peeking when hide_mode pushes top bars offscreen.
+ pub hide_mode_preview: i32,
+}
+
+/// The usable (tileable) area of an output: the layout box, shrunk by the
+/// layer-shell non-exclusive area and by one bar-height per screen edge that
+/// has a status bar docked to it. Top bars reserve no space in hide mode.
+///
+/// `non_exclusive` is relative to the output box; a zero-sized rect means no
+/// layer-shell exclusion. `status_edges` holds the raw edge of every live
+/// status-bar window (`Unspecified` resolves to `TopLeft`).
+pub fn compute_usable_area(
+ output: Rect,
+ non_exclusive: Rect,
+ bar_height: i32,
+ status_hide_mode: bool,
+ status_edges: &[StatusEdge],
+) -> Rect {
+ let mut usable = output;
+
+ if non_exclusive.width > 0 && non_exclusive.height > 0 {
+ usable.x = output.x + non_exclusive.x;
+ usable.y = output.y + non_exclusive.y;
+ usable.width = non_exclusive.width;
+ usable.height = non_exclusive.height;
+ }
+
+ let mut has_top = false;
+ let mut has_bottom = false;
+ let mut has_left = false;
+ let mut has_right = false;
+
+ for &edge in status_edges {
+ let edge = if edge == StatusEdge::Unspecified { StatusEdge::TopLeft } else { edge };
+ match edge {
+ StatusEdge::Unspecified | StatusEdge::TopLeft | StatusEdge::TopCenter | StatusEdge::TopRight => {
+ if !status_hide_mode {
+ has_top = true;
+ }
+ }
+ StatusEdge::BottomLeft | StatusEdge::BottomCenter | StatusEdge::BottomRight => {
+ has_bottom = true;
+ }
+ StatusEdge::Left => {
+ has_left = true;
+ }
+ StatusEdge::Right => {
+ has_right = true;
+ }
+ }
+ }
+
+ if has_top {
+ usable.y += bar_height;
+ usable.height -= bar_height;
+ }
+ if has_bottom {
+ usable.height -= bar_height;
+ }
+ if has_left {
+ usable.x += bar_height;
+ usable.width -= bar_height;
+ }
+ if has_right {
+ usable.width -= bar_height;
+ }
+
+ usable
+}
+
+/// How `arrange_views` treats a window this frame. `Background`/`StatusBar`
+/// windows get fixed geometry regardless of visibility; `Hidden` windows are
+/// disabled in the scene; `Overlay` windows get the overlay slot unless they
+/// are mid-drag (then they arrange as `Normal`).
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum WindowClass {
+ Background,
+ StatusBar,
+ Hidden,
+ Overlay,
+ Normal,
+}
+
+pub fn classify_window(
+ role: WindowRole,
+ minimized: bool,
+ closing_or_init: bool,
+ mode: TilingMode,
+ is_moving: bool,
+) -> WindowClass {
+ match role {
+ WindowRole::Background => WindowClass::Background,
+ WindowRole::StatusBar => WindowClass::StatusBar,
+ _ => {
+ if minimized || closing_or_init {
+ WindowClass::Hidden
+ } else if mode == TilingMode::Overlay && !is_moving {
+ WindowClass::Overlay
+ } else {
+ WindowClass::Normal
+ }
+ }
+ }
+}
+
+/// Legacy border width, baked into the overlay geometry formulas.
+const BW: i32 = 0;
+
+/// Overlay windows keep 16px clear above for decorations.
+const OVERLAY_DEC_H: i32 = 16;
+
+/// Windows further than this many pixels outside the output are culled.
+const OFFSCREEN_MARGIN: f64 = 50.0;
+
+pub const OVERLAY_UNFOCUSED_OPACITY: f32 = 0.85;
+pub const NORMAL_UNFOCUSED_OPACITY: f32 = 0.90;
+
+pub fn window_opacity(is_focused: bool, opacity_enabled: bool, unfocused: f32) -> f32 {
+ if is_focused || !opacity_enabled { 1.0 } else { unfocused }
+}
+
+/// Per-output placement context: the physical box, the usable area from
+/// `compute_usable_area`, and the desktop viewport (pan/zoom).
+pub struct PlacementCtx {
+ pub phys: Rect,
+ pub usable: Rect,
+ pub pan_x: f64,
+ pub pan_y: f64,
+ pub zoom: f64,
+}
+
+impl PlacementCtx {
+ fn virtual_to_screen(&self, vx: f64, vy: f64) -> (i32, i32) {
+ (
+ self.phys.x + ((vx - self.pan_x) * self.zoom) as i32,
+ self.phys.y + ((vy - self.pan_y) * self.zoom) as i32,
+ )
+ }
+
+ fn is_offscreen(&self, x: i32, y: i32, scaled_w: f64, scaled_h: f64) -> bool {
+ let viewport_w = self.phys.width as f64;
+ let viewport_h = self.phys.height as f64;
+ (x as f64 + scaled_w + OFFSCREEN_MARGIN) < self.phys.x as f64
+ || (x as f64 - OFFSCREEN_MARGIN) > (self.phys.x as f64 + viewport_w)
+ || (y as f64 + scaled_h + OFFSCREEN_MARGIN) < self.phys.y as f64
+ || (y as f64 - OFFSCREEN_MARGIN) > (self.phys.y as f64 + viewport_h)
+ }
+}
+
+pub struct OverlaySnapshot {
+ pub box_geom: Rect,
+ pub min_width: i32,
+ pub is_cloud: bool,
+ pub ssd: bool,
+ pub decorations_size: (i32, i32),
+}
+
+pub struct OverlayParams {
+ pub overlay_width: i32,
+ pub border_gap: i32,
+ pub position_right: bool,
+ pub cloud_position_default: Option<[i32; 2]>,
+}
+
+pub struct OverlayPlacement {
+ pub pos: (i32, i32),
+ /// Persistent geometry to store back on the window, if placement chose it.
+ pub box_geom_write: Option<Rect>,
+ pub virtual_pos: (f64, f64),
+ pub size: (u32, u32),
+}
+
+/// Place the primary overlay window: fresh windows get the configured overlay
+/// slot (left or right edge, full usable height); cloud windows snap to their
+/// configured default position; anything else keeps its stored geometry.
+pub fn place_overlay_window(
+ snap: &OverlaySnapshot,
+ p: &OverlayParams,
+ ctx: &PlacementCtx,
+) -> OverlayPlacement {
+ let bw = BW;
+ let g = p.border_gap;
+ let dec_h = std::cmp::max(bw, OVERLAY_DEC_H);
+
+ let mut sp_x = snap.box_geom.x;
+ let mut sp_y = snap.box_geom.y;
+ let mut sp_w = snap.box_geom.width;
+ let mut sp_h = snap.box_geom.height;
+ let mut box_geom_write = None;
+
+ if sp_w == 0 || sp_h == 0 {
+ sp_w = if snap.min_width > 32 {
+ std::cmp::max(p.overlay_width, snap.min_width)
+ } else {
+ p.overlay_width
+ };
+ sp_h = (ctx.usable.height - (dec_h + bw) - 2 * g).max(1);
+
+ sp_x = if p.position_right {
+ ctx.usable.x + ctx.usable.width - sp_w - g + bw
+ } else {
+ ctx.usable.x + g + bw
+ };
+ sp_y = ctx.usable.y + dec_h + g;
+
+ box_geom_write = Some(Rect { x: sp_x, y: sp_y, width: sp_w, height: sp_h });
+ } else if snap.is_cloud {
+ if let Some(pos) = p.cloud_position_default {
+ sp_x = ctx.usable.x + pos[0];
+ sp_y = ctx.usable.y + pos[1];
+ box_geom_write = Some(Rect { x: sp_x, y: sp_y, width: sp_w, height: sp_h });
+ }
+ }
+
+ let vx = ctx.pan_x + (sp_x - ctx.phys.x) as f64 / ctx.zoom;
+ let vy = ctx.pan_y + (sp_y - ctx.phys.y) as f64 / ctx.zoom;
+
+ let mut target_w = sp_w;
+ let mut target_h = sp_h;
+ if !snap.ssd {
+ let (dec_w, dec_h) = snap.decorations_size;
+ target_w = (sp_w - dec_w).max(1);
+ target_h = (sp_h - dec_h).max(1);
+ }
+
+ OverlayPlacement {
+ pos: (sp_x, sp_y),
+ box_geom_write,
+ virtual_pos: (vx, vy),
+ size: (target_w as u32, target_h as u32),
+ }
+}
+
+/// State-machine step for entering/leaving Maximized mode. `Enter` tells the
+/// mechanism to save the given restore size (plus the window's current virtual
+/// position) and set `was_maximized`; `Exit` restores the saved geometry.
+/// Leaving with an invalid saved size does nothing (`was_maximized` stays set).
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum MaximizedTransition {
+ Enter { width: i32, height: i32 },
+ Exit { width: i32, height: i32, virtual_x: f64, virtual_y: f64 },
+}
+
+pub fn maximized_transition(
+ is_maximized_mode: bool,
+ was_maximized: bool,
+ box_size: (i32, i32),
+ min_size: (i32, i32),
+ saved_size: (i32, i32),
+ saved_virtual: (f64, f64),
+) -> Option<MaximizedTransition> {
+ if is_maximized_mode && !was_maximized {
+ let mut w = box_size.0;
+ let mut h = box_size.1;
+ if w <= 0 {
+ w = if min_size.0 > 32 { min_size.0 } else { 800 };
+ }
+ if h <= 0 {
+ h = if min_size.1 > 32 { min_size.1 } else { 600 };
+ }
+ Some(MaximizedTransition::Enter { width: w, height: h })
+ } else if !is_maximized_mode && was_maximized {
+ if saved_size.0 > 0 && saved_size.1 > 0 {
+ Some(MaximizedTransition::Exit {
+ width: saved_size.0,
+ height: saved_size.1,
+ virtual_x: saved_virtual.0,
+ virtual_y: saved_virtual.1,
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+}
+
+pub struct NormalSnapshot {
+ pub mode: TilingMode,
+ pub box_geom: Rect,
+ pub min_size: (i32, i32),
+ pub virtual_pos: (f64, f64),
+ /// Live interactive-resize dimensions, if a resize op is in progress.
+ pub active_resize: Option<(u32, u32)>,
+ pub is_cloud: bool,
+ pub saved_maximized_size: (i32, i32),
+ pub saved_maximized_virtual: (f64, f64),
+}
+
+pub struct NormalParams {
+ pub gap_right: i32,
+ pub gap_top: i32,
+ pub cloud_position_default: Option<[i32; 2]>,
+ pub desktop_grid_scale: f64,
+}
+
+pub struct NormalPlacement {
+ pub pos: (i32, i32),
+ pub scale: f64,
+ pub size: (u32, u32),
+ /// Fullscreen windows report all edges tiled to the client.
+ pub tiled_all_edges: bool,
+ /// Offscreen-culling result; `None` leaves the window's flag untouched.
+ pub hidden: Option<bool>,
+ /// Maximized grid-snap moves the window's virtual position.
+ pub virtual_write: Option<(f64, f64)>,
+}
+
+/// Place a non-overlay window according to its tiling mode: `Popup` docks to
+/// the usable area's top-right (or the cloud default position), `Fullscreen`
+/// covers the physical output, `Maximized` snaps to cover every desktop-grid
+/// cell its saved geometry touches, and everything else pans on the virtual
+/// surface under the current viewport.
+pub fn place_normal_window(
+ snap: &NormalSnapshot,
+ p: &NormalParams,
+ ctx: &PlacementCtx,
+) -> NormalPlacement {
+ match snap.mode {
+ TilingMode::Popup => {
+ let fw = if snap.box_geom.width > 0 {
+ snap.box_geom.width
+ } else if snap.min_size.0 > 32 {
+ snap.min_size.0
+ } else {
+ 360
+ };
+ let fh = if snap.box_geom.height > 0 {
+ snap.box_geom.height
+ } else if snap.min_size.1 > 32 {
+ snap.min_size.1
+ } else {
+ 100
+ };
+
+ let (fx, fy) = if snap.is_cloud && p.cloud_position_default.is_some() {
+ let pos = p.cloud_position_default.unwrap();
+ (ctx.usable.x + pos[0], ctx.usable.y + pos[1])
+ } else {
+ (ctx.usable.x + ctx.usable.width - fw - p.gap_right, ctx.usable.y + p.gap_top)
+ };
+
+ NormalPlacement {
+ pos: (fx, fy),
+ scale: 1.0,
+ size: (fw as u32, fh as u32),
+ tiled_all_edges: false,
+ hidden: None,
+ virtual_write: None,
+ }
+ }
+ TilingMode::Fullscreen => NormalPlacement {
+ pos: (ctx.phys.x, ctx.phys.y),
+ scale: 1.0,
+ size: (ctx.phys.width as u32, ctx.phys.height as u32),
+ tiled_all_edges: true,
+ hidden: None,
+ virtual_write: None,
+ },
+ TilingMode::Maximized => {
+ // Resize to fully fill all desktop-grid cells the saved geometry
+ // is fully/partially inside of.
+ let scale = p.desktop_grid_scale;
+
+ let x1 = snap.saved_maximized_virtual.0;
+ let y1 = snap.saved_maximized_virtual.1;
+ let w = snap.saved_maximized_size.0 as f64;
+ let h = snap.saved_maximized_size.1 as f64;
+ let x2 = x1 + w;
+ let y2 = y1 + h;
+
+ let col_min = (x1 / scale).floor() as i32;
+ let col_max = ((x2 / scale).ceil() as i32 - 1).max(col_min);
+ let row_min = (y1 / scale).floor() as i32;
+ let row_max = ((y2 / scale).ceil() as i32 - 1).max(row_min);
+
+ let snapped_x1 = col_min as f64 * scale;
+ let snapped_x2 = (col_max + 1) as f64 * scale;
+ let snapped_y1 = row_min as f64 * scale;
+ let snapped_y2 = (row_max + 1) as f64 * scale;
+
+ let fw = snapped_x2 - snapped_x1;
+ let fh = snapped_y2 - snapped_y1;
+
+ let (final_x, final_y) = ctx.virtual_to_screen(snapped_x1, snapped_y1);
+
+ NormalPlacement {
+ pos: (final_x, final_y),
+ scale: ctx.zoom,
+ size: (fw as u32, fh as u32),
+ tiled_all_edges: false,
+ hidden: Some(ctx.is_offscreen(final_x, final_y, fw * ctx.zoom, fh * ctx.zoom)),
+ virtual_write: Some((snapped_x1, snapped_y1)),
+ }
+ }
+ _ => {
+ // Regular pannable window on the virtual surface.
+ let fw = if let Some(resize_size) = snap.active_resize {
+ resize_size.0 as i32
+ } else if snap.box_geom.width > 0 {
+ snap.box_geom.width
+ } else if snap.min_size.0 > 32 {
+ snap.min_size.0
+ } else {
+ 800
+ };
+ let fh = if let Some(resize_size) = snap.active_resize {
+ resize_size.1 as i32
+ } else if snap.box_geom.height > 0 {
+ snap.box_geom.height
+ } else if snap.min_size.1 > 32 {
+ snap.min_size.1
+ } else {
+ 600
+ };
+
+ let (final_x, final_y) = ctx.virtual_to_screen(snap.virtual_pos.0, snap.virtual_pos.1);
+
+ NormalPlacement {
+ pos: (final_x, final_y),
+ scale: ctx.zoom,
+ size: (fw as u32, fh as u32),
+ tiled_all_edges: false,
+ hidden: Some(ctx.is_offscreen(final_x, final_y, fw as f64 * ctx.zoom, fh as f64 * ctx.zoom)),
+ virtual_write: None,
+ }
+ }
+ }
+}
+
+const SPACING: i32 = 12;
+const MARGIN: i32 = 12;
+
+const LEFT_ORDER: &[&str] = &["viewport", "window"];
+const RIGHT_ORDER: &[&str] = &["tray", "cpu", "memory", "brightness", "volume", "battery", "clock"];
+
+fn left_sort_key(app_id: &str) -> usize {
+ let name = app_id.strip_prefix("cce-status-interface-left-")
+ .or_else(|| app_id.strip_prefix("cce-status-left-"))
+ .unwrap_or(app_id);
+ LEFT_ORDER.iter().position(|&m| m == name).unwrap_or(99)
+}
+
+fn right_sort_key(app_id: &str) -> usize {
+ let name = app_id.strip_prefix("cce-status-interface-right-")
+ .or_else(|| app_id.strip_prefix("cce-status-right-"))
+ .unwrap_or(app_id);
+ RIGHT_ORDER.iter().position(|&m| m == name).unwrap_or(99)
+}
+
+/// Bar length to use: previous major length, or 100 for a fresh bar.
+fn bar_len(prev_len: i32) -> u32 {
+ if prev_len > 0 { prev_len as u32 } else { 100 }
+}
+
+/// Lay out status-bar windows on one output: horizontal groups on the top and
+/// bottom edges (left/center/right within each), vertical stacks on the left
+/// and right edges, and full-width bars across the top.
+///
+/// Returns one placement per item, index-aligned with `items`.
+pub fn layout_status_bars(
+ items: &[StatusBarItem],
+ p: &StatusBarLayoutParams,
+) -> Vec<Option<StatusBarPlacement>> {
+ let wlr_box = p.output;
+ let bar_h = p.bar_height;
+ let spacing = SPACING;
+ let margin = MARGIN;
+
+ let mut top_left: Vec<usize> = Vec::new();
+ let mut top_center: Vec<usize> = Vec::new();
+ let mut top_right: Vec<usize> = Vec::new();
+ let mut bottom_left: Vec<usize> = Vec::new();
+ let mut bottom_center: Vec<usize> = Vec::new();
+ let mut bottom_right: Vec<usize> = Vec::new();
+ let mut left_side: Vec<usize> = Vec::new();
+ let mut right_side: Vec<usize> = Vec::new();
+ let mut full_top: Vec<usize> = Vec::new();
+
+ for (idx, item) in items.iter().enumerate() {
+ let edge = if item.edge == StatusEdge::Unspecified {
+ StatusEdge::TopLeft
+ } else {
+ item.edge
+ };
+ match edge {
+ StatusEdge::TopLeft => top_left.push(idx),
+ StatusEdge::TopCenter => top_center.push(idx),
+ StatusEdge::TopRight => top_right.push(idx),
+ StatusEdge::BottomLeft => bottom_left.push(idx),
+ StatusEdge::BottomCenter => bottom_center.push(idx),
+ StatusEdge::BottomRight => bottom_right.push(idx),
+ StatusEdge::Left => left_side.push(idx),
+ StatusEdge::Right => right_side.push(idx),
+ _ => full_top.push(idx),
+ }
+ }
+
+ log::info!("[ArrangeStatus] top_left_len={}, top_center_len={}, top_right_len={}, left_side_len={}", top_left.len(), top_center.len(), top_right.len(), left_side.len());
+
+ let sort_left = |list: &mut Vec<usize>| {
+ list.sort_by_key(|&i| left_sort_key(&items[i].app_id));
+ };
+ let sort_right = |list: &mut Vec<usize>| {
+ list.sort_by_key(|&i| right_sort_key(&items[i].app_id));
+ };
+
+ sort_left(&mut top_left);
+ sort_left(&mut top_center);
+ sort_right(&mut top_right);
+ sort_left(&mut bottom_left);
+ sort_left(&mut bottom_center);
+ sort_right(&mut bottom_right);
+
+ let mut placements: Vec<Option<StatusBarPlacement>> = vec![None; items.len()];
+
+ // 1. Top Edge
+ let status_y_top = if p.hide_mode {
+ wlr_box.y - (bar_h as i32 - p.hide_mode_preview)
+ } else {
+ wlr_box.y
+ };
+
+ let mut top_right_width_needed = 0;
+ for &i in &top_right {
+ let w = bar_len(items[i].prev_len);
+ top_right_width_needed += w as i32 + spacing;
+ }
+ let top_right_boundary = wlr_box.x + wlr_box.width - margin - top_right_width_needed;
+
+ // 1a. Left Group (TopLeft / nw)
+ let mut cur_left_x = wlr_box.x + margin;
+ for &i in &top_left {
+ let mut w = bar_len(items[i].prev_len);
+ let max_allowed_w = top_right_boundary - cur_left_x - spacing;
+ if w as i32 > max_allowed_w {
+ w = std::cmp::max(max_allowed_w, 20) as u32;
+ }
+ log::info!("[TopLeftLayout] app_id={} x={}, w={}", items[i].app_id, cur_left_x, w);
+ placements[i] = Some(StatusBarPlacement { x: cur_left_x, y: status_y_top, width: w, height: bar_h });
+ cur_left_x += w as i32 + spacing;
+ }
+
+ // 1b. Center Group (TopCenter / n)
+ let mut top_center_width_needed = 0;
+ for &i in &top_center {
+ let w = bar_len(items[i].prev_len);
+ top_center_width_needed += w as i32 + spacing;
+ }
+ if top_center_width_needed > 0 {
+ top_center_width_needed -= spacing;
+ }
+ let center_start_x = wlr_box.x + (wlr_box.width - top_center_width_needed) / 2;
+ let mut cur_center_x = std::cmp::max(center_start_x, cur_left_x + spacing);
+
+ for &i in &top_center {
+ let mut w = bar_len(items[i].prev_len);
+ let max_allowed_w = top_right_boundary - cur_center_x - spacing;
+ if w as i32 > max_allowed_w {
+ w = std::cmp::max(max_allowed_w, 20) as u32;
+ }
+ log::info!("[TopCenterLayout] app_id={} x={}, w={}", items[i].app_id, cur_center_x, w);
+ placements[i] = Some(StatusBarPlacement { x: cur_center_x, y: status_y_top, width: w, height: bar_h });
+ cur_center_x += w as i32 + spacing;
+ }
+
+ // 1c. Right Group (TopRight / ne)
+ let mut cur_right_x = wlr_box.x + wlr_box.width - margin;
+ for &i in top_right.iter().rev() {
+ let w = bar_len(items[i].prev_len);
+ let x = cur_right_x - w as i32;
+ placements[i] = Some(StatusBarPlacement { x, y: status_y_top, width: w, height: bar_h });
+ cur_right_x = x - spacing;
+ }
+
+ for &i in &full_top {
+ placements[i] = Some(StatusBarPlacement { x: wlr_box.x, y: status_y_top, 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 bottom_right_width_needed = 0;
+ for &i in &bottom_right {
+ let w = bar_len(items[i].prev_len);
+ bottom_right_width_needed += w as i32 + spacing;
+ }
+ let bottom_right_boundary = wlr_box.x + wlr_box.width - margin - bottom_right_width_needed;
+
+ // 2a. Left Group (BottomLeft / sw)
+ let mut cur_left_x = wlr_box.x + margin;
+ for &i in &bottom_left {
+ let mut w = bar_len(items[i].prev_len);
+ let max_allowed_w = bottom_right_boundary - cur_left_x - spacing;
+ if w as i32 > max_allowed_w {
+ w = std::cmp::max(max_allowed_w, 20) as u32;
+ }
+ placements[i] = Some(StatusBarPlacement { x: cur_left_x, y: status_y_bottom, width: w, height: bar_h });
+ cur_left_x += w as i32 + spacing;
+ }
+
+ // 2b. Center Group (BottomCenter / s)
+ let mut bottom_center_width_needed = 0;
+ for &i in &bottom_center {
+ let w = bar_len(items[i].prev_len);
+ bottom_center_width_needed += w as i32 + spacing;
+ }
+ if bottom_center_width_needed > 0 {
+ bottom_center_width_needed -= spacing;
+ }
+ let center_start_x = wlr_box.x + (wlr_box.width - bottom_center_width_needed) / 2;
+ let mut cur_center_x = std::cmp::max(center_start_x, cur_left_x + spacing);
+
+ for &i in &bottom_center {
+ let mut w = bar_len(items[i].prev_len);
+ let max_allowed_w = bottom_right_boundary - cur_center_x - spacing;
+ if w as i32 > max_allowed_w {
+ w = std::cmp::max(max_allowed_w, 20) as u32;
+ }
+ placements[i] = Some(StatusBarPlacement { x: cur_center_x, y: status_y_bottom, width: w, height: bar_h });
+ cur_center_x += w as i32 + spacing;
+ }
+
+ // 2c. Right Group (BottomRight / se)
+ let mut cur_right_x = wlr_box.x + wlr_box.width - margin;
+ for &i in bottom_right.iter().rev() {
+ let w = bar_len(items[i].prev_len);
+ let x = cur_right_x - w as i32;
+ placements[i] = Some(StatusBarPlacement { x, y: status_y_bottom, width: w, height: bar_h });
+ cur_right_x = x - spacing;
+ }
+
+ // 3. Left Edge (Vertical stacking)
+ let mut left_total_height = 0;
+ for &i in &left_side {
+ let actual_h = bar_len(items[i].prev_len);
+ left_total_height += actual_h as i32;
+ }
+ if !left_side.is_empty() {
+ left_total_height += (left_side.len() as i32 - 1) * spacing;
+ }
+ let mut cur_left_y = wlr_box.y + (wlr_box.height - left_total_height) / 2;
+
+ for &i in &left_side {
+ let actual_h = bar_len(items[i].prev_len);
+ placements[i] = Some(StatusBarPlacement { x: wlr_box.x, y: cur_left_y, width: bar_h, height: actual_h });
+ cur_left_y += actual_h as i32 + spacing;
+ }
+
+ // 4. Right Edge (Vertical stacking)
+ let mut right_total_height = 0;
+ for &i in &right_side {
+ let actual_h = bar_len(items[i].prev_len);
+ right_total_height += actual_h as i32;
+ }
+ if !right_side.is_empty() {
+ right_total_height += (right_side.len() as i32 - 1) * spacing;
+ }
+ let mut cur_right_y = wlr_box.y + (wlr_box.height - right_total_height) / 2;
+
+ for &i in &right_side {
+ let actual_h = bar_len(items[i].prev_len);
+ placements[i] = Some(StatusBarPlacement { x: wlr_box.x + wlr_box.width - bar_h as i32, y: cur_right_y, width: bar_h, height: actual_h });
+ cur_right_y += actual_h as i32 + spacing;
+ }
+
+ placements
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn params() -> StatusBarLayoutParams {
+ StatusBarLayoutParams {
+ output: Rect { x: 0, y: 0, width: 1920, height: 1080 },
+ bar_height: 30,
+ hide_mode: false,
+ hide_mode_preview: 5,
+ }
+ }
+
+ fn item(app_id: &str, edge: StatusEdge, prev_len: i32) -> StatusBarItem {
+ StatusBarItem { app_id: app_id.to_string(), edge, prev_len }
+ }
+
+ #[test]
+ fn top_groups_flow_from_edges() {
+ let items = vec![
+ item("cce-status-left-viewport", StatusEdge::TopLeft, 0),
+ item("cce-status-left-window", StatusEdge::TopLeft, 0),
+ item("cce-status-right-clock", StatusEdge::TopRight, 200),
+ ];
+ let p = layout_status_bars(&items, ¶ms());
+ // Left group flows right from the margin; fresh bars default to width 100.
+ assert_eq!(p[0], Some(StatusBarPlacement { x: 12, y: 0, width: 100, height: 30 }));
+ assert_eq!(p[1], Some(StatusBarPlacement { x: 124, y: 0, width: 100, height: 30 }));
+ // Right group is placed from the right edge inward.
+ assert_eq!(p[2], Some(StatusBarPlacement { x: 1908 - 200, y: 0, width: 200, height: 30 }));
+ }
+
+ #[test]
+ fn right_group_sorted_by_module_order() {
+ let items = vec![
+ item("cce-status-right-clock", StatusEdge::TopRight, 100),
+ item("cce-status-right-battery", StatusEdge::TopRight, 100),
+ ];
+ let p = layout_status_bars(&items, ¶ms());
+ // RIGHT_ORDER puts battery before clock left-to-right, so clock hugs the edge.
+ assert_eq!(p[0].unwrap().x, 1808);
+ assert_eq!(p[1].unwrap().x, 1808 - 12 - 100);
+ }
+
+ #[test]
+ fn hide_mode_pushes_top_bars_offscreen_with_preview() {
+ let mut prm = params();
+ prm.hide_mode = true;
+ let items = vec![item("cce-status-left-viewport", StatusEdge::Unspecified, 0)];
+ let p = layout_status_bars(&items, &prm);
+ // Unspecified resolves to TopLeft; y = 0 - (30 - 5).
+ assert_eq!(p[0].unwrap().y, -25);
+ }
+
+ #[test]
+ fn usable_area_reserves_bar_edges() {
+ let output = Rect { x: 0, y: 0, width: 1920, height: 1080 };
+ let no_excl = Rect { x: 0, y: 0, width: 0, height: 0 };
+
+ // No bars, no exclusion: the full output.
+ assert_eq!(compute_usable_area(output, no_excl, 30, false, &[]), output);
+
+ // Top + left bars each reserve one bar-height.
+ let edges = [StatusEdge::TopLeft, StatusEdge::Left];
+ assert_eq!(
+ compute_usable_area(output, no_excl, 30, false, &edges),
+ Rect { x: 30, y: 30, width: 1890, height: 1050 }
+ );
+
+ // Hide mode releases the top reservation but not the others.
+ assert_eq!(
+ compute_usable_area(output, no_excl, 30, true, &edges),
+ Rect { x: 30, y: 0, width: 1890, height: 1080 }
+ );
+
+ // Layer-shell non-exclusive area applies before bar reservations.
+ let excl = Rect { x: 10, y: 20, width: 1900, height: 1040 };
+ assert_eq!(
+ compute_usable_area(output, excl, 30, false, &[StatusEdge::BottomCenter]),
+ Rect { x: 10, y: 20, width: 1900, height: 1010 }
+ );
+ }
+
+ #[test]
+ fn window_roles_from_app_id() {
+ assert_eq!(WindowRole::from_app_id(Some("cce-wallpaper")), WindowRole::Background);
+ assert_eq!(WindowRole::from_app_id(Some("cce-status-interface-right-clock")), WindowRole::StatusBar);
+ assert_eq!(WindowRole::from_app_id(Some("firefox")), WindowRole::Normal);
+ assert_eq!(WindowRole::from_app_id(None), WindowRole::Normal);
+ }
+
+ #[test]
+ fn classification_precedence() {
+ // Role wins over visibility: a minimized wallpaper still arranges as background.
+ assert_eq!(
+ classify_window(WindowRole::Background, true, true, TilingMode::Floating, false),
+ WindowClass::Background
+ );
+ assert_eq!(
+ classify_window(WindowRole::StatusBar, false, false, TilingMode::Floating, false),
+ WindowClass::StatusBar
+ );
+ // Minimized or closing/init normal windows are hidden.
+ assert_eq!(
+ classify_window(WindowRole::Normal, true, false, TilingMode::Grid, false),
+ WindowClass::Hidden
+ );
+ // Overlay mode gets the overlay slot — unless mid-drag.
+ assert_eq!(
+ classify_window(WindowRole::Normal, false, false, TilingMode::Overlay, false),
+ WindowClass::Overlay
+ );
+ assert_eq!(
+ classify_window(WindowRole::Normal, false, false, TilingMode::Overlay, true),
+ WindowClass::Normal
+ );
+ assert_eq!(
+ classify_window(WindowRole::Normal, false, false, TilingMode::Cascade, false),
+ WindowClass::Normal
+ );
+ }
+
+ fn ctx() -> PlacementCtx {
+ PlacementCtx {
+ phys: Rect { x: 0, y: 0, width: 1920, height: 1080 },
+ usable: Rect { x: 0, y: 30, width: 1920, height: 1050 },
+ pan_x: 0.0,
+ pan_y: 0.0,
+ zoom: 1.0,
+ }
+ }
+
+ #[test]
+ fn fresh_overlay_gets_configured_slot() {
+ let placement = place_overlay_window(
+ &OverlaySnapshot {
+ box_geom: Rect { x: 0, y: 0, width: 0, height: 0 },
+ min_width: 0,
+ is_cloud: false,
+ ssd: true,
+ decorations_size: (0, 16),
+ },
+ &OverlayParams {
+ overlay_width: 400,
+ border_gap: 8,
+ position_right: true,
+ cloud_position_default: None,
+ },
+ &ctx(),
+ );
+ // Right slot: x = usable right edge - width - gap; height fills the
+ // usable area minus the 16px decoration strip and both gaps.
+ assert_eq!(placement.pos, (1512, 54));
+ assert_eq!(placement.size, (400, 1018));
+ assert_eq!(placement.box_geom_write, Some(Rect { x: 1512, y: 54, width: 400, height: 1018 }));
+ assert_eq!(placement.virtual_pos, (1512.0, 54.0));
+ }
+
+ #[test]
+ fn overlay_without_ssd_shrinks_by_decorations() {
+ let placement = place_overlay_window(
+ &OverlaySnapshot {
+ box_geom: Rect { x: 100, y: 100, width: 400, height: 500 },
+ min_width: 0,
+ is_cloud: false,
+ ssd: false,
+ decorations_size: (2, 18),
+ },
+ &OverlayParams { overlay_width: 400, border_gap: 8, position_right: false, cloud_position_default: None },
+ &ctx(),
+ );
+ // Existing geometry is kept; the client is sized minus decorations.
+ assert_eq!(placement.pos, (100, 100));
+ assert_eq!(placement.size, (398, 482));
+ assert_eq!(placement.box_geom_write, None);
+ }
+
+ #[test]
+ fn maximized_transitions() {
+ // Entering with no usable geometry falls back to 800x600.
+ assert_eq!(
+ maximized_transition(true, false, (0, 0), (0, 0), (0, 0), (0.0, 0.0)),
+ Some(MaximizedTransition::Enter { width: 800, height: 600 })
+ );
+ // Entering keeps real geometry.
+ assert_eq!(
+ maximized_transition(true, false, (640, 480), (0, 0), (0, 0), (0.0, 0.0)),
+ Some(MaximizedTransition::Enter { width: 640, height: 480 })
+ );
+ // Steady states do nothing.
+ assert_eq!(maximized_transition(true, true, (640, 480), (0, 0), (640, 480), (0.0, 0.0)), None);
+ assert_eq!(maximized_transition(false, false, (640, 480), (0, 0), (0, 0), (0.0, 0.0)), None);
+ // Exit restores the saved geometry; invalid saved size is a no-op.
+ assert_eq!(
+ maximized_transition(false, true, (0, 0), (0, 0), (640, 480), (10.0, 20.0)),
+ Some(MaximizedTransition::Exit { width: 640, height: 480, virtual_x: 10.0, virtual_y: 20.0 })
+ );
+ assert_eq!(maximized_transition(false, true, (0, 0), (0, 0), (0, 480), (10.0, 20.0)), None);
+ }
+
+ #[test]
+ fn maximized_snaps_to_grid_cells() {
+ let snap = NormalSnapshot {
+ mode: TilingMode::Maximized,
+ box_geom: Rect { x: 0, y: 0, width: 100, height: 50 },
+ min_size: (0, 0),
+ virtual_pos: (150.0, 120.0),
+ active_resize: None,
+ is_cloud: false,
+ saved_maximized_size: (100, 50),
+ saved_maximized_virtual: (150.0, 120.0),
+ };
+ let p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_grid_scale: 100.0 };
+ let placement = place_normal_window(&snap, &p, &ctx());
+ // Saved geometry spans grid columns 1-2 and row 1 → snapped to
+ // (100,100) with size 200x100.
+ assert_eq!(placement.virtual_write, Some((100.0, 100.0)));
+ assert_eq!(placement.pos, (100, 100));
+ assert_eq!(placement.size, (200, 100));
+ assert_eq!(placement.hidden, Some(false));
+ assert_eq!(placement.scale, 1.0);
+ }
+
+ #[test]
+ fn popup_docks_top_right_of_usable_area() {
+ let snap = NormalSnapshot {
+ mode: TilingMode::Popup,
+ box_geom: Rect { x: 0, y: 0, width: 0, height: 0 },
+ min_size: (0, 0),
+ virtual_pos: (0.0, 0.0),
+ active_resize: None,
+ is_cloud: false,
+ saved_maximized_size: (0, 0),
+ saved_maximized_virtual: (0.0, 0.0),
+ };
+ let p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_grid_scale: 100.0 };
+ let placement = place_normal_window(&snap, &p, &ctx());
+ // Defaults to 360x100, docked inside the usable area (below the bar).
+ assert_eq!(placement.pos, (1920 - 360 - 10, 30 + 6));
+ assert_eq!(placement.size, (360, 100));
+ assert_eq!(placement.hidden, None);
+ }
+
+ #[test]
+ fn pannable_window_follows_viewport() {
+ let snap = NormalSnapshot {
+ mode: TilingMode::Cascade,
+ box_geom: Rect { x: 0, y: 0, width: 640, height: 480 },
+ min_size: (0, 0),
+ virtual_pos: (100.0, 200.0),
+ active_resize: None,
+ is_cloud: false,
+ saved_maximized_size: (0, 0),
+ saved_maximized_virtual: (0.0, 0.0),
+ };
+ let p = NormalParams { gap_right: 10, gap_top: 6, cloud_position_default: None, desktop_grid_scale: 100.0 };
+ let mut c = ctx();
+ c.pan_x = 50.0;
+ c.pan_y = 100.0;
+ c.zoom = 2.0;
+ let placement = place_normal_window(&snap, &p, &c);
+ assert_eq!(placement.pos, (100, 200));
+ assert_eq!(placement.scale, 2.0);
+ assert_eq!(placement.size, (640, 480));
+ assert_eq!(placement.hidden, Some(false));
+
+ // Pan far enough away and the window is culled.
+ c.pan_x = 5000.0;
+ let placement = place_normal_window(&snap, &p, &c);
+ assert_eq!(placement.hidden, Some(true));
+
+ // Interactive resize dimensions override stored geometry.
+ let resizing = NormalSnapshot { active_resize: Some((800, 600)), ..snap };
+ c.pan_x = 50.0;
+ let placement = place_normal_window(&resizing, &p, &c);
+ assert_eq!(placement.size, (800, 600));
+ }
+
+ #[test]
+ fn opacity_policy() {
+ assert_eq!(window_opacity(true, true, OVERLAY_UNFOCUSED_OPACITY), 1.0);
+ assert_eq!(window_opacity(false, false, OVERLAY_UNFOCUSED_OPACITY), 1.0);
+ assert_eq!(window_opacity(false, true, OVERLAY_UNFOCUSED_OPACITY), 0.85);
+ assert_eq!(window_opacity(false, true, NORMAL_UNFOCUSED_OPACITY), 0.90);
+ }
+
+ #[test]
+ fn side_stacks_center_vertically() {
+ let items = vec![
+ item("cce-status-a", StatusEdge::Left, 200),
+ item("cce-status-b", StatusEdge::Left, 100),
+ ];
+ let p = layout_status_bars(&items, ¶ms());
+ // Total stack: 200 + 12 + 100 = 312, centered in 1080 → starts at 384.
+ assert_eq!(p[0], Some(StatusBarPlacement { x: 0, y: 384, width: 30, height: 200 }));
+ assert_eq!(p[1], Some(StatusBarPlacement { x: 0, y: 596, width: 30, height: 100 }));
+ }
+}
diff --git a/src/server/policy/mod.rs b/src/server/policy/mod.rs
new file mode 100644
index 0000000..68e2e5e
--- /dev/null
+++ b/src/server/policy/mod.rs
@@ -0,0 +1,20 @@
+// The window-management policy layer.
+//
+// This module is the seam for the planned compositor / window-manager split:
+// everything under `policy/` is pure Rust — no FFI, no raw wlroots pointers —
+// and is intended to eventually move to a separate `cce-window-manager` crate.
+// The mechanism side (scene graph, seats, shells, sockets) stays in this crate
+// and talks to policy code only through the types in `api`.
+//
+// Migration status:
+// - `tiling`: pure layout formulas (moved here from `src/server/tiling.rs`).
+// - `state`: persisted session state (moved here from `window_manager.rs`).
+// - `arrange`: pure pieces of `arrange_views()`; currently the status-bar
+// layout engine. `StatusEdge` lives here (re-exported via `window.rs`).
+// - `api`: the `Policy` / `Compositor` trait boundary. Skeleton — defined but
+// not yet driven by `window_manager.rs`.
+
+pub mod api;
+pub mod arrange;
+pub mod state;
+pub mod tiling;
diff --git a/src/server/policy/state.rs b/src/server/policy/state.rs
new file mode 100644
index 0000000..7f677cd
--- /dev/null
+++ b/src/server/policy/state.rs
@@ -0,0 +1,32 @@
+// Persisted session state, saved to $XDG_STATE_HOME/cce/state.json on shutdown
+// and restored on startup. Pure data — serialization and matching logic only;
+// the save/load I/O lives in `window_manager.rs`.
+
+use super::tiling::TilingMode;
+
+#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
+pub struct SavedWindowState {
+ pub app_id: String,
+ pub title: String,
+ pub tiling_mode: TilingMode,
+ pub minimized: bool,
+ pub virtual_x: f64,
+ pub virtual_y: f64,
+ pub scale: f64,
+ pub width: u32,
+ pub height: u32,
+ pub cmdline: String,
+ #[serde(default)]
+ pub focused: bool,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
+pub struct SavedState {
+ pub desk_pan_x: f64,
+ pub desk_pan_y: f64,
+ pub desk_zoom: f64,
+ pub global_layout: TilingMode,
+ pub windows: Vec<SavedWindowState>,
+ #[serde(default)]
+ pub last_window_states: Vec<SavedWindowState>,
+}
diff --git a/src/server/tiling.rs b/src/server/policy/tiling.rs
similarity index 100%
rename from src/server/tiling.rs
rename to src/server/policy/tiling.rs
diff --git a/src/server/window.rs b/src/server/window.rs
index 438bd9f..ac7adfb 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -261,18 +261,7 @@ pub struct Window {
pub status_edge: StatusEdge,
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum StatusEdge {
- Unspecified,
- TopLeft,
- TopCenter,
- TopRight,
- BottomLeft,
- BottomCenter,
- BottomRight,
- Left,
- Right,
-}
+pub use crate::policy::arrange::StatusEdge;
impl Window {
pub unsafe fn is_wine(&self) -> bool {
@@ -284,12 +273,16 @@ impl Window {
|| !self.wm_requested.fullscreen.is_null()
}
+ pub unsafe fn role(&self) -> crate::policy::api::WindowRole {
+ crate::policy::api::WindowRole::from_app_id(self.get_app_id_string().as_deref())
+ }
+
pub unsafe fn is_status_bar(&self) -> bool {
- self.get_app_id_string().as_deref().map_or(false, |id| id.starts_with("cce-status"))
+ self.role() == crate::policy::api::WindowRole::StatusBar
}
pub unsafe fn is_wallpaper(&self) -> bool {
- self.get_app_id_string().as_deref() == Some("cce-wallpaper")
+ self.role() == crate::policy::api::WindowRole::Background
}
pub unsafe fn is_linked(&self) -> bool {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index e3854ac..476cbe4 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -47,32 +47,7 @@ pub struct WindowManagerRenderingRequested {
pub order_hash: u64,
}
-#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
-pub struct SavedWindowState {
- pub app_id: String,
- pub title: String,
- pub tiling_mode: crate::tiling::TilingMode,
- pub minimized: bool,
- pub virtual_x: f64,
- pub virtual_y: f64,
- pub scale: f64,
- pub width: u32,
- pub height: u32,
- pub cmdline: String,
- #[serde(default)]
- pub focused: bool,
-}
-
-#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
-pub struct SavedState {
- pub desk_pan_x: f64,
- pub desk_pan_y: f64,
- pub desk_zoom: f64,
- pub global_layout: crate::tiling::TilingMode,
- pub windows: Vec<SavedWindowState>,
- #[serde(default)]
- pub last_window_states: Vec<SavedWindowState>,
-}
+pub use crate::policy::state::{SavedState, SavedWindowState};
pub struct WindowManager {
pub server: *mut Server,
@@ -844,7 +819,7 @@ impl WindowManager {
}
}
- let has_wallpaper = self.windows.iter().any(|&w| !w.is_null() && !(*w).closed && (*w).get_app_id_string().as_deref() == Some("cce-wallpaper"));
+ let has_wallpaper = self.windows.iter().any(|&w| !w.is_null() && !(*w).closed && (*w).is_wallpaper());
let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
let mut curr_out = (*outputs_list).next;
while curr_out != outputs_list {
@@ -1140,7 +1115,7 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
return;
}
- let has_wallpaper = self.windows.iter().any(|&w| !w.is_null() && !(*w).closed && (*w).get_app_id_string().as_deref() == Some("cce-wallpaper"));
+ let has_wallpaper = self.windows.iter().any(|&w| !w.is_null() && !(*w).closed && (*w).is_wallpaper());
for &output in &active_outputs {
if !(*output).background_rect.is_null() {
ffi::wlr_scene_node_set_enabled((*output).background_rect as *mut ffi::wlr_scene_node, !has_wallpaper);
@@ -1167,73 +1142,23 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
let phys_w = wlr_box.width;
let phys_h = wlr_box.height;
- let mut usable_x = phys_x;
- let mut usable_y = phys_y;
- let mut usable_w = phys_w;
- let mut usable_h = phys_h;
-
let non_ex = (*output).layer_shell.scheduled.non_exclusive_area;
- if non_ex.width > 0 && non_ex.height > 0 {
- usable_x = phys_x + non_ex.x;
- usable_y = phys_y + non_ex.y;
- usable_w = non_ex.width;
- usable_h = non_ex.height;
- }
-
- let bar_h = self.layout.bar_height as i32;
- let mut has_top = false;
- let mut has_bottom = false;
- let mut has_left = false;
- let mut has_right = false;
-
- let resolve_edge = |win_ptr: *mut Window| {
- let edge = (*win_ptr).status_edge;
- if edge == crate::window::StatusEdge::Unspecified {
- crate::window::StatusEdge::TopLeft
- } else {
- edge
- }
- };
-
+ let mut status_edges: Vec<crate::window::StatusEdge> = Vec::new();
for &win_ptr in self.windows.iter() {
if win_ptr.is_null() || (*win_ptr).closed {
continue;
}
if (*win_ptr).is_status_bar() {
- match resolve_edge(win_ptr) {
- crate::window::StatusEdge::Unspecified | crate::window::StatusEdge::TopLeft | crate::window::StatusEdge::TopCenter | crate::window::StatusEdge::TopRight => {
- if !self.status_hide_mode {
- has_top = true;
- }
- }
- crate::window::StatusEdge::BottomLeft | crate::window::StatusEdge::BottomCenter | crate::window::StatusEdge::BottomRight => {
- has_bottom = true;
- }
- crate::window::StatusEdge::Left => {
- has_left = true;
- }
- crate::window::StatusEdge::Right => {
- has_right = true;
- }
- }
+ status_edges.push((*win_ptr).status_edge);
}
}
-
- if has_top {
- usable_y += bar_h;
- usable_h -= bar_h;
- }
- if has_bottom {
- usable_h -= bar_h;
- }
- if has_left {
- usable_x += bar_h;
- usable_w -= bar_h;
- }
- if has_right {
- usable_w -= bar_h;
- }
-
+ let usable = crate::policy::arrange::compute_usable_area(
+ crate::policy::api::Rect { x: phys_x, y: phys_y, width: phys_w, height: phys_h },
+ crate::policy::api::Rect { x: non_ex.x, y: non_ex.y, width: non_ex.width, height: non_ex.height },
+ self.layout.bar_height as i32,
+ self.status_hide_mode,
+ &status_edges,
+ );
let viewport_w = wlr_box.width as f64;
let viewport_h = wlr_box.height as f64;
let camera_center_x = self.desk_pan_x + (viewport_w / 2.0) / self.desk_zoom;
@@ -1248,161 +1173,151 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
continue;
}
- let app_id = (*win_ptr).get_app_id_string();
- let is_status_bar = app_id.as_deref().map_or(false, |id| id.starts_with("cce-status"));
- let is_wallpaper = app_id.as_deref() == Some("cce-wallpaper");
-
- if is_wallpaper {
- (*win_ptr).tiling_mode = crate::tiling::TilingMode::Status;
- (*win_ptr).wm_requested.tiled = 0;
- (*win_ptr).wm_requested.ssd = false;
- (*win_ptr).scale = 1.0;
- ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, true);
- (*win_ptr).rendering_requested.hidden = false;
- (*win_ptr).rendering_requested.blur = false;
- (*win_ptr).rendering_requested.x = phys_x;
- (*win_ptr).rendering_requested.y = phys_y;
- (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
- width: phys_w as u32,
- height: phys_h as u32,
- });
- (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
- width: phys_w as u32,
- height: phys_h as u32,
- };
- continue;
- }
-
- if is_status_bar {
- (*win_ptr).tiling_mode = crate::tiling::TilingMode::Status;
- (*win_ptr).wm_requested.tiled = 0;
- (*win_ptr).wm_requested.ssd = false;
- (*win_ptr).scale = 1.0;
- ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, true);
- (*win_ptr).rendering_requested.hidden = false;
- (*win_ptr).rendering_requested.blur = self.layout.status_background_blur > 0.001;
- continue;
- }
-
- let visible = !(*win_ptr).minimized
- && !matches!((*win_ptr).state, crate::window::WindowState::Closing | crate::window::WindowState::Init);
- if !visible {
- ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, false);
- (*win_ptr).rendering_requested.hidden = true;
- continue;
- }
-
- ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, true);
- (*win_ptr).rendering_requested.hidden = false;
-
let mode = self.get_mode_for_window(win_ptr);
- (*win_ptr).tiling_mode = mode;
+ let class = crate::policy::arrange::classify_window(
+ (*win_ptr).role(),
+ (*win_ptr).minimized,
+ matches!((*win_ptr).state, crate::window::WindowState::Closing | crate::window::WindowState::Init),
+ mode,
+ self.is_window_being_moved(win_ptr),
+ );
- if !(*win_ptr).mode_locked {
- if let Some(rule) = self.get_rule_for_window(win_ptr) {
- if let Some(rule_ssd) = rule.ssd {
- (*win_ptr).wm_requested.ssd = rule_ssd;
- }
+ match class {
+ crate::policy::arrange::WindowClass::Background => {
+ (*win_ptr).tiling_mode = crate::tiling::TilingMode::Status;
+ (*win_ptr).wm_requested.tiled = 0;
+ (*win_ptr).wm_requested.ssd = false;
+ (*win_ptr).scale = 1.0;
+ ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, true);
+ (*win_ptr).rendering_requested.hidden = false;
+ (*win_ptr).rendering_requested.blur = false;
+ (*win_ptr).rendering_requested.x = phys_x;
+ (*win_ptr).rendering_requested.y = phys_y;
+ (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
+ width: phys_w as u32,
+ height: phys_h as u32,
+ });
+ (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
+ width: phys_w as u32,
+ height: phys_h as u32,
+ };
}
- }
+ crate::policy::arrange::WindowClass::StatusBar => {
+ (*win_ptr).tiling_mode = crate::tiling::TilingMode::Status;
+ (*win_ptr).wm_requested.tiled = 0;
+ (*win_ptr).wm_requested.ssd = false;
+ (*win_ptr).scale = 1.0;
+ ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, true);
+ (*win_ptr).rendering_requested.hidden = false;
+ (*win_ptr).rendering_requested.blur = self.layout.status_background_blur > 0.001;
+ }
+ crate::policy::arrange::WindowClass::Hidden => {
+ ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, false);
+ (*win_ptr).rendering_requested.hidden = true;
+ }
+ crate::policy::arrange::WindowClass::Overlay | crate::policy::arrange::WindowClass::Normal => {
+ ffi::wlr_scene_node_set_enabled((*win_ptr).tree as *mut ffi::wlr_scene_node, true);
+ (*win_ptr).rendering_requested.hidden = false;
+
+ (*win_ptr).tiling_mode = mode;
+
+ if !(*win_ptr).mode_locked {
+ if let Some(rule) = self.get_rule_for_window(win_ptr) {
+ if let Some(rule_ssd) = rule.ssd {
+ (*win_ptr).wm_requested.ssd = rule_ssd;
+ }
+ }
+ }
- let is_moving = self.is_window_being_moved(win_ptr);
- if mode == crate::tiling::TilingMode::Overlay && !is_moving {
- overlay_windows.push(win_ptr);
- } else {
- normal_windows.push(win_ptr);
+ if class == crate::policy::arrange::WindowClass::Overlay {
+ overlay_windows.push(win_ptr);
+ } else {
+ normal_windows.push(win_ptr);
+ }
+ }
}
}
let bw = 0;
- let g = self.layout.overlay_border_gap;
- let dec_h = std::cmp::max(bw, 16);
+ let overlay_params = crate::policy::arrange::OverlayParams {
+ overlay_width: self.layout.overlay_width,
+ border_gap: self.layout.overlay_border_gap,
+ position_right: self.layout.overlay_position == "right",
+ cloud_position_default: self.layout.cloud_position_default,
+ };
+ let normal_params = crate::policy::arrange::NormalParams {
+ gap_right: self.layout.gap_right,
+ gap_top: self.layout.gap_top,
+ cloud_position_default: self.layout.cloud_position_default,
+ desktop_grid_scale: self.layout.desktop_grid_scale,
+ };
+ let ctx = crate::policy::arrange::PlacementCtx {
+ phys: crate::policy::api::Rect { x: phys_x, y: phys_y, width: phys_w, height: phys_h },
+ usable,
+ pan_x: self.desk_pan_x,
+ pan_y: self.desk_pan_y,
+ zoom: self.desk_zoom,
+ };
+
for (sp_idx, &win_ptr) in overlay_windows.iter().enumerate() {
if sp_idx == 0 {
let app_id = (*win_ptr).get_app_id_string();
let is_cce_cloud = app_id.as_deref().map_or(false, |id| id.starts_with("cce-cloud"));
- let mut sp_x = (*win_ptr).box_geom.x;
- let mut sp_y = (*win_ptr).box_geom.y;
- let mut sp_w = (*win_ptr).box_geom.width as i32;
- let mut sp_h = (*win_ptr).box_geom.height as i32;
-
-
- if sp_w == 0 || sp_h == 0 {
- sp_w = if (*win_ptr).wm_scheduled.dimensions_hint.min_width > 32 {
- std::cmp::max(self.layout.overlay_width, (*win_ptr).wm_scheduled.dimensions_hint.min_width as i32)
- } else {
- self.layout.overlay_width
- };
- sp_h = (usable_h - (dec_h + bw) - 2 * g).max(1);
+ let placement = crate::policy::arrange::place_overlay_window(
+ &crate::policy::arrange::OverlaySnapshot {
+ box_geom: crate::policy::api::Rect {
+ x: (*win_ptr).box_geom.x,
+ y: (*win_ptr).box_geom.y,
+ width: (*win_ptr).box_geom.width,
+ height: (*win_ptr).box_geom.height,
+ },
+ min_width: (*win_ptr).wm_scheduled.dimensions_hint.min_width as i32,
+ is_cloud: is_cce_cloud,
+ ssd: (*win_ptr).wm_requested.ssd,
+ decorations_size: (*win_ptr).get_decorations_size(),
+ },
+ &overlay_params,
+ &ctx,
+ );
- sp_x = if self.layout.overlay_position == "right" {
- usable_x + usable_w - sp_w - g + bw
- } else {
- usable_x + g + bw
- };
- sp_y = usable_y + dec_h + g;
-
- (*win_ptr).box_geom.x = sp_x;
- (*win_ptr).box_geom.y = sp_y;
- (*win_ptr).box_geom.width = sp_w;
- (*win_ptr).box_geom.height = sp_h;
- } else if is_cce_cloud {
- if let Some(pos) = self.layout.cloud_position_default {
- sp_x = usable_x + pos[0];
- sp_y = usable_y + pos[1];
- (*win_ptr).box_geom.x = sp_x;
- (*win_ptr).box_geom.y = sp_y;
- }
+ if let Some(bg) = placement.box_geom_write {
+ (*win_ptr).box_geom.x = bg.x;
+ (*win_ptr).box_geom.y = bg.y;
+ (*win_ptr).box_geom.width = bg.width;
+ (*win_ptr).box_geom.height = bg.height;
}
-
- (*win_ptr).rendering_requested.x = sp_x;
- (*win_ptr).rendering_requested.y = sp_y;
+ (*win_ptr).rendering_requested.x = placement.pos.0;
+ (*win_ptr).rendering_requested.y = placement.pos.1;
(*win_ptr).scale = 1.0;
-
-
-
- let vx = self.desk_pan_x + (sp_x - phys_x) as f64 / self.desk_zoom;
- let vy = self.desk_pan_y + (sp_y - phys_y) as f64 / self.desk_zoom;
- (*win_ptr).virtual_x = vx;
- (*win_ptr).virtual_y = vy;
-
- let mut sp_target_w = sp_w;
- let mut sp_target_h = sp_h;
- if !(*win_ptr).wm_requested.ssd {
- let (dec_w, dec_h) = (*win_ptr).get_decorations_size();
- sp_target_w = (sp_w - dec_w).max(1);
- sp_target_h = (sp_h - dec_h).max(1);
- }
+ (*win_ptr).virtual_x = placement.virtual_pos.0;
+ (*win_ptr).virtual_y = placement.virtual_pos.1;
(*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
- width: sp_target_w as u32,
- height: sp_target_h as u32,
+ width: placement.size.0,
+ height: placement.size.1,
});
(*win_ptr).wm_requested.bounds = crate::window::Dimensions {
- width: sp_target_w as u32,
- height: sp_target_h as u32,
+ width: placement.size.0,
+ height: placement.size.1,
};
(*win_ptr).wm_requested.tiled = 1 | 2 | 4 | 8;
let is_focused = win_ptr == focused_window;
- let r = self.layout.border_r;
- let g_color = self.layout.border_g;
- let b = self.layout.border_b;
- let a = self.layout.border_a;
-
(*win_ptr).rendering_requested.border = crate::window::Border {
edges: crate::window::Edges { top: true, bottom: true, left: true, right: true },
width: bw as u32,
- r,
- g: g_color,
- b,
- a,
+ r: self.layout.border_r,
+ g: self.layout.border_g,
+ b: self.layout.border_b,
+ a: self.layout.border_a,
};
(*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.85f32 }
- };
+ (*win_ptr).rendering_requested.opacity = crate::policy::arrange::window_opacity(
+ is_focused,
+ self.layout.window_opacity,
+ crate::policy::arrange::OVERLAY_UNFOCUSED_OPACITY,
+ );
} else {
normal_windows.push(win_ptr);
}
@@ -1410,58 +1325,51 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
// Manage entering/exiting Maximized state for normal windows
for &win_ptr in &normal_windows {
- let mode = (*win_ptr).tiling_mode;
- if mode == crate::tiling::TilingMode::Maximized && !(*win_ptr).was_maximized {
- // Entering Maximized mode
- let mut w = (*win_ptr).box_geom.width;
- let mut h = (*win_ptr).box_geom.height;
- if w <= 0 {
- w = if (*win_ptr).wm_scheduled.dimensions_hint.min_width > 32 {
- (*win_ptr).wm_scheduled.dimensions_hint.min_width as i32
- } else {
- 800
- };
- }
- if h <= 0 {
- h = if (*win_ptr).wm_scheduled.dimensions_hint.min_height > 32 {
- (*win_ptr).wm_scheduled.dimensions_hint.min_height as i32
- } else {
- 600
- };
+ let transition = crate::policy::arrange::maximized_transition(
+ (*win_ptr).tiling_mode == crate::tiling::TilingMode::Maximized,
+ (*win_ptr).was_maximized,
+ ((*win_ptr).box_geom.width, (*win_ptr).box_geom.height),
+ (
+ (*win_ptr).wm_scheduled.dimensions_hint.min_width as i32,
+ (*win_ptr).wm_scheduled.dimensions_hint.min_height as i32,
+ ),
+ ((*win_ptr).saved_maximized_width, (*win_ptr).saved_maximized_height),
+ ((*win_ptr).saved_maximized_virtual_x, (*win_ptr).saved_maximized_virtual_y),
+ );
+ match transition {
+ Some(crate::policy::arrange::MaximizedTransition::Enter { width, height }) => {
+ (*win_ptr).saved_maximized_width = width;
+ (*win_ptr).saved_maximized_height = height;
+ (*win_ptr).saved_maximized_virtual_x = (*win_ptr).virtual_x;
+ (*win_ptr).saved_maximized_virtual_y = (*win_ptr).virtual_y;
+ (*win_ptr).was_maximized = true;
+ log::info!("[Maximized] Saved window {:?} geometry: {}x{} at ({}, {})",
+ (*win_ptr).get_title_string().as_deref().unwrap_or(""),
+ width, height,
+ (*win_ptr).saved_maximized_virtual_x, (*win_ptr).saved_maximized_virtual_y
+ );
}
- (*win_ptr).saved_maximized_width = w;
- (*win_ptr).saved_maximized_height = h;
- (*win_ptr).saved_maximized_virtual_x = (*win_ptr).virtual_x;
- (*win_ptr).saved_maximized_virtual_y = (*win_ptr).virtual_y;
- (*win_ptr).was_maximized = true;
- log::info!("[Maximized] Saved window {:?} geometry: {}x{} at ({}, {})",
- (*win_ptr).get_title_string().as_deref().unwrap_or(""),
- (*win_ptr).saved_maximized_width, (*win_ptr).saved_maximized_height,
- (*win_ptr).saved_maximized_virtual_x, (*win_ptr).saved_maximized_virtual_y
- );
- } else if mode != crate::tiling::TilingMode::Maximized && (*win_ptr).was_maximized {
- // Exiting Maximized mode
- if (*win_ptr).saved_maximized_width > 0 && (*win_ptr).saved_maximized_height > 0 {
- (*win_ptr).box_geom.width = (*win_ptr).saved_maximized_width;
- (*win_ptr).box_geom.height = (*win_ptr).saved_maximized_height;
- (*win_ptr).virtual_x = (*win_ptr).saved_maximized_virtual_x;
- (*win_ptr).virtual_y = (*win_ptr).saved_maximized_virtual_y;
+ Some(crate::policy::arrange::MaximizedTransition::Exit { width, height, virtual_x, virtual_y }) => {
+ (*win_ptr).box_geom.width = width;
+ (*win_ptr).box_geom.height = height;
+ (*win_ptr).virtual_x = virtual_x;
+ (*win_ptr).virtual_y = virtual_y;
(*win_ptr).was_maximized = false;
-
+
(*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
- width: (*win_ptr).saved_maximized_width as u32,
- height: (*win_ptr).saved_maximized_height as u32,
+ width: width as u32,
+ height: height as u32,
});
(*win_ptr).wm_requested.bounds = crate::window::Dimensions {
- width: (*win_ptr).saved_maximized_width as u32,
- height: (*win_ptr).saved_maximized_height as u32,
+ width: width as u32,
+ height: height as u32,
};
log::info!("[Maximized] Restored window {:?} geometry: {}x{} at ({}, {})",
(*win_ptr).get_title_string().as_deref().unwrap_or(""),
- (*win_ptr).saved_maximized_width, (*win_ptr).saved_maximized_height,
- (*win_ptr).saved_maximized_virtual_x, (*win_ptr).saved_maximized_virtual_y
+ width, height, virtual_x, virtual_y
);
}
+ None => {}
}
}
@@ -1469,191 +1377,75 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
for &win_ptr in &normal_windows {
let mode = (*win_ptr).tiling_mode;
let is_focused = win_ptr == focused_window;
+ let app_id = (*win_ptr).get_app_id_string();
+ let is_cce_cloud = app_id.as_deref().map_or(false, |id| id.starts_with("cce-cloud"));
+
+ let placement = crate::policy::arrange::place_normal_window(
+ &crate::policy::arrange::NormalSnapshot {
+ mode,
+ box_geom: crate::policy::api::Rect {
+ x: (*win_ptr).box_geom.x,
+ y: (*win_ptr).box_geom.y,
+ width: (*win_ptr).box_geom.width,
+ height: (*win_ptr).box_geom.height,
+ },
+ min_size: (
+ (*win_ptr).wm_scheduled.dimensions_hint.min_width as i32,
+ (*win_ptr).wm_scheduled.dimensions_hint.min_height as i32,
+ ),
+ virtual_pos: ((*win_ptr).virtual_x, (*win_ptr).virtual_y),
+ active_resize: self.get_active_resize_dimensions(win_ptr),
+ is_cloud: is_cce_cloud,
+ saved_maximized_size: ((*win_ptr).saved_maximized_width, (*win_ptr).saved_maximized_height),
+ saved_maximized_virtual: ((*win_ptr).saved_maximized_virtual_x, (*win_ptr).saved_maximized_virtual_y),
+ },
+ &normal_params,
+ &ctx,
+ );
- if mode == crate::tiling::TilingMode::Popup {
- let hint_min_w = (*win_ptr).wm_scheduled.dimensions_hint.min_width as i32;
- let hint_min_h = (*win_ptr).wm_scheduled.dimensions_hint.min_height as i32;
- let fw = if (*win_ptr).box_geom.width > 0 {
- (*win_ptr).box_geom.width as i32
- } else if hint_min_w > 32 {
- hint_min_w
- } else {
- 360
- };
- let fh = if (*win_ptr).box_geom.height > 0 {
- (*win_ptr).box_geom.height as i32
- } else if hint_min_h > 32 {
- hint_min_h
- } else {
- 100
- };
- let app_id = (*win_ptr).get_app_id_string();
- let is_cce_cloud = app_id.as_deref().map_or(false, |id| id.starts_with("cce-cloud"));
-
- let (fx, fy) = if is_cce_cloud && self.layout.cloud_position_default.is_some() {
- let pos = self.layout.cloud_position_default.unwrap();
- (usable_x + pos[0], usable_y + pos[1])
- } else {
- (usable_x + usable_w - fw - self.layout.gap_right, usable_y + self.layout.gap_top)
- };
-
- (*win_ptr).rendering_requested.x = fx;
- (*win_ptr).rendering_requested.y = fy;
- (*win_ptr).scale = 1.0;
-
- (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
- width: fw as u32,
- height: fh as u32,
- });
- (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
- width: fw as u32,
- height: fh as u32,
- };
- } else if mode == crate::tiling::TilingMode::Fullscreen {
- (*win_ptr).rendering_requested.x = phys_x;
- (*win_ptr).rendering_requested.y = phys_y;
- (*win_ptr).scale = 1.0;
- (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
- width: phys_w as u32,
- height: phys_h as u32,
- });
- (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
- width: phys_w as u32,
- height: phys_h as u32,
- };
+ (*win_ptr).rendering_requested.x = placement.pos.0;
+ (*win_ptr).rendering_requested.y = placement.pos.1;
+ (*win_ptr).scale = placement.scale;
+ if let Some((vx, vy)) = placement.virtual_write {
+ (*win_ptr).virtual_x = vx;
+ (*win_ptr).virtual_y = vy;
+ }
+ if let Some(hidden) = placement.hidden {
+ (*win_ptr).rendering_requested.hidden = hidden;
+ }
+ (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
+ width: placement.size.0,
+ height: placement.size.1,
+ });
+ (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
+ width: placement.size.0,
+ height: placement.size.1,
+ };
+ if placement.tiled_all_edges {
(*win_ptr).wm_requested.tiled = 1 | 2 | 4 | 8;
- } else if mode == crate::tiling::TilingMode::Maximized {
- // Maximized mode: Resizes to fully fill all cells of the desktop grid it is fully/partially inside of.
- let scale = self.layout.desktop_grid_scale;
-
- // Use saved maximized geometry for cell calculation
- let x1 = (*win_ptr).saved_maximized_virtual_x;
- let y1 = (*win_ptr).saved_maximized_virtual_y;
- let w = (*win_ptr).saved_maximized_width as f64;
- let h = (*win_ptr).saved_maximized_height as f64;
- let x2 = x1 + w;
- let y2 = y1 + h;
-
- let col_min = (x1 / scale).floor() as i32;
- let col_max = ((x2 / scale).ceil() as i32 - 1).max(col_min);
- let row_min = (y1 / scale).floor() as i32;
- let row_max = ((y2 / scale).ceil() as i32 - 1).max(row_min);
-
- let snapped_x1 = col_min as f64 * scale;
- let snapped_x2 = (col_max + 1) as f64 * scale;
- let snapped_y1 = row_min as f64 * scale;
- let snapped_y2 = (row_max + 1) as f64 * scale;
-
- let fw = snapped_x2 - snapped_x1;
- let fh = snapped_y2 - snapped_y1;
-
- // Update current virtual position for rendering
- (*win_ptr).virtual_x = snapped_x1;
- (*win_ptr).virtual_y = snapped_y1;
-
- let final_x = phys_x + (((*win_ptr).virtual_x - self.desk_pan_x) * self.desk_zoom) as i32;
- let final_y = phys_y + (((*win_ptr).virtual_y - self.desk_pan_y) * self.desk_zoom) as i32;
-
- (*win_ptr).rendering_requested.x = final_x;
- (*win_ptr).rendering_requested.y = final_y;
- (*win_ptr).scale = self.desk_zoom;
-
- // Offscreen check
- let scaled_w = fw * self.desk_zoom;
- let scaled_h = fh * self.desk_zoom;
- let is_offscreen = (final_x as f64 + scaled_w + 50.0) < phys_x as f64
- || (final_x as f64 - 50.0) > (phys_x as f64 + viewport_w)
- || (final_y as f64 + scaled_h + 50.0) < phys_y as f64
- || (final_y as f64 - 50.0) > (phys_y as f64 + viewport_h);
- (*win_ptr).rendering_requested.hidden = is_offscreen;
-
- (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
- width: fw as u32,
- height: fh as u32,
- });
- (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
- width: fw as u32,
- height: fh as u32,
- };
- } else {
- // Regular pannable window on the virtual surface
- let fw = if let Some(resize_size) = self.get_active_resize_dimensions(win_ptr) {
- resize_size.0 as i32
- } else if (*win_ptr).box_geom.width > 0 {
- (*win_ptr).box_geom.width as i32
- } else if (*win_ptr).wm_scheduled.dimensions_hint.min_width > 32 {
- (*win_ptr).wm_scheduled.dimensions_hint.min_width as i32
- } else {
- 800
- };
- let fh = if let Some(resize_size) = self.get_active_resize_dimensions(win_ptr) {
- resize_size.1 as i32
- } else if (*win_ptr).box_geom.height > 0 {
- (*win_ptr).box_geom.height as i32
- } else if (*win_ptr).wm_scheduled.dimensions_hint.min_height > 32 {
- (*win_ptr).wm_scheduled.dimensions_hint.min_height as i32
- } else {
- 600
- };
-
- let final_x = phys_x + (((*win_ptr).virtual_x - self.desk_pan_x) * self.desk_zoom) as i32;
- let final_y = phys_y + (((*win_ptr).virtual_y - self.desk_pan_y) * self.desk_zoom) as i32;
-
- (*win_ptr).rendering_requested.x = final_x;
- (*win_ptr).rendering_requested.y = final_y;
- (*win_ptr).scale = self.desk_zoom;
-
- // Offscreen check
- let scaled_w = fw as f64 * self.desk_zoom;
- let scaled_h = fh as f64 * self.desk_zoom;
- let is_offscreen = (final_x as f64 + scaled_w + 50.0) < phys_x as f64
- || (final_x as f64 - 50.0) > (phys_x as f64 + viewport_w)
- || (final_y as f64 + scaled_h + 50.0) < phys_y as f64
- || (final_y as f64 - 50.0) > (phys_y as f64 + viewport_h);
- (*win_ptr).rendering_requested.hidden = is_offscreen;
-
- (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions {
- width: fw as u32,
- height: fh as u32,
- });
- (*win_ptr).wm_requested.bounds = crate::window::Dimensions {
- width: fw as u32,
- height: fh as u32,
- };
}
// Apply borders, opacity, and blur to normal windows
- let r = self.layout.border_r;
- let g_val = self.layout.border_g;
- let b = self.layout.border_b;
- let a = self.layout.border_a;
-
(*win_ptr).rendering_requested.border = crate::window::Border {
edges: crate::window::Edges { top: true, bottom: true, left: true, right: true },
width: bw as u32,
- r,
- g: g_val,
- b,
- a,
- };
-(*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 }
+ r: self.layout.border_r,
+ g: self.layout.border_g,
+ b: self.layout.border_b,
+ a: self.layout.border_a,
};
+ (*win_ptr).rendering_requested.blur = self.layout.window_blur;
+ (*win_ptr).rendering_requested.opacity = crate::policy::arrange::window_opacity(
+ is_focused,
+ self.layout.window_opacity,
+ crate::policy::arrange::NORMAL_UNFOCUSED_OPACITY,
+ );
}
- // Position status bar windows on this output
- let bar_h = self.layout.bar_height as u32;
- let spacing = 12;
- let margin = 12;
- let mut top_left = Vec::new();
- let mut top_center = Vec::new();
- let mut top_right = Vec::new();
- let mut bottom_left = Vec::new();
- let mut bottom_center = 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();
+ // Position status bar windows on this output.
+ // Snapshot the bars (excluding any being interactively dragged),
+ // lay them out in policy code, then apply the placements.
+ let mut status_items: Vec<crate::policy::arrange::StatusBarItem> = Vec::new();
+ let mut status_wins: Vec<*mut Window> = Vec::new();
for &win_ptr in self.windows.iter() {
if win_ptr.is_null() || (*win_ptr).closed {
@@ -1682,265 +1474,39 @@ fn get_closest_tag(x: f64, y: f64) -> i32 {
continue;
}
- let edge = resolve_edge(win_ptr);
- match edge {
- crate::window::StatusEdge::TopLeft => {
- top_left.push(win_ptr);
- }
- crate::window::StatusEdge::TopCenter => {
- top_center.push(win_ptr);
- }
- crate::window::StatusEdge::TopRight => {
- top_right.push(win_ptr);
- }
- crate::window::StatusEdge::BottomLeft => {
- bottom_left.push(win_ptr);
- }
- crate::window::StatusEdge::BottomCenter => {
- bottom_center.push(win_ptr);
- }
- crate::window::StatusEdge::BottomRight => {
- bottom_right.push(win_ptr);
- }
- crate::window::StatusEdge::Left => {
- left_side.push(win_ptr);
- }
- crate::window::StatusEdge::Right => {
- right_side.push(win_ptr);
- }
- _ => {
- full_top.push(win_ptr);
- }
- }
log::info!("[ArrangeStatus] app_id={} status_edge={:?}", app_id, (*win_ptr).status_edge);
+ status_items.push(crate::policy::arrange::StatusBarItem {
+ app_id,
+ edge: (*win_ptr).status_edge,
+ prev_len: std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height),
+ });
+ status_wins.push(win_ptr);
}
}
}
- log::info!("[ArrangeStatus] top_left_len={}, top_center_len={}, top_right_len={}, left_side_len={}", top_left.len(), top_center.len(), top_right.len(), left_side.len());
-
- const LEFT_ORDER: &[&str] = &["viewport", "window"];
- const RIGHT_ORDER: &[&str] = &["tray", "cpu", "memory", "brightness", "volume", "battery", "clock"];
+ let placements = crate::policy::arrange::layout_status_bars(
+ &status_items,
+ &crate::policy::arrange::StatusBarLayoutParams {
+ output: crate::policy::api::Rect {
+ x: wlr_box.x,
+ y: wlr_box.y,
+ width: wlr_box.width,
+ height: wlr_box.height,
+ },
+ bar_height: self.layout.bar_height as u32,
+ hide_mode: self.status_hide_mode,
+ hide_mode_preview: self.layout.status_module_hide_mode_preview as i32,
+ },
+ );
- 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-interface-left-")
- .or_else(|| app_id.strip_prefix("cce-status-left-"))
- .unwrap_or(&app_id);
- LEFT_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-interface-right-")
- .or_else(|| app_id.strip_prefix("cce-status-right-"))
- .unwrap_or(&app_id);
- RIGHT_ORDER.iter().position(|&m| m == name).unwrap_or(99)
- });
- };
-
- sort_left(&mut top_left);
- sort_left(&mut top_center);
- sort_right(&mut top_right);
- sort_left(&mut bottom_left);
- sort_left(&mut bottom_center);
- sort_right(&mut bottom_right);
-
- // 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
- };
-
- let mut top_right_width_needed = 0;
- for &win_ptr in &top_right {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let w = if prev_len > 0 { prev_len as u32 } else { 100 };
- top_right_width_needed += w as i32 + spacing;
- }
- let top_right_boundary = wlr_box.x + wlr_box.width - margin - top_right_width_needed;
-
- // 1a. Left Group (TopLeft / nw)
- let mut cur_left_x = wlr_box.x + margin;
- for &win_ptr in &top_left {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let mut w = if prev_len > 0 { prev_len as u32 } else { 100 };
- let max_allowed_w = top_right_boundary - cur_left_x - spacing;
- if w as i32 > max_allowed_w {
- w = std::cmp::max(max_allowed_w, 20) as u32;
- }
- let app_id = (*win_ptr).get_app_id_string().unwrap_or_default();
- log::info!("[TopLeftLayout] app_id={} x={}, w={}", app_id, cur_left_x, w);
- (*win_ptr).rendering_requested.x = cur_left_x;
- (*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;
- }
-
- // 1b. Center Group (TopCenter / n)
- let mut top_center_width_needed = 0;
- for &win_ptr in &top_center {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let w = if prev_len > 0 { prev_len as u32 } else { 100 };
- top_center_width_needed += w as i32 + spacing;
- }
- if top_center_width_needed > 0 {
- top_center_width_needed -= spacing;
- }
- let center_start_x = wlr_box.x + (wlr_box.width - top_center_width_needed) / 2;
- let mut cur_center_x = std::cmp::max(center_start_x, cur_left_x + spacing);
-
- for &win_ptr in &top_center {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let mut w = if prev_len > 0 { prev_len as u32 } else { 100 };
- let max_allowed_w = top_right_boundary - cur_center_x - spacing;
- if w as i32 > max_allowed_w {
- w = std::cmp::max(max_allowed_w, 20) as u32;
- }
- let app_id = (*win_ptr).get_app_id_string().unwrap_or_default();
- log::info!("[TopCenterLayout] app_id={} x={}, w={}", app_id, cur_center_x, w);
- (*win_ptr).rendering_requested.x = cur_center_x;
- (*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_center_x += w as i32 + spacing;
- }
-
- // 1c. Right Group (TopRight / ne)
- let mut cur_right_x = wlr_box.x + wlr_box.width - margin;
- for win_ptr in top_right.into_iter().rev() {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let w = if prev_len > 0 { prev_len 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_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;
- }
-
- for win_ptr in full_top {
- (*win_ptr).rendering_requested.x = wlr_box.x;
- (*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 bottom_right_width_needed = 0;
- for &win_ptr in &bottom_right {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let w = if prev_len > 0 { prev_len as u32 } else { 100 };
- bottom_right_width_needed += w as i32 + spacing;
- }
- let bottom_right_boundary = wlr_box.x + wlr_box.width - margin - bottom_right_width_needed;
-
- // 2a. Left Group (BottomLeft / sw)
- let mut cur_left_x = wlr_box.x + margin;
- for &win_ptr in &bottom_left {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let mut w = if prev_len > 0 { prev_len as u32 } else { 100 };
- let max_allowed_w = bottom_right_boundary - cur_left_x - spacing;
- if w as i32 > max_allowed_w {
- w = std::cmp::max(max_allowed_w, 20) as u32;
- }
- (*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;
- }
-
- // 2b. Center Group (BottomCenter / s)
- let mut bottom_center_width_needed = 0;
- for &win_ptr in &bottom_center {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let w = if prev_len > 0 { prev_len as u32 } else { 100 };
- bottom_center_width_needed += w as i32 + spacing;
- }
- if bottom_center_width_needed > 0 {
- bottom_center_width_needed -= spacing;
- }
- let center_start_x = wlr_box.x + (wlr_box.width - bottom_center_width_needed) / 2;
- let mut cur_center_x = std::cmp::max(center_start_x, cur_left_x + spacing);
-
- for &win_ptr in &bottom_center {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let mut w = if prev_len > 0 { prev_len as u32 } else { 100 };
- let max_allowed_w = bottom_right_boundary - cur_center_x - spacing;
- if w as i32 > max_allowed_w {
- w = std::cmp::max(max_allowed_w, 20) as u32;
+ for (&win_ptr, placement) in status_wins.iter().zip(placements.iter()) {
+ if let Some(pl) = placement {
+ (*win_ptr).rendering_requested.x = pl.x;
+ (*win_ptr).rendering_requested.y = pl.y;
+ (*win_ptr).wm_requested.dimensions = Some(crate::window::Dimensions { width: pl.width, height: pl.height });
+ (*win_ptr).wm_requested.bounds = crate::window::Dimensions { width: pl.width, height: pl.height };
}
- (*win_ptr).rendering_requested.x = cur_center_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_center_x += w as i32 + spacing;
- }
-
- // 2c. Right Group (BottomRight / se)
- let mut cur_right_x = wlr_box.x + wlr_box.width - margin;
- for win_ptr in bottom_right.into_iter().rev() {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let w = if prev_len > 0 { prev_len 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 left_total_height = 0;
- for &win_ptr in &left_side {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let actual_h = if prev_len > 0 { prev_len as u32 } else { 100 };
- left_total_height += actual_h as i32;
- }
- if !left_side.is_empty() {
- left_total_height += (left_side.len() as i32 - 1) * spacing;
- }
- let mut cur_left_y = wlr_box.y + (wlr_box.height - left_total_height) / 2;
-
- for win_ptr in left_side {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let actual_h = if prev_len > 0 { prev_len 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 right_total_height = 0;
- for &win_ptr in &right_side {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let actual_h = if prev_len > 0 { prev_len as u32 } else { 100 };
- right_total_height += actual_h as i32;
- }
- if !right_side.is_empty() {
- right_total_height += (right_side.len() as i32 - 1) * spacing;
- }
- let mut cur_right_y = wlr_box.y + (wlr_box.height - right_total_height) / 2;
-
- for win_ptr in right_side {
- let prev_len = std::cmp::max((*win_ptr).box_geom.width, (*win_ptr).box_geom.height);
- let actual_h = if prev_len > 0 { prev_len 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;
}
// Force configure for all status bar windows on this output so they receive the new geometry immediately