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}