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 zng_app::{
21 static_id,
22 update::UPDATES,
23 view_process::{
24 VIEW_PROCESS, VIEW_PROCESS_INITED_EVENT, ViewImageHandle,
25 raw_events::{
26 RAW_FRAME_RENDERED_EVENT, RAW_HEADLESS_OPEN_EVENT, RAW_IMAGE_DECODE_ERROR_EVENT, RAW_IMAGE_DECODED_EVENT,
27 RAW_IMAGE_METADATA_DECODED_EVENT, RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT,
28 },
29 },
30 widget::{
31 WIDGET,
32 node::{IntoUiNode, UiNode, UiNodeOp, match_node},
33 },
34 window::{WINDOW, WindowId},
35};
36use zng_app_context::app_local;
37use zng_clone_move::clmv;
38use zng_layout::unit::{ByteLength, ByteUnits};
39use zng_state_map::StateId;
40use zng_task::channel::{IpcBytes, IpcReadHandle};
41use zng_task::parking_lot::Mutex;
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> {
323 let mut formats = VIEW_PROCESS.info().image.clone();
324
325 for ext in IMAGES_EXTENSIONS.read().iter() {
326 ext.available_formats(&mut formats);
327 }
328
329 formats
330 }
331
332 #[cfg(feature = "http")]
333 fn http_accept(&self) -> zng_txt::Txt {
334 let mut s = String::new();
335 let mut sep = "";
336 for f in self.available_formats() {
337 for f in f.media_type_suffixes_iter() {
338 s.push_str(sep);
339 s.push_str("image/");
340 s.push_str(f);
341 sep = ",";
342 }
343 }
344 s.into()
345 }
346}
347
348fn image(mut source: ImageSource, mut options: ImageOptions, limits: Option<ImageLimits>, r: Var<ImageEntry>) {
349 let limits = limits.unwrap_or_else(|| IMAGES_SV.read().limits.get());
350
351 {
353 let mut exts = IMAGES_EXTENSIONS.write();
354 if !exts.is_empty() {
355 tracing::trace!("process image with {} extensions", exts.len());
356 }
357 for ext in exts.iter_mut() {
358 ext.image(&limits, &mut source, &mut options);
359 }
360 }
361
362 let mut s = IMAGES_SV.write();
364
365 if let ImageSource::Image(var) = source {
366 var.set_bind(&r).perm();
368 r.hold(var).perm();
369 return;
370 } else if let ImageSource::Entries { primary, entries } = source {
371 let entries: Vec<_> = entries
372 .into_iter()
373 .map(|(k, e)| (k, IMAGES.image(e, options.clone(), Some(limits.clone()))))
374 .collect();
375 let r_weak = r.downgrade();
376 let binding = move |mut primary: ImageEntry| -> bool {
377 let primary_id = primary.handle.image_id();
378 for (i, (kind, entry)) in entries.iter().enumerate() {
379 let kind = kind.clone();
380 primary.insert_entry(entry.map(move |e| {
381 let mut e = e.clone();
382 e.data.meta.parent = Some(zng_view_api::image::ImageEntryMetadata::new(primary_id, i, kind.clone()));
383 e
384 }));
385 }
386 if let Some(r) = r_weak.upgrade() {
387 r.set(primary);
388 true
389 } else {
390 false
391 }
392 };
393 let primary = IMAGES.image(*primary, options.clone(), Some(limits.clone()));
394 binding(primary.get());
395 primary.hook(move |a| binding(a.value().clone())).perm();
396 r.hold(primary).perm();
397 return;
398 }
399
400 if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
401 tracing::debug!("ignoring image request due headless mode");
402 return;
403 }
404
405 let key = source.hash128(&options).unwrap();
406
407 match options.cache_mode {
409 ImageCacheMode::Ignore => (),
410 ImageCacheMode::Cache => {
411 match s.cache.entry(key) {
412 IdEntry::Occupied(e) => {
413 let var = e.get();
415 var.set_bind(&r).perm();
416 r.hold(var.clone()).perm();
417 return;
418 }
419 IdEntry::Vacant(e) => {
420 e.insert(r.clone());
422 }
423 }
424 }
425 ImageCacheMode::Retry => {
426 match s.cache.entry(key) {
427 IdEntry::Occupied(mut e) => {
428 let var = e.get();
429 if var.with(ImageEntry::is_error) {
430 r.set_bind(var).perm();
435 var.hold(r.clone()).perm();
436
437 e.insert(r.clone());
439 } else {
440 var.set_bind(&r).perm();
442 r.hold(var.clone()).perm();
443 return;
444 }
445 }
446 IdEntry::Vacant(e) => {
447 e.insert(r.clone());
449 }
450 }
451 }
452 ImageCacheMode::Reload => {
453 match s.cache.entry(key) {
454 IdEntry::Occupied(mut e) => {
455 let var = e.get();
456 r.set_bind(var).perm();
457 var.hold(r.clone()).perm();
458
459 e.insert(r.clone());
460 }
461 IdEntry::Vacant(e) => {
462 e.insert(r.clone());
464 }
465 }
466 }
467 }
468 drop(s);
469
470 match source {
471 ImageSource::Read(path) => {
472 fn read(path: &PathBuf, limit: (&ImageSourceFilter<PathBuf>, ByteLength)) -> std::io::Result<(IpcReadHandle, u64)> {
473 if !limit.0.allows(path) {
474 return Err(std::io::Error::new(
475 std::io::ErrorKind::PermissionDenied,
476 "file path no allowed by limit",
477 ));
478 }
479 let file = std::fs::File::open(path)?;
480 let len = file.metadata()?.len();
481 if len > limit.1.bytes() {
482 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
483 }
484 Ok((IpcReadHandle::best_read_blocking(file)?, len))
485 }
486 let data_format = match path.extension() {
487 Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
488 None => ImageDataFormat::Unknown,
489 };
490 zng_task::spawn_wait(move || match read(&path, (&limits.allow_path, limits.max_encoded_len)) {
491 Ok((data, len)) => {
492 tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", len.bytes());
493 image_data(false, Some(key), data_format, data, options, limits, r)
494 }
495 Err(e) => {
496 tracing::debug!("cannot read {path:?}, {e}");
497 r.set(ImageEntry::new_error(e.to_txt()));
498 }
499 });
500 }
501 #[cfg(feature = "http")]
502 ImageSource::Download(uri, accept) => {
503 let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
504
505 use zng_task::http::*;
506 async fn download(
507 uri: Uri,
508 accept: zng_txt::Txt,
509 limit: (ImageSourceFilter<Uri>, ByteLength),
510 ) -> Result<(ImageDataFormat, IpcBytes), Error> {
511 if !limit.0.allows(&uri) {
512 return Err(Box::new(std::io::Error::new(
513 std::io::ErrorKind::PermissionDenied,
514 "uri no allowed by limit",
515 )));
516 }
517
518 let request = Request::get(uri)?.max_length(limit.1).header(header::ACCEPT, accept.as_str())?;
519 let mut response = send(request).await?;
520 response.error().await?;
521 let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
522 Some(m) => ImageDataFormat::MimeType(m.to_txt()),
523 None => ImageDataFormat::Unknown,
524 };
525 let data = response.body().await?;
526
527 Ok((data_format, data))
528 }
529
530 zng_task::spawn(async move {
531 match download(uri.clone(), accept, (limits.allow_uri.clone(), limits.max_encoded_len)).await {
532 Ok((fmt, data)) => {
533 tracing::trace!("download {uri:?}, len: {:?}, fmt: {fmt:?}", (data.len() as u64).bytes());
534 image_data(false, Some(key), fmt, data.into(), options, limits, r);
535 }
536 Err(e) => {
537 tracing::debug!("cannot download {uri:?}, {e}");
538 r.set(ImageEntry::new_error(e.to_txt()));
539 }
540 }
541 });
542 }
543 ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data.into(), options, limits, r),
544 ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
545 _ => unreachable!(),
546 }
547}
548
549fn image_data(
551 is_respawn: bool,
552 cache_key: Option<ImageHash>,
553 format: ImageDataFormat,
554 data: IpcReadHandle,
555 options: ImageOptions,
556 limits: ImageLimits,
557 r: Var<ImageEntry>,
558) {
559 if !is_respawn && let Some(key) = cache_key {
560 let mut exts = IMAGES_EXTENSIONS.write();
561 if !exts.is_empty() {
562 tracing::trace!("process image_data with {} extensions", exts.len());
563 }
564 for ext in exts.iter_mut() {
565 if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
566 replacement.set_bind(&r).perm();
567 r.hold(replacement).perm();
568
569 tracing::trace!("extension replaced image_data");
570 return;
571 }
572 }
573 }
574
575 if !VIEW_PROCESS.is_available() {
576 tracing::debug!("ignoring image view request after test load due to headless mode");
577 return;
578 }
579
580 let mut data = data;
581 let data_clone = match data.duplicate_or_read_blocking() {
582 Ok(d) => d,
583 Err(e) => {
584 tracing::error!("image data lost, {e}");
585 return;
586 }
587 };
588 let data = data;
589
590 let mut request = ImageRequest::new(
591 format.clone(),
592 data_clone,
593 limits.max_decoded_len.bytes(),
594 options.downscale.clone(),
595 options.mask,
596 );
597 request.entries = options.entries;
598
599 if is_respawn {
600 request.parent = r.with(|r| r.data.meta.parent.clone());
601 }
602
603 if VIEW_PROCESS.is_connected()
604 && let Ok(view_img) = VIEW_PROCESS.add_image(request)
605 {
606 image_view(
608 cache_key,
609 view_img,
610 ImageDecoded::default(),
611 Some((format, data, options, limits)),
612 r,
613 );
614 } else {
615 tracing::debug!("image view request failed, will retry on respawn");
616 let mut once = Some((format, data, options, limits, r));
617 VIEW_PROCESS_INITED_EVENT
618 .hook(move |_| {
619 let (format, data, options, limits, r) = once.take().unwrap();
620 image_data(true, cache_key, format, data, options, limits, r);
621 false
622 })
623 .perm();
624 }
625}
626fn image_view(
628 cache_key: Option<ImageHash>,
629 handle: ViewImageHandle,
630 decoded: ImageDecoded,
631 respawn_data: Option<(ImageDataFormat, IpcReadHandle, ImageOptions, ImageLimits)>,
632 r: Var<ImageEntry>,
633) {
634 let mut img = r.get();
636 img.cache_key = cache_key;
637 img.handle = handle;
638 img.data = decoded;
639
640 let is_loaded = img.is_loaded();
641 let is_dummy = img.view_handle().is_dummy();
642 r.set(img);
643
644 if is_loaded {
645 image_decoded(r);
646 return;
647 }
648
649 if is_dummy {
650 tracing::error!("tried to register dummy handle");
651 return;
652 }
653
654 let decoding_respawn_handle = if respawn_data.is_some() {
656 let r_weak = r.downgrade();
657 let mut respawn_data = respawn_data;
658 VIEW_PROCESS_INITED_EVENT.hook(move |_| {
659 if let Some(r) = r_weak.upgrade() {
660 let (format, data, options, limits) = respawn_data.take().unwrap();
661 image_data(true, cache_key, format, data, options, limits, r);
662 }
663 false
664 })
665 } else {
666 VarHandle::dummy()
668 };
669
670 let r_weak = r.downgrade();
672 let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
673 Some(r) => {
674 if let Some(handle) = args.handle.upgrade()
675 && r.with(|img| img.view_handle() == &handle)
676 {
677 tracing::debug!("image view error, {}", args.error);
678 r.set(ImageEntry::new_error(args.error.clone()));
679 false
680 } else {
681 r.with(ImageEntry::is_loading)
682 }
683 }
684 None => false,
685 });
686
687 let r_weak = r.downgrade();
689 let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
690 Some(r) => {
691 let handle = match args.handle.upgrade() {
692 Some(h) => h,
693 None => return r.with(ImageEntry::is_loading),
694 };
695 if r.with(|img| img.view_handle() == &handle) {
696 let meta = args.meta.clone();
697 tracing::trace!("image view metadata decoded for request");
698 r.modify(move |i| i.data.meta = meta);
699 } else if let Some(p) = &args.meta.parent
700 && p.parent == r.with(|img| img.view_handle().image_id())
701 {
702 tracing::trace!("image view metadata decoded for entry of request");
704 let mut entry_d = ImageDecoded::default();
705 entry_d.meta = args.meta.clone();
706 let entry = var(ImageEntry::new(None, handle.clone(), entry_d.clone()));
707 r.modify(clmv!(entry, |i| i.insert_entry(entry)));
708 image_view(None, handle, entry_d, None, entry);
709 }
710 r.with(ImageEntry::is_loading)
711 }
712 None => false,
713 });
714
715 let r_weak = r.downgrade();
717 RAW_IMAGE_DECODED_EVENT
718 .hook(move |args| {
719 let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
720 match r_weak.upgrade() {
721 Some(r) => {
722 if let Some(handle) = args.handle.upgrade()
723 && r.with(|img| img.view_handle() == &handle)
724 {
725 let data = args.image.upgrade().unwrap();
726 let is_loading = data.partial.is_some();
727 tracing::trace!("image view decoded, partial={:?}", is_loading);
728 r.modify(move |i| i.data = (*data.0).clone());
729 if !is_loading {
730 image_decoded(r);
731 }
732 is_loading
733 } else {
734 r.with(ImageEntry::is_loading)
735 }
736 }
737 None => false,
738 }
739 })
740 .perm();
741}
742fn image_decoded(r: Var<ImageEntry>) {
744 let r_weak = r.downgrade();
745 VIEW_PROCESS_INITED_EVENT
746 .hook(move |_| {
747 if let Some(r) = r_weak.upgrade() {
748 let img = r.get();
749 if !img.is_loaded() {
750 return false;
752 }
753
754 let size = img.size();
756 let mut options = ImageOptions::none();
757 let format = match img.is_mask() {
758 true => {
759 options.mask = Some(ImageMaskMode::A);
760 ImageDataFormat::A8 { size }
761 }
762 false => ImageDataFormat::Bgra8 {
763 size,
764 density: img.density(),
765 original_color_type: img.original_color_type(),
766 },
767 };
768 image_data(
769 true,
770 img.cache_key,
771 format,
772 img.data.pixels.clone().into(),
773 options,
774 ImageLimits::none(),
775 r,
776 );
777 }
778 false
779 })
780 .perm();
781}
782
783fn image_render(
785 cache_key: Option<ImageHash>,
786 render_fn: crate::RenderFn,
787 args: Option<ImageRenderArgs>,
788 options: ImageOptions,
789 r: Var<ImageEntry>,
790) {
791 let s = IMAGES_SV.read();
792 let windows = s.render_windows();
793 let windows_ctx = windows.clone_boxed();
794 let mask = options.mask;
795 windows.open_headless_window(Box::new(move || {
796 let ctx = ImageRenderCtx::new();
797 let retain = ctx.retain.clone();
798 WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
799 let w = render_fn(&args.unwrap_or_default());
800 windows_ctx.enable_frame_capture_in_window_context(mask);
801 image_render_open(cache_key, WINDOW.id(), retain, r);
802 w
803 }));
804}
805
806fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
807 let r_weak = r.downgrade();
809 let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
810 if args.window_id == win_id {
811 if let Some(r) = r_weak.upgrade() {
812 r.set(ImageEntry::new_error(args.error.clone()));
813 }
814 false
815 } else {
816 true
817 }
818 });
819 RAW_HEADLESS_OPEN_EVENT
821 .hook(move |args| {
822 let _hold = &error_handle;
823 args.window_id != win_id
824 })
825 .perm();
826
827 let r_weak = r.downgrade();
829 RAW_FRAME_RENDERED_EVENT
830 .hook(move |args| {
831 if args.window_id == win_id {
832 if let Some(r) = r_weak.upgrade() {
833 match args.frame_image.clone() {
834 Some(h) => {
835 let h = h.upgrade().unwrap();
836 let handle = h.0.0.clone();
837 let data = h.1.clone();
838 let retain = retain.get();
839 r.set(ImageEntry::new(cache_key, handle, data));
840 if !retain {
841 IMAGES_SV.read().render_windows().close_window(win_id);
842 image_decoded(r);
844 }
845 retain
847 }
848 None => {
849 r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
850 false
851 }
852 }
853 } else {
854 false
855 }
856 } else {
857 true
858 }
859 })
860 .perm();
861}
862
863impl IMAGES {
864 pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
877 where
878 N: FnOnce() -> R + Send + Sync + 'static,
879 R: ImageRenderWindowRoot,
880 {
881 let render = Mutex::new(Some(render));
882 let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
883 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
884 self.image_impl(source, options, None)
885 }
886
887 pub fn render_node(
898 &self,
899 render_mode: RenderMode,
900 mask: Option<ImageMaskMode>,
901 render: impl FnOnce() -> UiNode + Send + Sync + 'static,
902 ) -> ImageVar {
903 let render = Mutex::new(Some(render));
904 let source = ImageSource::render_node(render_mode, move |_| {
905 render.lock().take().expect("IMAGES.render closure called more than once")()
906 });
907 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
908 self.image_impl(source, options, None)
909 }
910}
911
912#[expect(non_camel_case_types)]
914pub struct IMAGES_WINDOW;
915impl IMAGES_WINDOW {
916 pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
920 let mut img = IMAGES_SV.write();
921 img.render_windows = Some(service);
922 }
923}
924
925pub trait ImageRenderWindowsService: Send + Sync + 'static {
929 fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
931
932 fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
936
937 fn set_parent_in_window_context(&self, parent_id: WindowId);
941
942 fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
948
949 fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
953
954 fn close_window(&self, window_id: WindowId);
956}
957
958pub trait ImageRenderWindowRoot: Send + Any + 'static {}
962
963#[expect(non_camel_case_types)]
967pub struct IMAGE_RENDER;
968impl IMAGE_RENDER {
969 pub fn is_in_render(&self) -> bool {
973 WINDOW.contains_state(*IMAGE_RENDER_ID)
974 }
975
976 pub fn retain(&self) -> Var<bool> {
980 WINDOW.req_state(*IMAGE_RENDER_ID).retain
981 }
982}
983
984#[zng_app::widget::property(CONTEXT, default(false))]
992pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
993 let retain = retain.into_var();
994 match_node(child, move |_, op| {
995 if let UiNodeOp::Init = op {
996 if IMAGE_RENDER.is_in_render() {
997 let actual_retain = IMAGE_RENDER.retain();
998 actual_retain.set_from(&retain);
999 let handle = actual_retain.bind(&retain);
1000 WIDGET.push_var_handle(handle);
1001 } else {
1002 tracing::error!("can only set `render_retain` in render widgets")
1003 }
1004 }
1005 })
1006}
1007
1008#[derive(Clone)]
1009struct ImageRenderCtx {
1010 retain: Var<bool>,
1011}
1012impl ImageRenderCtx {
1013 fn new() -> Self {
1014 Self { retain: var(false) }
1015 }
1016}
1017
1018static_id! {
1019 static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
1020}