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 } else if let ImageSource::Entries { primary, entries } = source {
378 let entries: Vec<_> = entries
379 .into_iter()
380 .map(|(k, e)| (k, IMAGES.image(e, options.clone(), Some(limits.clone()))))
381 .collect();
382 let r_weak = r.downgrade();
383 let binding = move |mut primary: ImageEntry| -> bool {
384 let primary_id = primary.handle.image_id();
385 for (i, (kind, entry)) in entries.iter().enumerate() {
386 let kind = kind.clone();
387 primary.insert_entry(entry.map(move |e| {
388 let mut e = e.clone();
389 e.data.meta.parent = Some(zng_view_api::image::ImageEntryMetadata::new(primary_id, i, kind.clone()));
390 e
391 }));
392 }
393 if let Some(r) = r_weak.upgrade() {
394 r.set(primary);
395 true
396 } else {
397 false
398 }
399 };
400 let primary = IMAGES.image(*primary, options.clone(), Some(limits.clone()));
401 binding(primary.get());
402 primary.hook(move |a| binding(a.value().clone())).perm();
403 r.hold(primary).perm();
404 return;
405 }
406
407 if !VIEW_PROCESS.is_available() && !s.load_in_headless.get() {
408 tracing::debug!("ignoring image request due headless mode");
409 return;
410 }
411
412 let key = source.hash128(&options).unwrap();
413
414 match options.cache_mode {
416 ImageCacheMode::Ignore => (),
417 ImageCacheMode::Cache => {
418 match s.cache.entry(key) {
419 IdEntry::Occupied(e) => {
420 let var = e.get();
422 var.set_bind(&r).perm();
423 r.hold(var.clone()).perm();
424 return;
425 }
426 IdEntry::Vacant(e) => {
427 e.insert(r.clone());
429 }
430 }
431 }
432 ImageCacheMode::Retry => {
433 match s.cache.entry(key) {
434 IdEntry::Occupied(mut e) => {
435 let var = e.get();
436 if var.with(ImageEntry::is_error) {
437 r.set_bind(var).perm();
442 var.hold(r.clone()).perm();
443
444 e.insert(r.clone());
446 } else {
447 var.set_bind(&r).perm();
449 r.hold(var.clone()).perm();
450 return;
451 }
452 }
453 IdEntry::Vacant(e) => {
454 e.insert(r.clone());
456 }
457 }
458 }
459 ImageCacheMode::Reload => {
460 match s.cache.entry(key) {
461 IdEntry::Occupied(mut e) => {
462 let var = e.get();
463 r.set_bind(var).perm();
464 var.hold(r.clone()).perm();
465
466 e.insert(r.clone());
467 }
468 IdEntry::Vacant(e) => {
469 e.insert(r.clone());
471 }
472 }
473 }
474 }
475 drop(s);
476
477 match source {
478 ImageSource::Read(path) => {
479 fn read(path: &Path, limit: ByteLength) -> std::io::Result<IpcBytes> {
480 let file = std::fs::File::open(path)?;
481 if file.metadata()?.len() > limit.bytes() as u64 {
482 return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "file length exceeds limit"));
483 }
484 IpcBytes::from_file_blocking(file)
485 }
486 let limit = limits.max_encoded_len;
487 let data_format = match path.extension() {
488 Some(ext) => ImageDataFormat::FileExtension(ext.to_string_lossy().to_txt()),
489 None => ImageDataFormat::Unknown,
490 };
491 zng_task::spawn_wait(move || match read(&path, limit) {
492 Ok(data) => {
493 tracing::trace!("read {path:?}, len: {:?}, fmt: {data_format:?}", data.len().bytes());
494 image_data(false, Some(key), data_format, data, options, limits, r)
495 }
496 Err(e) => {
497 tracing::debug!("cannot read {path:?}, {e}");
498 r.set(ImageEntry::new_error(e.to_txt()));
499 }
500 });
501 }
502 #[cfg(feature = "http")]
503 ImageSource::Download(uri, accept) => {
504 let accept = accept.unwrap_or_else(|| IMAGES.http_accept());
505
506 use zng_task::http::*;
507 async fn download(uri: Uri, accept: zng_txt::Txt, limit: ByteLength) -> Result<(ImageDataFormat, IpcBytes), Error> {
508 let request = Request::get(uri)?.max_length(limit).header(header::ACCEPT, accept.as_str())?;
509 let mut response = send(request).await?;
510 let data_format = match response.header().get(&header::CONTENT_TYPE).and_then(|m| m.to_str().ok()) {
511 Some(m) => ImageDataFormat::MimeType(m.to_txt()),
512 None => ImageDataFormat::Unknown,
513 };
514 let data = response.body().await?;
515
516 Ok((data_format, data))
517 }
518
519 let limit = limits.max_encoded_len;
520 zng_task::spawn(async move {
521 match download(uri.clone(), accept, limit).await {
522 Ok((fmt, data)) => {
523 tracing::trace!("download {uri:?}, len: {:?}, fmt: {fmt:?}", data.len().bytes());
524 image_data(false, Some(key), fmt, data, options, limits, r);
525 }
526 Err(e) => {
527 tracing::debug!("cannot download {uri:?}, {e}");
528 r.set(ImageEntry::new_error(e.to_txt()));
529 }
530 }
531 });
532 }
533 ImageSource::Data(_, data, format) => image_data(false, Some(key), format, data, options, limits, r),
534 ImageSource::Render(render_fn, args) => image_render(Some(key), render_fn, args, options, r),
535 _ => unreachable!(),
536 }
537}
538
539fn image_data(
541 is_respawn: bool,
542 cache_key: Option<ImageHash>,
543 format: ImageDataFormat,
544 data: IpcBytes,
545 options: ImageOptions,
546 limits: ImageLimits,
547 r: Var<ImageEntry>,
548) {
549 if !is_respawn && let Some(key) = cache_key {
550 let mut replaced = false;
551 let mut exts = mem::take(&mut IMAGES_SV.write().extensions);
552 if !exts.is_empty() {
553 tracing::trace!("process image_data with {} extensions", exts.len());
554 }
555 for ext in &mut exts {
556 if let Some(replacement) = ext.image_data(limits.max_decoded_len, &key, &data, &format, &options) {
557 replacement.set_bind(&r).perm();
558 r.hold(replacement).perm();
559
560 replaced = true;
561 break;
562 }
563 }
564 {
565 let mut s = IMAGES_SV.write();
566 exts.append(&mut s.extensions);
567 s.extensions = exts;
568
569 if replaced {
570 tracing::trace!("extension replaced image_data");
571 return;
572 }
573 }
574 }
575
576 if !VIEW_PROCESS.is_available() {
577 tracing::debug!("ignoring image view request after test load due to headless mode");
578 return;
579 }
580
581 let mut request = ImageRequest::new(
582 format.clone(),
583 data.clone(),
584 limits.max_decoded_len.bytes() as u64,
585 options.downscale.clone(),
586 options.mask,
587 );
588 request.entries = options.entries;
589
590 if is_respawn {
591 request.parent = r.with(|r| r.data.meta.parent.clone());
592 }
593
594 if VIEW_PROCESS.is_connected()
595 && let Ok(view_img) = VIEW_PROCESS.add_image(request)
596 {
597 image_view(
599 cache_key,
600 view_img,
601 ImageDecoded::default(),
602 Some((format, data, options, limits)),
603 r,
604 );
605 } else {
606 tracing::debug!("image view request failed, will retry on respawn");
607 let mut once = Some((format, data, options, limits, r));
608 VIEW_PROCESS_INITED_EVENT
609 .hook(move |_| {
610 let (format, data, options, limits, r) = once.take().unwrap();
611 image_data(true, cache_key, format, data, options, limits, r);
612 false
613 })
614 .perm();
615 }
616}
617fn image_view(
619 cache_key: Option<ImageHash>,
620 handle: ViewImageHandle,
621 decoded: ImageDecoded,
622 respawn_data: Option<(ImageDataFormat, IpcBytes, ImageOptions, ImageLimits)>,
623 r: Var<ImageEntry>,
624) {
625 let mut img = r.get();
627 img.cache_key = cache_key;
628 img.handle = handle;
629 img.data = decoded;
630
631 let is_loaded = img.is_loaded();
632 let is_dummy = img.view_handle().is_dummy();
633 r.set(img);
634
635 if is_loaded {
636 image_decoded(r);
637 return;
638 }
639
640 if is_dummy {
641 tracing::error!("tried to register dummy handle");
642 return;
643 }
644
645 let decoding_respawn_handle = if respawn_data.is_some() {
647 let r_weak = r.downgrade();
648 let mut respawn_data = respawn_data;
649 VIEW_PROCESS_INITED_EVENT.hook(move |_| {
650 if let Some(r) = r_weak.upgrade() {
651 let (format, data, options, limits) = respawn_data.take().unwrap();
652 image_data(true, cache_key, format, data, options, limits, r);
653 }
654 false
655 })
656 } else {
657 VarHandle::dummy()
659 };
660
661 let r_weak = r.downgrade();
663 let decode_error_handle = RAW_IMAGE_DECODE_ERROR_EVENT.hook(move |args| match r_weak.upgrade() {
664 Some(r) => {
665 if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
666 tracing::debug!("image view error, {}", args.error);
667 r.set(ImageEntry::new_error(args.error.clone()));
668 false
669 } else {
670 r.with(ImageEntry::is_loading)
671 }
672 }
673 None => false,
674 });
675
676 let r_weak = r.downgrade();
678 let decode_meta_handle = RAW_IMAGE_METADATA_DECODED_EVENT.hook(move |args| match r_weak.upgrade() {
679 Some(r) => {
680 if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
681 let meta = args.meta.clone();
682 tracing::trace!("image view metadata decoded for request");
683 r.modify(move |i| i.data.meta = meta);
684 } else if let Some(p) = &args.meta.parent
685 && p.parent == r.with(|img| img.view_handle().image_id())
686 {
687 tracing::trace!("image view metadata decoded for entry of request");
689 let mut entry_d = ImageDecoded::default();
690 entry_d.meta = args.meta.clone();
691 let entry = var(ImageEntry::new(None, args.handle.upgrade().unwrap(), entry_d.clone()));
692 r.modify(clmv!(entry, |i| i.insert_entry(entry)));
693 image_view(None, args.handle.upgrade().unwrap(), entry_d, None, entry);
694 }
695 r.with(ImageEntry::is_loading)
696 }
697 None => false,
698 });
699
700 let r_weak = r.downgrade();
702 RAW_IMAGE_DECODED_EVENT
703 .hook(move |args| {
704 let _hold = [&decoding_respawn_handle, &decode_error_handle, &decode_meta_handle];
705 match r_weak.upgrade() {
706 Some(r) => {
707 if r.with(|img| img.view_handle() == &args.handle.upgrade().unwrap()) {
708 let data = args.image.upgrade().unwrap();
709 let is_loading = data.partial.is_some();
710 tracing::trace!("image view decoded, partial={:?}", is_loading);
711 r.modify(move |i| i.data = (*data.0).clone());
712 if !is_loading {
713 image_decoded(r);
714 }
715 is_loading
716 } else {
717 r.with(ImageEntry::is_loading)
718 }
719 }
720 None => false,
721 }
722 })
723 .perm();
724}
725fn image_decoded(r: Var<ImageEntry>) {
727 let r_weak = r.downgrade();
728 VIEW_PROCESS_INITED_EVENT
729 .hook(move |_| {
730 if let Some(r) = r_weak.upgrade() {
731 let img = r.get();
732 if !img.is_loaded() {
733 return false;
735 }
736
737 let size = img.size();
739 let mut options = ImageOptions::none();
740 let format = match img.is_mask() {
741 true => {
742 options.mask = Some(ImageMaskMode::A);
743 ImageDataFormat::A8 { size }
744 }
745 false => ImageDataFormat::Bgra8 {
746 size,
747 density: img.density(),
748 original_color_type: img.original_color_type(),
749 },
750 };
751 image_data(
752 true,
753 img.cache_key,
754 format,
755 img.data.pixels.clone(),
756 options,
757 ImageLimits::none(),
758 r,
759 );
760 }
761 false
762 })
763 .perm();
764}
765
766fn image_render(
768 cache_key: Option<ImageHash>,
769 render_fn: crate::RenderFn,
770 args: Option<ImageRenderArgs>,
771 options: ImageOptions,
772 r: Var<ImageEntry>,
773) {
774 let s = IMAGES_SV.read();
775 let windows = s.render_windows();
776 let windows_ctx = windows.clone_boxed();
777 let mask = options.mask;
778 windows.open_headless_window(Box::new(move || {
779 let ctx = ImageRenderCtx::new();
780 let retain = ctx.retain.clone();
781 WINDOW.set_state(*IMAGE_RENDER_ID, ctx);
782 let w = render_fn(&args.unwrap_or_default());
783 windows_ctx.enable_frame_capture_in_window_context(mask);
784 image_render_open(cache_key, WINDOW.id(), retain, r);
785 w
786 }));
787}
788
789fn image_render_open(cache_key: Option<ImageHash>, win_id: WindowId, retain: Var<bool>, r: Var<ImageEntry>) {
790 let r_weak = r.downgrade();
792 let error_handle = RAW_WINDOW_OR_HEADLESS_OPEN_ERROR_EVENT.hook(move |args| {
793 if args.window_id == win_id {
794 if let Some(r) = r_weak.upgrade() {
795 r.set(ImageEntry::new_error(args.error.clone()));
796 }
797 false
798 } else {
799 true
800 }
801 });
802 RAW_HEADLESS_OPEN_EVENT
804 .hook(move |args| {
805 let _hold = &error_handle;
806 args.window_id != win_id
807 })
808 .perm();
809
810 let r_weak = r.downgrade();
812 RAW_FRAME_RENDERED_EVENT
813 .hook(move |args| {
814 if args.window_id == win_id {
815 if let Some(r) = r_weak.upgrade() {
816 match args.frame_image.clone() {
817 Some(h) => {
818 let h = h.upgrade().unwrap();
819 let handle = h.0.0.clone();
820 let data = h.1.clone();
821 let retain = retain.get();
822 r.set(ImageEntry::new(cache_key, handle, data));
823 if !retain {
824 IMAGES_SV.read().render_windows().close_window(win_id);
825 image_decoded(r);
827 }
828 retain
830 }
831 None => {
832 r.set(ImageEntry::new_error("image render window did not capture a frame".to_txt()));
833 false
834 }
835 }
836 } else {
837 false
838 }
839 } else {
840 true
841 }
842 })
843 .perm();
844}
845
846impl IMAGES {
847 pub fn render<N, R>(&self, mask: Option<ImageMaskMode>, render: N) -> ImageVar
860 where
861 N: FnOnce() -> R + Send + Sync + 'static,
862 R: ImageRenderWindowRoot,
863 {
864 let render = Mutex::new(Some(render));
865 let source = ImageSource::render(move |_| render.lock().take().expect("IMAGES.render closure called more than once")());
866 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
867 self.image_impl(source, options, None)
868 }
869
870 pub fn render_node(
881 &self,
882 render_mode: RenderMode,
883 mask: Option<ImageMaskMode>,
884 render: impl FnOnce() -> UiNode + Send + Sync + 'static,
885 ) -> ImageVar {
886 let render = Mutex::new(Some(render));
887 let source = ImageSource::render_node(render_mode, move |_| {
888 render.lock().take().expect("IMAGES.render closure called more than once")()
889 });
890 let options = ImageOptions::new(ImageCacheMode::Ignore, None, mask, ImageEntriesMode::empty());
891 self.image_impl(source, options, None)
892 }
893}
894
895#[expect(non_camel_case_types)]
897pub struct IMAGES_WINDOW;
898impl IMAGES_WINDOW {
899 pub fn hook_render_windows_service(&self, service: Box<dyn ImageRenderWindowsService>) {
903 let mut img = IMAGES_SV.write();
904 img.render_windows = Some(service);
905 }
906}
907
908pub trait ImageRenderWindowsService: Send + Sync + 'static {
912 fn clone_boxed(&self) -> Box<dyn ImageRenderWindowsService>;
914
915 fn new_window_root(&self, node: UiNode, render_mode: RenderMode) -> Box<dyn ImageRenderWindowRoot>;
919
920 fn set_parent_in_window_context(&self, parent_id: WindowId);
924
925 fn enable_frame_capture_in_window_context(&self, mask: Option<ImageMaskMode>);
931
932 fn open_headless_window(&self, new_window_root: Box<dyn FnOnce() -> Box<dyn ImageRenderWindowRoot> + Send>);
936
937 fn close_window(&self, window_id: WindowId);
939}
940
941pub trait ImageRenderWindowRoot: Send + Any + 'static {}
945
946#[expect(non_camel_case_types)]
950pub struct IMAGE_RENDER;
951impl IMAGE_RENDER {
952 pub fn is_in_render(&self) -> bool {
956 WINDOW.contains_state(*IMAGE_RENDER_ID)
957 }
958
959 pub fn retain(&self) -> Var<bool> {
963 WINDOW.req_state(*IMAGE_RENDER_ID).retain
964 }
965}
966
967#[zng_app::widget::property(CONTEXT, default(false))]
975pub fn render_retain(child: impl IntoUiNode, retain: impl IntoVar<bool>) -> UiNode {
976 let retain = retain.into_var();
977 match_node(child, move |_, op| {
978 if let UiNodeOp::Init = op {
979 if IMAGE_RENDER.is_in_render() {
980 let actual_retain = IMAGE_RENDER.retain();
981 actual_retain.set_from(&retain);
982 let handle = actual_retain.bind(&retain);
983 WIDGET.push_var_handle(handle);
984 } else {
985 tracing::error!("can only set `render_retain` in render widgets")
986 }
987 }
988 })
989}
990
991#[derive(Clone)]
992struct ImageRenderCtx {
993 retain: Var<bool>,
994}
995impl ImageRenderCtx {
996 fn new() -> Self {
997 Self { retain: var(false) }
998 }
999}
1000
1001static_id! {
1002 static ref IMAGE_RENDER_ID: StateId<ImageRenderCtx>;
1003}