Skip to main content

teksilo_core/off_thread/
live_image.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Live images in a tree: the attachment of a source to one widget, and the
5//! Signals through which its size and status reach layout.
6
7use std::cell::Cell;
8use std::rc::Rc;
9
10use teksilo_canvas::live_image::{LiveImageConsumer, LiveImageSource, LiveImageStatus};
11use teksilo_canvas::{ImageGeometry, Transform2D};
12
13use crate::signal::Signal;
14use crate::widget_id::WidgetId;
15
16/// The two Signals an attachment writes: a source's size and status as the
17/// window's last layout saw them. A widget showing a live image owns them
18/// from construction, so they exist before it mounts and survive a switch to
19/// another source.
20#[derive(Clone)]
21pub struct LiveImageSignals {
22    /// The size layout uses: the buffer's, else the size hint; `None` with
23    /// neither. Written only by the layout pre-pass and by an attach. Bind it
24    /// at `Relayout`.
25    pub frame_size: Signal<Option<(u32, u32)>>,
26    /// Written only by the layout pre-pass and by an attach. Bind it at
27    /// `RepaintOnly`, and at `AccessibilityOnly` where it changes what is
28    /// announced.
29    pub status: Signal<LiveImageStatus>,
30}
31
32impl LiveImageSignals {
33    /// Signals holding `source`'s current size and status.
34    pub fn new(source: &LiveImageSource) -> Self {
35        Self {
36            frame_size: Signal::new(source.size()),
37            status: Signal::new(source.status()),
38        }
39    }
40}
41
42impl Default for LiveImageSignals {
43    /// Signals for no source yet: no size, `Disconnected`. An attach writes
44    /// the source's.
45    fn default() -> Self {
46        Self {
47            frame_size: Signal::new(None),
48            status: Signal::new(LiveImageStatus::Disconnected),
49        }
50    }
51}
52
53impl std::fmt::Debug for LiveImageSignals {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.debug_struct("LiveImageSignals")
56            .field("frame_size", &self.frame_size.get())
57            .field("status", &self.status.get())
58            .finish()
59    }
60}
61
62/// The UI-thread side of one attachment of a source to a widget, in one
63/// window. `Clone` is an `Rc` clone; `!Send`. The widget's rebuild or
64/// destruction detaches it, as does the tree's drop.
65#[derive(Clone)]
66pub struct LiveImageAttachment {
67    pub(crate) entry: Rc<AttachmentEntry>,
68}
69
70pub(crate) struct AttachmentEntry {
71    pub(crate) consumer: LiveImageConsumer,
72    pub(crate) signals: LiveImageSignals,
73    pub(crate) widget: WidgetId,
74    geometry: Cell<Option<ImageGeometry>>,
75    /// The ancestors' transform the widget's placement snapped under.
76    snapped_under: Cell<Option<Transform2D>>,
77}
78
79impl AttachmentEntry {
80    pub(crate) fn new(
81        consumer: LiveImageConsumer,
82        signals: LiveImageSignals,
83        widget: WidgetId,
84    ) -> Self {
85        Self {
86            consumer,
87            signals,
88            widget,
89            geometry: Cell::new(None),
90            snapped_under: Cell::new(None),
91        }
92    }
93}
94
95impl AttachmentEntry {
96    pub(crate) fn geometry(&self) -> Option<ImageGeometry> {
97        self.geometry.get()
98    }
99
100    pub(crate) fn snapped_under(&self) -> Option<Transform2D> {
101        self.snapped_under.get()
102    }
103}
104
105impl LiveImageAttachment {
106    /// What a widget hands `Canvas::draw_live_image`.
107    pub fn consumer(&self) -> &LiveImageConsumer {
108        &self.entry.consumer
109    }
110
111    pub fn signals(&self) -> &LiveImageSignals {
112        &self.entry.signals
113    }
114
115    pub fn widget_id(&self) -> WidgetId {
116        self.entry.widget
117    }
118
119    /// Record the placement the widget paints from, in its `place_children`,
120    /// so automation and lookups through the tree see it.
121    pub fn set_geometry(&self, geometry: ImageGeometry) {
122        self.entry.geometry.set(Some(geometry));
123    }
124
125    /// The placement last recorded; `None` before the first layout.
126    pub fn geometry(&self) -> Option<ImageGeometry> {
127        self.entry.geometry()
128    }
129
130    /// Record, in `place_children`, the transform the placement was snapped
131    /// to the device-pixel grid under: the widget's effective transform
132    /// (`WidgetArena::effective_transform`), or `None` when the placement
133    /// does not snap. A transform scope changes without a layout pass; when
134    /// this transform changes in a way that moves the snap
135    /// (`ImageGeometry::snaps_under` either side), the tree lays the widget
136    /// out again, so it snaps anew.
137    pub fn set_snapped_under(&self, transform: Option<Transform2D>) {
138        self.entry.snapped_under.set(transform);
139    }
140
141    /// Whether it was released: its widget was rebuilt or destroyed, or its
142    /// tree dropped.
143    pub fn is_detached(&self) -> bool {
144        self.entry.consumer.is_detached()
145    }
146}
147
148impl std::fmt::Debug for LiveImageAttachment {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        f.debug_struct("LiveImageAttachment")
151            .field("widget", &self.entry.widget)
152            .field("consumer", &self.entry.consumer)
153            .finish()
154    }
155}