From aa9d68f16fa5eb4dc1eed916d25d15837ce81d4e Mon Sep 17 00:00:00 2001 From: Leo dev Date: Sun, 17 Nov 2024 17:01:48 +0100 Subject: [PATCH] updated a few things --- Cargo.toml | 3 +- osui-element/Cargo.toml | 13 +++ osui-element/lib.rs | 125 +++++++++++++++++++++++ src/app.c | 6 +- src/app.rs | 29 +++++- src/lib.rs | 76 +++++++------- src/macros.rs | 215 +++------------------------------------- src/main.rs | 10 +- src/ui/elements.rs | 9 -- src/ui/mod.rs | 91 +++++++++++++++-- src/utils.rs | 25 ++--- 11 files changed, 325 insertions(+), 277 deletions(-) create mode 100644 osui-element/Cargo.toml create mode 100644 osui-element/lib.rs delete mode 100644 src/ui/elements.rs diff --git a/Cargo.toml b/Cargo.toml index f13b9de..f113075 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,5 @@ path = "src/main.rs" crossterm = "0.28.1" regex = "*" lazy_static = "1.4" -paste = "1.0.15" \ No newline at end of file +paste = "1.0.15" +osui-element = { path = "./osui-element" } \ No newline at end of file diff --git a/osui-element/Cargo.toml b/osui-element/Cargo.toml new file mode 100644 index 0000000..e0ebe52 --- /dev/null +++ b/osui-element/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "osui-element" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true +path = "./lib.rs" + +[dependencies] +quote = "1.0" +syn = { version = "2.0", features = ["full"] } +proc-macro2 = "*" \ No newline at end of file diff --git a/osui-element/lib.rs b/osui-element/lib.rs new file mode 100644 index 0000000..7d44047 --- /dev/null +++ b/osui-element/lib.rs @@ -0,0 +1,125 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse::Parser, parse_macro_input, DeriveInput, Fields, LifetimeParam}; + +#[proc_macro_attribute] +pub fn element(_args: TokenStream, input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let struct_name = &ast.ident; + + match &mut ast.data { + syn::Data::Struct(ref mut struct_data) => { + if ast.generics.lifetimes().count() == 0 { + ast.generics + .params + .push(syn::GenericParam::Lifetime(LifetimeParam::new( + syn::Lifetime::new("'a", proc_macro2::Span::call_site()), + ))); + } + + if let Fields::Named(fields) = &mut struct_data.fields { + // Example fields, modify according to your needs + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub x: Value }) + .unwrap(), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub y: usize }) + .unwrap(), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub width: Value }) + .unwrap(), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub height: Value }) + .unwrap(), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub children: Children }) + .unwrap(), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub id: &'a str }) + .unwrap(), + ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub class: &'a str }) + .unwrap(), + ); + } + } + _ => panic!("`element` can only be used with structs"), + } + + let expanded_impl = quote! { + impl<'a> ElementComponent for #struct_name<'a> { + fn get_data(&self) -> (Value, usize, String) { + (self.x, self.y, self.id.to_string()) + } + fn update_data(&mut self, width: usize, height: usize) { + self.width.try_set_value(width); + self.height.try_set_value(height); + if let Children::Children(children, _) = &mut self.children { + for child in children { + child.update_data(width, height); + } + } + } + fn get_element_by_id(&mut self, id: &str) -> Option<&mut Box> { + if let Children::Children(children, _) = &mut self.children { + for elem in children { + if elem.get_data().2 == id { + return Some(elem); + } + } + } + None + } + fn get_child(&mut self) -> Option<&mut Box> { + if let Children::Children(children, child) = &mut self.children { + children.get_mut(*child) + } else { + None + } + } + } + }; + + let expanded = quote! { + #ast + #expanded_impl + }; + + expanded.into() +} + +#[proc_macro_attribute] +pub fn elem_fn(_args: TokenStream, input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let struct_name = &ast.ident; + let func_name = syn::Ident::new( + &struct_name.to_string().to_lowercase(), + proc_macro2::Span::call_site(), + ); + + let elem_fn = quote! { + pub fn #func_name<'a>() -> Box<#struct_name<'a>> { + Box::new(#struct_name::default()) + } + }; + + let expanded = quote! { + #ast + #elem_fn + }; + + expanded.into() +} diff --git a/src/app.c b/src/app.c index 313dd75..8677ed4 100644 --- a/src/app.c +++ b/src/app.c @@ -11,6 +11,7 @@ typedef struct { extern void render(void* element, uint32_t state); extern void event(void* element, uint32_t* state); +extern void init_event(void* element, uint32_t* state); void sleep_ms(long milliseconds) { struct timespec ts; @@ -22,6 +23,7 @@ void sleep_ms(long milliseconds) { void *event_checker(void *args) { EventArgs* event_args = (EventArgs*)args; + init_event(event_args->element, event_args->state); while (1) { if (event_args->state == 0) { return NULL; } event(event_args->element, event_args->state); @@ -32,7 +34,7 @@ void *event_checker(void *args) boolean_t c_run(void* element) { pthread_t thread; - uint32_t state = 2; + uint32_t state = 3; EventArgs event_args = { .element = element, .state = &state }; if (pthread_create(&thread, NULL, event_checker, &event_args) != 0) { @@ -49,6 +51,6 @@ boolean_t c_run(void* element) return 1; } render(element, state); - sleep_ms(200); + sleep_ms(20); } } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index f4d887a..dd3a4e8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,9 @@ -use crossterm::terminal::enable_raw_mode; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; -use crate::{clear, create_frame, flush, get_term_size, render_to_frame, Element, State, StateChanger, Value}; +use crate::{ + utils::{clear, create_frame, flush, get_term_size, hide_cursor, render_to_frame, show_cursor}, Element, + State, StateChanger, +}; use std::ffi::c_void; pub struct ElementWrapper<'a> { @@ -27,7 +30,7 @@ extern "C" { pub extern "C" fn render(ptr: *mut c_void, state: u32) { let element = unsafe { ElementWrapper::from_raw(ptr) }; let (width, height) = get_term_size(); - let mut frame: Vec = create_frame(Value::new(width), Value::new(height)); + let mut frame: Vec = create_frame(width, height); render_to_frame(State::from_u32(state), width, &mut frame, element); clear(); print!("{}", frame.join("")); @@ -38,13 +41,31 @@ pub extern "C" fn render(ptr: *mut c_void, state: u32) { pub extern "C" fn event(ptr: *mut c_void, state: *mut u32) { let event = crossterm::event::read().unwrap(); let element = unsafe { ElementWrapper::from_raw(ptr) }; + if let crossterm::event::Event::Resize(width, height) = event { + element.update_data(width as usize, height as usize); + } + element.event(event, &StateChanger(state)); +} + +#[no_mangle] +pub extern "C" fn init_event(ptr: *mut c_void, state: *mut u32) { + let event = crossterm::event::Event::FocusGained; + let element = unsafe { ElementWrapper::from_raw(ptr) }; + let (width, height) = get_term_size(); + element.update_data(width, height); element.event(event, &StateChanger(state)); } pub fn run(data: &mut Box) -> bool { + hide_cursor(); enable_raw_mode().unwrap(); + clear(); unsafe { let element_ptr = ElementWrapper::new(data); - return c_run(element_ptr); + let c = c_run(element_ptr); + show_cursor(); + disable_raw_mode().unwrap(); + clear(); + return c; } } diff --git a/src/lib.rs b/src/lib.rs index b8c7c60..34f8671 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,20 +16,15 @@ //! ``` //! //! ## Modules -//! - `utils` - Utility functions for common TUI tasks such as clearing the screen. +//! - `app` - The main function for rendering. //! - `ui` - Contains all user interface components, enabling rich CLI experiences. +//! - `utils` - Utility functions for common TUI tasks such as clearing the screen. +pub mod app; pub mod macros; pub mod ui; pub mod utils; -pub use utils::*; -pub mod app; - -/// Enum representing the wether it's the default or custom. -/// -/// - `Default(usize)` - Uses a default value. -/// - `Custom(usize)` - Allows specifying a custom value. #[derive(Debug, Clone, Copy)] pub enum Value { Default(T), @@ -37,20 +32,6 @@ pub enum Value { } impl Value { - /// Creates a new Value. - /// - /// # Returns - /// - /// Value::Custom(usize) - pub fn new(value: T) -> Value { - Value::Custom(value) - } - - /// Retrieves the value. - /// - /// # Returns - /// - /// The value by the type T. pub fn get_value(&self) -> T { match self { Value::Custom(s) => *s, @@ -58,13 +39,6 @@ impl Value { } } - /// Attempts to set the value if it is currently set to `Default`. - /// - /// If the size is `Custom`, this function will not modify it. - /// - /// # Arguments - /// - /// * `value` - The value to change to. pub fn try_set_value(&mut self, value: T) { if let Value::Default(_) = *self { *self = Value::Default(value); @@ -72,16 +46,20 @@ impl Value { } } -/// A trait for defining UI elements in OSUI. -/// -/// Elements must implement methods for updating and rendering data. -pub trait Element: Send + Sync { +impl Default for Value { + fn default() -> Self { + Self::Default(T::default()) + } +} + +pub trait ElementComponent: Send + Sync { fn get_data(&self) -> (Value, usize, String); - fn update_data(&mut self, width: usize, height: usize); - fn get_element_by_id(&mut self, id: &str) -> Option<&mut Box>; + fn get_child(&mut self) -> Option<&mut Box>; +} +pub trait Element: std::fmt::Debug + Send + Sync + ElementComponent { fn render(&self, state: State) -> String { _ = state; String::new() @@ -92,18 +70,18 @@ pub trait Element: Send + Sync { } } -pub struct Handler(pub Box); +pub struct Handler(pub Box); impl Handler { pub fn new(handler_fn: F) -> Handler where - F: FnMut(&T, &StateChanger, crossterm::event::Event) + 'static + Send, + F: FnMut(&T, crossterm::event::Event, &StateChanger) + 'static + Send, { Handler(Box::new(handler_fn)) } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum State { Exit, Rebuild, @@ -145,3 +123,25 @@ impl StateChanger { State::from_u32(unsafe { *self.0 }) } } + +#[derive(Debug)] +pub enum Children { + None, + Text(String), + Children(Vec>, usize), +} + +impl Default for Children { + fn default() -> Self { + Self::None + } +} + +impl Children { + pub fn is_none(&self) -> bool { + match self { + Children::None => true, + _ => false, + } + } +} diff --git a/src/macros.rs b/src/macros.rs index d2d58d5..89e55b9 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,180 +1,3 @@ -//! # Macros Module -//! -//! This module defines various macros for creating and manipulating OSUI elements. -//! The macros provide a clean and concise syntax for defining UI components and -//! handling their parameters, commands, and rendering. - -/// Macro for defining an OSUI `Element`. -/// -/// This macro generates a new element type with specified parameters and default values. -/// -/// # Example -/// ``` -/// element! { -/// MyElem {} -/// defaults {} -/// // functions (render, update) -/// } -/// ``` -/// -/// # Parameters -/// - `name`: The name of the element type being defined. -/// - `inner`: Additional fields or methods for the element. -/// - `defaults`: Default values for the element's properties. -/// - `functions`: Functions to implement for the element (e.g., rendering, updating). -#[macro_export] -macro_rules! element { - ( - $(#[$meta_style:meta])* - style $style:ident { - $($sn:ident: $st:ty),*$(,)? - } - $(#[$meta:meta])* - $name:ident { - $( $inner:tt )* - } - defaults {$( $defaults:tt )*} - $( $functions:tt )* - ) => { - $(#[$meta_style])* - #[derive(Clone)] - pub struct $style { - pub color: Color, - pub background: Color, - pub font: Font, - pub hover_color: Color, - pub hover_background: Color, - pub hover_font: Font, - $(pub $sn: $st),* - } - - impl Default for $style { - fn default() -> $style { - $style { - color: Color::default(), - background: Color::default(), - font: Font::default(), - hover_color: Color::default(), - hover_background: Color::default(), - hover_font: Font::default(), - $($sn: <$st>::default()),* - } - } - } - - element! { - $(#[$meta])* - $name { - pub style: $style, - $( $inner )* - } - defaults { - style: $style::default(), - $($defaults)* - } - $( $functions )* - } - }; - ( - $(#[$meta:meta])* - $name:ident { - $( $inner:tt )* - } - defaults {$( $defaults:tt )*} - $( $functions:tt )* - ) => { - $(#[$meta])* - pub struct $name<'a> { - pub x: $crate::Value, - pub y: usize, - pub width: $crate::Value, - pub height: $crate::Value, - pub children: Vec>, - pub child: usize, - pub text: String, - pub id: &'a str, - $( $inner )* - } - - impl $crate::Element for $name<'_> { - fn get_data(&self) -> ($crate::Value, usize, String) { - (self.x, self.y, self.id.to_string()) - } - - fn update_data(&mut self, width: usize, height: usize) { - self.width.try_set_value(width); - self.height.try_set_value(height); - for child in &mut self.children { - child.update_data(width, height); - } - } - - fn get_element_by_id(&mut self, id: &str) -> Option<&mut Box> { - for elem in &mut self.children { - if elem.get_data().2 == id { - return Some(elem); - } - } - None - } - - $( $functions )* - } - - impl<'a> $name<'_> { - /// Creates a new instance of the element with default values. - pub fn new() -> $name<'a> { - $name { - children: Vec::new(), - x: $crate::Value::Default(0), - y: 0, - width: $crate::Value::Default(0), - height: $crate::Value::Default(0), - child: 0, - text: String::new(), - id: "", - $( $defaults )* - } - } - - /// Retrieves a mutable reference to the current child element, if any. - /// - /// # Returns - /// An `Option` containing a mutable reference to the child `Element` or `None`. - pub fn get_child(&mut self) -> Option<&mut Box> { - self.children.get_mut(self.child) - } - } - }; -} - -#[macro_export] -macro_rules! def_ { - ($name:ident { $($k:ident : $v:expr),* $(,)? }) => { - $name { - $( - $k : std::sync::Mutex::new($v) - ),* - } - }; -} - -/// Macro for creating a command response from a list of commands. -/// -/// # Example -/// ``` -/// command!(Update(1), Render(2)); -/// ``` -/// -/// # Parameters -/// - A list of commands to be included in the `EventResponse::CommandList`. -#[macro_export] -macro_rules! command { - ($($cmd:expr),*) => { - EventResponse::CommandList(vec![$($cmd),*]) - }; -} - /// Macro for parsing parameters and children for an OSUI element. /// /// This macro updates the properties of an element based on provided key-value pairs, @@ -219,30 +42,39 @@ macro_rules! parse_rsx_param { // For loop ($elem:expr, for ($($for:tt)*) { $($inner:tt)* } $($rest:tt)*) => { - for $($for)* { - $elem.children.push({$($inner)*}) + if $elem.children.is_none() {$elem.children = $crate::Children::Children(Vec::new(), 0)} + if let $crate::Children::Children(children, _) = &mut $elem.children { + for $($for)* { + children.push({$($inner)*}) + } } osui::parse_rsx_param!($elem, $($rest)*); }; // Expression ($elem:expr, {$ielem:expr} $($rest:tt)*) => { - $elem.children.push($ielem); + if $elem.children.is_none() {$elem.children = $crate::Children::Children(Vec::new(), 0)} + if let $crate::Children::Children(children, _) = &mut $elem.children { + children.push({$ielem}); + } osui::parse_rsx_param!($elem, $($rest)*); }; // Element ($elem:expr, $pelem:path { $($inner:tt)* } $($rest:tt)*) => { - $elem.children.push(osui::rsx_elem!($pelem { $($inner)* })); + if $elem.children.is_none() {$elem.children = $crate::Children::Children(Vec::new(), 0)} + if let $crate::Children::Children(children, _) = &mut $elem.children { + children.push(osui::rsx_elem!($pelem { $($inner)* })); + } osui::parse_rsx_param!($elem, $($rest)*) }; // Text ($elem:expr, $text:expr) => { - $elem.text = format!($text); + if $elem.children.is_none() {$elem.children = $crate::Children::Text(format!($text))} }; ($elem:expr, $text:expr, $($inner:tt)*) => { - $elem.text = format!($text, $($inner)*); + if $elem.children.is_none() {$elem.children = $crate::Children::Text(format!($text, $($inner)*))} }; // Empty @@ -309,7 +141,7 @@ macro_rules! rsx_elem_raw { #[macro_export] macro_rules! rsx { ($($inner:tt)*) => {{ - $crate::rsx_elem_raw!( $crate::ui::div { $($inner)* } ) + $crate::rsx_elem!( $crate::ui::div { $($inner)* } ) }}; } @@ -335,21 +167,6 @@ macro_rules! css { }}; } -/// Macro for defining a `Arc>` -#[macro_export] -macro_rules! mux { - ($a:expr) => { - std::sync::Mutex::new($a) - }; -} - -#[macro_export] -macro_rules! arc { - ($a:expr) => { - std::sync::Arc::new(std::sync::Mutex::new($a)) - }; -} - /// Macro for defining a `Value< T >` #[macro_export] macro_rules! val { diff --git a/src/main.rs b/src/main.rs index 7a0b653..986a1d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,11 @@ -use osui::{app::run, rsx_elem, ui::text}; +use osui::{app::run, rsx, ui::*, Element}; fn main() { - run(&mut rsx_elem! { text { "Hello, World!" } }); + while run(&mut app()) {} +} + +fn app() -> Box { + rsx! { + text { "Hello, World!" } + } } diff --git a/src/ui/elements.rs b/src/ui/elements.rs deleted file mode 100644 index dcca735..0000000 --- a/src/ui/elements.rs +++ /dev/null @@ -1,9 +0,0 @@ -use crate::{element, State}; - -element! { - Text {} - defaults {} - fn render(&self, _state: State) -> String { - self.text.clone() - } -} \ No newline at end of file diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1c8fd4e..b5a59c0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,13 +1,92 @@ /// The `ui` module provides user interface components and utilities for building /// a text-based user interface (TUI). It includes predefined styles and elements /// such as text, buttons, and containers. - pub mod styles; pub use styles::*; -pub mod elements; -pub use elements::*; +use crate::{Children, Element, ElementComponent, Value}; +use osui_element::{elem_fn, element}; -pub fn text<'a>() -> Box> { - Box::new(Text::new()) -} \ No newline at end of file +#[element] +#[elem_fn] +#[derive(Default, Debug)] +pub struct Text {} + +impl Element for Text<'_> { + fn render(&self, _: crate::State) -> String { + if let Children::Text(text) = &self.children { + text.clone() + } else { + String::new() + } + } +} + +#[element] +#[elem_fn] +#[derive(Default, Debug)] +pub struct Div {} + +impl Element for Div<'_> { + fn render(&self, state: crate::State) -> String { + let mut frame = crate::utils::create_frame(self.width.get_value(), self.height.get_value()); + + if let Children::Children(children, child) = &self.children { + for (i, elem) in children.iter().enumerate() { + if i == *child { + crate::utils::render_to_frame(state, self.width.get_value(), &mut frame, elem); + } else { + crate::utils::render_to_frame( + crate::State::Normal, + self.width.get_value(), + &mut frame, + elem, + ); + } + } + } + + frame.join("\n") + } + + fn event(&mut self, event: crossterm::event::Event, state: &crate::StateChanger) { + match event { + crossterm::event::Event::Key(key) => { + if let Children::Children(children, child) = &mut self.children { + *child = match key.code { + crossterm::event::KeyCode::Up => crate::utils::closest_component( + children, + *child, + crate::utils::Direction::Up, + ), + + crossterm::event::KeyCode::Down => crate::utils::closest_component( + children, + *child, + crate::utils::Direction::Down, + ), + + crossterm::event::KeyCode::Left => crate::utils::closest_component( + children, + *child, + crate::utils::Direction::Left, + ), + + crossterm::event::KeyCode::Right => crate::utils::closest_component( + children, + *child, + crate::utils::Direction::Right, + ), + + _ => *child, + } + } + } + _ => { + if let Some(child) = self.get_child() { + child.event(event, state); + } + } + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 6c8e231..23da4b8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -78,7 +78,7 @@ pub fn render_to_frame( state: crate::State, width: usize, frame: &mut Vec, - element: &mut Box, + element: &Box, ) { let data = element.get_data(); for (i, line) in element.render(state).split('\n').enumerate() { @@ -130,7 +130,7 @@ pub enum Direction { } pub fn closest_component( - components: &[&mut dyn Element], + components: &[Box], current_index: usize, direction: Direction, ) -> usize { @@ -144,39 +144,32 @@ pub fn closest_component( match direction { Direction::Left => match comp.0 { Value::Custom(_) => { - comp.0.get_value() < current.0.get_value() - && comp.1 == current.1 + comp.0.get_value() < current.0.get_value() && comp.1 == current.1 } Value::Default(_) => *i < current_index, }, // Left Direction::Right => match comp.0 { Value::Custom(_) => { - comp.0.get_value() > current.0.get_value() - && comp.1 == current.1 + comp.0.get_value() > current.0.get_value() && comp.1 == current.1 } Value::Default(_) => *i > current_index, }, // Right - Direction::Up => { - comp.1 < current.1 - && comp.0.get_value() == current.0.get_value() - } // Up + Direction::Up => comp.1 < current.1 && comp.0.get_value() == current.0.get_value(), // Up Direction::Down => { - comp.1 > current.1 - && comp.0.get_value() == current.0.get_value() + comp.1 > current.1 && comp.0.get_value() == current.0.get_value() } // Down } }) .min_by_key(|(_, comp_)| { let comp = comp_.get_data(); - current.0.get_value().abs_diff(comp.0.get_value()) - + current.1.abs_diff(comp.1) + current.0.get_value().abs_diff(comp.0.get_value()) + current.1.abs_diff(comp.1) }) // Find the closest component .map(|(index, _)| index) // Return the index of the closest component .unwrap_or(current_index) // If no component is found, return the current index } -pub fn create_frame(width: Value, height: Value) -> Vec { - vec![" ".repeat(width.get_value()); height.get_value()] +pub fn create_frame(width: usize, height: usize) -> Vec { + vec![" ".repeat(width); height] } pub fn get_term_size() -> (usize, usize) {