1use zng_ext_image::{IMAGES, ImageCacheMode, ImageOptions, ImageRenderArgs};
4use zng_wgt_stack::stack_nodes;
5
6use super::image_properties::{
7 IMAGE_ALIGN_VAR, IMAGE_AUTO_SCALE_VAR, IMAGE_CACHE_VAR, IMAGE_CROP_VAR, IMAGE_DOWNSCALE_VAR, IMAGE_ERROR_FN_VAR, IMAGE_FIT_VAR,
8 IMAGE_LIMITS_VAR, IMAGE_LOADING_FN_VAR, IMAGE_OFFSET_VAR, IMAGE_RENDERING_VAR, IMAGE_SCALE_VAR, ImageFit, ImgErrorArgs, ImgLoadingArgs,
9};
10use super::*;
11
12context_var! {
13 pub static CONTEXT_IMAGE_VAR: ImageEntry = no_context_image();
19
20 pub static CONTEXT_IMAGE_REDUCED_VAR: Option<ImageEntry> = None;
25}
26fn no_context_image() -> ImageEntry {
27 ImageEntry::new_error(Txt::from_static("no image source in context"))
28}
29
30pub fn image_source(child: impl IntoUiNode, source: impl IntoVar<ImageSource>) -> UiNode {
41 let source = source.into_var();
42 let ctx_img = var(ImageEntry::new_loading());
43 let child = with_context_var(child, CONTEXT_IMAGE_REDUCED_VAR, var(None));
44 let child = with_context_var(child, CONTEXT_IMAGE_VAR, ctx_img.read_only());
45 let mut img = var(ImageEntry::new_loading()).read_only();
46 let mut _ctx_binding = None;
47
48 match_node(child, move |child, op| match op {
49 UiNodeOp::Init => {
50 WIDGET
51 .sub_var(&source)
52 .sub_var(&IMAGE_CACHE_VAR)
53 .sub_var(&IMAGE_DOWNSCALE_VAR)
54 .sub_var(&IMAGE_ENTRIES_MODE_VAR);
55
56 let mode = if IMAGE_CACHE_VAR.get() {
57 ImageCacheMode::Cache
58 } else {
59 ImageCacheMode::Ignore
60 };
61
62 let mut source = source.get();
63 if let ImageSource::Render(_, args) = &mut source {
64 *args = Some(ImageRenderArgs::new(WINDOW.id()));
65 }
66 let opt = ImageOptions::new(mode, IMAGE_DOWNSCALE_VAR.get(), None, IMAGE_ENTRIES_MODE_VAR.get());
67 img = IMAGES.image(source, opt, IMAGE_LIMITS_VAR.get());
68
69 ctx_img.set_from(&img);
70 _ctx_binding = Some(img.bind(&ctx_img));
71 }
72 UiNodeOp::Deinit => {
73 child.deinit();
74
75 ctx_img.set(no_context_image());
76 img = var(no_context_image()).read_only();
77 _ctx_binding = None;
78 }
79 UiNodeOp::Update { .. } => {
80 if source.is_new() || IMAGE_DOWNSCALE_VAR.is_new() || IMAGE_ENTRIES_MODE_VAR.is_new() {
81 let mut source = source.get();
84
85 if let ImageSource::Render(_, args) = &mut source {
86 *args = Some(ImageRenderArgs::new(WINDOW.id()));
87 }
88
89 let mode = if IMAGE_CACHE_VAR.get() {
90 ImageCacheMode::Cache
91 } else {
92 ImageCacheMode::Ignore
93 };
94 let opt = ImageOptions::new(mode, IMAGE_DOWNSCALE_VAR.get(), None, IMAGE_ENTRIES_MODE_VAR.get());
95 img = IMAGES.image(source, opt, IMAGE_LIMITS_VAR.get());
96
97 ctx_img.set_from(&img);
98 _ctx_binding = Some(img.bind(&ctx_img));
99 } else if let Some(enabled) = IMAGE_CACHE_VAR.get_new() {
100 let is_cached = ctx_img.with(|img| IMAGES.is_cached(img));
102 if enabled != is_cached {
103 let source = source.get();
104 let mut opt = ImageOptions::new(ImageCacheMode::Cache, IMAGE_DOWNSCALE_VAR.get(), None, IMAGE_ENTRIES_MODE_VAR.get());
105
106 if is_cached {
107 img = const_var(ImageEntry::new_loading());
108 if let Some(h) = source.hash128(&opt) {
109 IMAGES.clean(h);
110 }
111 opt.cache_mode = ImageCacheMode::Ignore;
112 }
113 img = IMAGES.image(source, opt, IMAGE_LIMITS_VAR.get());
114
115 ctx_img.set_from(&img);
116 _ctx_binding = Some(img.bind(&ctx_img));
117 }
118 }
119 }
120 _ => {}
121 })
122}
123
124context_local! {
125 static IN_ERROR_VIEW: bool = false;
127 static IN_LOADING_VIEW: bool = false;
129}
130
131pub fn image_error_presenter(child: impl IntoUiNode) -> UiNode {
137 let view = CONTEXT_IMAGE_VAR
138 .map(|i| i.error().map(|e| ImgErrorArgs { error: e }))
139 .present_opt(IMAGE_ERROR_FN_VAR.map(|f| {
140 wgt_fn!(f, |e| {
141 if IN_ERROR_VIEW.get_clone() {
142 UiNode::nil()
143 } else {
144 with_context_local(f(e), &IN_ERROR_VIEW, true)
145 }
146 })
147 }));
148
149 stack_nodes(ui_vec![view, child], 1, |constraints, _, img_size| {
150 if img_size == PxSize::zero() {
151 constraints
152 } else {
153 PxConstraints2d::new_fill_size(img_size)
154 }
155 })
156}
157
158pub fn image_loading_presenter(child: impl IntoUiNode) -> UiNode {
166 let args = expr_var! {
167 let is_loading = match #{CONTEXT_IMAGE_REDUCED_VAR} {
168 Some(img) => img.is_loading(),
169 None => #{CONTEXT_IMAGE_VAR}.is_loading(),
170 };
171 if is_loading { Some(ImgLoadingArgs {}) } else { None }
172 };
173 let view = args.present_opt(IMAGE_LOADING_FN_VAR.map(|f| {
174 wgt_fn!(f, |a| {
175 if IN_LOADING_VIEW.get_clone() {
176 UiNode::nil()
177 } else {
178 with_context_local(f(a), &IN_LOADING_VIEW, true)
179 }
180 })
181 }));
182
183 stack_nodes(ui_vec![view, child], 1, |constraints, _, img_size| {
184 if img_size == PxSize::zero() {
185 constraints
186 } else {
187 PxConstraints2d::new_fill_size(img_size)
188 }
189 })
190}
191
192pub fn image_presenter() -> UiNode {
206 let mut img_size = PxSize::zero();
207 let mut render_clip = PxRect::zero();
208 let mut render_img_size = PxSize::zero();
209 let mut render_tile_size = PxSize::zero();
210 let mut render_tile_spacing = PxSize::zero();
211 let mut render_offset = PxVector::zero();
212 let spatial_id = SpatialFrameId::new_unique();
213 let mut reduced_img = None;
214
215 match_node_leaf(move |op| match op {
216 UiNodeOp::Init => {
217 WIDGET
218 .sub_var(&CONTEXT_IMAGE_VAR)
219 .sub_var_layout(&IMAGE_CROP_VAR)
220 .sub_var_layout(&IMAGE_AUTO_SCALE_VAR)
221 .sub_var_layout(&IMAGE_SCALE_VAR)
222 .sub_var_layout(&IMAGE_FIT_VAR)
223 .sub_var_layout(&IMAGE_ALIGN_VAR)
224 .sub_var_layout(&IMAGE_OFFSET_VAR)
225 .sub_var_layout(&IMAGE_REPEAT_VAR)
226 .sub_var_layout(&IMAGE_REPEAT_SPACING_VAR)
227 .sub_var_render(&IMAGE_RENDERING_VAR);
228
229 img_size = CONTEXT_IMAGE_VAR.with(ImageEntry::size);
230 }
231 UiNodeOp::Deinit => {
232 if reduced_img.take().is_some() {
233 CONTEXT_IMAGE_REDUCED_VAR.set(None);
234 }
235 }
236 UiNodeOp::Update { .. } => {
237 if let Some(img) = CONTEXT_IMAGE_VAR.get_new() {
238 let ig_size = img.size();
239 if img_size != ig_size {
240 img_size = ig_size;
241 if reduced_img.take().is_some() {
242 CONTEXT_IMAGE_REDUCED_VAR.set(None);
243 }
244 WIDGET.layout();
245 } else {
246 if img.has_entries() {
247 let r = img.best_reduce(render_tile_size);
249 let h = r.subscribe(UpdateOp::Render, WIDGET.id());
250 r.set_bind_map(&CONTEXT_IMAGE_REDUCED_VAR, |i| Some(i.clone())).perm();
251 reduced_img = Some((r, h));
252 } else if reduced_img.take().is_some() {
253 CONTEXT_IMAGE_REDUCED_VAR.set(None);
254 }
255 WIDGET.render();
256 }
257 }
258 }
259 UiNodeOp::Measure { desired_size, .. } => {
260 let metrics = LAYOUT.metrics();
263
264 let mut scale = IMAGE_SCALE_VAR.get();
265 match IMAGE_AUTO_SCALE_VAR.get() {
266 ImageAutoScale::Pixel => {}
267 ImageAutoScale::Factor => {
268 scale *= metrics.scale_factor();
269 }
270 ImageAutoScale::Density => {
271 let screen = metrics.screen_density();
272 let image = CONTEXT_IMAGE_VAR.with(ImageEntry::density).unwrap_or(PxDensity2d::splat(screen));
273 scale *= Factor2d::new(screen.ppcm() / image.width.ppcm(), screen.ppcm() / image.height.ppcm());
274 }
275 }
276
277 let img_rect = PxRect::from_size(img_size);
278 let crop = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(img_size), || {
279 let mut r = IMAGE_CROP_VAR.get();
280 r.replace_default(&img_rect.into());
281 r.layout()
282 });
283 let render_clip = img_rect.intersection(&crop).unwrap_or_default() * scale;
284
285 let min_size = metrics.constraints().clamp_size(render_clip.size);
286 let wgt_ratio = metrics.constraints().with_min_size(min_size).fill_ratio(render_clip.size);
287
288 *desired_size = metrics.constraints().inner().fill_size_or(wgt_ratio);
289 }
290 UiNodeOp::Layout { final_size, .. } => {
291 let metrics = LAYOUT.metrics();
295
296 let mut scale = IMAGE_SCALE_VAR.get();
297 match IMAGE_AUTO_SCALE_VAR.get() {
298 ImageAutoScale::Pixel => {}
299 ImageAutoScale::Factor => {
300 scale *= metrics.scale_factor();
301 }
302 ImageAutoScale::Density => {
303 let screen = metrics.screen_density();
304 let image = CONTEXT_IMAGE_VAR.with(ImageEntry::density).unwrap_or(PxDensity2d::splat(screen));
305 scale *= Factor2d::new(screen.ppcm() / image.width.ppcm(), screen.ppcm() / image.height.ppcm());
306 }
307 }
308
309 let mut r_img_size = img_size * scale;
311
312 let img_rect = PxRect::from_size(img_size);
314 let crop = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(img_size), || {
315 let mut r = IMAGE_CROP_VAR.get();
316 r.replace_default(&img_rect.into());
317 r.layout()
318 });
319 let mut r_clip = img_rect.intersection(&crop).unwrap_or_default() * scale;
320 let mut r_offset = -r_clip.origin.to_vector();
321
322 let mut align = IMAGE_ALIGN_VAR.get();
326
327 let constraints = metrics.constraints();
328 let min_size = constraints.clamp_size(r_clip.size);
329 let wgt_ratio = constraints.with_min_size(min_size).fill_ratio(r_clip.size);
330 let wgt_size = constraints.inner().fill_size_or(wgt_ratio);
331
332 let mut fit = IMAGE_FIT_VAR.get();
333 if let ImageFit::ScaleDown = fit {
334 if r_clip.size.width < wgt_size.width && r_clip.size.height < wgt_size.height {
335 fit = ImageFit::None;
336 } else {
337 fit = ImageFit::Contain;
338 }
339 }
340 match fit {
341 ImageFit::Fill => {
342 align = Align::FILL;
343 }
344 ImageFit::Contain => {
345 let container = wgt_size.to_f32();
346 let content = r_clip.size.to_f32();
347 let scale = (container.width / content.width).min(container.height / content.height).fct();
348 r_clip *= scale;
349 r_img_size *= scale;
350 r_offset *= scale;
351 }
352 ImageFit::Cover => {
353 let container = wgt_size.to_f32();
354 let content = r_clip.size.to_f32();
355 let scale = (container.width / content.width).max(container.height / content.height).fct();
356 r_clip *= scale;
357 r_img_size *= scale;
358 r_offset *= scale;
359 }
360 ImageFit::None => {}
361 ImageFit::ScaleDown => unreachable!(),
362 }
363
364 if align.is_fill_x() {
365 let factor = wgt_size.width.0 as f32 / r_clip.size.width.0 as f32;
366 r_clip.size.width = wgt_size.width;
367 r_clip.origin.x *= factor;
368 r_img_size.width *= factor;
369 r_offset.x = -r_clip.origin.x;
370 } else {
371 let diff = wgt_size.width - r_clip.size.width;
372 let offset = diff * align.x(metrics.direction());
373 r_offset.x += offset;
374 if diff < Px(0) {
375 r_clip.origin.x -= offset;
376 r_clip.size.width += diff;
377 }
378 }
379 if align.is_fill_y() {
380 let factor = wgt_size.height.0 as f32 / r_clip.size.height.0 as f32;
381 r_clip.size.height = wgt_size.height;
382 r_clip.origin.y *= factor;
383 r_img_size.height *= factor;
384 r_offset.y = -r_clip.origin.y;
385 } else {
386 let diff = wgt_size.height - r_clip.size.height;
387 let offset = diff * align.y();
388 r_offset.y += offset;
389 if diff < Px(0) {
390 r_clip.origin.y -= offset;
391 r_clip.size.height += diff;
392 }
393 }
394
395 let offset = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(wgt_size), || IMAGE_OFFSET_VAR.layout());
397 if offset != PxVector::zero() {
398 r_offset += offset;
399
400 let screen_clip = PxRect::new(-r_offset.to_point(), wgt_size);
401 r_clip.origin -= offset;
402 r_clip = r_clip.intersection(&screen_clip).unwrap_or_default();
403 }
404
405 let mut r_tile_size = r_img_size;
407 let mut r_tile_spacing = PxSize::zero();
408 if matches!(IMAGE_REPEAT_VAR.get(), ImageRepeat::Repeat) {
409 r_clip = PxRect::from_size(wgt_size);
410 r_tile_size = r_img_size;
411 r_img_size = wgt_size;
412 r_offset = PxVector::zero();
413
414 let leftover = tile_leftover(r_tile_size, wgt_size);
415 r_tile_spacing = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(r_tile_size), || {
416 LAYOUT.with_leftover(Some(leftover.width), Some(leftover.height), || IMAGE_REPEAT_SPACING_VAR.layout())
417 });
418 }
419
420 if render_tile_size != r_tile_size {
421 CONTEXT_IMAGE_VAR.with(|img| {
422 if img.has_entries() {
423 let r = img.best_reduce(r_tile_size);
424 let h = r.subscribe(UpdateOp::Render, WIDGET.id());
425 r.set_bind_map(&CONTEXT_IMAGE_REDUCED_VAR, |i| Some(i.clone())).perm();
426 reduced_img = Some((r, h));
427 }
428 });
429 }
430
431 if render_clip != r_clip
432 || render_img_size != r_img_size
433 || render_offset != r_offset
434 || render_tile_size != r_tile_size
435 || render_tile_spacing != r_tile_spacing
436 {
437 render_clip = r_clip;
438 render_img_size = r_img_size;
439 render_offset = r_offset;
440 render_tile_size = r_tile_size;
441 render_tile_spacing = r_tile_spacing;
442 WIDGET.render();
443 }
444
445 *final_size = wgt_size;
446 }
447 UiNodeOp::Render { frame } => {
448 if render_clip.is_empty() {
449 return;
450 }
451
452 let render = |img: &ImageEntry| {
453 if render_offset != PxVector::zero() {
454 let transform = PxTransform::from(render_offset);
455 frame.push_reference_frame(spatial_id.into(), FrameValue::Value(transform), true, false, |frame| {
456 frame.push_image(
457 render_clip,
458 render_img_size,
459 render_tile_size,
460 render_tile_spacing,
461 img,
462 IMAGE_RENDERING_VAR.get(),
463 )
464 });
465 } else {
466 frame.push_image(
467 render_clip,
468 render_img_size,
469 render_tile_size,
470 render_tile_spacing,
471 img,
472 IMAGE_RENDERING_VAR.get(),
473 );
474 }
475 };
476
477 if let Some((img, _)) = &reduced_img {
478 img.with(render);
479 } else {
480 CONTEXT_IMAGE_VAR.with(render);
481 }
482 }
483 _ => {}
484 })
485}
486
487fn tile_leftover(tile_size: PxSize, wgt_size: PxSize) -> PxSize {
488 if tile_size.is_empty() || wgt_size.is_empty() {
489 return PxSize::zero();
490 }
491
492 let full_leftover_x = wgt_size.width % tile_size.width;
493 let full_leftover_y = wgt_size.height % tile_size.height;
494 let full_tiles_x = wgt_size.width / tile_size.width;
495 let full_tiles_y = wgt_size.height / tile_size.height;
496 let spaces_x = full_tiles_x - Px(1);
497 let spaces_y = full_tiles_y - Px(1);
498 PxSize::new(
499 if spaces_x > Px(0) { full_leftover_x / spaces_x } else { Px(0) },
500 if spaces_y > Px(0) { full_leftover_y / spaces_y } else { Px(0) },
501 )
502}