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
+2
View File
@@ -4,5 +4,7 @@
target/
.DS_Store
dist/
# These are backup files generated by rustfmt
**/*.rs.bk
+4020
View File
File diff suppressed because it is too large Load Diff
+263
View File
@@ -0,0 +1,263 @@
[package]
name = "unshell-gui"
version = "0.1.0"
edition = "2024"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
include = ["LICENSE-APACHE", "LICENSE-MIT", "**/*.rs", "Cargo.toml"]
rust-version = "1.88"
[package.metadata.docs.rs]
all-features = true
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[dependencies]
egui = "0.33.0"
eframe = { version = "0.33.0", default-features = false, features = [
"accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies.
"default_fonts", # Embed the default egui fonts.
"glow", # Use the glow rendering backend. Alternative: "wgpu".
"persistence", # Enable restoring app state when restarting the app.
"wayland", # To support Linux (and CI)
"x11", # To support older Linux distributions (restores one of the default features)
] }
log = "0.4.27"
# You only need serde if you want app persistence:
serde = { version = "1.0.219", features = ["derive"] }
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
env_logger = "0.11.8"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen-futures = "0.4.50"
web-sys = "0.3.70" # to access the DOM (to hide the loading text)
[profile.release]
opt-level = 2 # fast and small wasm
# Optimize all dependencies even in debug builds:
[profile.dev.package."*"]
opt-level = 2
[patch.crates-io]
# If you want to use the bleeding edge version of egui and eframe:
# egui = { git = "https://github.com/emilk/egui", branch = "main" }
# eframe = { git = "https://github.com/emilk/egui", branch = "main" }
# If you fork https://github.com/emilk/egui you can test with:
# egui = { path = "../egui/crates/egui" }
# eframe = { path = "../egui/crates/eframe" }
# ----------------------------------------------------------------------------------------
# Lints:
[lints]
workspace = true
[workspace.lints.rust]
unsafe_code = "deny"
elided_lifetimes_in_paths = "warn"
future_incompatible = { level = "warn", priority = -1 }
nonstandard_style = { level = "warn", priority = -1 }
rust_2018_idioms = { level = "warn", priority = -1 }
rust_2021_prelude_collisions = "warn"
semicolon_in_expressions_from_macros = "warn"
trivial_numeric_casts = "warn"
unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_lifetimes = "warn"
trivial_casts = "allow"
unused_qualifications = "allow"
[workspace.lints.rustdoc]
all = "warn"
missing_crate_level_docs = "warn"
[workspace.lints.clippy]
allow_attributes = "warn"
as_ptr_cast_mut = "warn"
await_holding_lock = "warn"
bool_to_int_with_if = "warn"
branches_sharing_code = "warn"
char_lit_as_u8 = "warn"
checked_conversions = "warn"
clear_with_drain = "warn"
cloned_instead_of_copied = "warn"
dbg_macro = "warn"
debug_assert_with_mut_call = "warn"
default_union_representation = "warn"
derive_partial_eq_without_eq = "warn"
disallowed_macros = "warn" # See clippy.toml
disallowed_methods = "warn" # See clippy.toml
disallowed_names = "warn" # See clippy.toml
disallowed_script_idents = "warn" # See clippy.toml
disallowed_types = "warn" # See clippy.toml
doc_comment_double_space_linebreaks = "warn"
doc_link_with_quotes = "warn"
doc_markdown = "warn"
elidable_lifetime_names = "warn"
empty_enum = "warn"
empty_enum_variants_with_brackets = "warn"
empty_line_after_outer_attr = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
exit = "warn"
expl_impl_clone_on_copy = "warn"
explicit_deref_methods = "warn"
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
fallible_impl_from = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
float_cmp_const = "warn"
fn_params_excessive_bools = "warn"
fn_to_numeric_cast_any = "warn"
from_iter_instead_of_collect = "warn"
get_unwrap = "warn"
if_let_mutex = "warn"
ignore_without_reason = "warn"
implicit_clone = "warn"
implied_bounds_in_impls = "warn"
imprecise_flops = "warn"
inconsistent_struct_constructor = "warn"
index_refutable_slice = "warn"
indexing_slicing = "warn"
inefficient_to_string = "warn"
infinite_loop = "warn"
into_iter_without_iter = "warn"
invalid_upcast_comparisons = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
iter_not_returning_iterator = "warn"
iter_on_empty_collections = "warn"
iter_on_single_items = "warn"
iter_over_hash_type = "warn"
iter_without_into_iter = "warn"
large_digit_groups = "warn"
large_include_file = "warn"
large_stack_arrays = "warn"
large_stack_frames = "warn"
large_types_passed_by_value = "warn"
let_underscore_must_use = "warn"
let_underscore_untyped = "warn"
let_unit_value = "warn"
linkedlist = "warn"
literal_string_with_formatting_args = "warn"
lossy_float_literal = "warn"
macro_use_imports = "warn"
manual_assert = "warn"
manual_clamp = "warn"
manual_instant_elapsed = "warn"
manual_is_power_of_two = "warn"
manual_is_variant_and = "warn"
manual_let_else = "warn"
manual_midpoint = "warn"
manual_ok_or = "warn"
manual_string_new = "warn"
map_err_ignore = "warn"
map_flatten = "warn"
match_bool = "warn"
match_same_arms = "warn"
match_wild_err_arm = "warn"
match_wildcard_for_single_variants = "warn"
mem_forget = "warn"
mismatching_type_param_order = "warn"
missing_assert_message = "warn"
missing_enforced_import_renames = "warn"
missing_errors_doc = "warn"
missing_safety_doc = "warn"
mixed_attributes_style = "warn"
mut_mut = "warn"
mutex_integer = "warn"
needless_borrow = "warn"
needless_continue = "warn"
needless_for_each = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
negative_feature_names = "warn"
non_std_lazy_statics = "warn"
non_zero_suggestions = "warn"
nonstandard_macro_braces = "warn"
option_as_ref_cloned = "warn"
option_option = "warn"
path_buf_push_overwrite = "warn"
pathbuf_init_then_push = "warn"
precedence_bits = "warn"
print_stderr = "warn"
print_stdout = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
pub_underscore_fields = "warn"
pub_without_shorthand = "warn"
rc_mutex = "warn"
readonly_write_lock = "warn"
redundant_type_annotations = "warn"
ref_as_ptr = "warn"
ref_option_ref = "warn"
ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn"
same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn"
set_contains_or_insert = "warn"
should_panic_without_expect = "warn"
single_char_pattern = "warn"
single_match_else = "warn"
single_option_map = "warn"
str_split_at_newline = "warn"
str_to_string = "warn"
string_add = "warn"
string_add_assign = "warn"
string_lit_as_bytes = "warn"
string_lit_chars_any = "warn"
string_to_string = "warn"
suspicious_command_arg_space = "warn"
suspicious_xor_used_as_pow = "warn"
todo = "warn"
too_long_first_doc_paragraph = "warn"
too_many_lines = "warn"
trailing_empty_array = "warn"
trait_duplication_in_bounds = "warn"
transmute_ptr_to_ptr = "warn"
tuple_array_conversions = "warn"
unchecked_duration_subtraction = "warn"
undocumented_unsafe_blocks = "warn"
unimplemented = "warn"
uninhabited_references = "warn"
uninlined_format_args = "warn"
unnecessary_box_returns = "warn"
unnecessary_debug_formatting = "warn"
unnecessary_literal_bound = "warn"
unnecessary_safety_comment = "warn"
unnecessary_safety_doc = "warn"
unnecessary_self_imports = "warn"
unnecessary_semicolon = "warn"
unnecessary_struct_initialization = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
unused_peekable = "warn"
unused_rounding = "warn"
unused_self = "warn"
unused_trait_names = "warn"
unwrap_used = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
useless_transmute = "warn"
verbose_file_reads = "warn"
wildcard_dependencies = "warn"
wildcard_imports = "warn"
zero_sized_map_values = "warn"
manual_range_contains = "allow" # this is better on 'allow'
map_unwrap_or = "allow" # this is better on 'allow'
+2
View File
@@ -0,0 +1,2 @@
[build]
filehash = false
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+11
View File
@@ -0,0 +1,11 @@
{
"name": "egui Template PWA",
"short_name": "egui-template-pwa",
"icons": [],
"lang": "en-US",
"id": "/index.html",
"start_url": "./index.html",
"display": "standalone",
"background_color": "white",
"theme_color": "white"
}
+25
View File
@@ -0,0 +1,25 @@
var cacheName = "egui-template-pwa";
var filesToCache = [
"./",
"./index.html",
"./eframe_template.js",
"./eframe_template_bg.wasm",
];
/* Start the service worker and cache all of the app's content */
self.addEventListener("install", function (e) {
e.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll(filesToCache);
}),
);
});
/* Serve cached content when offline */
self.addEventListener("fetch", function (e) {
e.respondWith(
caches.match(e.request).then(function (response) {
return response || fetch(e.request);
}),
);
});
+178
View File
@@ -0,0 +1,178 @@
<!doctype html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<head>
<!-- change this to your project name -->
<title>eframe template</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<link data-trunk rel="rust" data-wasm-opt="2" />
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base data-trunk-public-url />
<link data-trunk rel="icon" href="assets/favicon.ico" />
<link data-trunk rel="copy-file" href="assets/sw.js" />
<link data-trunk rel="copy-file" href="assets/manifest.json" />
<!--<link
data-trunk
rel="copy-file"
href="assets/icon-1024.png"
data-target-path="assets"
/>
<link
data-trunk
rel="copy-file"
href="assets/icon-256.png"
data-target-path="assets"
/>
<link
data-trunk
rel="copy-file"
href="assets/icon_ios_touch_192.png"
data-target-path="assets"
/>
<link
data-trunk
rel="copy-file"
href="assets/maskable_icon_x512.png"
data-target-path="assets"
/>-->
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="assets/icon_ios_touch_192.png" />
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content="white"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="#404040"
/>
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Make canvas fill entire document: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<!-- the loading spinner will be removed in main.rs -->
<div class="centered" id="loading_text">
<noscript>You need javascript to use this website</noscript>
<p style="font-size: 16px">Loading…</p>
<div class="lds-dual-ring"></div>
</div>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if (
"serviceWorker" in navigator &&
window.location.hash !== "#dev"
) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("sw.js");
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->
+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:?}");
}
}
}
});
}