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.
726 lines
23 KiB
Rust
726 lines
23 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||
|
||
//! Rendering surface used by every widget.
|
||
//!
|
||
//! [`Canvas`] is a thin enum wrapper over the per-frame rendering
|
||
//! backend. The CPU backend is [`SoftwareCanvas`] (tiny-skia + fontdue
|
||
//! rasterised into a `Pixmap`). The GPU backend is
|
||
//! [`crate::gles_render::GlesCanvas`] (EGL + GLES2/3).
|
||
//!
|
||
//! Widgets only ever see `&mut Canvas` — they call `fill_rect`,
|
||
//! `draw_text`, etc. The enum dispatches by `match self` (no `dyn`,
|
||
//! so the call sites stay monomorphic and inlinable). Field-style
|
||
//! access to backend internals (`pixmap`, `font`, `dpi_scale`…) is
|
||
//! replaced by accessor methods that the GPU variant can also
|
||
//! implement.
|
||
//!
|
||
//! # Submodule layout
|
||
//!
|
||
//! * [`setup`] — `SoftwareCanvas::{new, sub_canvas, resize, blit,
|
||
//! set_font_registry, font_for}` (construction + accessors).
|
||
//! * [`clip`] — `SoftwareCanvas::{set_clip_rects, clear_clip,
|
||
//! has_clip, strip_intersects_clip, clear_rects_transparent}`.
|
||
//! * [`primitives`] — `SoftwareCanvas::{clear, fill, fill_rect,
|
||
//! stroke_rect, draw_line}`.
|
||
//! * [`text`] — `SoftwareCanvas::{draw_text, measure_text}`.
|
||
//! * [`image`] — `SoftwareCanvas::{draw_image_data,
|
||
//! write_to_wayland_buf}`.
|
||
//! * [`helpers`] — free functions: `build_rounded_rect`,
|
||
//! `find_font`, `find_font_opt`, `SYSTEM_FONT_CANDIDATES`.
|
||
|
||
use std::cell::Cell;
|
||
use std::sync::Arc;
|
||
|
||
use fontdue::{ Font, LineMetrics, Metrics };
|
||
use tiny_skia::{ Mask, Pixmap };
|
||
|
||
use crate::gles_render::{ BorrowedGlesTexture, GlesCanvas, GlesVersion };
|
||
use crate::theme::{ FontRegistry, FontStyle, InsetShadow, Paint as ThemePaint, Shadow };
|
||
use crate::types::{ Color, Corners, Rect };
|
||
|
||
pub( crate ) mod setup;
|
||
pub( crate ) mod clip;
|
||
pub( crate ) mod primitives;
|
||
pub( crate ) mod text;
|
||
pub( crate ) mod image;
|
||
pub( crate ) mod helpers;
|
||
|
||
// ─── Backend flag ────────────────────────────────────────────────────────────
|
||
|
||
thread_local!
|
||
{
|
||
/// `true` when this thread's surfaces are rendered through the
|
||
/// software (tiny-skia / SHM) path, `false` when they go through
|
||
/// the GLES path. Set once at startup based on EGL availability
|
||
/// and read by view code that needs to branch on backend (e.g. a
|
||
/// layout that costs something specific to one path and isn't
|
||
/// worth replicating on the other). Stays a thread-local so view
|
||
/// code does not need to plumb a flag through every layout call.
|
||
static SOFTWARE_RENDER: Cell<bool> = const { Cell::new( false ) };
|
||
}
|
||
|
||
/// Toggle the software-render flag for this thread. Consumers read
|
||
/// with [`is_software_render`].
|
||
pub fn set_software_render( on: bool )
|
||
{
|
||
SOFTWARE_RENDER.with( | c | c.set( on ) );
|
||
}
|
||
|
||
/// `true` when the active surfaces on this thread render through the
|
||
/// software path. Used by view code that wants to avoid pipeline
|
||
/// effects the software backend doesn't implement.
|
||
pub fn is_software_render() -> bool
|
||
{
|
||
SOFTWARE_RENDER.with( | c | c.get() )
|
||
}
|
||
|
||
// ─── Glyph cache ─────────────────────────────────────────────────────────────
|
||
|
||
/// Cache key for a rasterized glyph. `size_bits` is the f32 bit
|
||
/// pattern of `size * dpi_scale`; `font_id` is the address of the
|
||
/// `Arc<Font>` used for the rasterisation, so distinct weights /
|
||
/// families of the same `(glyph_id, size)` do not collide on the
|
||
/// cache. `glyph_id` is the per-font glyph index returned by
|
||
/// HarfBuzz shaping, so cached entries persist across script
|
||
/// transitions and Arabic / Devanagari / CJK forms cluster
|
||
/// independently of the `char` codepoint that produced them.
|
||
#[ derive( Hash, PartialEq, Eq, Clone, Copy ) ]
|
||
pub ( super ) struct GlyphKey
|
||
{
|
||
pub ( super ) glyph_id: u16,
|
||
pub ( super ) size_bits: u32,
|
||
pub ( super ) font_id: usize,
|
||
}
|
||
|
||
/// Cached glyph bitmap and metrics. Fontdue's rasterize call is the
|
||
/// dominant per-frame CPU cost for text-heavy UIs; reusing across
|
||
/// frames avoids that work.
|
||
pub ( super ) struct GlyphEntry
|
||
{
|
||
pub ( super ) metrics: Metrics,
|
||
pub ( super ) bitmap: Vec<u8>,
|
||
}
|
||
|
||
// ─── SoftwareCanvas ──────────────────────────────────────────────────────────
|
||
|
||
/// Software rendering backend backed by a tiny-skia [`Pixmap`] and a
|
||
/// fontdue [`Font`].
|
||
///
|
||
/// Wrapped by [`Canvas`] so the GPU backend can be slotted in by the
|
||
/// runtime without changing widget code. Widgets themselves never see
|
||
/// `SoftwareCanvas` directly.
|
||
pub struct SoftwareCanvas
|
||
{
|
||
/// The pixel buffer drawn into each frame.
|
||
pub pixmap: Pixmap,
|
||
/// The loaded system font used for all text rendering.
|
||
///
|
||
/// Kept as the default fallback so widgets that do not yet ask for a
|
||
/// specific family through [`SoftwareCanvas::font_for`] keep
|
||
/// working. Populated from
|
||
/// [`crate::render::helpers::find_font`] at construction time.
|
||
pub font: Arc<Font>,
|
||
/// Raw bytes of the default font. Kept alongside `font` so the
|
||
/// HarfBuzz shaper (rustybuzz) can be invoked without re-reading
|
||
/// the file — fontdue does not expose its internal byte buffer.
|
||
pub font_bytes: Arc<Vec<u8>>,
|
||
/// TTC sub-face index for the default font (0 for single-face
|
||
/// files; collection index for `.ttc` archives).
|
||
pub font_face: u32,
|
||
/// Optional theme font registry. When present,
|
||
/// [`SoftwareCanvas::font_for`] consults it before falling back
|
||
/// to `font`. Populated by the caller once the theme's `fonts`
|
||
/// block has been loaded.
|
||
pub font_registry: Option<Arc<FontRegistry>>,
|
||
/// DPI scale factor applied to font sizes.
|
||
pub dpi_scale: f32,
|
||
/// Global alpha multiplier for all drawing operations (0.0 =
|
||
/// transparent, 1.0 = opaque).
|
||
pub global_alpha: f32,
|
||
/// Persistent cache of rasterized glyphs, indexed by (char, scaled size).
|
||
/// Grows on demand; not LRU-bounded since typical UIs use few sizes.
|
||
glyph_cache: std::collections::HashMap<GlyphKey, GlyphEntry>,
|
||
/// Optional clip mask applied to all paint operations. Set via
|
||
/// [`Canvas::set_clip_rects`] during a partial redraw so only
|
||
/// pixels inside the dirty rects are touched. `None` means "draw
|
||
/// everywhere".
|
||
clip_mask: Option<Mask>,
|
||
/// Bounding boxes of the clip rects in physical pixels. Used by
|
||
/// [`SoftwareCanvas::draw_text`] to do an early reject without
|
||
/// poking the mask byte by byte (the Mask buffer is still
|
||
/// authoritative inside the pixel loop).
|
||
clip_bounds: Vec<Rect>,
|
||
}
|
||
|
||
// ─── Canvas enum + dispatch ─────────────────────────────────────────────────
|
||
|
||
/// Per-frame rendering surface. Wraps a backend (software or GPU)
|
||
/// behind an enum so widgets can stay backend-agnostic.
|
||
///
|
||
/// All drawing methods are dispatched by `match self` — no `dyn`
|
||
/// indirection, so the backend branch stays predictable and
|
||
/// inlinable in the hot path.
|
||
pub enum Canvas
|
||
{
|
||
/// CPU rasterisation via tiny-skia + fontdue, written to a
|
||
/// `wl_shm` buffer.
|
||
Software( SoftwareCanvas ),
|
||
/// GPU rasterisation via EGL + GLES 2/3. Presents via
|
||
/// `eglSwapBuffers`; [`Canvas::write_to_wayland_buf`] is a no-op
|
||
/// for this variant.
|
||
Gles( GlesCanvas ),
|
||
}
|
||
|
||
impl Canvas
|
||
{
|
||
/// Build a software canvas. The GPU backend requires an EGL
|
||
/// context — see [`Canvas::new_gles`].
|
||
pub fn new( width: u32, height: u32 ) -> Self
|
||
{
|
||
Canvas::Software( SoftwareCanvas::new( width, height ) )
|
||
}
|
||
|
||
/// Build a GPU canvas on an already-current EGL context.
|
||
pub fn new_gles(
|
||
gl: Arc<glow::Context>, version: GlesVersion, width: u32, height: u32,
|
||
) -> Self
|
||
{
|
||
Canvas::Gles( GlesCanvas::new( gl, version, width, height ) )
|
||
}
|
||
|
||
/// `(width, height)` of the underlying surface in physical pixels.
|
||
pub fn size( &self ) -> ( u32, u32 )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => ( c.pixmap.width(), c.pixmap.height() ),
|
||
Canvas::Gles( c ) => c.size(),
|
||
}
|
||
}
|
||
|
||
/// `(width, height)` of the surface in **logical** pixels (physical
|
||
/// size divided by `dpi_scale`). This is the right viewport for
|
||
/// resolving [`crate::Length`] values, which are themselves in
|
||
/// logical units. Falls back to physical size if `dpi_scale` is
|
||
/// 0 or negative so a misconfigured canvas still returns a usable
|
||
/// non-zero viewport instead of `NaN`/`inf`.
|
||
///
|
||
/// ```rust,no_run
|
||
/// # use ltk::core::Canvas;
|
||
/// let mut c = Canvas::new( 720, 1440 );
|
||
/// c.set_dpi_scale( 2.0 );
|
||
/// assert_eq!( c.viewport_logical(), ( 360.0, 720.0 ) );
|
||
/// ```
|
||
pub fn viewport_logical( &self ) -> ( f32, f32 )
|
||
{
|
||
let ( pw, ph ) = self.size();
|
||
let scale = self.dpi_scale();
|
||
if scale > 0.0
|
||
{
|
||
( pw as f32 / scale, ph as f32 / scale )
|
||
} else {
|
||
( pw as f32, ph as f32 )
|
||
}
|
||
}
|
||
|
||
/// `(width, height)` of the surface in **physical** pixels — the same
|
||
/// space the layout tree is computed in (the root rect is `pw × ph`).
|
||
/// This is the viewport that layout-affecting [`crate::Length`] values
|
||
/// (widths, paddings, gaps, widget sizes) must resolve against so a
|
||
/// `Vw(100)` fills the surface. Text font sizes are the exception:
|
||
/// they resolve against [`Self::viewport_logical`] and are then scaled
|
||
/// by `dpi_scale` at raster time, so they must NOT use this.
|
||
pub fn viewport_layout( &self ) -> ( f32, f32 )
|
||
{
|
||
let ( pw, ph ) = self.size();
|
||
( pw as f32, ph as f32 )
|
||
}
|
||
|
||
/// Borrow the GLES texture backing this canvas, when the canvas
|
||
/// is GPU-backed.
|
||
pub fn borrowed_gles_texture( &self ) -> Option<BorrowedGlesTexture>
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( _ ) => None,
|
||
Canvas::Gles( c ) => Some( c.borrowed_texture() ),
|
||
}
|
||
}
|
||
|
||
/// Read a GLES canvas into tightly packed RGBA8, top-left row
|
||
/// first. Intentionally unavailable for software canvases because
|
||
/// the software backend's canonical export path is
|
||
/// [`Self::write_to_wayland_buf`].
|
||
pub fn read_gles_rgba_pixels( &self, out: &mut [u8] ) -> Result<(), String>
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( _ ) => Err( "read_gles_rgba_pixels requires Canvas::Gles".to_string() ),
|
||
Canvas::Gles( c ) => c.read_rgba_pixels( out ),
|
||
}
|
||
}
|
||
|
||
/// Composite an externally-owned GL texture into `dest`. No-op on
|
||
/// the software backend (no GL state to sample from). Used by
|
||
/// widgets that host content rendered by an external producer —
|
||
/// the producer keeps ownership of the texture name; this call
|
||
/// only samples it through the standard texture program.
|
||
pub fn draw_external_texture( &mut self, texture: glow::Texture, dest: Rect, opacity: f32 )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( _ ) => {}
|
||
Canvas::Gles( c ) => c.draw_external_texture( texture, dest, opacity ),
|
||
}
|
||
}
|
||
|
||
pub fn dpi_scale( &self ) -> f32
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.dpi_scale,
|
||
Canvas::Gles( c ) => c.dpi_scale(),
|
||
}
|
||
}
|
||
|
||
pub fn set_dpi_scale( &mut self, s: f32 )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.dpi_scale = s,
|
||
Canvas::Gles( c ) => c.set_dpi_scale( s ),
|
||
}
|
||
}
|
||
|
||
pub fn global_alpha( &self ) -> f32
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.global_alpha,
|
||
Canvas::Gles( c ) => c.global_alpha(),
|
||
}
|
||
}
|
||
|
||
pub fn set_global_alpha( &mut self, a: f32 )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.global_alpha = a,
|
||
Canvas::Gles( c ) => c.set_global_alpha( a ),
|
||
}
|
||
}
|
||
|
||
/// Shared font handle. Exposed so widgets that need raw `fontdue`
|
||
/// access (e.g. `Text` for ascent/descent) do not have to go
|
||
/// through wrappers for every metric they read.
|
||
pub fn font( &self ) -> &Font
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => &c.font,
|
||
Canvas::Gles( c ) => c.font(),
|
||
}
|
||
}
|
||
|
||
/// Install a theme font registry on the active backend.
|
||
pub fn set_font_registry( &mut self, registry: Arc<FontRegistry> )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.set_font_registry( registry ),
|
||
Canvas::Gles( c ) => c.set_font_registry( registry ),
|
||
}
|
||
}
|
||
|
||
/// Resolve a specific font via the theme registry, falling back
|
||
/// to the system-default [`Self::font`] when no registry is
|
||
/// installed or the triple cannot be satisfied.
|
||
pub fn font_for( &self, family: &str, weight: u16, style: FontStyle ) -> Arc<Font>
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.font_for( family, weight, style ),
|
||
Canvas::Gles( c ) => c.font_for( family, weight, style ),
|
||
}
|
||
}
|
||
|
||
/// Convenience wrapper around `font().metrics(...)` already
|
||
/// pre-scaled by `dpi_scale`. Most callers want this rather than
|
||
/// the raw font handle.
|
||
pub fn font_metrics( &self, ch: char, size: f32 ) -> Metrics
|
||
{
|
||
self.font().metrics( ch, size * self.dpi_scale() )
|
||
}
|
||
|
||
/// Convenience wrapper around `font().horizontal_line_metrics(...)`.
|
||
pub fn font_line_metrics( &self, size: f32 ) -> Option<LineMetrics>
|
||
{
|
||
self.font().horizontal_line_metrics( size )
|
||
}
|
||
|
||
pub fn resize( &mut self, width: u32, height: u32 )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.resize( width, height ),
|
||
Canvas::Gles( c ) => c.resize( width, height ),
|
||
}
|
||
}
|
||
|
||
pub fn sub_canvas( &self, width: u32, height: u32 ) -> Canvas
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => Canvas::Software( c.sub_canvas( width, height ) ),
|
||
Canvas::Gles( c ) => Canvas::Gles( c.sub_canvas( width, height ) ),
|
||
}
|
||
}
|
||
|
||
pub fn blit( &mut self, src: &Canvas, dest_x: i32, dest_y: i32 )
|
||
{
|
||
self.blit_fade_bottom( src, dest_x, dest_y, 0.0 )
|
||
}
|
||
|
||
/// Like [`Self::blit`] but feathers the last `fade_bottom_px` source
|
||
/// rows so the bottom edge fades to transparent. The software backend
|
||
/// currently ignores `fade_bottom_px`, so the dissolve is GLES-only.
|
||
pub fn blit_fade_bottom( &mut self, src: &Canvas, dest_x: i32, dest_y: i32, fade_bottom_px: f32 )
|
||
{
|
||
match ( self, src )
|
||
{
|
||
( Canvas::Software( dst ), Canvas::Software( s ) ) =>
|
||
{
|
||
let _ = fade_bottom_px;
|
||
dst.blit( s, dest_x, dest_y );
|
||
}
|
||
( Canvas::Gles( dst ), Canvas::Gles( s ) ) =>
|
||
{
|
||
dst.blit_fade_bottom( s, dest_x, dest_y, fade_bottom_px );
|
||
}
|
||
// Cross-backend blits would need an SHM↔texture upload.
|
||
// The toolkit only ever creates sub-canvases of the same
|
||
// kind as their parent, so this is unreachable in practice.
|
||
_ => unimplemented!( "cross-backend blit not supported" ),
|
||
}
|
||
}
|
||
|
||
pub fn set_clip_rects( &mut self, rects: &[Rect] )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.set_clip_rects( rects ),
|
||
Canvas::Gles( c ) => c.set_clip_rects( rects ),
|
||
}
|
||
}
|
||
|
||
/// Snapshot the currently installed clip bounds (empty when no clip
|
||
/// is active). Used by widgets that need to install a tighter clip
|
||
/// for a single primitive and then restore whatever the outer
|
||
/// partial-redraw or sub-canvas clip was — there is no stack
|
||
/// internally, so round-tripping through
|
||
/// [`Self::set_clip_rects`] with the snapshot is how to compose.
|
||
pub fn clip_bounds( &self ) -> Vec<Rect>
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.clip_bounds_snapshot(),
|
||
Canvas::Gles( c ) => c.clip_bounds_snapshot(),
|
||
}
|
||
}
|
||
|
||
pub fn clear_clip( &mut self )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.clear_clip(),
|
||
Canvas::Gles( c ) => c.clear_clip(),
|
||
}
|
||
}
|
||
|
||
pub fn clear( &mut self )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.clear(),
|
||
Canvas::Gles( c ) => c.clear(),
|
||
}
|
||
}
|
||
|
||
pub fn fill( &mut self, color: Color )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.fill( color ),
|
||
Canvas::Gles( c ) => c.fill( color ),
|
||
}
|
||
}
|
||
|
||
pub fn fill_rect( &mut self, rect: Rect, color: Color, corners: impl Into<Corners> )
|
||
{
|
||
let corners = corners.into();
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.fill_rect( rect, color, corners ),
|
||
Canvas::Gles( c ) => c.fill_rect( rect, color, corners ),
|
||
}
|
||
}
|
||
|
||
/// Paint-driven rectangle fill.
|
||
///
|
||
/// Dispatches on the [`crate::theme::Paint`] variant. Solid
|
||
/// fills go straight through [`Self::fill_rect`]. Gradients
|
||
/// (linear and radial) are routed to dedicated shaders on the
|
||
/// GPU backend; on the Software backend they still collapse to a
|
||
/// flat fill from the first stop — tiny-skia can render
|
||
/// gradients natively, but wiring that up is left for a
|
||
/// follow-up.
|
||
pub fn fill_paint_rect( &mut self, rect: Rect, paint: &ThemePaint, corners: impl Into<Corners> )
|
||
{
|
||
let corners = corners.into();
|
||
match paint
|
||
{
|
||
ThemePaint::Solid( c ) => self.fill_rect( rect, *c, corners ),
|
||
ThemePaint::Linear( g ) =>
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( sc ) =>
|
||
{
|
||
let c = g.stops.first().map( |s| s.color ).unwrap_or( Color::TRANSPARENT );
|
||
sc.fill_rect( rect, c, corners );
|
||
}
|
||
Canvas::Gles( gc ) => gc.fill_linear_gradient_rect( rect, g, corners ),
|
||
}
|
||
}
|
||
ThemePaint::Radial( g ) =>
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( sc ) =>
|
||
{
|
||
let c = g.stops.first().map( |s| s.color ).unwrap_or( Color::TRANSPARENT );
|
||
sc.fill_rect( rect, c, corners );
|
||
}
|
||
Canvas::Gles( gc ) => gc.fill_radial_gradient_rect( rect, g, corners ),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn stroke_rect( &mut self, rect: Rect, color: Color, width: f32, corners: impl Into<Corners> )
|
||
{
|
||
let corners = corners.into();
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.stroke_rect( rect, color, width, corners ),
|
||
Canvas::Gles( c ) => c.stroke_rect( rect, color, width, corners ),
|
||
}
|
||
}
|
||
|
||
/// Paint an outer drop shadow behind the rounded rect `target`.
|
||
///
|
||
/// On the GPU backend this runs an analytic soft-shadow shader
|
||
/// in one draw call — no FBO, no cache, no readback. On the
|
||
/// Software backend it is a no-op today.
|
||
pub fn fill_shadow_outer( &mut self, target: Rect, shadow: &Shadow, corners: impl Into<Corners> )
|
||
{
|
||
let corners = corners.into();
|
||
match self
|
||
{
|
||
Canvas::Software( _ ) => { /* TODO: tiny-skia BlurDropShadow */ }
|
||
Canvas::Gles( c ) => c.fill_shadow_outer( target, shadow, corners ),
|
||
}
|
||
}
|
||
|
||
/// Paint an inner (inset) shadow inside the rounded rect
|
||
/// `target`.
|
||
///
|
||
/// On the GPU backend, uses a dedicated shader whose inner SDF
|
||
/// encodes `shadow.offset` and `shadow.spread`. The blend state
|
||
/// is switched per-call to honour `shadow.blend`: `Normal`,
|
||
/// `PlusLighter`, `Multiply` and `Screen` map to fixed-function
|
||
/// blend modes; `Overlay` routes through a dedicated shader that
|
||
/// snapshots the FBO and computes the CSS Overlay formula
|
||
/// in-shader.
|
||
///
|
||
/// On the Software backend this is a no-op today.
|
||
pub fn fill_shadow_inset( &mut self, target: Rect, shadow: &InsetShadow, corners: impl Into<Corners> )
|
||
{
|
||
let corners = corners.into();
|
||
match self
|
||
{
|
||
Canvas::Software( _ ) => { /* TODO: tiny-skia inner shadow */ }
|
||
Canvas::Gles( c ) => c.fill_shadow_inset( target, shadow, corners ),
|
||
}
|
||
}
|
||
|
||
/// Unified surface painter. Composes a themed surface in the canonical
|
||
/// paint order: outer shadows → fill → insets.
|
||
pub fn fill_surface
|
||
(
|
||
&mut self,
|
||
rect: Rect,
|
||
fill: &ThemePaint,
|
||
outer_shadows: &[Shadow],
|
||
inset_shadows: &[InsetShadow],
|
||
corners: impl Into<Corners>,
|
||
)
|
||
{
|
||
let corners = corners.into();
|
||
|
||
for shadow in outer_shadows
|
||
{
|
||
self.fill_shadow_outer( rect, shadow, corners );
|
||
}
|
||
|
||
self.fill_paint_rect( rect, fill, corners );
|
||
|
||
for inset in inset_shadows
|
||
{
|
||
self.fill_shadow_inset( rect, inset, corners );
|
||
}
|
||
}
|
||
|
||
pub fn draw_line( &mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color, width: f32 )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.draw_line( x0, y0, x1, y1, color, width ),
|
||
Canvas::Gles( c ) => c.draw_line( x0, y0, x1, y1, color, width ),
|
||
}
|
||
}
|
||
|
||
pub fn draw_text( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.draw_text( text, x, y, size, color ),
|
||
Canvas::Gles( c ) => c.draw_text( text, x, y, size, color ),
|
||
}
|
||
}
|
||
|
||
/// Draw `text` with an explicitly supplied font instead of the
|
||
/// canvas default. Use [`Self::font_for`] to resolve a `(family,
|
||
/// weight, style)` triple from the active theme registry first.
|
||
pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.draw_text_with_font( text, x, y, size, color, font ),
|
||
Canvas::Gles( c ) => c.draw_text_with_font( text, x, y, size, color, font ),
|
||
}
|
||
}
|
||
|
||
pub fn measure_text( &self, text: &str, size: f32 ) -> f32
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.measure_text( text, size ),
|
||
Canvas::Gles( c ) => c.measure_text( text, size ),
|
||
}
|
||
}
|
||
|
||
/// Width of `text` rendered with `font`. Mirrors
|
||
/// [`Self::measure_text`] but bypasses the canvas default font so
|
||
/// text laid out at one weight and drawn at another stays aligned.
|
||
pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.measure_text_with_font( text, size, font ),
|
||
Canvas::Gles( c ) => c.measure_text_with_font( text, size, font ),
|
||
}
|
||
}
|
||
|
||
pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.draw_image_data( rgba_data, img_w, img_h, dest, opacity ),
|
||
Canvas::Gles( c ) => c.draw_image_data( rgba_data, img_w, img_h, dest, opacity ),
|
||
}
|
||
}
|
||
|
||
/// Zero pixels inside each rect — used by the partial-redraw
|
||
/// path when the surface background is fully transparent.
|
||
pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.clear_rects_transparent( rects ),
|
||
Canvas::Gles( c ) => c.clear_rects_transparent( rects ),
|
||
}
|
||
}
|
||
|
||
/// Copy / present the rendered frame. For software this fills a
|
||
/// `wl_shm` buffer (with optional R/B swap for Argb8888). For
|
||
/// GPU the commit happens via `eglSwapBuffers` elsewhere — this
|
||
/// call is a no-op.
|
||
pub fn write_to_wayland_buf( &self, buf: &mut [u8], swap_rb: bool )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( c ) => c.write_to_wayland_buf( buf, swap_rb ),
|
||
Canvas::Gles( _ ) => {}
|
||
}
|
||
}
|
||
|
||
/// Publish the in-progress GPU frame: blit the FBO onto the EGL
|
||
/// window's default framebuffer. The follow-up `eglSwapBuffers`
|
||
/// (done outside the canvas) is what actually commits to the
|
||
/// compositor. No-op on software, where presentation is the SHM
|
||
/// `attach_to`/`commit` pair.
|
||
pub fn present( &mut self )
|
||
{
|
||
match self
|
||
{
|
||
Canvas::Software( _ ) => {}
|
||
Canvas::Gles( c ) => c.present(),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[ cfg( test ) ]
|
||
mod viewport_tests
|
||
{
|
||
use super::Canvas;
|
||
|
||
#[ test ]
|
||
fn viewport_logical_at_scale_one_matches_physical()
|
||
{
|
||
let c = Canvas::new( 800, 600 );
|
||
assert_eq!( c.viewport_logical(), ( 800.0, 600.0 ) );
|
||
}
|
||
|
||
#[ test ]
|
||
fn viewport_logical_divides_by_dpi_scale()
|
||
{
|
||
let mut c = Canvas::new( 720, 1440 );
|
||
c.set_dpi_scale( 2.0 );
|
||
assert_eq!( c.viewport_logical(), ( 360.0, 720.0 ) );
|
||
}
|
||
|
||
#[ test ]
|
||
fn viewport_logical_falls_back_to_physical_when_scale_is_zero()
|
||
{
|
||
// Guard the misconfigured-scale path: a divide-by-zero would
|
||
// poison every `Length::Vmin`/`Vw`/`Vh` resolution downstream.
|
||
let mut c = Canvas::new( 800, 600 );
|
||
c.set_dpi_scale( 0.0 );
|
||
assert_eq!( c.viewport_logical(), ( 800.0, 600.0 ) );
|
||
}
|
||
|
||
#[ test ]
|
||
fn viewport_layout_is_physical_and_ignores_dpi_scale()
|
||
{
|
||
// Layout-affecting `Length` values resolve against this, so it must
|
||
// stay in physical space (where the layout tree is computed) even on
|
||
// HiDPI — unlike `viewport_logical`, it does not divide by the scale.
|
||
let mut c = Canvas::new( 720, 1440 );
|
||
assert_eq!( c.viewport_layout(), ( 720.0, 1440.0 ) );
|
||
c.set_dpi_scale( 2.0 );
|
||
assert_eq!( c.viewport_layout(), ( 720.0, 1440.0 ) );
|
||
}
|
||
}
|