Skip to main content

fui/node/
svg_node.rs

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