1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
15#![warn(unused_extern_crates)]
16#![warn(missing_docs)]
17
18use std::{
19 any::Any,
20 mem,
21 path::{Path, PathBuf},
22 pin::Pin,
23};
24
25use parking_lot::Mutex;
26use zng_app::{
27 static_id,
28 update::UPDATES,
29 view_process::{
30 VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImageHandle,
31 raw_events::{
32 RAW_FRAME_RENDERED_EVENT, RAW_HEADLESS_OPEN_EVENT, RAW_IMAGE_DECODE_ERROR_EVENT, RAW_IMAGE_DECODED_EVENT,
33 RAW_IMAGE_METADATA_DECODED_EVENT, RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT,
34 },
35 },
36 widget::{
37 WIDGET,
38 node::{IntoUiNode, UiNode, UiNodeOp, match_node},
39 },
40 window::{WINDOW, WindowId},
41};
42use zng_app_context::app_local;
43use zng_clone_move::clmv;
44use zng_layout::unit::{ByteLength, ByteUnits};
45use zng_state_map::StateId;
46use zng_task::channel::IpcBytes;
47use zng_txt::ToTxt;
48use zng_unique_id::{IdEntry, IdMap};
49use zng_var::{IntoVar, Var, VarHandle, var};
50use zng_view_api::{
51 image::{ImageDecoded, ImageRequest},
52 window::RenderMode,
53};
54
55mod types;
56pub use types::*;
57
58app_local! {
59 static IMAGES_SV: ImagesService = ImagesService::new();
60}
61
62struct ImagesService {
63 load_in_headless: Var<bool>,
64 limits: Var<ImageLimits>,
65
66 extensions: Vec<Box<dyn ImagesExtension>>,
67 render_windows: Option<Box<dyn ImageRenderWindowsService>>,
68
69 cache: IdMap<ImageHash, ImageVar>,
70}
71impl ImagesService {
72 pub fn new() -> Self {
73 Self {
74 load_in_headless: var(false),
75 limits: var(ImageLimits::default()),
76
77 extensions: vec![],
78 render_windows: None,
79
80 cache: IdMap::new(),
81 }
82 }
83
84 pub fn render_windows(&self) -> Box<dyn ImageRenderWindowsService> {
85 self.render_windows
86 .as_ref()
87 .expect("WINDOWS service not integrated with IMAGES service")
88 .clone_boxed()
89 }
90}
91
92pub struct IMAGES;
100impl IMAGES {
101 pub fn load_in_headless(&self) -> Var<bool> {
111 IMAGES_SV.read().load_in_headless.clone()
112 }
113
114 pub fn limits(&self) -> Var<ImageLimits> {
116 IMAGES_SV.read().limits.clone()
117 }
118
119 pub fn read(&self, path: impl Into<PathBuf>) -> ImageVar {
125 self.image_impl(path.into().into(), ImageOptions::cache(), None)
126 }
127
128 #[cfg(feature = "http")]
137 pub fn download<U>(&self, uri: U, accept: Option<zng_txt::Txt>) -> ImageVar
138 where
139 U: TryInto<zng_task::http::Uri>,
140 <U as TryInto<zng_task::http::Uri>>::Error: ToTxt,
141 {
142 match uri.try_into() {
143 Ok(uri) => self.image_impl(ImageSource::Download(uri, accept), ImageOptions::cache(), None),
144 Err(e) => {
145 let e = e.to_txt();
146 tracing::debug!("cannot convert into download URI, {e}");
147 zng_var::const_var(ImageEntry::new_error(e))
148 }
149 }
150 }
151
152 pub fn from_static(&self, data: &'static [u8], format: impl Into<ImageDataFormat>) -> ImageVar {
172 self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
173 }
174
175 pub fn from_data(&self, data: IpcBytes, format: impl Into<ImageDataFormat>) -> ImageVar {
183 self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
184 }
185
186 pub fn image(&self, source: impl Into<ImageSource>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
195 self.image_impl(source.into(), options, limits)
196 }
197 fn image_impl(&self, source: ImageSource, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
198 tracing::trace!("image request ({source:?}, {options:?}, {limits:?})");
199 let r = var(ImageEntry::new_loading());
200 let ri = r.read_only();
201 UPDATES.once_update("IMAGES.image", move || {
202 image(source, options, limits, r);
203 });
204 ri
205 }
206
207 pub fn image_task<F>(&self, source: impl IntoFuture<IntoFuture = F>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar
219 where
220 F: Future<Output = ImageSource> + Send + 'static,
221 {
222 self.image_task_impl(Box::pin(source.into_future()), options, limits)
223 }
224 fn image_task_impl(
225 &self,
226 source: Pin<Box<dyn Future<Output = ImageSource> + Send + 'static>>,
227 options: ImageOptions,
228 limits: Option<ImageLimits>,
229 ) -> ImageVar {
230 let r = var(ImageEntry::new_loading());
231 let ri = r.read_only();
232 zng_task::spawn(async move {
233 let source = source.await;
234 image(source, options, limits, r);
235 });
236 ri
237 }
238
239 pub fn register(&self, key: Option<ImageHash>, image: (ViewImageHandle, ImageDecoded)) -> ImageVar {
247 let r = var(ImageEntry::new_loading());
248 let rr = r.read_only();
249 UPDATES.once_update("IMAGES.register", move || {
250 image_view(key, image.0, image.1, None, r);
251 });
252 rr
253 }
254
255 pub fn clean(&self, key: ImageHash) {
260 UPDATES.once_update("IMAGES.clean", move || {
261 if let IdEntry::Occupied(e) = IMAGES_SV.write().cache.entry(key)
262 && e.get().strong_count() == 1
263 {
264 e.remove();
265 }
266 });
267 }
268
269 pub fn purge(&self, key: ImageHash) {
274 UPDATES.once_update("IMAGES.purge", move || {
275 IMAGES_SV.write().cache.remove(&key);
276 });
277 }
278
279 pub fn cache_key(&self, image: &ImageEntry) -> Option<ImageHash> {
281 let key = image.cache_key?;
282 if IMAGES_SV.read().cache.contains_key(&key) {
283 Some(key)
284 } else {
285 None
286 }
287 }
288
289 pub fn is_cached(&self, image: &ImageEntry) -> bool {
291 match &image.cache_key {
292 Some(k) => IMAGES_SV.read().cache.contains_key(k),
293 None => false,
294 }
295 }
296
297 pub fn clean_all(&self) {
299 UPDATES.once_update("IMAGES.clean_all", || {
300 IMAGES_SV.write().cache.retain(|_, v| v.strong_count() > 1);
301 });
302 }
303
304 pub fn purge_all(&self) {
309 UPDATES.once_update("IMAGES.purge_all", || {
310 IMAGES_SV.write().cache.clear();
311 });
312 }
313
314 pub fn extend(&self, extension: Box<dyn ImagesExtension>) {
318 UPDATES.once_update("IMAGES.extend", move || {
319 IMAGES_SV.write().extensions.push(extension);
320 });
321 }
322
323 pub fn available_formats(&self) -> Vec<ImageFormat> {
325 let mut formats = VIEW_PROCESS.info().image.clone();
326
327 let mut exts = mem::take(&mut IMAGES_SV.write().extensions);
328 for ext in exts.iter_mut() {
329 ext.available_formats(&mut formats);
330 }
331 let mut s = IMAGES_SV.write();
332 exts.append(&mut s.extensions);
333 s.extensions = exts;
334
335 formats
336 }
337
338 #[cfg(feature = "http")]
339 fn http_accept(&self) -> zng_txt::Txt {
340 let mut s = String::new();
341 let mut sep = "";
342 for f in self.available_formats() {
343 for f in f.media_type_suffixes_iter() {
344 s.push_str(sep);
345 s.push_str("image/");
346 s.push_str(f);
347 sep = ",";
348 }
349 }
350 s.into()
351 }
352}
353
354fn image(mut source: ImageSource, mut options: ImageOptions, limits: Option<ImageLimits>, r: Var<ImageEntry>) {
355 let mut s = IMAGES_SV.write();
356
357 let limits = limits.unwrap_or_else(|| s.limits.get());
358
359 let mut exts = mem::take(&mut s.extensions);
361 drop(s); if !exts.is_empty() {
363 tracing::trace!("process image with {} extensions", exts.len());
364 }
365 for ext in &mut exts {
366 ext.image(&limits, &mut source, &mut options);
367 }
368 let mut s = IMAGES_SV.write();
369 exts.append(&mut s.extensions);
370 s.extensions = exts;
371
372 if let ImageSource::Image(var) = source {
373 var.set_bind(&r).perm();
375 r.hold(var).perm();
376 return;
377 }
378
379 if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
380 tracing::debug!("ignoring image request due headless mode");
381 return;
382 }
383
384 let key = source.hash128(&options).unwrap();
385
386 match options.cache_mode {
388 ImageCacheMode::Ignore => (),
389 ImageCacheMode::Cache => {
390 match s.cache.entry(key) {
391 IdEntry::Occupied(e) => {
392 let var = e.get();
394 var.set_bind(&r).perm();
395 r.hold(var.clone()).perm();
396 return;
397 }
398 IdEntry::Vacant(e) => {
399 e.insert(r.clone());
401 }
402 }
403 }
404 ImageCacheMode::Retry => {
405 match s.cache.entry(key) {
406 IdEntry::Occupied(mut e) => {
407 let var = e.get();
408 if var.with(ImageEntry::is_error) {
409 r.set_bind(var).perm();
414 var.hold(r.clone()).perm();
415
416 e.insert(r.clone());
418 } else {
419 var.set_bind(&r).perm();
421 r.hold(var.clone()).perm();
422 return;
423 }
424 }
425 IdEntry::Vacant(e) => {
426 e.insert(r.clone());
428 }
429 }
430 }
431 ImageCacheMode::Reload => {
432 match s.cache.entry(key) {
433 IdEntry::Occupied(mut e) => {
434 let var = e.get();
435 r.set_bind(var).perm();
436 var.hold(r.clone()).perm();
437
438 e.insert(r.clone());
439 }
440 IdEntry::Vacant(e) => {
441 e.insert(r.clone());
443 }
444 }
445 }
446 }
447 drop(s);
448
449 match source {
450 ImageSource::Read(path) => {
451 fn read(path: &Path, limit: ByteLength) -> std::io::Result<IpcBytes> {
452 let file = std::fs::File::open(path)?;
453 if file.metadata()?.len() > limit.bytes() as u64 {
454 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
455 }
456 IpcBytes::from_file_blocking(file)
457 }
458 let limit = limits.max_encoded_len;
459 let data_format = match path.extension() {
460 Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
461 None => ImageDataFormat::Unknown,
462 };
463 zng_task::spawn_wait(move || match read(&path, limit) {
464 Ok(data) => {
465 tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", data.len().bytes());
466 image_data(false, Some(key), data_format, data, options, limits, r)
467 }
468 Err(e) => {
469 r.set(ImageEntry::new_error(e.to_txt()));
470 }
471 });
472 }
473 #[cfg(feature = "http")]
474 ImageSource::Download(uri, accept) => {
475 let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
476
477 use zng_task::http::*;
478 async fn download(uri: Uri, accept: zng_txt::Txt, limit: ByteLength) -> Result<(ImageDataFormat, IpcBytes), Error> {
479 let request = Request::get(uri)?.max_length(limit).header(header::ACCEPT, accept.as_str())?;
480 let mut response = send(request).await?;
481 let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
482 Some(m) => ImageDataFormat::MimeType(m.to_txt()),
483 None => ImageDataFormat::Unknown,
484 };
485 let data = response.body().await?;
486
487 Ok((data_format, data))
488 }
489
490 let limit = limits.max_encoded_len;
491 zng_task::spawn(async move {
492 match download(uri, accept, limit).await {
493 Ok((fmt, data)) => {
494 image_data(false, Some(key), fmt, data, options, limits, r);
495 }
496 Err(e) => r.set(ImageEntry::new_error(e.to_txt())),
497 }
498 });
499 }
500 ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data, options, limits, r),
501 ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
502 _ => unreachable!(),
503 }
504}
505
506fn image_data(
508 is_respawn: bool,
509 cache_key: Option<ImageHash>,
510 format: ImageDataFormat,
511 data: IpcBytes,
512 options: ImageOptions,
513 limits: ImageLimits,
514 r: Var<ImageEntry>,
515) {
516 if !is_respawn && let Some(key) = cache_key {
517 let mut replaced = false;
518 let mut exts = mem::take(&mut IMAGES_SV.write().extensions);
519 if !exts.is_empty() {
520 tracing::trace!("process image_data with {} extensions", exts.len());
521 }
522 for ext in &mut exts {
523 if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
524 replacement.set_bind(&r).perm();
525 r.hold(replacement).perm();
526
527 replaced = true;
528 break;
529 }
530 }
531 {
532 let mut s = IMAGES_SV.write();
533 exts.append(&mut s.extensions);
534 s.extensions = exts;
535
536 if replaced {
537 tracing::trace!("extension replaced image_data");
538 return;
539 }
540 }
541 }
542
543 if !VIEW_PROCESS.is_available() {
544 tracing::debug!("ignoring image view request after test load due to headless mode");
545 return;
546 }
547
548 let mut request = ImageRequest::new(
549 format.clone(),
550 data.clone(),
551 limits.max_decoded_len.bytes() as u64,
552 options.downscale.clone(),
553 options.mask,
554 );
555 request.entries = options.entries;
556
557 if is_respawn {
558 request.parent = r.with(|r| r.data.meta.parent.clone());
559 }
560
561 if VIEW_PROCESS.is_connected()
562 && let Ok(view_img) = VIEW_PROCESS.add_image(request)
563 {
564 image_view(
566 cache_key,
567 view_img,
568 ImageDecoded::default(),
569 Some((format, data, options, limits)),
570 r,
571 );
572 } else {
573 tracing::debug!("image view request failed, will retry on respawn");
574 let mut once = Some((format, data, options, limits, r));
575 VIEW_PROCESS_INITED_EVENT
576 .hook(move |_| {
577 let (format, data, options, limits, r) = once.take().unwrap();
578 image_data(true, cache_key, format, data, options, limits, r);
579 false
580 })
581 .perm();
582 }
583}
584fn image_view(
586 cache_key: Option<ImageHash>,
587 handle: ViewImageHandle,
588 decoded: ImageDecoded,
589 respawn_data: Option<(ImageDataFormat, IpcBytes, ImageOptions, ImageLimits)>,
590 r: Var<ImageEntry>,
591) {
592 let mut img = r.get();
594 img.cache_key = cache_key;
595 img.handle = handle;
596 img.data = decoded;
597
598 let is_loaded = img.is_loaded();
599 let is_dummy = img.view_handle().is_dummy();
600 r.set(img);
601
602 if is_loaded {
603 image_decoded(r);
604 return;
605 }
606
607 if is_dummy {
608 tracing::error!("tried to register dummy handle");
609 return;
610 }
611
612 let decoding_respawn_handle = if respawn_data.is_some() {
614 let r_weak = r.downgrade();
615 let mut respawn_data = respawn_data;
616 VIEW_PROCESS_INITED_EVENT.hook(move |_| {
617 if let Some(r) = r_weak.upgrade() {
618 let (format, data, options, limits) = respawn_data.take().unwrap();
619 image_data(true, cache_key, format, data, options, limits, r);
620 }
621 false
622 })
623 } else {
624 VarHandle::dummy()
626 };
627
628 let r_weak = r.downgrade();
630 let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
631 Some(r) => {
632 if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
633 tracing::debug!("image view error, {}", args.error);
634 r.set(ImageEntry::new_error(args.error.clone()));
635 false
636 } else {
637 r.with(ImageEntry::is_loading)
638 }
639 }
640 None => false,
641 });
642
643 let r_weak = r.downgrade();
645 let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
646 Some(r) => {
647 if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
648 let meta = args.meta.clone();
649 tracing::trace!("image view metadata decoded for request");
650 r.modify(move |i| i.data.meta = meta);
651 } else if let Some(p) = &args.meta.parent
652 && p.parent == r.with(|img| img.view_handle().image_id())
653 {
654 tracing::trace!("image view metadata decoded for entry of request");
656 let mut entry_d = ImageDecoded::default();
657 entry_d.meta = args.meta.clone();
658 let entry = var(ImageEntry::new(None, args.handle.upgrade().unwrap(), entry_d.clone()));
659 r.modify(clmv!(entry, |i| i.insert_entry(entry)));
660 image_view(None, args.handle.upgrade().unwrap(), entry_d, None, entry);
661 }
662 r.with(ImageEntry::is_loading)
663 }
664 None => false,
665 });
666
667 let r_weak = r.downgrade();
669 RAW_IMAGE_DECODED_EVENT
670 .hook(move |args| {
671 let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
672 match r_weak.upgrade() {
673 Some(r) => {
674 if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
675 let data = args.image.upgrade().unwrap();
676 let is_loading = data.partial.is_some();
677 tracing::trace!("image view decoded, partial={:?}", is_loading);
678 r.modify(move |i| i.data = (*data.0).clone());
679 if !is_loading {
680 image_decoded(r);
681 }
682 is_loading
683 } else {
684 r.with(ImageEntry::is_loading)
685 }
686 }
687 None => false,
688 }
689 })
690 .perm();
691}
692fn image_decoded(r: Var<ImageEntry>) {
694 let r_weak = r.downgrade();
695 VIEW_PROCESS_INITED_EVENT
696 .hook(move |_| {
697 if let Some(r) = r_weak.upgrade() {
698 let img = r.get();
699 if !img.is_loaded() {
700 return false;
702 }
703
704 let size = img.size();
706 let mut options = ImageOptions::none();
707 let format = match img.is_mask() {
708 true => {
709 options.mask = Some(ImageMaskMode::A);
710 ImageDataFormat::A8 { size }
711 }
712 false => ImageDataFormat::Bgra8 {
713 size,
714 density: img.density(),
715 original_color_type: img.original_color_type(),
716 },
717 };
718 image_data(
719 true,
720 img.cache_key,
721 format,
722 img.data.pixels.clone(),
723 options,
724 ImageLimits::none(),
725 r,
726 );
727 }
728 false
729 })
730 .perm();
731}
732
733fn image_render(
735 cache_key: Option<ImageHash>,
736 render_fn: crate::RenderFn,
737 args: Option<ImageRenderArgs>,
738 options: ImageOptions,
739 r: Var<ImageEntry>,
740) {
741 let s = IMAGES_SV.read();
742 let windows = s.render_windows();
743 let windows_ctx = windows.clone_boxed();
744 let mask = options.mask;
745 windows.open_headless_window(Box::new(move || {
746 let ctx = ImageRenderCtx::new();
747 let retain = ctx.retain.clone();
748 WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
749 let w = render_fn(&args.unwrap_or_default());
750 windows_ctx.enable_frame_capture_in_window_context(mask);
751 image_render_open(cache_key, WINDOW.id(), retain, r);
752 w
753 }));
754}
755
756fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
757 let r_weak = r.downgrade();
759 let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
760 if args.window_id == win_id {
761 if let Some(r) = r_weak.upgrade() {
762 r.set(ImageEntry::new_error(args.error.clone()));
763 }
764 false
765 } else {
766 true
767 }
768 });
769 RAW_HEADLESS_OPEN_EVENT
771 .hook(move |args| {
772 let _hold = &error_handle;
773 args.window_id != win_id
774 })
775 .perm();
776
777 let r_weak = r.downgrade();
779 RAW_FRAME_RENDERED_EVENT
780 .hook(move |args| {
781 if args.window_id == win_id {
782 if let Some(r) = r_weak.upgrade() {
783 match args.frame_image.clone() {
784 Some(h) => {
785 let h = h.upgrade().unwrap();
786 let handle = h.0.0.clone();
787 let data = h.1.clone();
788 let retain = retain.get();
789 r.set(ImageEntry::new(cache_key, handle, data));
790 if !retain {
791 IMAGES_SV.read().render_windows().close_window(win_id);
792 image_decoded(r);
794 }
795 retain
797 }
798 None => {
799 r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
800 false
801 }
802 }
803 } else {
804 false
805 }
806 } else {
807 true
808 }
809 })
810 .perm();
811}
812
813impl IMAGES {
814 pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
827 where
828 N: FnOnce() -> R + Send + Sync + 'static,
829 R: ImageRenderWindowRoot,
830 {
831 let render = Mutex::new(Some(render));
832 let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
833 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
834 self.image_impl(source, options, None)
835 }
836
837 pub fn render_node(
848 &self,
849 render_mode: RenderMode,
850 mask: Option<ImageMaskMode>,
851 render: impl FnOnce() -> UiNode + Send + Sync + 'static,
852 ) -> ImageVar {
853 let render = Mutex::new(Some(render));
854 let source = ImageSource::render_node(render_mode, move |_| {
855 render.lock().take().expect("IMAGES.render closure called more than once")()
856 });
857 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
858 self.image_impl(source, options, None)
859 }
860}
861
862#[expect(non_camel_case_types)]
864pub struct IMAGES_WINDOW;
865impl IMAGES_WINDOW {
866 pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
870 let mut img = IMAGES_SV.write();
871 img.render_windows = Some(service);
872 }
873}
874
875pub trait ImageRenderWindowsService: Send + Sync + 'static {
879 fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
881
882 fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
886
887 fn set_parent_in_window_context(&self, parent_id: WindowId);
891
892 fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
898
899 fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
903
904 fn close_window(&self, window_id: WindowId);
906}
907
908pub trait ImageRenderWindowRoot: Send + Any + 'static {}
912
913#[expect(non_camel_case_types)]
917pub struct IMAGE_RENDER;
918impl IMAGE_RENDER {
919 pub fn is_in_render(&self) -> bool {
923 WINDOW.contains_state(*IMAGE_RENDER_ID)
924 }
925
926 pub fn retain(&self) -> Var<bool> {
930 WINDOW.req_state(*IMAGE_RENDER_ID).retain
931 }
932}
933
934#[zng_app::widget::property(CONTEXT, default(false))]
942pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
943 let retain = retain.into_var();
944 match_node(child, move |_, op| {
945 if let UiNodeOp::Init = op {
946 if IMAGE_RENDER.is_in_render() {
947 let actual_retain = IMAGE_RENDER.retain();
948 actual_retain.set_from(&retain);
949 let handle = actual_retain.bind(&retain);
950 WIDGET.push_var_handle(handle);
951 } else {
952 tracing::error!("can only set `render_retain` in render widgets")
953 }
954 }
955 })
956}
957
958#[derive(Clone)]
959struct ImageRenderCtx {
960 retain: Var<bool>,
961}
962impl ImageRenderCtx {
963 fn new() -> Self {
964 Self { retain: var(false) }
965 }
966}
967
968static_id! {
969 static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
970}