Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Fix wallpaper grid mode graphical glitches
Makefile | 1 +
scripts/cce-app-menu | 20 ++++----
scripts/cce-desktop-menu | 12 ++---
src/bin/cce.rs | 10 ++++
src/server/config.rs | 7 ++-
src/server/output.rs | 104 ++++++++++++++++++++++++-----------------
src/server/window.rs | 6 ++-
src/server/window_manager.rs | 109 +++++++++++++++++++++++++++++++++++++++++--
8 files changed, 202 insertions(+), 67 deletions(-)
diff --git a/Makefile b/Makefile
index 7382053..e774cca 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ install: build
@if [ -f ../target/release/cce-fx ]; then \
install -m 755 ../target/release/cce-fx ~/.local/bin/cce-fx; \
ln -sf cce-fx ~/.local/bin/cce; \
+ ln -sf cce-fx ~/.local/bin/cce-ctl; \
else \
echo "Error: cce-fx binary not found"; exit 1; \
fi
diff --git a/scripts/cce-app-menu b/scripts/cce-app-menu
index 3112702..504e4ca 100644
--- a/scripts/cce-app-menu
+++ b/scripts/cce-app-menu
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# cce-app-menu — Context menu for CCE application windows
-CLEARCTL="/home/lsgalante/.local/bin/cce control"
-CLEAR_CLOUD="/home/lsgalante/.local/bin/clear-cloud"
+CCE_CTL="/home/lsgalante/.local/bin/cce-ctl"
+CLEAR_CLOUD="/home/lsgalante/.local/bin/cce-cloud"
# Default to empty/none
x_arg=""
@@ -57,7 +57,7 @@ json_layout='{
]
}'
-# Spawn clear-cloud with the json layout and coordinates
+# Spawn cce-cloud with the json layout and coordinates
selected=$(echo "$json_layout" | $CLEAR_CLOUD --json $x_arg $y_arg 2>/dev/null)
[[ -z "$selected" ]] && exit 0
@@ -68,25 +68,25 @@ btn=$(echo "$selected" | python3 -c "import sys, json; print(json.load(sys.stdin
[[ -z "$btn" ]] && exit 0
# First focus the window so the commands apply to it
-$CLEARCTL focus-window "$window_index"
+$CCE_CTL focus-window "$window_index"
case "$btn" in
"maximize")
- $CLEARCTL mode maximized "$app_id"
+ $CCE_CTL mode maximized "$app_id"
;;
"tile_grid")
- $CLEARCTL mode grid "$app_id"
+ $CCE_CTL mode grid "$app_id"
;;
"float")
- $CLEARCTL mode floating "$app_id"
+ $CCE_CTL mode floating "$app_id"
;;
"fullscreen")
- $CLEARCTL mode fullscreen "$app_id"
+ $CCE_CTL mode fullscreen "$app_id"
;;
"minimize")
- $CLEARCTL minimize
+ $CCE_CTL minimize
;;
"close")
- $CLEARCTL close
+ $CCE_CTL close
;;
esac
diff --git a/scripts/cce-desktop-menu b/scripts/cce-desktop-menu
index ec5ab11..0115fdf 100644
--- a/scripts/cce-desktop-menu
+++ b/scripts/cce-desktop-menu
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# cce-desktop-menu — Context menu for CCE desktop background
-CLEARCTL="/home/lsgalante/.local/bin/cce control"
-CLEAR_CLOUD="/home/lsgalante/.local/bin/clear-cloud"
+CCE_CTL="/home/lsgalante/.local/bin/cce-ctl"
+CLEAR_CLOUD="/home/lsgalante/.local/bin/cce-cloud"
# Default to empty coordinates if not provided
x_arg=""
@@ -44,7 +44,7 @@ json_layout='{
]
}'
-# Spawn clear-cloud with the json layout and coordinates
+# Spawn cce-cloud with the json layout and coordinates
selected=$(echo "$json_layout" | $CLEAR_CLOUD --json $x_arg $y_arg 2>/dev/null)
[[ -z "$selected" ]] && exit 0
@@ -74,12 +74,12 @@ case "$btn" in
/home/lsgalante/.local/bin/cce-system-settings &
;;
"expose")
- $CLEARCTL expose
+ $CCE_CTL expose
;;
"reload")
- $CLEARCTL reload
+ $CCE_CTL reload
;;
"exit")
- $CLEARCTL exit
+ $CCE_CTL exit
;;
esac
diff --git a/src/bin/cce.rs b/src/bin/cce.rs
index 6b9b97a..e3b9f3c 100644
--- a/src/bin/cce.rs
+++ b/src/bin/cce.rs
@@ -2,6 +2,16 @@ use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
+ let program_name = args.get(0)
+ .and_then(|s| std::path::Path::new(s).file_name())
+ .map(|n| n.to_string_lossy().into_owned())
+ .unwrap_or_default();
+
+ if program_name == "cce-ctl" {
+ cce_fx::run_cce_ctl(args);
+ return;
+ }
+
if args.len() > 1 {
match args[1].as_str() {
"client" => {
diff --git a/src/server/config.rs b/src/server/config.rs
index a03af67..c7e17df 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -1261,6 +1261,11 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
surface.desktop_solid_color = val.to_string();
}
}
+ "grid_cell_size" | "desktop_grid_scale" => {
+ if let Some(val) = entry.value().as_i64() {
+ surface.desktop_grid_scale = val;
+ }
+ }
_ => {}
}
}
@@ -1312,7 +1317,7 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "surface") {
surface.desktop_gap_color = get_child_arg_string(node, "desktop_gap_color", &default_desktop_gap_color());
surface.desktop_cell_color = get_child_arg_string(node, "desktop_cell_color", &default_desktop_cell_color());
- surface.desktop_grid_scale = get_child_arg_i64(node, "desktop_grid_scale", default_desktop_grid_scale());
+ surface.desktop_grid_scale = get_child_arg_i64(node, "grid_cell_size", get_child_arg_i64(node, "desktop_grid_scale", default_desktop_grid_scale()));
surface.desktop_gap_width = get_child_arg_i64(node, "desktop_gap_width", default_desktop_gap_width());
surface.desktop_cell_corner_radius = get_child_arg_i64(node, "desktop_cell_corner_radius", default_desktop_cell_corner_radius());
surface.desktop_cell_fade_inset = get_child_arg_i64(node, "desktop_cell_fade_inset", default_desktop_cell_fade_inset());
diff --git a/src/server/output.rs b/src/server/output.rs
index 4567624..560e5ce 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -166,6 +166,7 @@ pub struct Output {
pub last_grid_cell_corner_radius: i32,
pub last_grid_cell_fade_inset: i64,
pub last_grid_gap_color: String,
+ pub last_grid_gap_color_rgba: [f32; 4],
pub grid_is_low_res: bool,
pub last_grid_enable_solid_color: bool,
pub grid_rect_pool: Vec<*mut ffi::wlr_scene_rect>,
@@ -419,6 +420,7 @@ impl Output {
last_grid_cell_corner_radius: 0,
last_grid_cell_fade_inset: 0,
last_grid_gap_color: String::new(),
+ last_grid_gap_color_rgba: [0.0, 0.0, 0.0, 0.0],
grid_is_low_res: false,
last_grid_enable_solid_color: false,
grid_rect_pool: Vec::new(),
@@ -468,6 +470,9 @@ impl Output {
}
pub unsafe fn render_and_commit(&mut self) -> Result<(), &'static str> {
+ // Update grid node positions and parameters first, which marks the scene output as damaged if changed
+ self.draw_grid();
+
if !ffi::wlr_scene_output_needs_frame(self.scene_output) {
return Ok(());
}
@@ -480,8 +485,6 @@ impl Output {
}
}
- self.draw_grid();
-
let mut state = std::mem::zeroed();
ffi::wlr_output_state_init(&mut state);
@@ -569,6 +572,9 @@ impl Output {
// Enable the grid tree.
ffi::wlr_scene_node_set_enabled(self.grid_tree as *mut ffi::wlr_scene_node, true);
+ // Keep the grid tree at the top of the background layer to prevent wallpaper windows from overlapping it
+ ffi::wlr_scene_node_raise_to_top(self.grid_tree as *mut ffi::wlr_scene_node);
+
// Force redrawing if switching back from solid color mode.
if self.last_grid_enable_solid_color {
self.last_grid_enable_solid_color = false;
@@ -576,7 +582,11 @@ impl Output {
}
let (viewport_w, viewport_h) = self.current.dimensions();
- let zoom = wm.desk_zoom;
+ let zoom = if wm.desk_zoom.is_nan() || wm.desk_zoom <= 0.0 {
+ 1.0
+ } else {
+ wm.desk_zoom
+ };
// Calculate Level of Detail (LOD) grid spacing.
// As the user zooms out, we scale up spacing to prevent grid density from becoming too high.
@@ -585,16 +595,18 @@ impl Output {
let mut period = cell_size + gap_size;
const MIN_PERIOD_PIXELS: f64 = 40.0;
- while period * zoom < MIN_PERIOD_PIXELS {
+ let mut iterations = 0;
+ while period * zoom < MIN_PERIOD_PIXELS && iterations < 20 {
cell_size *= 2.0;
gap_size *= 2.0;
period = cell_size + gap_size;
+ iterations += 1;
}
let cell_color: [f32; 4] = wm.layout.desktop_cell_color;
let cell_corner_radius = wm.layout.desktop_cell_corner_radius;
let cell_fade_inset = wm.layout.desktop_cell_fade_inset;
- let gap_color = wm.layout.desktop_gap_color.clone();
+ let gap_color = &wm.layout.desktop_gap_color;
// Detect layout or viewport changes to request a redraw.
let structure_changed = self.last_grid_viewport_w != viewport_w
@@ -605,23 +617,26 @@ impl Output {
|| self.last_grid_cell_color != cell_color
|| self.last_grid_cell_corner_radius != cell_corner_radius
|| self.last_grid_cell_fade_inset != cell_fade_inset
- || self.last_grid_gap_color != gap_color;
+ || &self.last_grid_gap_color != gap_color;
+
+ let pan_changed = self.last_grid_pan_x != wm.desk_pan_x
+ || self.last_grid_pan_y != wm.desk_pan_y;
if structure_changed {
self.grid_force_redraw_frames = 3;
}
- let period_pixels = period * zoom;
-
- // Modulo shift calculations for virtual grid infinite scrolling.
- let shift_x = -((wm.desk_pan_x * zoom).rem_euclid(period_pixels));
- let shift_y = -((wm.desk_pan_y * zoom).rem_euclid(period_pixels));
-
- let dest_x = self.sent.x + shift_x.round() as i32;
- let dest_y = self.sent.y + shift_y.round() as i32;
+ // Keep root grid tree node static at physical output position.
+ ffi::river_scene_node_set_position_if_changed(
+ self.grid_tree as *mut ffi::wlr_scene_node,
+ self.sent.x,
+ self.sent.y,
+ );
- if self.grid_force_redraw_frames > 0 {
- self.grid_force_redraw_frames -= 1;
+ if self.grid_force_redraw_frames > 0 || pan_changed {
+ if self.grid_force_redraw_frames > 0 {
+ self.grid_force_redraw_frames -= 1;
+ }
self.last_grid_viewport_w = viewport_w;
self.last_grid_viewport_h = viewport_h;
@@ -631,12 +646,10 @@ impl Output {
self.last_grid_cell_color = cell_color;
self.last_grid_cell_corner_radius = cell_corner_radius;
self.last_grid_cell_fade_inset = cell_fade_inset;
- self.last_grid_gap_color = gap_color;
+ self.last_grid_gap_color = gap_color.clone();
+ self.last_grid_gap_color_rgba = crate::config::parse_hex_color_rgba(&self.last_grid_gap_color);
self.grid_is_low_res = self.last_grid_zoom != zoom;
- // Bring the grid to the top of the scene.
- ffi::wlr_scene_node_raise_to_top(self.grid_tree as *mut ffi::wlr_scene_node);
-
let grid_tree = self.grid_tree;
let pool = &mut self.grid_rect_pool;
let mut pool_idx = 0;
@@ -667,29 +680,39 @@ impl Output {
};
// 1. Draw base/background rect using the gap color.
- // Sized larger by one period to prevent flickering at edges during pan shifts.
- let bg_w = viewport_w + period_pixels.ceil() as i32;
- let bg_h = viewport_h + period_pixels.ceil() as i32;
- let gap_color_rgba = crate::config::parse_hex_color_rgba(&self.last_grid_gap_color);
- get_rect(bg_w, bg_h, gap_color_rgba.as_ptr(), 0, 0, 0);
+ // Sized exactly to the viewport (since the grid tree is static).
+ get_rect(viewport_w, viewport_h, self.last_grid_gap_color_rgba.as_ptr(), 0, 0, 0);
// 2. Draw grid cells.
- let cols = (viewport_w as f64 / period_pixels).ceil() as i32 + 1;
- let rows = (viewport_h as f64 / period_pixels).ceil() as i32 + 1;
- let rw = (cell_size * zoom).round() as i32;
- let rh = (cell_size * zoom).round() as i32;
+ let min_col = (wm.desk_pan_x / period).floor() as i32 - 1;
+ let max_col = (((viewport_w as f64 / zoom) + wm.desk_pan_x) / period).ceil() as i32 + 1;
+ let min_row = (wm.desk_pan_y / period).floor() as i32 - 1;
+ let max_row = (((viewport_h as f64 / zoom) + wm.desk_pan_y) / period).ceil() as i32 + 1;
+
+ let col_range = max_col.saturating_sub(min_col);
+ let row_range = max_row.saturating_sub(min_row);
- if rw > 0 && rh > 0 {
+ if col_range > 0 && row_range > 0 && col_range <= 1000 && row_range <= 1000 && col_range * row_range <= 20000 {
let scaled_corner_radius = (cell_corner_radius as f64 * zoom).round() as i32;
let inset_scaled = (cell_fade_inset as f64 * zoom * 1000.0).round() as i32;
- for col in 0..=cols {
- let rel_x = (col as f64 * period_pixels).round() as i32;
- for row in 0..=rows {
- let rel_y = (row as f64 * period_pixels).round() as i32;
- let cell_rect = get_rect(rw, rh, cell_color.as_ptr(), rel_x, rel_y, scaled_corner_radius);
- if !cell_rect.is_null() && inset_scaled > 0 {
- ffi::wlr_scene_rect_set_fade_inset(cell_rect, inset_scaled);
+ for col in min_col..=max_col {
+ let x1 = (((col as f64 * period) - wm.desk_pan_x) * zoom).round() as i32;
+ let x2 = (((col as f64 * period) + cell_size - wm.desk_pan_x) * zoom).round() as i32;
+ let rw = x2 - x1;
+
+ if rw > 0 {
+ for row in min_row..=max_row {
+ let y1 = (((row as f64 * period) - wm.desk_pan_y) * zoom).round() as i32;
+ let y2 = (((row as f64 * period) + cell_size - wm.desk_pan_y) * zoom).round() as i32;
+ let rh = y2 - y1;
+
+ if rh > 0 {
+ let cell_rect = get_rect(rw, rh, cell_color.as_ptr(), x1, y1, scaled_corner_radius);
+ if !cell_rect.is_null() && inset_scaled > 0 {
+ ffi::wlr_scene_rect_set_fade_inset(cell_rect, inset_scaled);
+ }
+ }
}
}
}
@@ -701,13 +724,6 @@ impl Output {
}
}
- // Apply shift translation to root grid tree node.
- ffi::river_scene_node_set_position_if_changed(
- self.grid_tree as *mut ffi::wlr_scene_node,
- dest_x,
- dest_y,
- );
-
self.last_grid_pan_x = wm.desk_pan_x;
self.last_grid_pan_y = wm.desk_pan_y;
}
diff --git a/src/server/window.rs b/src/server/window.rs
index 38192bb..d16e1ea 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -650,7 +650,11 @@ impl Window {
return;
}
let title_str = self.get_title_string().unwrap_or_default();
- if let Some(saved) = (*self.server).wm.match_and_remove_restore_state(&app_id_str, &title_str) {
+ let mut saved_opt = (*self.server).wm.match_and_remove_restore_state(&app_id_str, &title_str);
+ if saved_opt.is_none() {
+ saved_opt = (*self.server).wm.match_last_window_state(&app_id_str, &title_str);
+ }
+ if let Some(saved) = saved_opt {
log::info!("Restoring saved state for window: app_id={}, title={}. Position: ({}, {}), Size: {}x{}", app_id_str, title_str, saved.virtual_x, saved.virtual_y, saved.width, saved.height);
self.tiling_mode = saved.tiling_mode;
self.minimized = saved.minimized;
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 9994ac5..5fbc251 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -70,6 +70,8 @@ pub struct SavedState {
pub desk_zoom: f64,
pub global_layout: crate::tiling::TilingMode,
pub windows: Vec<SavedWindowState>,
+ #[serde(default)]
+ pub last_window_states: Vec<SavedWindowState>,
}
pub struct WindowManager {
@@ -107,6 +109,7 @@ pub struct WindowManager {
pub input_config: crate::config::InputConfig,
pub last_status_update: std::cell::RefCell<Option<crate::status_server::StatusUpdate>>,
pub restore_queue: Vec<SavedWindowState>,
+ pub last_window_states: Vec<SavedWindowState>,
pub shutting_down: bool,
pub target_desk_pan_x: Option<f64>,
pub target_desk_pan_y: Option<f64>,
@@ -169,6 +172,7 @@ impl WindowManager {
self.mode = WindowManagerMode::Normal;
self.global_layout = crate::tiling::TilingMode::Cascade;
self.restore_queue = Vec::new();
+ self.last_window_states = Vec::new();
self.shutting_down = false;
self.layout = crate::config::Layout::default();
self.output_scale = 1.0;
@@ -234,6 +238,7 @@ impl WindowManager {
self.mode = if (state.desk_zoom - 1.0).abs() > 0.001 { WindowManagerMode::Overview } else { WindowManagerMode::Normal };
self.global_layout = state.global_layout;
self.restore_queue = state.windows;
+ self.last_window_states = state.last_window_states;
self.has_restored_focused_window = self.restore_queue.iter().any(|w| w.focused);
self.restored_focused_window_mapped = false;
log::info!(
@@ -249,7 +254,7 @@ impl WindowManager {
}
}
- pub unsafe fn save_state(&self) {
+ pub unsafe fn save_state(&mut self) {
if self.shutting_down {
return;
}
@@ -261,6 +266,8 @@ impl WindowManager {
let focused_win = self.focused_window();
let mut saved_wins = Vec::new();
+ let mut last_states = self.last_window_states.clone();
+
for &w in self.windows.iter() {
if w.is_null() || (*w).closed || matches!((*w).state, crate::window::WindowState::Closing | crate::window::WindowState::Init) {
continue;
@@ -270,6 +277,9 @@ impl WindowManager {
}
let app_id = (*w).get_app_id_string().unwrap_or_default();
+ if app_id.is_empty() {
+ continue;
+ }
let title = (*w).get_title_string().unwrap_or_default();
let pid = (*w).unreliable_pid();
@@ -310,9 +320,9 @@ impl WindowManager {
let is_focused = w == focused_win;
- saved_wins.push(SavedWindowState {
- app_id,
- title,
+ let win_state = SavedWindowState {
+ app_id: app_id.clone(),
+ title: title.clone(),
tiling_mode: (*w).tiling_mode,
minimized: (*w).minimized,
virtual_x: (*w).virtual_x,
@@ -322,8 +332,17 @@ impl WindowManager {
height: (*w).box_geom.height as u32,
cmdline,
focused: is_focused,
- });
+ };
+
+ saved_wins.push(win_state.clone());
+
+ if let Some(pos) = last_states.iter().position(|s| s.app_id == app_id) {
+ last_states[pos] = win_state;
+ } else {
+ last_states.push(win_state);
+ }
}
+ self.last_window_states = last_states;
let state = SavedState {
desk_pan_x: self.desk_pan_x,
@@ -331,6 +350,7 @@ impl WindowManager {
desk_zoom: self.desk_zoom,
global_layout: self.global_layout,
windows: saved_wins,
+ last_window_states: self.last_window_states.clone(),
};
if let Ok(json_str) = serde_json::to_string_pretty(&state) {
@@ -370,6 +390,32 @@ impl WindowManager {
None
}
+ pub unsafe fn match_last_window_state(&self, app_id: &str, title: &str) -> Option<SavedWindowState> {
+ if app_id.is_empty() {
+ return None;
+ }
+ // First pass: Exact match (app_id AND title)
+ if let Some(w) = self.last_window_states.iter().find(|w| w.app_id == app_id && w.title == title) {
+ return Some(w.clone());
+ }
+ // Second pass: Fuzzy title match
+ if let Some(w) = self.last_window_states.iter().find(|w| {
+ if w.app_id != app_id {
+ return false;
+ }
+ let t1 = title.trim_end_matches('*');
+ let t2 = w.title.trim_end_matches('*');
+ t1 == t2 || t1.starts_with(t2) || t2.starts_with(t1)
+ }) {
+ return Some(w.clone());
+ }
+ // Third pass: app_id only match
+ if let Some(w) = self.last_window_states.iter().find(|w| w.app_id == app_id) {
+ return Some(w.clone());
+ }
+ None
+ }
+
pub unsafe fn spawn_restored_windows(&mut self) {
log::info!("Spawning restored windows. Total: {}", self.restore_queue.len());
let restored = self.restore_queue.clone();
@@ -3196,3 +3242,56 @@ pub(crate) unsafe extern "C" fn handle_panning_animation_tick(data: *mut std::ff
}
0
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ #[allow(invalid_value)]
+ fn test_last_window_state_matching() {
+ let mut wm = unsafe { std::mem::MaybeUninit::<WindowManager>::zeroed().assume_init() };
+ unsafe {
+ std::ptr::write(&mut wm.last_window_states, Vec::new());
+ }
+
+ wm.last_window_states.push(SavedWindowState {
+ app_id: "test-app".to_string(),
+ title: "My App Window".to_string(),
+ tiling_mode: crate::tiling::TilingMode::Floating,
+ minimized: false,
+ virtual_x: 100.0,
+ virtual_y: 200.0,
+ scale: 1.0,
+ width: 800,
+ height: 600,
+ cmdline: "test-app".to_string(),
+ focused: false,
+ });
+
+ unsafe {
+ // Test exact match
+ let matched = wm.match_last_window_state("test-app", "My App Window");
+ assert!(matched.is_some());
+ let m = matched.unwrap();
+ assert_eq!(m.app_id, "test-app");
+ assert_eq!(m.virtual_x, 100.0);
+ assert_eq!(m.virtual_y, 200.0);
+
+ // Test fuzzy title match
+ let matched_fuzzy = wm.match_last_window_state("test-app", "My App Window*");
+ assert!(matched_fuzzy.is_some());
+
+ // Test app_id only match
+ let matched_appid = wm.match_last_window_state("test-app", "Different Title");
+ assert!(matched_appid.is_some());
+ assert_eq!(matched_appid.unwrap().width, 800);
+
+ // Test no match
+ let no_match = wm.match_last_window_state("other-app", "My App Window");
+ assert!(no_match.is_none());
+ }
+
+ std::mem::forget(wm);
+ }
+}