simple_editor\ui/
paste_ui.rs

1//! "Paste Attributes" window (Ctrl+Alt+V): a checkbox per `AttrSet` field, All / None buttons, a line
2//! saying what is on the clipboard ("from 'clip.mp4'") and how many clips will change. Non-modal, like
3//! every other window. Returns Some(set) when the user confirms; the app then calls
4//! `Project::paste_attributes` (one undo step).
5
6use crate::model::AttrSet;
7use eframe::egui;
8
9#[derive(Default)]
10pub struct PasteUi {
11    pub open: bool,
12    pub set: AttrSet,
13}
14
15/// Test-only: remember a widget rect so the headless test can click the real button.
16#[cfg(test)]
17fn mark(ui: &egui::Ui, name: &str, r: &egui::Response) {
18    ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new(("paste_btn", name)), r.rect));
19}
20#[cfg(not(test))]
21fn mark(_ui: &egui::Ui, _name: &str, _r: &egui::Response) {}
22
23pub fn show(ctx: &egui::Context, state: &mut PasteUi, source_name: &str, targets: usize) -> Option<AttrSet> {
24    if !state.open {
25        return None;
26    }
27    let mut out = None;
28    let mut close = false;
29    let mut open = true;
30    egui::Window::new("Paste Attributes").open(&mut open).resizable(false).show(ctx, |ui| {
31        if source_name.is_empty() {
32            ui.weak("Nothing copied — use Copy Attributes (Ctrl+Alt+C) first");
33        } else {
34            ui.weak(format!("from '{source_name}'"));
35        }
36        ui.horizontal(|ui| {
37            if ui.small_button("All").clicked() {
38                for (_, v) in state.set.fields() {
39                    *v = true;
40                }
41            }
42            if ui.small_button("None").clicked() {
43                state.set = AttrSet::NONE;
44            }
45        });
46        ui.separator();
47        for (label, v) in state.set.fields() {
48            ui.checkbox(v, label);
49        }
50        ui.separator();
51        ui.weak(match targets {
52            1 => "1 clip will change".to_string(),
53            n => format!("{n} clips will change"),
54        });
55        let ready = targets > 0 && state.set.any() && !source_name.is_empty();
56        ui.horizontal(|ui| {
57            let r = ui.add_enabled(ready, egui::Button::new("Paste"));
58            mark(ui, "paste", &r);
59            if r.clicked() {
60                out = Some(state.set);
61            }
62            if ui.button("Close").clicked() {
63                close = true;
64            }
65        });
66    });
67    // confirming closes the window too (the app applies the set once)
68    state.open = open && !close && out.is_none();
69    out
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    struct H {
77        ctx: egui::Context,
78        state: PasteUi,
79        time: f64,
80    }
81
82    impl H {
83        fn new() -> Self {
84            Self { ctx: egui::Context::default(), state: PasteUi { open: true, ..Default::default() }, time: 0.0 }
85        }
86        fn frame(&mut self, events: Vec<egui::Event>) -> Option<AttrSet> {
87            self.time += 0.05;
88            let input = egui::RawInput { time: Some(self.time), events, ..Default::default() };
89            let mut out = None;
90            let (state, _) = (&mut self.state, ());
91            let _ = self.ctx.run(input, |ctx| out = show(ctx, state, "clip.mp4", 3));
92            out
93        }
94        fn rect(&self, name: &str) -> egui::Rect {
95            self.ctx
96                .data(|d| d.get_temp::<egui::Rect>(egui::Id::new(("paste_btn", name))))
97                .expect("button rect recorded")
98        }
99        fn click(&mut self, pos: egui::Pos2) -> Option<AttrSet> {
100            self.frame(vec![egui::Event::PointerMoved(pos)]);
101            let press = egui::Event::PointerButton {
102                pos,
103                button: egui::PointerButton::Primary,
104                pressed: true,
105                modifiers: egui::Modifiers::NONE,
106            };
107            self.frame(vec![press]);
108            let release = egui::Event::PointerButton {
109                pos,
110                button: egui::PointerButton::Primary,
111                pressed: false,
112                modifiers: egui::Modifiers::NONE,
113            };
114            self.frame(vec![release])
115        }
116    }
117
118    #[test]
119    fn closed_window_returns_nothing() {
120        let ctx = egui::Context::default();
121        let mut st = PasteUi::default();
122        let _ = ctx.run(egui::RawInput::default(), |ctx| {
123            assert!(show(ctx, &mut st, "a.mp4", 1).is_none());
124        });
125    }
126
127    #[test]
128    fn paste_button_returns_the_set_and_closes() {
129        let mut h = H::new();
130        h.frame(vec![]);
131        h.frame(vec![]);
132        let r = h.rect("paste");
133        let got = h.click(r.center()).expect("Paste returns the attribute set");
134        assert_eq!(got, AttrSet::default());
135        assert!(!h.state.open, "confirming closes the window");
136    }
137
138    #[test]
139    fn all_and_none_flip_every_field() {
140        let mut set = AttrSet::NONE;
141        assert!(!set.any());
142        for (_, v) in set.fields() {
143            *v = true;
144        }
145        assert!(set.any());
146        assert!(set.fields().iter().all(|(_, v)| **v));
147    }
148}