simple_editor\ui/
import_ui.rs

1//! Import report window: after `engine::import::import_file`, show what came through and what did not —
2//! a summary line (clips / tracks / missing media), then a scrollable table of `Issue`s grouped by level
3//! (Ok / Warning / Skipped) with the subject and detail. Buttons: "Use this project" (replaces the current
4//! one, with the usual unsaved-changes prompt), "Copy report" (markdown) and Close. Non-modal.
5
6use crate::engine::import::{ImportReport, Level};
7use crate::theme::Palette;
8use eframe::egui;
9
10#[derive(Default)]
11pub struct ImportUi {
12    pub open: bool,
13    pub report: Option<ImportReport>,
14}
15
16/// True when the user accepted the imported project (the app swaps it in).
17pub fn show(ctx: &egui::Context, state: &mut ImportUi, palette: &Palette) -> bool {
18    if !state.open {
19        return false;
20    }
21    let mut accepted = false;
22    let mut open = state.open;
23    let mut close = false;
24    egui::Window::new("Import Report").open(&mut open).default_width(520.0).default_height(360.0).show(ctx, |ui| {
25        let Some(r) = &state.report else {
26            ui.weak("Nothing imported.");
27            return;
28        };
29        ui.horizontal(|ui| {
30            ui.strong(&r.project.name);
31            ui.weak(format!("{}×{} @ {:.3} fps", r.project.width, r.project.height, r.project.fps));
32        });
33        ui.label(format!("{} clips on {} tracks", r.clips, r.tracks));
34        ui.horizontal(|ui| {
35            if r.missing_media > 0 {
36                ui.colored_label(ui.visuals().warn_fg_color, format!("{} missing files", r.missing_media));
37            }
38            ui.weak(format!("{} problems, {} notes", r.problems(), r.ok()));
39        });
40        ui.separator();
41        egui::ScrollArea::vertical().auto_shrink([false, false]).max_height(220.0).show(ui, |ui| {
42            egui::Grid::new("import_issues").num_columns(3).striped(true).spacing([10.0, 2.0]).show(ui, |ui| {
43                // worst first: what the user has to act on is at the top
44                for level in [Level::Skipped, Level::Warning, Level::Ok] {
45                    for i in r.issues.iter().filter(|i| i.level == level) {
46                        ui.colored_label(level_color(level, ui, palette), level_name(level));
47                        ui.label(&i.subject);
48                        ui.label(&i.detail);
49                        ui.end_row();
50                    }
51                }
52                if r.issues.is_empty() {
53                    ui.weak("Everything mapped cleanly.");
54                    ui.end_row();
55                }
56            });
57        });
58        ui.separator();
59        ui.horizontal(|ui| {
60            if ui.add_enabled(r.clips > 0, egui::Button::new("Use this project")).clicked() {
61                accepted = true;
62                close = true;
63            }
64            if ui.button("Copy report").clicked() {
65                ui.ctx().copy_text(r.to_markdown());
66            }
67            if ui.button("Close").clicked() {
68                close = true;
69            }
70        });
71    });
72    state.open = open && !close;
73    if !state.open && !accepted {
74        state.report = None;
75    }
76    accepted
77}
78
79fn level_name(level: Level) -> &'static str {
80    match level {
81        Level::Ok => "Ok",
82        Level::Warning => "Warning",
83        Level::Skipped => "Skipped",
84    }
85}
86
87fn level_color(level: Level, ui: &egui::Ui, palette: &Palette) -> egui::Color32 {
88    match level {
89        Level::Ok => palette.text_dim,
90        Level::Warning => ui.visuals().warn_fg_color,
91        Level::Skipped => ui.visuals().error_fg_color,
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::engine::import::Issue;
99    use crate::model::Project;
100
101    fn report(clips: usize) -> ImportReport {
102        ImportReport {
103            project: Project::new(),
104            issues: vec![
105                Issue { level: Level::Warning, subject: "clip 'a.mp4'".into(), detail: "media not found".into() },
106                Issue { level: Level::Ok, subject: "sequence".into(), detail: "1920×1080".into() },
107            ],
108            clips,
109            tracks: 2,
110            missing_media: 1,
111        }
112    }
113
114    /// Headless: lays out with and without a report, and stays closed once closed.
115    #[test]
116    fn show_headless() {
117        let ctx = egui::Context::default();
118        let palette = Palette::new(true, egui::Color32::BLUE);
119        let mut state = ImportUi { open: true, report: Some(report(3)) };
120        for _ in 0..2 {
121            let _ = ctx.run(egui::RawInput::default(), |ctx| assert!(!show(ctx, &mut state, &palette)));
122        }
123        assert!(state.open && state.report.is_some());
124        // closed → the report is dropped, and a closed window reports nothing
125        state.open = false;
126        let _ = ctx.run(egui::RawInput::default(), |ctx| assert!(!show(ctx, &mut state, &palette)));
127        assert!(state.report.is_some(), "a closed window is not asked to clean up");
128        let mut empty = ImportUi { open: true, report: None };
129        let _ = ctx.run(egui::RawInput::default(), |ctx| assert!(!show(ctx, &mut empty, &palette)));
130    }
131}