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::{any::Any, path::PathBuf, pin::Pin};
19
20use parking_lot::Mutex;
21use zng_app::{
22 static_id,
23 update::UPDATES,
24 view_process::{
25 VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImageHandle,
26 raw_events::{
27 RAW_FRAME_RENDERED_EVENT, RAW_HEADLESS_OPEN_EVENT, RAW_IMAGE_DECODE_ERROR_EVENT, RAW_IMAGE_DECODED_EVENT,
28 RAW_IMAGE_METADATA_DECODED_EVENT, RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT,
29 },
30 },
31 widget::{
32 WIDGET,
33 node::{IntoUiNode, UiNode, UiNodeOp, match_node},
34 },
35 window::{WINDOW, WindowId},
36};
37use zng_app_context::app_local;
38use zng_clone_move::clmv;
39use zng_layout::unit::{ByteLength, ByteUnits};
40use zng_state_map::StateId;
41use zng_task::channel::{IpcBytes, IpcReadHandle};
42use zng_txt::ToTxt;
43use zng_unique_id::{IdEntry, IdMap};
44use zng_var::{IntoVar, Var, VarHandle, var};
45use zng_view_api::{
46 image::{ImageDecoded, ImageRequest},
47 window::RenderMode,
48};
49
50mod types;
51pub use types::*;
52
53app_local! {
54 static IMAGES_SV: ImagesService = ImagesService::new();
55 static IMAGES_EXTENSIONS: Vec<Box<dyn ImagesExtension>> = vec![];
56}
57
58struct ImagesService {
59 load_in_headless: Var<bool>,
60 limits: Var<ImageLimits>,
61
62 render_windows: Option<Box<dyn ImageRenderWindowsService>>,
63
64 cache: IdMap<ImageHash, ImageVar>,
65}
66impl ImagesService {
67 pub fn new() -> Self {
68 Self {
69 load_in_headless: var(false),
70 limits: var(ImageLimits::default()),
71
72 render_windows: None,
73
74 cache: IdMap::new(),
75 }
76 }
77
78 pub fn render_windows(&self) -> Box<dyn ImageRenderWindowsService> {
79 self.render_windows
80 .as_ref()
81 .expect("WINDOWS service not integrated with IMAGES service")
82 .clone_boxed()
83 }
84}
85
86pub struct IMAGES;
94impl IMAGES {
95 pub fn load_in_headless(&self) -> Var<bool> {
105 IMAGES_SV.read().load_in_headless.clone()
106 }
107
108 pub fn limits(&self) -> Var<ImageLimits> {
110 IMAGES_SV.read().limits.clone()
111 }
112
113 pub fn read(&self, path: impl Into<PathBuf>) -> ImageVar {
119 self.image_impl(path.into().into(), ImageOptions::cache(), None)
120 }
121
122 #[cfg(feature = "http")]
131 pub fn download<U>(&self, uri: U, accept: Option<zng_txt::Txt>) -> ImageVar
132 where
133 U: TryInto<zng_task::http::Uri>,
134 <U as TryInto<zng_task::http::Uri>>::Error: ToTxt,
135 {
136 match uri.try_into() {
137 Ok(uri) => self.image_impl(ImageSource::Download(uri, accept), ImageOptions::cache(), None),
138 Err(e) => {
139 let e = e.to_txt();
140 tracing::debug!("cannot convert into download URI, {e}");
141 zng_var::const_var(ImageEntry::new_error(e))
142 }
143 }
144 }
145
146 pub fn from_static(&self, data: &'static [u8], format: impl Into<ImageDataFormat>) -> ImageVar {
166 self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
167 }
168
169 pub fn from_data(&self, data: IpcBytes, format: impl Into<ImageDataFormat>) -> ImageVar {
177 self.image_impl((data, format.into()).into(), ImageOptions::cache(), None)
178 }
179
180 pub fn image(&self, source: impl Into<ImageSource>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
189 self.image_impl(source.into(), options, limits)
190 }
191 fn image_impl(&self, source: ImageSource, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar {
192 tracing::trace!("image request ({source:?}, {options:?}, {limits:?})");
193 let r = var(ImageEntry::new_loading());
194 let ri = r.read_only();
195 UPDATES.once_update("IMAGES.image", move || {
196 image(source, options, limits, r);
197 });
198 ri
199 }
200
201 pub fn image_task<F>(&self, source: impl IntoFuture<IntoFuture = F>, options: ImageOptions, limits: Option<ImageLimits>) -> ImageVar
213 where
214 F: Future<Output = ImageSource> + Send + 'static,
215 {
216 self.image_task_impl(Box::pin(source.into_future()), options, limits)
217 }
218 fn image_task_impl(
219 &self,
220 source: Pin<Box<dyn Future<Output = ImageSource> + Send + 'static>>,
221 options: ImageOptions,
222 limits: Option<ImageLimits>,
223 ) -> ImageVar {
224 let r = var(ImageEntry::new_loading());
225 let ri = r.read_only();
226 zng_task::spawn(async move {
227 let source = source.await;
228 image(source, options, limits, r);
229 });
230 ri
231 }
232
233 pub fn register(&self, key: Option<ImageHash>, image: (ViewImageHandle, ImageDecoded)) -> ImageVar {
241 let r = var(ImageEntry::new_loading());
242 let rr = r.read_only();
243 UPDATES.once_update("IMAGES.register", move || {
244 image_view(key, image.0, image.1, None, r);
245 });
246 rr
247 }
248
249 pub fn clean(&self, key: ImageHash) {
254 UPDATES.once_update("IMAGES.clean", move || {
255 if let IdEntry::Occupied(e) = IMAGES_SV.write().cache.entry(key)
256 && e.get().strong_count() == 1
257 {
258 e.remove();
259 }
260 });
261 }
262
263 pub fn purge(&self, key: ImageHash) {
268 UPDATES.once_update("IMAGES.purge", move || {
269 IMAGES_SV.write().cache.remove(&key);
270 });
271 }
272
273 pub fn cache_key(&self, image: &ImageEntry) -> Option<ImageHash> {
275 let key = image.cache_key?;
276 if IMAGES_SV.read().cache.contains_key(&key) {
277 Some(key)
278 } else {
279 None
280 }
281 }
282
283 pub fn is_cached(&self, image: &ImageEntry) -> bool {
285 match &image.cache_key {
286 Some(k) => IMAGES_SV.read().cache.contains_key(k),
287 None => false,
288 }
289 }
290
291 pub fn clean_all(&self) {
293 UPDATES.once_update("IMAGES.clean_all", || {
294 IMAGES_SV.write().cache.retain(|_, v| v.strong_count() > 1);
295 });
296 }
297
298 pub fn purge_all(&self) {
303 UPDATES.once_update("IMAGES.purge_all", || {
304 IMAGES_SV.write().cache.clear();
305 });
306 }
307
308 pub fn extend(&self, extension: Box<dyn ImagesExtension>) {
312 UPDATES.once_update("IMAGES.extend", move || {
313 IMAGES_EXTENSIONS.write().push(extension);
314 });
315 }
316
317 pub fn available_formats(&self) -> Vec<ImageFormat> {
319 let mut formats = VIEW_PROCESS.info().image.clone();
320
321 for ext in IMAGES_EXTENSIONS.read().iter() {
322 ext.available_formats(&mut formats);
323 }
324
325 formats
326 }
327
328 #[cfg(feature = "http")]
329 fn http_accept(&self) -> zng_txt::Txt {
330 let mut s = String::new();
331 let mut sep = "";
332 for f in self.available_formats() {
333 for f in f.media_type_suffixes_iter() {
334 s.push_str(sep);
335 s.push_str("image/");
336 s.push_str(f);
337 sep = ",";
338 }
339 }
340 s.into()
341 }
342}
343
344fn image(mut source: ImageSource, mut options: ImageOptions, limits: Option<ImageLimits>, r: Var<ImageEntry>) {
345 let limits = limits.unwrap_or_else(|| IMAGES_SV.read().limits.get());
346
347 {
349 let mut exts = IMAGES_EXTENSIONS.write();
350 if !exts.is_empty() {
351 tracing::trace!("process image with {} extensions", exts.len());
352 }
353 for ext in exts.iter_mut() {
354 ext.image(&limits, &mut source, &mut options);
355 }
356 }
357
358 let mut s = IMAGES_SV.write();
360
361 if let ImageSource::Image(var) = source {
362 var.set_bind(&r).perm();
364 r.hold(var).perm();
365 return;
366 } else if let ImageSource::Entries { primary, entries } = source {
367 let entries: Vec<_> = entries
368 .into_iter()
369 .map(|(k, e)| (k, IMAGES.image(e, options.clone(), Some(limits.clone()))))
370 .collect();
371 let r_weak = r.downgrade();
372 let binding = move |mut primary: ImageEntry| -> bool {
373 let primary_id = primary.handle.image_id();
374 for (i, (kind, entry)) in entries.iter().enumerate() {
375 let kind = kind.clone();
376 primary.insert_entry(entry.map(move |e| {
377 let mut e = e.clone();
378 e.data.meta.parent = Some(zng_view_api::image::ImageEntryMetadata::new(primary_id, i, kind.clone()));
379 e
380 }));
381 }
382 if let Some(r) = r_weak.upgrade() {
383 r.set(primary);
384 true
385 } else {
386 false
387 }
388 };
389 let primary = IMAGES.image(*primary, options.clone(), Some(limits.clone()));
390 binding(primary.get());
391 primary.hook(move |a| binding(a.value().clone())).perm();
392 r.hold(primary).perm();
393 return;
394 }
395
396 if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
397 tracing::debug!("ignoring image request due headless mode");
398 return;
399 }
400
401 let key = source.hash128(&options).unwrap();
402
403 match options.cache_mode {
405 ImageCacheMode::Ignore => (),
406 ImageCacheMode::Cache => {
407 match s.cache.entry(key) {
408 IdEntry::Occupied(e) => {
409 let var = e.get();
411 var.set_bind(&r).perm();
412 r.hold(var.clone()).perm();
413 return;
414 }
415 IdEntry::Vacant(e) => {
416 e.insert(r.clone());
418 }
419 }
420 }
421 ImageCacheMode::Retry => {
422 match s.cache.entry(key) {
423 IdEntry::Occupied(mut e) => {
424 let var = e.get();
425 if var.with(ImageEntry::is_error) {
426 r.set_bind(var).perm();
431 var.hold(r.clone()).perm();
432
433 e.insert(r.clone());
435 } else {
436 var.set_bind(&r).perm();
438 r.hold(var.clone()).perm();
439 return;
440 }
441 }
442 IdEntry::Vacant(e) => {
443 e.insert(r.clone());
445 }
446 }
447 }
448 ImageCacheMode::Reload => {
449 match s.cache.entry(key) {
450 IdEntry::Occupied(mut e) => {
451 let var = e.get();
452 r.set_bind(var).perm();
453 var.hold(r.clone()).perm();
454
455 e.insert(r.clone());
456 }
457 IdEntry::Vacant(e) => {
458 e.insert(r.clone());
460 }
461 }
462 }
463 }
464 drop(s);
465
466 match source {
467 ImageSource::Read(path) => {
468 fn read(path: &PathBuf, limit: (&ImageSourceFilter<PathBuf>, ByteLength)) -> std::io::Result<(IpcReadHandle, u64)> {
469 if !limit.0.allows(path) {
470 return Err(std::io::Error::new(
471 std::io::ErrorKind::PermissionDenied,
472 "file path no allowed by limit",
473 ));
474 }
475 let file = std::fs::File::open(path)?;
476 let len = file.metadata()?.len();
477 if len > limit.1.bytes() {
478 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
479 }
480 Ok((IpcReadHandle::best_read_blocking(file)?, len))
481 }
482 let data_format = match path.extension() {
483 Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
484 None => ImageDataFormat::Unknown,
485 };
486 zng_task::spawn_wait(move || match read(&path, (&limits.allow_path, limits.max_encoded_len)) {
487 Ok((data, len)) => {
488 tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", len.bytes());
489 image_data(false, Some(key), data_format, data, options, limits, r)
490 }
491 Err(e) => {
492 tracing::debug!("cannot read {path:?}, {e}");
493 r.set(ImageEntry::new_error(e.to_txt()));
494 }
495 });
496 }
497 #[cfg(feature = "http")]
498 ImageSource::Download(uri, accept) => {
499 let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
500
501 use zng_task::http::*;
502 async fn download(
503 uri: Uri,
504 accept: zng_txt::Txt,
505 limit: (ImageSourceFilter<Uri>, ByteLength),
506 ) -> Result<(ImageDataFormat, IpcBytes), Error> {
507 if !limit.0.allows(&uri) {
508 return Err(Box::new(std::io::Error::new(
509 std::io::ErrorKind::PermissionDenied,
510 "uri no allowed by limit",
511 )));
512 }
513
514 let request = Request::get(uri)?.max_length(limit.1).header(header::ACCEPT, accept.as_str())?;
515 let mut response = send(request).await?;
516 let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
517 Some(m) => ImageDataFormat::MimeType(m.to_txt()),
518 None => ImageDataFormat::Unknown,
519 };
520 let data = response.body().await?;
521
522 Ok((data_format, data))
523 }
524
525 zng_task::spawn(async move {
526 match download(uri.clone(), accept, (limits.allow_uri.clone(), limits.max_encoded_len)).await {
527 Ok((fmt, data)) => {
528 tracing::trace!("download {uri:?}, len: {:?}, fmt: {fmt:?}", (data.len() as u64).bytes());
529 image_data(false, Some(key), fmt, data.into(), options, limits, r);
530 }
531 Err(e) => {
532 tracing::debug!("cannot download {uri:?}, {e}");
533 r.set(ImageEntry::new_error(e.to_txt()));
534 }
535 }
536 });
537 }
538 ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data.into(), options, limits, r),
539 ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
540 _ => unreachable!(),
541 }
542}
543
544fn image_data(
546 is_respawn: bool,
547 cache_key: Option<ImageHash>,
548 format: ImageDataFormat,
549 data: IpcReadHandle,
550 options: ImageOptions,
551 limits: ImageLimits,
552 r: Var<ImageEntry>,
553) {
554 if !is_respawn && let Some(key) = cache_key {
555 let mut exts = IMAGES_EXTENSIONS.write();
556 if !exts.is_empty() {
557 tracing::trace!("process image_data with {} extensions", exts.len());
558 }
559 for ext in exts.iter_mut() {
560 if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
561 replacement.set_bind(&r).perm();
562 r.hold(replacement).perm();
563
564 tracing::trace!("extension replaced image_data");
565 return;
566 }
567 }
568 }
569
570 if !VIEW_PROCESS.is_available() {
571 tracing::debug!("ignoring image view request after test load due to headless mode");
572 return;
573 }
574
575 let mut data = data;
576 let data_clone = match data.duplicate_or_read_blocking() {
577 Ok(d) => d,
578 Err(e) => {
579 tracing::error!("image data lost, {e}");
580 return;
581 }
582 };
583 let data = data;
584
585 let mut request = ImageRequest::new(
586 format.clone(),
587 data_clone,
588 limits.max_decoded_len.bytes(),
589 options.downscale.clone(),
590 options.mask,
591 );
592 request.entries = options.entries;
593
594 if is_respawn {
595 request.parent = r.with(|r| r.data.meta.parent.clone());
596 }
597
598 if VIEW_PROCESS.is_connected()
599 && let Ok(view_img) = VIEW_PROCESS.add_image(request)
600 {
601 image_view(
603 cache_key,
604 view_img,
605 ImageDecoded::default(),
606 Some((format, data, options, limits)),
607 r,
608 );
609 } else {
610 tracing::debug!("image view request failed, will retry on respawn");
611 let mut once = Some((format, data, options, limits, r));
612 VIEW_PROCESS_INITED_EVENT
613 .hook(move |_| {
614 let (format, data, options, limits, r) = once.take().unwrap();
615 image_data(true, cache_key, format, data, options, limits, r);
616 false
617 })
618 .perm();
619 }
620}
621fn image_view(
623 cache_key: Option<ImageHash>,
624 handle: ViewImageHandle,
625 decoded: ImageDecoded,
626 respawn_data: Option<(ImageDataFormat, IpcReadHandle, ImageOptions, ImageLimits)>,
627 r: Var<ImageEntry>,
628) {
629 let mut img = r.get();
631 img.cache_key = cache_key;
632 img.handle = handle;
633 img.data = decoded;
634
635 let is_loaded = img.is_loaded();
636 let is_dummy = img.view_handle().is_dummy();
637 r.set(img);
638
639 if is_loaded {
640 image_decoded(r);
641 return;
642 }
643
644 if is_dummy {
645 tracing::error!("tried to register dummy handle");
646 return;
647 }
648
649 let decoding_respawn_handle = if respawn_data.is_some() {
651 let r_weak = r.downgrade();
652 let mut respawn_data = respawn_data;
653 VIEW_PROCESS_INITED_EVENT.hook(move |_| {
654 if let Some(r) = r_weak.upgrade() {
655 let (format, data, options, limits) = respawn_data.take().unwrap();
656 image_data(true, cache_key, format, data, options, limits, r);
657 }
658 false
659 })
660 } else {
661 VarHandle::dummy()
663 };
664
665 let r_weak = r.downgrade();
667 let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
668 Some(r) => {
669 if let Some(handle) = args.handle.upgrade()
670 && r.with(|img| img.view_handle() == &handle)
671 {
672 tracing::debug!("image view error, {}", args.error);
673 r.set(ImageEntry::new_error(args.error.clone()));
674 false
675 } else {
676 r.with(ImageEntry::is_loading)
677 }
678 }
679 None => false,
680 });
681
682 let r_weak = r.downgrade();
684 let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
685 Some(r) => {
686 let handle = match args.handle.upgrade() {
687 Some(h) => h,
688 None => return r.with(ImageEntry::is_loading),
689 };
690 if r.with(|img| img.view_handle() == &handle) {
691 let meta = args.meta.clone();
692 tracing::trace!("image view metadata decoded for request");
693 r.modify(move |i| i.data.meta = meta);
694 } else if let Some(p) = &args.meta.parent
695 && p.parent == r.with(|img| img.view_handle().image_id())
696 {
697 tracing::trace!("image view metadata decoded for entry of request");
699 let mut entry_d = ImageDecoded::default();
700 entry_d.meta = args.meta.clone();
701 let entry = var(ImageEntry::new(None, handle.clone(), entry_d.clone()));
702 r.modify(clmv!(entry, |i| i.insert_entry(entry)));
703 image_view(None, handle, entry_d, None, entry);
704 }
705 r.with(ImageEntry::is_loading)
706 }
707 None => false,
708 });
709
710 let r_weak = r.downgrade();
712 RAW_IMAGE_DECODED_EVENT
713 .hook(move |args| {
714 let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
715 match r_weak.upgrade() {
716 Some(r) => {
717 if let Some(handle) = args.handle.upgrade()
718 && r.with(|img| img.view_handle() == &handle)
719 {
720 let data = args.image.upgrade().unwrap();
721 let is_loading = data.partial.is_some();
722 tracing::trace!("image view decoded, partial={:?}", is_loading);
723 r.modify(move |i| i.data = (*data.0).clone());
724 if !is_loading {
725 image_decoded(r);
726 }
727 is_loading
728 } else {
729 r.with(ImageEntry::is_loading)
730 }
731 }
732 None => false,
733 }
734 })
735 .perm();
736}
737fn image_decoded(r: Var<ImageEntry>) {
739 let r_weak = r.downgrade();
740 VIEW_PROCESS_INITED_EVENT
741 .hook(move |_| {
742 if let Some(r) = r_weak.upgrade() {
743 let img = r.get();
744 if !img.is_loaded() {
745 return false;
747 }
748
749 let size = img.size();
751 let mut options = ImageOptions::none();
752 let format = match img.is_mask() {
753 true => {
754 options.mask = Some(ImageMaskMode::A);
755 ImageDataFormat::A8 { size }
756 }
757 false => ImageDataFormat::Bgra8 {
758 size,
759 density: img.density(),
760 original_color_type: img.original_color_type(),
761 },
762 };
763 image_data(
764 true,
765 img.cache_key,
766 format,
767 img.data.pixels.clone().into(),
768 options,
769 ImageLimits::none(),
770 r,
771 );
772 }
773 false
774 })
775 .perm();
776}
777
778fn image_render(
780 cache_key: Option<ImageHash>,
781 render_fn: crate::RenderFn,
782 args: Option<ImageRenderArgs>,
783 options: ImageOptions,
784 r: Var<ImageEntry>,
785) {
786 let s = IMAGES_SV.read();
787 let windows = s.render_windows();
788 let windows_ctx = windows.clone_boxed();
789 let mask = options.mask;
790 windows.open_headless_window(Box::new(move || {
791 let ctx = ImageRenderCtx::new();
792 let retain = ctx.retain.clone();
793 WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
794 let w = render_fn(&args.unwrap_or_default());
795 windows_ctx.enable_frame_capture_in_window_context(mask);
796 image_render_open(cache_key, WINDOW.id(), retain, r);
797 w
798 }));
799}
800
801fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
802 let r_weak = r.downgrade();
804 let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
805 if args.window_id == win_id {
806 if let Some(r) = r_weak.upgrade() {
807 r.set(ImageEntry::new_error(args.error.clone()));
808 }
809 false
810 } else {
811 true
812 }
813 });
814 RAW_HEADLESS_OPEN_EVENT
816 .hook(move |args| {
817 let _hold = &error_handle;
818 args.window_id != win_id
819 })
820 .perm();
821
822 let r_weak = r.downgrade();
824 RAW_FRAME_RENDERED_EVENT
825 .hook(move |args| {
826 if args.window_id == win_id {
827 if let Some(r) = r_weak.upgrade() {
828 match args.frame_image.clone() {
829 Some(h) => {
830 let h = h.upgrade().unwrap();
831 let handle = h.0.0.clone();
832 let data = h.1.clone();
833 let retain = retain.get();
834 r.set(ImageEntry::new(cache_key, handle, data));
835 if !retain {
836 IMAGES_SV.read().render_windows().close_window(win_id);
837 image_decoded(r);
839 }
840 retain
842 }
843 None => {
844 r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
845 false
846 }
847 }
848 } else {
849 false
850 }
851 } else {
852 true
853 }
854 })
855 .perm();
856}
857
858impl IMAGES {
859 pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
872 where
873 N: FnOnce() -> R + Send + Sync + 'static,
874 R: ImageRenderWindowRoot,
875 {
876 let render = Mutex::new(Some(render));
877 let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
878 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
879 self.image_impl(source, options, None)
880 }
881
882 pub fn render_node(
893 &self,
894 render_mode: RenderMode,
895 mask: Option<ImageMaskMode>,
896 render: impl FnOnce() -> UiNode + Send + Sync + 'static,
897 ) -> ImageVar {
898 let render = Mutex::new(Some(render));
899 let source = ImageSource::render_node(render_mode, move |_| {
900 render.lock().take().expect("IMAGES.render closure called more than once")()
901 });
902 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
903 self.image_impl(source, options, None)
904 }
905}
906
907#[expect(non_camel_case_types)]
909pub struct IMAGES_WINDOW;
910impl IMAGES_WINDOW {
911 pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
915 let mut img = IMAGES_SV.write();
916 img.render_windows = Some(service);
917 }
918}
919
920pub trait ImageRenderWindowsService: Send + Sync + 'static {
924 fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
926
927 fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
931
932 fn set_parent_in_window_context(&self, parent_id: WindowId);
936
937 fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
943
944 fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
948
949 fn close_window(&self, window_id: WindowId);
951}
952
953pub trait ImageRenderWindowRoot: Send + Any + 'static {}
957
958#[expect(non_camel_case_types)]
962pub struct IMAGE_RENDER;
963impl IMAGE_RENDER {
964 pub fn is_in_render(&self) -> bool {
968 WINDOW.contains_state(*IMAGE_RENDER_ID)
969 }
970
971 pub fn retain(&self) -> Var<bool> {
975 WINDOW.req_state(*IMAGE_RENDER_ID).retain
976 }
977}
978
979#[zng_app::widget::property(CONTEXT, default(false))]
987pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
988 let retain = retain.into_var();
989 match_node(child, move |_, op| {
990 if let UiNodeOp::Init = op {
991 if IMAGE_RENDER.is_in_render() {
992 let actual_retain = IMAGE_RENDER.retain();
993 actual_retain.set_from(&retain);
994 let handle = actual_retain.bind(&retain);
995 WIDGET.push_var_handle(handle);
996 } else {
997 tracing::error!("can only set `render_retain` in render widgets")
998 }
999 }
1000 })
1001}
1002
1003#[derive(Clone)]
1004struct ImageRenderCtx {
1005 retain: Var<bool>,
1006}
1007impl ImageRenderCtx {
1008 fn new() -> Self {
1009 Self { retain: var(false) }
1010 }
1011}
1012
1013static_id! {
1014 static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
1015}