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 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 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 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}