Keep fullscreen dialogs visible and preserve window focus

This commit is contained in:
Samuel Onoja committed 2026-09-29 16:14:19 +01:00
1 parent eda453dabd
commit b3de13e27e
3 files changed
+142 -22

No files matched your search

+21 -5
View File
@@ -1,8 +1,15 @@
use ferese_layout::WindowId; use ferese_layout::WindowId;
use std::collections::HashMap; use std::collections::HashMap;
pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool) -> u8 { pub(crate) fn layer_priority(
if fullscreen { floating: bool,
zooming: bool,
fullscreen: bool,
above_fullscreen: bool,
) -> u8 {
if above_fullscreen {
4
} else if fullscreen {
3 3
} else if floating { } else if floating {
2 2
@@ -90,9 +97,18 @@ mod tests {
#[test] #[test]
fn floating_windows_stay_above_tiled_and_maximized_windows() { fn floating_windows_stay_above_tiled_and_maximized_windows() {
assert!(layer_priority(true, false, false) > layer_priority(false, true, false)); assert!(
assert!(layer_priority(false, true, false) > layer_priority(false, false, false)); layer_priority(true, false, false, false) > layer_priority(false, true, false, false)
assert!(layer_priority(false, false, true) > layer_priority(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] #[test]
+80 -10
View File
@@ -128,6 +128,7 @@ pub struct Ferese {
maximized_windows: HashSet<WindowId>, maximized_windows: HashSet<WindowId>,
maximized_column_widths: HashMap<WindowId, ColumnWidth>, maximized_column_widths: HashMap<WindowId, ColumnWidth>,
window_stack: crate::stacking::WindowStack, window_stack: crate::stacking::WindowStack,
floating_above_fullscreen: HashMap<WindowId, WindowId>,
natural_floating_pending: HashSet<WindowId>, natural_floating_pending: HashSet<WindowId>,
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>, pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
pub(crate) window_dims: HashMap<WindowId, crate::winit::WindowBorderBuffers>, pub(crate) window_dims: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
@@ -347,6 +348,7 @@ impl Ferese {
maximized_windows: HashSet::new(), maximized_windows: HashSet::new(),
maximized_column_widths: HashMap::new(), maximized_column_widths: HashMap::new(),
window_stack: crate::stacking::WindowStack::default(), window_stack: crate::stacking::WindowStack::default(),
floating_above_fullscreen: HashMap::new(),
natural_floating_pending: HashSet::new(), natural_floating_pending: HashSet::new(),
window_borders: HashMap::new(), window_borders: HashMap::new(),
window_dims: HashMap::new(), window_dims: HashMap::new(),
@@ -804,15 +806,27 @@ impl Ferese {
self.inverse_presented_window_point(*id, position.x, position.y)?; self.inverse_presented_window_point(*id, position.x, position.y)?;
let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64();
window let surface = window.surface_under(source_point, WindowSurfaceType::ALL);
.surface_under(source_point, WindowSurfaceType::ALL) let (surface, surface_location) = match surface {
.map(|(surface, surface_location)| { Some(hit) => hit,
let surface_point = source_point - surface_location.to_f64(); None if self.window_fills_visual_bounds(*id, workspace) => {
(surface, position - surface_point) (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( fn layer_surface_under(
&self, &self,
position: Point<f64, Logical>, position: Point<f64, Logical>,
@@ -887,7 +901,13 @@ impl Ferese {
self.inverse_presented_window_point(*id, position.x, position.y)?; self.inverse_presented_window_point(*id, position.x, position.y)?;
let source_point = let source_point =
Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); 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()) Some(window.clone())
@@ -941,6 +961,17 @@ impl Ferese {
floating, floating,
geometry.is_some_and(|geometry| geometry.is_zooming()), geometry.is_some_and(|geometry| geometry.is_zooming()),
geometry.is_some_and(|geometry| geometry.is_fullscreen()), 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, priority,
@@ -1027,7 +1058,8 @@ impl Ferese {
let rect = client_size(&window) let rect = client_size(&window)
.map(|size| natural_floating_rect(bounds, size)) .map(|size| natural_floating_rect(bounds, size))
.unwrap_or_else(|| centered_floating_rect(bounds)); .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) = if let Err(error) =
self.workspaces self.workspaces
.insert_floating_window(id, self.workspaces.active_id(), rect, focus) .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_ids.insert(window.clone(), id);
self.window_stack.insert(id); self.window_stack.insert(id);
if let Some(fullscreen) = fullscreen {
self.floating_above_fullscreen.insert(id, fullscreen);
}
if focus { if focus {
self.focused_window = Some(id); self.focused_window = Some(id);
} }
@@ -1095,9 +1130,12 @@ impl Ferese {
let id = WindowId(self.next_window_id); let id = WindowId(self.next_window_id);
self.next_window_id += 1; 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 if let Err(error) = self
.workspaces .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"); tracing::error!(%error, ?id, ?parent, "failed to insert transient window");
return; return;
@@ -1105,8 +1143,21 @@ impl Ferese {
self.window_ids.insert(window.clone(), id); self.window_ids.insert(window.clone(), id);
self.window_stack.insert(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(); self.relayout();
if focus {
self.restore_keyboard_focus();
}
} }
pub(crate) fn apply_initial_window_rules( pub(crate) fn apply_initial_window_rules(
@@ -1273,7 +1324,25 @@ impl Ferese {
return; 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.relayout();
self.restore_keyboard_focus();
} }
pub fn remove_tiled_window(&mut self, window: &Window) { pub fn remove_tiled_window(&mut self, window: &Window) {
@@ -1289,6 +1358,7 @@ impl Ferese {
self.maximized_windows.remove(&id); self.maximized_windows.remove(&id);
self.maximized_column_widths.remove(&id); self.maximized_column_widths.remove(&id);
self.window_stack.remove(id); self.window_stack.remove(id);
self.floating_above_fullscreen.remove(&id);
self.natural_floating_pending.remove(&id); self.natural_floating_pending.remove(&id);
self.window_borders.remove(&id); self.window_borders.remove(&id);
self.window_dims.remove(&id); self.window_dims.remove(&id);
+41 -7
View File
@@ -515,7 +515,14 @@ impl WorkspaceSet {
self.active = workspace; self.active = workspace;
debug_assert!(self.validate().is_ok()); 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> { pub fn workspace(&self, id: WorkspaceId) -> Option<&Workspace> {
@@ -631,11 +638,13 @@ impl WorkspaceSet {
"focused window is not on the active workspace", "focused window is not on the active workspace",
)); ));
} }
if self if self.active().fullscreen.is_some_and(|fullscreen| {
.active() fullscreen != window
.fullscreen && !matches!(
.is_some_and(|fullscreen| fullscreen != window) self.placement(window),
{ Some(WindowPlacement::Floating { .. })
)
}) {
return Err(WorkspaceError::InvalidState( return Err(WorkspaceError::InvalidState(
"focused window is hidden by fullscreen", "focused window is hidden by fullscreen",
)); ));
@@ -823,7 +832,7 @@ impl WorkspaceSet {
.get_mut(&workspace_id) .get_mut(&workspace_id)
.ok_or(WorkspaceError::UnknownWorkspace(workspace_id))?; .ok_or(WorkspaceError::UnknownWorkspace(workspace_id))?;
workspace.floating.push(window); workspace.floating.push(window);
if focus && workspace.fullscreen.is_none() { if focus {
workspace.last_focused = Some(window); workspace.last_focused = Some(window);
} }
self.window_workspaces.insert(window, workspace_id); self.window_workspaces.insert(window, workspace_id);
@@ -1613,6 +1622,31 @@ mod tests {
assert!(workspaces.validate().is_ok()); 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] #[test]
fn switching_to_fullscreen_restores_the_visible_window() { fn switching_to_fullscreen_restores_the_visible_window() {
let mut workspaces = WorkspaceSet::default(); let mut workspaces = WorkspaceSet::default();