Start working on gui

This commit is contained in:
Michael Mikovsky
2025-11-26 08:55:07 -07:00
parent b43f2f5181
commit 0a3e3d9765
12 changed files with 4834 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
use crate::flowchart::FlowChart;
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct TemplateApp {
tab: Tab,
#[serde(skip)]
flowchart: FlowChart,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub enum Tab {
Flowchart,
Test,
}
impl Default for TemplateApp {
fn default() -> Self {
Self {
tab: Tab::Flowchart,
// Example stuff:
// label: "Hello World!".to_owned(),
// value: 2.7,
flowchart: FlowChart::new(),
}
}
}
impl TemplateApp {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// This is also where you can customize the look and feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if let Some(storage) = cc.storage {
eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
} else {
Default::default()
}
}
}
impl eframe::App for TemplateApp {
/// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
/// Called each time the UI needs repainting, which may be many times per second.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`.
// For inspiration and more examples, go to https://emilk.github.io/egui
egui::TopBottomPanel::top("tab_panel").show(ctx, |ui| {
// The top panel is often a good place for a menu bar:
egui::MenuBar::new()
// .style(StyleModifier::new(|s| s.visuals))
.ui(ui, |ui| {
if ui
.menu_button("Network", |ui| if ui.button("Quit").clicked() {})
.response
.clicked()
{
self.tab = Tab::Flowchart;
};
if ui
.menu_button("Test", |ui| if ui.button("Quit").clicked() {})
.response
.clicked()
{
self.tab = Tab::Test;
};
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
egui::widgets::global_theme_preference_switch(ui);
});
});
});
egui::TopBottomPanel::bottom("tab_panel").show(ctx, |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(format!("UnShell UI {}", env!("CARGO_PKG_VERSION")));
egui::warn_if_debug_build(ui);
});
});
// egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
// // The top panel is often a good place for a menu bar:
// egui::MenuBar::new().ui(ui, |ui| {
// if ui
// .menu_button("Network", |ui| if ui.button("Quit").clicked() {})
// .response
// .clicked()
// {
// self.tab = Tab::Flowchart;
// };
// if ui
// .menu_button("Test", |ui| if ui.button("Quit").clicked() {})
// .response
// .clicked()
// {
// self.tab = Tab::Test;
// };
// });
// });
egui::CentralPanel::default().show(ctx, |ui| {
match self.tab {
Tab::Flowchart => {
self.flowchart.paint(ui);
}
Tab::Test => {
// The central panel the region left after adding TopPanel's and SidePanel's
ui.heading("eframe template");
ui.horizontal(|ui| {
ui.label("Write something: ");
// ui.text_edit_singleline(&mut self.label);
});
// ui.add(egui::Slider::new(&mut self.value, 0.0..=10.0).text("value"));
// if ui.button("Increment").clicked() {
// self.value += 1.0;
// }
ui.separator();
ui.add(egui::github_link_file!(
"https://github.com/emilk/eframe_template/blob/main/",
"Source code."
));
}
}
});
}
}
+109
View File
@@ -0,0 +1,109 @@
use egui::Ui;
use egui::{Color32, Painter, Pos2, Rect, Stroke, UiBuilder, Vec2};
const TARGET_LINE_GAP: f32 = 80.;
const BG_STROKE: Stroke = Stroke {
width: 0.3,
color: Color32::GRAY,
};
pub struct FlowChart {
// frame: Frame,
container: DraggableContainer,
}
impl FlowChart {
pub fn new() -> Self {
Self {
container: DraggableContainer::new(
Pos2 { x: 100., y: 100. },
Vec2 { x: 100., y: 100. },
),
}
}
fn paint_bg(&self, rect: &Rect, painter: &Painter) {
let h_count = (rect.width() / TARGET_LINE_GAP).round() as usize;
let h_spacing = rect.width() / h_count as f32;
for n in 0..h_count {
painter.vline(rect.min.x + n as f32 * h_spacing, rect.y_range(), BG_STROKE);
}
let v_count = (rect.height() / TARGET_LINE_GAP).round() as usize;
let v_spacing = rect.height() / v_count as f32;
for n in 0..v_count {
painter.hline(rect.x_range(), rect.min.y + n as f32 * v_spacing, BG_STROKE);
}
}
pub fn paint(&mut self, ui: &mut Ui) {
self.paint_bg(&ui.clip_rect(), ui.painter());
self.container.show(ui, |ui, rect| {
ui.painter().rect(
// ui.top
*rect,
0.,
Color32::PURPLE,
BG_STROKE,
egui::StrokeKind::Middle,
);
ui.label("Tests");
ui.button("Test");
})
}
}
pub struct DraggableContainer {
pub pos: egui::Pos2,
pub size: egui::Vec2,
is_dragging: bool,
drag_offset: egui::Vec2,
}
impl DraggableContainer {
pub fn new(pos: egui::Pos2, size: egui::Vec2) -> Self {
Self {
pos,
size,
is_dragging: false,
drag_offset: egui::Vec2::ZERO,
}
}
pub fn show<R>(
&mut self,
ui: &mut egui::Ui,
add_contents: impl FnOnce(&mut egui::Ui, &Rect) -> R,
) -> R {
let rect = egui::Rect::from_min_size(self.pos, self.size);
// Handle dragging logic
let response = ui.interact(rect, ui.id().with("drag_area"), egui::Sense::drag());
if response.drag_started() {
self.is_dragging = true;
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
self.drag_offset = self.pos - pointer_pos;
}
}
if response.dragged() && self.is_dragging {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
self.pos = pointer_pos + self.drag_offset;
}
}
if response.drag_stopped() {
self.is_dragging = false;
}
// Create a child UI at the specified position
// let mut child_ui = ui.child_ui(rect, egui::Layout::top_down(egui::Align::LEFT), None);
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(rect));
// Add contents
add_contents(&mut child_ui, &rect)
}
}
+6
View File
@@ -0,0 +1,6 @@
#![warn(clippy::all, rust_2018_idioms)]
mod app;
pub use app::TemplateApp;
mod flowchart;
+74
View File
@@ -0,0 +1,74 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use unshell_gui::TemplateApp;
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result {
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
let native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([400.0, 300.0])
.with_min_inner_size([300.0, 220.0]),
// .with_icon(
// // NOTE: Adding an icon is optional
// eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..])
// .expect("Failed to load icon"),
// ),
..Default::default()
};
eframe::run_native(
"eframe template",
native_options,
Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
)
}
// When compiling to web using trunk:
#[cfg(target_arch = "wasm32")]
fn main() {
use eframe::wasm_bindgen::JsCast as _;
// Redirect `log` message to `console.log` and friends:
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
let web_options = eframe::WebOptions::default();
wasm_bindgen_futures::spawn_local(async {
let document = web_sys::window()
.expect("No window")
.document()
.expect("No document");
let canvas = document
.get_element_by_id("the_canvas_id")
.expect("Failed to find the_canvas_id")
.dyn_into::<web_sys::HtmlCanvasElement>()
.expect("the_canvas_id was not a HtmlCanvasElement");
let start_result = eframe::WebRunner::new()
.start(
canvas,
web_options,
Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
)
.await;
// Remove the loading text and spinner:
if let Some(loading_text) = document.get_element_by_id("loading_text") {
match start_result {
Ok(_) => {
loading_text.remove();
}
Err(e) => {
loading_text.set_inner_html(
"<p> The app has crashed. See the developer console for details. </p>",
);
panic!("Failed to start eframe: {e:?}");
}
}
}
});
}