//! Code editor panel //! //! Provides a multi-line text editor for writing Geolog code. use eframe::egui; use crate::gui::state::GuiState; /// Code editor panel pub struct EditorPanel { /// Whether to show line numbers show_line_numbers: bool, } impl EditorPanel { pub fn new() -> Self { Self { show_line_numbers: true, } } pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) { // Header with title and buttons ui.horizontal(|ui| { ui.heading("Editor"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if ui.button("Clear").clicked() { state.editor_content.clear(); } if ui.button("Run").clicked() { state.execute_editor(); } // Show current file if any if let Some(path) = &state.current_file { ui.label(format!( "{}", path.file_name() .map(|s| s.to_string_lossy()) .unwrap_or_default() )); } }); }); ui.separator(); // Editor area with syntax highlighting egui::ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| { self.show_editor_content(ui, state); }); } fn show_editor_content(&mut self, ui: &mut egui::Ui, state: &mut GuiState) { // Use a monospace font for code let font_id = egui::FontId::monospace(14.0); // Calculate line numbers if needed let line_count = state.editor_content.lines().count().max(1); let line_number_width = if self.show_line_numbers { let digits = (line_count as f32).log10().floor() as usize + 1; digits.max(2) as f32 * 10.0 + 8.0 } else { 0.0 }; ui.horizontal(|ui| { // Line numbers column if self.show_line_numbers { ui.allocate_ui_with_layout( egui::vec2(line_number_width, ui.available_height()), egui::Layout::top_down(egui::Align::RIGHT), |ui| { ui.style_mut().visuals.override_text_color = Some(egui::Color32::GRAY); for i in 1..=line_count { ui.label( egui::RichText::new(format!("{}", i)) .font(font_id.clone()) .color(egui::Color32::GRAY), ); } }, ); ui.separator(); } // Main editor area let text_edit = egui::TextEdit::multiline(&mut state.editor_content) .font(font_id) .code_editor() .desired_width(f32::INFINITY) .desired_rows(20) .lock_focus(true); let response = ui.add(text_edit); // Handle keyboard shortcuts if response.has_focus() { let modifiers = ui.input(|i| i.modifiers); if modifiers.ctrl || modifiers.command { if ui.input(|i| i.key_pressed(egui::Key::Enter)) { state.execute_editor(); } } } }); } } impl Default for EditorPanel { fn default() -> Self { Self::new() } } /// Simple syntax highlighting for Geolog code /// Returns a layouter function for egui's code_editor #[allow(dead_code)] fn geolog_highlighter( _ui: &egui::Ui, text: &str, _wrap_width: f32, ) -> egui::text::LayoutJob { let mut job = egui::text::LayoutJob::default(); let keywords = [ "theory", "instance", "query", "forall", "exists", "Sort", "Prop", "true", "false", ]; let font_id = egui::FontId::monospace(14.0); let default_color = egui::Color32::WHITE; let keyword_color = egui::Color32::from_rgb(86, 156, 214); // Blue let comment_color = egui::Color32::from_rgb(106, 153, 85); // Green let string_color = egui::Color32::from_rgb(206, 145, 120); // Orange let mut chars = text.char_indices().peekable(); while let Some((i, c)) = chars.next() { // Check for comments if c == '/' && chars.peek().map(|(_, c)| *c) == Some('/') { // Line comment - consume until end of line let start = i; while let Some((_, c)) = chars.next() { if c == '\n' { break; } } let end = chars.peek().map(|(i, _)| *i).unwrap_or(text.len()); job.append( &text[start..end], 0.0, egui::TextFormat::simple(font_id.clone(), comment_color), ); continue; } // Check for strings if c == '"' { let start = i; while let Some((_, c)) = chars.next() { if c == '"' { break; } if c == '\\' { chars.next(); // Skip escaped character } } let end = chars.peek().map(|(i, _)| *i).unwrap_or(text.len()); job.append( &text[start..end], 0.0, egui::TextFormat::simple(font_id.clone(), string_color), ); continue; } // Check for identifiers/keywords if c.is_alphabetic() || c == '_' { let start = i; while chars.peek().map(|(_, c)| c.is_alphanumeric() || *c == '_') == Some(true) { chars.next(); } let end = chars.peek().map(|(i, _)| *i).unwrap_or(text.len()); let word = &text[start..end]; let color = if keywords.contains(&word) { keyword_color } else { default_color }; job.append( word, 0.0, egui::TextFormat::simple(font_id.clone(), color), ); continue; } // Default: single character job.append( &text[i..i + c.len_utf8()], 0.0, egui::TextFormat::simple(font_id.clone(), default_color), ); } job }