feat: float transient windows with their parent
This commit is contained in:
1 parent
677e0ba5d1
commit
6060257ffe
3 files changed
+145
-1
No files matched your search
@@ -34,7 +34,21 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||||
self.add_tiled_window(Window::new_wayland_window(surface));
|
let parent = surface.parent().and_then(|parent| {
|
||||||
|
self.window_ids.iter().find_map(|(window, id)| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.is_some_and(|toplevel| toplevel.wl_surface() == &parent)
|
||||||
|
.then_some(*id)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let window = Window::new_wayland_window(surface);
|
||||||
|
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
self.add_transient_window(window, parent);
|
||||||
|
} else {
|
||||||
|
self.add_tiled_window(window);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||||
|
|||||||
@@ -195,6 +195,33 @@ impl Ferese {
|
|||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn add_transient_window(&mut self, window: Window, parent: WindowId) {
|
||||||
|
let Some(workspace) = self.workspaces.workspace_for_window(parent) else {
|
||||||
|
self.add_tiled_window(window);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(bounds) = self.output_bounds() else {
|
||||||
|
self.add_tiled_window(window);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let parent_rect = self.logical_window_rect(parent, bounds).unwrap_or(bounds);
|
||||||
|
let rect = centered_transient_rect(parent_rect);
|
||||||
|
let id = WindowId(self.next_window_id);
|
||||||
|
self.next_window_id += 1;
|
||||||
|
|
||||||
|
if let Err(error) = self
|
||||||
|
.workspaces
|
||||||
|
.insert_floating_window(id, workspace, rect, false)
|
||||||
|
{
|
||||||
|
tracing::error!(%error, ?id, ?parent, "failed to insert transient window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.window_ids.insert(window.clone(), id);
|
||||||
|
self.space.map_element(window, (0, 0), false);
|
||||||
|
self.relayout();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_tiled_window(&mut self, window: &Window) {
|
pub fn remove_tiled_window(&mut self, window: &Window) {
|
||||||
let Some(id) = self.window_ids.remove(window) else {
|
let Some(id) = self.window_ids.remove(window) else {
|
||||||
return;
|
return;
|
||||||
@@ -538,6 +565,25 @@ impl Ferese {
|
|||||||
geometry.size.h as f64,
|
geometry.size.h as f64,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn logical_window_rect(&self, window: WindowId, bounds: Rect) -> Option<Rect> {
|
||||||
|
let workspace = self.workspaces.workspace_for_window(window)?;
|
||||||
|
let workspace = self.workspaces.workspace(workspace)?;
|
||||||
|
|
||||||
|
if workspace.fullscreen == Some(window) {
|
||||||
|
return Some(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.workspaces.placement(window)? {
|
||||||
|
WindowPlacement::Tiled => workspace
|
||||||
|
.layout
|
||||||
|
.geometry_with_gaps(bounds, GapConfig::default())
|
||||||
|
.ok()?
|
||||||
|
.get(&window)
|
||||||
|
.copied(),
|
||||||
|
WindowPlacement::Floating { rect } => Some(rect),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn centered_floating_rect(bounds: Rect) -> Rect {
|
fn centered_floating_rect(bounds: Rect) -> Rect {
|
||||||
@@ -552,6 +598,18 @@ fn centered_floating_rect(bounds: Rect) -> Rect {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn centered_transient_rect(parent: Rect) -> Rect {
|
||||||
|
let width = (parent.width * 0.75).clamp(1.0, 640.0);
|
||||||
|
let height = (parent.height * 0.75).clamp(1.0, 480.0);
|
||||||
|
|
||||||
|
Rect::new(
|
||||||
|
parent.x + (parent.width - width) / 2.0,
|
||||||
|
parent.y + (parent.height - height) / 2.0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ClientState {
|
pub struct ClientState {
|
||||||
pub compositor_state: CompositorClientState,
|
pub compositor_state: CompositorClientState,
|
||||||
@@ -561,3 +619,16 @@ impl ClientData for ClientState {
|
|||||||
fn initialized(&self, _client_id: ClientId) {}
|
fn initialized(&self, _client_id: ClientId) {}
|
||||||
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
|
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transient_geometry_is_centered_and_bounded_by_its_parent() {
|
||||||
|
let parent = Rect::new(100.0, 50.0, 1_000.0, 800.0);
|
||||||
|
let transient = centered_transient_rect(parent);
|
||||||
|
|
||||||
|
assert_eq!(transient, Rect::new(280.0, 210.0, 640.0, 480.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -232,6 +232,37 @@ impl WorkspaceSet {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn insert_floating_window(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
rect: Rect,
|
||||||
|
focus: bool,
|
||||||
|
) -> Result<(), WorkspaceError> {
|
||||||
|
if self.window_workspaces.contains_key(&window) {
|
||||||
|
return Err(WorkspaceError::DuplicateWindow(window));
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = self
|
||||||
|
.workspaces
|
||||||
|
.get_mut(&workspace_id)
|
||||||
|
.ok_or(WorkspaceError::UnknownWorkspace(workspace_id))?;
|
||||||
|
workspace.floating.push(window);
|
||||||
|
if focus && workspace.fullscreen.is_none() {
|
||||||
|
workspace.last_focused = Some(window);
|
||||||
|
}
|
||||||
|
self.window_workspaces.insert(window, workspace_id);
|
||||||
|
self.placements.insert(
|
||||||
|
window,
|
||||||
|
WindowPlacement::Floating {
|
||||||
|
rect: normalized_floating_rect(rect),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
debug_assert!(self.validate().is_ok());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_window(&mut self, window: WindowId) -> Result<(), WorkspaceError> {
|
pub fn remove_window(&mut self, window: WindowId) -> Result<(), WorkspaceError> {
|
||||||
let workspace_id = self
|
let workspace_id = self
|
||||||
.window_workspaces
|
.window_workspaces
|
||||||
@@ -675,6 +706,34 @@ mod tests {
|
|||||||
assert!(workspaces.validate().is_ok());
|
assert!(workspaces.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn floating_window_can_follow_its_parent_workspace_without_focus() {
|
||||||
|
let mut workspaces = WorkspaceSet::default();
|
||||||
|
workspaces
|
||||||
|
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
let parent_workspace = workspaces.active_id();
|
||||||
|
workspaces.switch_to_numeric(2).unwrap();
|
||||||
|
workspaces
|
||||||
|
.insert_floating_window(
|
||||||
|
WindowId(2),
|
||||||
|
parent_workspace,
|
||||||
|
Rect::new(20.0, 30.0, 400.0, 300.0),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
workspaces.workspace_for_window(WindowId(2)),
|
||||||
|
Some(parent_workspace)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
workspaces.workspace(parent_workspace).unwrap().last_focused,
|
||||||
|
Some(WindowId(1))
|
||||||
|
);
|
||||||
|
assert!(workspaces.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fullscreen_preserves_underlying_placement() {
|
fn fullscreen_preserves_underlying_placement() {
|
||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
|
|||||||
Reference in new issue
Block a user