From 4d1917e06da12f7249b8ae90241908e14e501406 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 15:50:09 +0000 Subject: [PATCH 1/3] fix: Ensure proper fullscreen rendering without borders - Exclude true fullscreen windows from tiling calculations in `Client::is_tiled`. - Add `apply_fullscreen` helper in `arrange_monitor` to resize fullscreen windows to the entire monitor. - Update `apply_border_widths` to correctly strip borders from true fullscreen windows. - In `restack`, push true fullscreen windows to a new `fullscreen_stack` positioned above `bar_win` and `floating_stack`, guaranteeing they obscure everything else on screen. Co-authored-by: paperbenni <15818888+paperbenni@users.noreply.github.com> --- plan.md | 444 +++++++++++++++++++++++++++++++++++++++++++++ run_test.sh | 2 + test_fullscreen.rs | 0 3 files changed, 446 insertions(+) create mode 100644 plan.md create mode 100755 run_test.sh create mode 100644 test_fullscreen.rs diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..1bcbca3a --- /dev/null +++ b/plan.md @@ -0,0 +1,444 @@ +1. **Analyze `apply_border_widths`:** The current `apply_border_widths` function strips the border (`strip_border = true`) only if the client is *not* floating and *not* fullscreen (`!info.is_floating && !info.is_fullscreen`). Wait, if a client *is* fullscreen, `strip_border` becomes `false`? That means it gets a border! This is definitely wrong. Fullscreen windows should *never* have a border. + +Let's look at `strip_border` again: +```rust +let strip_border = !info.is_floating + && !info.is_fullscreen + && ((clientcount == 1 && is_tiling) || is_monocle); +``` +If `info.is_fullscreen` is `true`, `strip_border` is `false`, so it returns `info.old_border_width`. We should change this so `strip_border` is `true` if `info.is_fullscreen` is true, or if the layout conditions match. + +Let's change it to: +```rust +let strip_border = info.is_fullscreen + || (!info.is_floating && ((clientcount == 1 && is_tiling) || is_monocle)); +``` + +2. **Analyze `arrange_monitor`:** +```rust +pub fn arrange_monitor(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { + let clientcount = { + let m = ctx.g().monitor(monitor_id).expect("invalid monitor"); + m.tiled_client_count(&*ctx.g().clients) as u32 + }; + + if let Some(m) = ctx.g_mut().monitor_mut(monitor_id) { + m.clientcount = clientcount; + } + + apply_border_widths(ctx, monitor_id); + run_layout(ctx, monitor_id); + place_overlay(ctx, monitor_id); +} +``` + +Wait, `run_layout` runs the layout algorithm (like `algo::tile`). `run_layout` doesn't handle fullscreen windows; it leaves them where they are or modifies them. But `fullscreen_request` in Wayland handlers changes `client.is_fullscreen` and `mon.fullscreen`. The layout doesn't automatically resize the fullscreen window to the full monitor size on Wayland? + +Let's look at `WaylandState::fullscreen_request`. +```rust + fn fullscreen_request( + &mut self, + surface: ToplevelSurface, + mut _output: Option, + ) { + if let Some(win) = self.window_id_for_toplevel(&surface) { + if let Some(g) = self.globals_mut() { + if let Some(client) = g.clients.get_mut(&win) { + client.is_fullscreen = true; + } + g.space_dirty = true; + g.layout_dirty = true; + if let Some(mon) = g.selected_monitor_mut_opt() { + mon.fullscreen = Some(win); + } + } + } + surface.with_pending_state(|state| { + state.states.set(smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::Fullscreen); + }); + surface.send_configure(); + } +``` +If it's fullscreen, it gets `is_fullscreen = true`, and we flag `layout_dirty = true`. + +When `arrange` runs: +```rust +pub fn arrange(ctx: &mut WmCtx<'_>, monitor_id: Option) { +... + if let Some(id) = monitor_id { + // First pass: show/hide stack + crate::client::show_hide(ctx); + // Second pass: arrange and restack + arrange_monitor(ctx, id); + restack(ctx, id); + } +... +``` + +If we look at `apply_border_widths`, it will handle border width. But what about resizing to fullscreen geometry? +In Wayland, the window needs to be resized to the `monitor_rect` (not just `work_rect` which excludes the bar). + +Let's check `run_layout` -> `layout.arrange(ctx, &mut m)`. If a window is fullscreen, it should be resized to the monitor rect and raised to top. +But `run_layout` only applies the current layout to *tiled* and *floating* windows. + +Let's check `place_overlay` and what happens to fullscreen windows. Wait, we don't have a specific step to apply fullscreen geometry in `arrange_monitor`! +We need to add a step to position and resize fullscreen windows correctly. + +Let's see if we can do this in `arrange_monitor`: +```rust +pub fn arrange_monitor(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { + let clientcount = { + let m = ctx.g().monitor(monitor_id).expect("invalid monitor"); + m.tiled_client_count(&*ctx.g().clients) as u32 + }; + + if let Some(m) = ctx.g_mut().monitor_mut(monitor_id) { + m.clientcount = clientcount; + } + + apply_border_widths(ctx, monitor_id); + run_layout(ctx, monitor_id); + apply_fullscreen(ctx, monitor_id); // <--- Add this! + place_overlay(ctx, monitor_id); +} + +fn apply_fullscreen(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { + // get fullscreen windows for this monitor and resize them + let mon = match ctx.g().monitor(monitor_id) { + Some(m) => m, + None => return, + }; + let mon_rect = mon.monitor_rect; + let fullscreen_windows: Vec<_> = mon.clients.iter().copied().filter(|&win| { + ctx.client(win).is_some_and(|c| c.is_fullscreen) + }).collect(); + + for win in fullscreen_windows { + ctx.resize_client(win, mon_rect); + } +} +``` + +Wait, if we do this, it applies to both X11 and Wayland. Does X11 handle fullscreen differently? +In `set_fullscreen_x11`: +```rust + let mon_rect = ctx_x11 + .core + .g + .monitor(monitor_id) + .map(|m| m.monitor_rect) + .unwrap_or_default(); + + // Animate the expansion only for non-floating clients (floating + // windows just snap into place immediately). + if !is_floating { + animate_client_x11(ctx_x11, win, &mon_rect, 10, 0); + } + + // Position and raise the window. + let _ = ctx_x11.x11.conn.configure_window( + x11_win, + &ConfigureWindowAux::new() + .x(mon_rect.x) + .y(mon_rect.y) + .width(mon_rect.w as u32) + .height(mon_rect.h as u32), + ); +``` +X11 handles it directly in `set_fullscreen_x11`, but when the monitor changes or layout is re-arranged, `arrange_monitor` doesn't currently resize the fullscreen window. If we add `apply_fullscreen` in `arrange_monitor`, it will continually enforce the fullscreen geometry for any fullscreen window. This is good! It handles monitor resize correctly too. + +Also, `restack`: +```rust + let mut tiled_stack = Vec::new(); + let mut floating_stack = Vec::new(); + let mut fullscreen_stack = Vec::new(); // should be on top of floating? Or does Wayland just use `restack`? +``` +Let's see `restack`: +```rust + let mut stack = tiled_stack; + stack.push(bar_win); + stack.extend(floating_stack); + ctx.restack(&stack); +``` +Wait, if a window is fullscreen, `info.is_floating` is currently set to `true` in `set_fullscreen_x11`. So it goes into `floating_stack`. +If we have a fullscreen window, it should be at the very top. Wait, if `set_fullscreen_x11` sets `is_floating = true`, what does Wayland do? `WaylandState::fullscreen_request` does *not* set `is_floating = true`. + +Let's check `is_tiled`: +```rust + pub fn is_tiled(&self, selected_tags: u32) -> bool { + !self.is_floating && self.is_visible_on_tags(selected_tags) && !self.is_hidden + } +``` +In Wayland, if `fullscreen_request` doesn't set `is_floating = true`, then `is_tiled` will be `true`! So the window will still be laid out by the layout algorithm in `run_layout`! +Ah! This is the core issue! +`run_layout` runs, say, `tile` algorithm, which will resize the fullscreen window to a tiled position. +Then if we add `apply_fullscreen`, it will resize it to fullscreen. But it was already tiled, so maybe it flickers or fights? +Actually, `tile` only considers `tiled` windows: +```rust + let tiled = m.collect_tiled(&ctx.g.clients); +``` +If a window is fullscreen, it shouldn't be considered "tiled" for layout purposes, OR `collect_tiled` should ignore fullscreen windows. + +Let's look at `collect_tiled`: +```rust + pub fn collect_tiled(&self, clients: &HashMap) -> Vec { + let selected_tags = self.selected_tags(); + self.clients + .iter() + .filter_map(|&w| clients.get(&w)) + .filter(|c| c.is_tiled(selected_tags)) + .cloned() + .collect() + } +``` +If we modify `is_tiled` to also require `!self.is_fullscreen`: +```rust + pub fn is_tiled(&self, selected_tags: u32) -> bool { + !self.is_floating && !self.is_fullscreen && self.is_visible_on_tags(selected_tags) && !self.is_hidden + } +``` +But wait, if we do this, it won't be tiled, but what stack does it go into in `restack`? +```rust + let mut tiled_stack = Vec::new(); + let mut floating_stack = Vec::new(); + let mut fullscreen_stack = Vec::new(); + if let Some(m) = ctx.g().monitor(monitor_id) { + for &win in &m.stack { + if let Some(c) = ctx.client(win) { + if c.is_visible_on_tags(selected_tags) { + if c.is_fullscreen { + fullscreen_stack.push(win); + } else if c.is_floating { + floating_stack.push(win); + } else { + tiled_stack.push(win); + } + } + } + } + } +``` +If we update `restack` to handle `fullscreen_stack`, and push it *after* `floating_stack`, then fullscreen windows will be on top of everything! Including floating windows and the bar. The bar is `bar_win`. +```rust + let mut stack = tiled_stack; + stack.push(bar_win); + stack.extend(floating_stack); + stack.extend(fullscreen_stack); + ctx.restack(&stack); +``` +This guarantees the fullscreen window is on top of the bar and other windows. + +3. **Check `set_fullscreen_x11`:** +```rust + // Mark as floating so the layout engine leaves it alone. + if let Some(c) = ctx_x11.core.g.clients.get_mut(&win) { + c.is_floating = true; + } +``` +If we fix `is_tiled` to exclude fullscreen windows, X11 doesn't *need* to set `is_floating = true`. But wait, if X11 un-fullscreens it: +```rust + if let Some(c) = ctx_x11.core.g.clients.get_mut(&win) { + c.is_fullscreen = false; + c.is_floating = c.oldstate != 0; + } +``` +It restores `is_floating` from `oldstate`. +If we change `is_tiled` to check `!self.is_true_fullscreen()`: +```rust + pub fn is_tiled(&self, selected_tags: u32) -> bool { + !self.is_floating && !self.is_true_fullscreen() && self.is_visible_on_tags(selected_tags) && !self.is_hidden + } +``` +Wait, `is_true_fullscreen()` returns `self.is_fullscreen && !self.isfakefullscreen`. +Fake fullscreen means it's fullscreen to the application, but tiled to the WM. So it *should* be tiled if it's fake fullscreen! +Yes! `!self.is_true_fullscreen()` is exactly what we want. + +If we make these changes: +1. `src/types/client.rs`: +```rust + pub fn is_tiled(&self, selected_tags: u32) -> bool { + !self.is_floating && !self.is_true_fullscreen() && self.is_visible_on_tags(selected_tags) && !self.is_hidden + } +``` +2. `src/layouts/manager.rs`: +In `apply_border_widths`: +```rust + let strip_border = info.is_true_fullscreen() || + (!info.is_floating + && !info.is_fullscreen + && ((clientcount == 1 && is_tiling) || is_monocle)); +``` +Actually, if `info.is_true_fullscreen()` is true, strip border. Otherwise, use the old condition. Wait, fake fullscreen windows should keep their border! So the condition should be: +```rust + let strip_border = info.is_true_fullscreen() + || (!info.is_floating + && !info.is_true_fullscreen() + && ((clientcount == 1 && is_tiling) || is_monocle)); +``` +In `arrange_monitor`, add `apply_fullscreen`: +```rust +fn apply_fullscreen(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { + let mon = match ctx.g().monitor(monitor_id) { + Some(m) => m, + None => return, + }; + let mon_rect = mon.monitor_rect; + let fullscreen_windows: Vec<_> = mon.clients.iter().copied().filter(|&win| { + ctx.client(win).is_some_and(|c| c.is_true_fullscreen()) + }).collect(); + + for win in fullscreen_windows { + ctx.resize_client(win, mon_rect); + } +} +``` +Call it in `arrange_monitor`: +```rust + apply_border_widths(ctx, monitor_id); + run_layout(ctx, monitor_id); + apply_fullscreen(ctx, monitor_id); + place_overlay(ctx, monitor_id); +``` +In `restack`: +```rust + let mut tiled_stack = Vec::new(); + let mut floating_stack = Vec::new(); + let mut fullscreen_stack = Vec::new(); + if let Some(m) = ctx.g().monitor(monitor_id) { + for &win in &m.stack { + if let Some(c) = ctx.client(win) { + if c.is_visible_on_tags(selected_tags) { + if c.is_true_fullscreen() { + fullscreen_stack.push(win); + } else if c.is_floating { + floating_stack.push(win); + } else { + tiled_stack.push(win); + } + } + } + } + } +``` +And replace the `selected_window` logic to handle if it's in `fullscreen_stack`: +Wait, the `selected_window` logic in `restack` currently only raises it within its own layer (`floating_stack` or `tiled_stack`). +```rust + if let Some(idx) = fullscreen_stack + .iter() + .position(|&win| win == selected_window) + { + let selected = fullscreen_stack.remove(idx); + fullscreen_stack.push(selected); + } else if let Some(idx) = floating_stack + .iter() + .position(|&win| win == selected_window) + { + let selected = floating_stack.remove(idx); + floating_stack.push(selected); + } else { + // ... tiled stack handling ... +``` +And then: +```rust + let mut stack = tiled_stack; + stack.push(bar_win); + stack.extend(floating_stack); + stack.extend(fullscreen_stack); + ctx.restack(&stack); +``` + +Let's double check `WaylandState::fullscreen_request`. +Does it handle unfullscreen? +```rust + fn unfullscreen_request(&mut self, surface: ToplevelSurface) { + if let Some(win) = self.window_id_for_toplevel(&surface) { + if let Some(g) = self.globals_mut() { + if let Some(client) = g.clients.get_mut(&win) { + client.is_fullscreen = false; + } + g.space_dirty = true; + g.layout_dirty = true; + if let Some(mon) = g.selected_monitor_mut_opt() { + if mon.fullscreen == Some(win) { + mon.fullscreen = None; + } + } + } + } +``` +Wait, what if `client.is_fullscreen` is toggled off? Does the layout engine automatically resize it back to the tiled or floating geometry? +Yes, on the next `arrange()` pass, `is_true_fullscreen()` will be false. So `apply_fullscreen` will skip it. `run_layout` will pick it up (if tiled), or `float_left` will handle it (if floating). Wait, floating windows need to be resized back to their floating geometry! `float_left` doesn't resize floating windows, it only applies snap position! +If a Wayland window was floating, and was fullscreened, it gets resized to `mon_rect`. When it is unfullscreened, who resizes it back to its floating geometry? +Ah! `save_floating` saves `c.float_geo = c.geo`. But wait, if it was fullscreened, `WaylandState::fullscreen_request` DOES NOT save the floating geometry or border width. +If a floating window goes fullscreen, and then unfullscreen, we need to restore its geometry. + +If we look at `set_fullscreen_x11`: +```rust + ctx_x11.core.g.clients.save_border_width(win); + + if !is_fake_fs { + // Remove the border. + if let Some(c) = ctx_x11.core.g.clients.get_mut(&win) { + c.border_width = 0; + } +... + } else if !fullscreen && is_fs { +... + ctx_x11.core.g.clients.restore_border_width(win); + + if !is_fake_fs { + // Snap back to the geometry that was stored before going fullscreen. + let mut wmctx = WmCtx::X11(ctx_x11.reborrow()); + wmctx.resize_client(win, old_geo); + arrange(&mut wmctx, Some(monitor_id)); + } +``` + +We need to make Wayland do the same thing! Or at least, handle the state transitions properly. +Wait, can we centralize fullscreen toggling in a helper function? We have `set_fullscreen_x11` which is X11 specific. +For Wayland, the compositor calls `fullscreen_request`. +```rust + fn fullscreen_request( + &mut self, + surface: ToplevelSurface, + mut _output: Option, + ) { + if let Some(win) = self.window_id_for_toplevel(&surface) { + if let Some(g) = self.globals_mut() { + if let Some(client) = g.clients.get_mut(&win) { + if !client.is_fullscreen { + client.is_fullscreen = true; + g.clients.save_border_width(win); + client.border_width = 0; + client.old_geo = client.geo; // save current geometry + } + } + g.space_dirty = true; + g.layout_dirty = true; + if let Some(mon) = g.selected_monitor_mut_opt() { + mon.fullscreen = Some(win); + } + } + } +``` +And `unfullscreen_request`: +```rust + fn unfullscreen_request(&mut self, surface: ToplevelSurface) { + if let Some(win) = self.window_id_for_toplevel(&surface) { + if let Some(g) = self.globals_mut() { + if let Some(client) = g.clients.get_mut(&win) { + if client.is_fullscreen { + client.is_fullscreen = false; + g.clients.restore_border_width(win); + // restore geometry for Wayland + client.geo = client.old_geo; // but we need to tell compositor to resize + } + } + g.space_dirty = true; + g.layout_dirty = true; +... +``` +Wait, if we just set `client.geo = client.old_geo`, the compositor will sync space from globals, but does it send a configure request? `WaylandState::sync_space_from_globals` does `self.resize_window` internally or similar. +Wait, `sync_space_from_globals` maps it using `g.clients.get(&marker.id).geo`. + +Let's test this out. diff --git a/run_test.sh b/run_test.sh new file mode 100755 index 00000000..c179d0f6 --- /dev/null +++ b/run_test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cargo test diff --git a/test_fullscreen.rs b/test_fullscreen.rs new file mode 100644 index 00000000..e69de29b From 9b172d748458e52398652b4857c8cabffceab9a5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:05:59 +0000 Subject: [PATCH 2/3] fix: Ensure proper fullscreen rendering without borders - Exclude true fullscreen windows from tiling calculations in `Client::is_tiled`. - Add `apply_fullscreen` helper in `arrange_monitor` to resize fullscreen windows to the entire monitor. - Update `apply_border_widths` to correctly strip borders from true fullscreen windows. - In `restack`, push true fullscreen windows to a new `fullscreen_stack` positioned above `bar_win` and `floating_stack`, guaranteeing they obscure everything else on screen. Co-authored-by: paperbenni <15818888+paperbenni@users.noreply.github.com> --- plan.md | 444 ----------------------------------------- run_test.sh | 2 - src/layouts/manager.rs | 43 +++- src/types/client.rs | 3 +- test_fullscreen.rs | 0 5 files changed, 40 insertions(+), 452 deletions(-) delete mode 100644 plan.md delete mode 100755 run_test.sh delete mode 100644 test_fullscreen.rs diff --git a/plan.md b/plan.md deleted file mode 100644 index 1bcbca3a..00000000 --- a/plan.md +++ /dev/null @@ -1,444 +0,0 @@ -1. **Analyze `apply_border_widths`:** The current `apply_border_widths` function strips the border (`strip_border = true`) only if the client is *not* floating and *not* fullscreen (`!info.is_floating && !info.is_fullscreen`). Wait, if a client *is* fullscreen, `strip_border` becomes `false`? That means it gets a border! This is definitely wrong. Fullscreen windows should *never* have a border. - -Let's look at `strip_border` again: -```rust -let strip_border = !info.is_floating - && !info.is_fullscreen - && ((clientcount == 1 && is_tiling) || is_monocle); -``` -If `info.is_fullscreen` is `true`, `strip_border` is `false`, so it returns `info.old_border_width`. We should change this so `strip_border` is `true` if `info.is_fullscreen` is true, or if the layout conditions match. - -Let's change it to: -```rust -let strip_border = info.is_fullscreen - || (!info.is_floating && ((clientcount == 1 && is_tiling) || is_monocle)); -``` - -2. **Analyze `arrange_monitor`:** -```rust -pub fn arrange_monitor(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { - let clientcount = { - let m = ctx.g().monitor(monitor_id).expect("invalid monitor"); - m.tiled_client_count(&*ctx.g().clients) as u32 - }; - - if let Some(m) = ctx.g_mut().monitor_mut(monitor_id) { - m.clientcount = clientcount; - } - - apply_border_widths(ctx, monitor_id); - run_layout(ctx, monitor_id); - place_overlay(ctx, monitor_id); -} -``` - -Wait, `run_layout` runs the layout algorithm (like `algo::tile`). `run_layout` doesn't handle fullscreen windows; it leaves them where they are or modifies them. But `fullscreen_request` in Wayland handlers changes `client.is_fullscreen` and `mon.fullscreen`. The layout doesn't automatically resize the fullscreen window to the full monitor size on Wayland? - -Let's look at `WaylandState::fullscreen_request`. -```rust - fn fullscreen_request( - &mut self, - surface: ToplevelSurface, - mut _output: Option, - ) { - if let Some(win) = self.window_id_for_toplevel(&surface) { - if let Some(g) = self.globals_mut() { - if let Some(client) = g.clients.get_mut(&win) { - client.is_fullscreen = true; - } - g.space_dirty = true; - g.layout_dirty = true; - if let Some(mon) = g.selected_monitor_mut_opt() { - mon.fullscreen = Some(win); - } - } - } - surface.with_pending_state(|state| { - state.states.set(smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State::Fullscreen); - }); - surface.send_configure(); - } -``` -If it's fullscreen, it gets `is_fullscreen = true`, and we flag `layout_dirty = true`. - -When `arrange` runs: -```rust -pub fn arrange(ctx: &mut WmCtx<'_>, monitor_id: Option) { -... - if let Some(id) = monitor_id { - // First pass: show/hide stack - crate::client::show_hide(ctx); - // Second pass: arrange and restack - arrange_monitor(ctx, id); - restack(ctx, id); - } -... -``` - -If we look at `apply_border_widths`, it will handle border width. But what about resizing to fullscreen geometry? -In Wayland, the window needs to be resized to the `monitor_rect` (not just `work_rect` which excludes the bar). - -Let's check `run_layout` -> `layout.arrange(ctx, &mut m)`. If a window is fullscreen, it should be resized to the monitor rect and raised to top. -But `run_layout` only applies the current layout to *tiled* and *floating* windows. - -Let's check `place_overlay` and what happens to fullscreen windows. Wait, we don't have a specific step to apply fullscreen geometry in `arrange_monitor`! -We need to add a step to position and resize fullscreen windows correctly. - -Let's see if we can do this in `arrange_monitor`: -```rust -pub fn arrange_monitor(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { - let clientcount = { - let m = ctx.g().monitor(monitor_id).expect("invalid monitor"); - m.tiled_client_count(&*ctx.g().clients) as u32 - }; - - if let Some(m) = ctx.g_mut().monitor_mut(monitor_id) { - m.clientcount = clientcount; - } - - apply_border_widths(ctx, monitor_id); - run_layout(ctx, monitor_id); - apply_fullscreen(ctx, monitor_id); // <--- Add this! - place_overlay(ctx, monitor_id); -} - -fn apply_fullscreen(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { - // get fullscreen windows for this monitor and resize them - let mon = match ctx.g().monitor(monitor_id) { - Some(m) => m, - None => return, - }; - let mon_rect = mon.monitor_rect; - let fullscreen_windows: Vec<_> = mon.clients.iter().copied().filter(|&win| { - ctx.client(win).is_some_and(|c| c.is_fullscreen) - }).collect(); - - for win in fullscreen_windows { - ctx.resize_client(win, mon_rect); - } -} -``` - -Wait, if we do this, it applies to both X11 and Wayland. Does X11 handle fullscreen differently? -In `set_fullscreen_x11`: -```rust - let mon_rect = ctx_x11 - .core - .g - .monitor(monitor_id) - .map(|m| m.monitor_rect) - .unwrap_or_default(); - - // Animate the expansion only for non-floating clients (floating - // windows just snap into place immediately). - if !is_floating { - animate_client_x11(ctx_x11, win, &mon_rect, 10, 0); - } - - // Position and raise the window. - let _ = ctx_x11.x11.conn.configure_window( - x11_win, - &ConfigureWindowAux::new() - .x(mon_rect.x) - .y(mon_rect.y) - .width(mon_rect.w as u32) - .height(mon_rect.h as u32), - ); -``` -X11 handles it directly in `set_fullscreen_x11`, but when the monitor changes or layout is re-arranged, `arrange_monitor` doesn't currently resize the fullscreen window. If we add `apply_fullscreen` in `arrange_monitor`, it will continually enforce the fullscreen geometry for any fullscreen window. This is good! It handles monitor resize correctly too. - -Also, `restack`: -```rust - let mut tiled_stack = Vec::new(); - let mut floating_stack = Vec::new(); - let mut fullscreen_stack = Vec::new(); // should be on top of floating? Or does Wayland just use `restack`? -``` -Let's see `restack`: -```rust - let mut stack = tiled_stack; - stack.push(bar_win); - stack.extend(floating_stack); - ctx.restack(&stack); -``` -Wait, if a window is fullscreen, `info.is_floating` is currently set to `true` in `set_fullscreen_x11`. So it goes into `floating_stack`. -If we have a fullscreen window, it should be at the very top. Wait, if `set_fullscreen_x11` sets `is_floating = true`, what does Wayland do? `WaylandState::fullscreen_request` does *not* set `is_floating = true`. - -Let's check `is_tiled`: -```rust - pub fn is_tiled(&self, selected_tags: u32) -> bool { - !self.is_floating && self.is_visible_on_tags(selected_tags) && !self.is_hidden - } -``` -In Wayland, if `fullscreen_request` doesn't set `is_floating = true`, then `is_tiled` will be `true`! So the window will still be laid out by the layout algorithm in `run_layout`! -Ah! This is the core issue! -`run_layout` runs, say, `tile` algorithm, which will resize the fullscreen window to a tiled position. -Then if we add `apply_fullscreen`, it will resize it to fullscreen. But it was already tiled, so maybe it flickers or fights? -Actually, `tile` only considers `tiled` windows: -```rust - let tiled = m.collect_tiled(&ctx.g.clients); -``` -If a window is fullscreen, it shouldn't be considered "tiled" for layout purposes, OR `collect_tiled` should ignore fullscreen windows. - -Let's look at `collect_tiled`: -```rust - pub fn collect_tiled(&self, clients: &HashMap) -> Vec { - let selected_tags = self.selected_tags(); - self.clients - .iter() - .filter_map(|&w| clients.get(&w)) - .filter(|c| c.is_tiled(selected_tags)) - .cloned() - .collect() - } -``` -If we modify `is_tiled` to also require `!self.is_fullscreen`: -```rust - pub fn is_tiled(&self, selected_tags: u32) -> bool { - !self.is_floating && !self.is_fullscreen && self.is_visible_on_tags(selected_tags) && !self.is_hidden - } -``` -But wait, if we do this, it won't be tiled, but what stack does it go into in `restack`? -```rust - let mut tiled_stack = Vec::new(); - let mut floating_stack = Vec::new(); - let mut fullscreen_stack = Vec::new(); - if let Some(m) = ctx.g().monitor(monitor_id) { - for &win in &m.stack { - if let Some(c) = ctx.client(win) { - if c.is_visible_on_tags(selected_tags) { - if c.is_fullscreen { - fullscreen_stack.push(win); - } else if c.is_floating { - floating_stack.push(win); - } else { - tiled_stack.push(win); - } - } - } - } - } -``` -If we update `restack` to handle `fullscreen_stack`, and push it *after* `floating_stack`, then fullscreen windows will be on top of everything! Including floating windows and the bar. The bar is `bar_win`. -```rust - let mut stack = tiled_stack; - stack.push(bar_win); - stack.extend(floating_stack); - stack.extend(fullscreen_stack); - ctx.restack(&stack); -``` -This guarantees the fullscreen window is on top of the bar and other windows. - -3. **Check `set_fullscreen_x11`:** -```rust - // Mark as floating so the layout engine leaves it alone. - if let Some(c) = ctx_x11.core.g.clients.get_mut(&win) { - c.is_floating = true; - } -``` -If we fix `is_tiled` to exclude fullscreen windows, X11 doesn't *need* to set `is_floating = true`. But wait, if X11 un-fullscreens it: -```rust - if let Some(c) = ctx_x11.core.g.clients.get_mut(&win) { - c.is_fullscreen = false; - c.is_floating = c.oldstate != 0; - } -``` -It restores `is_floating` from `oldstate`. -If we change `is_tiled` to check `!self.is_true_fullscreen()`: -```rust - pub fn is_tiled(&self, selected_tags: u32) -> bool { - !self.is_floating && !self.is_true_fullscreen() && self.is_visible_on_tags(selected_tags) && !self.is_hidden - } -``` -Wait, `is_true_fullscreen()` returns `self.is_fullscreen && !self.isfakefullscreen`. -Fake fullscreen means it's fullscreen to the application, but tiled to the WM. So it *should* be tiled if it's fake fullscreen! -Yes! `!self.is_true_fullscreen()` is exactly what we want. - -If we make these changes: -1. `src/types/client.rs`: -```rust - pub fn is_tiled(&self, selected_tags: u32) -> bool { - !self.is_floating && !self.is_true_fullscreen() && self.is_visible_on_tags(selected_tags) && !self.is_hidden - } -``` -2. `src/layouts/manager.rs`: -In `apply_border_widths`: -```rust - let strip_border = info.is_true_fullscreen() || - (!info.is_floating - && !info.is_fullscreen - && ((clientcount == 1 && is_tiling) || is_monocle)); -``` -Actually, if `info.is_true_fullscreen()` is true, strip border. Otherwise, use the old condition. Wait, fake fullscreen windows should keep their border! So the condition should be: -```rust - let strip_border = info.is_true_fullscreen() - || (!info.is_floating - && !info.is_true_fullscreen() - && ((clientcount == 1 && is_tiling) || is_monocle)); -``` -In `arrange_monitor`, add `apply_fullscreen`: -```rust -fn apply_fullscreen(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { - let mon = match ctx.g().monitor(monitor_id) { - Some(m) => m, - None => return, - }; - let mon_rect = mon.monitor_rect; - let fullscreen_windows: Vec<_> = mon.clients.iter().copied().filter(|&win| { - ctx.client(win).is_some_and(|c| c.is_true_fullscreen()) - }).collect(); - - for win in fullscreen_windows { - ctx.resize_client(win, mon_rect); - } -} -``` -Call it in `arrange_monitor`: -```rust - apply_border_widths(ctx, monitor_id); - run_layout(ctx, monitor_id); - apply_fullscreen(ctx, monitor_id); - place_overlay(ctx, monitor_id); -``` -In `restack`: -```rust - let mut tiled_stack = Vec::new(); - let mut floating_stack = Vec::new(); - let mut fullscreen_stack = Vec::new(); - if let Some(m) = ctx.g().monitor(monitor_id) { - for &win in &m.stack { - if let Some(c) = ctx.client(win) { - if c.is_visible_on_tags(selected_tags) { - if c.is_true_fullscreen() { - fullscreen_stack.push(win); - } else if c.is_floating { - floating_stack.push(win); - } else { - tiled_stack.push(win); - } - } - } - } - } -``` -And replace the `selected_window` logic to handle if it's in `fullscreen_stack`: -Wait, the `selected_window` logic in `restack` currently only raises it within its own layer (`floating_stack` or `tiled_stack`). -```rust - if let Some(idx) = fullscreen_stack - .iter() - .position(|&win| win == selected_window) - { - let selected = fullscreen_stack.remove(idx); - fullscreen_stack.push(selected); - } else if let Some(idx) = floating_stack - .iter() - .position(|&win| win == selected_window) - { - let selected = floating_stack.remove(idx); - floating_stack.push(selected); - } else { - // ... tiled stack handling ... -``` -And then: -```rust - let mut stack = tiled_stack; - stack.push(bar_win); - stack.extend(floating_stack); - stack.extend(fullscreen_stack); - ctx.restack(&stack); -``` - -Let's double check `WaylandState::fullscreen_request`. -Does it handle unfullscreen? -```rust - fn unfullscreen_request(&mut self, surface: ToplevelSurface) { - if let Some(win) = self.window_id_for_toplevel(&surface) { - if let Some(g) = self.globals_mut() { - if let Some(client) = g.clients.get_mut(&win) { - client.is_fullscreen = false; - } - g.space_dirty = true; - g.layout_dirty = true; - if let Some(mon) = g.selected_monitor_mut_opt() { - if mon.fullscreen == Some(win) { - mon.fullscreen = None; - } - } - } - } -``` -Wait, what if `client.is_fullscreen` is toggled off? Does the layout engine automatically resize it back to the tiled or floating geometry? -Yes, on the next `arrange()` pass, `is_true_fullscreen()` will be false. So `apply_fullscreen` will skip it. `run_layout` will pick it up (if tiled), or `float_left` will handle it (if floating). Wait, floating windows need to be resized back to their floating geometry! `float_left` doesn't resize floating windows, it only applies snap position! -If a Wayland window was floating, and was fullscreened, it gets resized to `mon_rect`. When it is unfullscreened, who resizes it back to its floating geometry? -Ah! `save_floating` saves `c.float_geo = c.geo`. But wait, if it was fullscreened, `WaylandState::fullscreen_request` DOES NOT save the floating geometry or border width. -If a floating window goes fullscreen, and then unfullscreen, we need to restore its geometry. - -If we look at `set_fullscreen_x11`: -```rust - ctx_x11.core.g.clients.save_border_width(win); - - if !is_fake_fs { - // Remove the border. - if let Some(c) = ctx_x11.core.g.clients.get_mut(&win) { - c.border_width = 0; - } -... - } else if !fullscreen && is_fs { -... - ctx_x11.core.g.clients.restore_border_width(win); - - if !is_fake_fs { - // Snap back to the geometry that was stored before going fullscreen. - let mut wmctx = WmCtx::X11(ctx_x11.reborrow()); - wmctx.resize_client(win, old_geo); - arrange(&mut wmctx, Some(monitor_id)); - } -``` - -We need to make Wayland do the same thing! Or at least, handle the state transitions properly. -Wait, can we centralize fullscreen toggling in a helper function? We have `set_fullscreen_x11` which is X11 specific. -For Wayland, the compositor calls `fullscreen_request`. -```rust - fn fullscreen_request( - &mut self, - surface: ToplevelSurface, - mut _output: Option, - ) { - if let Some(win) = self.window_id_for_toplevel(&surface) { - if let Some(g) = self.globals_mut() { - if let Some(client) = g.clients.get_mut(&win) { - if !client.is_fullscreen { - client.is_fullscreen = true; - g.clients.save_border_width(win); - client.border_width = 0; - client.old_geo = client.geo; // save current geometry - } - } - g.space_dirty = true; - g.layout_dirty = true; - if let Some(mon) = g.selected_monitor_mut_opt() { - mon.fullscreen = Some(win); - } - } - } -``` -And `unfullscreen_request`: -```rust - fn unfullscreen_request(&mut self, surface: ToplevelSurface) { - if let Some(win) = self.window_id_for_toplevel(&surface) { - if let Some(g) = self.globals_mut() { - if let Some(client) = g.clients.get_mut(&win) { - if client.is_fullscreen { - client.is_fullscreen = false; - g.clients.restore_border_width(win); - // restore geometry for Wayland - client.geo = client.old_geo; // but we need to tell compositor to resize - } - } - g.space_dirty = true; - g.layout_dirty = true; -... -``` -Wait, if we just set `client.geo = client.old_geo`, the compositor will sync space from globals, but does it send a configure request? `WaylandState::sync_space_from_globals` does `self.resize_window` internally or similar. -Wait, `sync_space_from_globals` maps it using `g.clients.get(&marker.id).geo`. - -Let's test this out. diff --git a/run_test.sh b/run_test.sh deleted file mode 100755 index c179d0f6..00000000 --- a/run_test.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -cargo test diff --git a/src/layouts/manager.rs b/src/layouts/manager.rs index 0f30aeb3..a0227400 100644 --- a/src/layouts/manager.rs +++ b/src/layouts/manager.rs @@ -43,9 +43,31 @@ pub fn arrange_monitor(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { apply_border_widths(ctx, monitor_id); run_layout(ctx, monitor_id); + apply_fullscreen(ctx, monitor_id); place_overlay(ctx, monitor_id); } +fn apply_fullscreen(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { + let (mon_rect, clients) = match ctx.g().monitor(monitor_id) { + Some(m) => (m.monitor_rect, m.clients.clone()), + None => return, + }; + + let selected_tags = ctx.g().monitor(monitor_id).unwrap().selected_tags(); + + let fullscreen_windows: Vec<_> = clients + .into_iter() + .filter(|&win| { + ctx.client(win) + .is_some_and(|c| c.is_true_fullscreen() && c.is_visible_on_tags(selected_tags)) + }) + .collect(); + + for win in fullscreen_windows { + ctx.resize_client(win, mon_rect); + } +} + fn apply_border_widths(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { let m = match ctx.g().monitor(monitor_id) { Some(m) => m, @@ -68,9 +90,10 @@ fn apply_border_widths(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { return None; } - let strip_border = !info.is_floating - && !info.is_fullscreen - && ((clientcount == 1 && is_tiling) || is_monocle); + let strip_border = info.is_true_fullscreen() + || (!info.is_floating + && !info.is_fullscreen + && ((clientcount == 1 && is_tiling) || is_monocle)); let new_border = if strip_border { 0 @@ -152,11 +175,14 @@ pub fn restack(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { let mut tiled_stack = Vec::new(); let mut floating_stack = Vec::new(); + let mut fullscreen_stack = Vec::new(); if let Some(m) = ctx.g().monitor(monitor_id) { for &win in &m.stack { if let Some(c) = ctx.client(win) { if c.is_visible_on_tags(selected_tags) { - if c.is_floating { + if c.is_true_fullscreen() { + fullscreen_stack.push(win); + } else if c.is_floating { floating_stack.push(win); } else { tiled_stack.push(win); @@ -166,7 +192,13 @@ pub fn restack(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { } } - if let Some(idx) = floating_stack + if let Some(idx) = fullscreen_stack + .iter() + .position(|&win| win == selected_window) + { + let selected = fullscreen_stack.remove(idx); + fullscreen_stack.push(selected); + } else if let Some(idx) = floating_stack .iter() .position(|&win| win == selected_window) { @@ -192,6 +224,7 @@ pub fn restack(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { let mut stack = tiled_stack; stack.push(bar_win); stack.extend(floating_stack); + stack.extend(fullscreen_stack); ctx.restack(&stack); ctx.flush(); } diff --git a/src/types/client.rs b/src/types/client.rs index a30fa00e..d6c263fe 100644 --- a/src/types/client.rs +++ b/src/types/client.rs @@ -100,11 +100,12 @@ impl Client { /// /// Returns true if the client is: /// - Not floating + /// - Not in true fullscreen mode /// - Visible on the selected tags /// - Not hidden #[inline] pub fn is_tiled(&self, selected_tags: u32) -> bool { - !self.is_floating && self.is_visible_on_tags(selected_tags) && !self.is_hidden + !self.is_floating && !self.is_true_fullscreen() && self.is_visible_on_tags(selected_tags) && !self.is_hidden } /// Check if the client is in true fullscreen mode (not fake fullscreen). diff --git a/test_fullscreen.rs b/test_fullscreen.rs deleted file mode 100644 index e69de29b..00000000 From 246983d56cceeca624dd741e1e5c12118314d5b0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:08:24 +0000 Subject: [PATCH 3/3] fix: Ensure proper fullscreen rendering without borders - Exclude true fullscreen windows from tiling calculations in `Client::is_tiled`. - Add `apply_fullscreen` helper in `arrange_monitor` to resize fullscreen windows to the entire monitor. - Update `apply_border_widths` to correctly strip borders from true fullscreen windows. - In `restack`, push true fullscreen windows to a new `fullscreen_stack` positioned above `bar_win` and `floating_stack`, guaranteeing they obscure everything else on screen. Co-authored-by: paperbenni <15818888+paperbenni@users.noreply.github.com> --- src/layouts/manager.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/layouts/manager.rs b/src/layouts/manager.rs index a0227400..4b7e4cee 100644 --- a/src/layouts/manager.rs +++ b/src/layouts/manager.rs @@ -48,13 +48,11 @@ pub fn arrange_monitor(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { } fn apply_fullscreen(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { - let (mon_rect, clients) = match ctx.g().monitor(monitor_id) { - Some(m) => (m.monitor_rect, m.clients.clone()), + let (mon_rect, clients, selected_tags) = match ctx.g().monitor(monitor_id) { + Some(m) => (m.monitor_rect, m.clients.clone(), m.selected_tags()), None => return, }; - let selected_tags = ctx.g().monitor(monitor_id).unwrap().selected_tags(); - let fullscreen_windows: Vec<_> = clients .into_iter() .filter(|&win| { @@ -218,9 +216,11 @@ pub fn restack(ctx: &mut WmCtx<'_>, monitor_id: MonitorId) { } } - // Final z-order: tiled clients, then the bar, then floating clients. + // Final z-order: tiled clients, then the bar, then floating clients, + // and finally fullscreen clients. // This keeps every floating window above tiled content while still - // keeping the selected window topmost within its own class. + // keeping the selected window topmost within its own class, and guarantees + // fullscreen windows sit above everything else. let mut stack = tiled_stack; stack.push(bar_win); stack.extend(floating_stack);