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> {
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 response.error().await?;
517 let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
518 Some(m) => ImageDataFormat::MimeType(m.to_txt()),
519 None => ImageDataFormat::Unknown,
520 };
521 let data = response.body().await?;
522
523 Ok((data_format, data))
524 }
525
526 zng_task::spawn(async move {
527 match download(uri.clone(), accept, (limits.allow_uri.clone(), limits.max_encoded_len)).await {
528 Ok((fmt, data)) => {
529 tracing::trace!("download {uri:?}, len: {:?}, fmt: {fmt:?}", (data.len() as u64).bytes());
530 image_data(false, Some(key), fmt, data.into(), options, limits, r);
531 }
532 Err(e) => {
533 tracing::debug!("cannot download {uri:?}, {e}");
534 r.set(ImageEntry::new_error(e.to_txt()));
535 }
536 }
537 });
538 }
539 ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data.into(), options, limits, r),
540 ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
541 _ => unreachable!(),
542 }
543}
544
545fn image_data(
547 is_respawn: bool,
548 cache_key: Option<ImageHash>,
549 format: ImageDataFormat,
550 data: IpcReadHandle,
551 options: ImageOptions,
552 limits: ImageLimits,
553 r: Var<ImageEntry>,
554) {
555 if !is_respawn && let Some(key) = cache_key {
556 let mut exts = IMAGES_EXTENSIONS.write();
557 if !exts.is_empty() {
558 tracing::trace!("process image_data with {} extensions", exts.len());
559 }
560 for ext in exts.iter_mut() {
561 if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
562 replacement.set_bind(&r).perm();
563 r.hold(replacement).perm();
564
565 tracing::trace!("extension replaced image_data");
566 return;
567 }
568 }
569 }
570
571 if !VIEW_PROCESS.is_available() {
572 tracing::debug!("ignoring image view request after test load due to headless mode");
573 return;
574 }
575
576 let mut data = data;
577 let data_clone = match data.duplicate_or_read_blocking() {
578 Ok(d) => d,
579 Err(e) => {
580 tracing::error!("image data lost, {e}");
581 return;
582 }
583 };
584 let data = data;
585
586 let mut request = ImageRequest::new(
587 format.clone(),
588 data_clone,
589 limits.max_decoded_len.bytes(),
590 options.downscale.clone(),
591 options.mask,
592 );
593 request.entries = options.entries;
594
595 if is_respawn {
596 request.parent = r.with(|r| r.data.meta.parent.clone());
597 }
598
599 if VIEW_PROCESS.is_connected()
600 && let Ok(view_img) = VIEW_PROCESS.add_image(request)
601 {
602 image_view(
604 cache_key,
605 view_img,
606 ImageDecoded::default(),
607 Some((format, data, options, limits)),
608 r,
609 );
610 } else {
611 tracing::debug!("image view request failed, will retry on respawn");
612 let mut once = Some((format, data, options, limits, r));
613 VIEW_PROCESS_INITED_EVENT
614 .hook(move |_| {
615 let (format, data, options, limits, r) = once.take().unwrap();
616 image_data(true, cache_key, format, data, options, limits, r);
617 false
618 })
619 .perm();
620 }
621}
622fn image_view(
624 cache_key: Option<ImageHash>,
625 handle: ViewImageHandle,
626 decoded: ImageDecoded,
627 respawn_data: Option<(ImageDataFormat, IpcReadHandle, ImageOptions, ImageLimits)>,
628 r: Var<ImageEntry>,
629) {
630 let mut img = r.get();
632 img.cache_key = cache_key;
633 img.handle = handle;
634 img.data = decoded;
635
636 let is_loaded = img.is_loaded();
637 let is_dummy = img.view_handle().is_dummy();
638 r.set(img);
639
640 if is_loaded {
641 image_decoded(r);
642 return;
643 }
644
645 if is_dummy {
646 tracing::error!("tried to register dummy handle");
647 return;
648 }
649
650 let decoding_respawn_handle = if respawn_data.is_some() {
652 let r_weak = r.downgrade();
653 let mut respawn_data = respawn_data;
654 VIEW_PROCESS_INITED_EVENT.hook(move |_| {
655 if let Some(r) = r_weak.upgrade() {
656 let (format, data, options, limits) = respawn_data.take().unwrap();
657 image_data(true, cache_key, format, data, options, limits, r);
658 }
659 false
660 })
661 } else {
662 VarHandle::dummy()
664 };
665
666 let r_weak = r.downgrade();
668 let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
669 Some(r) => {
670 if let Some(handle) = args.handle.upgrade()
671 && r.with(|img| img.view_handle() == &handle)
672 {
673 tracing::debug!("image view error, {}", args.error);
674 r.set(ImageEntry::new_error(args.error.clone()));
675 false
676 } else {
677 r.with(ImageEntry::is_loading)
678 }
679 }
680 None => false,
681 });
682
683 let r_weak = r.downgrade();
685 let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
686 Some(r) => {
687 let handle = match args.handle.upgrade() {
688 Some(h) => h,
689 None => return r.with(ImageEntry::is_loading),
690 };
691 if r.with(|img| img.view_handle() == &handle) {
692 let meta = args.meta.clone();
693 tracing::trace!("image view metadata decoded for request");
694 r.modify(move |i| i.data.meta = meta);
695 } else if let Some(p) = &args.meta.parent
696 && p.parent == r.with(|img| img.view_handle().image_id())
697 {
698 tracing::trace!("image view metadata decoded for entry of request");
700 let mut entry_d = ImageDecoded::default();
701 entry_d.meta = args.meta.clone();
702 let entry = var(ImageEntry::new(None, handle.clone(), entry_d.clone()));
703 r.modify(clmv!(entry, |i| i.insert_entry(entry)));
704 image_view(None, handle, entry_d, None, entry);
705 }
706 r.with(ImageEntry::is_loading)
707 }
708 None => false,
709 });
710
711 let r_weak = r.downgrade();
713 RAW_IMAGE_DECODED_EVENT
714 .hook(move |args| {
715 let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
716 match r_weak.upgrade() {
717 Some(r) => {
718 if let Some(handle) = args.handle.upgrade()
719 && r.with(|img| img.view_handle() == &handle)
720 {
721 let data = args.image.upgrade().unwrap();
722 let is_loading = data.partial.is_some();
723 tracing::trace!("image view decoded, partial={:?}", is_loading);
724 r.modify(move |i| i.data = (*data.0).clone());
725 if !is_loading {
726 image_decoded(r);
727 }
728 is_loading
729 } else {
730 r.with(ImageEntry::is_loading)
731 }
732 }
733 None => false,
734 }
735 })
736 .perm();
737}
738fn image_decoded(r: Var<ImageEntry>) {
740 let r_weak = r.downgrade();
741 VIEW_PROCESS_INITED_EVENT
742 .hook(move |_| {
743 if let Some(r) = r_weak.upgrade() {
744 let img = r.get();
745 if !img.is_loaded() {
746 return false;
748 }
749
750 let size = img.size();
752 let mut options = ImageOptions::none();
753 let format = match img.is_mask() {
754 true => {
755 options.mask = Some(ImageMaskMode::A);
756 ImageDataFormat::A8 { size }
757 }
758 false => ImageDataFormat::Bgra8 {
759 size,
760 density: img.density(),
761 original_color_type: img.original_color_type(),
762 },
763 };
764 image_data(
765 true,
766 img.cache_key,
767 format,
768 img.data.pixels.clone().into(),
769 options,
770 ImageLimits::none(),
771 r,
772 );
773 }
774 false
775 })
776 .perm();
777}
778
779fn image_render(
781 cache_key: Option<ImageHash>,
782 render_fn: crate::RenderFn,
783 args: Option<ImageRenderArgs>,
784 options: ImageOptions,
785 r: Var<ImageEntry>,
786) {
787 let s = IMAGES_SV.read();
788 let windows = s.render_windows();
789 let windows_ctx = windows.clone_boxed();
790 let mask = options.mask;
791 windows.open_headless_window(Box::new(move || {
792 let ctx = ImageRenderCtx::new();
793 let retain = ctx.retain.clone();
794 WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
795 let w = render_fn(&args.unwrap_or_default());
796 windows_ctx.enable_frame_capture_in_window_context(mask);
797 image_render_open(cache_key, WINDOW.id(), retain, r);
798 w
799 }));
800}
801
802fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
803 let r_weak = r.downgrade();
805 let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
806 if args.window_id == win_id {
807 if let Some(r) = r_weak.upgrade() {
808 r.set(ImageEntry::new_error(args.error.clone()));
809 }
810 false
811 } else {
812 true
813 }
814 });
815 RAW_HEADLESS_OPEN_EVENT
817 .hook(move |args| {
818 let _hold = &error_handle;
819 args.window_id != win_id
820 })
821 .perm();
822
823 let r_weak = r.downgrade();
825 RAW_FRAME_RENDERED_EVENT
826 .hook(move |args| {
827 if args.window_id == win_id {
828 if let Some(r) = r_weak.upgrade() {
829 match args.frame_image.clone() {
830 Some(h) => {
831 let h = h.upgrade().unwrap();
832 let handle = h.0.0.clone();
833 let data = h.1.clone();
834 let retain = retain.get();
835 r.set(ImageEntry::new(cache_key, handle, data));
836 if !retain {
837 IMAGES_SV.read().render_windows().close_window(win_id);
838 image_decoded(r);
840 }
841 retain
843 }
844 None => {
845 r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
846 false
847 }
848 }
849 } else {
850 false
851 }
852 } else {
853 true
854 }
855 })
856 .perm();
857}
858
859impl IMAGES {
860 pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
873 where
874 N: FnOnce() -> R + Send + Sync + 'static,
875 R: ImageRenderWindowRoot,
876 {
877 let render = Mutex::new(Some(render));
878 let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
879 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
880 self.image_impl(source, options, None)
881 }
882
883 pub fn render_node(
894 &self,
895 render_mode: RenderMode,
896 mask: Option<ImageMaskMode>,
897 render: impl FnOnce() -> UiNode + Send + Sync + 'static,
898 ) -> ImageVar {
899 let render = Mutex::new(Some(render));
900 let source = ImageSource::render_node(render_mode, move |_| {
901 render.lock().take().expect("IMAGES.render closure called more than once")()
902 });
903 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
904 self.image_impl(source, options, None)
905 }
906}
907
908#[expect(non_camel_case_types)]
910pub struct IMAGES_WINDOW;
911impl IMAGES_WINDOW {
912 pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
916 let mut img = IMAGES_SV.write();
917 img.render_windows = Some(service);
918 }
919}
920
921pub trait ImageRenderWindowsService: Send + Sync + 'static {
925 fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
927
928 fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
932
933 fn set_parent_in_window_context(&self, parent_id: WindowId);
937
938 fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
944
945 fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
949
950 fn close_window(&self, window_id: WindowId);
952}
953
954pub trait ImageRenderWindowRoot: Send + Any + 'static {}
958
959#[expect(non_camel_case_types)]
963pub struct IMAGE_RENDER;
964impl IMAGE_RENDER {
965 pub fn is_in_render(&self) -> bool {
969 WINDOW.contains_state(*IMAGE_RENDER_ID)
970 }
971
972 pub fn retain(&self) -> Var<bool> {
976 WINDOW.req_state(*IMAGE_RENDER_ID).retain
977 }
978}
979
980#[zng_app::widget::property(CONTEXT, default(false))]
988pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
989 let retain = retain.into_var();
990 match_node(child, move |_, op| {
991 if let UiNodeOp::Init = op {
992 if IMAGE_RENDER.is_in_render() {
993 let actual_retain = IMAGE_RENDER.retain();
994 actual_retain.set_from(&retain);
995 let handle = actual_retain.bind(&retain);
996 WIDGET.push_var_handle(handle);
997 } else {
998 tracing::error!("can only set `render_retain` in render widgets")
999 }
1000 }
1001 })
1002}
1003
1004#[derive(Clone)]
1005struct ImageRenderCtx {
1006 retain: Var<bool>,
1007}
1008impl ImageRenderCtx {
1009 fn new() -> Self {
1010 Self { retain: var(false) }
1011 }
1012}
1013
1014static_id! {
1015 static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
1016}