zng_wgt_inspector/live/
inspector_window.rs

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
38// l10n-## Inspector Window (always en-US)
39
40/// New inspector window.
41pub(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    // hit_select var is used to communicate with the `select_on_click` node on the inspected window.
63    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            // clicked on a widget to select
66            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/// Sets the inspected window on the inspector root widget info.
122#[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
133/// Gets the window that is inspected by the current inspector window.
134pub 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                                        // not async_hn here because menu is dropped on click
231                                        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
288/// Widgets tree view.
289fn 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
404/// Selected widget properties, info.
405fn 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}