1use std::mem;
2
3use zng_app::widget::{
4 OnVarArgs,
5 border::{BorderSide, BorderSides},
6 builder::{Importance, PropertyArgs, PropertyInfo, WidgetType},
7 inspector::{InspectorActualVars, InstanceItem},
8};
9use zng_color::Rgba;
10use zng_ext_font::{FontStyle, FontWeight};
11use zng_ext_input::focus::FOCUS;
12use zng_ext_l10n::{l10n, lang};
13use zng_ext_window::{WINDOWS, WindowRoot};
14use zng_var::animation::easing;
15use zng_wgt::{Wgt, border, corner_radius, margin, prelude::*, visibility};
16use zng_wgt_button::Button;
17use zng_wgt_container::{Container, child_align, padding};
18use zng_wgt_dialog::{DIALOG, FileDialogFilters};
19use zng_wgt_fill::background_color;
20use zng_wgt_filter::opacity;
21use zng_wgt_input::{focus::focus_shortcut, gesture::click_shortcut, is_hovered};
22use zng_wgt_rule_line::hr::Hr;
23use zng_wgt_scroll::{Scroll, ScrollMode};
24use zng_wgt_size_offset::{size, width};
25use zng_wgt_stack::{Stack, StackDirection};
26use zng_wgt_style::Style;
27use zng_wgt_text::{Text, font_family, lang};
28use zng_wgt_text_input::TextInput;
29use zng_wgt_toggle::{self as toggle, Toggle};
30use zng_wgt_tooltip::{Tip, tooltip};
31use zng_wgt_window as window;
32use zng_wgt_wrap::Wrap;
33
34use super::data_model::*;
35
36use super::HitSelect;
37
38pub(super) fn new(
42 inspected: WindowId,
43 inspected_tree: InspectedTree,
44 selected_wgt: impl Var<Option<InspectedWidget>>,
45 hit_select: impl Var<HitSelect>,
46 adorn_selected: impl Var<bool>,
47 select_focused: impl Var<bool>,
48) -> WindowRoot {
49 let parent = WINDOWS.vars(inspected).unwrap().parent().get().unwrap_or(inspected);
50
51 let vars = WINDOWS.vars(inspected).unwrap();
52
53 let title = l10n!(
54 "inspector/window.title",
55 "{$inspected_window_title} - Inspector",
56 inspected_window_title = vars.title()
57 );
58 let icon = vars.icon();
59
60 let wgt_filter = var(Txt::from_static(""));
61
62 let hit_select_handle = hit_select.on_new(app_hn!(inspected_tree, selected_wgt, |a: &OnVarArgs<HitSelect>, _| {
64 if let HitSelect::Select(id) = a.value {
65 let _ = selected_wgt.set(inspected_tree.inspect(id));
67 }
68 }));
69
70 let mut last_focused = None;
71 let focus_selected = merge_var!(
72 FOCUS.focused(),
73 select_focused.clone(),
74 clmv!(inspected_tree, selected_wgt, |focused, select| {
75 if let Some(p) = focused {
76 if p.window_id() == inspected {
77 last_focused = Some(p.widget_id())
78 }
79 }
80
81 if let (Some(id), true) = (last_focused, *select) {
82 let _ = selected_wgt.set(inspected_tree.inspect(id));
83 }
84 })
85 );
86
87 window::Window! {
88 parent;
89 title;
90 icon;
91 lang = lang!(en_US);
92 width = 1100;
93 set_inspected = inspected;
94 color_scheme = ColorScheme::Dark;
95 on_close = hn!(selected_wgt, |_| {
96 let _ = selected_wgt.set(None);
97 });
98 child = Container! {
99 child_top = menu(hit_select, adorn_selected, select_focused, wgt_filter.clone()), 0;
100 child = Scroll! {
101 toggle::selector = toggle::Selector::single_opt(selected_wgt.clone());
102 child = tree_view(inspected_tree, wgt_filter.clone());
103 child_align = Align::FILL_TOP;
104 padding = 5;
105 };
106 };
107 child_right = Container! {
108 width = 600;
109 child = presenter(selected_wgt, wgt_fn!(|w| {
110 selected_view(w).boxed()
111 }));
112 background_color = SELECTED_BKG_VAR;
113 }, 0;
114
115 zng_wgt::on_deinit = hn!(|_| {
116 let _keep_alive = (&hit_select_handle, &focus_selected);
117 });
118 }
119}
120
121#[property(CONTEXT)]
123fn set_inspected(child: impl UiNode, inspected: impl IntoValue<WindowId>) -> impl UiNode {
124 let inspected = inspected.into();
125 match_node(child, move |_, op| {
126 if let UiNodeOp::Info { info } = op {
127 assert!(WIDGET.parent_id().is_none());
128 info.set_meta(*INSPECTED_ID, inspected);
129 }
130 })
131}
132
133pub fn inspected() -> Option<WindowId> {
135 WINDOW.info().root().meta().get(*INSPECTED_ID).copied()
136}
137
138static_id! {
139 pub(super) static ref INSPECTED_ID: StateId<WindowId>;
140}
141
142context_var! {
143 static TREE_ITEM_BKG_HOVERED_VAR: Rgba = rgb(0.21, 0.21, 0.21);
144 static TREE_ITEM_BKG_CHECKED_VAR: Rgba = rgb(0.29, 0.29, 0.29);
145 static TREE_ITEM_LINE_VAR: Rgba = rgb(0.21, 0.21, 0.21);
146 static WIDGET_ID_COLOR_VAR: Rgba = colors::GRAY;
147 static WIDGET_MACRO_COLOR_VAR: Rgba = colors::AZURE;
148 static PROPERTY_COLOR_VAR: Rgba = colors::YELLOW;
149 static PROPERTY_VALUE_COLOR_VAR: Rgba = colors::ROSE.lighten(50.pct());
150 static NEST_GROUP_COLOR_VAR: Rgba = colors::GRAY;
151 static SELECTED_BKG_VAR: Rgba = rgb(0.15, 0.15, 0.15);
152 static MENU_BKG_VAR: Rgba = rgb(0.13, 0.13, 0.13);
153 pub static SELECTED_BORDER_VAR: Rgba = colors::AZURE;
154}
155
156fn menu(
157 hit_test_select: impl Var<HitSelect>,
158 adorn_selected: impl Var<bool>,
159 select_focused: impl Var<bool>,
160 search: impl Var<Txt>,
161) -> impl UiNode {
162 Container! {
163 background_color = MENU_BKG_VAR;
164 child_left = Stack! {
165 padding = 4;
166 spacing = 2;
167 direction = StackDirection::left_to_right();
168 toggle::style_fn = Style! {
169 padding = 2;
170 corner_radius = 2;
171 };
172 child_align = Align::CENTER;
173 children = ui_vec![
174 Toggle! {
175 child = crosshair_16x16();
176 tooltip = Tip!(Text!("select widget (Ctrl+Shift+C)"));
177 click_shortcut = shortcut!(CTRL|SHIFT+'C');
178 checked = hit_test_select.map_bidi(
179 |c| matches!(c, HitSelect::Enabled),
180 |b| if *b { HitSelect::Enabled } else { HitSelect::Disabled }
181 );
182 },
183 Toggle! {
184 child = Wgt! {
185 size = 16;
186 border = {
187 widths: 3,
188 sides: SELECTED_BORDER_VAR.map_into(),
189 }
190 };
191 tooltip = Tip!(Text!("highlight selected widget"));
192 checked = adorn_selected;
193 },
194 Toggle! {
195 child = Wgt! {
196 size = 14;
197 corner_radius = 14;
198 border = {
199 widths: 1,
200 sides: SELECTED_BORDER_VAR.map(|c| BorderSides::dashed(*c)),
201 }
202 };
203 tooltip = Tip!(Text!("select focused widget"));
204 checked = select_focused;
205 },
206 zng_wgt_rule_line::vr::Vr!(),
207 Toggle! {
208 child = Stack! {
209 size = (14, 10);
210 direction = StackDirection::top_to_bottom();
211 zng_wgt_rule_line::hr::margin = 0;
212 zng_wgt_rule_line::hr::color = zng_wgt_text::FONT_COLOR_VAR;
213 spacing = 3;
214 children = ui_vec![
215 zng_wgt_rule_line::hr::Hr!(),
216 zng_wgt_rule_line::hr::Hr!(),
217 zng_wgt_rule_line::hr::Hr!(),
218 ]
219 };
220 checked = var(false);
221 checked_popup = {
222 let screenshot_idle = var(true);
223 wgt_fn!(screenshot_idle, |_| {
224 zng_wgt_menu::context::ContextMenu!(ui_vec![
225 Button! {
226 child = Text!("Save Screenshot");
227 zng_wgt_menu::icon = zng_wgt::ICONS.get("save");
228 zng_wgt::enabled = screenshot_idle.clone();
229 on_click = hn!(screenshot_idle, |_| {
230 task::spawn(async_clmv!(screenshot_idle, {
232 screenshot_idle.set(false);
233 save_screenshot(inspected().unwrap()).await;
234 screenshot_idle.set(true);
235 }));
236 });
237 },
238 Button! {
239 child = Text!("Copy Screenshot");
240 zng_wgt_menu::icon = zng_wgt::ICONS.get("copy");
241 zng_wgt::enabled = screenshot_idle.clone();
242 on_click = hn!(screenshot_idle, |_| {
243 task::spawn(async_clmv!(screenshot_idle, {
244 screenshot_idle.set(false);
245 copy_screenshot(inspected().unwrap()).await;
246 screenshot_idle.set(true);
247 }));
248 });
249 },
250 ])
251 })
252 };
253 }
254 ]
255 }, 0;
256 child = TextInput! {
257 style_fn = zng_wgt_text_input::SearchStyle!();
258 margin = (0, 0, 0, 50);
259 padding = (3, 5);
260 txt_align = Align::START;
261 focus_shortcut = [shortcut!['S'], shortcut![CTRL+'F'], shortcut![Find]];
262 placeholder_txt = "search widgets (S)";
263 txt = search;
264 }
265 }
266}
267
268fn crosshair_16x16() -> impl UiNode {
269 match_node_leaf(|op| match op {
270 UiNodeOp::Layout { final_size, .. } => {
271 *final_size = DipSize::splat(Dip::new(16)).to_px(LAYOUT.scale_factor());
272 }
273 UiNodeOp::Render { frame } => {
274 let factor = frame.scale_factor();
275 let a = Dip::new(2).to_px(factor);
276 let b = Dip::new(16).to_px(factor);
277 let m = b / Px(2) - a / Px(2);
278
279 let color = FrameValue::Value(colors::WHITE);
280
281 frame.push_color(PxRect::new(PxPoint::new(m, Px(0)), PxSize::new(a, b)), color);
282 frame.push_color(PxRect::new(PxPoint::new(Px(0), m), PxSize::new(b, a)), color);
283 }
284 _ => {}
285 })
286}
287
288fn tree_view(tree: InspectedTree, filter: impl Var<Txt>) -> impl UiNode {
290 Container! {
291 font_family = ["JetBrains Mono", "Consolas", "monospace"];
292 child = tree_item_view(tree.inspect_root(), filter, LocalVar(0u32).boxed());
293 }
294}
295
296fn tree_item_view(wgt: InspectedWidget, filter: impl Var<Txt>, parent_desc_filter: BoxedVar<u32>) -> impl UiNode {
297 let wgt_type = wgt.wgt_type();
298 let wgt_id = wgt.id();
299
300 let mut pass = false;
301 let pass_filter = merge_var!(
302 filter.clone(),
303 wgt_type,
304 clmv!(parent_desc_filter, |f, t| {
305 let p = wgt_filter(f, *t, wgt_id);
306 if p != pass {
307 pass = p;
308 let _ = parent_desc_filter.modify(move |c| {
309 if pass {
310 *c.to_mut() += 1;
311 } else {
312 *c.to_mut() -= 1;
313 }
314 });
315 }
316 p
317 })
318 );
319
320 let descendants_pass_filter = var(0u32).boxed();
321
322 let prev_any_desc = std::sync::atomic::AtomicBool::new(false);
323 descendants_pass_filter
324 .hook(move |a| {
325 let any_desc = 0 < *a.value();
326 if any_desc != prev_any_desc.swap(any_desc, std::sync::atomic::Ordering::Relaxed) {
327 let _ = parent_desc_filter.modify(move |c| {
328 if any_desc {
329 *c.to_mut() += 1;
330 } else {
331 *c.to_mut() -= 1;
332 }
333 });
334 }
335 true
336 })
337 .perm();
338
339 Container! {
340 when !*#{pass_filter.clone()} && *#{descendants_pass_filter.clone()} == 0 {
341 visibility = Visibility::Collapsed;
342 }
343
344 child = Toggle! {
345 toggle::value = wgt.clone();
346
347 style_fn = Style!(replace = true);
348 padding = 2;
349 when *#is_hovered {
350 background_color = TREE_ITEM_BKG_HOVERED_VAR;
351 }
352 when *#toggle::is_checked {
353 background_color = TREE_ITEM_BKG_CHECKED_VAR;
354 }
355
356 child = Wrap! {
357 children = ui_vec![
358 Text! {
359 txt = wgt.wgt_macro_name();
360 font_weight = FontWeight::BOLD;
361 font_color = WIDGET_MACRO_COLOR_VAR;
362
363 when !*#{pass_filter.clone()} {
364 opacity = 50.pct();
365 }
366 },
367 Text!(" {{ "),
368 Text! {
369 txt = formatx!("{:#}", wgt.id());
370 font_color = WIDGET_ID_COLOR_VAR;
371 },
372 Text!(wgt.descendants_len().map(|&l| if l == 0 { Txt::from_static(" }") } else { Txt::from_static("") })),
373 ];
374 }
375 };
376
377 child_bottom = presenter(wgt.children(), wgt_fn!(descendants_pass_filter, |children: Vec<InspectedWidget>| {
378 let children: UiVec = children.into_iter().map(|c| {
379 tree_item_view(c, filter.clone(), descendants_pass_filter.clone())
380 }).collect();
381 if children.is_empty() {
382 NilUiNode.boxed()
383 } else {
384 Container! {
385 child = Stack! {
386 padding = (0, 0, 0, 2.em());
387 direction = StackDirection::top_to_bottom();
388 children;
389
390 border = {
391 widths: (0, 0, 0, 1),
392 sides: TREE_ITEM_LINE_VAR.map(|&c| BorderSides::new_left(BorderSide::dashed(c))),
393 };
394 };
395 child_bottom = Text!("}}"), 0;
396 }.boxed()
397
398 }
399
400 })), 2;
401 }
402}
403
404fn selected_view(wgt: Option<InspectedWidget>) -> impl UiNode {
406 if let Some(wgt) = wgt {
407 Scroll! {
408 mode = ScrollMode::VERTICAL;
409 child_align = Align::FILL_TOP;
410 padding = 4;
411 child = Stack! {
412 direction = StackDirection::top_to_bottom();
413 font_family = ["JetBrains Mono", "Consolas", "monospace"];
414 children = ui_vec![
415 Wrap! {
416 children = ui_vec![
417 Text! {
418 txt = wgt.wgt_macro_name();
419 font_size = 1.2.em();
420 font_weight = FontWeight::BOLD;
421 font_color = WIDGET_MACRO_COLOR_VAR;
422 },
423 Text! {
424 txt = formatx!(" {:#}", wgt.id());
425 font_size = 1.2.em();
426 font_color = WIDGET_ID_COLOR_VAR;
427 },
428 {
429 let parent_property = wgt.parent_property_name();
430 Wrap! {
431 visibility = parent_property.map(|p| (!p.is_empty()).into());
432 tooltip = Tip!(Text!("parent property"));
433 children = ui_vec![
434 Text!(" (in "),
435 Text! {
436 txt = parent_property;
437 font_color = PROPERTY_COLOR_VAR;
438 },
439 Text!(")"),
440 ]
441 }
442 },
443 ]
444 },
445 presenter(
446 wgt.inspector_info(),
447 wgt_fn!(|i| {
448 if let Some(i) = i {
449 inspector_info_view(i).boxed()
450 } else {
451 NilUiNode.boxed()
452 }
453 })
454 ),
455 Hr!(),
456 info_watchers(&wgt),
457 ]
458 }
459 }
460 } else {
461 Text! {
462 txt_align = Align::TOP;
463 padding = 20;
464 font_style = FontStyle::Italic;
465 txt = l10n!("inspector/select-widget", "select a widget to inspect");
466 }
467 }
468}
469
470fn inspector_info_view(info: InspectedInfo) -> impl UiNode {
471 let mut current_group = None;
472 let mut group_items = UiVec::new();
473 let mut out = UiVec::new();
474
475 for item in info.items.iter() {
476 match item {
477 InstanceItem::Property { args, captured } => {
478 let p_info = args.property();
479 let user_assigned = info
480 .builder
481 .property(p_info.id)
482 .map(|p| p.importance == Importance::INSTANCE)
483 .unwrap_or_default();
484
485 if current_group.as_ref() != Some(&p_info.group) {
486 if let Some(g) = current_group.take() {
487 out.push(nest_group_view(g, mem::take(&mut group_items)));
488 }
489 current_group = Some(p_info.group);
490 }
491
492 group_items.push(property_view(&info.actual_vars, &**args, p_info, *captured, user_assigned));
493 }
494 InstanceItem::Intrinsic { group, name } => {
495 if current_group.as_ref() != Some(group) {
496 if let Some(g) = current_group.take() {
497 out.push(nest_group_view(g, mem::take(&mut group_items)));
498 }
499 current_group = Some(*group);
500 }
501 group_items.push(intrinsic_view(name));
502 }
503 }
504 }
505
506 if !group_items.is_empty() {
507 out.push(nest_group_view(current_group.unwrap(), group_items));
508 }
509
510 Stack! {
511 direction = StackDirection::top_to_bottom();
512 children = out;
513 }
514}
515
516fn nest_group_view(group: NestGroup, mut items: UiVec) -> impl UiNode {
517 items.insert(
518 0,
519 Text! {
520 txt = formatx!("// {}", group.name());
521 tooltip = Tip!(Text!(l10n!("inspector/nest-group-help", "nest group")));
522 margin = (10, 0, 0, 0);
523 font_color = NEST_GROUP_COLOR_VAR;
524 },
525 );
526
527 Stack! {
528 direction = StackDirection::top_to_bottom();
529 spacing = 3;
530 children = items;
531 }
532}
533
534fn value_background(value: &BoxedVar<Txt>) -> impl Var<Rgba> {
535 let flash = var(rgba(0, 0, 0, 0));
536 let mut _flashing = None;
537 value
538 .on_pre_new(app_hn!(flash, |_, _| {
539 let h = flash.set_ease(colors::BLACK, colors::BLACK.transparent(), 500.ms(), easing::linear);
540 _flashing = Some(h);
541 }))
542 .perm();
543 flash
544}
545
546fn property_view(
547 actual_vars: &InspectorActualVars,
548 args: &dyn PropertyArgs,
549 info: PropertyInfo,
550 captured: bool,
551 user_assigned: bool,
552) -> impl UiNode {
553 let mut children = ui_vec![
554 Text! {
555 txt = info.name;
556 font_color = PROPERTY_COLOR_VAR;
557 tooltip = Tip!(Text!(if captured { "captured property" } else { "property" }));
558 },
559 Text!(" = "),
560 ];
561 if info.inputs.len() == 1 {
562 let value = actual_vars.get_debug(info.id, 0).unwrap_or_else(|| args.live_debug(0));
563 let flash = value_background(&value);
564
565 children.push(Text! {
566 txt = value;
567 font_color = PROPERTY_VALUE_COLOR_VAR;
568 background_color = flash;
569 tooltip = Tip!(Text!(if user_assigned { "instance value" } else { "intrinsic value" }))
570 });
571 children.push(Text!(";"));
572 } else {
573 children.push(Text!("{{\n"));
574 for (i, input) in info.inputs.iter().enumerate() {
575 children.push(Text!(" {}: ", input.name));
576
577 let value = actual_vars.get_debug(info.id, i).unwrap_or_else(|| args.live_debug(i));
578 let flash = value_background(&value);
579
580 children.push(Text! {
581 txt = value;
582 font_color = PROPERTY_VALUE_COLOR_VAR;
583 background_color = flash;
584 });
585 children.push(Text!(",\n"));
586 }
587 children.push(Text!("}};"));
588 }
589
590 Wrap! {
591 children;
592 }
593}
594
595fn intrinsic_view(name: &'static str) -> impl UiNode {
596 Text! {
597 txt = name;
598 font_style = FontStyle::Italic;
599 tooltip = Tip!(Text!(l10n!("inspector/intrinsic-help", "intrinsic node")));
600 }
601}
602
603fn info_watchers(wgt: &InspectedWidget) -> impl UiNode {
604 let mut children = UiVec::new();
605 children.push(Text! {
606 txt = "interactivity: ";
607 });
608
609 let value = wgt.info().map(|i| formatx!("{:?}", i.interactivity())).boxed();
610 let flash = value_background(&value);
611 children.push(Text! {
612 txt = value;
613 font_color = PROPERTY_VALUE_COLOR_VAR;
614 background_color = flash;
615 });
616
617 children.push(Text! {
618 txt = ",\nvisibility: ";
619 });
620 let value = wgt.render_watcher(|i| formatx!("{:?}", i.visibility())).boxed();
621 let flash = value_background(&value);
622 children.push(Text! {
623 txt = value;
624 font_color = PROPERTY_VALUE_COLOR_VAR;
625 background_color = flash;
626 });
627
628 children.push(Text! {
629 txt = ",\ninner_bounds: ";
630 });
631 let value = wgt.render_watcher(|i| formatx!("{:?}", i.bounds_info().inner_bounds())).boxed();
632 let flash = value_background(&value);
633 children.push(Text! {
634 txt = value;
635 font_color = PROPERTY_VALUE_COLOR_VAR;
636 background_color = flash;
637 });
638 children.push(Text! {
639 txt = ",";
640 });
641
642 Stack! {
643 direction = StackDirection::top_to_bottom();
644 spacing = 3;
645 children = ui_vec![
646 Text! {
647 txt = formatx!("/* INFO */");
648 tooltip = Tip!(Text!(l10n!("inspector/info-help", "watched widget info")));
649 font_color = NEST_GROUP_COLOR_VAR;
650 },
651 Wrap!(children),
652 ];
653 }
654}
655
656fn wgt_filter(filter: &str, wgt_ty: Option<WidgetType>, wgt_id: WidgetId) -> bool {
657 if filter.is_empty() {
658 return true;
659 }
660
661 if let Some(t) = wgt_ty {
662 if let Some(tn) = filter.strip_suffix('!') {
663 if t.name() == tn {
664 return true;
665 }
666 } else if t.name().contains(filter) {
667 return true;
668 }
669 }
670
671 if wgt_id.name().contains(filter) {
672 return true;
673 }
674
675 if let Some(f) = filter.strip_prefix('#') {
676 if let Ok(i) = f.parse::<u64>() {
677 if wgt_id.sequential() == i {
678 return true;
679 }
680 }
681 }
682
683 false
684}
685
686async fn save_screenshot(inspected: WindowId) {
687 let frame = WINDOWS.frame_image(inspected, None);
688
689 let mut filters = FileDialogFilters::new();
690 let encoders = zng_ext_image::IMAGES.available_encoders();
691 for enc in &encoders {
692 filters.push_filter(&enc.to_uppercase(), &[enc]);
693 }
694 filters.push_filter(
695 l10n!("inspector/screenshot.save-dlg-filter", "Image Files").get().as_str(),
696 &encoders,
697 );
698
699 let r = DIALOG.save_file(
700 l10n!("inspector/screenshot.save-dlg-title", "Save Screenshot"),
701 "",
702 l10n!("inspector/screenshot.save-dlg-starting-name", "screenshot.png"),
703 filters,
704 );
705 let path = match r.await {
706 zng_view_api::dialog::FileDialogResponse::Selected(mut p) => p.remove(0),
707 zng_view_api::dialog::FileDialogResponse::Cancel => return,
708 zng_view_api::dialog::FileDialogResponse::Error(e) => {
709 screenshot_error(e).await;
710 return;
711 }
712 };
713
714 frame.wait_value(|f| !f.is_loading()).await;
715 let frame = frame.get();
716
717 if let Some(e) = frame.error() {
718 screenshot_error(e).await;
719 } else {
720 let r = frame.save(path).await;
721 if let Err(e) = r {
722 screenshot_error(
723 l10n!(
724 "inspector/screenshot.save-error",
725 "Screenshot save error. {$error}",
726 error = e.to_string()
727 )
728 .get(),
729 )
730 .await;
731 }
732 }
733}
734
735async fn copy_screenshot(inspected: WindowId) {
736 let frame = WINDOWS.frame_image(inspected, None);
737
738 frame.wait_value(|f| !f.is_loading()).await;
739 let frame = frame.get();
740
741 if let Some(e) = frame.error() {
742 screenshot_error(e).await;
743 } else {
744 let r = zng_ext_clipboard::CLIPBOARD.set_image(frame).wait_rsp().await;
745 if let Err(e) = r {
746 screenshot_error(
747 l10n!(
748 "inspector/screenshot.copy-error",
749 "Screenshot copy error. {$error}",
750 error = e.to_string()
751 )
752 .get(),
753 )
754 .await;
755 }
756 }
757}
758
759async fn screenshot_error(e: Txt) {
760 DIALOG
761 .error(l10n!("inspector/screenshot.error-dlg-title", "Screenshot Error"), e)
762 .await;
763}