Add a simple GUI
This commit is contained in:
parent
7a57c6daef
commit
a6a57ab53f
3908
Cargo.lock
generated
3908
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@ -26,6 +26,27 @@ proptest = "1.4" # property-based testing
|
|||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
tempfile = "3.10" # temp dirs for persistence tests
|
tempfile = "3.10" # temp dirs for persistence tests
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
gui = ["eframe", "egui_extras", "rfd"]
|
||||||
|
|
||||||
|
[dependencies.eframe]
|
||||||
|
version = "0.29"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.egui_extras]
|
||||||
|
version = "0.29"
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.rfd]
|
||||||
|
version = "0.15"
|
||||||
|
optional = true
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "geolog"
|
name = "geolog"
|
||||||
path = "src/bin/geolog.rs"
|
path = "src/bin/geolog.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "geolog-gui"
|
||||||
|
path = "src/bin/geolog-gui.rs"
|
||||||
|
required-features = ["gui"]
|
||||||
|
|||||||
249
README2.md
Normal file
249
README2.md
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
# Geolog Development Notes
|
||||||
|
|
||||||
|
This document describes the features and components added to the geolog project (by me, Hassan).
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Geolog is a language for geometric logic - a type theory with semantics in topoi and geometric morphisms, designed as a unified language for database schemas, queries, and migrations.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Core Components (Pre-existing)
|
||||||
|
|
||||||
|
- **Parser & Lexer** (`src/lexer.rs`, `src/parser.rs`) - Chumsky-based parser for geolog syntax
|
||||||
|
- **AST** (`src/ast.rs`) - Abstract syntax tree representation
|
||||||
|
- **Core Types** (`src/core.rs`) - Signatures, structures, theories, axioms
|
||||||
|
- **Elaboration** (`src/elaborate.rs`) - Type checking and elaboration
|
||||||
|
- **Store** (`src/store.rs`) - Append-only persistent storage with version control
|
||||||
|
- **REPL** (`src/repl.rs`) - Interactive command-line environment
|
||||||
|
- **Solver** (`src/solver.rs`) - Model enumeration for geometric theories
|
||||||
|
- **Query Engine** (`src/query/`) - Relational algebra backend for queries
|
||||||
|
- **Chase Algorithm** (`src/query/chase.rs`) - Fixpoint computation for axiom closure
|
||||||
|
|
||||||
|
### Debugger (`src/debugger.rs`)
|
||||||
|
|
||||||
|
Interactive debugger for the chase algorithm providing:
|
||||||
|
- Step-through execution
|
||||||
|
- Breakpoints on axiom name patterns
|
||||||
|
- Variable inspection
|
||||||
|
- Iteration stepping
|
||||||
|
|
||||||
|
Usage in REPL:
|
||||||
|
```
|
||||||
|
:debug-chase <instance> [max_iterations]
|
||||||
|
```
|
||||||
|
|
||||||
|
### GUI Application (`src/gui/`)
|
||||||
|
|
||||||
|
A full-featured graphical interface built with egui.
|
||||||
|
|
||||||
|
#### Building the GUI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with GUI feature
|
||||||
|
cargo build --features gui
|
||||||
|
|
||||||
|
# Run the GUI
|
||||||
|
cargo run --features gui --bin geolog-gui
|
||||||
|
|
||||||
|
# With workspace persistence
|
||||||
|
cargo run --features gui --bin geolog-gui -- -d ./myproject
|
||||||
|
|
||||||
|
# Load a file on startup
|
||||||
|
cargo run --features gui --bin geolog-gui -- examples/preorder.geolog
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GUI Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/gui/
|
||||||
|
├── mod.rs # Module exports
|
||||||
|
├── app.rs # Main GeologApp (egui::App implementation)
|
||||||
|
├── state.rs # GuiState wrapping ReplState
|
||||||
|
├── panels/
|
||||||
|
│ ├── mod.rs
|
||||||
|
│ ├── browser.rs # Theory/instance tree browser
|
||||||
|
│ ├── editor.rs # Code editor with line numbers
|
||||||
|
│ ├── inspector.rs # Detail view for selected items
|
||||||
|
│ └── console.rs # Output/error messages
|
||||||
|
└── visualizations/
|
||||||
|
├── mod.rs
|
||||||
|
├── chase.rs # Chase step-through visualization
|
||||||
|
└── graph.rs # Relation graph visualization
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GUI Features
|
||||||
|
|
||||||
|
**Browser Panel (Left)**
|
||||||
|
- Tree view of all theories and instances
|
||||||
|
- Filter/search functionality
|
||||||
|
- Click to select, right-click for context menu
|
||||||
|
- Shows counts (sorts, functions, relations, axioms, elements)
|
||||||
|
|
||||||
|
**Editor Panel (Center Top)**
|
||||||
|
- Multi-line code editor
|
||||||
|
- Line numbers
|
||||||
|
- Run button (or Ctrl+Enter) to execute code
|
||||||
|
- Clear button
|
||||||
|
|
||||||
|
**Inspector Panel (Center Bottom)**
|
||||||
|
- Theory details: parameters, sorts, functions, relations, instance fields, axioms
|
||||||
|
- Instance details: elements by sort, function values, relation tuples, nested instances
|
||||||
|
- Collapsible sections
|
||||||
|
- "View Graph" button for instances
|
||||||
|
|
||||||
|
**Console Panel (Bottom)**
|
||||||
|
- Scrollable message log
|
||||||
|
- Color-coded messages: info (gray), success (green), error (red), warning (orange)
|
||||||
|
- Auto-scroll option
|
||||||
|
- Clear button
|
||||||
|
|
||||||
|
**Chase Visualization**
|
||||||
|
- Step/Run All/Stop controls
|
||||||
|
- Events log showing axiom firings with bindings
|
||||||
|
- Breakpoint management
|
||||||
|
- Current structure info (elements, relation tuples)
|
||||||
|
- Mode indicator (Idle/Stepping/Running/Completed)
|
||||||
|
|
||||||
|
**Graph Visualization**
|
||||||
|
- Nodes represent elements (colored by sort)
|
||||||
|
- Edges represent relation tuples
|
||||||
|
- Interactive: drag nodes, zoom (scroll), pan
|
||||||
|
- Relation filter dropdown
|
||||||
|
- Reset Layout / Fit buttons
|
||||||
|
- Hover to see sort name
|
||||||
|
|
||||||
|
#### UI Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ File Edit View Chase Help │
|
||||||
|
├────────────┬────────────────────────────────────────────────┤
|
||||||
|
│ ▼ Theories │ ┌─────────────────────────────────────────┐ │
|
||||||
|
│ Preorder │ │ Editor │ │
|
||||||
|
│ Graph │ │ theory Foo { │ │
|
||||||
|
│ ▼ Instances│ │ X : Sort; │ │
|
||||||
|
│ Chain3 │ │ } [Run] [Clear] │ │
|
||||||
|
│ │ └─────────────────────────────────────────┘ │
|
||||||
|
│ │ ┌─────────────────────────────────────────┐ │
|
||||||
|
│ │ │ Inspector: Chain3 : Preorder │ │
|
||||||
|
│ │ │ ▼ Elements (3) │ │
|
||||||
|
│ │ │ X: bot, mid, top │ │
|
||||||
|
│ │ │ ▼ Relations │ │
|
||||||
|
│ │ │ leq: 6 tuples [View Graph] │ │
|
||||||
|
│ │ └─────────────────────────────────────────┘ │
|
||||||
|
├────────────┴────────────────────────────────────────────────┤
|
||||||
|
│ Console [Clear]│
|
||||||
|
│ > Defined theory Preorder (1 sorts, 1 relations, 2 axioms) │
|
||||||
|
│ + Defined instance Chain3 : Preorder (3 elements) │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Commands
|
||||||
|
|
||||||
|
### REPL Commands
|
||||||
|
|
||||||
|
```
|
||||||
|
:help [topic] Show help (topics: syntax, examples)
|
||||||
|
:quit Exit the REPL
|
||||||
|
:list [target] List theories/instances
|
||||||
|
:inspect <name> Show details of a theory or instance
|
||||||
|
:source <file> Load and execute a geolog file
|
||||||
|
:clear Clear the screen
|
||||||
|
:reset Reset all state
|
||||||
|
|
||||||
|
# Version Control
|
||||||
|
:commit [msg] Commit current changes
|
||||||
|
:history Show commit history
|
||||||
|
|
||||||
|
# Instance Mutation
|
||||||
|
:add <inst> <elem> <sort> Add element to instance
|
||||||
|
:assert <inst> <rel> args Assert relation tuple
|
||||||
|
:retract <inst> <elem> Retract element
|
||||||
|
|
||||||
|
# Query
|
||||||
|
:query <inst> <sort> List all elements of a sort
|
||||||
|
:explain <inst> <sort> Show query execution plan
|
||||||
|
:compile <inst> <sort> Show RelAlgIR compilation
|
||||||
|
:chase <inst> [max_iter] Run chase on instance axioms
|
||||||
|
:debug-chase <inst> [max] Run chase with interactive debugger
|
||||||
|
|
||||||
|
# Solver
|
||||||
|
:solve <theory> [budget] Find model of theory from scratch
|
||||||
|
:extend <inst> <theory> Find extension of instance to theory
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start REPL (in-memory)
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
# Start REPL with workspace persistence
|
||||||
|
cargo run -- -d ./myproject
|
||||||
|
|
||||||
|
# Load file on startup
|
||||||
|
cargo run -- examples/preorder.geolog
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
cargo run -- --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Core Dependencies
|
||||||
|
- `chumsky` - Parser combinators
|
||||||
|
- `ariadne` - Error reporting
|
||||||
|
- `uuid` - Unique identifiers
|
||||||
|
- `rkyv` - Zero-copy serialization
|
||||||
|
- `rustyline` - Readline for REPL
|
||||||
|
- `indexmap` - Ordered hash maps
|
||||||
|
|
||||||
|
### GUI Dependencies (feature = "gui")
|
||||||
|
- `eframe` - egui framework with native window
|
||||||
|
- `egui_extras` - Additional egui widgets
|
||||||
|
- `rfd` - Native file dialogs
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```geolog
|
||||||
|
// Define a preorder theory
|
||||||
|
theory Preorder {
|
||||||
|
X : Sort;
|
||||||
|
leq : [a: X, b: X] -> Prop;
|
||||||
|
|
||||||
|
// Reflexivity
|
||||||
|
forall x : X. |- [a: x, b: x] leq;
|
||||||
|
|
||||||
|
// Transitivity
|
||||||
|
forall x : X, y : X, z : X.
|
||||||
|
[a: x, b: y] leq, [a: y, b: z] leq |- [a: x, b: z] leq;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define an instance
|
||||||
|
instance Chain3 : Preorder = {
|
||||||
|
bot : X;
|
||||||
|
mid : X;
|
||||||
|
top : X;
|
||||||
|
[a: bot, b: mid] leq;
|
||||||
|
[a: mid, b: top] leq;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run `:chase Chain3` to compute the transitive closure.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check without GUI
|
||||||
|
cargo check
|
||||||
|
|
||||||
|
# Check with GUI
|
||||||
|
cargo check --features gui
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Build release
|
||||||
|
cargo build --release --features gui
|
||||||
|
```
|
||||||
134
src/bin/geolog-gui.rs
Normal file
134
src/bin/geolog-gui.rs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
//! Geolog GUI - Graphical interface for geometric logic
|
||||||
|
//!
|
||||||
|
//! Usage: geolog-gui [workspace]
|
||||||
|
//!
|
||||||
|
//! Provides a graphical interface for:
|
||||||
|
//! - Browsing and editing theories and instances
|
||||||
|
//! - Visualizing chase execution
|
||||||
|
//! - Displaying relation graphs
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
use geolog::gui::GeologApp;
|
||||||
|
|
||||||
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
fn main() -> eframe::Result<()> {
|
||||||
|
// Parse command line arguments
|
||||||
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
let (workspace_path, source_files) = parse_args(&args);
|
||||||
|
|
||||||
|
// Set up native window options
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_inner_size([1200.0, 800.0])
|
||||||
|
.with_min_inner_size([800.0, 600.0])
|
||||||
|
.with_title(format!("Geolog v{}", VERSION)),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the application
|
||||||
|
eframe::run_native(
|
||||||
|
"Geolog",
|
||||||
|
options,
|
||||||
|
Box::new(move |cc| {
|
||||||
|
// Set up custom fonts and styles
|
||||||
|
setup_fonts(&cc.egui_ctx);
|
||||||
|
|
||||||
|
// Create the app
|
||||||
|
let mut app = if let Some(path) = workspace_path {
|
||||||
|
GeologApp::with_path(cc, path)
|
||||||
|
} else {
|
||||||
|
GeologApp::new(cc)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load source files if provided
|
||||||
|
for path in source_files {
|
||||||
|
app.state.load_file(&path);
|
||||||
|
app.state.execute_editor();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Box::new(app))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse command line arguments
|
||||||
|
fn parse_args(args: &[String]) -> (Option<PathBuf>, Vec<PathBuf>) {
|
||||||
|
let mut workspace_path = None;
|
||||||
|
let mut source_files = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < args.len() {
|
||||||
|
let arg = &args[i];
|
||||||
|
match arg.as_str() {
|
||||||
|
"-d" | "--dir" => {
|
||||||
|
if i + 1 < args.len() {
|
||||||
|
workspace_path = Some(PathBuf::from(&args[i + 1]));
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
eprintln!("Error: -d requires a path argument");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"-h" | "--help" => {
|
||||||
|
println!("geolog-gui v{} - Geometric Logic GUI", VERSION);
|
||||||
|
println!();
|
||||||
|
println!("Usage: geolog-gui [OPTIONS] [source_files...]");
|
||||||
|
println!();
|
||||||
|
println!("Options:");
|
||||||
|
println!(" -d, --dir <path> Use <path> as workspace directory for persistence");
|
||||||
|
println!(" -h, --help Show this help message");
|
||||||
|
println!(" -v, --version Show version");
|
||||||
|
println!();
|
||||||
|
println!("Examples:");
|
||||||
|
println!(" geolog-gui Start GUI (in-memory, no persistence)");
|
||||||
|
println!(" geolog-gui -d ./myproject Start GUI with workspace persistence");
|
||||||
|
println!(" geolog-gui file.geolog Load file.geolog on startup");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
"-v" | "--version" => {
|
||||||
|
println!("geolog-gui v{}", VERSION);
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
_ if arg.starts_with('-') => {
|
||||||
|
eprintln!("Error: Unknown option '{}'", arg);
|
||||||
|
eprintln!("Try 'geolog-gui --help' for usage information");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Positional argument - treat as source file
|
||||||
|
source_files.push(PathBuf::from(arg));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(workspace_path, source_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up custom fonts and styles
|
||||||
|
fn setup_fonts(ctx: &egui::Context) {
|
||||||
|
// Use a slightly larger default font
|
||||||
|
let mut style = (*ctx.style()).clone();
|
||||||
|
style.text_styles.insert(
|
||||||
|
egui::TextStyle::Body,
|
||||||
|
egui::FontId::proportional(14.0),
|
||||||
|
);
|
||||||
|
style.text_styles.insert(
|
||||||
|
egui::TextStyle::Heading,
|
||||||
|
egui::FontId::proportional(18.0),
|
||||||
|
);
|
||||||
|
style.text_styles.insert(
|
||||||
|
egui::TextStyle::Monospace,
|
||||||
|
egui::FontId::monospace(13.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set up dark theme with custom colors
|
||||||
|
style.visuals = egui::Visuals::dark();
|
||||||
|
style.visuals.override_text_color = Some(egui::Color32::from_gray(220));
|
||||||
|
|
||||||
|
ctx.set_style(style);
|
||||||
|
}
|
||||||
347
src/gui/app.rs
Normal file
347
src/gui/app.rs
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
//! Main Geolog GUI application
|
||||||
|
//!
|
||||||
|
//! Implements the egui::App trait for the main application window.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::panels::{BrowserPanel, ConsolePanel, EditorPanel, InspectorPanel};
|
||||||
|
use super::state::{CentralView, GuiState};
|
||||||
|
use super::visualizations::{ChaseVisualization, GraphView};
|
||||||
|
|
||||||
|
/// Main Geolog GUI application
|
||||||
|
pub struct GeologApp {
|
||||||
|
/// Application state
|
||||||
|
pub state: GuiState,
|
||||||
|
|
||||||
|
/// Panel instances
|
||||||
|
browser: BrowserPanel,
|
||||||
|
editor: EditorPanel,
|
||||||
|
inspector: InspectorPanel,
|
||||||
|
console: ConsolePanel,
|
||||||
|
chase_viz: ChaseVisualization,
|
||||||
|
graph_view: GraphView,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GeologApp {
|
||||||
|
/// Create a new Geolog application
|
||||||
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
Self {
|
||||||
|
state: GuiState::new(),
|
||||||
|
browser: BrowserPanel::new(),
|
||||||
|
editor: EditorPanel::new(),
|
||||||
|
inspector: InspectorPanel::new(),
|
||||||
|
console: ConsolePanel::new(),
|
||||||
|
chase_viz: ChaseVisualization::new(),
|
||||||
|
graph_view: GraphView::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new application with a workspace path
|
||||||
|
pub fn with_path(_cc: &eframe::CreationContext<'_>, path: PathBuf) -> Self {
|
||||||
|
let mut app = Self::new(_cc);
|
||||||
|
app.state = GuiState::with_path(path);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the menu bar
|
||||||
|
fn menu_bar(&mut self, ui: &mut egui::Ui) {
|
||||||
|
egui::menu::bar(ui, |ui| {
|
||||||
|
ui.menu_button("File", |ui| {
|
||||||
|
if ui.button("Open...").clicked() {
|
||||||
|
self.handle_open_file();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Save").clicked() {
|
||||||
|
self.handle_save_file();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
if ui.button("Reset State").clicked() {
|
||||||
|
self.state.repl.reset();
|
||||||
|
self.state.log_info("State reset");
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
ui.separator();
|
||||||
|
if ui.button("Quit").clicked() {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.menu_button("Edit", |ui| {
|
||||||
|
if ui.button("Clear Editor").clicked() {
|
||||||
|
self.state.editor_content.clear();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Clear Console").clicked() {
|
||||||
|
self.state.clear_console();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.menu_button("View", |ui| {
|
||||||
|
if ui
|
||||||
|
.selectable_label(
|
||||||
|
self.state.central_view == CentralView::EditorAndInspector,
|
||||||
|
"Editor & Inspector",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.state.central_view = CentralView::EditorAndInspector;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.selectable_label(
|
||||||
|
self.state.central_view == CentralView::ChaseVisualization,
|
||||||
|
"Chase Visualization",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.state.central_view = CentralView::ChaseVisualization;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.selectable_label(
|
||||||
|
self.state.central_view == CentralView::GraphVisualization,
|
||||||
|
"Graph Visualization",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.state.central_view = CentralView::GraphVisualization;
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.menu_button("Chase", |ui| {
|
||||||
|
if let Some(crate::gui::state::SelectedItem::Instance(name)) =
|
||||||
|
&self.state.selected_item
|
||||||
|
{
|
||||||
|
let name = name.clone();
|
||||||
|
if ui.button(format!("Run Chase on {}", name)).clicked() {
|
||||||
|
self.handle_run_chase(&name);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button(format!("Debug Chase on {}", name)).clicked() {
|
||||||
|
self.state.start_chase(&name);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.label("(Select an instance first)");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.menu_button("Help", |ui| {
|
||||||
|
if ui.button("About").clicked() {
|
||||||
|
self.state.log_info(format!(
|
||||||
|
"Geolog GUI v{}",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
));
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Syntax Help").clicked() {
|
||||||
|
self.show_syntax_help();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle opening a file
|
||||||
|
fn handle_open_file(&mut self) {
|
||||||
|
if let Some(path) = rfd::FileDialog::new()
|
||||||
|
.add_filter("Geolog", &["geolog"])
|
||||||
|
.add_filter("All Files", &["*"])
|
||||||
|
.pick_file()
|
||||||
|
{
|
||||||
|
self.state.load_file(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle saving a file
|
||||||
|
fn handle_save_file(&mut self) {
|
||||||
|
if let Some(path) = &self.state.current_file {
|
||||||
|
match std::fs::write(path, &self.state.editor_content) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.state.log_success(format!("Saved: {}", path.display()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.state.log_error(format!("Failed to save: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Save As
|
||||||
|
if let Some(path) = rfd::FileDialog::new()
|
||||||
|
.add_filter("Geolog", &["geolog"])
|
||||||
|
.save_file()
|
||||||
|
{
|
||||||
|
match std::fs::write(&path, &self.state.editor_content) {
|
||||||
|
Ok(()) => {
|
||||||
|
self.state.current_file = Some(path.clone());
|
||||||
|
self.state.log_success(format!("Saved: {}", path.display()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.state.log_error(format!("Failed to save: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run chase on an instance
|
||||||
|
fn handle_run_chase(&mut self, instance_name: &str) {
|
||||||
|
use crate::core::RelationStorage;
|
||||||
|
use crate::query::chase::chase_fixpoint;
|
||||||
|
|
||||||
|
// Get theory name first (immutable borrow)
|
||||||
|
let theory_name = match self.state.repl.instances.get(instance_name) {
|
||||||
|
Some(e) => e.theory_name.clone(),
|
||||||
|
None => {
|
||||||
|
self.state
|
||||||
|
.log_error(format!("Instance '{}' not found", instance_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let theory = match self.state.repl.theories.get(&theory_name) {
|
||||||
|
Some(t) => t.clone(),
|
||||||
|
None => {
|
||||||
|
self.state
|
||||||
|
.log_error(format!("Theory '{}' not found", theory_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let sig = &theory.theory.signature;
|
||||||
|
let axioms = &theory.theory.axioms;
|
||||||
|
|
||||||
|
if axioms.is_empty() {
|
||||||
|
self.state
|
||||||
|
.log_warning(format!("Theory '{}' has no axioms to chase", theory_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state.log_info(format!(
|
||||||
|
"Running chase on instance '{}' ({} axioms)...",
|
||||||
|
instance_name,
|
||||||
|
axioms.len()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Now get mutable access to entry
|
||||||
|
let entry = self.state.repl.instances.get_mut(instance_name).unwrap();
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let result = chase_fixpoint(
|
||||||
|
axioms,
|
||||||
|
&mut entry.structure,
|
||||||
|
&mut self.state.repl.store.universe,
|
||||||
|
sig,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(iterations) => {
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
// Get structure info before releasing borrow
|
||||||
|
let total_tuples: usize =
|
||||||
|
entry.structure.relations.iter().map(|r| r.len()).sum();
|
||||||
|
let num_elements = entry.structure.len();
|
||||||
|
|
||||||
|
self.state.log_success(format!(
|
||||||
|
"Chase completed in {} iterations ({:.2}ms)",
|
||||||
|
iterations,
|
||||||
|
elapsed.as_secs_f64() * 1000.0
|
||||||
|
));
|
||||||
|
|
||||||
|
// Show structure summary
|
||||||
|
self.state.log_info(format!(
|
||||||
|
"Structure: {} elements, {} relation tuples",
|
||||||
|
num_elements,
|
||||||
|
total_tuples
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.state.log_error(format!("Chase error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Show syntax help in the console
|
||||||
|
fn show_syntax_help(&mut self) {
|
||||||
|
self.state.log_info("=== Geolog Syntax ===");
|
||||||
|
self.state.log_info("");
|
||||||
|
self.state.log_info("theory Name {");
|
||||||
|
self.state.log_info(" Sort1 : Sort;");
|
||||||
|
self.state.log_info(" func : Sort1 -> Sort2;");
|
||||||
|
self.state.log_info(" rel : Sort1 -> Prop;");
|
||||||
|
self.state.log_info("}");
|
||||||
|
self.state.log_info("");
|
||||||
|
self.state.log_info("instance Name : Theory = {");
|
||||||
|
self.state.log_info(" elem : Sort1;");
|
||||||
|
self.state.log_info(" elem func = other_elem;");
|
||||||
|
self.state.log_info(" elem rel;");
|
||||||
|
self.state.log_info("}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the central panel based on current view mode
|
||||||
|
fn central_panel(&mut self, ui: &mut egui::Ui) {
|
||||||
|
match self.state.central_view {
|
||||||
|
CentralView::EditorAndInspector => {
|
||||||
|
// Split vertically: editor on top, inspector on bottom
|
||||||
|
let available_height = ui.available_height();
|
||||||
|
let editor_height = available_height * 0.5;
|
||||||
|
|
||||||
|
// Editor section
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
egui::vec2(ui.available_width(), editor_height),
|
||||||
|
egui::Layout::top_down(egui::Align::LEFT),
|
||||||
|
|ui| {
|
||||||
|
self.editor.show(ui, &mut self.state);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Inspector section
|
||||||
|
self.inspector.show(ui, &mut self.state);
|
||||||
|
}
|
||||||
|
CentralView::ChaseVisualization => {
|
||||||
|
self.chase_viz.show(ui, &mut self.state);
|
||||||
|
}
|
||||||
|
CentralView::GraphVisualization => {
|
||||||
|
self.graph_view.show(ui, &mut self.state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for GeologApp {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
// Top menu bar
|
||||||
|
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||||
|
self.menu_bar(ui);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Left panel: Browser
|
||||||
|
egui::SidePanel::left("browser_panel")
|
||||||
|
.default_width(200.0)
|
||||||
|
.min_width(150.0)
|
||||||
|
.resizable(true)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
self.browser.show(ui, &mut self.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bottom panel: Console
|
||||||
|
egui::TopBottomPanel::bottom("console_panel")
|
||||||
|
.default_height(150.0)
|
||||||
|
.min_height(100.0)
|
||||||
|
.resizable(true)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
self.console.show(ui, &mut self.state);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Central panel: Editor + Inspector or Visualizations
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
self.central_panel(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/gui/mod.rs
Normal file
15
src/gui/mod.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
//! GUI module for Geolog using egui
|
||||||
|
//!
|
||||||
|
//! Provides a graphical interface for:
|
||||||
|
//! - Browsing theories and instances
|
||||||
|
//! - Editing geolog code
|
||||||
|
//! - Visualizing chase execution
|
||||||
|
//! - Displaying relation graphs
|
||||||
|
|
||||||
|
pub mod app;
|
||||||
|
pub mod panels;
|
||||||
|
pub mod state;
|
||||||
|
pub mod visualizations;
|
||||||
|
|
||||||
|
pub use app::GeologApp;
|
||||||
|
pub use state::GuiState;
|
||||||
178
src/gui/panels/browser.rs
Normal file
178
src/gui/panels/browser.rs
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
//! Browser panel for theory/instance navigation
|
||||||
|
//!
|
||||||
|
//! Displays a tree view of all theories and instances.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
use crate::gui::state::{GuiState, SelectedItem};
|
||||||
|
|
||||||
|
/// Browser panel for navigating theories and instances
|
||||||
|
pub struct BrowserPanel {
|
||||||
|
/// Search/filter text
|
||||||
|
filter: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BrowserPanel {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
filter: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
ui.heading("Browser");
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Filter input
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Filter:");
|
||||||
|
ui.text_edit_singleline(&mut self.filter);
|
||||||
|
});
|
||||||
|
ui.add_space(4.0);
|
||||||
|
|
||||||
|
// Scrollable tree view
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
self.show_theories_section(ui, state);
|
||||||
|
ui.add_space(8.0);
|
||||||
|
self.show_instances_section(ui, state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_theories_section(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
let theories = state.repl.list_theories();
|
||||||
|
let filtered: Vec<_> = theories
|
||||||
|
.iter()
|
||||||
|
.filter(|t| {
|
||||||
|
self.filter.is_empty()
|
||||||
|
|| t.name.to_lowercase().contains(&self.filter.to_lowercase())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let header = if self.filter.is_empty() {
|
||||||
|
format!("Theories ({})", theories.len())
|
||||||
|
} else {
|
||||||
|
format!("Theories ({}/{})", filtered.len(), theories.len())
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::CollapsingHeader::new(header)
|
||||||
|
.default_open(state.theories_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
state.theories_expanded = true;
|
||||||
|
if filtered.is_empty() {
|
||||||
|
ui.label("(none)");
|
||||||
|
} else {
|
||||||
|
for theory in filtered {
|
||||||
|
let is_selected = matches!(
|
||||||
|
&state.selected_item,
|
||||||
|
Some(SelectedItem::Theory(name)) if name == &theory.name
|
||||||
|
);
|
||||||
|
|
||||||
|
let label = format!(
|
||||||
|
"{} ({} sorts{}{}{})",
|
||||||
|
theory.name,
|
||||||
|
theory.num_sorts,
|
||||||
|
if theory.num_functions > 0 {
|
||||||
|
format!(", {} funcs", theory.num_functions)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
},
|
||||||
|
if theory.num_relations > 0 {
|
||||||
|
format!(", {} rels", theory.num_relations)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
},
|
||||||
|
if theory.num_axioms > 0 {
|
||||||
|
format!(", {} axioms", theory.num_axioms)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if ui.selectable_label(is_selected, label).clicked() {
|
||||||
|
state.selected_item = Some(SelectedItem::Theory(theory.name.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_instances_section(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
let instances = state.repl.list_instances();
|
||||||
|
let filtered: Vec<_> = instances
|
||||||
|
.iter()
|
||||||
|
.filter(|i| {
|
||||||
|
self.filter.is_empty()
|
||||||
|
|| i.name.to_lowercase().contains(&self.filter.to_lowercase())
|
||||||
|
|| i.theory_name
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(&self.filter.to_lowercase())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let header = if self.filter.is_empty() {
|
||||||
|
format!("Instances ({})", instances.len())
|
||||||
|
} else {
|
||||||
|
format!("Instances ({}/{})", filtered.len(), instances.len())
|
||||||
|
};
|
||||||
|
|
||||||
|
egui::CollapsingHeader::new(header)
|
||||||
|
.default_open(state.instances_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
state.instances_expanded = true;
|
||||||
|
if filtered.is_empty() {
|
||||||
|
ui.label("(none)");
|
||||||
|
} else {
|
||||||
|
for instance in filtered {
|
||||||
|
let is_selected = matches!(
|
||||||
|
&state.selected_item,
|
||||||
|
Some(SelectedItem::Instance(name)) if name == &instance.name
|
||||||
|
);
|
||||||
|
|
||||||
|
let label = format!(
|
||||||
|
"{} : {} ({} elems)",
|
||||||
|
instance.name, instance.theory_name, instance.num_elements
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = ui.selectable_label(is_selected, label);
|
||||||
|
|
||||||
|
// Left click to select
|
||||||
|
if response.clicked() {
|
||||||
|
state.selected_item =
|
||||||
|
Some(SelectedItem::Instance(instance.name.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right-click context menu
|
||||||
|
response.context_menu(|ui| {
|
||||||
|
if ui.button("Inspect").clicked() {
|
||||||
|
state.selected_item =
|
||||||
|
Some(SelectedItem::Instance(instance.name.clone()));
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("Run Chase").clicked() {
|
||||||
|
// This will be handled by the app
|
||||||
|
state.selected_item =
|
||||||
|
Some(SelectedItem::Instance(instance.name.clone()));
|
||||||
|
state.log_info(format!(
|
||||||
|
"Use Chase menu to run chase on '{}'",
|
||||||
|
instance.name
|
||||||
|
));
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button("View Graph").clicked() {
|
||||||
|
state.show_graph(&instance.name, None);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BrowserPanel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/gui/panels/console.rs
Normal file
84
src/gui/panels/console.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//! Console panel for displaying output and error messages
|
||||||
|
//!
|
||||||
|
//! Shows a scrollable log of messages with color coding.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
use crate::gui::state::{GuiState, MessageKind};
|
||||||
|
|
||||||
|
/// Console panel for output messages
|
||||||
|
pub struct ConsolePanel {
|
||||||
|
/// Whether to auto-scroll to bottom
|
||||||
|
auto_scroll: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConsolePanel {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { auto_scroll: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
// Header with title and buttons
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.heading("Console");
|
||||||
|
|
||||||
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
if ui.button("Clear").clicked() {
|
||||||
|
state.clear_console();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.checkbox(&mut self.auto_scroll, "Auto-scroll");
|
||||||
|
|
||||||
|
ui.label(format!("{} messages", state.console_messages.len()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Scrollable message area
|
||||||
|
let scroll_area = egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.stick_to_bottom(self.auto_scroll);
|
||||||
|
|
||||||
|
scroll_area.show(ui, |ui| {
|
||||||
|
if state.console_messages.is_empty() {
|
||||||
|
ui.label("(no messages)");
|
||||||
|
} else {
|
||||||
|
for message in &state.console_messages {
|
||||||
|
let color = match message.kind {
|
||||||
|
MessageKind::Info => egui::Color32::LIGHT_GRAY,
|
||||||
|
MessageKind::Success => egui::Color32::from_rgb(100, 200, 100),
|
||||||
|
MessageKind::Error => egui::Color32::from_rgb(255, 100, 100),
|
||||||
|
MessageKind::Warning => egui::Color32::from_rgb(255, 200, 100),
|
||||||
|
};
|
||||||
|
|
||||||
|
let prefix = match message.kind {
|
||||||
|
MessageKind::Info => ">",
|
||||||
|
MessageKind::Success => "+",
|
||||||
|
MessageKind::Error => "!",
|
||||||
|
MessageKind::Warning => "?",
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(prefix)
|
||||||
|
.color(color)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(&message.text)
|
||||||
|
.color(color)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConsolePanel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
216
src/gui/panels/editor.rs
Normal file
216
src/gui/panels/editor.rs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
//! 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
|
||||||
|
}
|
||||||
318
src/gui/panels/inspector.rs
Normal file
318
src/gui/panels/inspector.rs
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
//! Inspector panel for viewing theory/instance details
|
||||||
|
//!
|
||||||
|
//! Shows detailed information about the currently selected item.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
use crate::gui::state::{GuiState, SelectedItem};
|
||||||
|
use crate::repl::{InstanceDetail, TheoryDetail};
|
||||||
|
|
||||||
|
/// Inspector panel for viewing details
|
||||||
|
pub struct InspectorPanel {}
|
||||||
|
|
||||||
|
impl InspectorPanel {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
ui.heading("Inspector");
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
match &state.selected_item {
|
||||||
|
Some(SelectedItem::Theory(name)) => {
|
||||||
|
if let Some(detail) = state.selected_theory_detail() {
|
||||||
|
self.show_theory_detail(ui, state, &detail);
|
||||||
|
} else {
|
||||||
|
ui.label(format!("Theory '{}' not found", name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(SelectedItem::Instance(name)) => {
|
||||||
|
if let Some(detail) = state.selected_instance_detail() {
|
||||||
|
self.show_instance_detail(ui, state, &detail);
|
||||||
|
} else {
|
||||||
|
ui.label(format!("Instance '{}' not found", name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
ui.label("Select a theory or instance to inspect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_theory_detail(&self, ui: &mut egui::Ui, state: &mut GuiState, detail: &TheoryDetail) {
|
||||||
|
// Theory header
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.strong("Theory:");
|
||||||
|
ui.label(&detail.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
if !detail.params.is_empty() {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Parameters:");
|
||||||
|
for (name, theory) in &detail.params {
|
||||||
|
ui.label(format!("{}: {}", name, theory));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
// Sorts section
|
||||||
|
egui::CollapsingHeader::new(format!("Sorts ({})", detail.sorts.len()))
|
||||||
|
.default_open(state.inspector_sorts_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
state.inspector_sorts_expanded = true;
|
||||||
|
if detail.sorts.is_empty() {
|
||||||
|
ui.label("(none)");
|
||||||
|
} else {
|
||||||
|
for sort in &detail.sorts {
|
||||||
|
ui.label(format!(" {} : Sort", sort));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Functions section
|
||||||
|
egui::CollapsingHeader::new(format!("Functions ({})", detail.functions.len()))
|
||||||
|
.default_open(state.inspector_functions_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
state.inspector_functions_expanded = true;
|
||||||
|
if detail.functions.is_empty() {
|
||||||
|
ui.label("(none)");
|
||||||
|
} else {
|
||||||
|
for (name, domain, codomain) in &detail.functions {
|
||||||
|
ui.label(format!(" {} : {} -> {}", name, domain, codomain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Relations section
|
||||||
|
egui::CollapsingHeader::new(format!("Relations ({})", detail.relations.len()))
|
||||||
|
.default_open(state.inspector_relations_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
state.inspector_relations_expanded = true;
|
||||||
|
if detail.relations.is_empty() {
|
||||||
|
ui.label("(none)");
|
||||||
|
} else {
|
||||||
|
for (name, domain) in &detail.relations {
|
||||||
|
ui.label(format!(" {} : {} -> Prop", name, domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Instance fields section
|
||||||
|
if !detail.instance_fields.is_empty() {
|
||||||
|
egui::CollapsingHeader::new(format!(
|
||||||
|
"Instance Fields ({})",
|
||||||
|
detail.instance_fields.len()
|
||||||
|
))
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for (name, theory_type) in &detail.instance_fields {
|
||||||
|
ui.label(format!(" {} : {} instance", name, theory_type));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axioms section
|
||||||
|
egui::CollapsingHeader::new(format!("Axioms ({})", detail.axioms.len()))
|
||||||
|
.default_open(state.inspector_axioms_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
state.inspector_axioms_expanded = true;
|
||||||
|
if detail.axioms.is_empty() {
|
||||||
|
ui.label("(none)");
|
||||||
|
} else {
|
||||||
|
for (i, axiom) in detail.axioms.iter().enumerate() {
|
||||||
|
let context_str: Vec<String> = axiom
|
||||||
|
.context
|
||||||
|
.iter()
|
||||||
|
.map(|(name, sort)| format!("{}: {}", name, sort))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let axiom_str = if axiom.premise == "true" {
|
||||||
|
format!(
|
||||||
|
"[{}] forall {}. |- {}",
|
||||||
|
i,
|
||||||
|
context_str.join(", "),
|
||||||
|
axiom.conclusion
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"[{}] forall {}. {} |- {}",
|
||||||
|
i,
|
||||||
|
context_str.join(", "),
|
||||||
|
axiom.premise,
|
||||||
|
axiom.conclusion
|
||||||
|
)
|
||||||
|
};
|
||||||
|
ui.label(axiom_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_instance_detail(
|
||||||
|
&self,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
state: &mut GuiState,
|
||||||
|
detail: &InstanceDetail,
|
||||||
|
) {
|
||||||
|
// Instance header
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.strong("Instance:");
|
||||||
|
ui.label(&detail.name);
|
||||||
|
ui.label(":");
|
||||||
|
ui.label(&detail.theory_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let detail_name = detail.name.clone();
|
||||||
|
if ui.button("View Graph").clicked() {
|
||||||
|
state.show_graph(&detail_name, None);
|
||||||
|
}
|
||||||
|
if ui.button("Run Chase").clicked() {
|
||||||
|
state.log_info(format!("Use Chase menu to run chase on '{}'", detail_name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.show(ui, |ui| {
|
||||||
|
// Elements section
|
||||||
|
let total_elements: usize = detail.elements.iter().map(|(_, v)| v.len()).sum();
|
||||||
|
egui::CollapsingHeader::new(format!("Elements ({})", total_elements))
|
||||||
|
.default_open(state.inspector_elements_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
state.inspector_elements_expanded = true;
|
||||||
|
if detail.elements.is_empty() {
|
||||||
|
ui.label("(none)");
|
||||||
|
} else {
|
||||||
|
for (sort_name, elements) in &detail.elements {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.strong(format!("{} ({}):", sort_name, elements.len()));
|
||||||
|
});
|
||||||
|
ui.indent("elements", |ui| {
|
||||||
|
// Show elements in a grid if there are many
|
||||||
|
if elements.len() <= 10 {
|
||||||
|
for elem in elements {
|
||||||
|
ui.label(format!(" {}", elem));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show first few and a count
|
||||||
|
for elem in elements.iter().take(5) {
|
||||||
|
ui.label(format!(" {}", elem));
|
||||||
|
}
|
||||||
|
ui.label(format!(" ... and {} more", elements.len() - 5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Functions section
|
||||||
|
let total_func_values: usize = detail.functions.iter().map(|(_, v)| v.len()).sum();
|
||||||
|
if total_func_values > 0 {
|
||||||
|
egui::CollapsingHeader::new(format!("Function Values ({})", total_func_values))
|
||||||
|
.default_open(state.inspector_functions_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for (func_name, values) in &detail.functions {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.strong(format!("{} ({}):", func_name, values.len()));
|
||||||
|
});
|
||||||
|
ui.indent("func_values", |ui| {
|
||||||
|
if values.len() <= 10 {
|
||||||
|
for value in values {
|
||||||
|
ui.label(format!(" {}", value));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for value in values.iter().take(5) {
|
||||||
|
ui.label(format!(" {}", value));
|
||||||
|
}
|
||||||
|
ui.label(format!(" ... and {} more", values.len() - 5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relations section
|
||||||
|
let total_tuples: usize = detail.relations.iter().map(|(_, _, t)| t.len()).sum();
|
||||||
|
if total_tuples > 0 {
|
||||||
|
egui::CollapsingHeader::new(format!("Relations ({} tuples)", total_tuples))
|
||||||
|
.default_open(state.inspector_relations_expanded)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for (rel_name, field_names, tuples) in &detail.relations {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.strong(format!("{} ({} tuples):", rel_name, tuples.len()));
|
||||||
|
|
||||||
|
// Add button to view as graph
|
||||||
|
let detail_name = detail.name.clone();
|
||||||
|
let rel_name_clone = rel_name.clone();
|
||||||
|
if ui.small_button("View Graph").clicked() {
|
||||||
|
state.show_graph(&detail_name, Some(&rel_name_clone));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.indent("rel_tuples", |ui| {
|
||||||
|
let display_tuples = if tuples.len() <= 10 {
|
||||||
|
tuples.as_slice()
|
||||||
|
} else {
|
||||||
|
&tuples[..5]
|
||||||
|
};
|
||||||
|
|
||||||
|
for tuple in display_tuples {
|
||||||
|
let tuple_str = if field_names.is_empty() {
|
||||||
|
// Unary relation
|
||||||
|
format!(" {} {}", tuple.join(", "), rel_name)
|
||||||
|
} else {
|
||||||
|
// Multi-ary relation with field names
|
||||||
|
let fields: Vec<String> = field_names
|
||||||
|
.iter()
|
||||||
|
.zip(tuple.iter())
|
||||||
|
.map(|(f, v)| format!("{}: {}", f, v))
|
||||||
|
.collect();
|
||||||
|
format!(" [{}] {}", fields.join(", "), rel_name)
|
||||||
|
};
|
||||||
|
ui.label(tuple_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
if tuples.len() > 10 {
|
||||||
|
ui.label(format!(" ... and {} more", tuples.len() - 5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nested instances section
|
||||||
|
if !detail.nested.is_empty() {
|
||||||
|
egui::CollapsingHeader::new(format!(
|
||||||
|
"Nested Instances ({})",
|
||||||
|
detail.nested.len()
|
||||||
|
))
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for (field_name, elem_count) in &detail.nested {
|
||||||
|
ui.label(format!(" {} ({} elements)", field_name, elem_count));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InspectorPanel {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/gui/panels/mod.rs
Normal file
11
src/gui/panels/mod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
//! GUI panels for the Geolog application
|
||||||
|
|
||||||
|
pub mod browser;
|
||||||
|
pub mod console;
|
||||||
|
pub mod editor;
|
||||||
|
pub mod inspector;
|
||||||
|
|
||||||
|
pub use browser::BrowserPanel;
|
||||||
|
pub use console::ConsolePanel;
|
||||||
|
pub use editor::EditorPanel;
|
||||||
|
pub use inspector::InspectorPanel;
|
||||||
321
src/gui/state.rs
Normal file
321
src/gui/state.rs
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
//! GUI state management
|
||||||
|
//!
|
||||||
|
//! Wraps ReplState with GUI-specific state for the egui application.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::repl::{InstanceDetail, ReplState, TheoryDetail};
|
||||||
|
|
||||||
|
/// What item is currently selected in the browser
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum SelectedItem {
|
||||||
|
Theory(String),
|
||||||
|
Instance(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A message displayed in the console
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ConsoleMessage {
|
||||||
|
pub text: String,
|
||||||
|
pub kind: MessageKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum MessageKind {
|
||||||
|
Info,
|
||||||
|
Success,
|
||||||
|
Error,
|
||||||
|
Warning,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A recorded chase event for visualization
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChaseEventRecord {
|
||||||
|
pub iteration: usize,
|
||||||
|
pub axiom_index: usize,
|
||||||
|
pub axiom_name: Option<String>,
|
||||||
|
pub binding: Vec<(String, String)>, // (var_name, element_name)
|
||||||
|
pub changed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chase execution mode
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ChaseMode {
|
||||||
|
/// Not running
|
||||||
|
Idle,
|
||||||
|
/// Stepping through events
|
||||||
|
Stepping,
|
||||||
|
/// Running continuously
|
||||||
|
Running,
|
||||||
|
/// Chase completed
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for chase visualization
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChaseVisualizationState {
|
||||||
|
pub mode: ChaseMode,
|
||||||
|
pub instance_name: String,
|
||||||
|
pub events: Vec<ChaseEventRecord>,
|
||||||
|
pub current_event: usize,
|
||||||
|
pub current_iteration: usize,
|
||||||
|
pub breakpoints: HashSet<String>,
|
||||||
|
pub total_iterations: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaseVisualizationState {
|
||||||
|
pub fn new(instance_name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: ChaseMode::Idle,
|
||||||
|
instance_name,
|
||||||
|
events: Vec::new(),
|
||||||
|
current_event: 0,
|
||||||
|
current_iteration: 0,
|
||||||
|
breakpoints: HashSet::new(),
|
||||||
|
total_iterations: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_event(&mut self, event: ChaseEventRecord) {
|
||||||
|
self.events.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(&mut self) {
|
||||||
|
if self.current_event < self.events.len().saturating_sub(1) {
|
||||||
|
self.current_event += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_next(&self) -> bool {
|
||||||
|
self.current_event < self.events.len().saturating_sub(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Central view mode
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||||
|
pub enum CentralView {
|
||||||
|
#[default]
|
||||||
|
EditorAndInspector,
|
||||||
|
ChaseVisualization,
|
||||||
|
GraphVisualization,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GUI state wrapping ReplState
|
||||||
|
pub struct GuiState {
|
||||||
|
/// The underlying REPL state (theories, instances, store)
|
||||||
|
pub repl: ReplState,
|
||||||
|
|
||||||
|
/// Currently selected item in the browser
|
||||||
|
pub selected_item: Option<SelectedItem>,
|
||||||
|
|
||||||
|
/// Content of the code editor
|
||||||
|
pub editor_content: String,
|
||||||
|
|
||||||
|
/// Console output messages
|
||||||
|
pub console_messages: Vec<ConsoleMessage>,
|
||||||
|
|
||||||
|
/// Chase visualization state (if running)
|
||||||
|
pub chase_state: Option<ChaseVisualizationState>,
|
||||||
|
|
||||||
|
/// Graph view state
|
||||||
|
pub graph_instance: Option<String>,
|
||||||
|
pub graph_relation: Option<String>,
|
||||||
|
|
||||||
|
/// Currently loaded file path (if any)
|
||||||
|
pub current_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Current view mode for the central panel
|
||||||
|
pub central_view: CentralView,
|
||||||
|
|
||||||
|
/// Whether the browser tree sections are expanded
|
||||||
|
pub theories_expanded: bool,
|
||||||
|
pub instances_expanded: bool,
|
||||||
|
|
||||||
|
/// Inspector collapsible sections
|
||||||
|
pub inspector_sorts_expanded: bool,
|
||||||
|
pub inspector_functions_expanded: bool,
|
||||||
|
pub inspector_relations_expanded: bool,
|
||||||
|
pub inspector_axioms_expanded: bool,
|
||||||
|
pub inspector_elements_expanded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GuiState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuiState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
repl: ReplState::new(),
|
||||||
|
selected_item: None,
|
||||||
|
editor_content: String::new(),
|
||||||
|
console_messages: Vec::new(),
|
||||||
|
chase_state: None,
|
||||||
|
graph_instance: None,
|
||||||
|
graph_relation: None,
|
||||||
|
current_file: None,
|
||||||
|
central_view: CentralView::EditorAndInspector,
|
||||||
|
theories_expanded: true,
|
||||||
|
instances_expanded: true,
|
||||||
|
inspector_sorts_expanded: true,
|
||||||
|
inspector_functions_expanded: true,
|
||||||
|
inspector_relations_expanded: true,
|
||||||
|
inspector_axioms_expanded: true,
|
||||||
|
inspector_elements_expanded: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new GUI state with a workspace path for persistence
|
||||||
|
pub fn with_path(path: impl Into<PathBuf>) -> Self {
|
||||||
|
Self {
|
||||||
|
repl: ReplState::with_path(path),
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an info message to the console
|
||||||
|
pub fn log_info(&mut self, text: impl Into<String>) {
|
||||||
|
self.console_messages.push(ConsoleMessage {
|
||||||
|
text: text.into(),
|
||||||
|
kind: MessageKind::Info,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a success message to the console
|
||||||
|
pub fn log_success(&mut self, text: impl Into<String>) {
|
||||||
|
self.console_messages.push(ConsoleMessage {
|
||||||
|
text: text.into(),
|
||||||
|
kind: MessageKind::Success,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an error message to the console
|
||||||
|
pub fn log_error(&mut self, text: impl Into<String>) {
|
||||||
|
self.console_messages.push(ConsoleMessage {
|
||||||
|
text: text.into(),
|
||||||
|
kind: MessageKind::Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a warning message to the console
|
||||||
|
pub fn log_warning(&mut self, text: impl Into<String>) {
|
||||||
|
self.console_messages.push(ConsoleMessage {
|
||||||
|
text: text.into(),
|
||||||
|
kind: MessageKind::Warning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all console messages
|
||||||
|
pub fn clear_console(&mut self) {
|
||||||
|
self.console_messages.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the editor content as geolog code
|
||||||
|
pub fn execute_editor(&mut self) {
|
||||||
|
let source = self.editor_content.clone();
|
||||||
|
if source.trim().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.repl.execute_geolog(&source) {
|
||||||
|
Ok(results) => {
|
||||||
|
for result in results {
|
||||||
|
match result {
|
||||||
|
crate::repl::ExecuteResult::Namespace(name) => {
|
||||||
|
self.log_info(format!("Namespace: {}", name));
|
||||||
|
}
|
||||||
|
crate::repl::ExecuteResult::Theory {
|
||||||
|
name,
|
||||||
|
num_sorts,
|
||||||
|
num_functions,
|
||||||
|
num_relations,
|
||||||
|
num_axioms,
|
||||||
|
} => {
|
||||||
|
let mut parts = vec![format!("{} sorts", num_sorts)];
|
||||||
|
if num_functions > 0 {
|
||||||
|
parts.push(format!("{} functions", num_functions));
|
||||||
|
}
|
||||||
|
if num_relations > 0 {
|
||||||
|
parts.push(format!("{} relations", num_relations));
|
||||||
|
}
|
||||||
|
if num_axioms > 0 {
|
||||||
|
parts.push(format!("{} axioms", num_axioms));
|
||||||
|
}
|
||||||
|
self.log_success(format!(
|
||||||
|
"Defined theory {} ({})",
|
||||||
|
name,
|
||||||
|
parts.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
crate::repl::ExecuteResult::Instance {
|
||||||
|
name,
|
||||||
|
theory_name,
|
||||||
|
num_elements,
|
||||||
|
} => {
|
||||||
|
self.log_success(format!(
|
||||||
|
"Defined instance {} : {} ({} elements)",
|
||||||
|
name, theory_name, num_elements
|
||||||
|
));
|
||||||
|
}
|
||||||
|
crate::repl::ExecuteResult::Query(_result) => {
|
||||||
|
self.log_info("Query executed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.log_error(format!("Error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load a file into the editor
|
||||||
|
pub fn load_file(&mut self, path: &PathBuf) {
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => {
|
||||||
|
self.editor_content = content;
|
||||||
|
self.current_file = Some(path.clone());
|
||||||
|
self.log_info(format!("Loaded: {}", path.display()));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.log_error(format!("Failed to load {}: {}", path.display(), e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the currently selected theory detail
|
||||||
|
pub fn selected_theory_detail(&self) -> Option<TheoryDetail> {
|
||||||
|
if let Some(SelectedItem::Theory(name)) = &self.selected_item {
|
||||||
|
if let Some(crate::repl::InspectResult::Theory(detail)) = self.repl.inspect(name) {
|
||||||
|
return Some(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the currently selected instance detail
|
||||||
|
pub fn selected_instance_detail(&self) -> Option<InstanceDetail> {
|
||||||
|
if let Some(SelectedItem::Instance(name)) = &self.selected_item {
|
||||||
|
if let Some(crate::repl::InspectResult::Instance(detail)) = self.repl.inspect(name) {
|
||||||
|
return Some(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a chase visualization for an instance
|
||||||
|
pub fn start_chase(&mut self, instance_name: &str) {
|
||||||
|
self.chase_state = Some(ChaseVisualizationState::new(instance_name.to_string()));
|
||||||
|
self.central_view = CentralView::ChaseVisualization;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start graph visualization for an instance and relation
|
||||||
|
pub fn show_graph(&mut self, instance_name: &str, relation_name: Option<&str>) {
|
||||||
|
self.graph_instance = Some(instance_name.to_string());
|
||||||
|
self.graph_relation = relation_name.map(|s| s.to_string());
|
||||||
|
self.central_view = CentralView::GraphVisualization;
|
||||||
|
}
|
||||||
|
}
|
||||||
497
src/gui/visualizations/chase.rs
Normal file
497
src/gui/visualizations/chase.rs
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
//! Chase algorithm visualization
|
||||||
|
//!
|
||||||
|
//! Provides step-through debugging and visualization of the chase algorithm.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
use crate::core::RelationStorage;
|
||||||
|
use crate::gui::state::{ChaseEventRecord, ChaseMode, ChaseVisualizationState, CentralView, GuiState, SelectedItem};
|
||||||
|
use crate::query::chase::{ChaseControl, ChaseEvent, ChaseObserver};
|
||||||
|
|
||||||
|
/// Chase visualization panel
|
||||||
|
pub struct ChaseVisualization {
|
||||||
|
/// Breakpoint being added
|
||||||
|
new_breakpoint: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaseVisualization {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
new_breakpoint: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
// Header
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.heading("Chase Visualization");
|
||||||
|
|
||||||
|
if let Some(chase_state) = &state.chase_state {
|
||||||
|
ui.label(format!("Instance: {}", chase_state.instance_name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if state.chase_state.is_none() {
|
||||||
|
self.show_start_panel(ui, state);
|
||||||
|
} else {
|
||||||
|
self.show_chase_panel(ui, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_start_panel(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
ui.label("Select an instance to start chase visualization:");
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
let instances = state.repl.list_instances();
|
||||||
|
if instances.is_empty() {
|
||||||
|
ui.label("No instances defined. Create an instance first.");
|
||||||
|
} else {
|
||||||
|
for instance in instances {
|
||||||
|
if ui.button(format!("{} : {}", instance.name, instance.theory_name)).clicked() {
|
||||||
|
state.start_chase(&instance.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or use currently selected instance
|
||||||
|
if let Some(SelectedItem::Instance(name)) = &state.selected_item {
|
||||||
|
ui.add_space(16.0);
|
||||||
|
let name = name.clone();
|
||||||
|
if ui.button(format!("Start Chase on selected: {}", name)).clicked() {
|
||||||
|
state.start_chase(&name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_chase_panel(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
// Read mode and instance_name before mutable operations
|
||||||
|
let (mode, instance_name) = {
|
||||||
|
let chase_state = state.chase_state.as_ref().unwrap();
|
||||||
|
(chase_state.mode, chase_state.instance_name.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Control buttons
|
||||||
|
let mut step_clicked = false;
|
||||||
|
let mut run_clicked = false;
|
||||||
|
let mut stop_clicked = false;
|
||||||
|
let mut reset_clicked = false;
|
||||||
|
let mut close_clicked = false;
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Mode indicator
|
||||||
|
let mode_text = match mode {
|
||||||
|
ChaseMode::Idle => "Idle",
|
||||||
|
ChaseMode::Stepping => "Stepping",
|
||||||
|
ChaseMode::Running => "Running",
|
||||||
|
ChaseMode::Completed => "Completed",
|
||||||
|
};
|
||||||
|
ui.label(format!("Mode: {}", mode_text));
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Only show Run/Step if not completed
|
||||||
|
if mode != ChaseMode::Completed {
|
||||||
|
if ui.button("Step").clicked() {
|
||||||
|
step_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Run All").clicked() {
|
||||||
|
run_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Stop").clicked() {
|
||||||
|
stop_clicked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
if ui.button("Reset").clicked() {
|
||||||
|
reset_clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
if ui.button("Close").clicked() {
|
||||||
|
close_clicked = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle button actions
|
||||||
|
if step_clicked {
|
||||||
|
self.step_chase(state);
|
||||||
|
}
|
||||||
|
if run_clicked {
|
||||||
|
self.run_chase_to_completion(state);
|
||||||
|
}
|
||||||
|
if stop_clicked {
|
||||||
|
if let Some(chase_state) = &mut state.chase_state {
|
||||||
|
chase_state.mode = ChaseMode::Completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reset_clicked {
|
||||||
|
state.chase_state = Some(ChaseVisualizationState::new(instance_name));
|
||||||
|
}
|
||||||
|
if close_clicked {
|
||||||
|
state.chase_state = None;
|
||||||
|
state.central_view = CentralView::EditorAndInspector;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Main content area split into events and info
|
||||||
|
ui.columns(2, |columns| {
|
||||||
|
// Left column: Events
|
||||||
|
columns[0].heading("Events");
|
||||||
|
self.show_events_panel(&mut columns[0], state);
|
||||||
|
|
||||||
|
// Right column: Info and breakpoints
|
||||||
|
columns[1].heading("Information");
|
||||||
|
self.show_info_panel(&mut columns[1], state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_events_panel(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
egui::ScrollArea::vertical()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.stick_to_bottom(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
if let Some(chase_state) = &state.chase_state {
|
||||||
|
if chase_state.events.is_empty() {
|
||||||
|
ui.label("No events yet. Click 'Step' or 'Run All' to start.");
|
||||||
|
} else {
|
||||||
|
for (i, event) in chase_state.events.iter().enumerate() {
|
||||||
|
let is_current = i == chase_state.current_event;
|
||||||
|
let prefix = if is_current { ">" } else { " " };
|
||||||
|
|
||||||
|
let axiom_name = event.axiom_name.as_deref().unwrap_or("(unnamed)");
|
||||||
|
let binding_str: Vec<String> = event
|
||||||
|
.binding
|
||||||
|
.iter()
|
||||||
|
.map(|(var, val)| format!("{}={}", var, val))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let changed_marker = if event.changed { " [changed]" } else { "" };
|
||||||
|
|
||||||
|
let text = format!(
|
||||||
|
"{} [{}] #{} {} {{ {} }}{}",
|
||||||
|
prefix,
|
||||||
|
event.iteration,
|
||||||
|
event.axiom_index,
|
||||||
|
axiom_name,
|
||||||
|
binding_str.join(", "),
|
||||||
|
changed_marker
|
||||||
|
);
|
||||||
|
|
||||||
|
let color = if is_current {
|
||||||
|
egui::Color32::YELLOW
|
||||||
|
} else if event.changed {
|
||||||
|
egui::Color32::from_rgb(100, 200, 100)
|
||||||
|
} else {
|
||||||
|
egui::Color32::LIGHT_GRAY
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.label(egui::RichText::new(text).color(color).monospace());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_info_panel(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
// Read info from chase_state
|
||||||
|
let (current_iteration, total_iterations, events_count, breakpoints, instance_name) = {
|
||||||
|
if let Some(chase_state) = &state.chase_state {
|
||||||
|
(
|
||||||
|
chase_state.current_iteration,
|
||||||
|
chase_state.total_iterations,
|
||||||
|
chase_state.events.len(),
|
||||||
|
chase_state.breakpoints.iter().cloned().collect::<Vec<_>>(),
|
||||||
|
chase_state.instance_name.clone(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iteration info
|
||||||
|
ui.label(format!("Current iteration: {}", current_iteration));
|
||||||
|
if let Some(total) = total_iterations {
|
||||||
|
ui.label(format!("Total iterations: {}", total));
|
||||||
|
}
|
||||||
|
ui.label(format!("Events recorded: {}", events_count));
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
// Breakpoints section
|
||||||
|
ui.heading("Breakpoints");
|
||||||
|
|
||||||
|
// Add new breakpoint
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.text_edit_singleline(&mut self.new_breakpoint);
|
||||||
|
if ui.button("+").clicked() && !self.new_breakpoint.is_empty() {
|
||||||
|
if let Some(chase_state) = &mut state.chase_state {
|
||||||
|
chase_state.breakpoints.insert(self.new_breakpoint.clone());
|
||||||
|
}
|
||||||
|
self.new_breakpoint.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// List breakpoints
|
||||||
|
if breakpoints.is_empty() {
|
||||||
|
ui.label("(no breakpoints)");
|
||||||
|
} else {
|
||||||
|
let mut to_remove = None;
|
||||||
|
for bp in &breakpoints {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(format!(" {}", bp));
|
||||||
|
if ui.small_button("x").clicked() {
|
||||||
|
to_remove = Some(bp.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(bp) = to_remove {
|
||||||
|
if let Some(chase_state) = &mut state.chase_state {
|
||||||
|
chase_state.breakpoints.remove(&bp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
// Structure info
|
||||||
|
ui.heading("Structure");
|
||||||
|
if let Some(entry) = state.repl.instances.get(&instance_name) {
|
||||||
|
ui.label(format!("Elements: {}", entry.structure.len()));
|
||||||
|
|
||||||
|
let total_tuples: usize = entry.structure.relations.iter().map(|r| r.len()).sum();
|
||||||
|
ui.label(format!("Relation tuples: {}", total_tuples));
|
||||||
|
|
||||||
|
// Show relation breakdown
|
||||||
|
let theory_name = entry.theory_name.clone();
|
||||||
|
if let Some(theory) = state.repl.theories.get(&theory_name) {
|
||||||
|
for (rel_id, rel) in theory.theory.signature.relations.iter().enumerate() {
|
||||||
|
if rel_id < entry.structure.relations.len() {
|
||||||
|
let count = entry.structure.relations[rel_id].len();
|
||||||
|
if count > 0 {
|
||||||
|
ui.label(format!(" {}: {} tuples", rel.name, count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a single step of the chase
|
||||||
|
fn step_chase(&mut self, state: &mut GuiState) {
|
||||||
|
let instance_name = match &state.chase_state {
|
||||||
|
Some(s) => s.instance_name.clone(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let current_iteration = state.chase_state.as_ref().map(|s| s.current_iteration).unwrap_or(0);
|
||||||
|
|
||||||
|
// Get theory name first (immutable borrow)
|
||||||
|
let theory_name = match state.repl.instances.get(&instance_name) {
|
||||||
|
Some(e) => e.theory_name.clone(),
|
||||||
|
None => {
|
||||||
|
state.log_error(format!("Instance '{}' not found", instance_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let theory = match state.repl.theories.get(&theory_name) {
|
||||||
|
Some(t) => t.clone(),
|
||||||
|
None => {
|
||||||
|
state.log_error(format!("Theory '{}' not found", theory_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now get mutable access to entry
|
||||||
|
let entry = state.repl.instances.get_mut(&instance_name).unwrap();
|
||||||
|
|
||||||
|
// Create a GUI observer that records events
|
||||||
|
let mut observer = GuiChaseObserver {
|
||||||
|
events: Vec::new(),
|
||||||
|
slid_to_name: &entry.slid_to_name,
|
||||||
|
iteration: current_iteration,
|
||||||
|
step_mode: true,
|
||||||
|
steps_remaining: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run chase with observer (just one step)
|
||||||
|
let sig = &theory.theory.signature;
|
||||||
|
let axioms = &theory.theory.axioms;
|
||||||
|
let axiom_names = &theory.theory.axiom_names;
|
||||||
|
|
||||||
|
let result = crate::query::chase::chase_fixpoint_with_observer(
|
||||||
|
axioms,
|
||||||
|
axiom_names,
|
||||||
|
&mut entry.structure,
|
||||||
|
&mut state.repl.store.universe,
|
||||||
|
sig,
|
||||||
|
1, // Just one iteration
|
||||||
|
&mut observer,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Record events
|
||||||
|
let events = observer.events;
|
||||||
|
if let Some(chase_state) = &mut state.chase_state {
|
||||||
|
chase_state.mode = ChaseMode::Stepping;
|
||||||
|
for event in events {
|
||||||
|
chase_state.events.push(event);
|
||||||
|
}
|
||||||
|
chase_state.current_event = chase_state.events.len().saturating_sub(1);
|
||||||
|
chase_state.current_iteration += 1;
|
||||||
|
|
||||||
|
if let Ok(iterations) = result {
|
||||||
|
if iterations == 0 {
|
||||||
|
chase_state.mode = ChaseMode::Completed;
|
||||||
|
chase_state.total_iterations = Some(chase_state.current_iteration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log completion separately
|
||||||
|
if let Ok(iterations) = result {
|
||||||
|
if iterations == 0 {
|
||||||
|
let total = state.chase_state.as_ref().map(|s| s.current_iteration).unwrap_or(0);
|
||||||
|
state.log_success(format!("Chase completed after {} iterations", total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the chase to completion
|
||||||
|
fn run_chase_to_completion(&mut self, state: &mut GuiState) {
|
||||||
|
let instance_name = match &state.chase_state {
|
||||||
|
Some(s) => s.instance_name.clone(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get theory name first (immutable borrow)
|
||||||
|
let theory_name = match state.repl.instances.get(&instance_name) {
|
||||||
|
Some(e) => e.theory_name.clone(),
|
||||||
|
None => {
|
||||||
|
state.log_error(format!("Instance '{}' not found", instance_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let theory = match state.repl.theories.get(&theory_name) {
|
||||||
|
Some(t) => t.clone(),
|
||||||
|
None => {
|
||||||
|
state.log_error(format!("Theory '{}' not found", theory_name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now get mutable access to entry
|
||||||
|
let entry = state.repl.instances.get_mut(&instance_name).unwrap();
|
||||||
|
|
||||||
|
// Run chase to fixpoint
|
||||||
|
let sig = &theory.theory.signature;
|
||||||
|
let axioms = &theory.theory.axioms;
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let result = crate::query::chase::chase_fixpoint(
|
||||||
|
axioms,
|
||||||
|
&mut entry.structure,
|
||||||
|
&mut state.repl.store.universe,
|
||||||
|
sig,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(iterations) => {
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
if let Some(chase_state) = &mut state.chase_state {
|
||||||
|
chase_state.mode = ChaseMode::Completed;
|
||||||
|
chase_state.total_iterations = Some(iterations);
|
||||||
|
}
|
||||||
|
state.log_success(format!(
|
||||||
|
"Chase completed in {} iterations ({:.2}ms)",
|
||||||
|
iterations,
|
||||||
|
elapsed.as_secs_f64() * 1000.0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
state.log_error(format!("Chase error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChaseVisualization {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observer that records chase events for the GUI
|
||||||
|
struct GuiChaseObserver<'a> {
|
||||||
|
events: Vec<ChaseEventRecord>,
|
||||||
|
slid_to_name: &'a std::collections::HashMap<crate::id::Slid, String>,
|
||||||
|
iteration: usize,
|
||||||
|
step_mode: bool,
|
||||||
|
steps_remaining: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChaseObserver for GuiChaseObserver<'a> {
|
||||||
|
fn on_event(&mut self, event: ChaseEvent<'_>) -> ChaseControl {
|
||||||
|
match event {
|
||||||
|
ChaseEvent::IterationStart { iteration } => {
|
||||||
|
self.iteration = iteration;
|
||||||
|
ChaseControl::Continue
|
||||||
|
}
|
||||||
|
ChaseEvent::AxiomFiring {
|
||||||
|
axiom_index,
|
||||||
|
axiom_name,
|
||||||
|
binding,
|
||||||
|
violation_count: _,
|
||||||
|
} => {
|
||||||
|
// Record the event
|
||||||
|
let binding_vec: Vec<(String, String)> = binding
|
||||||
|
.iter()
|
||||||
|
.map(|(var, slid)| {
|
||||||
|
let elem_name = self
|
||||||
|
.slid_to_name
|
||||||
|
.get(slid)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| format!("#{}", slid));
|
||||||
|
(var.clone(), elem_name)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.events.push(ChaseEventRecord {
|
||||||
|
iteration: self.iteration,
|
||||||
|
axiom_index,
|
||||||
|
axiom_name: axiom_name.map(|s| s.to_string()),
|
||||||
|
binding: binding_vec,
|
||||||
|
changed: false, // Will be updated in AxiomFired
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.step_mode {
|
||||||
|
self.steps_remaining = self.steps_remaining.saturating_sub(1);
|
||||||
|
if self.steps_remaining == 0 {
|
||||||
|
ChaseControl::Stop
|
||||||
|
} else {
|
||||||
|
ChaseControl::Continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ChaseControl::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChaseEvent::AxiomFired { changed, .. } => {
|
||||||
|
// Update the last event with the changed status
|
||||||
|
if let Some(last) = self.events.last_mut() {
|
||||||
|
last.changed = changed;
|
||||||
|
}
|
||||||
|
ChaseControl::Continue
|
||||||
|
}
|
||||||
|
ChaseEvent::IterationEnd { .. } => ChaseControl::Continue,
|
||||||
|
ChaseEvent::ChaseComplete { .. } => ChaseControl::Continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
562
src/gui/visualizations/graph.rs
Normal file
562
src/gui/visualizations/graph.rs
Normal file
@ -0,0 +1,562 @@
|
|||||||
|
//! Relation graph visualization
|
||||||
|
//!
|
||||||
|
//! Displays elements as nodes and relation tuples as edges.
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::core::RelationStorage;
|
||||||
|
use crate::gui::state::{CentralView, GuiState};
|
||||||
|
use crate::id::{NumericId, Slid};
|
||||||
|
|
||||||
|
/// A node in the graph
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GraphNode {
|
||||||
|
pub id: Slid,
|
||||||
|
pub name: String,
|
||||||
|
pub sort: String,
|
||||||
|
pub position: egui::Pos2,
|
||||||
|
pub color: egui::Color32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An edge in the graph
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GraphEdge {
|
||||||
|
pub from: Slid,
|
||||||
|
pub to: Slid,
|
||||||
|
pub relation: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Graph view panel
|
||||||
|
pub struct GraphView {
|
||||||
|
/// Nodes in the graph
|
||||||
|
nodes: Vec<GraphNode>,
|
||||||
|
/// Edges in the graph
|
||||||
|
edges: Vec<GraphEdge>,
|
||||||
|
/// Node positions (for dragging)
|
||||||
|
node_positions: HashMap<Slid, egui::Pos2>,
|
||||||
|
/// Currently dragged node
|
||||||
|
dragged_node: Option<Slid>,
|
||||||
|
/// Zoom level
|
||||||
|
zoom: f32,
|
||||||
|
/// Pan offset
|
||||||
|
pan: egui::Vec2,
|
||||||
|
/// Selected relation filter
|
||||||
|
selected_relation: Option<String>,
|
||||||
|
/// Whether we need to recalculate layout
|
||||||
|
needs_layout: bool,
|
||||||
|
/// Last instance/relation we rendered
|
||||||
|
last_instance: Option<String>,
|
||||||
|
last_relation: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphView {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
nodes: Vec::new(),
|
||||||
|
edges: Vec::new(),
|
||||||
|
node_positions: HashMap::new(),
|
||||||
|
dragged_node: None,
|
||||||
|
zoom: 1.0,
|
||||||
|
pan: egui::Vec2::ZERO,
|
||||||
|
selected_relation: None,
|
||||||
|
needs_layout: true,
|
||||||
|
last_instance: None,
|
||||||
|
last_relation: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
// Header
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.heading("Graph Visualization");
|
||||||
|
|
||||||
|
if let Some(instance) = &state.graph_instance {
|
||||||
|
ui.label(format!("Instance: {}", instance));
|
||||||
|
}
|
||||||
|
if let Some(relation) = &state.graph_relation {
|
||||||
|
ui.label(format!("Relation: {}", relation));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Check if we need to rebuild the graph
|
||||||
|
if state.graph_instance != self.last_instance || state.graph_relation != self.last_relation {
|
||||||
|
self.rebuild_graph(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No instance selected
|
||||||
|
if state.graph_instance.is_none() {
|
||||||
|
self.show_selection_panel(ui, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Relation filter dropdown
|
||||||
|
if let Some(instance_name) = &state.graph_instance {
|
||||||
|
if let Some(entry) = state.repl.instances.get(instance_name) {
|
||||||
|
if let Some(theory) = state.repl.theories.get(&entry.theory_name) {
|
||||||
|
let relations: Vec<&str> = theory
|
||||||
|
.theory
|
||||||
|
.signature
|
||||||
|
.relations
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.name.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
ui.label("Relation:");
|
||||||
|
egui::ComboBox::from_label("")
|
||||||
|
.selected_text(
|
||||||
|
self.selected_relation
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("(all)"),
|
||||||
|
)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
if ui.selectable_label(self.selected_relation.is_none(), "(all)").clicked() {
|
||||||
|
self.selected_relation = None;
|
||||||
|
self.needs_layout = true;
|
||||||
|
}
|
||||||
|
for rel in relations {
|
||||||
|
if ui.selectable_label(
|
||||||
|
self.selected_relation.as_deref() == Some(rel),
|
||||||
|
rel,
|
||||||
|
).clicked() {
|
||||||
|
self.selected_relation = Some(rel.to_string());
|
||||||
|
self.needs_layout = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("Reset Layout").clicked() {
|
||||||
|
self.needs_layout = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.button("Fit").clicked() {
|
||||||
|
self.fit_to_view(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoom controls
|
||||||
|
ui.label("Zoom:");
|
||||||
|
if ui.button("-").clicked() {
|
||||||
|
self.zoom = (self.zoom - 0.1).max(0.1);
|
||||||
|
}
|
||||||
|
ui.label(format!("{:.0}%", self.zoom * 100.0));
|
||||||
|
if ui.button("+").clicked() {
|
||||||
|
self.zoom = (self.zoom + 0.1).min(3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
|
if ui.button("Close").clicked() {
|
||||||
|
state.graph_instance = None;
|
||||||
|
state.graph_relation = None;
|
||||||
|
state.central_view = CentralView::EditorAndInspector;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
// Apply layout if needed
|
||||||
|
if self.needs_layout {
|
||||||
|
self.apply_layout(ui);
|
||||||
|
self.needs_layout = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the graph
|
||||||
|
self.draw_graph(ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_selection_panel(&mut self, ui: &mut egui::Ui, state: &mut GuiState) {
|
||||||
|
ui.label("Select an instance to visualize:");
|
||||||
|
ui.add_space(8.0);
|
||||||
|
|
||||||
|
let instances = state.repl.list_instances();
|
||||||
|
if instances.is_empty() {
|
||||||
|
ui.label("No instances defined.");
|
||||||
|
} else {
|
||||||
|
for instance in instances {
|
||||||
|
if ui.button(format!("{} : {}", instance.name, instance.theory_name)).clicked() {
|
||||||
|
state.graph_instance = Some(instance.name.clone());
|
||||||
|
self.needs_layout = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(16.0);
|
||||||
|
if ui.button("Back to Editor").clicked() {
|
||||||
|
state.central_view = CentralView::EditorAndInspector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebuild_graph(&mut self, state: &GuiState) {
|
||||||
|
self.nodes.clear();
|
||||||
|
self.edges.clear();
|
||||||
|
self.node_positions.clear();
|
||||||
|
|
||||||
|
let instance_name = match &state.graph_instance {
|
||||||
|
Some(name) => name,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let entry = match state.repl.instances.get(instance_name) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let theory = match state.repl.theories.get(&entry.theory_name) {
|
||||||
|
Some(t) => t,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let sig = &theory.theory.signature;
|
||||||
|
|
||||||
|
// Create nodes for each element
|
||||||
|
let sort_colors = generate_sort_colors(sig.sorts.len());
|
||||||
|
|
||||||
|
for (slid_idx, &sort_id) in entry.structure.sorts.iter().enumerate() {
|
||||||
|
let slid = Slid::from_usize(slid_idx);
|
||||||
|
let name = entry
|
||||||
|
.slid_to_name
|
||||||
|
.get(&slid)
|
||||||
|
.map(|s| s.clone())
|
||||||
|
.unwrap_or_else(|| format!("#{}", slid_idx));
|
||||||
|
|
||||||
|
let sort_name = sig.sorts.get(sort_id).cloned().unwrap_or_default();
|
||||||
|
let color = sort_colors.get(sort_id).copied().unwrap_or(egui::Color32::GRAY);
|
||||||
|
|
||||||
|
self.nodes.push(GraphNode {
|
||||||
|
id: slid,
|
||||||
|
name,
|
||||||
|
sort: sort_name,
|
||||||
|
position: egui::Pos2::ZERO, // Will be set by layout
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create edges for relations
|
||||||
|
for (rel_id, relation) in entry.structure.relations.iter().enumerate() {
|
||||||
|
if relation.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel_name = sig
|
||||||
|
.relations
|
||||||
|
.get(rel_id)
|
||||||
|
.map(|r| r.name.clone())
|
||||||
|
.unwrap_or_else(|| format!("rel#{}", rel_id));
|
||||||
|
|
||||||
|
// Filter by selected relation
|
||||||
|
if let Some(selected) = &self.selected_relation {
|
||||||
|
if &rel_name != selected {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for tuple in relation.iter() {
|
||||||
|
if tuple.len() >= 2 {
|
||||||
|
// Binary or higher-arity relation: draw edge from first to last
|
||||||
|
self.edges.push(GraphEdge {
|
||||||
|
from: tuple[0],
|
||||||
|
to: tuple[tuple.len() - 1],
|
||||||
|
relation: rel_name.clone(),
|
||||||
|
label: if tuple.len() > 2 {
|
||||||
|
Some(format!("via {} elements", tuple.len() - 2))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if tuple.len() == 1 {
|
||||||
|
// Unary relation: self-loop (or just highlight the node)
|
||||||
|
self.edges.push(GraphEdge {
|
||||||
|
from: tuple[0],
|
||||||
|
to: tuple[0],
|
||||||
|
relation: rel_name.clone(),
|
||||||
|
label: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_instance = state.graph_instance.clone();
|
||||||
|
self.last_relation = state.graph_relation.clone();
|
||||||
|
self.needs_layout = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_layout(&mut self, ui: &egui::Ui) {
|
||||||
|
let available = ui.available_size();
|
||||||
|
let center = egui::Pos2::new(available.x / 2.0, available.y / 2.0);
|
||||||
|
|
||||||
|
if self.nodes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple circular layout
|
||||||
|
let radius = (available.x.min(available.y) / 2.0 - 50.0).max(100.0);
|
||||||
|
let count = self.nodes.len();
|
||||||
|
|
||||||
|
for (i, node) in self.nodes.iter_mut().enumerate() {
|
||||||
|
let angle = (i as f32 / count as f32) * std::f32::consts::TAU;
|
||||||
|
let pos = egui::Pos2::new(
|
||||||
|
center.x + radius * angle.cos(),
|
||||||
|
center.y + radius * angle.sin(),
|
||||||
|
);
|
||||||
|
node.position = pos;
|
||||||
|
self.node_positions.insert(node.id, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fit_to_view(&mut self, ui: &egui::Ui) {
|
||||||
|
if self.nodes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate bounding box
|
||||||
|
let mut min_x = f32::MAX;
|
||||||
|
let mut min_y = f32::MAX;
|
||||||
|
let mut max_x = f32::MIN;
|
||||||
|
let mut max_y = f32::MIN;
|
||||||
|
|
||||||
|
for node in &self.nodes {
|
||||||
|
min_x = min_x.min(node.position.x);
|
||||||
|
min_y = min_y.min(node.position.y);
|
||||||
|
max_x = max_x.max(node.position.x);
|
||||||
|
max_y = max_y.max(node.position.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = max_x - min_x;
|
||||||
|
let height = max_y - min_y;
|
||||||
|
let available = ui.available_size();
|
||||||
|
|
||||||
|
// Calculate zoom to fit
|
||||||
|
let zoom_x = if width > 0.0 { (available.x - 100.0) / width } else { 1.0 };
|
||||||
|
let zoom_y = if height > 0.0 { (available.y - 100.0) / height } else { 1.0 };
|
||||||
|
self.zoom = zoom_x.min(zoom_y).clamp(0.1, 2.0);
|
||||||
|
|
||||||
|
// Calculate pan to center
|
||||||
|
let center_x = (min_x + max_x) / 2.0;
|
||||||
|
let center_y = (min_y + max_y) / 2.0;
|
||||||
|
self.pan = egui::Vec2::new(
|
||||||
|
available.x / 2.0 - center_x * self.zoom,
|
||||||
|
available.y / 2.0 - center_y * self.zoom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_graph(&mut self, ui: &mut egui::Ui) {
|
||||||
|
let (response, painter) = ui.allocate_painter(
|
||||||
|
ui.available_size(),
|
||||||
|
egui::Sense::click_and_drag(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let rect = response.rect;
|
||||||
|
|
||||||
|
// Handle panning
|
||||||
|
if response.dragged() && self.dragged_node.is_none() {
|
||||||
|
self.pan += response.drag_delta();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle zooming with scroll
|
||||||
|
if response.hovered() {
|
||||||
|
let scroll = ui.input(|i| i.raw_scroll_delta.y);
|
||||||
|
if scroll != 0.0 {
|
||||||
|
let old_zoom = self.zoom;
|
||||||
|
self.zoom = (self.zoom + scroll * 0.001).clamp(0.1, 3.0);
|
||||||
|
|
||||||
|
// Zoom toward mouse position
|
||||||
|
if let Some(pos) = response.hover_pos() {
|
||||||
|
let rel_pos = pos - rect.min;
|
||||||
|
let zoom_factor = self.zoom / old_zoom;
|
||||||
|
self.pan = rel_pos - (rel_pos - self.pan) * zoom_factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform helper
|
||||||
|
let transform = |pos: egui::Pos2| -> egui::Pos2 {
|
||||||
|
egui::Pos2::new(
|
||||||
|
rect.min.x + pos.x * self.zoom + self.pan.x,
|
||||||
|
rect.min.y + pos.y * self.zoom + self.pan.y,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Draw edges first (behind nodes)
|
||||||
|
for edge in &self.edges {
|
||||||
|
let from_pos = self.node_positions.get(&edge.from);
|
||||||
|
let to_pos = self.node_positions.get(&edge.to);
|
||||||
|
|
||||||
|
if let (Some(&from), Some(&to)) = (from_pos, to_pos) {
|
||||||
|
let from_screen = transform(from);
|
||||||
|
let to_screen = transform(to);
|
||||||
|
|
||||||
|
if edge.from == edge.to {
|
||||||
|
// Self-loop
|
||||||
|
let offset = 20.0 * self.zoom;
|
||||||
|
let loop_center = egui::Pos2::new(from_screen.x, from_screen.y - offset);
|
||||||
|
painter.circle_stroke(
|
||||||
|
loop_center,
|
||||||
|
10.0 * self.zoom,
|
||||||
|
egui::Stroke::new(1.5, egui::Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Regular edge
|
||||||
|
painter.line_segment(
|
||||||
|
[from_screen, to_screen],
|
||||||
|
egui::Stroke::new(1.5, egui::Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw arrowhead
|
||||||
|
let dir = (to_screen - from_screen).normalized();
|
||||||
|
let arrow_size = 8.0 * self.zoom;
|
||||||
|
let node_radius = 20.0 * self.zoom;
|
||||||
|
let arrow_tip = to_screen - dir * node_radius;
|
||||||
|
let perp = egui::Vec2::new(-dir.y, dir.x);
|
||||||
|
|
||||||
|
let p1 = arrow_tip;
|
||||||
|
let p2 = arrow_tip - dir * arrow_size + perp * arrow_size * 0.5;
|
||||||
|
let p3 = arrow_tip - dir * arrow_size - perp * arrow_size * 0.5;
|
||||||
|
|
||||||
|
painter.add(egui::Shape::convex_polygon(
|
||||||
|
vec![p1, p2, p3],
|
||||||
|
egui::Color32::LIGHT_GRAY,
|
||||||
|
egui::Stroke::NONE,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
let node_radius = 20.0 * self.zoom;
|
||||||
|
|
||||||
|
for node in &self.nodes {
|
||||||
|
let pos = self.node_positions.get(&node.id).copied().unwrap_or(node.position);
|
||||||
|
let screen_pos = transform(pos);
|
||||||
|
|
||||||
|
// Check if node is being hovered or dragged
|
||||||
|
let node_rect = egui::Rect::from_center_size(
|
||||||
|
screen_pos,
|
||||||
|
egui::Vec2::splat(node_radius * 2.0),
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_hovered = response.hovered()
|
||||||
|
&& ui.input(|i| i.pointer.hover_pos())
|
||||||
|
.map(|p| node_rect.contains(p))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// Handle node dragging
|
||||||
|
if is_hovered && response.drag_started() {
|
||||||
|
self.dragged_node = Some(node.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.dragged_node == Some(node.id) {
|
||||||
|
if response.dragged() {
|
||||||
|
let delta = response.drag_delta() / self.zoom;
|
||||||
|
if let Some(pos) = self.node_positions.get_mut(&node.id) {
|
||||||
|
*pos += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if response.drag_stopped() {
|
||||||
|
self.dragged_node = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw node circle
|
||||||
|
let fill_color = if is_hovered || self.dragged_node == Some(node.id) {
|
||||||
|
node.color.linear_multiply(1.3)
|
||||||
|
} else {
|
||||||
|
node.color
|
||||||
|
};
|
||||||
|
|
||||||
|
painter.circle_filled(screen_pos, node_radius, fill_color);
|
||||||
|
painter.circle_stroke(
|
||||||
|
screen_pos,
|
||||||
|
node_radius,
|
||||||
|
egui::Stroke::new(2.0, egui::Color32::WHITE),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw node label
|
||||||
|
let font_size = (12.0 * self.zoom).max(8.0);
|
||||||
|
painter.text(
|
||||||
|
screen_pos,
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
&node.name,
|
||||||
|
egui::FontId::proportional(font_size),
|
||||||
|
egui::Color32::WHITE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw sort label below
|
||||||
|
if is_hovered {
|
||||||
|
painter.text(
|
||||||
|
screen_pos + egui::Vec2::new(0.0, node_radius + 5.0),
|
||||||
|
egui::Align2::CENTER_TOP,
|
||||||
|
&node.sort,
|
||||||
|
egui::FontId::proportional(10.0 * self.zoom),
|
||||||
|
egui::Color32::LIGHT_GRAY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show info panel
|
||||||
|
if self.nodes.is_empty() {
|
||||||
|
painter.text(
|
||||||
|
rect.center(),
|
||||||
|
egui::Align2::CENTER_CENTER,
|
||||||
|
"No elements to display",
|
||||||
|
egui::FontId::proportional(16.0),
|
||||||
|
egui::Color32::GRAY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GraphView {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate distinct colors for each sort
|
||||||
|
fn generate_sort_colors(count: usize) -> Vec<egui::Color32> {
|
||||||
|
let mut colors = Vec::with_capacity(count);
|
||||||
|
|
||||||
|
for i in 0..count {
|
||||||
|
let hue = (i as f32 / count as f32) * 360.0;
|
||||||
|
let (r, g, b) = hsv_to_rgb(hue, 0.6, 0.8);
|
||||||
|
colors.push(egui::Color32::from_rgb(r, g, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
colors
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert HSV to RGB
|
||||||
|
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
|
||||||
|
let c = v * s;
|
||||||
|
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
|
||||||
|
let m = v - c;
|
||||||
|
|
||||||
|
let (r, g, b) = if h < 60.0 {
|
||||||
|
(c, x, 0.0)
|
||||||
|
} else if h < 120.0 {
|
||||||
|
(x, c, 0.0)
|
||||||
|
} else if h < 180.0 {
|
||||||
|
(0.0, c, x)
|
||||||
|
} else if h < 240.0 {
|
||||||
|
(0.0, x, c)
|
||||||
|
} else if h < 300.0 {
|
||||||
|
(x, 0.0, c)
|
||||||
|
} else {
|
||||||
|
(c, 0.0, x)
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
((r + m) * 255.0) as u8,
|
||||||
|
((g + m) * 255.0) as u8,
|
||||||
|
((b + m) * 255.0) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/gui/visualizations/mod.rs
Normal file
7
src/gui/visualizations/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//! Visualization components for the Geolog GUI
|
||||||
|
|
||||||
|
pub mod chase;
|
||||||
|
pub mod graph;
|
||||||
|
|
||||||
|
pub use chase::ChaseVisualization;
|
||||||
|
pub use graph::GraphView;
|
||||||
@ -27,6 +27,9 @@ pub mod universe;
|
|||||||
pub mod version;
|
pub mod version;
|
||||||
pub mod zerocopy;
|
pub mod zerocopy;
|
||||||
|
|
||||||
|
#[cfg(feature = "gui")]
|
||||||
|
pub mod gui;
|
||||||
|
|
||||||
pub use ast::*;
|
pub use ast::*;
|
||||||
pub use lexer::lexer;
|
pub use lexer::lexer;
|
||||||
pub use parser::parser;
|
pub use parser::parser;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user