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

commit7f4718fe4547848b9f7b322c574b799111464664
parentd59fd6dce6
authorLucas Galante <[email protected]>
date2026-07-01 20:05
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);
+    }
+}