Skip to main content

fui/node/
image.rs

1use super::core::*;
2use super::*;
3use crate::assets::{
4    acquire_texture_asset, get_texture_asset_error, get_texture_asset_height,
5    get_texture_asset_state, get_texture_asset_url, get_texture_asset_width, release_texture_asset,
6    AssetLoadState,
7};
8use crate::image_sampling::ImageSampling;
9use crate::signal::{Callback, SubscriptionGuard};
10use std::rc::Weak;
11
12struct ImageState {
13    source_url: String,
14    owned_texture_asset_id: u32,
15    requested_width_value: f32,
16    requested_width_unit: Unit,
17    has_requested_width: bool,
18    requested_height_value: f32,
19    requested_height_unit: Unit,
20    has_requested_height: bool,
21    tracked_texture_asset_id: u32,
22    asset_state_subscription: Option<SubscriptionGuard>,
23}
24
25impl Default for ImageState {
26    fn default() -> Self {
27        Self {
28            source_url: String::new(),
29            owned_texture_asset_id: 0,
30            requested_width_value: 0.0,
31            requested_width_unit: Unit::Auto,
32            has_requested_width: false,
33            requested_height_value: 0.0,
34            requested_height_unit: Unit::Auto,
35            has_requested_height: false,
36            tracked_texture_asset_id: 0,
37            asset_state_subscription: None,
38        }
39    }
40}
41
42#[derive(Clone)]
43pub struct ImageNode {
44    base: FlexBox,
45    props: Rc<RefCell<ImageProps>>,
46    state: Rc<RefCell<ImageState>>,
47}
48
49impl ImageNode {
50    pub fn new(texture_id: u32) -> Self {
51        let base = FlexBox::default();
52        base.core.borrow_mut().kind = NodeKind::Image;
53        let node = Self {
54            base,
55            props: Rc::new(RefCell::new(ImageProps {
56                texture_id,
57                source_url: None,
58                object_fit: ObjectFit::Fill,
59                sampling_kind: ImageSamplingKind::Linear,
60                max_aniso: 0,
61                image_nine: None,
62            })),
63            state: Rc::new(RefCell::new(ImageState {
64                requested_width_value: 0.0,
65                requested_width_unit: Unit::Auto,
66                has_requested_width: true,
67                requested_height_value: 0.0,
68                requested_height_unit: Unit::Auto,
69                has_requested_height: true,
70                ..ImageState::default()
71            })),
72        };
73        node.attach_asset_state_listener();
74        node
75    }
76
77    pub fn width(&self, width: f32, unit: Unit) -> &Self {
78        {
79            let mut state = self.state.borrow_mut();
80            state.requested_width_value = width;
81            state.requested_width_unit = unit;
82            state.has_requested_width = true;
83        }
84        self.apply_resolved_sizing();
85        self
86    }
87
88    pub fn height(&self, height: f32, unit: Unit) -> &Self {
89        {
90            let mut state = self.state.borrow_mut();
91            state.requested_height_value = height;
92            state.requested_height_unit = unit;
93            state.has_requested_height = true;
94        }
95        self.apply_resolved_sizing();
96        self
97    }
98
99    pub fn texture(&self, texture_id: u32) -> &Self {
100        self.release_owned_source_asset();
101        self.state.borrow_mut().source_url.clear();
102        self.props.borrow_mut().texture_id = texture_id;
103        self.props.borrow_mut().source_url = None;
104        self.retained_node_ref().set_image_url_for_routing(None);
105        self.attach_asset_state_listener();
106        self.apply_resolved_sizing();
107        if self.has_built_handle() {
108            self.apply_image_source();
109            self.notify_retained_mutation();
110        }
111        self
112    }
113
114    pub fn source(&self, url: impl Into<String>) -> &Self {
115        let url = url.into();
116        if url.is_empty() {
117            return self.clear_source();
118        }
119        if self.state.borrow().owned_texture_asset_id != 0 && self.state.borrow().source_url == url
120        {
121            return self;
122        }
123        self.release_owned_source_asset();
124        {
125            let mut state = self.state.borrow_mut();
126            state.source_url = url.clone();
127        }
128        let texture_id = acquire_texture_asset(&url);
129        {
130            let mut props = self.props.borrow_mut();
131            props.texture_id = texture_id;
132            props.source_url = Some(url.clone());
133        }
134        self.state.borrow_mut().owned_texture_asset_id = texture_id;
135        self.retained_node_ref()
136            .set_image_url_for_routing(Some(url.clone()));
137        self.attach_asset_state_listener();
138        self.apply_resolved_sizing();
139        if self.has_built_handle() {
140            self.apply_image_source();
141            self.notify_retained_mutation();
142        }
143        self
144    }
145
146    pub fn clear_source(&self) -> &Self {
147        self.release_owned_source_asset();
148        {
149            let mut state = self.state.borrow_mut();
150            state.source_url.clear();
151        }
152        {
153            let mut props = self.props.borrow_mut();
154            props.texture_id = 0;
155            props.source_url = None;
156            props.image_nine = None;
157        }
158        self.retained_node_ref().set_image_url_for_routing(None);
159        self.attach_asset_state_listener();
160        self.apply_resolved_sizing();
161        if self.has_built_handle() {
162            self.apply_image_source();
163            self.notify_retained_mutation();
164        }
165        self
166    }
167
168    pub fn alt_text(&self, value: impl Into<String>) -> &Self {
169        let value = value.into();
170        self.semantic_role(SemanticRole::Image);
171        self.semantic_label(value);
172        self
173    }
174
175    pub fn object_fit(&self, object_fit: ObjectFit) -> &Self {
176        self.props.borrow_mut().object_fit = object_fit;
177        if self.has_built_handle() {
178            self.apply_image_source();
179            self.notify_retained_mutation();
180        }
181        self
182    }
183
184    pub fn sampling(&self, sampling: ImageSampling) -> &Self {
185        let mut props = self.props.borrow_mut();
186        props.sampling_kind = sampling.ffi_kind();
187        props.max_aniso = sampling.max_aniso();
188        drop(props);
189        if self.has_built_handle() {
190            self.apply_image_source();
191            self.notify_retained_mutation();
192        }
193        self
194    }
195
196    pub fn image_nine(
197        &self,
198        inset_left: f32,
199        inset_top: f32,
200        inset_right: f32,
201        inset_bottom: f32,
202    ) -> &Self {
203        self.props.borrow_mut().image_nine =
204            Some((inset_left, inset_top, inset_right, inset_bottom));
205        if self.has_built_handle() {
206            self.apply_image_source();
207            self.notify_retained_mutation();
208        }
209        self
210    }
211
212    pub fn clear_image_nine(&self) -> &Self {
213        self.props.borrow_mut().image_nine = None;
214        if self.has_built_handle() {
215            self.apply_image_source();
216            self.notify_retained_mutation();
217        }
218        self
219    }
220
221    pub fn asset_state_signal(&self) -> crate::assets::AssetStateSignal {
222        get_texture_asset_state(self.props.borrow().texture_id)
223    }
224
225    pub fn asset_state(&self) -> AssetLoadState {
226        self.asset_state_signal().get()
227    }
228
229    pub fn asset_error(&self) -> String {
230        get_texture_asset_error(self.props.borrow().texture_id)
231    }
232
233    pub fn asset_url(&self) -> String {
234        let source_url = self.state.borrow().source_url.clone();
235        if source_url.is_empty() {
236            get_texture_asset_url(self.props.borrow().texture_id)
237        } else {
238            source_url
239        }
240    }
241
242    pub fn asset_width(&self) -> f32 {
243        get_texture_asset_width(self.props.borrow().texture_id)
244    }
245
246    pub fn asset_height(&self) -> f32 {
247        get_texture_asset_height(self.props.borrow().texture_id)
248    }
249
250    fn apply_image_source(&self) {
251        let props = self.props.borrow();
252        if let Some((left, top, right, bottom)) = props.image_nine {
253            ui::set_image_nine(
254                self.handle().raw(),
255                props.texture_id,
256                left,
257                top,
258                right,
259                bottom,
260                props.sampling_kind,
261                props.max_aniso,
262            );
263        } else {
264            ui::set_image(
265                self.handle().raw(),
266                props.texture_id,
267                props.object_fit as u32,
268                props.sampling_kind,
269                props.max_aniso,
270            );
271        }
272    }
273
274    fn apply_resolved_sizing(&self) {
275        let (
276            has_requested_width,
277            requested_width_value,
278            requested_width_unit,
279            has_requested_height,
280            requested_height_value,
281            requested_height_unit,
282        ) = {
283            let state = self.state.borrow();
284            (
285                state.has_requested_width,
286                state.requested_width_value,
287                state.requested_width_unit,
288                state.has_requested_height,
289                state.requested_height_value,
290                state.requested_height_unit,
291            )
292        };
293        if !has_requested_width && !has_requested_height {
294            return;
295        }
296
297        let asset_width = self.asset_width();
298        let asset_height = self.asset_height();
299        let has_intrinsic_size = asset_width > 0.0 && asset_height > 0.0;
300        if has_requested_width {
301            let mut resolved_width_value = requested_width_value;
302            let mut resolved_width_unit = requested_width_unit;
303            if requested_width_unit == Unit::Auto && has_intrinsic_size {
304                if has_requested_height && requested_height_unit == Unit::Pixel {
305                    resolved_width_value = requested_height_value * (asset_width / asset_height);
306                } else {
307                    resolved_width_value = asset_width;
308                }
309                resolved_width_unit = Unit::Pixel;
310            }
311            self.base.width(resolved_width_value, resolved_width_unit);
312        }
313
314        if has_requested_height {
315            let mut resolved_height_value = requested_height_value;
316            let mut resolved_height_unit = requested_height_unit;
317            if requested_height_unit == Unit::Auto && has_intrinsic_size {
318                if has_requested_width && requested_width_unit == Unit::Pixel {
319                    resolved_height_value = requested_width_value * (asset_height / asset_width);
320                } else {
321                    resolved_height_value = asset_height;
322                }
323                resolved_height_unit = Unit::Pixel;
324            }
325            self.base
326                .height(resolved_height_value, resolved_height_unit);
327        }
328    }
329
330    fn attach_asset_state_listener(&self) {
331        let texture_id = self.props.borrow().texture_id;
332        {
333            let mut state = self.state.borrow_mut();
334            if state.tracked_texture_asset_id == texture_id {
335                return;
336            }
337            state.asset_state_subscription = None;
338            state.tracked_texture_asset_id = texture_id;
339            if texture_id == 0 {
340                return;
341            }
342        }
343
344        let base_weak = self.base.downgrade();
345        let props_weak: Weak<RefCell<ImageProps>> = Rc::downgrade(&self.props);
346        let state_weak: Weak<RefCell<ImageState>> = Rc::downgrade(&self.state);
347        let callback: Callback = Rc::new(move || {
348            if let (Some(base), Some(props), Some(state)) = (
349                base_weak.upgrade(),
350                props_weak.upgrade(),
351                state_weak.upgrade(),
352            ) {
353                let node = ImageNode { base, props, state };
354                node.apply_resolved_sizing();
355            }
356        });
357
358        let guard = get_texture_asset_state(texture_id).subscribe(callback);
359        self.state.borrow_mut().asset_state_subscription = Some(guard);
360    }
361
362    fn release_owned_source_asset(&self) {
363        let owned_texture_asset_id = self.state.borrow().owned_texture_asset_id;
364        if owned_texture_asset_id == 0 {
365            return;
366        }
367        release_texture_asset(owned_texture_asset_id);
368        self.state.borrow_mut().owned_texture_asset_id = 0;
369    }
370
371    #[cfg(test)]
372    pub(crate) fn test_texture_id(&self) -> u32 {
373        self.props.borrow().texture_id
374    }
375}
376
377impl Node for ImageNode {
378    fn retained_node_ref(&self) -> NodeRef {
379        NodeRef::from_node(self.base.core.clone(), self.clone())
380    }
381
382    fn build_self(&self) {
383        self.apply_resolved_sizing();
384        self.base.build_self();
385        apply_image_props(self.handle(), &self.props.borrow());
386    }
387}
388
389impl HasFlexBoxRoot for ImageNode {
390    fn flex_box_root(&self) -> &FlexBox {
391        &self.base
392    }
393
394    fn set_flex_box_surface_width(&self, value: f32, unit: Unit) {
395        self.width(value, unit);
396    }
397
398    fn set_flex_box_surface_height(&self, value: f32, unit: Unit) {
399        self.height(value, unit);
400    }
401}
402
403impl ThemeBindable for ImageNode {
404    fn theme_binding_node(&self) -> NodeRef {
405        self.retained_node_ref()
406    }
407
408    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
409        let base = self.base.downgrade();
410        let props = Rc::downgrade(&self.props);
411        let state = Rc::downgrade(&self.state);
412        Box::new(move || {
413            Some(Self {
414                base: base.upgrade()?,
415                props: props.upgrade()?,
416                state: state.upgrade()?,
417            })
418        })
419    }
420}
421
422impl Drop for ImageNode {
423    fn drop(&mut self) {
424        if Rc::strong_count(&self.state) == 1 {
425            self.state.borrow_mut().asset_state_subscription = None;
426            let owned_texture_asset_id = self.state.borrow().owned_texture_asset_id;
427            if owned_texture_asset_id != 0 {
428                release_texture_asset(owned_texture_asset_id);
429                self.state.borrow_mut().owned_texture_asset_id = 0;
430            }
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::assets::{self, AssetLoadState};
439    use crate::bridge_callbacks;
440    use crate::ffi::{self, Call};
441    use crate::Application;
442
443    #[test]
444    fn source_url_reuses_registry_asset_and_resizes_after_ready() {
445        assets::test_reset();
446        ffi::test::reset();
447
448        let image = ImageNode::new(0);
449        image.source("/img/sample.png");
450        let texture_id = image.test_texture_id();
451        assert!(texture_id >= 0x2000_0000);
452        assert_eq!(image.asset_state(), AssetLoadState::Loading);
453
454        Application::mount(image.clone());
455        ffi::test::take_calls();
456
457        bridge_callbacks::__fui_on_texture_loaded(texture_id, 80.0, 40.0);
458        let calls = ffi::test::take_calls();
459        assert!(calls.iter().any(|call| matches!(
460            call,
461            Call::SetWidth { value, unit_enum, .. } if *value == 80.0 && *unit_enum == Unit::Pixel as u32
462        )));
463        assert!(calls.iter().any(|call| matches!(
464            call,
465            Call::SetHeight { value, unit_enum, .. } if *value == 40.0 && *unit_enum == Unit::Pixel as u32
466        )));
467        assert_eq!(image.asset_width(), 80.0);
468        assert_eq!(image.asset_height(), 40.0);
469        assert_eq!(image.asset_state(), AssetLoadState::Ready);
470    }
471
472    #[test]
473    fn explicit_texture_id_attaches_listener_and_resizes_after_ready() {
474        assets::test_reset();
475        ffi::test::reset();
476
477        let image = ImageNode::new(77);
478        assert_eq!(image.asset_state(), AssetLoadState::Idle);
479
480        Application::mount(image.clone());
481        ffi::test::take_calls();
482
483        bridge_callbacks::__fui_on_texture_loaded(77, 96.0, 48.0);
484        let calls = ffi::test::take_calls();
485        assert!(calls.iter().any(|call| matches!(
486            call,
487            Call::SetWidth { value, unit_enum, .. } if *value == 96.0 && *unit_enum == Unit::Pixel as u32
488        )));
489        assert!(calls.iter().any(|call| matches!(
490            call,
491            Call::SetHeight { value, unit_enum, .. } if *value == 48.0 && *unit_enum == Unit::Pixel as u32
492        )));
493        assert_eq!(image.asset_width(), 96.0);
494        assert_eq!(image.asset_height(), 48.0);
495        assert_eq!(image.asset_state(), AssetLoadState::Ready);
496    }
497}