From 8ce480cbd40cc8cfdc6ba44f8c3c96742514206f Mon Sep 17 00:00:00 2001 From: Ling Wang Date: Mon, 23 Sep 2024 02:56:20 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Basic=20UI=20window=20frame?= =?UTF-8?q?work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/code_editor.rs | 110 ++++++++++++++++++++++++++ src/ui/main.rs | 174 ++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 10 +++ src/ui/pyenv.rs | 54 +++++++++++++ tests/ui_test.rs | 34 +++++++++ 5 files changed, 382 insertions(+) create mode 100644 src/ui/code_editor.rs create mode 100644 src/ui/main.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/pyenv.rs create mode 100644 tests/ui_test.rs diff --git a/src/ui/code_editor.rs b/src/ui/code_editor.rs new file mode 100644 index 0000000..5be5400 --- /dev/null +++ b/src/ui/code_editor.rs @@ -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"); diff --git a/src/ui/main.rs b/src/ui/main.rs new file mode 100644 index 0000000..76e8f01 --- /dev/null +++ b/src/ui/main.rs @@ -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> { + 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::::default())), + )?; + + Ok(AppUi {}) + } +} + +struct SubWindowHolder { + window: Box, + id: Id, + title: String, + open: bool, +} + +struct MyApp { + sub_window_creator: Vec>, // We ensure that the sub windows only work in the main thread + sub_windows: Vec, + sub_window_idx: usize, +} + +impl Default for MyApp { + fn default() -> Self { + let mut sub_window_creator = Vec::new(); + for c in inventory::iter:: { + 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; +} + +/// 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 { + 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; + +#[doc(hidden)] +pub struct SubWindowCollector { + pub create: SubWindowCreatorCreator, +} + +inventory::collect!(SubWindowCollector); diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..e6f58cd --- /dev/null +++ b/src/ui/mod.rs @@ -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; \ No newline at end of file diff --git a/src/ui/pyenv.rs b/src/ui/pyenv.rs new file mode 100644 index 0000000..73564a6 --- /dev/null +++ b/src/ui/pyenv.rs @@ -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, + locals: Py, +} + +impl Default for PyEnv { + fn default() -> Self { + static GLOBALS: LazyLock> = 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); + } + }); + } +} diff --git a/tests/ui_test.rs b/tests/ui_test.rs new file mode 100644 index 0000000..f419466 --- /dev/null +++ b/tests/ui_test.rs @@ -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 (); +}