srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/udev.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-30 16:10:00 +0200
committersrdusr <[email protected]>2024-05-30 16:10:00 +0200
commit1c175642d073689ca11b9252411ea5f8446007d0 (patch)
tree00b8f9062cf1261d2d2642c059a14ff5790522e5 /crates/wayland/src/udev.rs
parent3d3057ae384ef7389284af8988410889e99c6bb9 (diff)
downloadsrdwm-1c175642d073689ca11b9252411ea5f8446007d0.tar.gz
srdwm-1c175642d073689ca11b9252411ea5f8446007d0.zip
Fix decoration drift during animated transitions; checkpoint IPC/global-menu/output-management work
Border/titlebar decoration was built from Window.geometry (the animation's final target) in both wayland backends' render loops, while sync_geometry already draws a window's actual content at window_anims' interpolated rect during any maximize/fullscreen/open-slide tween. Border and content read two different rectangles for the whole transition, so the border visibly detached from the window it was outlining - reported as "borders aren't flush." Both udev.rs and winit.rs now read the same animated rect for titlebar placement, border-strip placement, and the occlusion test against later windows in stacking order. Verified: cargo build --workspace, cargo clippy (0 new warnings), cargo test -p srdwm-core (111/111). Also checkpoints substantial protocol/IPC work from prior sessions that had accumulated uncommitted: gtk-shell1 support (gtk_shell.rs, the vendored gtk-shell.xml, xwayland.rs's X11-side mirror) backing the global app menu; zwlr_foreign_toplevel_manager_v1 (foreign_toplevel.rs) broadcasting distinct maximized/minimized/fullscreen/activated state per window; output_management (ext-output-management + layer-shell exclusive-zone reservation tracking); workspace.rs and context_menu.rs; a Unix-socket IPC crate (platform/src/ ipc.rs) and a `srd` control-CLI crate (crates/ctl); xkb_config.rs; and a theme module (core/src/theme.rs). A peer session working the AGS shell concurrently verified several of these live against a running srdwm: the global menu rendering a real app's File/Edit menu over gtk-shell1, and foreign-toplevel correctly reporting maximized and fullscreen as independent, non-simultaneous states with the geometry each implies (maximize stops at a reserved top bar and past a dock; fullscreen reaches the true monitor edge).
Diffstat (limited to 'crates/wayland/src/udev.rs')
-rw-r--r--crates/wayland/src/udev.rs640
1 files changed, 591 insertions, 49 deletions
diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs
index 95ef571..96d57d5 100644
--- a/crates/wayland/src/udev.rs
+++ b/crates/wayland/src/udev.rs
@@ -38,20 +38,21 @@ use smithay::backend::input::{
};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::damage::OutputDamageTracker;
-use smithay::backend::renderer::element::memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement};
+use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::pixman::PixmanRenderer;
-use smithay::backend::renderer::Bind;
+use smithay::backend::renderer::{Bind, ImportDma};
use smithay::backend::session::{libseat::LibSeatSession, libseat::LibSeatSessionNotifier, Event as SessionEvent, Session};
use smithay::backend::udev::{self, UdevBackend, UdevEvent};
use smithay::desktop::space::render_output;
-use smithay::desktop::{layer_map_for_output, Space};
+use smithay::desktop::{layer_map_for_output, PopupManager, Space};
+use smithay::backend::input::AxisSource;
use smithay::input::pointer::AxisFrame;
use smithay::input::SeatState;
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::generic::{FdWrapper, Generic};
use smithay::reexports::calloop::{EventLoop, Interest, LoopHandle, Mode as CalloopMode, PostAction};
-use smithay::reexports::drm::buffer::DrmFourcc;
+use smithay::reexports::drm::buffer::{Buffer as DrmBufferTrait, DrmFourcc};
use smithay::reexports::drm::control::{
connector, crtc, dumbbuffer::DumbBuffer, framebuffer, Device as ControlDevice, Event as DrmEvent, Mode as DrmMode,
ModeTypeFlags, PageFlipFlags,
@@ -62,18 +63,21 @@ use smithay::reexports::pixman::{FormatCode, Image};
use smithay::reexports::rustix;
use smithay::reexports::wayland_server::backend::GlobalId;
use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket};
-use smithay::utils::{Logical, Point, Transform};
+use smithay::utils::{Logical, Physical, Point, Rectangle, Scale, Size, Transform};
use smithay::wayland::compositor::CompositorState;
+use smithay::wayland::dmabuf::DmabufState;
use smithay::wayland::selection::data_device::DataDeviceState;
use smithay::wayland::selection::primary_selection::PrimarySelectionState;
use smithay::wayland::selection::wlr_data_control::DataControlState;
use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
use smithay::wayland::shell::xdg::XdgShellState;
use smithay::wayland::shm::ShmState;
+use smithay::wayland::xdg_activation::XdgActivationState;
use srdwm_core::{Event as CoreEvent, WindowManager};
use srdwm_platform::{Platform, PlatformError, PlatformKind, Result as PlatformResult};
+use crate::decoration;
use crate::err;
use crate::input::{handle_keyboard_key_event, handle_pointer_button, handle_pointer_position};
use crate::state::{ClientState, CompState};
@@ -117,6 +121,24 @@ pub(crate) struct UdevHead {
/// A flip is in flight; the next frame for this head waits for the DRM
/// page-flip event (matched by `crtc`) before starting.
pub(crate) flip_pending: bool,
+ /// Per-buffer-slot age passed to `damage_tracker.render_output`: how
+ /// many *damage-producing* renders ago that exact buffer was last
+ /// brought fully up to date. 0 means "never rendered, contents
+ /// undefined" and forces a full redraw. This used to be hardcoded to 0
+ /// on every single call regardless - which, per
+ /// `OutputDamageTracker::damage_output_internal`, forces the entire
+ /// output geometry to be treated as damaged every time, so every frame
+ /// was a full-screen software (pixman) recomposite plus a page-flip,
+ /// nonstop, at whatever rate the event loop's 16ms dispatch timeout
+ /// allowed - continuously, even with a fully idle desktop. That
+ /// competes for the same single thread's CPU time as libinput event
+ /// processing and is exactly what `client bug: event processing
+ /// lagging behind` (logged for both the keyboard and the mouse) was
+ /// reporting. With correct ages, a call that finds no real damage
+ /// returns near-free (`damage_output_internal`'s own element/geometry
+ /// comparison, no pixel work) and skips the flip entirely instead of
+ /// always finding "damage".
+ pub(crate) ages: [usize; 2],
/// Origin of this head in the global coordinate space.
pub(crate) location: Point<i32, Logical>,
pub(crate) size: (i32, i32),
@@ -154,6 +176,8 @@ impl CompState {
/// monitors on different refresh rates each run at their own pace
/// instead of the slowest one gating the rest.
pub(crate) fn render_udev_frame(&mut self) {
+ self.tick_animations();
+ self.tick_dirty_broadcasts();
let locked = self.lock.locked;
let elapsed = self.start_time.elapsed();
// Drained before the `&mut self.udev` borrow below, so screencopy can
@@ -161,12 +185,41 @@ impl CompState {
let mut captures = std::mem::take(&mut self.screencopy_pending);
// Same reason: the cursor needs the renderer that borrow owns.
let cursor_status = self.cursor_status.clone();
- let cursor_buffer = self.cursor_buffer.clone();
+ let cursor_buffers = self.cursor_buffers.clone();
+
+ // Border geometry is in global space, independent of which head
+ // renders it, so it's gathered once here rather than per head.
+ // Buffers are pre-built for the same reason as `cursor_buffers`:
+ // Rendered per window, front-to-back (topmost first), each window's
+ // content immediately followed by its decoration and border --
+ // fixes the same cross-window ordering bug documented in
+ // `winit.rs`'s render loop: a background window's titlebar could
+ // otherwise show through in front of the actually-focused window on
+ // top of it, since decorations/borders used to be a single flat
+ // layer drawn unconditionally above *every* window's content
+ // regardless of real stacking order. `visible_windows_front_to_back`
+ // is `WindowManager.order` reversed - not `visible_windows`, which
+ // iterates the `windows` HashMap with no ordering guarantee - the
+ // same "topmost first" convention `hit_test`/`window_at` use.
+ // Fetched once here (`&mut self` fields, id_to_window/space lookups
+ // happen per head below without needing `self` itself mutably) --
+ // see the per-head loop for why decoration/border buffers still get
+ // looked up fresh per head (head-local `origin` translation).
+ let ids: Vec<srdwm_core::WindowId> = if locked { Vec::new() } else { self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect() };
+ let focused = self.wm.borrow().focused_id();
+ let popup_targets = if locked { Vec::new() } else { crate::elements::popup_targets(self) };
// Which heads are eligible, and what each needs, gathered before the
- // mutable borrow of `self.udev`.
- let Some(udev) = self.udev.as_ref() else { return };
+ // mutable borrow of `self.udev`. Both early-outs below give the
+ // `captures` taken above nowhere to go this pass - put them back
+ // rather than silently dropping a client's pending screenshot
+ // because a VT switch happened to be in progress at that instant.
+ let Some(udev) = self.udev.as_ref() else {
+ self.screencopy_pending.extend(captures);
+ return;
+ };
if !udev.active {
+ self.screencopy_pending.extend(captures);
return;
}
let ready: Vec<(usize, Output)> = udev
@@ -176,22 +229,23 @@ impl CompState {
.filter(|(_, h)| !h.flip_pending)
.map(|(i, h)| (i, h.output.clone()))
.collect();
-
- let mut presented: Vec<Output> = Vec::new();
+ // Kept separately from `presented` below: layer-shell surfaces
+ // (bars, docks) get their frame callback every pass regardless of
+ // `has_damage`, unlike toplevel windows - see the callback loop at
+ // the end of this function for why the two can't share one gate.
+ let ready_outputs: Vec<Output> = ready.iter().map(|(_, o)| o.clone()).collect();
+
+ // Damage rects travel alongside each presented output so the
+ // frame-callback loop below (after `udev` is no longer borrowed)
+ // can notify only the windows that damage actually overlapped --
+ // see `windows_touched_by_damage`'s doc comment in elements.rs.
+ let mut presented: Vec<(Output, Vec<Rectangle<i32, Physical>>)> = Vec::new();
for (index, output) in ready {
let lock_surface = self.lock_surface_for(&output).cloned();
- // Decoration elements are built per head: `from_buffer` needs the
+ // Content/decoration elements are built per head: both need the
// renderer, and geometry is translated into head-local space.
let origin = self.udev.as_ref().map(|u| u.heads[index].location).unwrap_or_default();
- let decorations: Vec<(srdwm_core::Rect, MemoryRenderBuffer)> = if locked {
- Vec::new()
- } else {
- self.decorations
- .iter()
- .filter_map(|(&id, deco)| self.wm.borrow().window(id).map(|w| (w.geometry, deco.clone())))
- .collect()
- };
let Some(udev) = self.udev.as_mut() else { return };
let head = &mut udev.heads[index];
@@ -206,19 +260,143 @@ impl CompState {
let hsize = udev.heads[index].size;
custom_elements.extend(crate::cursor::render_elements(
&cursor_status,
- &cursor_buffer,
+ &cursor_buffers,
&mut udev.renderer,
pointer_pos,
origin,
hsize,
));
- for (geom, deco) in &decorations {
- let pos = ((geom.x - origin.x) as f64, (geom.y - origin.y) as f64);
- match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, deco, None, None, None, Kind::Unspecified) {
+ // The right-click titlebar menu, if open - pushed right
+ // after the cursor so it's still topmost over every window
+ // but never hides the pointer itself (you need to see what
+ // you're about to click).
+ if let (Some(menu), Some(buffer)) = (self.context_menu.as_ref(), self.context_menu_buffer.as_ref()) {
+ let pos = ((menu.pos.0 - origin.x) as f64, (menu.pos.1 - origin.y) as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, buffer, None, None, None, Kind::Unspecified) {
Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
- Err(e) => log::warn!("udev: failed to import titlebar buffer: {e}"),
+ Err(e) => log::warn!("udev: failed to import context menu buffer: {e}"),
}
}
+ // Content still comes from `render_output`'s own `spaces`
+ // argument (`self.space`, passed below), not from a
+ // per-window `render_elements` call here - see the long
+ // comment on the matching loop in `winit.rs`, and `state.rs`'s
+ // `resync_stacking_order` doc comment, for the real
+ // stacking-order bug this was chasing and its actual fix.
+ // Windows stacked in front of whichever one border/
+ // decoration is being built right now - `ids` is already
+ // front-to-back, so this only ever needs appending to, not
+ // recomputing. Only a window's own *content* occludes
+ // correctly on its own path (via `space`, real stacking
+ // order respected); everything drawn here goes through
+ // `custom_elements`, which composites above *all* content
+ // unconditionally, so both the border strips and the
+ // titlebar bitmap below need this explicit occlusion test
+ // against it.
+ let mut occluders: Vec<srdwm_core::Rect> = Vec::with_capacity(ids.len());
+ for &id in &ids {
+ let Some(w) = self.wm.borrow().window(id).cloned() else { continue };
+ // `w.geometry` is the animation's *target*, not
+ // necessarily where the window is actually drawn this
+ // frame - during a maximize/fullscreen/open-slide tween,
+ // `sync_geometry` already renders the window's own
+ // content at `window_anims`' interpolated rect (see its
+ // doc comment), but this loop used to read `w.geometry`
+ // straight from the model regardless, so the border and
+ // titlebar sat at the final rect while the content they
+ // were supposed to outline slid past underneath them --
+ // reported live as the border "not flush" with the
+ // window during any animated transition. Every use of
+ // this window's geometry below (titlebar/border
+ // placement *and* the occlusion test against later
+ // windows) has to agree with what `sync_geometry` mapped
+ // the content to, or they drift apart again.
+ let geom = self.window_anims.get(&id).map(crate::state::WindowAnim::current_rect).unwrap_or(w.geometry);
+ if let Some(deco) = self.decorations.get(&id) {
+ // Fragment-clipped, same as the three solid border
+ // strips below - an *all-or-nothing* version of
+ // this (skip only once fully covered) was tried
+ // first and reported live as still showing "the
+ // behind window's bar": a titlebar only *partially*
+ // covered - the common case for cascaded/
+ // overlapping windows - drew in full regardless,
+ // bleeding through the covered part. `from_buffer`'s
+ // `src` parameter crops the bitmap itself, so each
+ // visible fragment can come from the matching
+ // sub-rect of the source image rather than the
+ // whole thing.
+ let titlebar_rect = srdwm_core::Rect::new(geom.x, geom.y, geom.width, srdwm_core::TITLEBAR_HEIGHT);
+ for fragment in crate::elements::visible_border_fragments(titlebar_rect, &occluders) {
+ let pos = ((fragment.x - origin.x) as f64, (fragment.y - origin.y) as f64);
+ let src = Rectangle::new(
+ Point::from(((fragment.x - titlebar_rect.x) as f64, (fragment.y - titlebar_rect.y) as f64)),
+ Size::from((fragment.width as f64, fragment.height as f64)),
+ );
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, deco, None, Some(src), None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Err(e) => log::warn!("udev: failed to import titlebar buffer: {e}"),
+ }
+ }
+ }
+ // Border strips sit entirely outside this window's own
+ // `geometry` (see `decoration::border_strips`), so they
+ // never overlap its own decoration/content - draw
+ // order against those doesn't matter here, only against
+ // other windows', which iterating `ids` in stacking
+ // order already gets right *for windows also drawn via
+ // this same custom_elements loop* - but not against
+ // any window's own *content*, which is why `occluders`
+ // below is still needed even with that ordering.
+ if w.border_width > 0 {
+ let color = crate::state::effective_border_color(w.border_color, focused == Some(id));
+ let strips = decoration::border_strips(geom, w.border_width);
+ // Strip 0 (top) rounded to match the titlebar under
+ // it - see `render_border_top`'s doc comment - so
+ // it's a cached bitmap (rebuilt only in
+ // `redraw_decoration_buffer`, same as the titlebar
+ // itself), not rasterized fresh here every frame.
+ // Not fragment-clipped like the other three strips
+ // below - cropping a bitmap's source rect per
+ // fragment is real extra work for a strip that's
+ // only `border_width` pixels tall to begin with, so
+ // this only handles the all-or-nothing case: skip
+ // entirely once *fully* covered, accept a small
+ // residual bleed while only partially covered.
+ if strips[0].width > 0 && strips[0].height > 0 && !strips[0].subtract_all(&occluders).is_empty() {
+ if let Some(buffer) = self.border_top_decorations.get(&id) {
+ let pos = ((strips[0].x - origin.x) as f64, (strips[0].y - origin.y) as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, buffer, None, None, None, Kind::Unspecified) {
+ Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
+ Err(e) => log::warn!("udev: failed to import top border buffer: {e}"),
+ }
+ }
+ }
+ // The other three strips are persistent
+ // `SolidColorBuffer`s updated in place, not rebuilt
+ // with a fresh `Id` every frame - see
+ // `elements::border_side_render_element`'s doc
+ // comment for why that distinction is load-bearing
+ // for damage tracking, not cosmetic. Each strip is
+ // further split into whatever fragments remain
+ // visible after subtracting `occluders`, since a
+ // whole unclipped strip is exactly the bug fixed
+ // here.
+ let pool = self.border_side_buffers.entry(id).or_default();
+ let mut buf_index = 0;
+ for strip in &strips[1..] {
+ if strip.width == 0 || strip.height == 0 {
+ continue;
+ }
+ for fragment in crate::elements::visible_border_fragments(*strip, &occluders) {
+ let buf = crate::elements::border_fragment_buffer(pool, buf_index);
+ buf_index += 1;
+ custom_elements.push(crate::elements::OverlayElement::Solid(crate::elements::border_side_render_element(buf, fragment, color, (origin.x, origin.y))));
+ }
+ }
+ }
+ occluders.push(geom);
+ }
+ custom_elements.extend(crate::elements::popup_render_elements(&popup_targets, &mut udev.renderer, (origin.x, origin.y)));
}
let lock_elements = if locked {
crate::lock::lock_render_elements(lock_surface.as_ref(), &mut udev.renderer)
@@ -240,7 +418,7 @@ impl CompState {
let result = if locked {
head.damage_tracker
.render_output(&mut udev.renderer, &mut framebuffer, 0, &lock_elements, [0.0, 0.0, 0.0, 1.0])
- .map(|r| r.damage.is_some())
+ .map(|r| (r.damage.is_some(), Vec::new()))
.map_err(|e| e.to_string())
} else {
render_output(
@@ -248,24 +426,34 @@ impl CompState {
&mut udev.renderer,
&mut framebuffer,
1.0,
- 0, // always a full redraw: buffer "age" tracking isn't worth the complexity for a software-only backend
+ head.ages[back],
[&self.space],
&custom_elements,
&mut head.damage_tracker,
[0.05, 0.05, 0.08, 1.0],
)
- .map(|r| r.damage.is_some())
- // Both arms reduce to "was there damage"; the two error types
- // differ, so they are flattened to a message here.
+ .map(|r| (r.damage.is_some(), r.damage.cloned().unwrap_or_default()))
+ // Both arms reduce to "was there damage" plus the damage
+ // rects themselves; the two error types differ, so they are
+ // flattened to a message here.
.map_err(|e| e.to_string())
};
if !locked {
- crate::screencopy::service_pending(std::mem::take(&mut captures), &mut udev.renderer, &framebuffer);
+ // Only this head's own captures: `captures` holds requests
+ // for every output, and each must be read back from the
+ // framebuffer it was actually requested against, not
+ // whichever head happens to render first in this loop (a
+ // multi-monitor capture would otherwise silently read the
+ // wrong screen). Whatever doesn't match `output` stays in
+ // `captures` for a later head this same pass.
+ let (mine, rest): (Vec<_>, Vec<_>) = captures.into_iter().partition(|c| c.output == output);
+ captures = rest;
+ crate::screencopy::service_pending(mine, &mut udev.renderer, &framebuffer);
}
drop(framebuffer);
- let has_damage = match result {
- Ok(d) => d,
+ let (has_damage, damage_rects) = match result {
+ Ok(v) => v,
Err(e) => {
log::error!("udev: render_output failed: {e}");
continue;
@@ -277,24 +465,197 @@ impl CompState {
log::error!("udev: page flip failed: {e}");
continue;
}
+ // This buffer is now fully up to date. It won't be rendered
+ // into again until the *other* slot has also been presented
+ // once (strict two-buffer alternation), so by then it will
+ // be exactly 2 damage-producing renders stale - matching
+ // `damage_tracker`'s own history, which only advances on
+ // calls that actually found damage (see `ages`' doc
+ // comment).
+ head.ages[back] = 2;
+ // Only a head that actually presented a new frame should
+ // tell its windows they may render their next one - this
+ // used to run unconditionally for every "ready" head (i.e.
+ // every head not already mid-flip) on every single call to
+ // this function, which is every ~16ms regardless of
+ // activity. A client that renders on the standard
+ // wait-for-frame-callback pattern (which is most of
+ // them - confirmed live: wezterm-gui pinned at 140%+ CPU
+ // sitting on a fully idle, unchanged terminal) had no
+ // reason not to redraw at whatever rate this loop cycled,
+ // forever, since it kept getting told a new frame was
+ // wanted whether or not the screen had changed at all.
+ presented.push((output, damage_rects));
}
- presented.push(output);
}
// Frame callbacks + lock confirmation, once the `udev` borrow is done.
- for output in presented {
+ for (output, damage_rects) in presented {
if locked {
let surface = self.lock_surface_for(&output).cloned();
crate::lock::send_lock_frame(surface.as_ref(), &output, elapsed);
self.confirm_lock_if_presented(&output);
} else {
let out = output.clone();
- self.space.elements().for_each(|w| w.send_frame(&out, elapsed, None, |_, _| Some(out.clone())));
+ let scale = Scale::from(out.current_scale().fractional_scale());
+ for w in crate::elements::windows_touched_by_damage(&self.space, &damage_rects, scale) {
+ w.send_frame(&out, elapsed, None, |_, _| Some(out.clone()));
+ }
+ }
+ }
+ // Deliberately unconditional - not folded into the `presented`
+ // loop above, and not gated on any head having had damage this
+ // tick at all. The whole point of `always_notify` is covering the
+ // case where the output has *no* damage whatsoever (a fully idle
+ // desktop, cursor not moving) but the focused/hovered window still
+ // has a pending callback it needs answered to unblock an input-
+ // driven redraw - GTK's frame-clock model (Firefox's Wayland
+ // vsync source included) paces every repaint through that
+ // callback, even the first one after being idle, with no "just
+ // commit immediately" fallback. Nesting this inside the `presented`
+ // loop (the first version of this fix) meant it only ever ran on a
+ // tick that already had damage from something else happening --
+ // i.e. never in the exact scenario it exists for. Reported live as
+ // clicks in Firefox still doing nothing at all, not just
+ // intermittently, after the first version of this fix.
+ if !locked {
+ let pointer_pos = self.udev.as_ref().map(|u| u.pointer_pos).unwrap_or_default();
+ let wm = self.wm.borrow();
+ let always_notify = [wm.focused_id(), wm.window_at(pointer_pos.x as i32, pointer_pos.y as i32)];
+ drop(wm);
+ let outputs: Vec<Output> = self.outputs.iter().map(|e| e.output.clone()).collect();
+ for w in always_notify.into_iter().flatten().filter_map(|id| self.id_to_window.get(&id)) {
+ for out in &outputs {
+ w.send_frame(out, elapsed, None, |_, _| Some(out.clone()));
+ }
+ }
+ }
+ // Layer-shell surfaces (bars, docks, launchers) get their frame
+ // callback on every pass, unconditionally - NOT folded into the
+ // `presented`/`has_damage` gate above.
+ //
+ // That gate exists because most toplevel clients redraw on the
+ // standard wait-for-callback loop regardless of whether their own
+ // content changed (confirmed live: wezterm-gui pinned at 140%+ CPU
+ // on a fully idle terminal when it got a callback every ~16ms
+ // whether or not the screen had changed). Gating toplevel callbacks
+ // on real output damage fixed that.
+ //
+ // Applying the same gate to layer surfaces creates a real deadlock
+ // instead: many (GTK4/AGS among them) drive their *entire* repaint
+ // loop off frame callbacks with no independent timer fallback --
+ // paint once, request a callback, wait. If nothing ELSE on the
+ // desktop ever produces damage again (a static terminal, no other
+ // animation), that callback never arrives, so the surface can never
+ // draw its next frame, which means it can never produce damage,
+ // which means it never gets a callback - permanently frozen after
+ // exactly one frame. Confirmed live: AGS and waybar both hung this
+ // way, one frame in, with `wl_surface.frame` requests that were
+ // never answered (see docs/PANEL_SUPPORT_TODO.md).
+ //
+ // Splitting the gate is safe rather than reintroducing the wezterm
+ // bug: there are at most a handful of layer surfaces on a real
+ // desktop (a bar, maybe a dock/launcher), their content is cheap to
+ // redraw even when done needlessly, and periodic UI chrome (a
+ // clock, a resource graph) is exactly the case frame callbacks
+ // exist to pace - unlike a full toplevel window, whose redraw cost
+ // is what made the unconditional case expensive in the first place.
+ if !locked {
+ for output in &ready_outputs {
+ for layer in layer_map_for_output(output).layers() {
+ layer.send_frame(output, elapsed, None, |_, _| Some(output.clone()));
+ }
}
}
if locked {
crate::screencopy::fail_pending(captures);
+ } else if !captures.is_empty() {
+ // Left over because their target head wasn't in `ready` this
+ // pass (e.g. mid-page-flip). Put back rather than dropped: this
+ // function runs again on the next poll tick (or the page-flip
+ // completion that made the head ready), so the capture gets
+ // another chance instead of silently vanishing - which is what
+ // made `grim` hang waiting on a `ready`/`failed` that would
+ // otherwise never come (see docs/PANEL_SUPPORT_TODO.md, P1).
+ self.screencopy_pending.extend(captures);
+ }
+ }
+
+ /// Sets a connector's DPMS mode via the generic KMS "DPMS" property --
+ /// there is no dedicated legacy-API call for this in `drm-rs`, only the
+ /// same `get_properties`/`set_property` pair every other connector
+ /// property goes through, so the property has to be found by name each
+ /// time rather than through some `Dpms` -specific method. `None` if the
+ /// `wl_output` doesn't resolve to a live head, or the connector has no
+ /// "DPMS" property at all (rare on real hardware, but virtual/headless
+ /// outputs may not expose one) - either way maps to `zwlr_output_power_v1`'s
+ /// `failed` event, matching what the protocol asks for when the mode
+ /// can't be honoured.
+ pub(crate) fn set_output_power(&self, wl_output: &smithay::reexports::wayland_server::protocol::wl_output::WlOutput, on: bool) -> Option<()> {
+ // Raw KMS UAPI values for the "DPMS" connector property
+ // (`DRM_MODE_DPMS_ON`/`_OFF` in `drm_sys`/the kernel's
+ // `drm_mode.h`) - not worth a whole extra dependency on `drm-sys`
+ // just for two constants that have been stable since DPMS was
+ // added to the DRM UAPI.
+ const DRM_MODE_DPMS_ON: u64 = 0;
+ const DRM_MODE_DPMS_OFF: u64 = 3;
+
+ let target = self.output_for_wl(wl_output)?.output.clone();
+ let udev = self.udev.as_ref()?;
+ let head = udev.heads.iter().find(|h| h.output == target)?;
+ let props = udev.card.get_properties(head.connector).ok()?;
+ let dpms_prop = props.as_props_and_values().0.iter().copied().find(|&handle| udev.card.get_property(handle).is_ok_and(|info| info.name().to_str() == Ok("DPMS")))?;
+ let mode = if on { DRM_MODE_DPMS_ON } else { DRM_MODE_DPMS_OFF };
+ udev.card.set_property(head.connector, dpms_prop, mode).ok()
+ }
+
+ /// The CRTC's gamma ramp length, in elements per channel - what
+ /// `zwlr_gamma_control_v1.gamma_size` reports so a client knows how
+ /// large a table `set_gamma` expects. `None` if the output doesn't
+ /// resolve to a live head, or the CRTC reports a zero-length ramp
+ /// (no gamma hardware, common on virtual/headless outputs).
+ pub(crate) fn gamma_ramp_size(&self, wl_output: &smithay::reexports::wayland_server::protocol::wl_output::WlOutput) -> Option<u32> {
+ let target = self.output_for_wl(wl_output)?.output.clone();
+ let udev = self.udev.as_ref()?;
+ let head = udev.heads.iter().find(|h| h.output == target)?;
+ let len = udev.card.get_crtc(head.crtc).ok()?.gamma_length();
+ (len > 0).then_some(len)
+ }
+
+ /// Reads a client-supplied gamma table (`zwlr_gamma_control_v1.
+ /// set_gamma`'s `fd`: a memory-mapped blob of `gamma_size` `u16`s per
+ /// channel, red then green then blue, per the protocol) and applies it
+ /// to the CRTC. `None` on any failure - output/head not found, the
+ /// blob is the wrong size, or the DRM `set_gamma` call itself fails --
+ /// which the caller maps to `zwlr_gamma_control_v1.failed`, exactly
+ /// what the protocol specifies for "setting the gamma tables failed".
+ pub(crate) fn set_gamma_ramp(&self, wl_output: &smithay::reexports::wayland_server::protocol::wl_output::WlOutput, fd: std::os::fd::OwnedFd) -> Option<()> {
+ let target = self.output_for_wl(wl_output)?.output.clone();
+ let udev = self.udev.as_ref()?;
+ let head = udev.heads.iter().find(|h| h.output == target)?;
+ let size = udev.card.get_crtc(head.crtc).ok()?.gamma_length() as usize;
+ if size == 0 {
+ return None;
}
+ // Three channels, two bytes (one native-endian u16) per element --
+ // client and compositor are always the same machine, so there is
+ // no cross-endianness concern to handle here, unlike an over-the-
+ // wire protocol value.
+ let expected_bytes = size * 3 * 2;
+ // SAFETY: the fd is a client-supplied shared-memory blob, mapped
+ // read-only for the duration of this call and never touched again
+ // afterwards - the same trust boundary `wl_shm` buffers already
+ // cross for every window's actual pixel content elsewhere in this
+ // codebase.
+ let map = unsafe { memmap2::MmapOptions::new().map(&fd) }.ok()?;
+ if map.len() < expected_bytes {
+ return None;
+ }
+ let read_channel = |offset: usize| -> Vec<u16> { map[offset..offset + size * 2].chunks_exact(2).map(|b| u16::from_ne_bytes([b[0], b[1]])).collect() };
+ let red = read_channel(0);
+ let green = read_channel(size * 2);
+ let blue = read_channel(size * 4);
+ udev.card.set_gamma(head.crtc, &red, &green, &blue).ok()
}
}
@@ -423,17 +784,35 @@ impl UdevHead {
/// scanout memory directly, to avoid tying that image's lifetime to an
/// mmap - see this module's docs) and flips to it.
fn copy_and_flip(&mut self, card: &Card, back: usize) -> std::io::Result<()> {
- let (stride, height) = (self.buffers[back].image.stride(), self.buffers[back].image.height());
- let byte_len = stride * height;
+ let (src_stride, height) = (self.buffers[back].image.stride(), self.buffers[back].image.height());
+ let byte_len = src_stride * height;
// SAFETY: `image` owns this memory and outlives the byte slice we
// construct from it here; we only read, and only for the duration
// of this call.
let src: &[u8] = unsafe { std::slice::from_raw_parts(self.buffers[back].image.data() as *const u8, byte_len) };
+ // The dumb buffer's pitch is whatever the kernel driver actually
+ // allocated, which the DRM API does not guarantee equals pixman's
+ // own `src_stride` (drivers are free to pad each row for
+ // alignment). This used to be a single flat `copy_from_slice` sized
+ // off `src_stride` alone; on any driver that pads, that copies each
+ // source row into the wrong offset in the destination, shearing the
+ // image diagonally by one row per `dst_stride - src_stride` bytes of
+ // padding. Copying row by row, each clamped to the narrower of the
+ // two strides, is correct regardless of whether the strides happen
+ // to match.
+ let dst_stride = self.buffers[back].dumb.pitch() as usize;
{
let mut mapping = card.map_dumb_buffer(&mut self.buffers[back].dumb)?;
let dst = mapping.as_mut();
- let len = byte_len.min(dst.len());
- dst[..len].copy_from_slice(&src[..len]);
+ let row_len = src_stride.min(dst_stride);
+ for row in 0..height {
+ let s = row * src_stride;
+ let d = row * dst_stride;
+ if s + row_len > src.len() || d + row_len > dst.len() {
+ break;
+ }
+ dst[d..d + row_len].copy_from_slice(&src[s..s + row_len]);
+ }
}
card.page_flip(self.crtc, self.buffers[back].fb, PageFlipFlags::EVENT, None)?;
self.flip_pending = true;
@@ -448,6 +827,7 @@ pub struct UdevPlatform {
listener: ListeningSocket,
clients: Vec<Client>,
pending: Rc<RefCell<Vec<CoreEvent>>>,
+ ipc: Option<srdwm_platform::IpcServer>,
}
impl UdevPlatform {
@@ -476,6 +856,16 @@ impl UdevPlatform {
let renderer = PixmanRenderer::new().map_err(err)?;
let dh = Display::<CompState>::new().map_err(err)?;
let display_handle = dh.handle();
+ // `zwp_linux_dmabuf_v1` - see `protocols.rs`'s `DmabufHandler` for
+ // why `PixmanRenderer`, a pure software renderer, can still import
+ // these (mmap, not GPU). `create_global` (v3) rather than the v4
+ // `..._with_default_feedback` variant: the latter needs a
+ // `main_device` `dev_t` to steer multi-GPU clients toward the
+ // right render node, which is a real gap worth closing later but
+ // not required for a single-GPU client to allocate and hand over a
+ // Linear-modifier buffer, which is all this backend can use anyway.
+ let mut dmabuf_state = DmabufState::new();
+ dmabuf_state.create_global::<CompState>(&display_handle, renderer.dmabuf_formats());
let mut heads: Vec<UdevHead> = Vec::new();
let mut output_entries: Vec<crate::state::OutputEntry> = Vec::new();
@@ -514,7 +904,17 @@ impl UdevPlatform {
DataControlState::new::<CompState, _>(&display_handle, Some(&primary_selection_state), |_| true);
let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, "seat0");
- seat.add_keyboard(Default::default(), 200, 25).map_err(err)?;
+ let system_xkb = crate::xkb_config::read();
+ let xkb_config = smithay::input::keyboard::XkbConfig {
+ rules: "",
+ model: system_xkb.model.as_deref().unwrap_or(""),
+ layout: system_xkb.layout.as_deref().unwrap_or(""),
+ variant: system_xkb.variant.as_deref().unwrap_or(""),
+ options: system_xkb.options.clone(),
+ };
+ // 600ms delay, not 200 - see `state.rs`'s `REPEAT_DELAY` doc
+ // comment for why.
+ seat.add_keyboard(xkb_config, 600, 25).map_err(err)?;
seat.add_pointer();
// Each output occupies its own slice of the global space, so a
@@ -538,9 +938,15 @@ impl UdevPlatform {
xdg_shell_state,
_xdg_decoration_state: xdg_decoration_state,
shm_state,
+ dmabuf_state,
+ xdg_activation_state: XdgActivationState::new::<CompState>(&display_handle),
+ _text_input_manager_state: smithay::wayland::text_input::TextInputManagerState::new::<CompState>(&display_handle),
+ _input_method_manager_state: smithay::wayland::input_method::InputMethodManagerState::new::<CompState, _>(&display_handle, |_client| true),
+ _gtk_shell_state: crate::gtk_shell::GtkShellState::new::<CompState>(&display_handle),
seat_state,
seat,
space,
+ popups: PopupManager::default(),
outputs: output_entries,
layer_shell_state: smithay::wayland::shell::wlr_layer::WlrLayerShellState::new::<CompState>(&display_handle),
dh: display_handle.clone(),
@@ -553,14 +959,45 @@ impl UdevPlatform {
),
_screencopy_state: crate::screencopy::ScreencopyState::new::<CompState>(&display_handle),
screencopy_pending: Vec::new(),
+ _foreign_toplevel_state: crate::foreign_toplevel::ForeignToplevelState::new::<CompState>(&display_handle),
+ foreign_toplevel_managers: Vec::new(),
+ foreign_toplevel_handles: HashMap::new(),
+ _workspace_state: crate::workspace::WorkspaceManagerState::new::<CompState>(&display_handle),
+ _output_power_state: Some(crate::output_power::OutputPowerManagerState::new::<CompState>(&display_handle)),
+ _gamma_control_state: Some(crate::gamma_control::GammaControlManagerState::new::<CompState>(&display_handle)),
+ _output_management_state: crate::output_management::OutputManagementState::new::<CompState>(&display_handle),
+ output_managers: Vec::new(),
+ output_heads: HashMap::new(),
+ output_modes: HashMap::new(),
+ output_serial: 0,
+ last_broadcast_outputs: Vec::new(),
+ workspace_managers: Vec::new(),
+ workspace_groups: Vec::new(),
+ workspace_handles: HashMap::new(),
+ _viewporter_state: smithay::wayland::viewporter::ViewporterState::new::<CompState>(&display_handle),
+ _fractional_scale_state: smithay::wayland::fractional_scale::FractionalScaleManagerState::new::<CompState>(&display_handle),
+ _cursor_shape_state: smithay::wayland::cursor_shape::CursorShapeManagerState::new::<CompState>(&display_handle),
+ idle_notifier_state: smithay::wayland::idle_notify::IdleNotifierState::new(&display_handle, event_loop.handle()),
+ _idle_inhibit_manager_state: smithay::wayland::idle_inhibit::IdleInhibitManagerState::new::<CompState>(&display_handle),
+ idle_inhibiting_surfaces: Vec::new(),
+ last_idle_notify: None,
+ window_anims: HashMap::new(),
+ last_broadcast_flags: HashMap::new(),
+ last_broadcast_workspace: None,
lock: Default::default(),
cursor_status: smithay::input::pointer::CursorImageStatus::default_named(),
- cursor_buffer: crate::cursor::make_buffer(),
+ cursor_buffers: crate::cursor::make_buffers(),
last_titlebar_click: None,
+ context_menu: None,
+ context_menu_buffer: None,
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),
+ dead_layer_surfaces: HashSet::new(),
decorations: HashMap::new(),
+ border_top_decorations: HashMap::new(),
+ border_side_buffers: HashMap::new(),
+ last_synced_size: HashMap::new(),
pending: pending.clone(),
bound_keys: Rc::new(bound_keys.iter().cloned().collect::<HashSet<_>>()),
repeat_keys: Rc::new(repeat_keys.iter().cloned().collect::<HashSet<_>>()),
@@ -571,6 +1008,7 @@ impl UdevPlatform {
xwm: None,
xwayland_windows: HashMap::new(),
xwayland_pending: Vec::new(),
+ ewmh: None,
};
let listener = ListeningSocket::bind_auto("wayland", 0..32).map_err(err)?;
@@ -578,6 +1016,28 @@ impl UdevPlatform {
std::env::set_var("WAYLAND_DISPLAY", name);
log::info!("wayland socket: {}", name.to_string_lossy());
}
+ // Otherwise this is whatever the session inherited - typically
+ // stale from a *previous* login's compositor (a shell's exported
+ // `XDG_CURRENT_DESKTOP=Hyprland` surviving into this one), since
+ // nothing else ever sets it. `xdg-desktop-portal` and any client
+ // that sniffs this value to pick a desktop-specific integration
+ // (screenshot/file-picker backends, etc.) get actively misrouted by
+ // the stale value rather than just seeing "unknown". Only affects
+ // processes spawned from here on (autostart, `srd.spawn`) - an
+ // env var set mid-process doesn't retroactively reach anything
+ // already running.
+ std::env::set_var("XDG_CURRENT_DESKTOP", "srdwm");
+
+ let ipc = match listener.socket_name().map(|n| n.to_string_lossy().into_owned()) {
+ Some(name) => match srdwm_platform::IpcServer::bind(&name) {
+ Ok(ipc) => Some(ipc),
+ Err(e) => {
+ log::warn!("control socket unavailable ({e}); srd and scripts that use it won't work");
+ None
+ }
+ },
+ None => None,
+ };
let handle = event_loop.handle();
register_drm_fd(&handle, &card)?;
@@ -590,7 +1050,7 @@ impl UdevPlatform {
log::warn!("XWayland unavailable ({e}); X11-only clients will not run");
}
- Ok(Self { event_loop, display: dh, state, listener, clients: Vec::new(), pending })
+ Ok(Self { event_loop, display: dh, state, listener, clients: Vec::new(), pending, ipc })
}
fn accept_clients(&mut self) -> PlatformResult<()> {
@@ -630,9 +1090,18 @@ fn bring_up_head(
// Named after the real connector (eDP-1, HDMI-A-1, ...) so clients and
// the user can tell monitors apart; `wl_output.name` is what a bar's
// per-monitor config keys off.
+ //
+ // Physical size in millimeters comes straight from EDID via the
+ // connector, not the hardcoded (0, 0) this used to be - some clients
+ // compute their own effective DPI from it (independently of the
+ // compositor's own scale factor, which srdwm always reports as 1), so
+ // reporting "no physical size at all" was live, wrong data reaching
+ // every client, not just an unfilled-in placeholder.
+ let (phys_w, phys_h) = probe.info.size().unwrap_or((0, 0));
+ let physical_mm = (phys_w as i32, phys_h as i32);
let output = Output::new(
probe.name.clone(),
- PhysicalProperties { size: (0, 0).into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "drm".into() },
+ PhysicalProperties { size: physical_mm.into(), subpixel: Subpixel::Unknown, make: "srdwm".into(), model: "drm".into() },
);
let mode = OutputMode { size: (width, height).into(), refresh: mode_refresh_mhz(&probe.mode) };
output.change_current_state(Some(mode), Some(Transform::Normal), None, Some((x_offset, 0).into()));
@@ -649,6 +1118,7 @@ fn bring_up_head(
buffers,
front: 0,
flip_pending: false,
+ ages: [0, 0],
location,
size: (width, height),
};
@@ -798,8 +1268,10 @@ fn register_session_notifier(handle: &LoopHandle<'static, CompState>, notifier:
log::warn!("udev: failed to reassert crtc on resume: {e}");
}
// Force a full repaint: contents are undefined after
- // the VT switch.
+ // the VT switch (another VT's session may have
+ // scanned out something else entirely in between).
head.flip_pending = false;
+ head.ages = [0, 0];
}
data.render_udev_frame();
}
@@ -881,8 +1353,35 @@ fn handle_libinput_event(state: &mut CompState, event: InputEvent<LibinputInputB
let source = event.source();
let mut frame = AxisFrame::new(event.time_msec()).source(source);
for axis in [Axis::Horizontal, Axis::Vertical] {
- if let Some(value) = event.amount(axis) {
- frame = frame.value(axis, value);
+ match event.amount(axis) {
+ Some(value) => frame = frame.value(axis, value),
+ // `AxisSource::Finger` (a touchpad) *requires* a stop
+ // event on the frame where the finger lifts and the
+ // axis genuinely has no more motion - see `AxisFrame::
+ // source`'s own doc comment ("Using AxisSource::Finger
+ // requires a stop event to be sent, when the user lifts
+ // off the finger"). Never sending it left every
+ // two-finger scroll gesture with no way to tell Firefox/
+ // GTK it had actually ended, which is exactly the kind
+ // of thing that reads as "scrolling doesn't work" --
+ // not "no events arrive" (discrete wheel scrolling,
+ // which needs no stop event, was never affected) but
+ // kinetic/momentum scrolling and starting a fresh
+ // gesture right after a previous one never settling.
+ None if source == AxisSource::Finger => frame = frame.stop(axis),
+ None => {}
+ }
+ // Discrete wheel steps, additional to the pixel `value`
+ // above - optional (`value` is the only event a client
+ // strictly needs), but some clients use it to distinguish
+ // "one physical click" from a smooth/high-resolution
+ // scroll, so provide it whenever the device actually
+ // reports one (real scroll wheels; never touchpads, which
+ // have no discrete steps to report - `amount_v120` is
+ // `None` for those, same guarantee `amount` gives the
+ // other way around).
+ if let Some(v120) = event.amount_v120(axis) {
+ frame = frame.v120(axis, v120 as i32);
}
}
pointer.axis(state, frame);
@@ -904,6 +1403,11 @@ impl Platform for UdevPlatform {
self.state.tick_repeat();
self.display.dispatch_clients(&mut self.state).map_err(err)?;
self.display.flush_clients().map_err(err)?;
+ if let Some(ipc) = self.ipc.as_mut() {
+ if ipc.poll(&self.state.wm) {
+ self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged);
+ }
+ }
self.state.render_udev_frame();
Ok(self.pending.borrow_mut().drain(..).collect())
}
@@ -919,9 +1423,37 @@ impl Platform for UdevPlatform {
.iter()
.enumerate()
.map(|(i, head)| {
- let (w, h) = head.size;
- let rect = srdwm_core::Rect::new(head.location.x, head.location.y, w as u32, h as u32);
+ // Shrunk by whatever a layer-shell surface (bar, dock) has
+ // reserved via `set_exclusive_zone` - reporting the full
+ // head size here otherwise means core's placement/tiling
+ // treats that strip as ordinary free space, so a new
+ // window's titlebar lands right where the bar renders on
+ // top of it, unreachable to drag. `non_exclusive_zone()` is
+ // output-local, so it's translated into this head's
+ // position in the shared global space the same way
+ // `head.location` already is.
+ let zone = layer_map_for_output(&head.output).non_exclusive_zone();
+ let rect = srdwm_core::Rect::new(
+ head.location.x + zone.loc.x,
+ head.location.y + zone.loc.y,
+ zone.size.w as u32,
+ zone.size.h as u32,
+ );
let mut m = srdwm_core::Monitor::new(i as u32, head.output.name(), rect);
+ // `Monitor::new` defaults `full_geometry` to whatever
+ // `geometry` was constructed with - correct for a monitor
+ // with no layer-shell client at all, wrong the moment one
+ // exists, since `rect` above is already zone-shrunk. Without
+ // this, `full_geometry` was silently identical to `geometry`
+ // for every real monitor this backend ever reported, which
+ // made `toggle_fullscreen`'s whole "ignore the reserved
+ // zone" design a no-op in practice: fullscreen still
+ // stopped at the bar/dock exactly like maximize does.
+ // Reported live as "fullscreen isn't actually going
+ // fullscreen" - confirmed by triggering it and reading
+ // the resulting geometry back over IPC, not just from
+ // reading this code.
+ m.full_geometry = srdwm_core::Rect::new(head.location.x, head.location.y, head.size.0 as u32, head.size.1 as u32);
m.primary = i == 0;
m
})
@@ -937,8 +1469,18 @@ impl Platform for UdevPlatform {
Ok(())
}
+ /// Was `wm.focus_window(window)` alone - core-only, so a caller that
+ /// only has `Platform` to go through (`crates/platform`'s `IpcServer`,
+ /// which can't reach `CompState`/real Wayland focus at all) could make
+ /// a window *look* focused (rendering already reads live core state
+ /// for the highlighted-border/titlebar-text colour) without it ever
+ /// actually receiving a keystroke - confirmed live: `srd dispatch
+ /// focus <xwayland-window-id>` changed core's own focused-window
+ /// bookkeeping but left `_NET_ACTIVE_WINDOW` at `0x0` and real
+ /// keyboard input going nowhere. `crate::input::focus_window` is the
+ /// same full path a real mouse click already goes through.
fn focus(&mut self, window: srdwm_core::WindowId) -> PlatformResult<()> {
- self.state.wm.borrow_mut().focus_window(window);
+ crate::input::focus_window(&mut self.state, window);
Ok(())
}