Responsive scaling. ltk now offers two first-class ways to size a UI so it adapts across screens, chosen per process via `WidgetScaling { Fluid, Physical }` (`set_widget_scaling` / `widget_scaling`, default `Fluid`). Fluid sizing (`Length::fluid( px )`) makes a design pixel a proportion of the surface's smaller side, calibrated against a reference width (`set_fluid_reference` / `fluid_reference`, 412 px default) and bounded by `FLUID_MIN` / `FLUID_MAX`; physical sizing (`Length::dp( px )`) is a constant-physical-size pixel scaled by display density (`set_density` / `density`). `Length` gains `orient( portrait, landscape )` — resolve one value in portrait, another in landscape — plus `widget( px )`, which picks fluid or dp per the active mode. Canvas exposes `geom_px` (geometry, resolved in physical layout space) and `font_px` (font size, bridging logical / physical per mode) so widgets and apps share one resolution path. Note the rename: `set_design_reference` / `design_reference` became `set_fluid_reference` / `fluid_reference`, and `Length::dp` changed meaning — the old surface-proportional behaviour now lives on `Length::fluid`.
Widgets. Every stock widget resolves its default geometry and font through the widget-scaling mode instead of frozen pixels, so a whole UI scales coherently without per-call units. New size builders where they were missing: `button` gains `font_size` / `height`, `text_edit` gains `height` / `font_size_fluid`, `separator` gains `pad_v`, and assorted widgets accept a `Length` where they previously took only `f32`.
Overlays. `OverlaySpec::size` is now `( Length, Length )` instead of `( u32, u32 )`, resolved against the main surface when the overlay is materialized, so overlays can scale with the display; `Length::px( … )` reproduces the old fixed sizing.
API stabilization (toward 1.0). Widget struct fields are now `pub( crate )` — they are configured through builders, not field access — except the value / state types apps genuinely read or construct (`Time`, `Date`, `ComboState`), which stay public. The internal `test_support` helpers move behind a `test-support` Cargo feature (off by default, so third-party builds never see them; ltk's own `make test` enables it). `Separator` drops its `0.0`-means-mode sentinel for `Option<Length>`, so an explicit `pad_v( 0.0 )` is a real flush divider distinct from the mode-following default.
Performance guardrails. Opt-in diagnostics via `LTK_PERF_WARN=1` warn about stuck animations, sustained software-render animation, and low `poll_interval`; software-rendered animation is capped near 30 Hz to spare CPU on machines that fall back off EGL. Apps can override the cap with `App::cap_software_animation`.
Docs and build. The two scaling modes are documented in README, onboarding and architecture, with the earlier gradient / backdrop doc drift cleaned up. The Makefile now ships the `locales/` directory into the packaged crate (fixing i18n keys rendering raw for downstreams), builds the new `responsive` example, and runs tests with `--features test-support`.
162 lines
5.6 KiB
Rust
162 lines
5.6 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||
|
||
//! Widget that hosts content rendered by an external GL producer.
|
||
//!
|
||
//! Reserves layout space and, at draw time, samples a caller-provided GL
|
||
//! texture into the LTK canvas via [`Canvas::draw_external_texture`]. The
|
||
//! producer (a web engine, a video decoder, …) keeps ownership of the
|
||
//! texture and updates it on its own cadence; LTK only composites.
|
||
//!
|
||
//! # Source closure contract
|
||
//!
|
||
//! [`ExternalSource::Texture`] carries an
|
||
//! `Arc<dyn Fn(&glow::Context, Rect) -> Option<glow::Texture>>` that LTK
|
||
//! invokes once per frame, with its GLES context current. The producer:
|
||
//!
|
||
//! 1. Reads `Rect` (in physical pixels of the host surface) — useful for
|
||
//! sizing the inner viewport (e.g. resizing a `WPEToplevel` to match)
|
||
//! and for translating input coordinates by `rect.origin`.
|
||
//! 2. May allocate / update GL textures against the supplied
|
||
//! `glow::Context`.
|
||
//! 3. May bind extension-imported EGLImages onto a persistent texture.
|
||
//! 4. Returns the `glow::Texture` to sample, or `None` to paint
|
||
//! transparent (e.g. while the first frame is still being produced).
|
||
//!
|
||
//! Returning `None` for one frame and `Some` for the next is fine; LTK
|
||
//! re-invokes on every redraw.
|
||
//!
|
||
//! # Use cases
|
||
//!
|
||
//! * `ltk-webkit` hosting a `WPEView`-rendered page.
|
||
//! * Video / media playback widgets that decode into a GL texture.
|
||
//! * Any embedding that already has a GLES producer and wants its output
|
||
//! in-line with the rest of the LTK widget tree.
|
||
//!
|
||
//! # Example
|
||
//!
|
||
//! ```rust,no_run
|
||
//! # use std::sync::{ Arc, Mutex };
|
||
//! # use ltk::{ External, ExternalSource, Element };
|
||
//! # #[ derive( Clone ) ] enum Msg {}
|
||
//! # fn _ex() -> Element<Msg> {
|
||
//! let cached_texture: Arc<Mutex<Option<glow::Texture>>> = Arc::new( Mutex::new( None ) );
|
||
//! let cached = Arc::clone( &cached_texture );
|
||
//! External::new(
|
||
//! 800.0, 600.0,
|
||
//! ExternalSource::Texture( Arc::new( move | _gl, _rect | -> Option<glow::Texture>
|
||
//! {
|
||
//! *cached.lock().ok()?
|
||
//! } ) ),
|
||
//! ).into()
|
||
//! # }
|
||
//! ```
|
||
|
||
use std::sync::Arc;
|
||
|
||
use crate::render::Canvas;
|
||
use crate::types::Rect;
|
||
|
||
/// A widget that defers its pixels to an external GL texture producer.
|
||
///
|
||
/// The widget itself is non-interactive and produces no messages. Wrap it
|
||
/// in a [`crate::widget::pressable::Pressable`] (or similar) when input
|
||
/// capture is needed.
|
||
pub struct External
|
||
{
|
||
/// Reserved width in logical pixels.
|
||
pub( crate ) width: f32,
|
||
/// Reserved height in logical pixels.
|
||
pub( crate ) height: f32,
|
||
/// Source of the GL texture sampled at draw time.
|
||
pub( crate ) source: ExternalSource,
|
||
/// Opacity multiplier in `[0.0, 1.0]`.
|
||
pub( crate ) opacity: f32,
|
||
}
|
||
|
||
/// Backends an [`External`] widget can pull pixels from.
|
||
///
|
||
/// The only variant today is [`ExternalSource::Texture`], a closure
|
||
/// returning the current GL texture name. Future variants (DMA-BUF
|
||
/// imports, software pixmaps for the CPU backend, …) can be added
|
||
/// without changing call sites.
|
||
#[ derive( Clone ) ]
|
||
pub enum ExternalSource
|
||
{
|
||
/// Closure invoked once per frame with LTK's [`glow::Context`] current
|
||
/// and the widget's laid-out rect (in physical pixels of the host
|
||
/// surface). The producer can allocate textures, bind extension-
|
||
/// imported EGLImages, etc., and returns the texture name to sample.
|
||
/// Returning `None` paints transparent — useful while the producer
|
||
/// is still warming up its first frame. The rect is exposed so
|
||
/// embedders can size their inner viewport to match LTK's actual
|
||
/// allocation (e.g. resize a WPEToplevel on layout change) and
|
||
/// translate input coordinates by `rect.origin`.
|
||
Texture( Arc<dyn Fn( &glow::Context, Rect ) -> Option<glow::Texture> + Send + Sync> ),
|
||
|
||
/// Closure invoked once per frame with LTK's [`Canvas`] and the widget's
|
||
/// laid-out rect, for immediate-mode CPU drawing (no GL texture). Used to
|
||
/// host an Android `View`'s custom `onDraw` straight onto the LTK canvas.
|
||
/// Works on both the GLES and software backends.
|
||
Cpu( Arc<dyn Fn( &mut Canvas, Rect ) + Send + Sync> ),
|
||
}
|
||
|
||
impl External
|
||
{
|
||
/// Build a new external-content widget reserving `width × height`
|
||
/// logical pixels.
|
||
pub fn new( width: f32, height: f32, source: ExternalSource ) -> Self
|
||
{
|
||
Self { width, height, source, opacity: 1.0 }
|
||
}
|
||
|
||
/// Build an external widget that paints via an immediate-mode CPU closure
|
||
/// reserving `width × height` logical pixels.
|
||
pub fn cpu( width: f32, height: f32, draw: impl Fn( &mut Canvas, Rect ) + Send + Sync + 'static ) -> Self
|
||
{
|
||
Self::new( width, height, ExternalSource::Cpu( Arc::new( draw ) ) )
|
||
}
|
||
|
||
/// Override the opacity multiplier. Default: `1.0`.
|
||
pub fn opacity( mut self, opacity: f32 ) -> Self
|
||
{
|
||
self.opacity = opacity.clamp( 0.0, 1.0 );
|
||
self
|
||
}
|
||
|
||
pub fn preferred_size( &self, _max_width: f32 ) -> ( f32, f32 )
|
||
{
|
||
( self.width, self.height )
|
||
}
|
||
|
||
pub fn draw( &self, canvas: &mut Canvas, rect: Rect )
|
||
{
|
||
match &self.source
|
||
{
|
||
// Immediate CPU drawing works against any backend; the producer
|
||
// paints straight into the canvas at `rect`.
|
||
ExternalSource::Cpu( draw ) =>
|
||
{
|
||
draw( canvas, rect );
|
||
}
|
||
// External GL content only renders against the GLES backend — the
|
||
// software backend has no GL texture to sample. Skip silently.
|
||
ExternalSource::Texture( get ) =>
|
||
{
|
||
let gl = match canvas
|
||
{
|
||
Canvas::Software( _ ) => return,
|
||
Canvas::Gles( c ) => c.gl.clone(),
|
||
};
|
||
if let Some( tex ) = get( &gl, rect )
|
||
{
|
||
canvas.draw_external_texture( tex, rect, self.opacity );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[ cfg( test ) ]
|
||
mod tests;
|