GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/backend/window_runner.rs (107K)
1 use smithay_client_toolkit::{
2 compositor::{CompositorHandler, CompositorState},
3 data_device_manager::DataDeviceManagerState,
4 delegate_compositor, delegate_keyboard, delegate_pointer, delegate_registry,
5 delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window, delegate_output,
6 delegate_layer,
7 registry::{ProvidesRegistryState, RegistryState},
8 output::{OutputHandler, OutputState},
9 seat::{
10 keyboard::KeyboardHandler,
11 pointer::{PointerHandler, ThemedPointer, ThemeSpec, CursorIcon},
12 Capability, SeatHandler, SeatState,
13 },
14 shell::{
15 xdg::{
16 window::{Window as XdgWindow, WindowConfigure, WindowHandler, WindowDecorations},
17 XdgShell, XdgSurface as XdgSurfaceExt,
18 },
19 wlr_layer::{LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure},
20 WaylandSurface,
21 },
22 shm::{Shm, ShmHandler},
23 };
24 use wayland_client::{
25 globals::{registry_queue_init, GlobalList},
26 protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_touch, wl_surface, wl_registry, wl_region, wl_callback},
27 Connection, QueueHandle, Proxy,
28 };
29
30 use wayland_protocols::wp::text_input::zv3::client::{
31 zwp_text_input_manager_v3::ZwpTextInputManagerV3,
32 zwp_text_input_v3::{self, ZwpTextInputV3},
33 };
34 use wayland_protocols::wp::pointer_gestures::zv1::client::{
35 zwp_pointer_gesture_pinch_v1::{self, ZwpPointerGesturePinchV1},
36 zwp_pointer_gestures_v1::{self as zwp_pointer_gestures, ZwpPointerGesturesV1},
37 };
38 pub use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel;
39 pub use smithay_client_toolkit::seat::pointer::CursorIcon as PointerCursorIcon;
40 use calloop::EventLoop;
41 use calloop_wayland_source::WaylandSource;
42 use cosmic_text::FontSystem;
43 use crate::widget::{MouseButton, ElementState, Key, NamedKey};
44 use crate::wayland::detect_scale_factor;
45 use crate::vk::VkRenderer;
46
47 pub use super::app::*;
48 pub use super::driver::PressedKey;
49 use super::frame::build_frame;
50 use super::shell::{Pacer, Shell, Step, ACTIVE_DISPATCH};
51 use super::driver::{Driver, Modifiers, Press, PressSite, ResizeEdge, ScrollFrame, ScrollSource, Turn};
52 pub use super::tessellate::*;
53 pub use super::text::*;
54
55 pub use super::shell::IDLE_DISPATCH;
56
57 /// The `CCE_PRESENT_DEBUG` traces' timestamp: wall-clock milliseconds, mod
58 /// 100 s, short enough to read down a column of lines.
59 fn debug_clock_ms() -> u128 {
60 std::time::SystemTime::now()
61 .duration_since(std::time::UNIX_EPOCH)
62 .map_or(0, |d| d.as_millis() % 100_000)
63 }
64
65 pub struct EngineState<A: Application> {
66 /// The session's accessibility publisher, for an app that publishes its tree
67 /// (`backend::a11y_unix`).
68 pub a11y: Option<crate::backend::a11y_unix::Publisher>,
69 pub registry_state: RegistryState,
70 pub compositor_state: CompositorState,
71 pub xdg_shell_state: XdgShell,
72 pub layer_shell_state: Option<LayerShell>,
73 pub shm_state: Shm,
74 pub seat_state: SeatState,
75 pub output_state: OutputState,
76 pub seats: Vec<wl_seat::WlSeat>,
77 pub pointer: Option<ThemedPointer>,
78 pub keyboard: Option<wl_keyboard::WlKeyboard>,
79
80 pub window: Option<XdgWindow>,
81 pub layer_surface: Option<LayerSurface>,
82 /// This session's surface is a layer surface ([`Application::layer`]),
83 /// which is what makes [`Application::wants_surface`] apply.
84 pub is_layer_app: bool,
85 /// The app said [`Application::wants_surface`] = false and the layer
86 /// surface is gone until it says true (the renderer is kept, detached).
87 pub layer_hidden: bool,
88 pub surface: Option<wl_surface::WlSurface>,
89
90 pub inner: Option<A>,
91
92 pub renderer: Option<VkRenderer>,
93 pub font_system: Option<FontSystem>,
94 pub swash_cache: cosmic_text::SwashCache,
95
96 pub scale_factor: f64,
97 /// The buffer scale last sent to the surface. Updated in [`Self::render`],
98 /// paired with the present that commits a matching-size buffer — never on
99 /// the scale event itself, which races in-flight presents of old buffers.
100 pub committed_buffer_scale: i32,
101 /// Outputs the surface has entered and not left. Used by
102 /// `scale_factor_changed` to reject the SCTK no-outputs fallback: on
103 /// suspend/resume the DRM connector is destroyed and re-created, the
104 /// surface briefly sits on zero (live) outputs, and SCTK reports scale 1.
105 /// Acting on that report rebuilds the buffer at scale-1 size while the
106 /// surface's latched scale can still be 2 — a fatal `invalid_size`
107 /// protocol error for odd-sized surfaces (the status bar crash-loop on
108 /// every resume) and a silently HALF-SIZE window for even-sized ones
109 /// (the compositor reads buffer/scale as a self-resize and the halving
110 /// sticks, compounding per resume).
111 pub entered_outputs: Vec<wl_output::WlOutput>,
112 pub logical_width: f32,
113 pub logical_height: f32,
114 /// The window-frame logical size (surface minus the overflow rim) as of
115 /// the last configure/desired-size — what the surface is re-derived from
116 /// when [`Application::overflow_margin`] changes at runtime.
117 pub frame_logical: (f32, f32),
118 /// The overflow margin the current surface was actually sized with. Input
119 /// translation and the dl-text overlay offsets use THIS, never a live
120 /// `overflow_margin()` read — the app may have changed its answer since.
121 pub applied_margin: f32,
122 /// True while the previous frame ran with a nonzero margin — lets the
123 /// per-frame geometry publish reset state exactly once on deactivation.
124 pub overflow_was_active: bool,
125 /// The popover-union rect last sent via zcce set_popover_region, logical
126 /// surface px; None once a clear has been sent (or never anything).
127 pub sent_popover_region: Option<(i32, i32, i32, i32)>,
128 /// The context menu's popup surface while one is open — see
129 /// `backend::menu_popup`.
130 pub menu_popup: Option<crate::backend::menu_popup::MenuPopup>,
131 /// The popup's renderer, kept across opens and moved from one popup
132 /// surface to the next: a renderer costs a device and every pipeline
133 /// (tens of ms), a re-attach costs one swapchain.
134 pub menu_renderer: Option<VkRenderer>,
135 /// The popup renderer's copies of the bundled glyphs the menu draws,
136 /// by the window renderer's id for the same glyph (see
137 /// `crate::icon_source`). The popup keeps images of its own — it does
138 /// not take the shared upload queue — so a glyph the menu paints by the
139 /// window's id is uploaded again into the popup's table, once. Cleared
140 /// with the renderer it names.
141 pub menu_icon_ids: std::collections::HashMap<u32, Option<u32>>,
142 /// The `wl_display` the renderers were made from, as an address.
143 pub display_ptr: usize,
144
145 pub exit: bool,
146 pub redraw: bool,
147 /// A frame took the app's damage (`Application::take_damage`) and was
148 /// not presented: the next presented frame is a full one.
149 pub damage_owed: bool,
150 /// The last built frame, for the damage the runner derives
151 /// (`backend::frame::derive_damage`).
152 pub frame_record: crate::backend::frame::FrameRecord,
153 pub frame_callback_pending: bool,
154 /// When the pending frame callback was armed — the starvation fallback's
155 /// clock (see the render gate in `run`).
156 pub frame_callback_armed_at: Option<std::time::Instant>,
157 /// A warm-down commit's frame callback is outstanding (see
158 /// [`EngineState::keepalive_commit`]). Separate from
159 /// `frame_callback_pending` on purpose: a genuine redraw never waits on it.
160 pub keepalive_pending: bool,
161 pub keepalive_armed_at: Option<std::time::Instant>,
162 /// Consecutive renders skipped by the extent gate (pending swapchain size
163 /// != the size the current logical size and scale call for). Normally 0 or
164 /// 1; a persistent count means no frame is presenting and deserves a warn.
165 pub extent_gate_skips: u32,
166 pub first_configure_received: bool,
167 /// The session's input state and routing: modifiers, the held key, the
168 /// pointer's place and held buttons, the chords (see [`Driver`]).
169 pub driver: Driver,
170 pub sender: calloop::channel::Sender<A::Message>,
171 pub current_cursor_icon: Option<CursorIcon>,
172 pub qh: QueueHandle<EngineState<A>>,
173 pub just_configured: bool,
174 pub pointer_gestures: Option<ZwpPointerGesturesV1>,
175 pub pinch_gesture: Option<ZwpPointerGesturePinchV1>,
176 /// `text-input-v3`, when the compositor offers it: the input method's
177 /// way in (see `backend::text_input`). The text input is the first
178 /// keyboard seat's.
179 pub text_input_manager: Option<ZwpTextInputManagerV3>,
180 pub text_input: Option<ZwpTextInputV3>,
181 pub text_input_state: crate::backend::text_input::TextInput,
182 /// The cce window-management toplevel handle, held for the window's
183 /// lifetime once [`Application::utility`] declared the mode.
184 pub cce_toplevel: Option<crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1>,
185 /// Latest unrendered grid_patch (serial, x, y, w, h, scale) — a newer
186 /// event supersedes an unconsumed older one, per protocol.
187 pub pending_grid_patch: Option<(u32, f64, f64, f64, f64, f64)>,
188 /// Serial of the most recent pointer press, kept for
189 /// [`Application::take_window_action`] move/resize grabs.
190 pub last_press_serial: Option<u32>,
191 /// The touchscreen, once the seat offers one; see `backend/touch.rs`.
192 pub touch: Option<wl_touch::WlTouch>,
193 pub touch_tracker: super::touch::TouchTracker,
194 /// The followed finger's surface offset into window coordinates (the
195 /// menu popup's, or none), fixed at its down.
196 pub touch_offset: (f32, f32),
197 /// Where a touch scroll is dispatched: the finger's down point.
198 pub touch_scroll_at: Option<(f32, f32)>,
199 /// This frame's display-list text, shaped and held here so the `TextSpan`s built
200 /// in the render pass can borrow the buffers (Phase 6 —
201 /// [`Application::display_list_text`]).
202 pub(crate) dl_text_items: Vec<DlText>,
203
204 /// Drag-and-drop destination state (see [`crate::backend::dnd`]). The
205 /// manager is absent when the compositor exposes no wl_data_device_manager;
206 /// every drop path then no-ops.
207 pub data_device_manager: Option<smithay_client_toolkit::data_device_manager::DataDeviceManagerState>,
208 pub data_devices: Vec<smithay_client_toolkit::data_device_manager::data_device::DataDevice>,
209 /// Mime type accepted for the in-flight drag; `None` means the app wants
210 /// nothing this offer carries, so the drop is declined.
211 pub drag_mime: Option<String>,
212 /// Surface-local logical position of the last drag enter/motion — the
213 /// drop point handed to [`Application::handle_drop`].
214 pub drag_pos: LogicalPosition,
215 /// Reader threads post completed drops here; the main loop drains it.
216 pub drop_tx: Option<calloop::channel::Sender<crate::backend::dnd::DroppedData>>,
217 /// The offer being read right now, held so it can be finished only once
218 /// the transfer is actually done (see `dnd::drop_performed`).
219 pub pending_drop_offer:
220 Option<smithay_client_toolkit::data_device_manager::data_offer::DragOffer>,
221 /// The input region last sent to the compositor, so a per-frame
222 /// [`Application::input_regions`] only costs protocol traffic on change.
223 pub applied_input_regions: Option<Vec<(i32, i32, i32, i32)>>,
224 }
225
226 impl<A: Application> EngineState<A> {
227 /// Build the window's renderer. A [`SurfaceLost`](crate::vk::SurfaceLost)
228 /// means the connection under the surface is already dead; the session
229 /// ends as a lost connection, which reconnects if the compositor is still
230 /// there and exits if it is not.
231 pub fn init_gpu(
232 &mut self,
233 conn: &Connection,
234 width_logical: f32,
235 height_logical: f32,
236 ) -> Result<(), crate::vk::SurfaceLost> {
237 let s = self.scale_factor as f32;
238 let pw = (width_logical * s) as u32;
239 let ph = (height_logical * s) as u32;
240
241 let surface = self.surface.as_ref().expect("surface missing");
242
243 let display_ptr = conn.backend().display_id().as_ptr() as *mut std::ffi::c_void;
244 let surface_ptr = surface.id().as_ptr() as *mut std::ffi::c_void;
245 self.display_ptr = display_ptr as usize;
246
247 let load_system_fonts = self.inner.as_ref().is_some_and(|a| a.load_system_fonts());
248 // Corner radius 0: runner apps tessellate their own rounded corners.
249 let mut renderer = unsafe { VkRenderer::try_new(display_ptr, surface_ptr, pw, ph, 0.0) }?;
250 if self.inner.as_ref().is_some_and(|a| a.grid()) {
251 // Redrawn only per patch, and a patch is several screens of pixels.
252 renderer.set_minimal_swapchain();
253 }
254 self.font_system = Some(if load_system_fonts {
255 crate::create_font_system_with_system_fonts()
256 } else {
257 crate::create_font_system()
258 });
259 self.renderer = Some(renderer);
260 self.logical_width = width_logical;
261 self.logical_height = height_logical;
262 Ok(())
263 }
264
265 /// Buffer scale and physical extent for a logical size under the current
266 /// scale factor: rounded, then snapped up so the extent divides by the
267 /// buffer scale (a wl_surface requirement). In forced-scale mode the
268 /// surface stays at buffer_scale 1 (the compositor believes scale 1).
269 ///
270 /// This is the single source of the buffer-size formula: `resize` sizes
271 /// the swapchain with it and `render` refuses to present any extent that
272 /// disagrees with it — a mispaired buffer/scale commit is how the resume
273 /// output bounce halved even-sized windows (buffer at the old scale's
274 /// size, new scale latched; the compositor reads it as a self-resize).
275 pub(crate) fn buffer_geometry(scale_factor: f64, w: f32, h: f32) -> (i32, u32, u32) {
276 let s = if crate::scale::forced_scale().is_some() {
277 1
278 } else {
279 (scale_factor.round() as i32).max(1)
280 };
281 let su = s as u32;
282 let pw = ((w as f64 * scale_factor).round() as u32).max(1).div_ceil(su) * su;
283 let ph = ((h as f64 * scale_factor).round() as u32).max(1).div_ceil(su) * su;
284 (s, pw, ph)
285 }
286
287 pub fn resize(&mut self, w: f32, h: f32) {
288 let (w, h) = self.inner.as_ref().unwrap().adjust_size(w, h);
289 if w > 0.0 && h > 0.0 {
290 self.logical_width = w;
291 self.logical_height = h;
292 let (_, pw, ph) = Self::buffer_geometry(self.scale_factor, w, h);
293 if let Some(ref mut renderer) = self.renderer {
294 renderer.resize(pw, ph);
295 }
296 let scale = self.scale_factor;
297 self.inner.as_mut().unwrap().handle_resize(w, h, scale);
298 self.publish_window_geometry();
299 }
300 }
301
302 /// Overflow-margin mode ([`Application::overflow_margin`]): re-publish the
303 /// window frame — the surface rect inset by the margin — as the xdg window
304 /// geometry, and an input region of the frame PLUS any open popover rects
305 /// (an overhanging menu's rows must stay clickable; empty rim still falls
306 /// through). Applied on every resize and, while the rim is live, every
307 /// loop (the popover rects animate). Margin back at 0 resets both — a
308 /// no-op only for apps that never had a rim. (All double-buffered surface
309 /// state, latched by the next commit.)
310 fn publish_window_geometry(&mut self) {
311 let m = self.applied_margin;
312 let Some(ref window) = self.window else { return };
313 if m <= 0.0 {
314 if self.overflow_was_active {
315 let gw = (self.logical_width as i32).max(1);
316 let gh = (self.logical_height as i32).max(1);
317 window.xdg_surface().set_window_geometry(0, 0, gw, gh);
318 if let Some(ref surface) = self.surface {
319 surface.set_input_region(None);
320 }
321 }
322 return;
323 }
324 // Right/bottom rim: the frame keeps the surface origin — no offset,
325 // frame coords == surface coords.
326 let gw = ((self.logical_width - m) as i32).max(1);
327 let gh = ((self.logical_height - m) as i32).max(1);
328 window.xdg_surface().set_window_geometry(0, 0, gw, gh);
329 if let Some(ref surface) = self.surface {
330 let compositor = self.compositor_state.wl_compositor();
331 let wl_region = compositor.create_region(&self.qh, ());
332 wl_region.add(0, 0, gw, gh);
333 // Open popovers, clamped to the surface.
334 if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
335 for (id, ptr) in ctx.tree.iter_registered() {
336 unsafe {
337 let Some(w) = ptr.as_ref() else { continue };
338 if !w.visible() {
339 continue;
340 }
341 let Some((px, py, pw, ph)) = w.popover_rect() else { continue };
342 let (dx, dy) = self.inner.as_ref().unwrap().popover_offset(id);
343 let (px, py) = (px + dx, py + dy);
344 let x0 = px.max(0.0) as i32;
345 let y0 = py.max(0.0) as i32;
346 let x1 = ((px + pw).min(self.logical_width)) as i32;
347 let y1 = ((py + ph).min(self.logical_height)) as i32;
348 if x1 > x0 && y1 > y0 {
349 wl_region.add(x0, y0, x1 - x0, y1 - y0);
350 }
351 }
352 }
353 }
354 surface.set_input_region(Some(&wl_region));
355 wl_region.destroy();
356 }
357 }
358
359 /// Report the union of the open popover rects to the compositor
360 /// (zcce set_popover_region, manager v7), so its window chrome — the
361 /// overview resize ring — stays out from under an in-surface menu. Sent
362 /// only on change, and a clear is sent when the last popover closes;
363 /// rects are clamped to the surface in logical px, the coordinate space
364 /// the protocol specifies. Popovers animate, so this runs every loop —
365 /// the change gate is what keeps it quiet.
366 fn send_popover_region(&mut self) {
367 let Some(tl) = &self.cce_toplevel else { return };
368 // Version gate on the MANAGER numbering the resource carries (the
369 // toplevel inherits its bind version): 7 is where the request
370 // appeared. An older compositor would kill the client on the
371 // unknown opcode.
372 if tl.version() < 7 {
373 return;
374 }
375 let mut union: Option<(f32, f32, f32, f32)> = None;
376 if let Some(ctx) = self.inner.as_ref().unwrap().ui_context() {
377 for (id, ptr) in ctx.tree.iter_registered() {
378 unsafe {
379 let Some(w) = ptr.as_ref() else { continue };
380 if !w.visible() {
381 continue;
382 }
383 let Some((px, py, pw, ph)) = w.popover_rect() else { continue };
384 let (dx, dy) = self.inner.as_ref().unwrap().popover_offset(id);
385 let (px, py) = (px + dx, py + dy);
386 let (x0, y0) = (px.max(0.0), py.max(0.0));
387 let x1 = (px + pw).min(self.logical_width);
388 let y1 = (py + ph).min(self.logical_height);
389 if x1 <= x0 || y1 <= y0 {
390 continue;
391 }
392 union = Some(match union {
393 None => (x0, y0, x1, y1),
394 Some((ux0, uy0, ux1, uy1)) => {
395 (ux0.min(x0), uy0.min(y0), ux1.max(x1), uy1.max(y1))
396 }
397 });
398 }
399 }
400 }
401 let next = union.map(|(x0, y0, x1, y1)| {
402 (x0 as i32, y0 as i32, (x1 - x0).ceil() as i32, (y1 - y0).ceil() as i32)
403 });
404 if next == self.sent_popover_region {
405 return;
406 }
407 match next {
408 Some((x, y, w, h)) => tl.set_popover_region(x, y, w, h),
409 None => tl.set_popover_region(0, 0, 0, 0),
410 }
411 self.sent_popover_region = next;
412 }
413
414 fn logical_size(&self) -> LogicalSize {
415 LogicalSize::new(self.logical_width, self.logical_height)
416 }
417
418 /// The cursor for the pointer at (lx, ly) — see [`Driver::cursor_icon_at`].
419 fn cursor_icon_at(&self, lx: f32, ly: f32) -> CursorIcon {
420 self.driver.cursor_icon_at(self.inner.as_ref().unwrap(), lx, ly, self.logical_size())
421 }
422
423 pub fn render(&mut self) {
424 // Grid patch: resize to the patch's buffer size, tell the app what
425 // world region this frame covers, and ack — the commit this render
426 // produces is the one the compositor latches at the new anchor.
427 if let Some((serial, px, py, pw, ph, pscale)) = self.pending_grid_patch.take() {
428 self.resize((pw * pscale) as f32, (ph * pscale) as f32);
429 self.inner.as_mut().unwrap().grid_patch(px, py, pw, ph, pscale);
430 if let Some(tl) = &self.cce_toplevel {
431 tl.ack_grid_patch(serial);
432 }
433 }
434 // Nothing to present on: a layer surface the app has hidden whose
435 // renderer could not be rebuilt (`show_layer_surface`).
436 if self.renderer.is_none() {
437 return;
438 }
439 let logical_w = self.logical_width;
440 let logical_h = self.logical_height;
441 let scale_factor = self.scale_factor;
442
443 if let Some(ref surface) = self.surface {
444 if let Some(regions) = self.inner.as_ref().unwrap().input_regions() {
445 // Only re-send when it actually changes. This runs per frame,
446 // and a client whose region tracks its content (the desktop
447 // grid's items follow every pan) would otherwise create and
448 // destroy a wl_region on every frame of a camera flight.
449 if self.applied_input_regions.as_deref() != Some(regions.as_slice()) {
450 let compositor = self.compositor_state.wl_compositor();
451 let wl_region = compositor.create_region(&self.qh, ());
452 for &(rx, ry, rw, rh) in ®ions {
453 wl_region.add(rx, ry, rw, rh);
454 }
455 surface.set_input_region(Some(&wl_region));
456 wl_region.destroy();
457 self.applied_input_regions = Some(regions);
458 }
459 }
460 }
461
462 // The frame itself, built with no window system in it (`backend::frame`).
463 let owed = self.damage_owed;
464 let mut frame = build_frame(
465 self.inner.as_mut().unwrap(),
466 self.font_system.as_mut().unwrap(),
467 LogicalSize::new(logical_w, logical_h),
468 scale_factor,
469 &mut self.damage_owed,
470 &mut self.dl_text_items,
471 );
472 crate::backend::frame::derive_damage(&mut frame, &mut self.frame_record, &self.dl_text_items, owed);
473
474 // Upload the frame's glyphs. An app without display-list text owns
475 // the renderer's text state itself (it stages via stage_renderer
476 // below); don't wipe it here. The renderer owns swapchain
477 // rebuild/recovery.
478 let renderer = self.renderer.as_mut().unwrap();
479 if frame.dl_text {
480 let spans = frame.text_spans(&self.dl_text_items);
481 renderer.prepare_text(self.font_system.as_mut().unwrap(), &mut self.swash_cache, &spans);
482 }
483
484 // Commit the buffer scale together with a buffer it is legal for: the
485 // present inside draw_frame_2d is the only commit on this surface, so
486 // sending the request here orders it right before a matching-size
487 // attach+commit.
488 //
489 // Present only the EXACT extent the current logical size and scale
490 // call for. Divisibility is not enough: mid scale-transition (the
491 // resume output bounce) the pending extent can belong to the other
492 // scale, and an even-sized old-scale buffer divides cleanly by the
493 // new scale — the commit is protocol-legal, so the compositor reads
494 // it as a self-resize to half/double and reconfigures the window to
495 // match (how the color editor came back from suspend at exactly half
496 // size with the divisibility guard green). Odd sizes at least die
497 // loudly (invalid_size). On mismatch, re-request the right extent
498 // and skip — before the frame-callback request below, so the loop
499 // isn't left waiting on a callback no commit will ever latch.
500 if let Some(ref surface) = self.surface {
501 let (s, epw, eph) =
502 Self::buffer_geometry(self.scale_factor, self.logical_width, self.logical_height);
503 let e = renderer.pending_extent();
504 if e.width != epw || e.height != eph {
505 renderer.resize(epw, eph);
506 self.extent_gate_skips += 1;
507 // ~5s of continuous skipping at the 16ms loop cadence: nothing
508 // is presenting and nothing else will say so — this is the
509 // only witness to a wedged pending extent.
510 if self.extent_gate_skips.is_multiple_of(300) {
511 log::warn!(
512 "[window_runner] extent gate: pending {}x{} != expected {}x{} for {} consecutive renders; no frame is presenting",
513 e.width, e.height, epw, eph, self.extent_gate_skips,
514 );
515 }
516 self.redraw = true;
517 return;
518 }
519 self.extent_gate_skips = 0;
520 if s != self.committed_buffer_scale {
521 surface.set_buffer_scale(s);
522 self.committed_buffer_scale = s;
523 }
524 }
525
526 if let Some(ref surface) = self.surface {
527 let _callback = surface.frame(&self.qh, ());
528 self.frame_callback_pending = true;
529 self.frame_callback_armed_at = Some(std::time::Instant::now());
530 if crate::vk::present_debug() {
531 let t = debug_clock_ms();
532 eprintln!("[vk] t={} armed frame callback", t);
533 }
534 }
535
536 // Direct renderer staging (3D scenes, RT panes, app-shaped text).
537 if self.inner.as_mut().unwrap().stage_renderer(
538 renderer,
539 LogicalSize::new(logical_w, logical_h),
540 scale_factor,
541 ) {
542 self.redraw = true;
543 }
544
545 if !renderer.draw_frame_2d(frame.frame2d()) {
546 // No present happened (swapchain out-of-date, or the created
547 // swapchain didn't match the requested extent). The frame
548 // callback requested above will never latch without a commit —
549 // clear it or the demand-driven loop stalls waiting forever.
550 self.frame_callback_pending = false;
551 self.redraw = true;
552 } else {
553 self.damage_owed = false;
554 }
555 self.sync_text_input();
556 // The tree after the frame that may have changed it; nothing is built while no
557 // screen reader is connected.
558 if let (Some(publisher), Some(app)) = (self.a11y.as_mut(), self.inner.as_mut()) {
559 publisher.publish(app, crate::scale::scale_factor() as f64);
560 }
561 }
562
563 /// Bring the text input in step with the frame just built: enabled at
564 /// the editing widget's caret, disabled with nothing editing, reset for
565 /// a composition a widget dropped (`backend::text_input`).
566 fn sync_text_input(&mut self) {
567 use crate::backend::text_input::Send;
568 use zwp_text_input_v3::{ContentHint, ContentPurpose};
569 let reset = crate::ime::take_reset();
570 let Some(ti) = self.text_input.clone() else { return };
571 // Forced mode: the surface is the compositor's scale-1 space.
572 let surface_scale = crate::scale::forced_scale().unwrap_or(1.0);
573 let pressed = crate::ime::take_press();
574 match self.text_input_state.plan(crate::ime::caret(), surface_scale, reset, pressed) {
575 Send::Nothing => {}
576 Send::Enable { rect: [x, y, w, h], reset } => {
577 if reset {
578 ti.disable();
579 ti.commit();
580 }
581 ti.enable();
582 ti.set_content_type(ContentHint::None, ContentPurpose::Normal);
583 ti.set_cursor_rectangle(x, y, w, h);
584 ti.commit();
585 }
586 Send::Move { rect: [x, y, w, h] } => {
587 ti.set_cursor_rectangle(x, y, w, h);
588 ti.commit();
589 }
590 Send::Disable => {
591 ti.disable();
592 ti.commit();
593 }
594 }
595 }
596 }
597
598 impl<A: Application> Shell for EngineState<A> {
599 type App = A;
600
601 /// The driver and the app's turn, borrowed apart: every input dispatch
602 /// is `let (driver, t) = self.turn(); driver.<event>(t, ..)`.
603 fn turn(&mut self) -> (&mut Driver, Turn<'_, A>) {
604 (
605 &mut self.driver,
606 Turn { app: self.inner.as_mut().unwrap(), redraw: &mut self.redraw, exit: &mut self.exit },
607 )
608 }
609
610 fn app(&self) -> &A {
611 self.inner.as_ref().unwrap()
612 }
613
614 fn redraw(&mut self) -> &mut bool {
615 &mut self.redraw
616 }
617
618 fn exit_requested(&self) -> bool {
619 self.exit
620 }
621
622 fn take_just_configured(&mut self) -> bool {
623 std::mem::replace(&mut self.just_configured, false)
624 }
625
626 fn request_size(&mut self, w: u32, h: u32) {
627 // desired_size is a window-frame size; the surface adds the
628 // right/bottom overflow rim (0 for margin-less apps).
629 let m = self.inner.as_ref().unwrap().overflow_margin() as f32;
630 let (sw, sh) = (w as f32 + m, h as f32 + m);
631 if (self.logical_width - sw).abs() > 0.001 || (self.logical_height - sh).abs() > 0.001 {
632 self.frame_logical = (w as f32, h as f32);
633 self.applied_margin = m;
634 self.resize(sw, sh);
635 self.redraw = true;
636 }
637 }
638
639 fn sync(&mut self) {
640 self.sync_surface_wanted();
641 // Overflow-margin drift (configure-sized apps): the rim can change at
642 // runtime — a popover overhanging the window frame — so re-derive the
643 // surface from the stored frame whenever the app's answer moves. While
644 // the rim is live, re-publish geometry every loop: the input region
645 // tracks the animating popover rects.
646 let m_now = self.inner.as_ref().unwrap().overflow_margin() as f32;
647 if (m_now - self.applied_margin).abs() > 0.001 && self.frame_logical.0 > 0.0 {
648 self.applied_margin = m_now;
649 let (fw, fh) = self.frame_logical;
650 self.resize(fw + m_now, fh + m_now);
651 self.redraw = true;
652 }
653 if self.applied_margin > 0.0 || self.overflow_was_active {
654 self.publish_window_geometry();
655 self.overflow_was_active = self.applied_margin > 0.0;
656 }
657 self.send_popover_region();
658 self.sync_menu_popup();
659 }
660
661 fn set_title(&mut self, title: &str) {
662 if let Some(ref window) = self.window {
663 window.set_title(title);
664 window.commit();
665 }
666 }
667
668 fn frame_pending(&mut self) -> bool {
669 // Frame-callback starvation fallback: the compositor only sends
670 // frame-done for surfaces it actually renders, so a callback armed
671 // while the window sat off-viewport (or the scene went static) may
672 // never fire — and the vsync gate then freezes the app forever
673 // with a perfectly live event loop (input processes, state changes,
674 // nothing repaints). If a redraw has been waiting on a callback well
675 // past any real vsync interval, stop waiting and draw.
676 //
677 // Gated on the renderer's present mode: forcing a present past a
678 // dead callback is only safe under MAILBOX (the present replaces the
679 // queued buffer). Under FIFO the driver's throttle waits on the
680 // previous present's frame event, so the forced present itself
681 // blocks forever inside the driver — the exact freeze this fallback
682 // exists to prevent. There the gate stays closed: pixels may stale
683 // until the next frame-done/configure, but the loop stays alive.
684 if self.redraw
685 && self.frame_callback_pending
686 && self.renderer.as_ref().is_some_and(|r| r.forced_present_safe())
687 && self.frame_callback_armed_at.is_none_or(|t| t.elapsed().as_millis() > 250)
688 {
689 self.frame_callback_pending = false;
690 if crate::vk::present_debug() {
691 let t = debug_clock_ms();
692 eprintln!("[vk] t={} starvation fallback fired (callback never came)", t);
693 }
694 }
695 self.frame_callback_pending
696 }
697
698 fn configured(&self) -> bool {
699 self.first_configure_received
700 }
701
702 fn present(&mut self, fresh: bool) {
703 if !fresh {
704 // A warm-down step: the window alone, and nothing drawn — the
705 // pixels have not changed. An occluded surface gets no callbacks;
706 // past 250 ms stop waiting for one, as the starvation fallback does
707 // for a real frame.
708 let waiting = self.keepalive_pending
709 && self.keepalive_armed_at.is_some_and(|t| t.elapsed().as_millis() < 250);
710 if !waiting {
711 self.keepalive_commit();
712 }
713 return;
714 }
715 // A menu handed over from the window commits first, so
716 // there is no moment with neither (`take_menu_popup_lead`).
717 let lead = self.take_menu_popup_lead();
718 if lead {
719 self.render_menu_popup();
720 }
721 self.render();
722 if !lead {
723 self.render_menu_popup();
724 }
725 }
726 }
727
728 impl<A: Application> EngineState<A> {
729 /// Give `surface` its layer-shell role from `ls` at `width` x `height`
730 /// and commit, which asks the compositor for the first configure. The
731 /// session start and [`Self::show_layer_surface`] share this.
732 fn attach_layer_role(&mut self, surface: &wl_surface::WlSurface, ls: &LayerSettings, width: u32, height: u32) {
733 let layer_shell = self
734 .layer_shell_state
735 .as_ref()
736 .expect("compositor does not support wlr-layer-shell");
737 let layer_surface = layer_shell.create_layer_surface(
738 &self.qh,
739 surface.clone(),
740 ls.layer,
741 Some(ls.namespace.clone()),
742 None,
743 );
744 layer_surface.set_anchor(ls.anchor);
745 layer_surface.set_exclusive_zone(ls.exclusive_zone);
746 layer_surface.set_keyboard_interactivity(ls.keyboard_interactivity);
747 let (t, r, b, l) = ls.margin;
748 layer_surface.set_margin(t, r, b, l);
749 layer_surface.set_size(width, height);
750 layer_surface.commit();
751 self.layer_surface = Some(layer_surface);
752 }
753
754 /// Follow [`Application::wants_surface`]: tear the layer surface down
755 /// when the app has nothing to show, build it again when it does. Once a
756 /// loop turn, before the present decision.
757 fn sync_surface_wanted(&mut self) {
758 if !self.is_layer_app {
759 return;
760 }
761 let want = self.inner.as_ref().unwrap().wants_surface();
762 if !want && !self.layer_hidden {
763 self.hide_layer_surface();
764 } else if want && self.layer_hidden {
765 self.show_layer_surface();
766 }
767 }
768
769 /// Let the renderer go of the surface (its swapchain and `VkSurfaceKHR`;
770 /// the device, pipelines, atlases and image table stay), then destroy the
771 /// layer surface — SCTK destroys the role and then the `wl_surface`, which
772 /// must not happen while a swapchain still presents to it.
773 fn hide_layer_surface(&mut self) {
774 if let Some(renderer) = self.renderer.as_mut() {
775 renderer.detach_surface();
776 }
777 self.layer_surface = None;
778 self.surface = None;
779 self.layer_hidden = true;
780 self.first_configure_received = false;
781 self.frame_callback_pending = false;
782 self.keepalive_pending = false;
783 self.redraw = false;
784 self.entered_outputs.clear();
785 self.applied_input_regions = None;
786 log::info!("[window_runner] nothing to show; layer surface unmapped");
787 }
788
789 /// A fresh `wl_surface` with the app's layer role, and the renderer moved
790 /// onto it (`attach_surface`: one swapchain, where a new renderer costs a
791 /// device and every pipeline). The first configure then makes it
792 /// presentable, exactly as at session start. The renderer is the same one,
793 /// so its image ids are still good and `renderer_init` is not called; only
794 /// where there is none (an attach that failed before) is one made, and
795 /// that one is announced. A surface nothing can draw to stays hidden.
796 fn show_layer_surface(&mut self) {
797 let app = self.inner.as_ref().unwrap();
798 let settings = app.settings();
799 let Some(ls) = app.layer() else { return };
800 let surface = self.compositor_state.create_surface(&self.qh);
801 let buffer_scale = if crate::scale::forced_scale().is_some() { 1 } else { self.scale_factor as i32 };
802 surface.set_buffer_scale(buffer_scale);
803 self.committed_buffer_scale = buffer_scale;
804 self.attach_layer_role(&surface, &ls, settings.width, settings.height);
805
806 let s = self.scale_factor as f32;
807 let (pw, ph) = ((settings.width as f32 * s) as u32, (settings.height as f32 * s) as u32);
808 let display_ptr = self.display_ptr as *mut std::ffi::c_void;
809 let surface_ptr = surface.id().as_ptr() as *mut std::ffi::c_void;
810 self.surface = Some(surface);
811 let made = match self.renderer.as_mut() {
812 Some(renderer) => match unsafe { renderer.attach_surface(display_ptr, surface_ptr, pw, ph) } {
813 Ok(()) => Ok(false),
814 Err(lost) => Err(lost),
815 },
816 None => unsafe { VkRenderer::try_new(display_ptr, surface_ptr, pw, ph, 0.0) }.map(|renderer| {
817 self.renderer = Some(renderer);
818 true
819 }),
820 };
821 let made = match made {
822 Ok(made) => made,
823 Err(lost) => {
824 log::error!("[window_runner] cannot draw to the new surface, staying unmapped: {lost}");
825 self.renderer = None;
826 self.layer_surface = None;
827 self.surface = None;
828 return;
829 }
830 };
831 self.logical_width = settings.width as f32;
832 self.logical_height = settings.height as f32;
833 self.layer_hidden = false;
834 self.redraw = true;
835 log::info!(
836 "[window_runner] layer surface mapped again ({})",
837 if made { "a new renderer" } else { "the renderer moved onto it" }
838 );
839 if made {
840 self.inner.as_mut().unwrap().renderer_init(self.renderer.as_mut().unwrap());
841 }
842 }
843
844 /// One warm-down step: a frame callback and a commit with no buffer, so
845 /// the compositor keeps servicing this surface's callbacks at vsync
846 /// (sparse commits were measured getting theirs 22-128 ms late) while
847 /// nothing is drawn, uploaded or re-composited. wlroots schedules an
848 /// output frame for a commit that asks for a callback, so it arrives
849 /// without any damage.
850 fn keepalive_commit(&mut self) {
851 let Some(ref surface) = self.surface else { return };
852 let _callback = surface.frame(&self.qh, KeepAlive);
853 surface.commit();
854 self.keepalive_pending = true;
855 self.keepalive_armed_at = Some(std::time::Instant::now());
856 if crate::vk::present_debug() {
857 eprintln!("[vk] t={} armed keepalive callback", debug_clock_ms());
858 }
859 }
860 }
861
862 /// User data of a warm-down frame callback ([`EngineState::keepalive_commit`]),
863 /// which clears `keepalive_pending` rather than `frame_callback_pending`.
864 pub struct KeepAlive;
865
866 impl<A: Application> wayland_client::Dispatch<wl_callback::WlCallback, KeepAlive> for EngineState<A> {
867 fn event(
868 state: &mut Self,
869 _proxy: &wl_callback::WlCallback,
870 event: wl_callback::Event,
871 _data: &KeepAlive,
872 _conn: &Connection,
873 _qh: &QueueHandle<Self>,
874 ) {
875 if let wl_callback::Event::Done { .. } = event {
876 state.keepalive_pending = false;
877 if crate::vk::present_debug() {
878 let waited = state.keepalive_armed_at.map(|a| a.elapsed().as_millis()).unwrap_or(0);
879 eprintln!("[vk] t={} keepalive-done (waited {}ms)", debug_clock_ms(), waited);
880 }
881 }
882 }
883 }
884
885 impl<A: Application> Drop for EngineState<A> {
886 fn drop(&mut self) {
887 // The popup's renderer lets go of its surface before the popup (and
888 // its wl_surface) drops with the rest of the fields.
889 self.close_menu_popup();
890 self.menu_renderer = None;
891 self.renderer = None;
892 }
893 }
894
895 impl<A: Application> CompositorHandler for EngineState<A> {
896 fn scale_factor_changed(
897 &mut self,
898 _conn: &Connection,
899 _qh: &QueueHandle<Self>,
900 _surface: &wl_surface::WlSurface,
901 scale_factor: i32,
902 ) {
903 // Don't send set_buffer_scale here: an in-flight present can commit an
904 // old-scale-sized buffer right after it, which is a fatal invalid_size
905 // protocol error (seen on resume, when outputs bounce 2→1→2). The scale
906 // request is sent in `render`, paired with a matching-size present.
907 if crate::scale::forced_scale().is_some() {
908 // Forced mode: the compositor's opinion (scale 1 under cage) must
909 // not clobber the override.
910 return;
911 }
912 if self.inner.as_ref().is_some_and(|a| a.grid()) {
913 // Grid surfaces stay at scale 1 — patch.scale is the sole
914 // resolution authority (see the pin at surface creation).
915 return;
916 }
917 // Resume bounce: when the surface sits on no LIVE output (the DRM
918 // connector was destroyed and not yet re-created), the reported
919 // factor is SCTK's no-outputs fallback, not information — hold the
920 // last real scale. When the reborn output arrives, surface enter
921 // recomputes and this handler runs again with a live output backing
922 // it. Liveness matters (not just enter/leave counting): the leave
923 // for a destroyed output may never be delivered.
924 let on_live_output = self
925 .entered_outputs
926 .iter()
927 .any(|o| self.output_state.info(o).is_some());
928 if !on_live_output && (scale_factor as f64) < self.scale_factor {
929 return;
930 }
931 self.scale_factor = scale_factor as f64;
932 self.resize(self.logical_width, self.logical_height);
933 self.redraw = true;
934 }
935
936 fn transform_changed(
937 &mut self,
938 _conn: &Connection,
939 _qh: &QueueHandle<Self>,
940 _surface: &wl_surface::WlSurface,
941 _new_transform: wl_output::Transform,
942 ) {}
943
944 fn frame(
945 &mut self,
946 _conn: &Connection,
947 _qh: &QueueHandle<Self>,
948 _surface: &wl_surface::WlSurface,
949 _time: u32,
950 ) {}
951
952 fn surface_enter(
953 &mut self,
954 _conn: &Connection,
955 _qh: &QueueHandle<Self>,
956 _surface: &wl_surface::WlSurface,
957 output: &wl_output::WlOutput,
958 ) {
959 if !self.entered_outputs.contains(output) {
960 self.entered_outputs.push(output.clone());
961 }
962 // Dead entries (destroyed outputs never send leave) are harmless —
963 // the liveness check in scale_factor_changed skips them — but drop
964 // them here so the list doesn't grow across suspend cycles.
965 self.entered_outputs
966 .retain(|o| self.output_state.info(o).is_some());
967 self.redraw = true;
968 }
969
970 fn surface_leave(
971 &mut self,
972 _conn: &Connection,
973 _qh: &QueueHandle<Self>,
974 _surface: &wl_surface::WlSurface,
975 output: &wl_output::WlOutput,
976 ) {
977 self.entered_outputs.retain(|o| o != output);
978 }
979 }
980
981 impl<A: Application> OutputHandler for EngineState<A> {
982 fn output_state(&mut self) -> &mut OutputState {
983 &mut self.output_state
984 }
985
986 fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {
987 let scale = crate::wayland::detect_scale_factor(&self.output_state);
988 crate::scale::set_scale_factor(scale as f32);
989 crate::window_state::set_metric(crate::wayland::detect_metric(&self.output_state, scale));
990 }
991 fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {
992 let scale = crate::wayland::detect_scale_factor(&self.output_state);
993 crate::scale::set_scale_factor(scale as f32);
994 crate::window_state::set_metric(crate::wayland::detect_metric(&self.output_state, scale));
995 }
996 fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _output: wl_output::WlOutput) {}
997 }
998
999 impl<A: Application> ShmHandler for EngineState<A> {
1000 fn shm_state(&mut self) -> &mut Shm {
1001 &mut self.shm_state
1002 }
1003 }
1004
1005 impl<A: Application> ProvidesRegistryState for EngineState<A> {
1006 fn registry(&mut self) -> &mut RegistryState {
1007 &mut self.registry_state
1008 }
1009
1010 fn runtime_add_global(
1011 &mut self,
1012 _conn: &Connection,
1013 _qh: &QueueHandle<Self>,
1014 _name: u32,
1015 _interface: &str,
1016 _version: u32,
1017 ) {}
1018
1019 fn runtime_remove_global(
1020 &mut self,
1021 _conn: &Connection,
1022 _qh: &QueueHandle<Self>,
1023 _name: u32,
1024 _interface: &str,
1025 ) {}
1026 }
1027
1028 impl<A: Application> WindowHandler for EngineState<A> {
1029 fn configure(
1030 &mut self,
1031 _conn: &Connection,
1032 _qh: &QueueHandle<Self>,
1033 _window: &XdgWindow,
1034 configure: WindowConfigure,
1035 _serial: u32,
1036 ) {
1037 let is_fs = configure.is_fullscreen();
1038 let is_max = configure.is_maximized();
1039 crate::scale::set_fullscreen(is_fs);
1040 crate::scale::set_maximized(is_max);
1041
1042 let (w, h) = configure.new_size;
1043 // Configure sizes are window-geometry sizes; with an overflow margin
1044 // the surface is a rim larger on the right and bottom.
1045 let m = self.inner.as_ref().unwrap().overflow_margin() as f32;
1046 if let (Some(w), Some(h)) = (w, h) {
1047 let width = w.get();
1048 let height = h.get();
1049 // Forced mode: the compositor's logical size is really physical
1050 // pixels (scale-1 output); divide to get the app's logical space.
1051 let f = crate::scale::forced_scale().unwrap_or(1.0);
1052 self.frame_logical = (width as f32 / f, height as f32 / f);
1053 self.applied_margin = m;
1054 self.resize(width as f32 / f + m, height as f32 / f + m);
1055 } else if self.inner.as_ref().unwrap().grid() && self.logical_width > 1.0 {
1056 // A grid app's size belongs to its PATCHES: the compositor's
1057 // "you choose" 0x0 must not bounce the surface back to the
1058 // settings size — that thrash recreated multi-hundred-MB
1059 // swapchains per bounce (6.3G peak in 10s). Keep the current
1060 // size; the next grid_patch is the only resizer.
1061 } else {
1062 let settings = self.inner.as_ref().unwrap().settings();
1063 self.frame_logical = (settings.width as f32, settings.height as f32);
1064 self.applied_margin = m;
1065 self.resize(settings.width as f32 + m, settings.height as f32 + m);
1066 }
1067 self.redraw = true;
1068 self.frame_callback_pending = false;
1069 self.first_configure_received = true;
1070 self.just_configured = true;
1071 }
1072
1073 fn request_close(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _window: &XdgWindow) {
1074 self.exit = true;
1075 }
1076 }
1077
1078 impl<A: Application> LayerShellHandler for EngineState<A> {
1079 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {
1080 self.exit = true;
1081 }
1082
1083 fn configure(
1084 &mut self,
1085 _conn: &Connection,
1086 _qh: &QueueHandle<Self>,
1087 _layer: &LayerSurface,
1088 configure: LayerSurfaceConfigure,
1089 _serial: u32,
1090 ) {
1091 // new_size is in logical pixels; 0 means "client decides", so fall back
1092 // to the app's requested size (mirrors the xdg WindowHandler above).
1093 let (w, h) = configure.new_size;
1094 if w > 0 && h > 0 {
1095 self.resize(w as f32, h as f32);
1096 } else {
1097 let settings = self.inner.as_ref().unwrap().settings();
1098 self.resize(settings.width as f32, settings.height as f32);
1099 }
1100 self.redraw = true;
1101 self.frame_callback_pending = false;
1102 self.first_configure_received = true;
1103 self.just_configured = true;
1104 }
1105 }
1106
1107 impl<A: Application> SeatHandler for EngineState<A> {
1108 fn seat_state(&mut self) -> &mut SeatState {
1109 &mut self.seat_state
1110 }
1111
1112 fn new_seat(&mut self, _conn: &Connection, qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
1113 self.ensure_data_device(qh, &seat);
1114 self.seats.push(seat);
1115 }
1116
1117 fn new_capability(
1118 &mut self,
1119 _conn: &Connection,
1120 qh: &QueueHandle<Self>,
1121 seat: wl_seat::WlSeat,
1122 capability: Capability,
1123 ) {
1124 // Every seat arrives here, unlike `new_seat` — SCTK binds the seats
1125 // that already exist at startup without announcing them, so a device
1126 // created only there is never created at all on a normal launch.
1127 self.ensure_data_device(qh, &seat);
1128 if capability == Capability::Pointer && self.pointer.is_none() {
1129 let surface = self.compositor_state.create_surface::<Self>(qh);
1130 let themed_pointer = self.seat_state.get_pointer_with_theme(
1131 qh,
1132 &seat,
1133 self.shm_state.wl_shm(),
1134 surface,
1135 ThemeSpec::System,
1136 ).unwrap();
1137 if let Some(ref pg) = self.pointer_gestures {
1138 self.pinch_gesture = Some(pg.get_pinch_gesture(themed_pointer.pointer(), qh, ()));
1139 }
1140 self.pointer = Some(themed_pointer);
1141 }
1142 if capability == Capability::Keyboard && self.keyboard.is_none() {
1143 let keyboard = self.seat_state.get_keyboard(qh, &seat, None).unwrap();
1144 self.keyboard = Some(keyboard);
1145 if let (Some(m), None) = (&self.text_input_manager, &self.text_input) {
1146 self.text_input = Some(m.get_text_input(&seat, qh, ()));
1147 }
1148 }
1149 if capability == Capability::Touch && self.touch.is_none() {
1150 self.touch = self.seat_state.get_touch(qh, &seat).ok();
1151 }
1152 }
1153
1154 fn remove_capability(
1155 &mut self,
1156 _conn: &Connection,
1157 _qh: &QueueHandle<Self>,
1158 _seat: wl_seat::WlSeat,
1159 capability: Capability,
1160 ) {
1161 if capability == Capability::Pointer {
1162 self.pinch_gesture = None;
1163 self.pointer = None;
1164 }
1165 if capability == Capability::Keyboard {
1166 self.keyboard = None;
1167 self.text_input = None;
1168 }
1169 if capability == Capability::Touch {
1170 self.touch_lost();
1171 }
1172 }
1173
1174 fn remove_seat(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, seat: wl_seat::WlSeat) {
1175 self.seats.retain(|s| s != &seat);
1176 }
1177 }
1178
1179 impl<A: Application> PointerHandler for EngineState<A> {
1180 fn pointer_frame(
1181 &mut self,
1182 _conn: &Connection,
1183 _qh: &QueueHandle<Self>,
1184 _pointer: &wl_pointer::WlPointer,
1185 events: &[smithay_client_toolkit::seat::pointer::PointerEvent],
1186 ) {
1187 use smithay_client_toolkit::seat::pointer::PointerEventKind;
1188 let mut scroll = ScrollFrame::default();
1189 let mut has_scroll = false;
1190 let (mut last_lx, mut last_ly) = (0.0f32, 0.0f32);
1191
1192 // Forced mode: pointer positions arrive in the compositor's scale-1
1193 // logical space (= physical); divide into the app's logical space.
1194 let forced = crate::scale::forced_scale().unwrap_or(1.0);
1195 for event in events {
1196 let (x, y) = event.position;
1197 // Overflow-margin mode needs no translation: the rim is
1198 // right/bottom-only, so frame coords == surface coords.
1199 let lx = x as f32 / forced;
1200 let ly = y as f32 / forced;
1201 // An event on the context menu's popup surface is the app's too,
1202 // at the popup's offset from the window: menu dispatch works in
1203 // window coordinates, which now reach outside the window.
1204 let popup_offset = self.menu_popup_offset(&event.surface);
1205 let on_popup = popup_offset.is_some();
1206 let (lx, ly) = match popup_offset {
1207 Some((ox, oy)) => (lx + ox, ly + oy),
1208 None => (lx, ly),
1209 };
1210 let pos = LogicalPosition::new(lx, ly);
1211
1212 self.driver.cursor_pos = (lx, ly);
1213 match &event.kind {
1214 PointerEventKind::Enter { .. } => {
1215 let (driver, t) = self.turn();
1216 driver.pointer_enter(t, pos);
1217
1218 let cursor_icon = self.cursor_icon_at(lx, ly);
1219 self.current_cursor_icon = Some(cursor_icon);
1220 if let Some(ref themed_pointer) = self.pointer {
1221 let _ = themed_pointer.set_cursor(_conn, cursor_icon);
1222 }
1223 }
1224 PointerEventKind::Leave { .. } => {
1225 self.current_cursor_icon = None;
1226 let (driver, t) = self.turn();
1227 driver.pointer_leave(t);
1228 }
1229 PointerEventKind::Motion { .. } => {
1230 let (driver, t) = self.turn();
1231 driver.pointer_motion(t, pos);
1232
1233 let cursor_icon = self.cursor_icon_at(lx, ly);
1234 if self.current_cursor_icon != Some(cursor_icon) {
1235 self.current_cursor_icon = Some(cursor_icon);
1236 if let Some(ref themed_pointer) = self.pointer {
1237 let _ = themed_pointer.set_cursor(_conn, cursor_icon);
1238 }
1239 }
1240 }
1241 PointerEventKind::Press { button, serial, .. } => {
1242 let Some(btn) = evdev_button(*button) else { continue };
1243 self.last_press_serial = Some(*serial);
1244 let seat = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
1245 let site = PressSite {
1246 size: self.logical_size(),
1247 on_popup,
1248 can_grab: self.window.is_some() && seat.is_some(),
1249 own_edges: false,
1250 };
1251 let (driver, t) = self.turn();
1252 let press = driver.pointer_press(t, btn, pos, site);
1253 if let (Some(window), Some(seat)) = (&self.window, &seat) {
1254 match press {
1255 Press::Dispatched => {}
1256 Press::Resize(edge) => window.resize(seat, *serial, xdg_resize_edge(edge)),
1257 Press::Move => window.move_(seat, *serial),
1258 }
1259 }
1260 }
1261 PointerEventKind::Release { button, .. } => {
1262 let Some(btn) = evdev_button(*button) else { continue };
1263 let (driver, t) = self.turn();
1264 driver.pointer_release(t, btn, pos);
1265 }
1266 PointerEventKind::Axis { horizontal, vertical, source, .. } => {
1267 scroll.h += horizontal.absolute;
1268 scroll.v += vertical.absolute;
1269 scroll.discrete_h += horizontal.discrete;
1270 scroll.discrete_v += vertical.discrete;
1271 // The source and the finger-lift stop ride in the same
1272 // frame as the deltas (or alone, for the lift): they
1273 // decide the smooth-scroll phase.
1274 if let Some(source) = source {
1275 scroll.source = Some(scroll_source(*source));
1276 }
1277 scroll.stop |= horizontal.stop || vertical.stop;
1278 last_lx = lx;
1279 last_ly = ly;
1280 has_scroll = true;
1281 }
1282 }
1283 }
1284
1285 if has_scroll {
1286 let (driver, t) = self.turn();
1287 driver.scroll(t, scroll, LogicalPosition::new(last_lx, last_ly));
1288 }
1289
1290 // App-driven window move/resize (non-standard CSD; see WindowAction):
1291 // executed with the serial of the most recent pointer press.
1292 if let Some(action) = self.inner.as_mut().unwrap().take_window_action() {
1293 if let (Some(ref window), Some(serial)) = (&self.window, self.last_press_serial) {
1294 let seat_owned = self.seats.first().cloned().or_else(|| self.seat_state.seats().next());
1295 if let Some(ref seat) = seat_owned {
1296 match action {
1297 WindowAction::Move => window.move_(seat, serial),
1298 WindowAction::Resize(edge) => window.resize(seat, serial, edge),
1299 }
1300 }
1301 }
1302 }
1303 }
1304 }
1305
1306 /// An evdev button code as one of cce-ui's buttons; the rest are not routed.
1307 fn evdev_button(code: u32) -> Option<MouseButton> {
1308 match code {
1309 272 => Some(MouseButton::Left),
1310 273 => Some(MouseButton::Right),
1311 274 => Some(MouseButton::Middle),
1312 _ => None,
1313 }
1314 }
1315
1316 fn xdg_resize_edge(edge: ResizeEdge) -> xdg_toplevel::ResizeEdge {
1317 match edge {
1318 ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top,
1319 ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom,
1320 ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left,
1321 ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right,
1322 ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft,
1323 ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight,
1324 ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft,
1325 ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight,
1326 }
1327 }
1328
1329 /// A `wl_pointer` axis source as the driver's. Anything newer than the four
1330 /// known sources scrolls as a wheel, as it did when the runner matched on
1331 /// the protocol enum itself.
1332 fn scroll_source(source: wl_pointer::AxisSource) -> ScrollSource {
1333 match source {
1334 wl_pointer::AxisSource::Finger => ScrollSource::Finger,
1335 wl_pointer::AxisSource::Continuous => ScrollSource::Continuous,
1336 wl_pointer::AxisSource::WheelTilt => ScrollSource::WheelTilt,
1337 _ => ScrollSource::Wheel,
1338 }
1339 }
1340
1341 impl<A: Application> KeyboardHandler for EngineState<A> {
1342 fn enter(
1343 &mut self,
1344 _conn: &Connection,
1345 _qh: &QueueHandle<Self>,
1346 _keyboard: &wl_keyboard::WlKeyboard,
1347 _surface: &wl_surface::WlSurface,
1348 _serial: u32,
1349 _raw_modifiers: &[u32],
1350 _keysyms: &[xkeysym::Keysym],
1351 ) {
1352 if let Some(publisher) = self.a11y.as_mut() {
1353 publisher.window_focus(true);
1354 }
1355 let (driver, t) = self.turn();
1356 driver.keyboard_focus(t, true);
1357 }
1358
1359 fn leave(
1360 &mut self,
1361 _conn: &Connection,
1362 _qh: &QueueHandle<Self>,
1363 _keyboard: &wl_keyboard::WlKeyboard,
1364 _surface: &wl_surface::WlSurface,
1365 _serial: u32,
1366 ) {
1367 if let Some(publisher) = self.a11y.as_mut() {
1368 publisher.window_focus(false);
1369 }
1370 let (driver, t) = self.turn();
1371 driver.keyboard_focus(t, false);
1372 }
1373
1374 fn press_key(
1375 &mut self,
1376 _conn: &Connection,
1377 _qh: &QueueHandle<Self>,
1378 _keyboard: &wl_keyboard::WlKeyboard,
1379 _serial: u32,
1380 event: smithay_client_toolkit::seat::keyboard::KeyEvent,
1381 ) {
1382 self.handle_key(event, ElementState::Pressed);
1383 }
1384
1385 fn release_key(
1386 &mut self,
1387 _conn: &Connection,
1388 _qh: &QueueHandle<Self>,
1389 _keyboard: &wl_keyboard::WlKeyboard,
1390 _serial: u32,
1391 event: smithay_client_toolkit::seat::keyboard::KeyEvent,
1392 ) {
1393 self.handle_key(event, ElementState::Released);
1394 }
1395
1396 fn update_modifiers(
1397 &mut self,
1398 _conn: &Connection,
1399 _qh: &QueueHandle<Self>,
1400 _keyboard: &wl_keyboard::WlKeyboard,
1401 _serial: u32,
1402 modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
1403 _layout: u32,
1404 ) {
1405 let mods = Modifiers {
1406 ctrl: modifiers.ctrl,
1407 shift: modifiers.shift,
1408 alt: modifiers.alt,
1409 logo: modifiers.logo,
1410 };
1411 self.driver.set_modifiers(self.inner.as_mut().unwrap(), mods);
1412 }
1413
1414 fn update_repeat_info(
1415 &mut self,
1416 _conn: &Connection,
1417 _qh: &QueueHandle<Self>,
1418 _keyboard: &wl_keyboard::WlKeyboard,
1419 info: smithay_client_toolkit::seat::keyboard::RepeatInfo,
1420 ) {
1421 match info {
1422 smithay_client_toolkit::seat::keyboard::RepeatInfo::Repeat { rate, delay } => {
1423 // Store/expose delay/rate if required by the application
1424 let _ = (rate, delay);
1425 }
1426 smithay_client_toolkit::seat::keyboard::RepeatInfo::Disable => {}
1427 }
1428 }
1429 }
1430
1431 impl<A: Application> EngineState<A> {
1432 fn handle_key(&mut self, event: smithay_client_toolkit::seat::keyboard::KeyEvent, state: ElementState) {
1433 let Some(logical_key) = xkb_logical_key(&event, self.driver.mods.ctrl) else { return };
1434 let (driver, t) = self.turn();
1435 driver.key(t, logical_key, event.utf8, state);
1436 }
1437 }
1438
1439 /// An xkb key event as one of cce-ui's keys, or `None` for a key with no
1440 /// meaning to it (no name here and no text).
1441 fn xkb_logical_key(event: &smithay_client_toolkit::seat::keyboard::KeyEvent, ctrl: bool) -> Option<Key> {
1442 Some(match event.keysym {
1443 xkeysym::Keysym::Escape => Key::Named(NamedKey::Escape),
1444 xkeysym::Keysym::Return => Key::Named(NamedKey::Enter),
1445 xkeysym::Keysym::BackSpace => Key::Named(NamedKey::Backspace),
1446 xkeysym::Keysym::Down => Key::Named(NamedKey::ArrowDown),
1447 xkeysym::Keysym::Up => Key::Named(NamedKey::ArrowUp),
1448 xkeysym::Keysym::Left => Key::Named(NamedKey::ArrowLeft),
1449 xkeysym::Keysym::Right => Key::Named(NamedKey::ArrowRight),
1450 // xkb reports Shift+Tab as ISO_Left_Tab; apps see plain Tab plus
1451 // the shift modifier, matching winit.
1452 xkeysym::Keysym::Tab | xkeysym::Keysym::ISO_Left_Tab => Key::Named(NamedKey::Tab),
1453 xkeysym::Keysym::Delete => Key::Named(NamedKey::Delete),
1454 xkeysym::Keysym::space => Key::Named(NamedKey::Space),
1455 xkeysym::Keysym::Page_Up => Key::Named(NamedKey::PageUp),
1456 xkeysym::Keysym::Page_Down => Key::Named(NamedKey::PageDown),
1457 xkeysym::Keysym::Home => Key::Named(NamedKey::Home),
1458 xkeysym::Keysym::End => Key::Named(NamedKey::End),
1459 xkeysym::Keysym::Super_L | xkeysym::Keysym::Super_R => Key::Named(NamedKey::Super),
1460 xkeysym::Keysym::Alt_L | xkeysym::Keysym::Alt_R => Key::Named(NamedKey::Alt),
1461 xkeysym::Keysym::Control_L | xkeysym::Keysym::Control_R => Key::Named(NamedKey::Control),
1462 xkeysym::Keysym::Shift_L | xkeysym::Keysym::Shift_R => Key::Named(NamedKey::Shift),
1463 xkeysym::Keysym::F1 => Key::Named(NamedKey::F1),
1464 xkeysym::Keysym::F2 => Key::Named(NamedKey::F2),
1465 xkeysym::Keysym::F3 => Key::Named(NamedKey::F3),
1466 xkeysym::Keysym::F4 => Key::Named(NamedKey::F4),
1467 xkeysym::Keysym::F5 => Key::Named(NamedKey::F5),
1468 xkeysym::Keysym::F6 => Key::Named(NamedKey::F6),
1469 xkeysym::Keysym::F7 => Key::Named(NamedKey::F7),
1470 xkeysym::Keysym::F8 => Key::Named(NamedKey::F8),
1471 xkeysym::Keysym::F9 => Key::Named(NamedKey::F9),
1472 xkeysym::Keysym::F10 => Key::Named(NamedKey::F10),
1473 xkeysym::Keysym::F11 => Key::Named(NamedKey::F11),
1474 xkeysym::Keysym::F12 => Key::Named(NamedKey::F12),
1475 _ => {
1476 // With Ctrl held, xkb's utf8 goes through the legacy control-character
1477 // transformation (ctrl+j = "\n", ctrl+a = 0x01, ...); the keysym is
1478 // untransformed, so prefer it there or ctrl+<letter> shortcuts can
1479 // never match their letter.
1480 let from_keysym = || event.keysym.key_char().map(|ch| ch.to_string());
1481 let text = if ctrl { from_keysym().or_else(|| event.utf8.clone()) } else { event.utf8.clone().or_else(from_keysym) };
1482 Key::Character(text?)
1483 }
1484 })
1485 }
1486
1487 impl<A: Application> wayland_client::Dispatch<wl_registry::WlRegistry, GlobalList, Self> for EngineState<A> {
1488 fn event(
1489 _state: &mut Self,
1490 _proxy: &wl_registry::WlRegistry,
1491 _event: wl_registry::Event,
1492 _data: &GlobalList,
1493 _conn: &Connection,
1494 _qh: &QueueHandle<Self>,
1495 ) {}
1496 }
1497
1498 impl<A: Application> wayland_client::Dispatch<crate::protocol::zcce_inspector_v1::ZcceInspectorV1, ()> for EngineState<A> {
1499 fn event(
1500 _state: &mut Self,
1501 _proxy: &crate::protocol::zcce_inspector_v1::ZcceInspectorV1,
1502 _event: crate::protocol::zcce_inspector_v1::Event,
1503 _data: &(),
1504 _conn: &Connection,
1505 _qh: &QueueHandle<Self>,
1506 ) {}
1507 }
1508
1509 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, ()> for EngineState<A> {
1510 fn event(
1511 _state: &mut Self,
1512 _proxy: &crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1,
1513 _event: crate::protocol::cce_window_management_v1::zcce_window_manager_v1::Event,
1514 _data: &(),
1515 _conn: &Connection,
1516 _qh: &QueueHandle<Self>,
1517 ) {}
1518
1519 wayland_client::event_created_child!(
1520 EngineState<A>,
1521 crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1,
1522 [
1523 6 => (crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1, ()),
1524 7 => (crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1, ()),
1525 8 => (crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1, ()),
1526 ]
1527 );
1528 }
1529
1530 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1, ()> for EngineState<A> {
1531 fn event(
1532 _state: &mut Self,
1533 _proxy: &crate::protocol::cce_window_management_v1::zcce_window_v1::ZcceWindowV1,
1534 _event: crate::protocol::cce_window_management_v1::zcce_window_v1::Event,
1535 _data: &(),
1536 _conn: &Connection,
1537 _qh: &QueueHandle<Self>,
1538 ) {}
1539 }
1540
1541 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1, ()> for EngineState<A> {
1542 fn event(
1543 _state: &mut Self,
1544 _proxy: &crate::protocol::cce_window_management_v1::zcce_output_v1::ZcceOutputV1,
1545 _event: crate::protocol::cce_window_management_v1::zcce_output_v1::Event,
1546 _data: &(),
1547 _conn: &Connection,
1548 _qh: &QueueHandle<Self>,
1549 ) {}
1550 }
1551
1552 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1, ()> for EngineState<A> {
1553 fn event(
1554 _state: &mut Self,
1555 _proxy: &crate::protocol::cce_window_management_v1::zcce_seat_v1::ZcceSeatV1,
1556 _event: crate::protocol::cce_window_management_v1::zcce_seat_v1::Event,
1557 _data: &(),
1558 _conn: &Connection,
1559 _qh: &QueueHandle<Self>,
1560 ) {}
1561 }
1562
1563 impl<A: Application> wayland_client::Dispatch<crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1, ()> for EngineState<A> {
1564 fn event(
1565 state: &mut Self,
1566 _proxy: &crate::protocol::cce_window_management_v1::zcce_toplevel_v1::ZcceToplevelV1,
1567 event: crate::protocol::cce_window_management_v1::zcce_toplevel_v1::Event,
1568 _data: &(),
1569 _conn: &Connection,
1570 _qh: &QueueHandle<Self>,
1571 ) {
1572 use crate::protocol::cce_window_management_v1::zcce_toplevel_v1::Event;
1573 if let Event::GridPatch { serial, x, y, width, height, scale } = event {
1574 // A newer patch supersedes an unconsumed older one.
1575 state.pending_grid_patch = Some((serial, x, y, width, height, scale));
1576 state.redraw = true;
1577 }
1578 }
1579 }
1580
1581 delegate_compositor!(@<A: Application> EngineState<A>);
1582 delegate_xdg_shell!(@<A: Application> EngineState<A>);
1583 delegate_xdg_window!(@<A: Application> EngineState<A>);
1584 delegate_layer!(@<A: Application> EngineState<A>);
1585 delegate_shm!(@<A: Application> EngineState<A>);
1586 delegate_seat!(@<A: Application> EngineState<A>);
1587 delegate_pointer!(@<A: Application> EngineState<A>);
1588 delegate_keyboard!(@<A: Application> EngineState<A>);
1589 delegate_registry!(@<A: Application> EngineState<A>);
1590 delegate_output!(@<A: Application> EngineState<A>);
1591
1592 impl<A: Application> wayland_client::Dispatch<wl_region::WlRegion, ()> for EngineState<A> {
1593 fn event(
1594 _state: &mut Self,
1595 _proxy: &wl_region::WlRegion,
1596 _event: wl_region::Event,
1597 _data: &(),
1598 _conn: &Connection,
1599 _qh: &QueueHandle<Self>,
1600 ) {}
1601 }
1602
1603 impl<A: Application> wayland_client::Dispatch<wl_callback::WlCallback, ()> for EngineState<A> {
1604 fn event(
1605 state: &mut Self,
1606 _proxy: &wl_callback::WlCallback,
1607 event: wl_callback::Event,
1608 _data: &(),
1609 _conn: &Connection,
1610 _qh: &QueueHandle<Self>,
1611 ) {
1612 if let wl_callback::Event::Done { .. } = event {
1613 state.frame_callback_pending = false;
1614 if crate::vk::present_debug() {
1615 let t = debug_clock_ms();
1616 let waited = state.frame_callback_armed_at.map(|a| a.elapsed().as_millis()).unwrap_or(0);
1617 eprintln!("[vk] t={} frame-done (waited {}ms)", t, waited);
1618 }
1619 }
1620 }
1621 }
1622
1623 impl<A: Application> wayland_client::Dispatch<ZwpTextInputManagerV3, ()> for EngineState<A> {
1624 fn event(
1625 _state: &mut Self,
1626 _proxy: &ZwpTextInputManagerV3,
1627 _event: <ZwpTextInputManagerV3 as wayland_client::Proxy>::Event,
1628 _data: &(),
1629 _conn: &Connection,
1630 _qh: &QueueHandle<Self>,
1631 ) {
1632 }
1633 }
1634
1635 /// The input method's events (see `backend::text_input`): the focus, and
1636 /// the double-buffered composition, commit and deletion, applied on `done`.
1637 impl<A: Application> wayland_client::Dispatch<ZwpTextInputV3, ()> for EngineState<A> {
1638 fn event(
1639 state: &mut Self,
1640 _proxy: &ZwpTextInputV3,
1641 event: zwp_text_input_v3::Event,
1642 _data: &(),
1643 _conn: &Connection,
1644 _qh: &QueueHandle<Self>,
1645 ) {
1646 use crate::backend::text_input::Apply;
1647 match event {
1648 zwp_text_input_v3::Event::Enter { .. } => {
1649 state.text_input_state.enter();
1650 // At once, at the last frame's caret: an idle window builds
1651 // no frame to do it.
1652 state.sync_text_input();
1653 }
1654 zwp_text_input_v3::Event::Leave { .. } => {
1655 if state.text_input_state.leave() {
1656 if let Some(ti) = &state.text_input {
1657 ti.disable();
1658 ti.commit();
1659 }
1660 }
1661 let (driver, t) = state.turn();
1662 driver.preedit(t, None);
1663 }
1664 zwp_text_input_v3::Event::PreeditString { text, cursor_begin, cursor_end } => {
1665 state.text_input_state.pending.set_preedit(text, cursor_begin, cursor_end);
1666 }
1667 zwp_text_input_v3::Event::CommitString { text } => {
1668 state.text_input_state.pending.commit = text;
1669 }
1670 zwp_text_input_v3::Event::DeleteSurroundingText { before_length, after_length } => {
1671 state.text_input_state.pending.delete = Some((before_length, after_length));
1672 }
1673 zwp_text_input_v3::Event::Done { .. } => {
1674 for step in state.text_input_state.done().apply_order() {
1675 let (driver, t) = state.turn();
1676 match step {
1677 Apply::Preedit(p) => driver.preedit(t, p),
1678 Apply::Commit(text) => driver.commit_text(t, text),
1679 }
1680 }
1681 }
1682 _ => {}
1683 }
1684 }
1685 }
1686
1687 impl<A: Application> wayland_client::Dispatch<ZwpPointerGesturesV1, ()> for EngineState<A> {
1688 fn event(
1689 _state: &mut Self,
1690 _proxy: &ZwpPointerGesturesV1,
1691 _event: zwp_pointer_gestures::Event,
1692 _data: &(),
1693 _conn: &Connection,
1694 _qh: &QueueHandle<Self>,
1695 ) {}
1696 }
1697
1698 impl<A: Application> wayland_client::Dispatch<ZwpPointerGesturePinchV1, ()> for EngineState<A> {
1699 fn event(
1700 state: &mut Self,
1701 _proxy: &ZwpPointerGesturePinchV1,
1702 event: zwp_pointer_gesture_pinch_v1::Event,
1703 _data: &(),
1704 _conn: &Connection,
1705 _qh: &QueueHandle<Self>,
1706 ) {
1707 match event {
1708 zwp_pointer_gesture_pinch_v1::Event::Begin { .. } => state.driver.pinch_begin(),
1709 zwp_pointer_gesture_pinch_v1::Event::Update { scale, .. } => {
1710 let (driver, t) = state.turn();
1711 driver.pinch_update(t, scale as f32);
1712 }
1713 zwp_pointer_gesture_pinch_v1::Event::End { .. } => state.driver.pinch_end(),
1714 _ => {}
1715 }
1716 }
1717 }
1718
1719 /// Why a session's event loop stopped.
1720 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1721 enum SessionEnd {
1722 /// The app asked to exit.
1723 AppExit,
1724 /// The compositor connection died while the compositor itself may well be
1725 /// alive — a broken transport. The `Application` is intact and can be
1726 /// re-attached to a fresh connection.
1727 ConnectionLost,
1728 /// Nothing answered at the display socket: the compositor this app
1729 /// belonged to is gone. A deliberate exit unlinks the socket and a crash
1730 /// leaves it refusing; either way there is no session left to rejoin.
1731 NoCompositor,
1732 }
1733
1734 /// What [`run`] does once a session has ended.
1735 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1736 enum AfterSession {
1737 /// Leave the process-lifetime loop: run `on_exit` and quit.
1738 Exit,
1739 /// Sleep this long, then open a fresh session on the same `Application`.
1740 Reconnect(std::time::Duration),
1741 /// The compositor is gone and the app outlives it
1742 /// ([`Application::outlives_compositor`]): wait for a successor's socket,
1743 /// then open a fresh session on the same `Application`.
1744 AwaitCompositor,
1745 }
1746
1747 /// The display socket this process connects to: `$WAYLAND_DISPLAY` (absolute,
1748 /// or a name under `$XDG_RUNTIME_DIR`), `wayland-0` when unset — the lookup
1749 /// `Connection::connect_to_env` makes.
1750 fn wayland_socket_path() -> Option<std::path::PathBuf> {
1751 let name = std::env::var_os("WAYLAND_DISPLAY").unwrap_or_else(|| "wayland-0".into());
1752 let name = std::path::PathBuf::from(name);
1753 if name.is_absolute() {
1754 return Some(name);
1755 }
1756 Some(std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR")?).join(name))
1757 }
1758
1759 /// Sleep until the display socket exists again — the successor compositor
1760 /// has bound it. Polled at 250 ms: a quarter-second after the next login is
1761 /// soon enough, and a daemon waiting through a logged-out hour costs four
1762 /// `stat`s a second. A stale socket a crash left behind satisfies the poll
1763 /// and fails the connect, which comes back here after the same pause.
1764 fn await_compositor_socket() {
1765 loop {
1766 std::thread::sleep(std::time::Duration::from_millis(250));
1767 match wayland_socket_path() {
1768 Some(path) if path.exists() => return,
1769 Some(_) => {}
1770 // No runtime dir to look in: keep trying the connect itself.
1771 None => return,
1772 }
1773 }
1774 }
1775
1776 /// How many consecutive failed reconnects before giving up. Reset once a
1777 /// session has survived [`RECONNECT_RESET`], so a long-lived window that loses
1778 /// its connection twice in a day still gets a full budget the second time.
1779 const RECONNECT_ATTEMPTS: u32 = 8;
1780 const RECONNECT_RESET: std::time::Duration = std::time::Duration::from_secs(10);
1781
1782 /// Decide whether a finished session is followed by another.
1783 ///
1784 /// `lived` is how long the session that just ended lasted, `has_app` whether
1785 /// an `Application` exists to carry over, and `attempt` the running count of
1786 /// consecutive reconnects (reset here once a session outlives
1787 /// [`RECONNECT_RESET`]).
1788 ///
1789 /// Only a lost connection is retried, and only while the compositor is still
1790 /// there to reconnect to. A reconnect is a repair of THIS session's transport
1791 /// — the fd-exhaustion break `raise_fd_limit` documents — not a way to outlive
1792 /// the compositor. When the connect itself fails the compositor has exited,
1793 /// and it has already saved this window for restore: the next compositor
1794 /// respawns the app from `state.json` on its own. A client that kept
1795 /// retrying instead (the backoff below spans ~25s) reattached to that
1796 /// successor beside the respawned copy, and every restore after a forced
1797 /// exit or a crash came up with two of each cce-ui window. So the process
1798 /// exits, as a Wayland client whose display went away always has.
1799 ///
1800 /// Unless the app OUTLIVES the compositor (`outlives`,
1801 /// [`Application::outlives_compositor`]) — a daemon the compositor does not
1802 /// restore. Then there is no copy to collide with and every reason to stay:
1803 /// it waits for the successor and rejoins it.
1804 fn after_session(
1805 end: SessionEnd,
1806 has_app: bool,
1807 lived: std::time::Duration,
1808 attempt: &mut u32,
1809 outlives: bool,
1810 ) -> AfterSession {
1811 match end {
1812 SessionEnd::NoCompositor if has_app && outlives => {
1813 *attempt = 0;
1814 AfterSession::AwaitCompositor
1815 }
1816 SessionEnd::AppExit | SessionEnd::NoCompositor => AfterSession::Exit,
1817 SessionEnd::ConnectionLost => {
1818 // Nothing to preserve if we never got as far as building the
1819 // app — that is a failure to start, not a lost window.
1820 if !has_app {
1821 return AfterSession::Exit;
1822 }
1823 if lived > RECONNECT_RESET {
1824 *attempt = 0;
1825 }
1826 *attempt += 1;
1827 if *attempt > RECONNECT_ATTEMPTS {
1828 return AfterSession::Exit;
1829 }
1830 AfterSession::Reconnect(std::time::Duration::from_millis(
1831 100 * (1 << (*attempt).min(6)),
1832 ))
1833 }
1834 }
1835 }
1836
1837 /// Raise this process's file-descriptor soft limit toward its hard limit.
1838 ///
1839 /// A cce-ui client's fd usage is not bounded by anything the app controls.
1840 /// Every dmabuf-feedback event the compositor sends carries a format-table
1841 /// fd, and those arrive per surface whenever scanout candidacy changes —
1842 /// entering the overview re-sends one for every window at once. Long-lived
1843 /// windows sit at 700+ open fds in normal use, against a soft limit of 1024.
1844 ///
1845 /// Crossing that limit does not fail politely. `recvmsg` drops the SCM_RIGHTS
1846 /// payload when it cannot allocate descriptors, while still delivering the
1847 /// message body — so libwayland hits a message whose fd never arrived,
1848 /// reports "file descriptor expected", and the connection dies. That is
1849 /// precisely the transport break [`run`] reconnects from below, at the cost
1850 /// of a rebuilt window.
1851 ///
1852 /// The compositor raises itself to 65536 for the same reason and then
1853 /// deliberately restores the inherited limit for the programs it spawns
1854 /// (cce-compositor `process.rs::cleanup_child`) — right for an arbitrary
1855 /// child, far too low for a dmabuf-heavy Wayland client. So each client
1856 /// raises its own, to the same ceiling.
1857 fn raise_fd_limit() {
1858 unsafe {
1859 let mut lim: libc::rlimit = std::mem::zeroed();
1860 if libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) != 0 {
1861 return;
1862 }
1863 let want = std::cmp::min(65536, lim.rlim_max);
1864 if lim.rlim_cur >= want {
1865 return;
1866 }
1867 let raised = libc::rlimit { rlim_cur: want, rlim_max: lim.rlim_max };
1868 if libc::setrlimit(libc::RLIMIT_NOFILE, &raised) == 0 {
1869 log::info!("[window_runner] fd limit raised {} -> {}", lim.rlim_cur, want);
1870 } else {
1871 log::warn!("[window_runner] could not raise fd limit from {}", lim.rlim_cur);
1872 }
1873 }
1874 }
1875
1876 /// Run an [`Application`] to completion, surviving loss of the compositor
1877 /// connection.
1878 ///
1879 /// A Wayland connection cannot be repaired once its transport state breaks — a
1880 /// single dropped file descriptor on a dmabuf-feedback event is enough, and
1881 /// libwayland then fails every dispatch with `EINVAL`. Exiting the process on
1882 /// that error (the old behavior) threw away everything the window held: a
1883 /// terminal's shell and scrollback, an editor's unsaved buffer.
1884 ///
1885 /// So a connection is one *session*. Objects that belong to the connection —
1886 /// the Wayland globals, the surface, the swapchain, the renderer — are rebuilt
1887 /// per session. The things that carry user state outlive it: the `Application`
1888 /// itself, the calloop loop, and the message channel. Keeping the **same
1889 /// channel** matters as much as keeping the app: worker threads hold clones of
1890 /// its `Sender` (cce-terminal's pty reader is the canonical case), and a fresh
1891 /// channel would orphan them into a live-but-deaf process.
1892 ///
1893 /// What is repaired is the transport, never the compositor: a reconnect only
1894 /// goes through while the compositor that owned the lost session is still
1895 /// listening. If the connect itself fails the compositor has exited, and the
1896 /// process exits with it — see [`after_session`] for why staying alive there
1897 /// duplicated every window on the next session restore.
1898 ///
1899 /// Caveat: GPU resources belong to the renderer, so a rebuild re-runs
1900 /// [`Application::renderer_init`]. Images uploaded outside it (e.g. in
1901 /// [`Application::new`]) are not replayed into the new renderer — upload from
1902 /// `renderer_init` if they must survive a reconnect.
1903 pub fn run<A: Application>() {
1904 raise_fd_limit();
1905 // This window's interaction state (menu, hover highlight, swipe, composition), current
1906 // for the whole run: every session, every callback (`crate::window_state`). It outlives
1907 // a reconnect, as the app does, so a menu open across one stays open.
1908 let window_state = crate::window_state::WindowState::new();
1909 let _window = crate::window_state::enter(&window_state);
1910
1911 // Outlives every session: worker threads hold this Sender, and the app's
1912 // own event sources are registered on this loop once.
1913 let (sender, channel) = calloop::channel::channel::<A::Message>();
1914 // Drop payloads come back from the per-drop reader threads (see
1915 // `backend::dnd`); registered once, like the app channel, because the
1916 // loop outlives a reconnect while the EngineState does not.
1917 let (drop_tx, drop_rx) =
1918 calloop::channel::channel::<crate::backend::dnd::DroppedData>();
1919 // The accessibility adapter's callbacks (`backend::a11y_unix`), from its own thread;
1920 // registered once, as the loop outlives a reconnect.
1921 let (a11y_tx, a11y_rx) = calloop::channel::channel::<crate::backend::a11y_unix::Event>();
1922 let mut event_loop = match EventLoop::try_new() {
1923 Ok(l) => l,
1924 Err(e) => {
1925 log::error!("[window_runner] cannot create event loop: {e}");
1926 return;
1927 }
1928 };
1929 event_loop
1930 .handle()
1931 .insert_source(channel, |event, _metadata, app_state: &mut EngineState<A>| {
1932 if let calloop::channel::Event::Msg(msg) = event {
1933 let mut rebuild = false;
1934 app_state.inner.as_mut().unwrap().update(msg, &mut rebuild, &mut app_state.exit);
1935 if rebuild {
1936 app_state.redraw = true;
1937 }
1938 }
1939 })
1940 .unwrap();
1941 event_loop
1942 .handle()
1943 .insert_source(drop_rx, |event, _metadata, app_state: &mut EngineState<A>| {
1944 if let calloop::channel::Event::Msg(drop) = event {
1945 // The transfer is complete, so the source can be released now
1946 // — doing it any earlier costs the payload.
1947 if let Some(offer) = app_state.pending_drop_offer.take() {
1948 offer.finish();
1949 offer.destroy();
1950 }
1951 let mut rebuild = false;
1952 if let Some(app) = app_state.inner.as_mut() {
1953 app.handle_drop(&drop.mime, &drop.bytes, drop.pos, &mut rebuild);
1954 }
1955 if rebuild {
1956 app_state.redraw = true;
1957 }
1958 }
1959 })
1960 .unwrap();
1961
1962 event_loop
1963 .handle()
1964 .insert_source(a11y_rx, |event, _metadata, app_state: &mut EngineState<A>| {
1965 use crate::backend::a11y_unix::{act, Acted, Event};
1966 let calloop::channel::Event::Msg(event) = event else { return };
1967 if app_state.inner.is_none() {
1968 return;
1969 }
1970 if let Event::Action(request) = &event {
1971 match act(app_state.inner.as_mut().unwrap(), request) {
1972 Acted::Nothing => return,
1973 Acted::Changed => app_state.redraw = true,
1974 Acted::Key(key) => {
1975 app_state.redraw = true;
1976 let (driver, t) = app_state.turn();
1977 driver.press_named_key(t, key);
1978 }
1979 }
1980 }
1981 let (Some(publisher), Some(app)) = (app_state.a11y.as_mut(), app_state.inner.as_mut()) else { return };
1982 match event {
1983 // Published from here, not at the next frame: an idle window renders none,
1984 // and AccessKit wants the tree by the next refresh.
1985 Event::Activated => {
1986 if crate::backend::a11y_unix::debug() {
1987 eprintln!("[a11y] a screen reader connected");
1988 }
1989 publisher.publish(app, crate::scale::scale_factor() as f64)
1990 }
1991 Event::Deactivated => {}
1992 // Carried out above; the tree it left is published now, as on arrival.
1993 Event::Action(_) => publisher.publish(app, crate::scale::scale_factor() as f64),
1994 }
1995 })
1996 .unwrap();
1997
1998 let mut app: Option<A> = None;
1999 let mut sources_registered = false;
2000 let mut attempt: u32 = 0;
2001
2002 loop {
2003 let started = std::time::Instant::now();
2004 let (returned_app, end) =
2005 run_session(&mut event_loop, sender.clone(), drop_tx.clone(), a11y_tx.clone(), app.take(), !sources_registered);
2006 app = returned_app;
2007 sources_registered = true;
2008
2009 let outlives = app.as_ref().is_some_and(|a| a.outlives_compositor());
2010 match after_session(end, app.is_some(), started.elapsed(), &mut attempt, outlives) {
2011 AfterSession::Exit => {
2012 match end {
2013 SessionEnd::AppExit => {}
2014 SessionEnd::NoCompositor if app.is_some() => log::warn!(
2015 "[window_runner] compositor is gone; exiting (its successor restores the session itself)"
2016 ),
2017 SessionEnd::NoCompositor => {
2018 log::error!("[window_runner] no compositor connection; giving up")
2019 }
2020 SessionEnd::ConnectionLost if app.is_some() => log::error!(
2021 "[window_runner] connection lost; giving up after {} attempts",
2022 attempt - 1
2023 ),
2024 SessionEnd::ConnectionLost => {
2025 log::error!("[window_runner] no compositor connection; giving up")
2026 }
2027 }
2028 break;
2029 }
2030 AfterSession::Reconnect(backoff) => {
2031 log::warn!(
2032 "[window_runner] compositor connection lost; reconnecting in {backoff:?} (attempt {attempt})"
2033 );
2034 std::thread::sleep(backoff);
2035 }
2036 AfterSession::AwaitCompositor => {
2037 log::warn!("[window_runner] compositor is gone; waiting for the next one");
2038 await_compositor_socket();
2039 log::info!("[window_runner] a compositor is back; rejoining");
2040 }
2041 }
2042 }
2043
2044 if let Some(mut app) = app {
2045 app.on_exit();
2046 }
2047 }
2048
2049 /// One connection's lifetime: connect, build the surface and renderer, pump
2050 /// events until the app exits or the connection dies. Returns the
2051 /// `Application` so the caller can hand it to the next session.
2052 fn run_session<'l, A: Application>(
2053 event_loop: &mut EventLoop<'l, EngineState<A>>,
2054 sender: calloop::channel::Sender<A::Message>,
2055 drop_tx: calloop::channel::Sender<crate::backend::dnd::DroppedData>,
2056 a11y_tx: calloop::channel::Sender<crate::backend::a11y_unix::Event>,
2057 existing_app: Option<A>,
2058 register_app_sources: bool,
2059 ) -> (Option<A>, SessionEnd) {
2060 let conn = match Connection::connect_to_env() {
2061 Ok(c) => c,
2062 Err(e) => {
2063 log::error!("[window_runner] cannot connect to compositor: {e}");
2064 return (existing_app, SessionEnd::NoCompositor);
2065 }
2066 };
2067 let (globals, mut event_queue) = match registry_queue_init(&conn) {
2068 Ok(v) => v,
2069 Err(e) => {
2070 log::error!("[window_runner] registry init failed: {e}");
2071 return (existing_app, SessionEnd::ConnectionLost);
2072 }
2073 };
2074 let qh = event_queue.handle();
2075
2076 let compositor_state = CompositorState::bind(&globals, &qh).unwrap();
2077 let xdg_shell_state = XdgShell::bind(&globals, &qh).unwrap();
2078 let layer_shell_state = LayerShell::bind(&globals, &qh).ok();
2079 let shm_state = Shm::bind(&globals, &qh).unwrap();
2080 let seat_state = SeatState::new(&globals, &qh);
2081 let output_state = OutputState::new(&globals, &qh);
2082
2083 let pointer_gestures: Option<ZwpPointerGesturesV1> = globals.bind(&qh, 1..=3, ()).ok();
2084 let text_input_manager: Option<ZwpTextInputManagerV3> = globals.bind(&qh, 1..=1, ()).ok();
2085
2086 let mut engine_state = EngineState {
2087 a11y: None,
2088 data_device_manager: DataDeviceManagerState::bind(&globals, &qh).ok(),
2089 data_devices: Vec::new(),
2090 drag_mime: None,
2091 drag_pos: LogicalPosition::new(0.0, 0.0),
2092 drop_tx: Some(drop_tx),
2093 pending_drop_offer: None,
2094 applied_input_regions: None,
2095 registry_state: RegistryState::new(&globals),
2096 compositor_state,
2097 xdg_shell_state,
2098 layer_shell_state,
2099 shm_state,
2100 seat_state,
2101 output_state,
2102 seats: Vec::new(),
2103 pointer: None,
2104 keyboard: None,
2105 window: None,
2106 layer_surface: None,
2107 is_layer_app: false,
2108 layer_hidden: false,
2109 surface: None,
2110 inner: None,
2111 renderer: None,
2112 font_system: None,
2113 swash_cache: cosmic_text::SwashCache::new(),
2114 scale_factor: 1.0,
2115 committed_buffer_scale: 1,
2116 entered_outputs: Vec::new(),
2117 logical_width: 0.0,
2118 logical_height: 0.0,
2119 frame_logical: (0.0, 0.0),
2120 applied_margin: 0.0,
2121 overflow_was_active: false,
2122 sent_popover_region: None,
2123 menu_popup: None,
2124 menu_renderer: None,
2125 menu_icon_ids: std::collections::HashMap::new(),
2126 display_ptr: 0,
2127 exit: false,
2128 redraw: false,
2129 damage_owed: true,
2130 frame_record: Default::default(),
2131 frame_callback_pending: false,
2132 frame_callback_armed_at: None,
2133 keepalive_pending: false,
2134 keepalive_armed_at: None,
2135 extent_gate_skips: 0,
2136 first_configure_received: false,
2137 driver: Driver::new(),
2138 sender,
2139 current_cursor_icon: None,
2140 qh: qh.clone(),
2141 just_configured: false,
2142 pointer_gestures,
2143 text_input_manager,
2144 text_input: None,
2145 text_input_state: Default::default(),
2146 pinch_gesture: None,
2147 cce_toplevel: None,
2148 pending_grid_patch: None,
2149 last_press_serial: None,
2150 touch: None,
2151 touch_tracker: Default::default(),
2152 touch_offset: (0.0, 0.0),
2153 touch_scroll_at: None,
2154 dl_text_items: Vec::new(),
2155 };
2156
2157 if let Err(e) = event_queue.roundtrip(&mut engine_state) {
2158 log::error!("[window_runner] initial roundtrip failed: {e}");
2159 return (existing_app, SessionEnd::ConnectionLost);
2160 }
2161
2162 let scale = detect_scale_factor(&engine_state.output_state);
2163 engine_state.scale_factor = scale;
2164 crate::scale::set_scale_factor(scale as f32);
2165 crate::window_state::set_metric(crate::wayland::detect_metric(&engine_state.output_state, scale));
2166
2167 // A reconnect re-attaches the SAME app: its state is the thing worth
2168 // saving, and `A::new` would both discard it and hand a fresh Sender to
2169 // worker threads that are still holding the original.
2170 let inner = match existing_app {
2171 Some(app) => app,
2172 None => A::create(AppSender::from(engine_state.sender.clone())),
2173 };
2174 if crate::backend::a11y_unix::wanted(&inner) {
2175 engine_state.a11y = crate::backend::a11y_unix::Publisher::start(a11y_tx);
2176 }
2177 let settings = inner.settings();
2178 crate::scale::set_app_id(settings.app_id.clone());
2179 engine_state.logical_width = settings.width as f32;
2180 engine_state.logical_height = settings.height as f32;
2181 engine_state.inner = Some(inner);
2182
2183 let surface = engine_state.compositor_state.create_surface(&qh);
2184 // A grid app's surface is pinned to scale 1: the patch's `scale` is
2185 // BUFFER px per virtual unit and already carries the output scale (the
2186 // patch manager folds it in), so adopting the output scale here would
2187 // square it — the client renders a doubled buffer and the compositor
2188 // downsamples it straight back into blur.
2189 if engine_state.inner.as_ref().unwrap().grid() {
2190 engine_state.scale_factor = 1.0;
2191 }
2192 // Forced-scale mode renders scaled-up into a buffer_scale-1 surface.
2193 let buffer_scale = if crate::scale::forced_scale().is_some()
2194 || engine_state.inner.as_ref().unwrap().grid()
2195 {
2196 1
2197 } else {
2198 scale as i32
2199 };
2200 surface.set_buffer_scale(buffer_scale);
2201 engine_state.committed_buffer_scale = buffer_scale;
2202
2203 let layer_settings = engine_state.inner.as_ref().unwrap().layer();
2204 if let Some(ls) = layer_settings {
2205 engine_state.is_layer_app = true;
2206 engine_state.attach_layer_role(&surface, &ls, settings.width, settings.height);
2207 } else {
2208 let window = engine_state.xdg_shell_state.create_window(surface.clone(), WindowDecorations::None, &qh);
2209 window.set_title(&settings.title);
2210 window.set_app_id(&settings.app_id);
2211 if settings.fullscreen {
2212 window.set_fullscreen(None);
2213 }
2214 if let Some((min_w, min_h)) = settings.min_size {
2215 window.set_min_size(Some((min_w, min_h)));
2216 }
2217 let wants_utility = engine_state.inner.as_ref().unwrap().utility();
2218 let wants_grid = engine_state.inner.as_ref().unwrap().grid();
2219 {
2220 // Bound for EVERY app now, not just utility/grid ones: the
2221 // toplevel also carries the popover-region hint (manager v7),
2222 // which any app with a dropdown wants. Role declarations go
2223 // BEFORE the initial commit so the mode is set by the time the
2224 // compositor maps the window. Version floors: set_utility
2225 // appeared at manager 5, the grid role at 6; the range tops at 7
2226 // so a newer compositor grants the hint and an older one simply
2227 // yields a lower-versioned toplevel — the hint send is gated on
2228 // version() >= 7 (send_popover_region), and on a pre-5
2229 // compositor the bind fails and the app runs plain.
2230 let version = if wants_grid { 6..=7 } else { 5..=7 };
2231 match globals.bind::<crate::protocol::cce_window_management_v1::zcce_window_manager_v1::ZcceWindowManagerV1, _, _>(&qh, version, ()) {
2232 Ok(cce_wm) => {
2233 let toplevel = cce_wm.get_cce_toplevel(&surface, &qh, ());
2234 if wants_utility {
2235 toplevel.set_utility();
2236 }
2237 if wants_grid {
2238 toplevel.set_grid();
2239 }
2240 engine_state.cce_toplevel = Some(toplevel);
2241 }
2242 Err(e) => {
2243 log::warn!("[window_runner] cce window-management declaration unavailable: {e}");
2244 }
2245 }
2246 }
2247 window.commit();
2248 engine_state.window = Some(window);
2249 }
2250 engine_state.surface = Some(surface);
2251
2252 // Overflow-margin mode: the surface (and so the GPU swapchain) is a rim
2253 // larger than the window frame on every side; geometry/input-region are
2254 // published per-resize.
2255 let rim = 2.0 * engine_state.inner.as_ref().unwrap().overflow_margin() as f32;
2256 if let Err(lost) = engine_state.init_gpu(&conn, settings.width as f32 + rim, settings.height as f32 + rim) {
2257 log::error!("[window_runner] cannot create the renderer, ending session: {lost}");
2258 return (engine_state.inner.take(), SessionEnd::ConnectionLost);
2259 }
2260 engine_state
2261 .inner
2262 .as_mut()
2263 .unwrap()
2264 .renderer_init(engine_state.renderer.as_mut().unwrap());
2265
2266 let loop_handle = event_loop.handle();
2267 let wayland_token = match WaylandSource::new(conn.clone(), event_queue).insert(loop_handle.clone())
2268 {
2269 Ok(token) => token,
2270 Err(e) => {
2271 log::error!("[window_runner] cannot register the wayland source: {e}");
2272 return (engine_state.inner.take(), SessionEnd::ConnectionLost);
2273 }
2274 };
2275
2276 // The app's own sources live on the persistent loop, so they are registered
2277 // once for the process — re-registering per session would double-deliver
2278 // every event on them.
2279 if register_app_sources {
2280 engine_state.inner.as_mut().unwrap().register_sources(&loop_handle);
2281 }
2282
2283 /// Same switch as the renderer's present tracer, resolved once — this sits
2284 /// in the per-iteration path, so a `std::env::var` call here would be I/O
2285 /// on the loop that is under measurement.
2286 fn loop_debug() -> bool {
2287 crate::vk::present_debug()
2288 }
2289
2290 /// Seconds after session start at which to inject a simulated connection
2291 /// loss, from `CCE_UI_FAULT_RECONNECT`. Resolved once: this is read from
2292 /// the per-iteration path.
2293 fn fault_reconnect_after() -> Option<std::time::Duration> {
2294 static AFTER: std::sync::OnceLock<Option<std::time::Duration>> =
2295 std::sync::OnceLock::new();
2296 *AFTER.get_or_init(|| {
2297 std::env::var("CCE_UI_FAULT_RECONNECT")
2298 .ok()
2299 .and_then(|v| v.parse::<f32>().ok())
2300 .map(std::time::Duration::from_secs_f32)
2301 })
2302 }
2303
2304 let mut end = SessionEnd::AppExit;
2305 let session_start = std::time::Instant::now();
2306 // The loop's pacing — what a turn does and how long to sleep after it —
2307 // is the shared `Pacer`'s (backend::shell); this loop is the Wayland
2308 // side: dispatch, the connection's health, the close fade.
2309 let mut pacer = Pacer::new(settings.title.clone());
2310 let mut next_timeout = ACTIVE_DISPATCH;
2311 loop {
2312 // Frame callbacks arrive with a p50 of 0ms but a ~0.5s tail, while the
2313 // compositor's own trace shows it firing them within one or two vsyncs
2314 // of the arm. Tracing each iteration bisects that: if this loop keeps
2315 // turning at ~16ms all through a long wait, the event was not there to
2316 // read, and the delay is upstream rather than in dispatching it.
2317 let iter_start = if loop_debug() {
2318 Some(std::time::Instant::now())
2319 } else {
2320 None
2321 };
2322 if let Err(e) = event_loop.dispatch(next_timeout, &mut engine_state) {
2323 log::error!("[window_runner] event loop error, ending session: {e:?}");
2324 end = SessionEnd::ConnectionLost;
2325 break;
2326 }
2327 if let Some(start) = iter_start {
2328 let t = debug_clock_ms();
2329 eprintln!(
2330 "[vk] t={} loop dispatch={}us pending_cb={}",
2331 t,
2332 start.elapsed().as_micros(),
2333 engine_state.frame_callback_pending
2334 );
2335 }
2336 // A protocol error kills the connection permanently, but it surfaces
2337 // through queue flushes whose errors calloop's WaylandSource swallows
2338 // (it only treats Io errors as fatal) — without this check the loop
2339 // spins forever on a dead display while wayland-backend re-prints the
2340 // error on every flush attempt.
2341 if let Some(perr) = conn.protocol_error() {
2342 log::error!("[window_runner] wayland protocol error, ending session: {perr}");
2343 end = SessionEnd::ConnectionLost;
2344 break;
2345 }
2346 // Fault injection for the reconnect path (`CCE_UI_FAULT_RECONNECT=<secs>`):
2347 // real connection loss is a rare race that cannot be provoked on demand,
2348 // so this drops the session exactly as a transport error would. One-shot
2349 // per process, so the app reconnects and then stays up.
2350 if let Some(after) = fault_reconnect_after() {
2351 static FIRED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2352 if session_start.elapsed() >= after
2353 && !FIRED.swap(true, std::sync::atomic::Ordering::Relaxed)
2354 {
2355 log::warn!("[window_runner] CCE_UI_FAULT_RECONNECT: dropping the session");
2356 end = SessionEnd::ConnectionLost;
2357 break;
2358 }
2359 }
2360 match pacer.turn(&mut engine_state) {
2361 Step::Sleep(timeout) => next_timeout = timeout,
2362 Step::Exit => {
2363 // The close dissolve. It is the COMPOSITOR that fades us — it
2364 // ramps our scene subtree's opacity, which takes the backdrop
2365 // blur, drop shadow and bevel down with the window; all this side
2366 // has to do is not vanish before it finishes. So keep the surface
2367 // mapped and the loop turning for exactly as long as the
2368 // compositor asked for, then leave. Dispatching (rather than
2369 // sleeping) keeps the connection pumped and lets any last
2370 // animation finish on screen while the window dissolves.
2371 let fade = crate::ipc::request_close_fade();
2372 if !fade.is_zero() {
2373 let until = std::time::Instant::now() + fade;
2374 loop {
2375 let left = until.saturating_duration_since(std::time::Instant::now());
2376 if left.is_zero() {
2377 break;
2378 }
2379 if event_loop.dispatch(left.min(ACTIVE_DISPATCH), &mut engine_state).is_err() {
2380 break;
2381 }
2382 }
2383 }
2384 break;
2385 }
2386 }
2387 }
2388
2389 // Tear the session down: drop its Wayland source from the persistent loop
2390 // (leaving it would leak a dead source per reconnect), then hand the app
2391 // back before `engine_state` drops the renderer and the surface with it.
2392 // `on_exit` and process cleanup belong to the app's real exit, in `run`.
2393 loop_handle.remove(wayland_token);
2394 let app = engine_state.inner.take();
2395 drop(engine_state);
2396 (app, end)
2397 }
2398
2399 #[cfg(test)]
2400 mod reconnect_tests {
2401 use super::{after_session, AfterSession, SessionEnd, RECONNECT_ATTEMPTS, RECONNECT_RESET};
2402 use std::time::Duration;
2403
2404 const LONG: Duration = Duration::from_secs(60);
2405 const SHORT: Duration = Duration::from_millis(50);
2406
2407 #[test]
2408 fn app_exit_ends_the_process() {
2409 let mut attempt = 0;
2410 assert_eq!(after_session(SessionEnd::AppExit, true, LONG, &mut attempt, false), AfterSession::Exit);
2411 assert_eq!(attempt, 0);
2412 }
2413
2414 #[test]
2415 fn lost_transport_reconnects_with_backoff() {
2416 let mut attempt = 0;
2417 assert_eq!(
2418 after_session(SessionEnd::ConnectionLost, true, LONG, &mut attempt, false),
2419 AfterSession::Reconnect(Duration::from_millis(200))
2420 );
2421 assert_eq!(attempt, 1);
2422 assert_eq!(
2423 after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt, false),
2424 AfterSession::Reconnect(Duration::from_millis(400))
2425 );
2426 assert_eq!(attempt, 2);
2427 }
2428
2429 /// The compositor exited (its socket is unlinked, or refusing after a
2430 /// crash). It saved this window for restore, so the successor respawns
2431 /// the app itself; a client that waited for it reattached beside the
2432 /// respawned copy, and the restore came up with two of every window.
2433 #[test]
2434 fn compositor_gone_exits_instead_of_waiting_for_a_successor() {
2435 let mut attempt = 0;
2436 assert_eq!(
2437 after_session(SessionEnd::NoCompositor, true, LONG, &mut attempt, false),
2438 AfterSession::Exit
2439 );
2440 // Even mid-budget: a reconnect that finds nobody listening is the
2441 // compositor leaving, not another transport break.
2442 let mut attempt = 3;
2443 assert_eq!(
2444 after_session(SessionEnd::NoCompositor, true, SHORT, &mut attempt, false),
2445 AfterSession::Exit
2446 );
2447 }
2448
2449 /// A daemon the compositor does not restore (the status bar, the
2450 /// notifier) waits for the successor instead — with no copy to collide
2451 /// with, exiting only took its D-Bus names down with it. It starts a fresh
2452 /// budget, and a transport break still reconnects as before.
2453 #[test]
2454 fn an_app_that_outlives_the_compositor_waits_for_the_next() {
2455 let mut attempt = 3;
2456 assert_eq!(
2457 after_session(SessionEnd::NoCompositor, true, SHORT, &mut attempt, true),
2458 AfterSession::AwaitCompositor
2459 );
2460 assert_eq!(attempt, 0);
2461 assert_eq!(
2462 after_session(SessionEnd::ConnectionLost, true, LONG, &mut attempt, true),
2463 AfterSession::Reconnect(Duration::from_millis(200))
2464 );
2465 // Asked to exit, or never started: it still goes.
2466 assert_eq!(after_session(SessionEnd::AppExit, true, LONG, &mut attempt, true), AfterSession::Exit);
2467 assert_eq!(after_session(SessionEnd::NoCompositor, false, SHORT, &mut attempt, true), AfterSession::Exit);
2468 }
2469
2470 #[test]
2471 fn nothing_to_carry_over_gives_up() {
2472 let mut attempt = 0;
2473 assert_eq!(
2474 after_session(SessionEnd::ConnectionLost, false, SHORT, &mut attempt, false),
2475 AfterSession::Exit
2476 );
2477 assert_eq!(
2478 after_session(SessionEnd::NoCompositor, false, SHORT, &mut attempt, false),
2479 AfterSession::Exit
2480 );
2481 }
2482
2483 #[test]
2484 fn budget_is_bounded_and_resets_after_a_long_session() {
2485 let mut attempt = 0;
2486 for _ in 0..RECONNECT_ATTEMPTS {
2487 assert!(matches!(
2488 after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt, false),
2489 AfterSession::Reconnect(_)
2490 ));
2491 }
2492 assert_eq!(
2493 after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt, false),
2494 AfterSession::Exit
2495 );
2496 // A session that outlived the reset window earns a fresh budget.
2497 assert_eq!(
2498 after_session(SessionEnd::ConnectionLost, true, RECONNECT_RESET + SHORT, &mut attempt, false),
2499 AfterSession::Reconnect(Duration::from_millis(200))
2500 );
2501 assert_eq!(attempt, 1);
2502 }
2503
2504 #[test]
2505 fn backoff_caps_at_six_point_four_seconds() {
2506 let mut attempt = 6;
2507 assert_eq!(
2508 after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt, false),
2509 AfterSession::Reconnect(Duration::from_millis(6400))
2510 );
2511 assert_eq!(
2512 after_session(SessionEnd::ConnectionLost, true, SHORT, &mut attempt, false),
2513 AfterSession::Reconnect(Duration::from_millis(6400))
2514 );
2515 }
2516 }