// 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::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; use crate::draw::DrawCtx; use crate::draw::chrome::apply_input_region; use crate::draw::layout_and_draw; use crate::render::Canvas; use crate::types::Rect; use crate::widget::Element; use super::frame::pick_shm_format; use super::AppData; /// 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, pool: Option, canvas: Option, last_pos: ( i32, i32 ), last_version: u64, rastered: bool, } impl SubsurfaceSlot { fn destroy( self ) { self.subsurface.destroy(); self.surface.destroy(); } } /// Reconcile the live subsurfaces against [`App::subsurfaces`], render content /// where it changed and reposition where it moved. Cheap when nothing but a /// position changed. Must run after the parent surface is configured. pub( crate ) fn reconcile_subsurfaces( data: &mut AppData ) { if data.subcompositor.is_none() { return; } if !data.main.configured { return; } let parent = match data.main.surface.try_wl_surface() { Some( s ) => s.clone(), None => return, }; let scale = data.main.scale_factor.max( 1 ) as u32; let ( w, h ) = ( data.main.width, data.main.height ); if w == 0 || h == 0 { return; } let pw = w * scale; let ph = h * scale; let ( format, swap_rb ) = pick_shm_format( &data.shm ); let specs: Vec> = data.app.subsurfaces(); let mut parent_dirty = false; // 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.subsurfaces.keys() .copied() .filter( |id| !present.contains( id ) ) .collect(); for id in stale { if let Some( slot ) = data.subsurfaces.remove( &id ) { slot.destroy(); parent_dirty = true; } } for spec in &specs { // Create on first sight. if !data.subsurfaces.contains_key( &spec.id ) { let ( subsurface, surface ) = { let sc = data.subcompositor.as_ref().unwrap(); sc.create_subsurface( parent.clone(), &data.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 ); data.subsurfaces.insert( spec.id, SubsurfaceSlot { subsurface, surface, pool: None, canvas: None, last_pos: ( spec.x, spec.y ), last_version: u64::MAX, rastered: false, } ); parent_dirty = true; } let size_changed = data.subsurfaces.get( &spec.id ) .and_then( |s| s.canvas.as_ref() ) .map( |c| c.size() != ( pw, ph ) ) .unwrap_or( true ); let needs_raster = { let slot = data.subsurfaces.get( &spec.id ).unwrap(); !slot.rastered || size_changed || slot.last_version != spec.content_version }; if needs_raster { let slot = data.subsurfaces.get_mut( &spec.id ).unwrap(); render_slot::( slot, &data.shm, &data.compositor_state, &spec.view, pw, ph, scale, format, swap_rb, size_changed, ); slot.rastered = true; slot.last_version = spec.content_version; parent_dirty = true; } let slot = data.subsurfaces.get_mut( &spec.id ).unwrap(); if slot.last_pos != ( spec.x, spec.y ) { 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 ); parent_dirty = true; } } // Applies pending subsurface placement / mapping without re-attaching the // parent buffer — the cheap per-frame move. if parent_dirty { parent.commit(); } } #[ allow( clippy::too_many_arguments ) ] fn render_slot( 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: DrawCtx = DrawCtx { focused_idx: None, hovered_idx: None, pressed_idx: None, cursor_state: HashMap::new(), selection_anchor: HashMap::new(), widget_rects: Vec::new(), debug_layout: false, scroll_offsets: HashMap::new(), scroll_rects: Vec::new(), scroll_canvases: HashMap::new(), scroll_navigable_items: HashMap::new(), previous_widget_rects: Vec::new(), accessible_extras: Vec::new(), live_depth: 0, }; layout_and_draw::( view, canvas, screen_rect, &mut ctx, 0 ); 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(); }