git.lucas.co / cce-ui
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 &regions {
 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 }