Skip to main content

maurice_lib/gui/
mod.rs

1mod about;
2mod debug;
3mod dialogs;
4mod message;
5
6use crate::hardware::keyboard::vkey::MO5VirtualKeyCode;
7use egui::{DroppedFile, FontFamily, FontId, HoveredFile, Pos2, RichText};
8use {
9    eframe::{epaint::TextureHandle, App, Frame},
10    egui::{pos2, Color32, Context, Event, Key, Rect, TextureOptions, Ui, ViewportCommand},
11};
12
13use crate::gui::dialogs::Dialogs;
14use crate::gui::message::Message;
15use crate::hardware::k7::K7;
16use crate::hardware::machine::Machine;
17use crate::hardware::screen::{HEIGHT, WIDTH};
18use log::{info, warn};
19
20#[derive(Default)]
21pub struct Gui {
22    machine: Machine,
23    image: Option<TextureHandle>,
24    dialogs: Dialogs,
25    #[cfg(not(target_family = "wasm"))]
26    file_dialog: Option<egui_file_dialog::FileDialog>,
27    message: Option<Message>,
28}
29
30impl Gui {
31    fn handle_input(&mut self, ctx: &Context) -> bool {
32        ctx.input(|input_state| {
33            let modifiers = input_state.modifiers;
34            self.machine.keyboard.modifiers = modifiers.into();
35            input_state.events.iter().for_each(|event| {
36                if let Event::Key {
37                    key,
38                    physical_key: _,
39                    pressed,
40                    repeat: false,
41                    modifiers: _,
42                } = event
43                {
44                    match key {
45                        Key::F7 => self.machine.reset_soft(),
46                        Key::F8 => self.machine.reset_hard(),
47                        _ => {
48                            if let Ok(vk) = MO5VirtualKeyCode::try_from(*key) {
49                                if *pressed {
50                                    self.machine.keyboard.key_pressed(vk, &mut self.machine.mem);
51                                } else {
52                                    self.machine
53                                        .keyboard
54                                        .key_released(vk, &mut self.machine.mem);
55                                };
56                            }
57                        }
58                    };
59                }
60            });
61        });
62        self.handle_drag_drop(ctx)
63    }
64
65    fn handle_drag_drop(&mut self, ctx: &Context) -> bool {
66        ctx.input(|input_state| {
67            self.handle_dropped_files(&input_state.raw.dropped_files);
68            self.handle_hovered_files(&input_state.raw.hovered_files)
69        })
70    }
71
72    pub fn set_k7(&mut self, k7: K7) {
73        self.machine.set_k7(k7);
74    }
75
76    fn handle_hovered_files(&mut self, hovered_files: &[HoveredFile]) -> bool {
77        if hovered_files.is_empty() {
78            return false;
79        }
80
81        true
82    }
83
84    fn handle_dropped_files(&mut self, dropped_files: &[DroppedFile]) {
85        if dropped_files.is_empty() {
86            return;
87        }
88        for file in dropped_files.iter() {
89            match K7::try_from(file) {
90                Ok(k7) => {
91                    let message = format!("Opened tape {}", k7.name());
92                    info!("{message}");
93                    self.message = Some(Message::new(message));
94                    self.machine.set_k7(k7);
95                }
96                Err(e) => {
97                    let message = format!("Failed to open tape: {e}");
98                    warn!("{message}");
99                    self.message = Some(Message::new(message));
100                }
101            }
102        }
103    }
104
105    fn update_texture(&mut self, ctx: &Context) {
106        let pixels = self.machine.run_loop();
107
108        if let Some(buf) = pixels {
109            let image = egui::ColorImage::from_rgb([buf.width, buf.height], buf.data);
110            match &mut self.image {
111                None => {
112                    self.image =
113                        Some(ctx.load_texture("my_texture", image, TextureOptions::default()))
114                }
115                Some(texture) => texture.set(image, TextureOptions::default()),
116            }
117        }
118    }
119
120    fn build_menu_panel(&mut self, ctx: &Context) {
121        egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
122            egui::menu::bar(ui, |ui| {
123                self.file_menu(ui);
124                self.run_menu(ui);
125                self.reset_menu(ui);
126                #[cfg(not(target_family = "wasm"))]
127                {
128                    self.image_menu(ui, ctx);
129                    // todo debug the debug dialog
130                    self.debug_menu(ui);
131                }
132                self.help_menu(ui);
133            });
134        });
135    }
136
137    fn file_menu(&mut self, ui: &mut Ui) {
138        ui.menu_button("File", |ui| {
139            #[cfg(not(target_family = "wasm"))]
140            if ui.button("Select K7").clicked() {
141                let mut fd = egui_file_dialog::FileDialog::new();
142                fd.pick_file();
143                self.file_dialog = Some(fd);
144            }
145            if ui.button("Rewind Tape").clicked() {
146                self.machine.rewind_k7();
147            }
148            #[cfg(not(target_family = "wasm"))]
149            if ui.button("Exit").clicked() {
150                info!("Exit");
151                ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
152            }
153        });
154    }
155
156    fn run_menu(&mut self, ui: &mut Ui) {
157        ui.menu_button("Run", |ui| {
158            if ui.button("Stop").clicked() {
159                self.machine.stop();
160            }
161            if ui.button("Go").clicked() {
162                self.machine.start();
163            }
164        });
165    }
166
167    fn reset_menu(&mut self, ui: &mut Ui) {
168        ui.menu_button("Reset", |ui| {
169            if ui.button("Soft Reset").clicked() {
170                self.machine.reset_soft();
171            }
172            if ui.button("Hard Reset").clicked() {
173                self.machine.reset_hard();
174            }
175        });
176    }
177
178    #[cfg(not(target_family = "wasm"))]
179    fn image_menu(&mut self, ui: &mut Ui, ctx: &Context) {
180        ui.menu_button("Image", |ui| {
181            if ui.button("Zoom 1x").clicked() {
182                ctx.send_viewport_cmd(ViewportCommand::InnerSize(
183                    [WIDTH as f32, HEIGHT as f32].into(),
184                ))
185            }
186            if ui.button("Zoom 2x").clicked() {
187                ctx.send_viewport_cmd(ViewportCommand::InnerSize(
188                    [(2 * WIDTH) as f32, (2 * HEIGHT) as f32].into(),
189                ))
190            }
191            if ui.button("Zoom 3x").clicked() {
192                ctx.send_viewport_cmd(ViewportCommand::InnerSize(
193                    [(3 * WIDTH) as f32, (3 * HEIGHT) as f32].into(),
194                ))
195            }
196        });
197    }
198
199    //Reset
200    //     Soft Reset
201    // Hard Reset
202    fn debug_menu(&mut self, ui: &mut Ui) {
203        ui.menu_button("Debug", |ui| {
204            if ui.button("Debug").clicked() {
205                self.dialogs.set_show_debug();
206            }
207        });
208    }
209
210    //Reset
211    //     Soft Reset
212    // Hard Reset
213    fn help_menu(&mut self, ui: &mut Ui) {
214        ui.menu_button("Help", |ui| {
215            if ui.button("About").clicked() {
216                self.dialogs.set_show_about();
217            }
218        });
219    }
220
221    fn show_message(ui: &mut Ui, text: &str) {
222        ui.vertical_centered(|ui| {
223            ui.centered_and_justified(|ui| {
224                ui.label(RichText::new(text).color(Color32::DARK_GRAY).font(FontId {
225                    size: 60.0,
226                    family: FontFamily::default(),
227                }));
228            });
229        });
230    }
231}
232
233impl App for Gui {
234    fn update(&mut self, ctx: &Context, _: &mut Frame) {
235        let hovered = self.handle_input(ctx);
236        self.build_menu_panel(ctx);
237        self.dialogs.eventually_show_dialogs(ctx, &mut self.machine);
238        self.update_texture(ctx);
239
240        #[cfg(not(target_family = "wasm"))]
241        if let Some(fd) = &mut self.file_dialog {
242            fd.update(ctx);
243            if let Some(path) = fd.take_picked() {
244                match K7::try_from(path) {
245                    Ok(k7) => self.machine.set_k7(k7),
246                    Err(e) => warn!("{e}"),
247                }
248                self.file_dialog = None;
249            }
250        }
251
252        if let Some(image) = &self.image {
253            let available_rect = ctx.available_rect();
254            let rect = Rect {
255                min: pos2(available_rect.left(), available_rect.top()),
256                max: pos2(available_rect.width(), available_rect.height()),
257            };
258            let uv = Rect {
259                min: Pos2::ZERO,
260                max: pos2(1.0, 1.0),
261            };
262
263            egui::CentralPanel::default()
264                .show(ctx, |ui| {
265                    ui.painter().image(image.into(), rect, uv, Color32::WHITE);
266                    if hovered {
267                        Self::show_message(ui, "Drop a tape file here.");
268                    } else if let Some(message) = &self.message {
269                        if message.is_expired() {
270                            self.message = None;
271                        } else {
272                            Self::show_message(ui, message.message());
273                        }
274                    }
275                })
276                .response
277                .request_focus();
278            ctx.request_repaint();
279        }
280    }
281}