Files
ltk/src/draw/chrome.rs
T
admin 53e7ccc46e
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
Keep the GLES path when the scale arrives first, stop re-rendering what does not change, and let the compositor skip what the surface covers
Measured on a Librem 5 (Cortex-A53, GC7000Lite, 720×1440 at scale 2) with a frame trace on both sides of the socket: the shell's main surface repainted in 110 to 136 ms, quick settings drew at 20 fps with the GPU saturated, and every panel open uploaded its wallpaper and icons again. Most of it was the toolkit doing work the frame did not need.
Backend choice. When the compositor announces the output scale before the first `configure`, `scale_factor_changed` allocated an SHM pool for the surface and `on_configure` took that pool as the sign that the software path had been chosen, locking the surface to tiny-skia for its whole life. Every ltk main surface on a scale-2 phone ran in software while its overlays, which inherit the scale from the main and never receive that event, ran on the GPU. The scale handler now only reallocates a pool that already exists; before the first configure neither path is chosen.
Wallpapers. `WallpaperBundle::for_size` returned the crop at the source resolution, a 1103×2160 slice of a 4K image for a 720×1440 panel, 10 MB uploaded per texture and a hundred milliseconds each time. The crop is now downscaled to the surface (bilinear, once per size, cached), also for landscape surfaces smaller than the source.
Viewport. `Element::Viewport` created a fresh sub-canvas every frame, on GLES a framebuffer, a 2048² glyph atlas and empty image and glyph caches, so a panel clipped to a height that animates re-uploaded its wallpaper and re-rasterised every glyph on every frame. Without a bottom fade the clip is now a scissor on the parent canvas (a mask on the software backend): no offscreen pass, no blit, and the child's textures stay in the parent's caches. `local_viewport` is honoured through a temporary override of the layout viewport. Viewports with a fade keep their sub-canvas, which, like the scroll sub-canvases, is now kept across frames and resized in place instead of reallocated when its size changes.
Caches. The image texture cache moves from the canvas to the GL context: every canvas and sub-canvas shares one content-keyed LRU (32 MiB), so an overlay or subsurface that is destroyed and recreated finds its images resident. A GLES canvas dropped with an overlay is pooled in `AppData::canvas_pool` and handed to the next overlay, with its compiled programs and glyph atlas, which took 130 to 290 ms to build on that GPU. Subsurface slots keep their scroll and viewport sub-canvases between rasters.
Surfaces. A `container.surface()` with shadows costs a Gaussian drop-shadow pass, a fill and one pass per inset shadow, all full-quad fragment shaders; a dozen glass cards were 40 ms of GPU per frame for chrome that never changes. `Canvas::fill_surface` on GLES now renders the chrome once per size, corners and style into a texture held by the context cache (`gles_render/surface_cache.rs`, 16 MiB LRU) and blits it, drawing directly only for surfaces with `PlusLighter` or `Overlay` insets, which depend on the backdrop.
Opaque regions. ltk never called `wl_surface.set_opaque_region`, so the compositor blended every ltk surface and repainted everything underneath a full-screen homescreen. A surface whose `background_color` is opaque now declares itself opaque, and the new `App::overlay_opaque_region( id )` lets an overlay declare the physical rects it paints opaque, such as a panel column over an opaque wallpaper. `sync_opaque_region` sends the rects only when they change.
Frame pacing. `eglSwapBuffers` blocked 13 to 37 ms per frame waiting for Mesa's own throttle callback; ltk paces itself on `wl_surface.frame`, so `make_current` sets the swap interval to 0. A subsurface move is now applied only when its parent has no frame callback outstanding, and the parent commit that applies it requests the next callback: a drag commits once per vblank with the latest finger position instead of once per input sample, without the app throttling samples itself. While the app reports `subsurface_motion_only`, the main's callback, and the callback of an overlay that hosts a subsurface, no longer force a full repaint; they mark the surface and the reconcile keeps the cadence with a bare commit when nothing moved. Swipe releases arm a settle the same way instead of dirtying every cache. Two hooks, `on_overlay_frame_committed` and `on_subsurface_committed`, let an app hand a panel over between its host and a sliding subsurface on real commits rather than loop ticks.
Lines. A diagonal `draw_line` on GLES stamped one `fill_rect` per pixel of length, 190 draw calls for a spinner and over 2000 for a pattern-lock stroke; it is now a single rotated quad through the rect shader, with round caps and antialiased edges.
A pixel test covers the scissor viewport, `for_size` gets a downscale test, and `docs/architecture.md`, `docs/widgets.md` and `docs/backends.md` describe the new hook, the viewport paths, the shared caches and the line rasteriser.
2026-09-18 09:55:09 +02:00

152 lines
5.3 KiB
Rust

// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Surface chrome: the client-side title bar and the Wayland input
//! region. Both are backend-agnostic — the software and GPU draw
//! paths call them with a `&mut Canvas` / a `&WlSurface` and get back
//! the geometry they need for hit testing + commit.
use smithay_client_toolkit::compositor::{ CompositorState, Region };
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
use crate::render::Canvas;
use crate::types::{ Color, Rect };
/// Declare the opaque part of the surface: the whole of it when the
/// background is opaque, else the app's `region` (physical rects), so the
/// compositor can skip blending it and repainting what lies underneath.
/// `applied` remembers the rects last sent.
pub( crate ) fn sync_opaque_region(
wl_surface: &WlSurface,
compositor: &CompositorState,
applied: &mut Option<Vec<Rect>>,
bg: Color,
region: Option<&[Rect]>,
width: u32,
height: u32,
scale: u32,
)
{
let s = scale.max( 1 ) as f32;
let rects: Vec<Rect> = if bg.a >= 1.0
{
vec![ Rect { x: 0.0, y: 0.0, width: width as f32 * s, height: height as f32 * s } ]
} else {
region.map( |r| r.to_vec() ).unwrap_or_default()
};
if applied.as_ref() == Some( &rects ) { return; }
if rects.is_empty()
{
wl_surface.set_opaque_region( None );
} else if let Ok( wl_region ) = Region::new( compositor ) {
for r in &rects
{
wl_region.add(
( r.x / s ).round() as i32,
( r.y / s ).round() as i32,
( r.width / s ).round() as i32,
( r.height / s ).round() as i32,
);
}
wl_surface.set_opaque_region( Some( wl_region.wl_region() ) );
}
*applied = Some( rects );
}
/// Paint the client-side title bar (close button, divider, title text).
///
/// Returns the close button rect in physical pixels so the caller can
/// store it for hit testing. Returns `Rect::default()` when `tb_h <=
/// 0` — overlays / layer-shell surfaces have no title bar.
pub( crate ) fn draw_titlebar( canvas: &mut Canvas, title: &str, pw: u32, tb_h: f32, sf: f32 ) -> Rect
{
if tb_h <= 0.0 { return Rect::default(); }
let tb_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: tb_h };
canvas.fill_rect( tb_rect, Color::rgb( 0.15, 0.15, 0.18 ), 0.0 );
let title_size = 15.0;
// `Canvas::draw_text` interprets `y` as the baseline. Match the
// vertical-centring formula used by `Button::draw_text_button` so
// chrome and content text lines line up at the same optical height.
let title_y = ( tb_h + title_size * sf ) / 2.0 - 2.0;
let title_w = canvas.measure_text( title, title_size );
let title_x = ( pw as f32 - title_w ) / 2.0;
canvas.draw_text( title, title_x, title_y, title_size, Color::WHITE );
let btn_size = tb_h - 8.0 * sf;
let btn_x = pw as f32 - btn_size - 8.0 * sf;
let btn_y = 4.0 * sf;
let close_rect_phys = Rect { x: btn_x, y: btn_y, width: btn_size, height: btn_size };
canvas.fill_rect( close_rect_phys, Color::rgba( 1.0, 1.0, 1.0, 0.1 ), 4.0 * sf );
let cx = btn_x + btn_size / 2.0;
let cy = btn_y + btn_size / 2.0;
let arm = btn_size * 0.25;
canvas.draw_line( cx - arm, cy - arm, cx + arm, cy + arm, Color::WHITE, 2.0 * sf );
canvas.draw_line( cx + arm, cy - arm, cx - arm, cy + arm, Color::WHITE, 2.0 * sf );
canvas.draw_line( 0.0, tb_h - 0.5, pw as f32, tb_h - 0.5, Color::rgba( 1.0, 1.0, 1.0, 0.15 ), 1.0 );
close_rect_phys
}
/// Stamp a red warning banner at the top of the surface when the
/// active theme came from the in-memory B/W fallback (i.e.
/// [`crate::theme::is_fallback_active`] returned `true`). Gives the
/// user a clear visual cue that `ltk-theme-default` is missing.
///
/// Paints a thin red strip across the top of the surface (24 px
/// logical) with the install-instruction text. When the fallback is
/// not active this is a no-op.
pub( crate ) fn draw_fallback_banner(
canvas: &mut Canvas,
pw: u32,
sf: f32,
)
{
if !crate::theme::is_fallback_active() { return; }
let banner_h = 24.0 * sf;
let rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: banner_h };
canvas.fill_rect( rect, Color::rgb( 0.75, 0.10, 0.10 ), 0.0 );
let msg = "ltk: fallback theme — install `ltk-theme-default`";
let size = 12.0;
let msg_w = canvas.measure_text( msg, size );
let x = ( ( pw as f32 - msg_w ) / 2.0 ).max( 6.0 * sf );
let y = ( banner_h - size * sf ) / 2.0;
canvas.draw_text( msg, x, y, size, Color::WHITE );
}
/// Apply or clear the surface's input region (set by
/// `App::input_region`). `None` restores the default of "receive
/// input everywhere"; `Some(&[])` makes the surface pointer-through.
pub( crate ) fn apply_input_region(
wl_surface: &WlSurface,
compositor: &CompositorState,
input_region: Option<&[Rect]>,
scale: u32,
)
{
if let Some( regions ) = input_region
{
if let Ok( region ) = Region::new( compositor )
{
// `Rect`s are physical (layout space); `wl_region` is logical.
let s = scale.max( 1 ) as f32;
for r in regions
{
region.add(
( r.x / s ).round() as i32,
( r.y / s ).round() as i32,
( r.width / s ).round() as i32,
( r.height / s ).round() as i32,
);
}
wl_surface.set_input_region( Some( region.wl_region() ) );
}
} else {
wl_surface.set_input_region( None );
}
}