diff options
Diffstat (limited to 'crates/wayland/src/state/lifecycle.rs')
| -rw-r--r-- | crates/wayland/src/state/lifecycle.rs | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/crates/wayland/src/state/lifecycle.rs b/crates/wayland/src/state/lifecycle.rs new file mode 100644 index 0000000..f7ae40a --- /dev/null +++ b/crates/wayland/src/state/lifecycle.rs @@ -0,0 +1,175 @@ +use super::*; +impl CompState { + pub(crate) fn new_managed_window(&mut self, toplevel: ToplevelSurface) { + let surface = toplevel.wl_surface().clone(); + let id = { + let mut wm = self.wm.borrow_mut(); + let id = wm.alloc_window_id(); + let title = with_toplevel_title(toplevel.wl_surface()).unwrap_or_default(); + let mut w = CoreWindow::new(id, title); + w.app_id = with_toplevel_app_id(toplevel.wl_surface()).unwrap_or_default(); + w.geometry = srdwm_core::Rect::new(0, 0, 800, 600 + TITLEBAR_HEIGHT as i32 as u32); + wm.add_window(w); + // Starts the open-slide tween (see `WindowAnim`'s doc comment): + // the window's first `sync_geometry` call below will see this, + // register the tween, and place it here - a few pixels below + // its resting position - rather than jumping straight to + // `geometry`. Same size throughout, so no extra client configure + // is needed for the tween itself. + if wm.animations_enabled { + if let Some(win) = wm.window_mut(id) { + let g = win.geometry; + win.anim_from = Some(srdwm_core::Rect { y: g.y + OPEN_SLIDE_OFFSET, ..g }); + } + } + id + }; + + let dwindow = DWindow::new_wayland_window(toplevel.clone()); + self.surface_to_id.insert(surface.clone(), id); + self.id_to_window.insert(id, dwindow); + // `sync_geometry` handles the initial placement itself (map_element + // + the first configure, since `last_synced_size` has no entry yet + // for this id) as well as starting the open-slide tween registered + // above - see its own doc comment. + self.sync_geometry(id); + self.redraw_decoration_buffer(id); + // `WindowManager::add_window` already made this the focused window in + // srdwm's own state, but that alone is purely internal bookkeeping -- + // without this, a freshly-opened window receives no keystrokes and + // can't copy/paste until it's clicked, because nothing ever gave it + // real Wayland keyboard/selection focus. (Same class of bug as the + // click-to-focus one fixed earlier; this is the creation path.) + self.set_keyboard_focus(Some(surface)); + // A newly-mapped window goes on top, but not over a pinned one. + self.raise_pinned(); + self.pending.borrow_mut().push(CoreEvent::WindowCreated(id)); + foreign_toplevel::window_created(self, id); + } + + /// Applies a negotiated `zxdg_toplevel_decoration_v1` mode to our own + /// `Window.decorated` flag and refreshes (or drops) its titlebar buffer + /// to match - see `XdgDecorationHandler::request_mode`'s doc comment + /// for why. A no-op if the surface has no window yet (decoration + /// negotiation racing ahead of `new_toplevel`, which shouldn't happen + /// in practice but costs nothing to guard against). + pub(crate) fn set_decorated_from_mode(&mut self, surface: &WlSurface, decorated: bool) { + let Some(&id) = self.surface_to_id.get(surface) else { return }; + if let Some(w) = self.wm.borrow_mut().window_mut(id) { + w.decorated = decorated; + } + self.redraw_decoration_buffer(id); + // Re-applies content size/position for the now-changed titlebar + // reservation - see `sync_geometry`'s own doc comment on why this + // can't be skipped: redrawing the titlebar buffer alone doesn't + // touch the content area's size or offset at all. + self.sync_geometry(id); + } + + /// (Re)renders the titlebar band for `id` - background plus title text + /// via `decoration::render_titlebar` - and replaces the buffer in + /// `self.decorations`. Called on creation, geometry change (width + /// affects layout), and focus change (text color). + pub(crate) fn redraw_decoration_buffer(&mut self, id: WindowId) { + let Some(w) = self.wm.borrow().window(id).cloned() else { return }; + let focused = self.wm.borrow().focused_id() == Some(id); + let theme = self.wm.borrow().theme; + if w.decorated { + let fg = if focused { theme.titlebar_fg_focused } else { theme.titlebar_fg_unfocused }; + let width = w.geometry.width.max(1); + // Always rounded now, bordered or not - `render_border_top` + // gives a bordered window's border strip the matching rounded + // cut, so there's no more square-frame-around-a-round-titlebar + // clash to avoid. See `render_titlebar`'s `round_corners` doc + // comment. + let data = decoration::render_titlebar(width, TITLEBAR_HEIGHT, &w.title, theme.titlebar_bg, fg, true); + let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (width as i32, TITLEBAR_HEIGHT as i32), 1, Transform::Normal, None); + self.decorations.insert(id, buffer); + } else { + self.decorations.remove(&id); + } + // The border-top bitmap is independent of `decorated` - an + // undecorated (CSD) window can still have `border_width > 0` - so + // it's rebuilt here unconditionally rather than falling under the + // early return above. Cached the same way `decorations` is, at the + // same trigger points (creation, a size change, a rule re-applying, + // and - since this call is now also reached from focus changes -- + // `w.border_color`'s focused/unfocused dimming): see `elements:: + // border_side_render_element`'s doc comment for why re-rasterizing + // this every render frame (an earlier version of this method did) + // was a real, continuous cost, not just a redundant one. + if w.border_width > 0 { + let color = effective_border_color(w.border_color, focused); + let strips = decoration::border_strips(w.geometry, w.border_width); + if strips[0].width > 0 && strips[0].height > 0 { + let data = decoration::render_border_top(strips[0].width, w.border_width, color); + let buffer = + MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (strips[0].width as i32, w.border_width as i32), 1, Transform::Normal, None); + self.border_top_decorations.insert(id, buffer); + } else { + self.border_top_decorations.remove(&id); + } + } else { + self.border_top_decorations.remove(&id); + } + // No shadow for a maximized/fullscreen window: it already reaches + // (or, for fullscreen, exceeds) the monitor's own edge, so there is + // nowhere for `SHADOW_SIZE` pixels of shadow to actually fall, and + // a shadow drawn there would either be clipped to nothing useful or + // - for a maximized window short of the true monitor edge - read + // as a shadow the window doesn't visually need. Matches the + // Hyprland/GNOME convention `MISSING.md` measures this compositor + // against. + let shadows_enabled = self.wm.borrow().shadows_enabled; + if shadows_enabled && !w.maximized && !w.fullscreen { + let data = decoration::shadow_bitmap(w.geometry.width, w.geometry.height); + let rect = decoration::shadow_rect(w.geometry); + let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (rect.width as i32, rect.height as i32), 1, Transform::Normal, None); + self.shadow_buffers.insert(id, buffer); + } else { + self.shadow_buffers.remove(&id); + } + } + + pub(crate) fn remove_window(&mut self, surface: &WlSurface) { + let Some(id) = self.surface_to_id.remove(surface) else { return }; + if let Some(w) = self.id_to_window.remove(&id) { + self.space.unmap_elem(&w); + } + self.decorations.remove(&id); + self.border_top_decorations.remove(&id); + self.shadow_buffers.remove(&id); + self.border_side_buffers.remove(&id); + self.last_synced_size.remove(&id); + self.content_epoch.remove(&id); + self.rounded_content_buffers.remove(&id); + // A window closing (crash, kill, or its own menu's "Close" action + // racing ahead of this) while its context menu is still open would + // otherwise leave the menu pointing at a dead id - selecting any + // row on it would then silently no-op against a window that no + // longer exists, with no indication anything went wrong. + if self.context_menu.as_ref().is_some_and(|m| m.window == id) { + self.close_context_menu(); + } + self.wm.borrow_mut().remove_window(id); + self.pending.borrow_mut().push(CoreEvent::WindowDestroyed(id)); + foreign_toplevel::window_closed(self, id); + // `remove_window` may have picked a new focused window on its own + // (falls back to whatever's now on top) - see `sync_keyboard_focus`'s + // doc comment for why the Wayland/X11 side needs a separate nudge to + // actually catch up to that. + crate::input::sync_keyboard_focus(self); + // Safety net for `zwp_idle_inhibit_manager_v1`: smithay's own + // `IdleInhibitorState` only calls `uninhibit` on an explicit + // `destroy` request, never on `Dispatch::destroyed` - so a video + // player that crashes or gets killed instead of exiting cleanly + // would leave its inhibitor permanently stuck, holding the whole + // system awake forever with no client left to ever release it. + // Its window closing is the one thing guaranteed to happen either + // way, so this is what actually catches that case. + if self.idle_inhibiting_surfaces.contains(surface) { + self.idle_inhibiting_surfaces.retain(|s| s != surface); + self.idle_notifier_state.set_is_inhibited(!self.idle_inhibiting_surfaces.is_empty()); + } + } +} |