Skip to main content

frust_widgets/
texture_view.rs

1//! The `TextureView`/`TextureWidget` leaf: draws an externally rendered scene
2//! texture — a video frame, a camera preview, a 3D view — by the id its
3//! producer registered it under.
4//!
5//! [`texture_view`] takes that id and nothing else: the renderer's frame bus
6//! reports it (`TextureRegistration::scene_texture_id`), as does any other
7//! external pass's own `SceneTextureId::get`. This crate never sees the
8//! texture itself; it paints a `draw_scene_texture(id, dest)` command and the
9//! engine composites whatever is bound under the id that frame (nothing, if
10//! nothing is bound yet — never an error).
11//!
12//! `.fit(..)` places the texture in the widget's box like [`crate::Image`]'s
13//! fit does, given the texture's own extent from `.source_size(..)` (the
14//! widget cannot read it back from the GPU); without one every fit fills the
15//! box. `.corner_radius(..)` clips the box to rounded corners. Sizing is
16//! `.expand()` by default, or `.size(..)`.
17//!
18//! # Inside a retained bracket
19//!
20//! The texture changes behind the display list's back — a new frame is
21//! uploaded without the widget repainting — so a retained subtree holding a
22//! texture view cannot be replayed from a cached body: the engine accepts the
23//! draw inside a retained bracket but marks that body unretainable and paints
24//! it every frame. Keep a texture view outside a repaint boundary's subtree
25//! when the rest of that subtree is worth retaining.
26//!
27//! ```
28//! use frust_core::View;
29//! use frust_widgets::{TextureFit, texture_view};
30//! use kurbo::Size;
31//!
32//! fn player(video_id: u64) -> impl View<()> + use<> {
33//!     texture_view(video_id)
34//!         .source_size(Size::new(1920.0, 1080.0))
35//!         .fit(TextureFit::Contain)
36//!         .corner_radius(12.0)
37//!         .label("Video")
38//! }
39//! # let _ = player;
40//! ```
41
42use frust_core::accesskit::Role;
43use frust_core::{
44    BoxConstraints, BuildCtx, ChangeFlags, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View,
45    Widget,
46};
47use kurbo::{Rect, Size};
48
49/// How a [`TextureView`] places its texture in the widget's box.
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51pub enum TextureFit {
52    /// Scale (preserving aspect ratio) to fit entirely inside the box,
53    /// centred; the other axis is letterboxed.
54    #[default]
55    Contain,
56    /// Scale (preserving aspect ratio) to cover the box entirely, centred;
57    /// the overflow is clipped to the box.
58    Cover,
59    /// Stretch to exactly the box, ignoring aspect ratio.
60    Fill,
61}
62
63/// How a [`TextureView`] resolves its layout size.
64#[derive(Clone, Copy, Debug, PartialEq)]
65enum TextureSize {
66    /// Fill the incoming constraints' maximum.
67    Expand,
68    /// A fixed size, clamped into the incoming constraints.
69    Explicit(Size),
70}
71
72/// The rect (relative to the widget's own origin) a `natural`-sized texture
73/// is drawn into inside a `dest`-sized box under `fit`.
74///
75/// `Fill` — or a degenerate `natural`/`dest`, which has no aspect ratio to
76/// keep — is exactly the box. `Contain` is centred and no larger than the box
77/// on either axis; `Cover` is centred and no smaller (the caller clips).
78#[must_use]
79pub fn texture_fit_rect(natural: Size, dest: Size, fit: TextureFit) -> Rect {
80    let degenerate = !(natural.width > 0.0
81        && natural.height > 0.0
82        && dest.width > 0.0
83        && dest.height > 0.0
84        && natural.width.is_finite()
85        && natural.height.is_finite());
86    let fit_width = dest.width / natural.width;
87    let fit_height = dest.height / natural.height;
88    let scale = match fit {
89        _ if degenerate => return Rect::from_origin_size((0.0, 0.0), dest),
90        TextureFit::Fill => return Rect::from_origin_size((0.0, 0.0), dest),
91        TextureFit::Contain => fit_width.min(fit_height),
92        TextureFit::Cover => fit_width.max(fit_height),
93    };
94    let width = natural.width * scale;
95    let height = natural.height * scale;
96    let x = (dest.width - width) / 2.0;
97    let y = (dest.height - height) / 2.0;
98    Rect::new(x, y, x + width, y + height)
99}
100
101/// A declarative view of an externally rendered scene texture. See the
102/// [module docs](self).
103#[derive(Clone, Debug, PartialEq)]
104pub struct TextureView {
105    id: u64,
106    fit: TextureFit,
107    source_size: Option<Size>,
108    size: TextureSize,
109    corner_radius: f64,
110    label: Option<String>,
111}
112
113/// A view drawing the scene texture registered under `scene_texture_id`,
114/// full-bleed and [`TextureFit::Contain`] by default. See the
115/// [module docs](self).
116#[must_use]
117pub fn texture_view(scene_texture_id: u64) -> TextureView {
118    TextureView {
119        id: scene_texture_id,
120        fit: TextureFit::default(),
121        source_size: None,
122        size: TextureSize::Expand,
123        corner_radius: 0.0,
124        label: None,
125    }
126}
127
128impl TextureView {
129    /// How the texture is placed in the box. Needs [`Self::source_size`] to
130    /// keep an aspect ratio; without it every fit fills the box.
131    #[must_use]
132    pub fn fit(mut self, fit: TextureFit) -> Self {
133        self.fit = fit;
134        self
135    }
136
137    /// The texture's own extent (a video's resolution), which `fit` keeps the
138    /// aspect ratio of. Only the ratio matters.
139    #[must_use]
140    pub fn source_size(mut self, size: Size) -> Self {
141        self.source_size = Some(size);
142        self
143    }
144
145    /// A fixed size, clamped into the parent's constraints. Overrides
146    /// [`Self::expand`] when called after it.
147    #[must_use]
148    pub fn size(mut self, size: Size) -> Self {
149        self.size = TextureSize::Explicit(size);
150        self
151    }
152
153    /// Fill the parent's maximum constraints — the default.
154    #[must_use]
155    pub fn expand(mut self) -> Self {
156        self.size = TextureSize::Expand;
157        self
158    }
159
160    /// Clip the box to corners of `radius`; `0.0` (the default) is square.
161    #[must_use]
162    pub fn corner_radius(mut self, radius: f64) -> Self {
163        self.corner_radius = radius.max(0.0);
164        self
165    }
166
167    /// An accessible label, contributing a [`Role::Image`] semantics node; an
168    /// unlabelled texture view is decorative and reports nothing.
169    #[must_use]
170    pub fn label(mut self, label: impl Into<String>) -> Self {
171        self.label = Some(label.into());
172        self
173    }
174}
175
176impl<State: 'static> View<State> for TextureView {
177    type Element = TextureWidget;
178
179    fn build(&self, _ctx: &mut BuildCtx<'_>) -> TextureWidget {
180        TextureWidget { view: self.clone() }
181    }
182
183    fn rebuild(
184        &self,
185        prev: &Self,
186        element: &mut TextureWidget,
187        _ctx: &mut BuildCtx<'_>,
188    ) -> ChangeFlags {
189        if prev == self {
190            return ChangeFlags::NONE;
191        }
192        let mut flags = ChangeFlags::PAINT;
193        if prev.size != self.size {
194            flags |= ChangeFlags::LAYOUT;
195        }
196        element.view = self.clone();
197        flags
198    }
199}
200
201/// The retained widget for a [`TextureView`].
202pub struct TextureWidget {
203    view: TextureView,
204}
205
206impl TextureWidget {
207    /// The scene texture id this widget draws.
208    #[must_use]
209    pub fn scene_texture_id(&self) -> u64 {
210        self.view.id
211    }
212}
213
214impl Widget for TextureWidget {
215    fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
216        match self.view.size {
217            TextureSize::Explicit(size) => bc.constrain(size),
218            TextureSize::Expand => bc.max(),
219        }
220    }
221
222    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
223        let origin = ctx.origin();
224        let size = ctx.size();
225        let natural = self.view.source_size.unwrap_or(size);
226        let local = texture_fit_rect(natural, size, self.view.fit);
227        let dest = local + origin.to_vec2();
228
229        let overflows =
230            local.x0 < 0.0 || local.y0 < 0.0 || local.x1 > size.width || local.y1 > size.height;
231        let clip = if self.view.corner_radius > 0.0 {
232            scene.push_clip_rounded(origin, size, self.view.corner_radius);
233            true
234        } else if overflows {
235            scene.push_clip(origin, size);
236            true
237        } else {
238            false
239        };
240        scene.draw_scene_texture(self.view.id, dest);
241        if clip {
242            scene.pop_clip();
243        }
244    }
245
246    fn semantics(&self, ctx: &mut SemanticsCtx) {
247        if let Some(label) = &self.view.label {
248            ctx.push_node(Role::Image, |node| {
249                node.set_label(label.as_str());
250            });
251        }
252    }
253}