diff --git a/compositor/src/handlers/xdg_shell.rs b/compositor/src/handlers/xdg_shell.rs index 10b0e5e..d1a9852 100644 --- a/compositor/src/handlers/xdg_shell.rs +++ b/compositor/src/handlers/xdg_shell.rs @@ -273,21 +273,32 @@ fn check_grab( impl XdgDecorationHandler for Ferese { fn new_decoration(&mut self, toplevel: ToplevelSurface) { - set_decoration_mode(&toplevel, Mode::ClientSide); + let mode = self.decoration_mode_for(&toplevel); + set_decoration_mode(&toplevel, mode); } - fn request_mode(&mut self, toplevel: ToplevelSurface, mode: Mode) { - // Phase 1 negotiates decorations but does not yet draw a server frame. - // Honor CSD and safely fall back to CSD for premature SSD requests. - let mode = match mode { - Mode::ClientSide | Mode::ServerSide => Mode::ClientSide, - _ => Mode::ClientSide, - }; + fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: Mode) { + let mode = self.decoration_mode_for(&toplevel); set_decoration_mode(&toplevel, mode); } fn unset_mode(&mut self, toplevel: ToplevelSurface) { - set_decoration_mode(&toplevel, Mode::ClientSide); + let mode = self.decoration_mode_for(&toplevel); + set_decoration_mode(&toplevel, mode); + } +} + +impl Ferese { + fn decoration_mode_for(&self, toplevel: &ToplevelSurface) -> Mode { + self.space + .elements() + .find(|window| { + window + .toplevel() + .is_some_and(|candidate| candidate.wl_surface() == toplevel.wl_surface()) + }) + .filter(|window| self.is_floating_window(window)) + .map_or(Mode::ServerSide, |_| Mode::ClientSide) } } diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 3834e69..f86a651 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -120,6 +120,15 @@ impl Ferese { } } + FilterResult::Intercept(()) + } else if !modifiers.shift + && matches!(symbol, keysyms::KEY_q | keysyms::KEY_Q) + { + if state == KeyState::Pressed { + data.intercepted_keys.insert(keycode); + data.close_focused_window(); + } + FilterResult::Intercept(()) } else if !modifiers.shift && matches!(symbol, keysyms::KEY_f | keysyms::KEY_F) diff --git a/compositor/src/state.rs b/compositor/src/state.rs index f43d34d..e8f0a8d 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -23,7 +23,10 @@ use smithay::{ output::Output, reexports::{ calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic}, - wayland_protocols::xdg::shell::server::xdg_toplevel, + wayland_protocols::xdg::{ + decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode, + shell::server::xdg_toplevel, + }, wayland_server::{ Display, DisplayHandle, backend::{ClientData, ClientId, DisconnectReason}, @@ -511,13 +514,19 @@ impl Ferese { } }; - Some((window.clone(), *id, rect, fullscreen == Some(*id))) + let is_fullscreen = fullscreen == Some(*id); + let is_floating = matches!( + self.workspaces.placement(*id), + Some(WindowPlacement::Floating { .. }) + ); + + Some((window.clone(), *id, rect, is_fullscreen, is_floating)) }) .collect::>(); let now = self.start_time.elapsed(); - for (window, id, rect, is_fullscreen) in placements { + for (window, id, rect, is_fullscreen, is_floating) in placements { let committed_size = client_size(&window); let geometry = self .window_geometry @@ -534,11 +543,21 @@ impl Ferese { state.size = Some((size.width, size.height).into()); } - if is_fullscreen { + let fullscreen_changed = if is_fullscreen { state.states.set(xdg_toplevel::State::Fullscreen) } else { state.states.unset(xdg_toplevel::State::Fullscreen) - } + }; + + let decoration_mode = if is_floating && !is_fullscreen { + DecorationMode::ClientSide + } else { + DecorationMode::ServerSide + }; + let decoration_changed = state.decoration_mode != Some(decoration_mode); + state.decoration_mode = Some(decoration_mode); + + fullscreen_changed || decoration_changed }); if requested_size.is_some() || state_changed { @@ -763,6 +782,21 @@ impl Ferese { self.relayout(); } + pub fn close_focused_window(&self) { + let Some(focused) = self.focused_window else { + return; + }; + let Some(toplevel) = self + .window_ids + .iter() + .find_map(|(window, id)| (*id == focused).then(|| window.toplevel()).flatten()) + else { + return; + }; + + toplevel.send_close(); + } + pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) { match self.workspaces.set_fullscreen(window, enabled) { Ok(true) => self.relayout(), diff --git a/docs/ferese-window-manager-spec.md b/docs/ferese-window-manager-spec.md index af26b5b..a46f5cf 100644 --- a/docs/ferese-window-manager-spec.md +++ b/docs/ferese-window-manager-spec.md @@ -630,6 +630,11 @@ decorations, Ferese MUST provide usable move, resize, and close interactions. Shadows, masks, and focus borders are compositor effects and may apply in either mode. +Tiled and fullscreen windows default to borderless server-side decoration so +application title bars do not consume layout space. Floating windows and +dialogs default to client-side decoration until Ferese provides a complete +compositor-drawn floating frame. + ### 8.3 XWayland The desktop tier SHOULD use `xwayland-satellite`, launched on demand, so X11