Files
ltk/src/render/mod.rs
Pedro M. de Echanove Pasquin cfa0faff26
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
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.
2026-06-07 16:45:59 +02:00

726 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 ) );
}
}