diff --git a/Cargo.toml b/Cargo.toml index f113075..7e17f0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,4 +19,5 @@ crossterm = "0.28.1" regex = "*" lazy_static = "1.4" paste = "1.0.15" -osui-element = { path = "./osui-element" } \ No newline at end of file +osui-element = { path = "./osui-element" } +dyn-clone = "1.0.17" diff --git a/osui-element/lib.rs b/osui-element/lib.rs index b6a85d0..589a0a6 100644 --- a/osui-element/lib.rs +++ b/osui-element/lib.rs @@ -46,6 +46,11 @@ pub fn element(_args: TokenStream, input: TokenStream) -> TokenStream { .parse2(quote! { pub children: Children }) .unwrap(), ); + fields.named.push( + syn::Field::parse_named + .parse2(quote! { pub style: Style }) + .unwrap(), + ); fields.named.push( syn::Field::parse_named .parse2(quote! { pub id: &'a str }) @@ -92,6 +97,20 @@ pub fn element(_args: TokenStream, input: TokenStream) -> TokenStream { None } } + fn set_styling(&mut self, styling: &std::collections::HashMap) { + if let Some(style) = styling.get(&StyleName::Class(self.class.to_string())) { + self.style = style.clone(); + } else if let Some(style) = styling.get(&StyleName::Id(self.id.to_string())) { + self.style = style.clone(); + } else if let Some(style) = styling.get(&StyleName::Component(stringify!(#struct_name).to_string())) { + self.style = style.clone(); + } + if let Children::Children(children, _) = &mut self.children { + for child in children { + child.set_styling(styling); + } + } + } } }; diff --git a/src/lib.rs b/src/lib.rs index 2b3b5ae..82821f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,12 @@ //! - `ui` - Contains all user interface components, enabling rich CLI experiences. //! - `utils` - Utility functions for common TUI tasks such as clearing the screen. +use std::{ + collections::HashMap, + fmt::Debug, + sync::{Arc, Mutex}, +}; + use crossterm::event::Event; pub mod app; @@ -31,8 +37,19 @@ pub mod utils; /// of UI elements. It re-exports commonly used traits, structs, and utility functions to make /// the UI framework easier to use. pub mod prelude { - pub use crate::{app::run, rsx, rsx_elem, ui::*, StateChanger}; - pub use crate::{Children, Component, Element, ElementCore, State, Value}; + pub use crate::ui::Color::*; + pub use crate::{app::run, css, rsx, rsx_elem, ui::*, Handler}; + pub use crate::{Children, Element, ElementCore, State, Value}; + pub use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; + pub fn sleep(ms: u64) { + std::thread::sleep(std::time::Duration::from_millis(ms)); + } +} + +pub mod prelude_extra{ + pub use crate::ui::Color::*; + pub use crate::{app::run, css, rsx, rsx_elem, ui::*, Handler, StateChanger}; + pub use crate::{Children, Component, Element, ElementCore, State, Value, run_handler}; pub use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind}; pub fn sleep(ms: u64) { std::thread::sleep(std::time::Duration::from_millis(ms)); @@ -79,6 +96,7 @@ pub trait ElementCore: Send + Sync { fn update_data(&mut self, width: usize, height: usize); fn get_element_by_id(&mut self, id: &str) -> Option<&mut Element>; fn get_child(&mut self) -> Option<&mut Element>; + fn set_styling(&mut self, styling: &HashMap); } pub trait Component: ElementCore + std::fmt::Debug { @@ -98,14 +116,28 @@ pub type Element = Box; /// Handler Struct ////////////////////////////////////////////////////////////////////////////////////////////////// -pub struct Handler(pub Box); +pub struct Handler(pub Arc>); impl Handler { pub fn new(handler_fn: F) -> Handler where - F: FnMut(&T, Event, &StateChanger) + 'static + Send, + F: FnMut(&mut T, Event, &StateChanger) + 'static + Send + Sync, { - Handler(Box::new(handler_fn)) + Handler(Arc::new(Mutex::new(handler_fn))) + } +} + +impl Default for Handler { + fn default() -> Self { + Handler(Arc::new(Mutex::new( + |_: &mut T, _: Event, _: &StateChanger| {}, + ))) + } +} + +impl std::fmt::Debug for Handler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Handler()") } } diff --git a/src/macros.rs b/src/macros.rs index ab8bc89..88c3966 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,14 +1,14 @@ /// **parse_rsx_param!** - A macro for parsing and setting parameters on elements, allowing for dynamic handling of element properties and children. -/// +/// /// This macro enables flexible configuration of UI elements. It handles setting fields, adding children, invoking paths, and processing complex structures. /// /// ## Example /// ```rust /// parse_rsx_param!(elem, title: "Hello", for (i in 0..5) { }, text: "World"); /// ``` -/// +/// /// ## Syntax -/// +/// /// **Set property or field of an element:** /// ```rust /// parse_rsx_param!($elem, $($k:ident).+: $v:expr); @@ -33,7 +33,7 @@ /// ```rust /// parse_rsx_param!($elem, {$ielem:expr} $($rest:tt)*); /// ``` -/// +/// /// **Set element text:** /// ```rust /// parse_rsx_param!($elem, $text:expr); @@ -112,14 +112,14 @@ macro_rules! parse_rsx_param { } /// **rsx_elem!** - A macro to instantiate a UI element and apply parameters to it. -/// +/// /// This macro simplifies creating an element and applying a set of properties or children to it, enabling a declarative style of UI building. /// /// ## Example /// ```rust /// let element = rsx_elem!(ui::Button { text: "Click me" }); /// ``` -/// +/// /// ## Syntax /// ```rust /// rsx_elem!($elem:path { $($inner:tt)* }) @@ -143,9 +143,9 @@ macro_rules! rsx_elem { /// rsx! { /// text { "Hello, World" } /// div { button {"Click me!"} } -/// } +/// } /// ``` -/// +/// /// ## Syntax /// ```rust /// rsx!($($inner:tt)*) @@ -165,48 +165,174 @@ macro_rules! val { } #[macro_export] -macro_rules! write { - (($self:ident, $state:expr), $expr:expr) => {{ - if $state == 0 { - return format!( - "{}{}{}{}\x1b[0m", - $self.style.color.ansi(), - $self.style.background.ansi_bg(), - $self.style.font.ansi(), - $expr - ); - } - format!( - "{}{}{}{}\x1b[0m", - $self.style.hover_color.ansi(), - $self.style.hover_background.ansi_bg(), - $self.style.hover_font.ansi(), - $expr - ) +macro_rules! run_handler { + ($self:ident.$handler:ident ($($inner:tt)*)) => {{ + let a = std::sync::Arc::clone(&$self.$handler.0); + let mut o = a.lock().unwrap(); + o($self, $($inner)*); }}; - - ($self:ident, $kind:ident, $expr:expr) => {{ - use paste::paste; - format!( - "{}{}{}{}\x1b[0m", - paste! { - $self.style.[<$kind _color>].ansi() - }, - paste! { - $self.style.[<$kind _background>].ansi_bg() - }, - paste! { - $self.style.[<$kind _font>].ansi() - }, - $expr - ) + ($self:ident.$handler:ident) => {{ + &$self.$handler; }}; } #[macro_export] -macro_rules! run_handler { - ($self:ident.$handler:ident ($renderer:expr, $event:expr)) => { - let mut o = $self.$handler.0.lock().unwrap(); - o($self, $renderer, $event); - }; +macro_rules! __css { + ($style:expr,)=>{}; + ( + $style:expr, ($name:ident): $value:expr + ) => {{ + $style.other.insert(stringify!($name).to_string(), Box::new($value)); + }}; + ( + $style:expr, ($name:ident): $value:expr, $($other:tt)* + ) => {{ + $style.other.insert(stringify!($name).to_string(), Box::new($value)); + $crate::__css!($($other)*); + }}; + ( + $style:expr, $name:ident: $value:expr + ) => {{ + $style.$name = $value; + }}; +} + +#[macro_export] +macro_rules! _css { + ($style:expr,) => {}; + ( + $hm:expr, .$name:ident { $($inner:tt)* } $($rest:tt)* + ) => {{ + let name = $crate::ui::StyleName::Class(stringify!($name).to_string()); + if let Some(style) = $hm.get_mut(&name) { + $crate::__css!(style.0, $($inner)*); + } else { + let mut style = $crate::ui::Style::default(); + + $crate::__css!(style.0, $($inner)*); + + $hm.insert( + name, + style, + ); + } + $crate::_css!($hm, $($rest)*); + }}; + ( + $hm:expr, .$name:ident::$kind:ident { $($inner:tt)* } $($rest:tt)* + ) => {{ + let name = $crate::ui::StyleName::Class(stringify!($name).to_string()); + if let Some(style) = $hm.get_mut(&name) { + let mut style_elem = $crate::ui::StyleElement::default(); + $crate::__css!(style_elem, $($inner)*); + style.1.insert(stringify!($kind).to_string(), style_elem); + } else { + let mut style = $crate::ui::Style::default(); + + let mut style_elem = $crate::ui::StyleElement::default(); + $crate::__css!(style_elem, $($inner)*); + style.1.insert(stringify!($kind).to_string(), style_elem); + + $hm.insert( + name, + style, + ); + } + $crate::_css!($hm, $($rest)*); + }}; + + + ( + $hm:expr, #$name:ident { $($inner:tt)* } $($rest:tt)* + ) => {{ + let name = $crate::ui::StyleName::Id(stringify!($name).to_string()); + if let Some(style) = $hm.get_mut(&name) { + $crate::__css!(style.0, $($inner)*); + } else { + let mut style = $crate::ui::Style::default(); + + $crate::__css!(style.0, $($inner)*); + + $hm.insert( + name, + style, + ); + } + $crate::_css!($hm, $($rest)*); + }}; + ( + $hm:expr, #$name:ident::$kind:ident { $($inner:tt)* } $($rest:tt)* + ) => {{ + let name = $crate::ui::StyleName::Id(stringify!($name).to_string()); + if let Some(style) = $hm.get_mut(&name) { + let mut style_elem = $crate::ui::StyleElement::default(); + $crate::__css!(style_elem, $($inner)*); + style.1.insert(stringify!($kind).to_string(), style_elem); + } else { + let mut style = $crate::ui::Style::default(); + + let mut style_elem = $crate::ui::StyleElement::default(); + $crate::__css!(style_elem, $($inner)*); + style.1.insert(stringify!($kind).to_string(), style_elem); + + $hm.insert( + name, + style, + ); + } + $crate::_css!($hm, $($rest)*); + }}; + + + ( + $hm:expr, $name:ident { $($inner:tt)* } $($rest:tt)* + ) => {{ + let name = $crate::ui::StyleName::Component(stringify!($name).to_string()); + if let Some(style) = $hm.get_mut(&name) { + $crate::__css!(style.0, $($inner)*); + } else { + let mut style = $crate::ui::Style::default(); + + $crate::__css!(style.0, $($inner)*); + + $hm.insert( + name, + style, + ); + } + $crate::_css!($hm, $($rest)*); + }}; + ( + $hm:expr, $name:ident::$kind:ident { $($inner:tt)* } $($rest:tt)* + ) => {{ + let name = $crate::ui::StyleName::Component(stringify!($name).to_string()); + if let Some(style) = $hm.get_mut(&name) { + let mut style_elem = $crate::ui::StyleElement::default(); + $crate::__css!(style_elem, $($inner)*); + style.1.insert(stringify!($kind).to_string(), style_elem); + } else { + let mut style = $crate::ui::Style::default(); + + let mut style_elem = $crate::ui::StyleElement::default(); + $crate::__css!(style_elem, $($inner)*); + style.1.insert(stringify!($kind).to_string(), style_elem); + + $hm.insert( + name, + style, + ); + } + $crate::_css!($hm, $($rest)*); + }}; +} + +#[macro_export] +macro_rules! css { + ( + $($inner:tt)* + ) => {{ + let mut hm: $crate::ui::Css = std::collections::HashMap::new(); + $crate::_css!(hm, $($inner)*); + hm + }}; } diff --git a/src/main.rs b/src/main.rs index de9f0eb..a2f1d5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,15 @@ fn main() { } fn app() -> Element { - rsx! { - button { "This is a button!" } + rsx! { styling: Some(styles()), + button { class: "btn", "This is a button!" } + } +} + +fn styles() -> Css { + css! { + .btn::click { + color: Blue + } } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 70606c2..db37aa1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,18 +1,11 @@ /// The `ui` module defines the basic building blocks for creating a terminal-based UI framework. -/// This includes elements like `Text` and `Div`, which can be combined to create complex layouts -/// and interactive UIs in the terminal. It also provides utility functions for rendering, event -/// handling, and managing child components. /// -/// Key Components: -/// - `Text`: A simple text element that renders a string to the terminal. -/// - `Div`: A container element that holds and renders child components, allowing navigation -/// through them with keyboard events (Up, Down, Left, Right). +/// # Modules /// - `styles`: A module for defining UI styles. -/// - `utils`: Utility functions for rendering and managing state. pub mod styles; pub use styles::*; -use crate::prelude::*; +use crate::prelude_extra::*; use osui_element::{elem_fn, element}; /// The `Text` element represents a piece of static text in the terminal UI. @@ -68,7 +61,9 @@ impl Component for Text<'_> { #[element] #[elem_fn] #[derive(Default, Debug)] -pub struct Div {} +pub struct Div { + pub styling: Option>, +} impl Component for Div<'_> { fn render(&self, state: State) -> String { @@ -93,6 +88,9 @@ impl Component for Div<'_> { } fn event(&mut self, event: Event, state: &crate::StateChanger) { + if let Some(styling) = self.styling.clone() { + self.set_styling(&styling); + } match event { Event::Key(key) => { if let Children::Children(children, child) = &mut self.children { @@ -142,13 +140,16 @@ impl Component for Div<'_> { #[element] #[elem_fn] #[derive(Default, Debug)] -pub struct Button {} +pub struct Button { + pub on_click: Handler>, +} impl Component for Button<'_> { fn render(&self, state: State) -> String { match state { - State::Custom(_) => format!("\x1b[32m{}\x1b[0m", self.children.get_text()), - _ => self.children.get_text(), + State::Custom(_) => self.style.write("click", &self.children.get_text()), + State::Hover => self.style.write("hover", &self.children.get_text()), + _ => self.style.write("", &self.children.get_text()), } } @@ -157,6 +158,7 @@ impl Component for Button<'_> { Event::Key(key) => { if key.code == KeyCode::Enter { let prev_state = state.get_state(); + run_handler!(self.on_click(event, state)); state.set_state(State::Custom(0)); sleep(100); state.set_state(prev_state); diff --git a/src/ui/styles.rs b/src/ui/styles.rs index 483e5cc..9498ff1 100644 --- a/src/ui/styles.rs +++ b/src/ui/styles.rs @@ -1,18 +1,19 @@ //! The `styles` module defines various styling options for UI elements. This includes colors, //! fonts, padding, margins, and other visual properties that can be applied to UI components //! to change their appearance. -//! -//! # Example -//! ```rust -//! use crate::ui::styles::{Style, Color}; -//! -//! let style = Style { -//! color: Color::Blue, -//! padding: 5, -//! }; -//! ``` -#[derive(Debug, Clone, PartialEq)] +use std::{collections::HashMap, fmt::Debug}; + +use dyn_clone::{clone_trait_object, DynClone}; + +pub trait StyleCore: Debug + Send + Sync + DynClone { + fn ansi(&self) -> String; + fn ansi_bg(&self) -> String; +} + +clone_trait_object!(StyleCore); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Font { None, Bold, @@ -20,11 +21,11 @@ pub enum Font { Italic, Reverse, Strike, - Mul(Vec), + Mul(Vec), } -impl Font { - pub fn ansi(&self) -> String { +impl StyleCore for Font { + fn ansi(&self) -> String { String::from(match self { Font::None => "", Font::Bold => "\x1b[1m", @@ -41,24 +42,15 @@ impl Font { } }) } - - - - pub fn prioritize<'a>(&'a self, secondary: &'a Font) -> &Font { - if secondary == &Font::None { - self - } else { - secondary - } + fn ansi_bg(&self) -> String { + self.ansi() } } - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Color { - None, - - Rgb(u8, u8, u8), + NoColor, + Rgb(u8, u8, u8), Hex(String), Black, Red, @@ -70,11 +62,10 @@ pub enum Color { White, } -impl Color { - - pub fn ansi(&self) -> String { +impl StyleCore for Color { + fn ansi(&self) -> String { String::from(match self { - Color::None => "", + Color::NoColor => "", Color::Rgb(r, g, b) => { return format!("\x1b[38;2;{r};{g};{b}m"); } @@ -93,10 +84,9 @@ impl Color { }) } - - pub fn ansi_bg(&self) -> String { + fn ansi_bg(&self) -> String { String::from(match self { - Color::None => "", + Color::NoColor => "", Color::Rgb(r, g, b) => { return format!("\x1b[48;2;{r};{g};{b}m"); } @@ -114,21 +104,11 @@ impl Color { Color::White => "\x1b[47m", }) } - - - - pub fn prioritize<'a>(&'a self, secondary: &'a Color) -> &Color { - if secondary == &Color::None { - self - } else { - secondary - } - } } impl Default for Color { fn default() -> Self { - Color::None + Color::NoColor } } @@ -141,24 +121,77 @@ impl Default for Font { fn hex_to_rgb(hex: &str) -> (u8, u8, u8) { let hex = hex.trim_start_matches('#'); - let hex = if hex.len() == 3 { format!( "{}{}{}{}{}{}", - &hex[0..1], &hex[0..1], - &hex[1..2], &hex[1..2], - &hex[2..3], &hex[2..3] + &hex[0..1], + &hex[0..1], + &hex[1..2], + &hex[1..2], + &hex[2..3], + &hex[2..3] ) } else if hex.len() == 6 { hex.to_string() } else { - return (255, 255, 255); + return (255, 255, 255); }; - let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255); let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255); let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255); (r, g, b) -} \ No newline at end of file +} + +#[derive(PartialEq, Eq, Hash, Debug, Clone)] +pub enum StyleName { + Class(String), + Id(String), + Component(String), +} + +#[derive(Debug, Clone)] +pub struct StyleElement { + pub color: Color, + pub other: HashMap>, +} + +impl Default for StyleElement { + fn default() -> Self { + StyleElement { + color: Color::NoColor, + other: HashMap::new(), + } + } +} + +#[derive(Debug, Clone)] +pub struct Style(pub StyleElement, pub HashMap); + +impl Default for Style { + fn default() -> Self { + Style(StyleElement::default(), HashMap::new()) + } +} + +impl Style { + pub fn write(&self, state: &str, s: &str) -> String { + if state == "" { + return self.0.write(s); + } + if let Some(style_element) = self.1.get(state) { + style_element.write(s) + } else { + s.to_string() + } + } +} + +impl StyleElement { + pub fn write(&self, s: &str) -> String { + format!("{}{s}\x1b[0m", self.color.ansi()) + } +} + +pub type Css = HashMap;