ltk/widget/external/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
// 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 width: f32,
/// Reserved height in logical pixels.
pub height: f32,
/// Source of the GL texture sampled at draw time.
pub source: ExternalSource,
/// Opacity multiplier in `[0.0, 1.0]`.
pub 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> ),
}
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 }
}
/// 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 )
{
// SAFETY-ish: this is the only call site of the source closure;
// LTK's draw pass guarantees the GLES context is current and
// the canvas is bound to its FBO. The rect we pass is the
// widget's laid-out rect in physical pixels — the same
// coordinate space pointer events arrive in, so the producer
// can use `rect.origin` to translate input.
// External content only renders against the GLES backend — the
// software backend has no GL texture to sample. Skip silently.
let gl = match canvas
{
Canvas::Software( _ ) => return,
Canvas::Gles( c ) => c.gl.clone(),
};
match &self.source
{
ExternalSource::Texture( get ) =>
{
if let Some( tex ) = get( &gl, rect )
{
canvas.draw_external_texture( tex, rect, self.opacity );
}
}
}
}
}
#[ cfg( test ) ]
mod tests;