ltk: subsurface slides over overlays, axis-locked swipes, physical-space layout, touch reset on resume
Subsurfaces can now be parented to an overlay surface, not just the main surface. `SubsurfaceSpec` gains `parent: SubsurfaceParent { Main, Overlay(id) }` so a sliding panel can ride above app windows the way an overlay panel does, and an optional `gpu: bool` so content that uses the `surface-panel` backdrop-filter glass (a GLES-only pass) keeps it while sliding instead of dropping to the software rasteriser. `reconcile_subsurfaces` resolves each spec's parent independently — skipping an `Overlay` parent that is absent, unconfigured, or zero-sized — tracks the rastered size on the slot, and commits each touched parent once per frame. The ~14 GLES shader programs are compiled once into a shared `AppData::subsurface_gles_canvas` reused across every subsurface, so a lazily re-created sliding panel never recompiles them (hundreds of ms on a mobile GPU).
Vertical and horizontal swipes are now mutually exclusive: a gesture locks onto its dominant axis within the first 8 px of travel (new `SwipeAxis`) and ignores the perpendicular axis for the rest of the gesture, so a vertical swipe that drifts sideways no longer also drives the pager (and vice-versa). The upward swipe progress is no longer clamped at 1.0 — follow-the-finger panels can keep tracking the finger past the commit threshold — and a release below threshold delivers a final `progress = 0.0` cancellation pulse. A vertical swipe also no longer re-rasters the full-screen main surface on every motion event: only the overlays it drives are refreshed, while the horizontal pager (which does move the main surface) still redraws it. Re-rastering the main on every frame of a vertical drag was wasted work that stalled the loop and made the gesture feel laggy to start on a slow GPU.
Layout-affecting `Length` values (widths, paddings, gaps, widget sizes, including `Vw` / `Vh`) now resolve against the physical viewport via the new `Canvas::viewport_layout()` — the space the layout tree is actually computed in — so a `Vw(100)` fills the surface on a HiDPI (scale 2) output instead of covering half of it. Font sizes are unchanged: they still resolve against the logical viewport and are scaled at raster time. Switched column, row, spacer, wrap_grid, container, image, and vslider over to it; `img_widget` and `vslider` now document `Length::vw` / `vh` sizing and the showcase example demonstrates a viewport-relative image.
Touch gesture state is reset when the touch capability is added or removed — suspend / resume on devices that power the touchscreen down. A yanked capability never delivers the pending `up` / `cancel`, so the shared `reset_touch_state()` (also used by the `wl_touch.cancel` handler) drops the stranded `primary_touch_id` / slot state across the main surface and every overlay, keeping the first post-resume gesture clean. Also drops an accidental duplicate `on_scale_changed` call from the scale-change handler.
This commit is contained in:
+187
-44
@@ -20,7 +20,8 @@ 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::app::{ App, SubsurfaceParent };
|
||||
use crate::egl_context::{ EglContext, EglSurface };
|
||||
use crate::draw::DrawCtx;
|
||||
use crate::draw::chrome::apply_input_region;
|
||||
use crate::draw::layout_and_draw;
|
||||
@@ -46,46 +47,75 @@ pub( crate ) struct SubsurfaceSlot
|
||||
{
|
||||
subsurface: WlSubsurface,
|
||||
surface: WlSurface,
|
||||
/// SHM pool for the software raster path. `None` on the GLES path.
|
||||
pool: Option<SlotPool>,
|
||||
/// 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<EglSurface>,
|
||||
canvas: Option<Canvas>,
|
||||
last_pos: ( i32, i32 ),
|
||||
last_size: ( u32, u32 ),
|
||||
last_version: u64,
|
||||
rastered: bool,
|
||||
parent: SubsurfaceParent,
|
||||
}
|
||||
|
||||
impl SubsurfaceSlot
|
||||
{
|
||||
fn destroy( self )
|
||||
fn destroy( mut self )
|
||||
{
|
||||
// Drop the EGL surface before the wl_surface its wl_egl_window wraps.
|
||||
self.egl_surface = None;
|
||||
self.subsurface.destroy();
|
||||
self.surface.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<A: App>( data: &AppData<A>, 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.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. Must run after the parent surface is configured.
|
||||
/// 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<A: App>( data: &mut AppData<A> )
|
||||
{
|
||||
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<crate::app::SubsurfaceSpec<A::Message>> = data.app.subsurfaces();
|
||||
|
||||
let mut parent_dirty = false;
|
||||
// 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();
|
||||
|
||||
// Drop subsurfaces whose id disappeared from the spec list.
|
||||
let present: std::collections::HashSet<crate::app::SubsurfaceId> =
|
||||
@@ -98,19 +128,25 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
||||
{
|
||||
if let Some( slot ) = data.subsurfaces.remove( &id )
|
||||
{
|
||||
let parent = slot.parent;
|
||||
slot.destroy();
|
||||
parent_dirty = true;
|
||||
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 !data.subsurfaces.contains_key( &spec.id )
|
||||
{
|
||||
let ( subsurface, surface ) = {
|
||||
let sc = data.subcompositor.as_ref().unwrap();
|
||||
sc.create_subsurface( parent.clone(), &data.qh )
|
||||
sc.create_subsurface( parent_wl.clone(), &data.qh )
|
||||
};
|
||||
// Independent buffer commits; placement still applies on the
|
||||
// parent commit we issue below.
|
||||
@@ -121,17 +157,21 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
||||
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,
|
||||
} );
|
||||
parent_dirty = true;
|
||||
mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
|
||||
}
|
||||
|
||||
// Tracked on the slot (not the canvas) because the GLES canvas lives
|
||||
// in `data.subsurface_gles_canvas`, shared across all subsurfaces.
|
||||
let size_changed = data.subsurfaces.get( &spec.id )
|
||||
.and_then( |s| s.canvas.as_ref() )
|
||||
.map( |c| c.size() != ( pw, ph ) )
|
||||
.map( |s| s.last_size != ( pw, ph ) )
|
||||
.unwrap_or( true );
|
||||
let needs_raster = {
|
||||
let slot = data.subsurfaces.get( &spec.id ).unwrap();
|
||||
@@ -140,14 +180,19 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
||||
|
||||
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.egl_context.as_ref() } else { None };
|
||||
let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
|
||||
render_slot::<A::Message>(
|
||||
slot, &data.shm, &data.compositor_state, &spec.view,
|
||||
slot, egl, &mut data.subsurface_gles_canvas,
|
||||
&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;
|
||||
slot.last_size = ( pw, ph );
|
||||
mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
|
||||
}
|
||||
|
||||
let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
|
||||
@@ -160,20 +205,134 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
||||
// move actually lands. The parent commit below applies placement.
|
||||
slot.surface.commit();
|
||||
slot.last_pos = ( spec.x, spec.y );
|
||||
parent_dirty = true;
|
||||
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.
|
||||
if parent_dirty
|
||||
for ( _, wl ) in dirty_parents
|
||||
{
|
||||
parent.commit();
|
||||
wl.commit();
|
||||
}
|
||||
}
|
||||
|
||||
/// Input-transparent subsurfaces never carry focus / hover / scroll state,
|
||||
/// so they draw with an empty context.
|
||||
fn empty_draw_ctx<Msg: Clone>() -> DrawCtx<Msg>
|
||||
{
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Msg: Clone>(
|
||||
slot: &mut SubsurfaceSlot,
|
||||
egl_ctx: Option<&Arc<EglContext>>,
|
||||
gles_canvas: &mut Option<Canvas>,
|
||||
shm: &Shm,
|
||||
compositor: &CompositorState,
|
||||
view: &Element<Msg>,
|
||||
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::<Msg>( slot, ctx, gles_canvas, compositor, view, pw, ph, scale, size_changed );
|
||||
}
|
||||
else
|
||||
{
|
||||
render_slot_software::<Msg>( slot, shm, compositor, view, pw, ph, scale, shm_format, swap_rb, size_changed );
|
||||
}
|
||||
}
|
||||
|
||||
#[ allow( clippy::too_many_arguments ) ]
|
||||
fn render_slot<Msg: Clone>(
|
||||
fn render_slot_gpu<Msg: Clone>(
|
||||
slot: &mut SubsurfaceSlot,
|
||||
egl_ctx: &Arc<EglContext>,
|
||||
gles_canvas: &mut Option<Canvas>,
|
||||
compositor: &CompositorState,
|
||||
view: &Element<Msg>,
|
||||
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; }
|
||||
|
||||
// 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 = empty_draw_ctx::<Msg>();
|
||||
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
|
||||
canvas.present();
|
||||
|
||||
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<Msg: Clone>(
|
||||
slot: &mut SubsurfaceSlot,
|
||||
shm: &Shm,
|
||||
compositor: &CompositorState,
|
||||
@@ -222,23 +381,7 @@ fn render_slot<Msg: Clone>(
|
||||
canvas.clear();
|
||||
|
||||
let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
|
||||
let mut ctx: DrawCtx<Msg> = 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,
|
||||
};
|
||||
let mut ctx = empty_draw_ctx::<Msg>();
|
||||
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
|
||||
|
||||
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
|
||||
|
||||
Reference in New Issue
Block a user