From 7a561ab69c7c75cb4d3a8a7bd7c3dc1795c5b6ae Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Wed, 18 Feb 2026 21:33:36 +0100 Subject: [PATCH] Refactor UI layout and shortcuts The UI layout has been significantly refactored to use `egui::TopBottomPanel` and `egui::SidePanel` for a more organized structure. A new menu bar has been introduced with File, Build, and Tools menus, along with a toolbar for quick access to common actions. Keyboard shortcuts have been updated and consolidated: - `Shift + Enter` now compiles the code and is accessible from anywhere in the app. - `Ctrl + S` saves the current file. The `Save As` functionality has been moved into a dedicated group within the bottom panel and is now triggered by a button in the File menu. The output log and trace logs now use `egui::ScrollArea::both` for better scrolling experience. Minor adjustments to default panel heights and scroll area behaviors have been made for improved usability. --- src/main.rs | 395 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 260 insertions(+), 135 deletions(-) diff --git a/src/main.rs b/src/main.rs index dee46d8..6344080 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,14 +51,19 @@ impl Default for CompilerApp { let path = e.path(); let name = e.file_name().into_string().ok()?; let content = std::fs::read_to_string(&path).ok()?; - Some(Example { name, path, content }) + Some(Example { + name, + path, + content, + }) }) .collect() }) .unwrap_or_default(); Self { - source_code: String::from(r#" + source_code: String::from( + r#" (do (def fib (fn [n] (if (< n 2) @@ -67,11 +72,12 @@ impl Default for CompilerApp { (fib 10) ) -"#), +"#, + ), current_example_path: None, save_as_name: String::new(), show_save_as: false, - output_log: String::from("Ready to compile..."), + output_log: String::from(""), trace_logs: Vec::new(), active_tab: AppTab::Output, debug_enabled: false, @@ -86,10 +92,10 @@ impl CompilerApp { fn compile(&mut self) { let start_total = std::time::Instant::now(); self.trace_logs.clear(); - + // Reset environment for a fresh run self.env = Environment::new(); - + let res = (|| -> Result<(myc::ast::types::Value, std::time::Duration, std::time::Duration, std::time::Duration), String> { let start_compile = std::time::Instant::now(); let compiled = self.env.compile(&self.source_code)?; @@ -104,14 +110,14 @@ impl CompilerApp { let mut vm = myc::ast::vm::VM::new(self.env.global_values.clone()); let mut observer = myc::ast::vm::TracingObserver::new(); let res = vm.run_with_observer(&mut observer, &linked); - + // Batch-truncate logs for GUI performance (once after execution) let mut logs = observer.logs; if logs.len() > 1000 { logs.truncate(1000); logs.push("... [Trace truncated to 1,000 entries] ...".to_string()); } - + for line in &mut logs { if line.len() > 255 && let Some((idx, _)) = line.char_indices().nth(255) { @@ -137,12 +143,7 @@ impl CompilerApp { let now = chrono::Local::now().format("%H:%M:%S").to_string(); self.output_log = format!( "Execution Successful.\nResult: {}\n\nPhases:\n - Compile: {:?}\n - Link: {:?}\n - Run: {:?}\n\nTotal Duration: {:?}\nFinished at: {}", - val, - d_compile, - d_link, - d_run, - d_total, - now, + val, d_compile, d_link, d_run, d_total, now, ); if !self.debug_enabled { self.active_tab = AppTab::Output; @@ -159,10 +160,7 @@ impl CompilerApp { self.env = Environment::new(); match self.env.dump_ast(&self.source_code) { Ok(dump) => { - self.output_log = format!( - "--- BOUND AST DUMP ---\n\n{}", - dump - ); + self.output_log = format!("--- BOUND AST DUMP ---\n\n{}", dump); } Err(e) => { self.output_log = format!("Error during AST Binding: {}", e); @@ -224,7 +222,11 @@ impl CompilerApp { let path = e.path(); let name = e.file_name().into_string().ok()?; let content = std::fs::read_to_string(&path).ok()?; - Some(Example { name, path, content }) + Some(Example { + name, + path, + content, + }) }) .collect(); } @@ -236,8 +238,15 @@ impl CompilerApp { let mut log = String::from("FUNCTIONAL TEST RESULTS:\n\n"); let mut passed = 0; for res in &results { - log.push_str(&format!("{}: {} - {}\n", if res.success { "✅" } else { "❌" }, res.name, res.message)); - if res.success { passed += 1; } + log.push_str(&format!( + "{}: {} - {}\n", + if res.success { "✅" } else { "❌" }, + res.name, + res.message + )); + if res.success { + passed += 1; + } } log.push_str(&format!("\nTotal: {}/{}", passed, results.len())); self.output_log = log; @@ -246,17 +255,28 @@ impl CompilerApp { fn run_benchmarks(&mut self, update: bool) { use myc::utils::tester; if cfg!(debug_assertions) && !update { - self.output_log = String::from("⚠️ WARNING: Benchmarks in Debug mode are inaccurate!\nUse Release build for real measurements.\n\n"); + self.output_log = String::from( + "⚠️ WARNING: Benchmarks in Debug mode are inaccurate!\nUse Release build for real measurements.\n\n", + ); } else { - self.output_log = String::from(if update { "UPDATING BASELINES...\n\n" } else { "RUNNING BENCHMARKS...\n\n" }); + self.output_log = String::from(if update { + "UPDATING BASELINES...\n\n" + } else { + "RUNNING BENCHMARKS...\n\n" + }); } - + let results = tester::run_benchmarks(update); let mut log = self.output_log.clone(); - + for res in results { - let diff = res.diff_pct.map_or(String::new(), |d| format!(" ({:+.1}%)", d)); - log.push_str(&format!("{}: {} - {:?}{}\n", res.status, res.name, res.median, diff)); + let diff = res + .diff_pct + .map_or(String::new(), |d| format!(" ({:+.1}%)", d)); + log.push_str(&format!( + "{}: {} - {:?}{}\n", + res.status, res.name, res.median, diff + )); } self.output_log = log; } @@ -264,6 +284,123 @@ impl CompilerApp { impl eframe::App for CompilerApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Top Panel: Menu Bar & Toolbar + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + egui::MenuBar::new().ui(ui, |ui: &mut egui::Ui| { + ui.menu_button("File", |ui: &mut egui::Ui| { + let file_name = self + .current_example_path + .as_ref() + .map(|p| { + p.file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned() + }) + .unwrap_or_else(|| "None".to_string()); + + if ui.button(format!("Save {} (Ctrl+S)", file_name)).clicked() { + self.save(); + ui.close(); + } + if ui.button("Save As...").clicked() { + self.show_save_as = true; + ui.close(); + } + }); + + ui.menu_button("Build", |ui: &mut egui::Ui| { + if ui.button("Compile (Shift+Enter)").clicked() { + self.compile(); + ui.close(); + } + if ui.button("Dump AST").clicked() { + self.dump_ast(); + ui.close(); + } + ui.separator(); + ui.checkbox(&mut self.debug_enabled, "Debug Mode"); + }); + + ui.menu_button("Tools", |ui: &mut egui::Ui| { + if ui.button("Test All").clicked() { + self.run_all_tests(); + ui.close(); + } + ui.separator(); + + let is_debug = cfg!(debug_assertions); + ui.add_enabled_ui(!is_debug, |ui: &mut egui::Ui| { + let btn = ui.button("Run Benchmarks"); + if is_debug { + btn.on_hover_text("Benchmarks are only available in Release builds."); + } else if btn.clicked() { + self.run_benchmarks(false); + ui.close(); + } + + let btn_base = ui.button("Update Baselines"); + if is_debug { + btn_base.on_hover_text( + "Updating baselines is only allowed in Release builds.", + ); + } else if btn_base.clicked() { + self.run_benchmarks(true); + ui.close(); + } + }); + }); + }); + + ui.separator(); + + // Toolbar (Speed Buttons) + ui.horizontal(|ui| { + ui.style_mut().spacing.item_spacing.x = 4.0; // tighter spacing for speed buttons + + if ui + .button("▶ Run") + .on_hover_text("Compile and Run (Shift+Enter)") + .clicked() + { + self.compile(); + } + + if ui + .button("💾 Save") + .on_hover_text("Save current file (Ctrl+S)") + .clicked() + { + self.save(); + } + + ui.separator(); + + if ui + .button("🔍 AST") + .on_hover_text("Dump Bound AST") + .clicked() + { + self.dump_ast(); + } + + ui.separator(); + + ui.toggle_value(&mut self.debug_enabled, "🐛 Debug"); + + ui.separator(); + + if ui + .button("🧪 Test") + .on_hover_text("Run all functional tests") + .clicked() + { + self.run_all_tests(); + } + }); + ui.add_space(2.0); + }); + // Left Side Panel: Examples egui::SidePanel::left("examples_panel") .resizable(true) @@ -271,84 +408,39 @@ impl eframe::App for CompilerApp { .show(ctx, |ui| { ui.heading("Examples"); ui.add_space(5.0); - for example in &self.examples { - if ui.button(&example.name).clicked() { - self.source_code = example.content.clone(); - self.current_example_path = Some(example.path.clone()); - } - } + egui::ScrollArea::vertical().show(ui, |ui| { + ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { + for example in &self.examples { + if ui.button(&example.name).clicked() { + self.source_code = example.content.clone(); + self.current_example_path = Some(example.path.clone()); + } + } + }); + }); }); // Bottom Panel: Output Log & Controls (Resizable) egui::TopBottomPanel::bottom("bottom_panel") .resizable(true) - .min_height(150.0) - .default_height(250.0) + .min_height(100.0) + .default_height(200.0) + .max_height(ctx.available_rect().height() * 0.5) .show(ctx, |ui| { ui.add_space(5.0); - // Compile Button (at the top of the bottom panel) - ui.horizontal(|ui| { - if ui.button("Compile (Shift+Enter)").clicked() { - self.compile(); - } - - ui.checkbox(&mut self.debug_enabled, "Debug Mode"); - - if ui.button("Dump AST").clicked() { - self.dump_ast(); - } - - if let Some(path) = &self.current_example_path { - let file_name = path.file_name().unwrap_or_default().to_string_lossy(); - if ui.button(format!("Save {} (Ctrl+S)", file_name)).clicked() { - self.save(); - } - } else { - ui.add_enabled_ui(false, |ui| { - let _ = ui.button("Save (None loaded)"); - }); - } - - if ui.button("Save As...").clicked() { - self.show_save_as = !self.show_save_as; - } - - if ui.button("Test All").clicked() { - self.run_all_tests(); - } - - let is_debug = cfg!(debug_assertions); - - ui.add_enabled_ui(!is_debug, |ui| { - let btn = ui.button("Run Benchmarks"); - if is_debug { - btn.on_hover_text("Benchmarks are only available in Release builds for accuracy."); - } else if btn.clicked() { - self.run_benchmarks(false); - } - }); - - ui.add_enabled_ui(!is_debug, |ui| { - let btn = ui.button("Update Baselines"); - if is_debug { - btn.on_hover_text("Updating baselines is only allowed in Release builds."); - } else if btn.clicked() { - self.run_benchmarks(true); - } - }); - }); - if self.show_save_as { - ui.horizontal(|ui| { - ui.label("New Filename:"); - ui.text_edit_singleline(&mut self.save_as_name); - if ui.button("Save Now").clicked() { - self.save_as(); - } - if ui.button("Cancel").clicked() { - self.show_save_as = false; - } + ui.group(|ui| { + ui.horizontal(|ui| { + ui.label("New Filename:"); + ui.text_edit_singleline(&mut self.save_as_name); + if ui.button("Save Now").clicked() { + self.save_as(); + } + if ui.button("Cancel").clicked() { + self.show_save_as = false; + } + }); }); ui.add_space(5.0); } @@ -365,33 +457,48 @@ impl eframe::App for CompilerApp { }; ui.ctx().copy_text(text); } + + if ui.button("Clear Log").clicked() { + self.output_log.clear(); + self.trace_logs.clear(); + } }); + ui.add_space(5.0); + // Log Output Area - egui::ScrollArea::vertical() - .id_salt("output_log_scroll") - .auto_shrink([false, true]) // Don't shrink horizontally, fill width - .show(ui, |ui| { - match self.active_tab { - AppTab::Output => { - ui.add_sized( - ui.available_size(), + match self.active_tab { + AppTab::Output => { + egui::ScrollArea::both() + .id_salt("output_log_scroll") + .auto_shrink(false) + .show(ui, |ui| { + ui.add( egui::TextEdit::multiline(&mut self.output_log) .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) - .interactive(true) + .hint_text("Ready to compile...") + .code_editor() + .lock_focus(false) + .frame(false) + .desired_width(f32::INFINITY), ); - }, - AppTab::Trace => { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| { + }); + } + AppTab::Trace => { + egui::ScrollArea::both() + .id_salt("trace_scroll") + .auto_shrink(false) + .show(ui, |ui| { + ui.style_mut().override_text_style = + Some(egui::TextStyle::Monospace); + ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { for line in &self.trace_logs { ui.label(line); } }); - } - } - }); + }); + } + } }); // Central Panel: Source Code Editor (Fills remaining space) @@ -402,7 +509,8 @@ impl eframe::App for CompilerApp { let editor_id = ui.id().with("source_code_editor"); // Handle Shift+Enter shortcut BEFORE the editor handles it - let compile_shortcut = egui::KeyboardShortcut::new(egui::Modifiers::SHIFT, egui::Key::Enter); + let compile_shortcut = + egui::KeyboardShortcut::new(egui::Modifiers::SHIFT, egui::Key::Enter); if ui.input_mut(|i| i.consume_shortcut(&compile_shortcut)) { self.compile(); } @@ -418,21 +526,24 @@ impl eframe::App for CompilerApp { self.is_first_frame = false; } - egui::ScrollArea::vertical() - .id_salt("source_code_scroll") - .show(ui, |ui| { - let mut layouter = |ui: &egui::Ui, buffer: &dyn egui::TextBuffer, _wrap_width: f32| { - let layout_job = highlight_myc(ui.ctx(), buffer.as_str()); - ui.painter().layout_job(layout_job) - }; + let mut layouter = |ui: &egui::Ui, buffer: &dyn egui::TextBuffer, _wrap_width: f32| { + let mut layout_job = highlight_myc(ui.ctx(), buffer.as_str()); + layout_job.wrap.max_width = f32::INFINITY; // Disable wrapping for code editor + ui.painter().layout_job(layout_job) + }; - ui.add_sized( - ui.available_size(), + egui::ScrollArea::both() + .id_salt("source_editor_scroll") + .auto_shrink(false) + .show(ui, |ui| { + ui.add( egui::TextEdit::multiline(&mut self.source_code) .id(editor_id) .font(egui::TextStyle::Monospace) - .desired_width(f32::INFINITY) + .code_editor() .lock_focus(true) + .desired_width(f32::INFINITY) + .frame(false) .layouter(&mut layouter), ); }); @@ -443,10 +554,10 @@ impl eframe::App for CompilerApp { fn highlight_myc(ctx: &egui::Context, code: &str) -> egui::text::LayoutJob { let mut job = egui::text::LayoutJob::default(); let theme = &ctx.style().visuals; - - let color_kw = egui::Color32::from_rgb(86, 156, 214); // Blue - let color_str = egui::Color32::from_rgb(206, 145, 120); // Orange/Brown - let color_num = egui::Color32::from_rgb(181, 206, 168); // Greenish + + let color_kw = egui::Color32::from_rgb(86, 156, 214); // Blue + let color_str = egui::Color32::from_rgb(206, 145, 120); // Orange/Brown + let color_num = egui::Color32::from_rgb(181, 206, 168); // Greenish let color_comment = egui::Color32::from_rgb(106, 153, 85); // Green let color_bracket = [ egui::Color32::from_rgb(255, 215, 0), // Gold @@ -464,7 +575,9 @@ fn highlight_myc(ctx: &egui::Context, code: &str) -> egui::text::LayoutJob { match c { ';' => { while let Some(&next) = chars.peek() { - if next == '\n' { break; } + if next == '\n' { + break; + } text.push(chars.next().unwrap()); } color = color_comment; @@ -472,8 +585,12 @@ fn highlight_myc(ctx: &egui::Context, code: &str) -> egui::text::LayoutJob { '"' => { while let Some(next) = chars.next() { text.push(next); - if next == '"' { break; } - if next == '\\' && let Some(escaped) = chars.next() { + if next == '"' { + break; + } + if next == '\\' + && let Some(escaped) = chars.next() + { text.push(escaped); } } @@ -481,7 +598,9 @@ fn highlight_myc(ctx: &egui::Context, code: &str) -> egui::text::LayoutJob { } ':' => { while let Some(&next) = chars.peek() { - if next.is_whitespace() || "()[]{}".contains(next) { break; } + if next.is_whitespace() || "()[]{}".contains(next) { + break; + } text.push(chars.next().unwrap()); } color = color_kw; @@ -501,7 +620,9 @@ fn highlight_myc(ctx: &egui::Context, code: &str) -> egui::text::LayoutJob { while let Some(&next) = chars.peek() { if next.is_ascii_digit() || next == '.' { text.push(chars.next().unwrap()); - } else { break; } + } else { + break; + } } color = color_num; } @@ -509,9 +630,13 @@ fn highlight_myc(ctx: &egui::Context, code: &str) -> egui::text::LayoutJob { while let Some(&next) = chars.peek() { if next.is_alphanumeric() || "+-*/<=>!$%&_?.".contains(next) { text.push(chars.next().unwrap()); - } else { break; } + } else { + break; + } } - if ["fn", "def", "do", "if", "assign", "recur", "cond", "macro"].contains(&text.as_str()) { + if ["fn", "def", "do", "if", "assign", "recur", "cond", "macro"] + .contains(&text.as_str()) + { color = color_kw; } }