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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// 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::render::clip::interior_clip;
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.
///
/// A GL texture stretched over the rect ([`ExternalSource::Texture`]) or
/// drawn at its own size ([`ExternalSource::Frame`]), or immediate CPU
/// drawing ([`ExternalSource::Cpu`]).
#[ 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> ),

	/// Like [`Self::Texture`], but the closure also returns the texture's size in physical pixels, and the texture is drawn at that size from the rect's top left corner, cut to the rect, rather than stretched over it. For a producer that catches up with a new rect a few frames late, such as a web page while its window is resized.
	Frame( Arc<dyn Fn( &glow::Context, Rect ) -> Option<( glow::Texture, u32, u32 )> + 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 );
				}
			}
			ExternalSource::Frame( get ) =>
			{
				let gl = match canvas
				{
					Canvas::Software( _ ) => return,
					Canvas::Gles( c )     => c.gl.clone(),
				};
				let Some( ( tex, width, height ) ) = get( &gl, rect ) else { return };
				let frame = Rect { x: rect.x, y: rect.y, width: width as f32, height: height as f32 };
				let outer = canvas.clip_bounds();
				canvas.set_clip_rects( &interior_clip( &outer, rect ) );
				canvas.draw_external_texture( tex, frame, self.opacity );
				if outer.is_empty()
				{
					canvas.clear_clip();
				} else {
					canvas.set_clip_rects( &outer );
				}
			}
		}
	}
}

#[ cfg( test ) ]
mod tests;