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}