diff options
| author | srdusr <[email protected]> | 2024-05-29 14:40:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-29 14:40:00 +0200 |
| commit | 3d3057ae384ef7389284af8988410889e99c6bb9 (patch) | |
| tree | 57b05e9660afd8ed51b26e5db1ab541a811ccc16 /crates/wayland/src | |
| parent | dd31bf5ac2a692617d478478d36c733586b93cf8 (diff) | |
| download | srdwm-3d3057ae384ef7389284af8988410889e99c6bb9.tar.gz srdwm-3d3057ae384ef7389284af8988410889e99c6bb9.zip | |
Config at ~/.config/srd; cursor shapes, key repeat, pin, mouse defaults
Config path drops a level: ~/.config/srd, not ~/.config/srdwm/srd, which
said the same thing twice. No other user-facing path had the same problem --
srdwm reads the config dir and writes nothing else.
Cursor shapes. A client's own cursor surface is now rendered with the
hotspot it declared, so an I-beam over text or a hand over a link shows the
app's image instead of srdwm's arrow. The built-in arrow stays as the
fallback when no client has set one, over decorations and the desktop.
Named shapes still fall back to the arrow; most toolkits set a surface.
Decorations and cursors now share one OverlayElement type, since
render_output takes a single custom-element slice.
Key repeat (srd.bind_repeat, Hyprland's binde). Held volume, brightness and
switcher keys repeat at the seat's own rate rather than firing once. Driven
from the poll loop, not a timer source: the winit backend has no calloop
loop of its own, and poll_events already runs continuously in both backends.
Repeat stops when *that* key is released, not when any key is.
Always-on-top / pin, for the picture-in-picture and HUD rules that used it.
Window::always_on_top was another declared-but-never-read field. Enforced in
WindowManager's stacking order rather than at render time, so every consumer
of stacking_order gets it and none can forget to honour it.
Mouse-only window management, checked end to end: drag the titlebar to move,
drag any edge or corner to resize, titlebar buttons to close/maximise/
minimise, click to focus, drag to a screen edge to snap, and now
double-click the titlebar to maximise. The resize grab band went from 6px to
10px - a hairline is genuinely hard to hit with a mouse, which is why
Hyprland ships extend_border_grab_area.
Also removed the emoji status markers from docs/IMPLEMENTATION_STATUS.md.
Diffstat (limited to 'crates/wayland/src')
| -rw-r--r-- | crates/wayland/src/cursor.rs | 59 | ||||
| -rw-r--r-- | crates/wayland/src/elements.rs | 20 | ||||
| -rw-r--r-- | crates/wayland/src/input.rs | 26 | ||||
| -rw-r--r-- | crates/wayland/src/lib.rs | 7 | ||||
| -rw-r--r-- | crates/wayland/src/state.rs | 94 | ||||
| -rw-r--r-- | crates/wayland/src/udev.rs | 11 | ||||
| -rw-r--r-- | crates/wayland/src/winit.rs | 15 |
7 files changed, 203 insertions, 29 deletions
diff --git a/crates/wayland/src/cursor.rs b/crates/wayland/src/cursor.rs index 121e8e1..b489c03 100644 --- a/crates/wayland/src/cursor.rs +++ b/crates/wayland/src/cursor.rs @@ -7,13 +7,19 @@ //! can move it, click with it, and drag windows with it, but you cannot see //! where it is. That makes the udev backend unusable as a real session. //! -//! **Scope, stated plainly:** this draws one built-in arrow, always. It -//! honours `CursorImageStatus::Hidden` (so a client that hides the pointer -//! still gets its way), but it does *not* yet render a client's own cursor -//! surface or a named shape - an app asking for an I-beam or a resize arrow -//! still sees this arrow. That is a real limitation, and a visible one over -//! text fields; it is also strictly better than the previous behaviour of -//! drawing nothing at all. +//! Two sources, in priority order: +//! +//! 1. **The client's own cursor surface** (`CursorImageStatus::Surface`) -- +//! a terminal's I-beam, a browser's hand, an app's resize arrows. Drawn +//! from its surface tree, offset by the hotspot the client declared. +//! 2. **A built-in arrow**, for when no client has set an image (over +//! srdwm's own decorations and the desktop) or asked for a named shape +//! we have no art for. +//! +//! `CursorImageStatus::Hidden` is honoured, so a client that hides the +//! pointer still gets its way. Named shapes (`CursorIcon::Text` etc.) fall +//! back to the arrow rather than being drawn as the requested shape - most +//! toolkits set a surface rather than a name, so this is rarely visible. //! //! The built-in arrow is deliberate rather than loading an XCursor theme: //! theme loading pulls in a dependency, needs a theme to actually be @@ -21,8 +27,11 @@ //! is always present beats a prettier one that sometimes isn't there - the //! same reasoning as `decoration.rs`'s font fallback. +use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; use smithay::utils::{Logical, Point}; +use crate::elements::OverlayElement; + /// Side length of the built-in cursor bitmap, in pixels. pub(crate) const CURSOR_SIZE: i32 = 24; @@ -85,6 +94,10 @@ pub(crate) fn arrow_bitmap() -> Vec<u8> { } +/// One cursor render element, whatever the source. +/// +/// Client cursor surfaces and the built-in bitmap are different element +/// types, so they're unified here rather than forcing both through one. /// Render elements for the pointer, to be drawn above everything else. /// /// `pos` is in the global space and `origin` is the output's origin, since @@ -99,14 +112,17 @@ pub(crate) fn render_elements<R>( pos: Point<f64, Logical>, origin: Point<i32, Logical>, size: (i32, i32), -) -> Vec<smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement<R>> +) -> Vec<OverlayElement<R>> where - R: smithay::backend::renderer::Renderer + smithay::backend::renderer::ImportMem, + R: smithay::backend::renderer::Renderer + + smithay::backend::renderer::ImportAll + + smithay::backend::renderer::ImportMem, R::TextureId: Clone + Send + 'static, { - use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; + use smithay::backend::renderer::element::surface::render_elements_from_surface_tree; use smithay::backend::renderer::element::Kind; - use smithay::input::pointer::CursorImageStatus; + use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData}; + use smithay::wayland::compositor::with_states; if matches!(status, CursorImageStatus::Hidden) { return Vec::new(); @@ -117,10 +133,26 @@ where return Vec::new(); } - // Built-in arrow: hotspot is the tip, so no offset. + if let CursorImageStatus::Surface(surface) = status { + // The client picked an image. Its hotspot is the point *inside* that + // image which tracks the pointer, so the surface is drawn offset by + // it - without this the image sits down-right of where clicks land. + let hotspot = with_states(surface, |states| { + states + .data_map + .get::<CursorImageSurfaceData>() + .map(|d| d.lock().unwrap().hotspot) + .unwrap_or_default() + }); + let at = (local.0 - hotspot.x, local.1 - hotspot.y); + return render_elements_from_surface_tree(renderer, surface, at, 1.0, 1.0, Kind::Cursor); + } + + // No client image (or a named shape we don't have art for): the + // built-in arrow, whose hotspot is its tip, so no offset. let at = (local.0 as f64, local.1 as f64); match MemoryRenderBufferRenderElement::from_buffer(renderer, at, buffer, None, None, None, Kind::Cursor) { - Ok(e) => vec![e], + Ok(e) => vec![OverlayElement::Memory(e)], Err(e) => { log::warn!("cursor: failed to import bitmap: {e}"); Vec::new() @@ -128,6 +160,7 @@ where } } + /// The built-in arrow as an uploadable buffer. Built once at startup rather /// than per frame - the bitmap never changes. pub(crate) fn make_buffer() -> smithay::backend::renderer::element::memory::MemoryRenderBuffer { diff --git a/crates/wayland/src/elements.rs b/crates/wayland/src/elements.rs new file mode 100644 index 0000000..f1fbfee --- /dev/null +++ b/crates/wayland/src/elements.rs @@ -0,0 +1,20 @@ +//! The one render-element type for everything srdwm draws *itself*, on top +//! of client windows: its titlebars and the mouse pointer. +//! +//! `render_output` takes a single `custom_elements` slice, so these have to +//! be one type. They come from two different sources - an uploaded bitmap +//! (titlebars, the built-in cursor arrow) and a client's own surface (a +//! client-set cursor image) - hence the two variants. + +use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; +use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; +use smithay::backend::renderer::{ImportAll, ImportMem}; + +smithay::backend::renderer::element::render_elements! { + pub(crate) OverlayElement<R> where + R: ImportAll + ImportMem; + /// A client's own surface, used for client-set cursor images. + Surface=WaylandSurfaceRenderElement<R>, + /// A bitmap srdwm rasterised: a titlebar, or the built-in cursor arrow. + Memory=MemoryRenderBufferRenderElement<R>, +} diff --git a/crates/wayland/src/input.rs b/crates/wayland/src/input.rs index 6959c8f..17e94d4 100644 --- a/crates/wayland/src/input.rs +++ b/crates/wayland/src/input.rs @@ -206,7 +206,17 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic if let Some((id, hit)) = hit { focus_window(state, id); match hit { - TitlebarHit::Drag => state.wm.borrow_mut().start_drag(id, pos.x as i32, pos.y as i32), + TitlebarHit::Drag => { + // Double-click the titlebar to maximise, as every other + // desktop does - one of the few window operations that + // otherwise needs the keyboard or a precise button hit. + if state.is_double_click(id, time) { + state.wm.borrow_mut().toggle_maximize(id); + state.sync_geometry(id); + } else { + state.wm.borrow_mut().start_drag(id, pos.x as i32, pos.y as i32) + } + } TitlebarHit::Close => { if let Some(w) = state.id_to_window.get(&id) { close_dwindow(w); @@ -223,6 +233,8 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic if let Some((window, _loc)) = state.space.element_under(pos) { let window = window.clone(); state.space.raise_element(&window, true); + // Clicking a normal window must not bury a pinned one. + state.raise_pinned(); if let Some(&id) = dwindow_wl_surface(&window).and_then(|s| state.surface_to_id.get(&s)) { focus_window(state, id); } @@ -276,10 +288,16 @@ pub(crate) fn handle_keyboard_key_event<B: smithay::backend::input::InputBackend } }); - if key_state == BackendKeyState::Pressed { - if let Some((key_name, modifiers)) = matched { - state.pending.borrow_mut().push(CoreEvent::KeyPress { key_name, modifiers }); + match key_state { + BackendKeyState::Pressed => { + if let Some((key_name, modifiers)) = matched { + state.begin_repeat(keycode, &key_name, modifiers); + state.pending.borrow_mut().push(CoreEvent::KeyPress { key_name, modifiers }); + } } + // Any release ends a repeat of *that* key; releasing an unrelated + // key must not stop it. + BackendKeyState::Released => state.end_repeat(keycode), } // Unmatched keys were already forwarded to the focused client by // `FilterResult::Forward` inside the closure above. diff --git a/crates/wayland/src/lib.rs b/crates/wayland/src/lib.rs index 001c803..1d04d01 100644 --- a/crates/wayland/src/lib.rs +++ b/crates/wayland/src/lib.rs @@ -38,6 +38,7 @@ mod cursor; mod decoration; +mod elements; mod input; mod lock; mod protocols; @@ -67,13 +68,13 @@ pub(crate) fn err(e: impl std::fmt::Display) -> PlatformError { /// nested-vs-native. Falls back to winit if udev initialization fails for /// any reason (no seat access, no DRM device, ...), logging why rather than /// failing outright. -pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String]) -> PlatformResult<Box<dyn Platform>> { +pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String], repeat_keys: &[String]) -> PlatformResult<Box<dyn Platform>> { let no_host_display = std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none(); if no_host_display { - match udev::UdevPlatform::connect(wm.clone(), bound_keys) { + match udev::UdevPlatform::connect(wm.clone(), bound_keys, repeat_keys) { Ok(platform) => return Ok(Box::new(platform)), Err(e) => log::warn!("udev/DRM backend unavailable ({e}); falling back to nested winit backend"), } } - Ok(Box::new(WaylandPlatform::connect(wm, bound_keys)?)) + Ok(Box::new(WaylandPlatform::connect(wm, bound_keys, repeat_keys)?)) } diff --git a/crates/wayland/src/state.rs b/crates/wayland/src/state.rs index a1704cb..bea3453 100644 --- a/crates/wayland/src/state.rs +++ b/crates/wayland/src/state.rs @@ -14,7 +14,7 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; -use std::time::Instant; +use std::time::{Duration, Instant}; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::memory::MemoryRenderBuffer; @@ -127,12 +127,18 @@ pub(crate) struct CompState { /// Bitmap for the built-in arrow, built once at startup rather than /// per frame. pub(crate) cursor_buffer: MemoryRenderBuffer, + /// Last titlebar press, for double-click detection. + pub(crate) last_titlebar_click: Option<(WindowId, u32)>, pub(crate) wm: Rc<RefCell<WindowManager>>, pub(crate) surface_to_id: HashMap<WlSurface, WindowId>, pub(crate) id_to_window: HashMap<WindowId, DWindow>, pub(crate) decorations: HashMap<WindowId, MemoryRenderBuffer>, pub(crate) pending: Rc<RefCell<Vec<CoreEvent>>>, pub(crate) bound_keys: Rc<HashSet<String>>, + /// Combos that repeat while held (`srd.bind_repeat`). + pub(crate) repeat_keys: Rc<HashSet<String>>, + /// The binding currently held down and repeating, if any. + pub(crate) repeat: Option<RepeatState>, pub(crate) start_time: Instant, /// `Some` only for the udev/DRM backend; see `udev.rs` module docs for /// why its runtime state lives here rather than on a separate struct. @@ -147,6 +153,61 @@ pub(crate) struct CompState { pub(crate) xwayland_pending: Vec<smithay::xwayland::X11Surface>, } +/// A held keybinding that is firing repeatedly. +/// +/// Driven from the poll loop rather than a timer source: the winit backend +/// has no `calloop` loop of its own, and `poll_events` already runs +/// continuously in both backends, so this works the same in each. +pub(crate) struct RepeatState { + /// Which physical key is held - repeat stops when *this* key is + /// released, not when any key is. + pub(crate) keycode: smithay::input::keyboard::Keycode, + pub(crate) key_name: String, + pub(crate) modifiers: srdwm_core::Modifiers, + pub(crate) next_fire: Instant, +} + +/// Matches the seat's own repeat settings (`add_keyboard(.., 200, 25)`), so +/// held bindings feel the same as held keys in a text field. +const REPEAT_DELAY: Duration = Duration::from_millis(200); +const REPEAT_INTERVAL: Duration = Duration::from_millis(1000 / 25); + +impl CompState { + /// Starts repeating `combo` if it was registered with `srd.bind_repeat`. + pub(crate) fn begin_repeat(&mut self, keycode: smithay::input::keyboard::Keycode, key_name: &str, modifiers: srdwm_core::Modifiers) { + let combo = srdwm_core::key_combo_string(modifiers, key_name); + if !self.repeat_keys.contains(&combo) { + return; + } + self.repeat = Some(RepeatState { + keycode, + key_name: key_name.to_string(), + modifiers, + next_fire: Instant::now() + REPEAT_DELAY, + }); + } + + /// Stops repeating when the held key is released. + pub(crate) fn end_repeat(&mut self, keycode: smithay::input::keyboard::Keycode) { + if self.repeat.as_ref().is_some_and(|r| r.keycode == keycode) { + self.repeat = None; + } + } + + /// Emits another `KeyPress` if the held binding is due. Called once per + /// poll from both backends. + pub(crate) fn tick_repeat(&mut self) { + let Some(repeat) = self.repeat.as_mut() else { return }; + let now = Instant::now(); + if now < repeat.next_fire { + return; + } + repeat.next_fire = now + REPEAT_INTERVAL; + let (key_name, modifiers) = (repeat.key_name.clone(), repeat.modifiers); + self.pending.borrow_mut().push(CoreEvent::KeyPress { key_name, modifiers }); + } +} + /// Titlebar background is the same regardless of focus (matching the X11 /// backend); only the title text color changes. const TITLEBAR_BG: (u8, u8, u8) = (0x2e, 0x34, 0x40); @@ -219,6 +280,8 @@ impl CompState { // 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)); } @@ -339,6 +402,35 @@ impl CompState { keyboard.set_focus(self, surface, serial); } + /// True when this titlebar press is the second of a double-click on the + /// same window. Threshold is the usual 400ms. + pub(crate) fn is_double_click(&mut self, id: WindowId, time: u32) -> bool { + const DOUBLE_CLICK_MS: u32 = 400; + let doubled = match self.last_titlebar_click { + Some((last_id, last_time)) => last_id == id && time.saturating_sub(last_time) <= DOUBLE_CLICK_MS, + None => false, + }; + // Reset after a double, so a third click starts a fresh pair rather + // than counting as another double. + self.last_titlebar_click = if doubled { None } else { Some((id, time)) }; + doubled + } + + /// Re-raises always-on-top windows in the `Space`. + /// + /// `WindowManager` keeps pinned windows last in its own stacking order, + /// but the `Space` has an order of its own that decides what actually + /// draws on top - so pinning is only real once it is pushed here. + /// Called after anything that raises a window. + pub(crate) fn raise_pinned(&mut self) { + let pinned: Vec<WindowId> = self.wm.borrow().stacking_order().filter(|w| w.always_on_top).map(|w| w.id).collect(); + for id in pinned { + if let Some(w) = self.id_to_window.get(&id).cloned() { + self.space.raise_element(&w, false); + } + } + } + pub(crate) fn sync_geometry(&mut self, id: WindowId) { let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { return }; if let Some(w) = self.id_to_window.get(&id) { diff --git a/crates/wayland/src/udev.rs b/crates/wayland/src/udev.rs index 30517f8..95ef571 100644 --- a/crates/wayland/src/udev.rs +++ b/crates/wayland/src/udev.rs @@ -197,7 +197,7 @@ impl CompState { let head = &mut udev.heads[index]; let back = 1 - head.front; - let mut custom_elements: Vec<MemoryRenderBufferRenderElement<PixmanRenderer>> = Vec::new(); + let mut custom_elements: Vec<crate::elements::OverlayElement<PixmanRenderer>> = Vec::new(); if !locked { // Cursor first: `render_output` draws custom elements // front-to-back, so the earliest element is topmost. On a @@ -215,7 +215,7 @@ impl CompState { 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) { - Ok(elem) => custom_elements.push(elem), + Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)), Err(e) => log::warn!("udev: failed to import titlebar buffer: {e}"), } } @@ -451,7 +451,7 @@ pub struct UdevPlatform { } impl UdevPlatform { - pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String]) -> PlatformResult<Self> { + pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String], repeat_keys: &[String]) -> PlatformResult<Self> { let event_loop: EventLoop<'static, CompState> = EventLoop::try_new().map_err(err)?; let (session, notifier) = LibSeatSession::new().map_err(err)?; @@ -556,12 +556,15 @@ impl UdevPlatform { lock: Default::default(), cursor_status: smithay::input::pointer::CursorImageStatus::default_named(), cursor_buffer: crate::cursor::make_buffer(), + last_titlebar_click: None, wm: wm.clone(), surface_to_id: HashMap::new(), id_to_window: HashMap::new(), decorations: 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<_>>()), + repeat: None, start_time: Instant::now(), udev: Some(udev_state), xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::<CompState>(&display_handle), @@ -897,6 +900,8 @@ impl Platform for UdevPlatform { fn poll_events(&mut self) -> PlatformResult<Vec<CoreEvent>> { self.accept_clients()?; self.event_loop.dispatch(Some(Duration::from_millis(16)), &mut self.state).map_err(err)?; + // Held bindings that repeat - see `CompState::tick_repeat`. + self.state.tick_repeat(); self.display.dispatch_clients(&mut self.state).map_err(err)?; self.display.flush_clients().map_err(err)?; self.state.render_udev_frame(); diff --git a/crates/wayland/src/winit.rs b/crates/wayland/src/winit.rs index b21ca06..a11e263 100644 --- a/crates/wayland/src/winit.rs +++ b/crates/wayland/src/winit.rs @@ -64,7 +64,7 @@ impl WaylandPlatform { /// strings (see `srdwm_core::key_combo_string`) - the same set the X11 /// backend grabs individually via `XGrabKey`. Only a keypress matching /// one of these is withheld from the focused client. - pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String]) -> PlatformResult<Self> { + pub fn connect(wm: Rc<RefCell<WindowManager>>, bound_keys: &[String], repeat_keys: &[String]) -> PlatformResult<Self> { let display: Display<CompState> = Display::new().map_err(err)?; let dh = display.handle(); @@ -132,12 +132,15 @@ impl WaylandPlatform { lock: SessionLock::default(), cursor_status: smithay::input::pointer::CursorImageStatus::default_named(), cursor_buffer: crate::cursor::make_buffer(), + last_titlebar_click: None, wm: wm.clone(), surface_to_id: HashMap::new(), id_to_window: HashMap::new(), decorations: HashMap::new(), pending: pending.clone(), bound_keys: Rc::new(bound_keys.iter().cloned().collect()), + repeat_keys: Rc::new(repeat_keys.iter().cloned().collect()), + repeat: None, start_time: Instant::now(), udev: None, xwayland_shell_state: smithay::wayland::xwayland_shell::XWaylandShellState::new::<CompState>(&dh), @@ -212,11 +215,11 @@ impl WaylandPlatform { return Ok(()); } - let mut custom_elements: Vec<MemoryRenderBufferRenderElement<GlesRenderer>> = Vec::new(); + let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new(); for (&id, deco) in self.state.decorations.iter() { let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue }; match MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) { - Ok(elem) => custom_elements.push(elem), + Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)), Err(e) => log::warn!("failed to import titlebar buffer for window {id}: {e}"), } } @@ -274,11 +277,11 @@ impl WaylandPlatform { renderer.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()).map_err(err)?; let mut framebuffer = renderer.bind(&mut target).map_err(err)?; - let mut custom_elements: Vec<MemoryRenderBufferRenderElement<GlesRenderer>> = Vec::new(); + let mut custom_elements: Vec<crate::elements::OverlayElement<GlesRenderer>> = Vec::new(); for (&id, deco) in self.state.decorations.iter() { let Some(geom) = self.wm.borrow().window(id).map(|w| w.geometry) else { continue }; if let Ok(elem) = MemoryRenderBufferRenderElement::from_buffer(renderer, (geom.x as f64, geom.y as f64), deco, None, None, None, Kind::Unspecified) { - custom_elements.push(elem); + custom_elements.push(crate::elements::OverlayElement::Memory(elem)); } } @@ -334,6 +337,8 @@ impl Platform for WaylandPlatform { if closed { return Err(PlatformError::Other("compositor window closed".into())); } + // Held bindings that repeat - see `CompState::tick_repeat`. + self.state.tick_repeat(); self.display.dispatch_clients(&mut self.state).map_err(err)?; self.display.flush_clients().map_err(err)?; self.render_frame()?; |