// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Input-transparent child surfaces ([`crate::app::SubsurfaceSpec`]). //! //! Each spec maps to one `wl_subsurface` sized to the main surface, with an //! empty input region so all input falls through to the parent — the host //! keeps a single gesture/input model. The content buffer is rasterised only //! when the size or the spec's `content_version` changes; a position change //! emits `wl_subsurface.set_position` plus a bare parent commit, so an animated //! slide costs a compositor recomposite, not a client re-raster. use std::collections::HashMap; use std::collections::hash_map::Entry; use std::sync::Arc; use smithay_client_toolkit::compositor::CompositorState; use smithay_client_toolkit::shm::Shm; use smithay_client_toolkit::shm::slot::SlotPool; use smithay_client_toolkit::reexports::client::protocol::wl_shm; use smithay_client_toolkit::reexports::client::protocol::wl_subsurface::WlSubsurface; use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface; use crate::app::{ App, SubsurfaceParent }; use crate::egl_context::{ EglContext, EglSurface }; use crate::draw::DrawCtx; use crate::draw::layout_and_draw; use crate::event_loop::{ AppData, SurfaceFocus }; use crate::event_loop::perf::RasterTrace; use crate::render::Canvas; use crate::types::Rect; use crate::widget::Element; use super::present::{ apply_input_region, pick_shm_format }; /// Convert a layout (physical) position to the logical position the /// compositor expects for `wl_subsurface.set_position`. fn to_logical( x: i32, y: i32, scale: u32 ) -> ( i32, i32 ) { let s = scale.max( 1 ) as i32; ( x / s, y / s ) } /// Live state for one subsurface: its Wayland objects, its own SHM pool and /// canvas, and the last (size, version, position) we committed so the per-frame /// pass can tell a re-raster from a cheap reposition. pub( crate ) struct SubsurfaceSlot { subsurface: WlSubsurface, surface: WlSurface, /// SHM pool for the software raster path. `None` on the GLES path. pool: Option, /// EGL window surface for the GLES raster path (so the `surface-panel` /// backdrop blur, a GLES-only pass, renders). `None` on the software path. egl_surface: Option, canvas: Option, last_pos: ( i32, i32 ), last_size: ( u32, u32 ), last_version: u64, rastered: bool, parent: SubsurfaceParent, scroll_canvases: HashMap, viewport_canvases: HashMap, } impl SubsurfaceSlot { pub( crate ) fn parent( &self ) -> SubsurfaceParent { self.parent } fn destroy( mut self, egl: Option<&Arc> ) { // GLES sub-canvases free their objects on drop and need the context current. if let ( Some( ctx ), Some( es ) ) = ( egl, self.egl_surface.as_ref() ) { let _ = ctx.make_current( es ); } self.scroll_canvases.clear(); self.viewport_canvases.clear(); // Drop the EGL surface before the wl_surface its wl_egl_window wraps. self.egl_surface = None; self.subsurface.destroy(); self.surface.destroy(); } } fn focus_of( parent: SubsurfaceParent ) -> SurfaceFocus { match parent { SubsurfaceParent::Main => SurfaceFocus::Main, SubsurfaceParent::Overlay( id ) => SurfaceFocus::Overlay( id ), } } fn parent_frame_pending( data: &AppData, parent: SubsurfaceParent ) -> bool { match parent { SubsurfaceParent::Main => data.main.frame_pending, SubsurfaceParent::Overlay( id ) => data.overlays.get( &id ).is_some_and( |ss| ss.frame_pending ), } } /// Request a frame callback on `parent` ahead of the commit that applies a /// subsurface move, unless one is already outstanding. fn arm_parent_frame( data: &mut AppData, parent: SubsurfaceParent, wl: &WlSurface ) { let ss = match parent { SubsurfaceParent::Main => &mut data.main, SubsurfaceParent::Overlay( id ) => match data.overlays.get_mut( &id ) { Some( s ) => s, None => return }, }; ss.frame_tick_due = false; if ss.frame_pending { return; } let _ = wl.frame( &data.platform.qh, focus_of( parent ) ); ss.frame_pending = true; } /// Keep the frame cadence on a parent whose callback fired while the app /// animates by subsurface motion only, when this pass moved nothing on it. fn keep_parent_cadence( data: &mut AppData, parent: SubsurfaceParent, animating: bool ) { let ss = match parent { SubsurfaceParent::Main => &mut data.main, SubsurfaceParent::Overlay( id ) => match data.overlays.get_mut( &id ) { Some( s ) => s, None => return }, }; if !std::mem::take( &mut ss.frame_tick_due ) { return; } if ss.frame_pending || !animating { return; } let Some( wl ) = ss.platform.surface.try_wl_surface().cloned() else { return }; let _ = wl.frame( &data.platform.qh, focus_of( parent ) ); wl.commit(); ss.frame_pending = true; } /// Resolve a subsurface's parent surface to `( wl_surface, phys_w, phys_h, /// scale )`. Returns `None` when an [`SubsurfaceParent::Overlay`] target is /// absent, not yet configured, or zero-sized — the caller skips the spec for /// that frame. The `WlSurface` is cloned (an `Arc`) so the render / reposition /// loop can run without holding a borrow on `data.overlays` / `data.main`. fn resolve_parent( data: &AppData, parent: SubsurfaceParent ) -> Option<( WlSurface, u32, u32, u32 )> { let ss = match parent { SubsurfaceParent::Main => &data.main, SubsurfaceParent::Overlay( id ) => data.overlays.get( &id )?, }; if !ss.configured { return None; } if ss.width == 0 || ss.height == 0 { return None; } let surface = ss.platform.surface.try_wl_surface()?.clone(); let scale = ss.scale_factor.max( 1 ) as u32; Some( ( surface, ss.width * scale, ss.height * scale, scale ) ) } /// Record a parent surface that needs a commit this frame, deduped by parent /// identity so each is committed once. fn mark_parent_dirty( list: &mut Vec<( SubsurfaceParent, WlSurface )>, parent: SubsurfaceParent, wl: &WlSurface ) { if !list.iter().any( |( p, _ )| *p == parent ) { list.push( ( parent, wl.clone() ) ); } } /// Reconcile the live subsurfaces against [`App::subsurfaces`], render content /// where it changed and reposition where it moved. Cheap when nothing but a /// position changed. Each spec is composited under its own parent surface /// (main or an overlay), so a slide can ride above app windows. Must run after /// the parent surface is configured. pub( crate ) fn reconcile_subsurfaces( data: &mut AppData ) { if data.platform.subcompositor.is_none() { return; } if !data.main.configured { return; } let ( format, swap_rb ) = pick_shm_format( &data.platform.shm ); let specs: Vec> = data.app.subsurfaces(); // Parents touched this frame; each committed once at the end so the // placement / mapping actually lands. let mut dirty_parents: Vec<( SubsurfaceParent, WlSurface )> = Vec::new(); let mut committed: Vec = Vec::new(); // Drop subsurfaces whose id disappeared from the spec list. let present: std::collections::HashSet = specs.iter().map( |s| s.id ).collect(); let stale: Vec = data.platform.subsurfaces.keys() .copied() .filter( |id| !present.contains( id ) ) .collect(); for id in stale { if let Some( slot ) = data.platform.subsurfaces.remove( &id ) { let parent = slot.parent; slot.destroy( data.platform.egl_context.as_ref() ); if let Some( ( wl, _, _, _ ) ) = resolve_parent( data, parent ) { mark_parent_dirty( &mut dirty_parents, parent, &wl ); } } } for spec in &specs { let Some( ( parent_wl, pw, ph, scale ) ) = resolve_parent( data, spec.parent ) else { continue }; // Create on first sight. if let Entry::Vacant( vacant ) = data.platform.subsurfaces.entry( spec.id ) { let ( subsurface, surface ) = { let sc = data.platform.subcompositor.as_ref().unwrap(); sc.create_subsurface( parent_wl.clone(), &data.platform.qh ) }; // Independent buffer commits; placement still applies on the // parent commit we issue below. subsurface.set_desync(); let ( lx, ly ) = to_logical( spec.x, spec.y, scale ); subsurface.set_position( lx, ly ); vacant.insert( SubsurfaceSlot { subsurface, surface, pool: None, egl_surface: None, canvas: None, last_pos: ( spec.x, spec.y ), last_size: ( 0, 0 ), last_version: u64::MAX, rastered: false, parent: spec.parent, scroll_canvases: HashMap::new(), viewport_canvases: HashMap::new(), } ); mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl ); } let parent_pending = parent_frame_pending( data, spec.parent ); // Tracked on the slot (not the canvas) because the GLES canvas lives // in `data.platform.subsurface_gles_canvas`, shared across all subsurfaces. let size_changed = data.platform.subsurfaces.get( &spec.id ) .map( |s| s.last_size != ( pw, ph ) ) .unwrap_or( true ); let needs_raster = { let slot = data.platform.subsurfaces.get( &spec.id ).unwrap(); !slot.rastered || size_changed || slot.last_version != spec.content_version }; if needs_raster { // GLES only when the spec asks for it (glass content); otherwise // the cheaper-to-create software rasteriser. let egl = if spec.gpu { data.platform.egl_context.as_ref() } else { None }; let slot = data.platform.subsurfaces.get_mut( &spec.id ).unwrap(); render_slot::( slot, egl, &mut data.platform.subsurface_gles_canvas, &data.platform.shm, &data.platform.compositor_state, &spec.view, pw, ph, scale, format, swap_rb, size_changed, ); slot.rastered = true; slot.last_version = spec.content_version; slot.last_size = ( pw, ph ); committed.push( spec.id ); mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl ); } // A move waits for the parent's outstanding frame callback, so a drag // commits once per vblank with the latest position instead of once // per input sample. let slot = data.platform.subsurfaces.get_mut( &spec.id ).unwrap(); if slot.last_pos != ( spec.x, spec.y ) && !parent_pending { let ( lx, ly ) = to_logical( spec.x, spec.y, scale ); slot.subsurface.set_position( lx, ly ); // Desync subsurface state (the position) is applied on the child // surface's own commit, not the parent's; commit it here so the // move actually lands. The parent commit below applies placement. slot.surface.commit(); slot.last_pos = ( spec.x, spec.y ); mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl ); } } // Applies pending subsurface placement / mapping without re-attaching the // parent buffer — the cheap per-frame move. for ( parent, wl ) in dirty_parents { // A bufferless commit is a protocol error on a session-lock surface. if parent == SubsurfaceParent::Main && !data.first_frame_committed { continue; } arm_parent_frame( data, parent, &wl ); wl.commit(); } let animating = data.app.is_animating(); keep_parent_cadence( data, SubsurfaceParent::Main, animating ); let overlay_ids: Vec = data.overlays.keys().copied().collect(); for id in overlay_ids { keep_parent_cadence( data, SubsurfaceParent::Overlay( id ), animating ); } for id in committed { data.app.on_subsurface_committed( id ); } } /// Input-transparent subsurfaces never carry focus / hover / scroll state, /// so they draw with an empty context. fn empty_draw_ctx() -> DrawCtx { DrawCtx { focused_idx: None, hovered_idx: None, pressed_idx: None, cursor_state: HashMap::new(), selection_anchor: HashMap::new(), text_scroll: HashMap::new(), widget_rects: Vec::new(), debug_layout: false, scroll_offsets: HashMap::new(), scroll_rects: Vec::new(), scroll_canvases: HashMap::new(), viewport_canvases: HashMap::new(), viewport_seq: 0, scroll_navigable_items: HashMap::new(), scroll_clipped: Vec::new(), previous_widget_rects: Vec::new(), accessible_extras: Vec::new(), live_depth: 0, origin: ( 0.0, 0.0 ), blur_regions: Vec::new(), } } fn slot_draw_ctx( scroll: &mut HashMap, viewport: &mut HashMap ) -> DrawCtx { let mut ctx = empty_draw_ctx::(); ctx.scroll_canvases = std::mem::take( scroll ); ctx.viewport_canvases = std::mem::take( viewport ); ctx } /// Raster a subsurface's content. Uses the GLES path when an [`EglContext`] /// is available — only there does the `surface-panel` backdrop blur render; /// the software fallback paints the same widgets without the blur. #[ allow( clippy::too_many_arguments ) ] fn render_slot( slot: &mut SubsurfaceSlot, egl_ctx: Option<&Arc>, gles_canvas: &mut Option, shm: &Shm, compositor: &CompositorState, view: &Element, pw: u32, ph: u32, scale: u32, shm_format: wl_shm::Format, swap_rb: bool, size_changed: bool, ) { if let Some( ctx ) = egl_ctx { render_slot_gpu::( slot, ctx, gles_canvas, compositor, view, pw, ph, scale, size_changed ); } else { render_slot_software::( slot, shm, compositor, view, pw, ph, scale, shm_format, swap_rb, size_changed ); } } #[ allow( clippy::too_many_arguments ) ] fn render_slot_gpu( slot: &mut SubsurfaceSlot, egl_ctx: &Arc, gles_canvas: &mut Option, compositor: &CompositorState, view: &Element, pw: u32, ph: u32, scale: u32, size_changed: bool, ) { // (Re)create the EGL window surface on first sight or a size change. if slot.egl_surface.is_none() || size_changed { match egl_ctx.create_surface( &slot.surface, pw as i32, ph as i32 ) { Ok( es ) => slot.egl_surface = Some( es ), Err( e ) => { eprintln!( "ltk: subsurface EGL surface creation failed: {e}" ); return; } } } let es = slot.egl_surface.as_ref().unwrap(); if egl_ctx.make_current( es ).is_err() { return; } let trace = RasterTrace::begin(); // The canvas (and its compiled shader programs) is shared across all // subsurface rasters, so a lazily re-created panel doesn't recompile. let canvas = gles_canvas.get_or_insert_with( || { let mut c = Canvas::new_gles( Arc::clone( egl_ctx.gl() ), egl_ctx.version, pw, ph ); c.set_dpi_scale( scale as f32 ); if let Some( reg ) = crate::theme::build_font_registry() { c.set_font_registry( Arc::new( reg ) ); } c } ); if canvas.size() != ( pw, ph ) { canvas.resize( pw, ph ); canvas.set_dpi_scale( scale as f32 ); } canvas.clear_clip(); canvas.clear(); let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 }; let mut ctx = slot_draw_ctx::( &mut slot.scroll_canvases, &mut slot.viewport_canvases ); layout_and_draw::( view, canvas, screen_rect, &mut ctx, 0 ); canvas.present(); if let Some( t ) = trace { t.end( egl_ctx.gl(), "subsurface", pw, ph ); } slot.scroll_canvases = ctx.scroll_canvases; slot.viewport_canvases = ctx.viewport_canvases; let wl = &slot.surface; wl.set_buffer_scale( scale as i32 ); // Empty input region: pointer/touch fall through to the parent. apply_input_region( wl, compositor, Some( &[] ), scale ); // Implicitly commits the wl_surface with the freshly rendered buffer. let _ = egl_ctx.swap_buffers_with_damage( es, &[ ( 0, 0, pw as i32, ph as i32 ) ] ); } #[ allow( clippy::too_many_arguments ) ] fn render_slot_software( slot: &mut SubsurfaceSlot, shm: &Shm, compositor: &CompositorState, view: &Element, pw: u32, ph: u32, scale: u32, shm_format: wl_shm::Format, swap_rb: bool, size_changed: bool, ) { if slot.pool.is_none() || size_changed { match SlotPool::new( ( pw * ph * 4 ) as usize, shm ) { Ok( p ) => slot.pool = Some( p ), Err( _ ) => return, } } let pool = slot.pool.as_mut().unwrap(); let stride = pw * 4; let ( buffer, canvas_buf ) = match pool.create_buffer( pw as i32, ph as i32, stride as i32, shm_format, ) { Ok( r ) => r, Err( _ ) => return, }; let canvas = slot.canvas.get_or_insert_with( || { let mut c = Canvas::new( pw, ph ); c.set_dpi_scale( scale as f32 ); if let Some( reg ) = crate::theme::build_font_registry() { c.set_font_registry( Arc::new( reg ) ); } c } ); if canvas.size() != ( pw, ph ) { canvas.resize( pw, ph ); canvas.set_dpi_scale( scale as f32 ); } canvas.clear_clip(); canvas.clear(); let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 }; let mut ctx = slot_draw_ctx::( &mut slot.scroll_canvases, &mut slot.viewport_canvases ); layout_and_draw::( view, canvas, screen_rect, &mut ctx, 0 ); slot.scroll_canvases = ctx.scroll_canvases; slot.viewport_canvases = ctx.viewport_canvases; canvas.write_to_wayland_buf( canvas_buf, swap_rb ); let wl = &slot.surface; if buffer.attach_to( wl ).is_err() { return; } wl.set_buffer_scale( scale as i32 ); wl.damage_buffer( 0, 0, pw as i32, ph as i32 ); // Empty input region: pointer/touch fall through to the parent. apply_input_region( wl, compositor, Some( &[] ), scale ); wl.commit(); }