Skip to content

Commit

Permalink
✨ feat: Basic UI window framework
Browse files Browse the repository at this point in the history
  • Loading branch information
wychlw committed Sep 22, 2024
1 parent db8288e commit 8ce480c
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 0 deletions.
110 changes: 110 additions & 0 deletions src/ui/code_editor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::{fs::File, io::Write};

use eframe::egui::{Context, Id, ScrollArea, TextEdit, TextStyle, Ui, Window};
use egui_extras::syntax_highlighting::{highlight, CodeTheme};

use crate::{err, impl_sub_window};

use super::{main::SubWindow, pyenv::PyEnv};

pub struct CodeEditor {
code: String,
save_to: String,
pyenv: PyEnv,
}

impl Default for CodeEditor {
fn default() -> Self {
Self {
code: "\
from tester import *
print('Hello, world!')
".to_string(),
save_to: "".to_string(),
pyenv: PyEnv::default(),
}
}
}

impl CodeEditor {
fn editor(&mut self, ui: &mut Ui) {
let mut layout = |ui: &Ui, string: &str, width: f32| {
let theme = CodeTheme::from_style(ui.style());
let mut job = highlight(ui.ctx(), &theme, string, "Python");
job.wrap.max_width = width;
ui.fonts(|f| f.layout_job(job))
};

ScrollArea::vertical().show(ui, |ui| {
ui.add(
TextEdit::multiline(&mut self.code)
.font(TextStyle::Monospace)
.code_editor()
.desired_rows(25)
.lock_focus(true)
.desired_width(f32::INFINITY)
.layouter(&mut layout),
)
});
}
fn run_code(&mut self) {
self.pyenv.run_code(&self.code);
}
fn save_code(&mut self, ui: &mut Ui) {
if ui.button("Write to file").clicked() {
println!("Write to file: {}", self.save_to);
let f = File::options()
.append(true)
.create(true)
.open(&self.save_to);
match f {
Ok(mut file) => {
let e = file.write_all(self.code.as_bytes());
if let Err(e) = e {
err!("Write to file error: {}", e);
} else {
self.code.clear();
}
}
Err(e) => {
err!("Open file error: {}", e);
}
}
}
}
fn bottom_butt(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
if ui.button("Run").clicked() {
self.run_code();
}
self.save_code(ui);
});
}
fn bottom(&mut self, ui: &mut Ui) {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label("Save to:");
ui.text_edit_singleline(&mut self.save_to);
});
self.bottom_butt(ui);
});
}
fn show(&mut self, ui: &mut Ui) {
ui.vertical(|ui| {
self.editor(ui);
self.bottom(ui);
});
}
}

impl SubWindow for CodeEditor {
fn show(&mut self, ctx: &Context, title: &str, id: &Id, open: &mut bool) {
let window = Window::new(title).id(id.to_owned()).open(open).resizable([true, true]);
window.show(ctx, |ui| {
self.show(ui);
});
}
}

impl_sub_window!(CodeEditor, "CodeEditor");
174 changes: 174 additions & 0 deletions src/ui/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! Main UI render for the APP
use std::error::Error;

use eframe::{
egui::{Context, Id, SidePanel, Ui, ViewportBuilder},
run_native, App, Frame, NativeOptions,
};

use crate::info;

/// Main UI struct
///
/// NOTICE! NOTICE! This will block the main thread. If you have any other tasks to do, please run them in a separate thread.
/// Or, use IPC to communicate with the UI process.
#[derive(Default)]
pub struct AppUi {}

impl AppUi {
pub fn new() -> Result<Self, Box<dyn Error>> {
let options = NativeOptions {
viewport: ViewportBuilder::default()
.with_title("AutoTestor")
.with_inner_size([800.0, 600.0]),
..Default::default()
};

run_native(
"AutoTestor",
options,
Box::new(|_cc| Ok(Box::<MyApp>::default())),
)?;

Ok(AppUi {})
}
}

struct SubWindowHolder {
window: Box<dyn SubWindow>,
id: Id,
title: String,
open: bool,
}

struct MyApp {
sub_window_creator: Vec<Box<DynSubWindowCreator>>, // We ensure that the sub windows only work in the main thread
sub_windows: Vec<SubWindowHolder>,
sub_window_idx: usize,
}

impl Default for MyApp {
fn default() -> Self {
let mut sub_window_creator = Vec::new();
for c in inventory::iter::<SubWindowCollector> {
let f = c.create;
let w = f();
sub_window_creator.push(w);
}
Self {
sub_window_creator,
sub_windows: Vec::new(),
sub_window_idx: 0,
}
}
}

impl MyApp {
fn sub_window_pannel(&mut self, ctx: &Context, ui: &mut Ui) {
ui.label("SubWindow Panel");
ui.vertical(|ui| {
for creator in &self.sub_window_creator {
let name = creator.name();
if ui.button(name).clicked() {
let title = format!("{}: {}", name, self.sub_window_idx);
let id = Id::new(self.sub_window_idx);
info!("Try create sub window: {}", title);
self.sub_window_idx += 1;
self.sub_windows.push(SubWindowHolder {
window: creator.open(),
id,
title,
open: true,
});
}
}
self.sub_windows.retain(|w| w.open);
for w in &mut self.sub_windows {
w.window.show(ctx, &w.title, &w.id, &mut w.open);
}
});
}
}

impl App for MyApp {
fn update(&mut self, ctx: &Context, frame: &mut Frame) {
let _ = frame;
SidePanel::right("SubWindow Panel")
.default_width(200.0)
.show(ctx, |ui| {
self.sub_window_pannel(ctx, ui);
});
}
}

/// SubWindow trait
///
/// This trait is used to hold the sub window.
/// This is because the egui is a immediate mode UI. when you display a new window, it will be shown THAT particular frame. if you need that window to stay, you need to create a new window AGAIN next frame too and egui using the window's name (or other id source), egui will internally keep track of its position / focus status etc..
/// So, we need some way to keep track of the sub window.
pub trait SubWindow {
/// Show the window, this will be called every frame. Your window is identified by the `id` parameter.
/// However, that doesn't mean you should change the title, as this contains the window number, useful for the user.
fn show(&mut self, ctx: &Context, title: &str, id: &Id, open: &mut bool);
}

pub trait SubWindowCreator {
fn name(&self) -> &str;
fn open(&self) -> Box<dyn SubWindow>;
}

/// Snippet to register a sub window
///
/// # Arguments
/// $name: The struct name of the sub window
/// $window_name: The name of the window, will become the title of the window
///
/// # Example
/// `impl_sub_window!(TestUiStruct, "TestUiName");`
/// where TestUiStruct implements SubWindow trait and Default trait
///
/// # Notice
/// If you found rust-analyzer gives "invalid metavariable expression", this is a nightly feature, you can ignore it. It will work.
/// The problem is on `${concat()}` macro. Just suppress it.
#[macro_export]
macro_rules! impl_sub_window {
($name:ident, $window_name:literal) => {
struct ${concat($name, Creator)} {}

impl $crate::ui::main::SubWindowCreator for ${concat($name, Creator)} {
fn name(&self) -> &str {
$window_name
}
fn open(&self) -> Box<dyn $crate::ui::main::SubWindow> {
Box::new($name::default())
}
}

#[allow(non_snake_case)]
fn ${concat(create_, $name)}() -> Box<$crate::ui::main::DynSubWindowCreator> {
Box::new(${concat($name, Creator)} {})
}

inventory::submit! {
$crate::ui::main::SubWindowCollector {
create: ${concat(create_, $name)},
}
}
};
}

/// Type should return from the creator.
#[doc(hidden)]
pub type DynSubWindowCreator = dyn SubWindowCreator + Send + Sync + 'static;

#[doc(hidden)]
/// We need to use a function to create the sub window creator on start time.
pub type SubWindowCreatorCreator = fn() -> Box<DynSubWindowCreator>;

#[doc(hidden)]
pub struct SubWindowCollector {
pub create: SubWindowCreatorCreator,
}

inventory::collect!(SubWindowCollector);
10 changes: 10 additions & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Parts to handle the UI render part
//!
//! If only use CLI feats, this part can be ignored;
//! however, GUI part may need this part show what's going on.
//! Or, use to create needle for GUI part.
//!
pub mod main;
pub mod code_editor;
pub mod pyenv;
54 changes: 54 additions & 0 deletions src/ui/pyenv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::{collections::HashMap, sync::LazyLock};

use pyo3::{
ffi::{c_str, PyImport_AddModule, PyModule_GetDict},
prepare_freethreaded_python,
types::{IntoPyDict, PyAnyMethods, PyDict, PyDictMethods},
Bound, Py, Python,
};

use crate::{err, info};

pub struct PyEnv {
globals: Py<PyDict>,
locals: Py<PyDict>,
}

impl Default for PyEnv {
fn default() -> Self {
static GLOBALS: LazyLock<Py<PyDict>> = LazyLock::new(|| {
prepare_freethreaded_python();
Python::with_gil(|py| unsafe {
let mptr = PyImport_AddModule(c_str!("__main__").as_ptr());
if mptr.is_null() {
panic!("Failed to get __main__ module");
}
let globals = PyModule_GetDict(mptr);
Py::from_owned_ptr(py, globals)
})
});
prepare_freethreaded_python();
Python::with_gil(|py| {
let globals = GLOBALS.clone_ref(py);
globals.bind(py).set_item("__virt__", 1).unwrap(); // Force to copy globals dict, otherwise drop one PyEnv will affect others
let locals = unsafe {
let mptr = globals.as_ptr();
Py::from_owned_ptr(py, mptr)
};
Self { globals, locals }
})
}
}

impl PyEnv {
pub fn run_code(&mut self, code: &str) {
Python::with_gil(|py| {
let globals = self.globals.bind(py);
let locals = self.locals.bind(py);
let e = py.run_bound(code, Some(globals), Some(locals));
if let Err(e) = e {
err!("Run code error: {}", e);
}
});
}
}
34 changes: 34 additions & 0 deletions tests/ui_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#![feature(macro_metavar_expr_concat)]

use tester::{
impl_sub_window,
ui::main::{AppUi, SubWindow},
};

use eframe::egui::{Context, Id, Window};

pub struct TestUi {}

impl Default for TestUi {
fn default() -> Self {
Self {}
}
}

impl SubWindow for TestUi {
fn show(&mut self, ctx: &Context, title: &str, id: &Id, open: &mut bool) {
Window::new(title)
.id(id.to_owned())
.open(open)
.show(ctx, |ui| {
ui.label("TestUi");
});
}
}

impl_sub_window!(TestUi, "TestUi");

fn main() {
let _ui = AppUi::new().unwrap();
return ();
}

0 comments on commit 8ce480c

Please sign in to comment.