From b3de13e27ef79bc329baec8339cd0ea1b89439bb Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Tue, 29 Sep 2026 16:14:19 +0100 Subject: [PATCH] Keep fullscreen dialogs visible and preserve window focus --- compositor/src/stacking.rs | 26 +++++++-- compositor/src/state.rs | 90 +++++++++++++++++++++++++---- crates/ferese-core/src/workspace.rs | 48 ++++++++++++--- 3 files changed, 142 insertions(+), 22 deletions(-) diff --git a/compositor/src/stacking.rs b/compositor/src/stacking.rs index 952e0b8..d42bbcd 100644 --- a/compositor/src/stacking.rs +++ b/compositor/src/stacking.rs @@ -1,8 +1,15 @@ use ferese_layout::WindowId; use std::collections::HashMap; -pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool) -> u8 { - if fullscreen { +pub(crate) fn layer_priority( + floating: bool, + zooming: bool, + fullscreen: bool, + above_fullscreen: bool, +) -> u8 { + if above_fullscreen { + 4 + } else if fullscreen { 3 } else if floating { 2 @@ -90,9 +97,18 @@ mod tests { #[test] fn floating_windows_stay_above_tiled_and_maximized_windows() { - assert!(layer_priority(true, false, false) > layer_priority(false, true, false)); - assert!(layer_priority(false, true, false) > layer_priority(false, false, false)); - assert!(layer_priority(false, false, true) > layer_priority(true, false, false)); + assert!( + layer_priority(true, false, false, false) > layer_priority(false, true, false, false) + ); + assert!( + layer_priority(false, true, false, false) > layer_priority(false, false, false, false) + ); + assert!( + layer_priority(false, false, true, false) > layer_priority(true, false, false, false) + ); + assert!( + layer_priority(true, false, false, true) > layer_priority(false, false, true, false) + ); } #[test] diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 3ca51d8..38a9df0 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -128,6 +128,7 @@ pub struct Ferese { maximized_windows: HashSet, maximized_column_widths: HashMap, window_stack: crate::stacking::WindowStack, + floating_above_fullscreen: HashMap, natural_floating_pending: HashSet, pub(crate) window_borders: HashMap, pub(crate) window_dims: HashMap, @@ -347,6 +348,7 @@ impl Ferese { maximized_windows: HashSet::new(), maximized_column_widths: HashMap::new(), window_stack: crate::stacking::WindowStack::default(), + floating_above_fullscreen: HashMap::new(), natural_floating_pending: HashSet::new(), window_borders: HashMap::new(), window_dims: HashMap::new(), @@ -804,15 +806,27 @@ impl Ferese { self.inverse_presented_window_point(*id, position.x, position.y)?; let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); - window - .surface_under(source_point, WindowSurfaceType::ALL) - .map(|(surface, surface_location)| { - let surface_point = source_point - surface_location.to_f64(); - (surface, position - surface_point) - }) + let surface = window.surface_under(source_point, WindowSurfaceType::ALL); + let (surface, surface_location) = match surface { + Some(hit) => hit, + None if self.window_fills_visual_bounds(*id, workspace) => { + (window.toplevel()?.wl_surface().clone(), Point::from((0, 0))) + } + None => return None, + }; + let surface_point = source_point - surface_location.to_f64(); + Some((surface, position - surface_point)) }) } + fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool { + self.maximized_windows.contains(&id) + || self + .workspaces + .workspace(workspace) + .is_some_and(|workspace| workspace.fullscreen == Some(id)) + } + fn layer_surface_under( &self, position: Point, @@ -887,7 +901,13 @@ impl Ferese { self.inverse_presented_window_point(*id, position.x, position.y)?; let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); - window.surface_under(source_point, WindowSurfaceType::ALL)?; + if window + .surface_under(source_point, WindowSurfaceType::ALL) + .is_none() + && !self.window_fills_visual_bounds(*id, workspace) + { + return None; + } } Some(window.clone()) @@ -941,6 +961,17 @@ impl Ferese { floating, geometry.is_some_and(|geometry| geometry.is_zooming()), geometry.is_some_and(|geometry| geometry.is_fullscreen()), + floating + && id.is_some_and(|id| { + self.floating_above_fullscreen + .get(&id) + .is_some_and(|parent| { + self.workspaces + .workspace_for_window(id) + .and_then(|workspace| self.workspaces.workspace(workspace)) + .is_some_and(|workspace| workspace.fullscreen == Some(*parent)) + }) + }), ); ( priority, @@ -1027,7 +1058,8 @@ impl Ferese { let rect = client_size(&window) .map(|size| natural_floating_rect(bounds, size)) .unwrap_or_else(|| centered_floating_rect(bounds)); - let focus = self.workspaces.active().fullscreen.is_none(); + let fullscreen = self.workspaces.active().fullscreen; + let focus = true; if let Err(error) = self.workspaces .insert_floating_window(id, self.workspaces.active_id(), rect, focus) @@ -1040,6 +1072,9 @@ impl Ferese { } self.window_ids.insert(window.clone(), id); self.window_stack.insert(id); + if let Some(fullscreen) = fullscreen { + self.floating_above_fullscreen.insert(id, fullscreen); + } if focus { self.focused_window = Some(id); } @@ -1095,9 +1130,12 @@ impl Ferese { let id = WindowId(self.next_window_id); self.next_window_id += 1; + let focus = workspace == self.workspaces.active_id() + && (self.focused_window == Some(parent) + || self.workspaces.active().fullscreen == Some(parent)); if let Err(error) = self .workspaces - .insert_floating_window(id, workspace, rect, false) + .insert_floating_window(id, workspace, rect, focus) { tracing::error!(%error, ?id, ?parent, "failed to insert transient window"); return; @@ -1105,8 +1143,21 @@ impl Ferese { self.window_ids.insert(window.clone(), id); self.window_stack.insert(id); - self.space.map_element(window, (0, 0), false); + if self + .workspaces + .workspace(workspace) + .is_some_and(|workspace| workspace.fullscreen == Some(parent)) + { + self.floating_above_fullscreen.insert(id, parent); + } + if focus { + self.focused_window = Some(id); + } + self.space.map_element(window, (0, 0), focus); self.relayout(); + if focus { + self.restore_keyboard_focus(); + } } pub(crate) fn apply_initial_window_rules( @@ -1273,7 +1324,25 @@ impl Ferese { return; } + if self + .workspaces + .workspace_for_window(parent) + .and_then(|workspace| self.workspaces.workspace(workspace)) + .is_some_and(|workspace| workspace.fullscreen == Some(parent)) + { + self.floating_above_fullscreen.insert(id, parent); + } + if self.focused_window == Some(parent) { + if let Err(error) = self.workspaces.focus_window(id) { + tracing::warn!(%error, ?id, "failed to focus transient window"); + } else { + self.focused_window = Some(id); + self.window_stack.raise(id); + } + } + self.relayout(); + self.restore_keyboard_focus(); } pub fn remove_tiled_window(&mut self, window: &Window) { @@ -1289,6 +1358,7 @@ impl Ferese { self.maximized_windows.remove(&id); self.maximized_column_widths.remove(&id); self.window_stack.remove(id); + self.floating_above_fullscreen.remove(&id); self.natural_floating_pending.remove(&id); self.window_borders.remove(&id); self.window_dims.remove(&id); diff --git a/crates/ferese-core/src/workspace.rs b/crates/ferese-core/src/workspace.rs index 25de53c..3db091c 100644 --- a/crates/ferese-core/src/workspace.rs +++ b/crates/ferese-core/src/workspace.rs @@ -515,7 +515,14 @@ impl WorkspaceSet { self.active = workspace; debug_assert!(self.validate().is_ok()); - Ok(self.active().fullscreen.or(self.active().last_focused)) + let focused = self.active().last_focused; + let visible_floating = focused.filter(|window| { + matches!( + self.placement(*window), + Some(WindowPlacement::Floating { .. }) + ) + }); + Ok(visible_floating.or(self.active().fullscreen).or(focused)) } pub fn workspace(&self, id: WorkspaceId) -> Option<&Workspace> { @@ -631,11 +638,13 @@ impl WorkspaceSet { "focused window is not on the active workspace", )); } - if self - .active() - .fullscreen - .is_some_and(|fullscreen| fullscreen != window) - { + if self.active().fullscreen.is_some_and(|fullscreen| { + fullscreen != window + && !matches!( + self.placement(window), + Some(WindowPlacement::Floating { .. }) + ) + }) { return Err(WorkspaceError::InvalidState( "focused window is hidden by fullscreen", )); @@ -823,7 +832,7 @@ impl WorkspaceSet { .get_mut(&workspace_id) .ok_or(WorkspaceError::UnknownWorkspace(workspace_id))?; workspace.floating.push(window); - if focus && workspace.fullscreen.is_none() { + if focus { workspace.last_focused = Some(window); } self.window_workspaces.insert(window, workspace_id); @@ -1613,6 +1622,31 @@ mod tests { assert!(workspaces.validate().is_ok()); } + #[test] + fn floating_dialog_can_focus_above_fullscreen() { + let mut workspaces = WorkspaceSet::default(); + let workspace = workspaces.active_id(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); + workspaces.toggle_fullscreen(WindowId(1)).unwrap(); + workspaces + .insert_floating_window( + WindowId(2), + workspace, + Rect::new(100.0, 100.0, 500.0, 400.0), + true, + ) + .unwrap(); + + assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); + assert!(workspaces.focus_window(WindowId(2)).is_ok()); + workspaces.switch_to_numeric(2).unwrap(); + assert_eq!(workspaces.activate(workspace).unwrap(), Some(WindowId(2))); + assert_eq!(workspaces.active().fullscreen, Some(WindowId(1))); + assert!(workspaces.validate().is_ok()); + } + #[test] fn switching_to_fullscreen_restores_the_visible_window() { let mut workspaces = WorkspaceSet::default();