2026-03-20 11:01:04 +01:00

217 lines
6.5 KiB
Rust

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