Files
ltk/src/platform/wayland/subsurface.rs
T
admin cacf871ddd
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
An Android backend, a platform layer that keeps Wayland out of the runtime, the built-in keyboard for an app's own text input, and APKs packaged with Debian's own tools
ltk only ran on a Wayland compositor: the runtime, the input handlers and the draw paths named smithay-client-toolkit's types throughout, so no other windowing system could be added without touching all of them. Prompt, the Eydos terminal, has to run on an Android phone, with its sessions and its sharing, and needs a toolkit that runs there.
The platform layer. Input reaches the runtime as platform-neutral events: `input/event.rs` defines `KeyInput`, `PointerInput` and `TouchInput`, and the pointer, touch and keyboard handlers take them through a few entry points (`pointer_input`, `touch_input`, `key_press`, `key_release`, `key_repeated`, `modifiers_changed`, `keyboard_enter`, `keyboard_leave`, `set_repeat_info`). The sctk handler implementations moved to `platform/wayland/input.rs`, which records the serials later requests need, resolves surfaces and translates. What the runtime asks of the windowing system (moving, resizing, minimising and fullscreening the window, a bare frame request, the cursor's shape, the input method, publishing the clipboard and the primary selection, preparing a canvas, presenting it and its damage) forms the `platform::Platform` trait, with its surface type as `Platform::Surface`. The protocol state moved from `AppData` into `platform::wayland::WaylandPlatform`, held as `AppData::platform`, and each surface's Wayland half (its kind, SHM pool, EGL window surface and what the compositor was last told) from `SurfaceState` into `WaylandSurfaceState`, held as `SurfaceState::platform`. The compositor, shell, seat and output handlers, the data device, the primary selection, the run loop, overlay and subsurface reconciliation, session management, output probing and window fitting moved from `event_loop/` to `platform/wayland/`. Drawing is split from presenting: `draw/paint.rs` paints a surface in full or under its dirty rects, the same for the software and GPU canvases, in place of the four near-identical paths of `draw/software.rs` and `draw/gles.rs`, and the backend presents it (`platform/wayland/present.rs`, which also took the opaque and input regions and `pick_shm_format`). The runtime is generic over the platform, `AppData<A, P: Platform = NativePlatform<A>>` and `SurfaceState<Msg, S = NativeSurface>`, resolved at compile time. The backend-neutral half of a loop iteration is `event_loop/step.rs` (`process_messages`, `draw_due`, `next_wakeup`, `before_draw`, `after_draw`), and `event_loop/setup.rs` builds the state and the event sources every backend has; the accessibility tree and its actions moved to `event_loop/a11y.rs`. `platform/mock.rs` is a platform that logs what is asked of it, and `event_loop/tests.rs` drives the runtime on it through input, update, painting and presentation. The behaviour on Wayland is unchanged, except that a frame whose presentation fails leaves the next one to repaint in full. `ltk::Keysym` now comes from `xkeysym`, the crate sctk re-exported it from, so it is the same type.
Features. `wayland`, on by default, brings the Wayland backend and its dependencies, now optional, AT-SPI2 accessibility among them. `android` brings android-activity 0.6, ndk 0.9 and jni 0.21. A new `build.rs` refuses both at once and sets `cfg( backend )` when one is on, which builds the runtime behind `run` and `try_run`. Without a backend only the runtime-free core builds: `UiSurface`, widgets, layout, theming and rendering.
The Android backend. `ltk::run` runs inside a NativeActivity once `android_main` has handed its `AndroidApp`, re-exported as `ltk::AndroidApp`, to the new `ltk::set_android_app`; without it `run` fails with `RunError::NoAndroidApp`. The first call in the process sends standard output and error, panics included, to logcat, sets `HOME` to the app's internal storage and, unless set, `SSL_CERT_DIR` to Android's CA certificates and `LANG` to the device's language, and unpacks the bundled resources; every call keeps the latest activity, since Android starts the next one in the same process. The backend draws through EGL on the activity's window, or in software into a locked window buffer, and sizes the main surface at the display's density in whole multiples of 160 dpi. Fingers and styluses become touches, a mouse the pointer, and key events keysyms and text through the device's character map. calloop's descriptor rides on the activity's `ALooper`, so the app's channel and timers keep working. Text is typed on ltk's own keyboard, since a NativeActivity gets only key events from the system's. Through JNI on the activity (`platform/android/java.rs`): copies go to `ClipboardManager` and its text is read back whenever the app gains the focus; back with nothing left to go back to minimises the app with `moveTaskToBack` or closes it, as `App::back_at_root` says, which the new `Platform::back_leaves` decides per backend; and the window's insets come from `getRootWindowInsets()` on Android 11 and later, the status bar and any cutout on top and only bars that take taps on the other edges, so there is no gap above the view nor below it in gesture navigation, and a bar of buttons still keeps its room; earlier releases fall back to the content rect. When the app's last surface closes, ltk calls `finishAndRemoveTask` and waits for the activity to be destroyed, since a second activity started in a process whose `android_main` has returned never came up; an app should end the process after `run` returns. `ltk::keep_running( Some( text ) )` starts a foreground service with `text` on an ongoing notification, so Android does not freeze the app once it leaves the screen, and `None` stops it.
Surface insets. `SurfaceState::insets` holds the logical pixels the system covers along each edge; the view is laid out inside them while the background fills the whole surface, and positions stay absolute, so hit testing is unchanged. The built-in keyboard is placed inside them too, above a bar of navigation buttons. On Wayland they stay zero.
Resources in a package. `ltk::set_resource_dir( dir )` names an app's own copy of ltk's resources and `ltk::resource_dir()` returns it, so an app also finds its own bundled files there. Its `themes/` is searched after the user's themes and before `/usr/share/ltk/themes`, its `keyboards/` before `/usr/share`, and a font a theme names by an absolute path that does not exist is read from its `fonts/` by file name, so a theme written against Debian's paths works unchanged. On Android `set_android_app` unpacks `assets/ltk/` into the internal storage and uses it as the resource directory; `assets/ltk/MANIFEST` lists the tree, since the asset manager cannot list subdirectories, and its first line, a hash of the contents, decides whether to unpack again. On Android the per-script fallback fonts come from `/system/fonts` and the primary face is the embedded Sora.
APKs. `scripts/android-assets.sh` builds that tree from installed themes, without their cursors, with the fonts each `theme.json` names and ltk's keyboard layouts. `scripts/android-apk.sh OUT.apk LIB.so PACKAGE LABEL [ASSETS_DIR]` packages an app built as a `cdylib` with Debian's aapt, zipalign and apksigner, with no Gradle and no SDK download: it writes the manifest, with the library loaded as `libmain.so`, optional permissions and a theme without an action bar, which the content rect would otherwise leave room for; links it against `android-framework-res`; adds the assets and the library; aligns and signs, creating a debug keystore when there is none. `ICON` takes an SVG, rasterised with `rsvg-convert` for every density, or a PNG. `KEEP_ALIVE=1` compiles `java/net/liberux/ltk/KeepAlive.java` with javac and `dx` from art-standalone against the platform 34 `android.jar`, against which aapt then links too, since Debian's framework-res predates the `specialUse` service type, and declares the service with `KEEP_ALIVE_USE` as its reason and the permissions it needs. `examples/android_hello.rs` is the smoke test: `make android-check` typechecks the backend with Debian's rustc, `rust-src` and NDK through `-Zbuild-std`, `make android-apk` and `make android-install` package and install the example, `make deps` and `make android-deps` install what the targets need, and `make install` now copies `examples`, `scripts`, `java` and `build.rs` into the registry.
The built-in keyboard for an app's own text input. While `App::external_text_input` asks for text and no ltk text field has the focus, the built-in keyboard shows with `App::external_text_purpose`, so a terminal gets its row of Escape, Tab, Ctrl, Alt and arrows, comes back when the serial changes and hides when the request ends. Its keys reach the app as physical ones, pressed and released through `App::on_raw_key_event` with the panel's Ctrl and Alt held for them. Before, it only served ltk's text fields, so such an app had no keyboard where the compositor offers no input method.
Clippy. `make clippy` is clean again: derived `Default`s replace the hand-written ones, `div_ceil`, `then_some`, struct update syntax for strokes, the map entry API in the text renderer, function paths in place of closures in the examples and tests, and fewer casts and explicit lifetimes.
`README.md`, `docs/architecture.md` and `CHANGELOG.md` describe the features, the backend, the events and their units, and what stays with a backend.
Not done. On Android there are no overlays, secondary surfaces or subsurfaces, no dead keys from a physical keyboard and no saved state for the activity. Why a second activity in the same process never comes up was not found. The keep-alive service does not ask for the notification permission, so on Android 13 and later its notification may not show until it is granted in the app's settings.
2026-10-16 23:46:50 +02:00

506 lines
17 KiB
Rust

// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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<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,
scroll_canvases: HashMap<usize, Canvas>,
viewport_canvases: HashMap<usize, Canvas>,
}
impl SubsurfaceSlot
{
pub( crate ) fn parent( &self ) -> SubsurfaceParent
{
self.parent
}
fn destroy( mut self, egl: Option<&Arc<EglContext>> )
{
// 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<A: App>( data: &AppData<A>, 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<A: App>( data: &mut AppData<A>, 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<A: App>( data: &mut AppData<A>, 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<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.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<A: App>( data: &mut AppData<A> )
{
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<crate::app::SubsurfaceSpec<A::Message>> = 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<crate::app::SubsurfaceId> = Vec::new();
// Drop subsurfaces whose id disappeared from the spec list.
let present: std::collections::HashSet<crate::app::SubsurfaceId> =
specs.iter().map( |s| s.id ).collect();
let stale: Vec<crate::app::SubsurfaceId> = 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::<A::Message>(
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<crate::app::OverlayId> = 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<Msg: Clone>() -> DrawCtx<Msg>
{
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<Msg: Clone>( scroll: &mut HashMap<usize, Canvas>, viewport: &mut HashMap<usize, Canvas> ) -> DrawCtx<Msg>
{
let mut ctx = empty_draw_ctx::<Msg>();
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<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_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; }
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::<Msg>( &mut slot.scroll_canvases, &mut slot.viewport_canvases );
layout_and_draw::<Msg>( 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<Msg: Clone>(
slot: &mut SubsurfaceSlot,
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 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::<Msg>( &mut slot.scroll_canvases, &mut slot.viewport_canvases );
layout_and_draw::<Msg>( 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();
}