From 4f9a143008abad14ec9f82dbb09462ebbac48e20 Mon Sep 17 00:00:00 2001 From: Leo dev Date: Tue, 5 Nov 2024 17:26:19 +0100 Subject: [PATCH] Large structural changes --- Cargo.toml | 3 +- src/element.rs | 136 +++++++++++++++++++++ src/key.rs | 94 ++++++++++----- src/lib.rs | 274 ++++++++++++++++++++---------------------- src/macros.rs | 293 ++++++++++++++++++++++++++------------------- src/main.rs | 13 +- src/ui/elements.rs | 101 +++++++++++----- src/ui/mod.rs | 33 +++-- src/ui/styles.rs | 62 ++++++++-- src/utils.rs | 24 ++-- 10 files changed, 672 insertions(+), 361 deletions(-) create mode 100644 src/element.rs diff --git a/Cargo.toml b/Cargo.toml index 6493178..d744a32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ path = "src/main.rs" [dependencies] crossterm = "0.28.1" regex = "*" -lazy_static = "1.4" \ No newline at end of file +lazy_static = "1.4" +dyn-clone = "1.0.17" diff --git a/src/element.rs b/src/element.rs new file mode 100644 index 0000000..53a0eb0 --- /dev/null +++ b/src/element.rs @@ -0,0 +1,136 @@ +//! # Element Module +//! +//! This module defines the `Element` trait and associated types for creating, +//! configuring, and updating UI elements in OSUI. Elements are the building +//! blocks of the TUI, each with properties such as size and position and +//! methods for rendering and updating. + +use dyn_clone::DynClone; + +/// Enum representing the size of an `Element`. +/// +/// - `Default(usize)` - Uses a default size for the element. +/// - `Custom(usize)` - Allows specifying a custom size for the element. +#[derive(Debug, Clone, Copy)] +pub enum ElementSize { + Default(usize), + Custom(usize), +} + +impl ElementSize { + /// Retrieves the size as an `usize`. + /// + /// # Returns + /// + /// The size value, either the default or custom size. + pub fn get_size(&self) -> usize { + match *self { + ElementSize::Custom(s) => s, + ElementSize::Default(s) => s, + } + } + + /// Attempts to set the size of an `Element` if it is currently set to `Default`. + /// + /// If the size is `Custom`, this function will not modify it. + /// + /// # Arguments + /// + /// * `size` - The size to set for the `Element`. + pub fn try_set_size(&mut self, size: usize) { + if let ElementSize::Default(_) = *self { + *self = ElementSize::Default(size); + } + } +} + +/// A trait for defining UI elements in OSUI. +/// +/// Elements must implement methods for updating and rendering data. +/// This trait supports dynamic dispatch and cloning. +pub trait Element: std::fmt::Debug + Send + DynClone { + /// Retrieves the data for the `Element`. + /// + /// # Returns + /// + /// `ElementData` containing position and size information. + fn get_data(&self) -> ElementData; + + /// Updates the data of the `Element` based on the given dimensions. + /// + /// # Arguments + /// + /// * `_width` - The width of the terminal window or parent element. + /// * `_height` - The height of the terminal window or parent element. + fn update_data(&mut self, _width: usize, _height: usize); + + /// Renders the `Element` as a `String`. + /// + /// # Arguments + /// + /// * `_state` - The current state of the element; if one or higher, it indicates the element is active. + /// If zero, the element is just being rendered. + /// + /// # Returns + /// + /// A `String` representing the rendered output of the `Element`. + fn render(&self, _state: usize) -> String { + String::new() + } + + /// Updates the `Element` based on a `Key` event and the current state. + /// + /// # Arguments + /// + /// * `_state` - The current state of the element; if one or higher, it indicates the element is active. + /// * `_k` - The key input triggering the update. + /// + /// # Returns + /// + /// An `UpdateResponse` enum indicating the result of the update. + fn event(&mut self, _state: usize, _k: crate::key::Key) -> UpdateResponse { + UpdateResponse::None + } +} + +// Enable cloning of `Element` trait objects. +dyn_clone::clone_trait_object!(Element); + +/// Struct holding data relevant to an `Element`, including position and size. +#[derive(Debug)] +pub struct ElementData { + /// X coordinate of the `Element`. + pub x: usize, + /// Y coordinate of the `Element`. + pub y: usize, + /// Width of the `Element`, which can be default or custom. + pub width: ElementSize, + /// Height of the `Element`, which can be default or custom. + pub height: ElementSize, +} + +/// Enum representing the possible responses from an element update. +#[derive(Debug, Clone, PartialEq)] +pub enum UpdateResponse { + /// Indicates that the update is complete. + Done, + /// Indicates no response. + None, + /// Issues a single command. + Command(Command), + /// Issues a list of commands. + CommandList(Vec), +} + +/// Enum defining commands that can be issued by an `Element`. +#[derive(Debug, Clone, PartialEq)] +pub enum Command { + /// Renders the element with the specified state. + Render(usize), + /// Exits the application. + Exit, + /// Updates the element with the specified state. + Update(usize), + /// Pauses execution for the specified duration in milliseconds. + Sleep(u64), +} diff --git a/src/key.rs b/src/key.rs index b4c131e..cd90fb2 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,51 +1,89 @@ use std::io::{self, Read}; +/// Represents different key inputs that can be detected from the terminal. #[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub enum KeyKind { +pub enum Key { + /// Represents the Enter key (carriage return). Enter, + /// Represents the Tab key. Tab, + /// Represents the Shift+Tab key combination. ShiftTab, + /// Represents the Escape key. Escape, + /// Represents the Up arrow key. Up, + /// Represents the Down arrow key. Down, + /// Represents the Left arrow key. Left, + /// Represents the Right arrow key. Right, - Char(String), -} - -#[derive(Debug, Clone, Hash, Eq, PartialEq)] -pub struct Key { - pub kind: KeyKind, - pub raw: String, + /// Represents any other character or sequence that does not match a predefined key. + Other(String), } impl Key { + /// Creates a new `Key` instance based on the input string. + /// + /// This function matches specific strings with known keys (e.g., arrow keys, Enter, Tab) + /// and returns the corresponding `Key` variant. If the string does not match any of the + /// predefined keys, it returns `Key::Other` with the string content. + /// + /// # Arguments + /// + /// * `k` - A `String` representing the raw key input. + /// + /// # Returns + /// + /// A `Key` enum variant corresponding to the input. pub fn new(k: String) -> Key { - Key { - raw: k.clone(), - kind: match k.as_str() { - "\r" => KeyKind::Enter, - "\t" => KeyKind::Tab, - "\x1b[Z" => KeyKind::ShiftTab, - "\x1b" => KeyKind::Escape, - "\x1b[A" => KeyKind::Up, - "\x1b[B" => KeyKind::Down, - "\x1b[C" => KeyKind::Right, - "\x1b[D" => KeyKind::Left, - - _ => KeyKind::Char(k.clone()), - }, + match k.as_str() { + "\r" => Key::Enter, + "\t" => Key::Tab, + "\x1b[Z" => Key::ShiftTab, + "\x1b" => Key::Escape, + "\x1b[A" => Key::Up, + "\x1b[B" => Key::Down, + "\x1b[C" => Key::Right, + "\x1b[D" => Key::Left, + _ => Key::Other(k), } } } +/// Reads a key from standard input and returns it as a `Key` enum variant. +/// +/// This function calls `read_key_raw` to get the raw key input as a `String`, +/// then uses `Key::new` to convert it to a `Key` variant. +/// +/// # Returns +/// +/// A `Key` enum variant corresponding to the input read from standard input. pub fn read_key() -> Key { + Key::new(read_key_raw()) +} + +/// Reads raw key input from standard input as a UTF-8 encoded string. +/// +/// This function attempts to read up to 3 bytes from standard input and +/// returns them as a `String`. It uses a buffer size of 3, which is enough +/// to capture most common key sequences, including basic escape sequences +/// for arrow keys and other special keys. +/// +/// # Returns +/// +/// A `String` representing the raw key input read from standard input. +/// +/// # Panics +/// +/// This function will panic if reading from `stdin` fails or if the bytes +/// cannot be converted to a valid UTF-8 `String`. +pub fn read_key_raw() -> String { let mut buffer = vec![0; 3]; io::stdin().read(&mut buffer).unwrap(); - Key::new( - String::from_utf8(buffer) - .unwrap() - .trim_matches('\0') - .to_string(), - ) + String::from_utf8(buffer) + .unwrap() + .trim_matches('\0') + .to_string() } diff --git a/src/lib.rs b/src/lib.rs index ce0dbd4..a047463 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,163 +1,147 @@ -use crossterm::ExecutableCommand; +//! # OSUI +//! +//! A terminal user interface (TUI) library providing customizable components +//! to build command-line interfaces in Rust. OSUI enables users to create +//! interactive CLI applications with various UI elements and handle keyboard +//! input for real-time updates. +//! +//! ## Example Usage +//! +//! ```rust +//! use osui::{parse_rsx_param, rsx, ui::*}; +//! +//! osui::app::run(&mut rsx! { +//! text { "Hello, World!" } +//! }); +//! ``` +//! +//! ## Modules +//! - `element` - Defines base elements for constructing UI components. +//! - `key` - Handles keyboard input, providing key event management. +//! - `utils` - Utility functions for common TUI tasks such as clearing the screen. +//! - `ui` - Contains all user interface components, enabling rich CLI experiences. +pub mod element; pub mod key; pub mod macros; pub mod ui; pub mod utils; -pub trait Element: std::fmt::Debug { - fn get_child(&mut self) -> Option<&mut Box>; - fn get_data(&self) -> ElementData; - fn set_data(&mut self, _: ElementData); - fn clear_ticks(&mut self); - fn render(&mut self, _tick: usize) -> String { - String::new() - } - fn update(&mut self, _ctx: &mut UpdateContext) {} -} +pub use element::*; +pub use utils::*; -#[derive(Debug)] -pub struct ElementData { - pub x: usize, - pub y: usize, - pub width: usize, - pub height: usize, - pub style: ui::styles::Style, -} +pub mod app { + //! Application entry point and main event loop for OSUI. + //! + //! Provides functions to render and update UI elements based on keyboard + //! input. Manages cursor visibility, terminal size, and controls UI behavior + //! using custom commands such as rendering, updating, or exiting. -#[derive(Debug, Clone, PartialEq)] -pub enum UpdateResponse { - Exit, - Done, - None, -} + use crate::{ + clear, create_frame, flush, get_term_size, hide_cursor, + key::{read_key, Key}, + render_to_frame, show_cursor, Command, Element, ElementSize, UpdateResponse, + }; -pub struct UpdateContext { - pub key: key::Key, - pub tick: usize, - pub response: UpdateResponse, -} - -pub struct App { - element: Box, -} - -impl App { - /// Creates a new screen to render components - pub fn new() -> App { - App { - element: ui::text(), - } - } - - /// Creates a new screen to render components with a pre-existing component - pub fn from(elem: Box) -> App { - let mut app = App::new(); - app.set_component(elem); - app - } - - /// Sets a component - pub fn set_component(&mut self, element: Box) { - let (width, height) = crossterm::terminal::size().unwrap(); - self.element = element; - let mut data = self.element.get_data(); - data.style.is_active = true; - if data.width == 0 { - data.width = width as usize; - } - if data.height == 0 { - data.height = height as usize; - } - self.element.set_data(data); - } - - /// Render to the screen - fn render(&mut self, tick: usize) { - let (width, height) = crossterm::terminal::size().unwrap(); - let mut data = self.element.get_data(); - if data.width == 0 { - data.width = width as usize; - } - if data.height == 0 { - data.height = height as usize; - } - self.element.set_data(data); - let mut frame: Vec = create_frame!(width as usize, height as usize); - utils::render_to_frame(tick, &mut frame, &mut self.element); - utils::clear(); + /// Renders a single frame of the UI to the terminal. + /// + /// Sets up a new frame based on the terminal's current size, updates + /// the element dimensions, and renders the UI element to the frame. + /// + /// # Arguments + /// + /// * `elem` - A mutable reference to a boxed UI element that implements the `Element` trait. + /// * `state` - Current state of the element, typically used to track the UI's state in the app loop. + fn render(elem: &mut Box, state: usize) { + let (width, height) = get_term_size(); + elem.update_data(width, height); + let mut frame: Vec = + create_frame(ElementSize::Custom(width), ElementSize::Custom(height)); + render_to_frame(state, &mut frame, elem); + clear(); print!("{}", frame.join("")); - utils::flush(); - let mut data = self.element.get_data(); - if data.width == width as usize { - data.width = 0; - } - if data.height == height as usize { - data.height = 0; - } - self.element.set_data(data); + flush(); } - fn update(&mut self, ctx: &mut UpdateContext) { - self.element.update(ctx); - match ctx.response { - UpdateResponse::Exit => { - crossterm::terminal::disable_raw_mode().unwrap(); - utils::clear(); - utils::show_cursor(); - println!(""); - return; - } - _ => {} - } - } - - /// Run the screen - pub fn run(&mut self) { - // Initialize - utils::hide_cursor(); - utils::clear(); - let mut stdout = std::io::stdout(); - stdout - .execute(crossterm::terminal::EnterAlternateScreen) - .unwrap(); - crossterm::terminal::enable_raw_mode().unwrap(); - - // Start the update thread - let (tx, rx) = std::sync::mpsc::channel(); - std::thread::spawn(move || loop { - tx.send(key::read_key()).unwrap(); - }); - - // Start the render loop - let mut tick: usize = 0; - let tick_duration = std::time::Duration::from_millis(1000/30); - let mut last_tick = std::time::Instant::now(); - loop { - let now = std::time::Instant::now(); - let elapsed = now.duration_since(last_tick); - - if elapsed >= tick_duration { - last_tick = now; - if tick > 99 { - tick = 0; - } - self.render(tick); - tick += 1; - match rx.try_recv() { - Ok(k) => self.update(&mut UpdateContext { - key: k, - tick, - response: UpdateResponse::None, - }), - Err(std::sync::mpsc::TryRecvError::Empty) => {} - Err(std::sync::mpsc::TryRecvError::Disconnected) => { - panic!("disconnected") + /// Updates the UI element based on keyboard input and issues any commands in response. + /// + /// Processes the result of `Element::update` to handle commands like rendering, + /// updating, and exiting. Commands can be a single action or a list of actions. + /// + /// # Arguments + /// + /// * `elem` - A mutable reference to a boxed UI element. + /// * `state` - The current UI state, used for conditional updates. + /// * `k` - A `Key` input, typically read from the user’s keyboard input. + /// + /// # Returns + /// + /// `true` if an `Exit` command is issued, signaling the application to terminate. + fn update(elem: &mut Box, state: usize, k: Key) -> bool { + match elem.event(state, k.clone()) { + UpdateResponse::Command(command) => run_command(command, elem, k.clone()), + UpdateResponse::CommandList(commands) => { + for command in commands { + if run_command(command, elem, k.clone()) { + return true; } } + false } - if let Some(remaining) = tick_duration.checked_sub(elapsed) { - std::thread::sleep(remaining); + _ => false, + } + } + + /// Executes a command, performing actions such as rendering, updating, + /// sleeping, or exiting the application. + /// + /// # Arguments + /// + /// * `command` - The command to execute, such as rendering or updating the UI. + /// * `elem` - A mutable reference to the UI element. + /// * `k` - A `Key` input passed along for further processing. + /// + /// # Returns + /// + /// `true` if the command is `Exit`, ending the application loop. + fn run_command(command: Command, elem: &mut Box, k: Key) -> bool { + match command { + Command::Render(state) => { + render(elem, state); + false + } + Command::Update(state) => update(elem, state, k), + Command::Sleep(duration) => { + std::thread::sleep(std::time::Duration::from_millis(duration)); + false + } + Command::Exit => { + show_cursor(); + crossterm::terminal::disable_raw_mode().unwrap(); + clear(); + true + } + } + } + + /// Runs the main event loop for the application. + /// + /// Enables raw mode, hides the cursor, and continuously renders and updates + /// the UI based on user input. The loop will break if the `Exit` command is triggered. + /// + /// # Arguments + /// + /// * `elem` - A mutable reference to the main UI element to be rendered and updated. + pub fn run(elem: &mut Box) { + // Initialize terminal settings + hide_cursor(); + crossterm::terminal::enable_raw_mode().unwrap(); + clear(); + loop { + render(elem, 1); + let k = read_key(); + if update(elem, 1, k) { + break; } } } diff --git a/src/macros.rs b/src/macros.rs index 9ac0341..4f997ce 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,175 +1,222 @@ -#[macro_export] -macro_rules! __rsx { - () => { - (String::new(), Vec::new()) - }; +//! # 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. - ($p:expr;) => {{ - (String::new(), vec![$p as Box]) - }}; - ($p:expr; $($rest:tt)+) => {{ - let mut comps: Vec> = osui::__rsx!($($rest)+).1; - comps.insert(0, $p as Box); - (String::new(), comps) - }}; - // Props, With components (PC) - ($tag:path { $($inner:tt)* }) => {{ - (String::new(), vec![rsx!($tag {$($inner)*})]) - }}; - - ($tag:path { $($inner:tt)* } $($rest:tt)+) => {{ - let mut comps: Vec> = osui::__rsx!($($rest)+).1; - comps.insert(0, rsx!($tag {$($inner)*}) ); - (String::new(), comps) - }}; - - ($tag:path { $($inner:tt)* }) => {{ - (String::new(), vec![rsx!($tag {$($inner)*})]) - }}; - - ($tag:path { $($inner:tt)* } $($rest:tt)+) => {{ - let mut comps: Vec> = osui::__rsx!($($rest)+).1; - comps.insert(0, rsx!($tag {$($inner)*}) ); - (String::new(), comps) - }}; - - ($text:expr) => {{ - (format!($text), Vec::new()) - }}; -} - -#[macro_export] -/// Write OSUI Markup Language directly with rust. -macro_rules! rsx { - ( $tag:path { $($k:ident: $v:expr),*; $($inner:tt)* } ) => {{ - let mut c = $tag(); - let a = osui::__rsx!($($inner)*); - c.text = a.0; - c.children = a.1; - $( - c.$k = $v; - )* - c as Box - }}; - - ( $tag:path { $($k:ident: $v:expr),* } ) => {{ - let mut c = $tag(); - $( - c.$k = $v; - )* - c as Box - }}; - - ( $tag:path { $($inner:tt)* } ) => {{ - let mut c = $tag(); - let a = osui::__rsx!($($inner)*); - c.text = a.0; - c.children = a.1; - c as Box - }}; - - ( $p:expr; ) => {{ - $p as Box - }}; -} - -#[macro_export] -///```create_frame!(width, height)``` +/// Macro for defining an OSUI `Element`. /// -/// Create a frame for rendering multiple components -macro_rules! create_frame { - ($width:expr, $height:expr) => { - vec![" ".repeat($width); $height] - }; -} - -#[macro_export] -///```component!{ +/// This macro generates a new element type with specified parameters and default values. +/// +/// # Example +/// ``` +/// element! { /// MyElem {} /// defaults {} -/// }``` +/// // functions (render, update) +/// } +/// ``` /// -/// Create a frame for rendering multiple components +/// # 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:meta])* $name:ident { $( $inner:tt )* } defaults {$( $defaults:tt )*} $( $functions:tt )* ) => { - #[derive(Debug)] + $(#[$meta])* + #[derive(Debug, Clone)] pub struct $name { pub x: usize, pub y: usize, - pub width: usize, - pub height: usize, + pub width: ElementSize, + pub height: ElementSize, pub children: Vec>, pub child: usize, pub text: String, pub style: Style, - pub tick_line: std::collections::HashMap, $( $inner )* } impl Element for $name { - fn get_child(&mut self) -> Option<&mut Box> { - self.children.get_mut(self.child) - } fn get_data(&self) -> ElementData { ElementData { x: self.x, y: self.y, - width: self.width, - height: self.height, - style: self.style.clone(), + width: self.width.clone(), + height: self.height.clone(), } } - fn set_data(&mut self, data: ElementData) { - self.x = data.x; - self.y = data.y; - self.width = data.width; - self.height = data.height; - self.style = data.style; - } - - fn clear_ticks(&mut self) { - self.tick_line.clear(); + fn update_data(&mut self, width: usize, height: usize) { + self.width.try_set_size(width); + self.height.try_set_size(height); + for child in &mut self.children { + child.update_data(width, height); + } } $( $functions )* } impl $name { + /// Creates a new instance of the element with default values. pub fn new() -> $name { $name { children: Vec::new(), x: 0, y: 0, - width: 0, - height: 0, + width: ElementSize::Default(0), + height: ElementSize::Default(0), child: 0, text: String::new(), style: Style::default(), - tick_line: std::collections::HashMap::new(), $( $defaults )* } } - pub fn get_action(&self, tick: usize) -> String { - match self.tick_line.get(&tick) { - Some(action) => action.clone(), - None => String::new(), - } - } - - pub fn add_action(&mut self, tick: usize, action: &str) { - if tick > 99 { - self.tick_line.insert(tick-100, action.to_string()); - } else { - self.tick_line.insert(tick, action.to_string()); - } + /// 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 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 `UpdateResponse::CommandList`. +#[macro_export] +macro_rules! command { + ($($cmd:expr),*) => { + UpdateResponse::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, +/// and adds child elements to the parent element. +/// +/// # Parameters +/// - `elem`: The element being updated. +/// - Various key-value pairs, child elements, and text content. +#[macro_export] +macro_rules! parse_rsx_param { + ($elem:expr, ) => {}; + + ($elem:expr, $($k:ident: $v:expr),*) => { + $( + $elem.$k = $v; + )* + }; + + ($elem:expr, $($k:ident: $v:expr),*; $($rest:tt)*) => { + $( + $elem.$k = $v; + )* + osui::parse_rsx_param!($elem, $($rest)*); + }; + + ($elem:expr, {$ielem:expr} $($rest:tt)*) => { + $elem.children.push($ielem); + osui::parse_rsx_param!($elem, $($rest)*); + }; + + ($elem:expr, $($k:ident: $v:expr),*, $text:expr) => { + $( + $elem.$k = $v; + )* + $elem.text = format!($text); + }; + + ($elem:expr, $pelem:path { $($inner:tt)* } $($rest:tt)*) => { + $elem.children.push(osui::rsx_elem!($pelem { $($inner)* })); + osui::parse_rsx_param!($elem, $($rest)*) + }; + + ($elem:expr, $text:expr) => { + $elem.text = format!($text); + }; +} + +/// Macro for creating an OSUI element with parsed parameters. +/// +/// # Example +/// ``` +/// rsx! { +/// text { "Hello, World!" } +/// div { +/// button { y: 2, "click me" } +/// } +/// } +/// ``` +/// +/// This macro provides a clean way to express OSUI elements, functioning like a div that can +/// contain multiple components. +#[macro_export] +macro_rules! rsx_elem { + ($elem:path { $($inner:tt)* }) => {{ + let mut elem = $elem(); + osui::parse_rsx_param!(elem, $($inner)*); + elem as Box + }}; +} + +/// Macro for defining a structured representation of UI elements in OSUI. +/// +/// # Example +/// ``` +/// rsx! { +/// text { "Hello, World!" } +/// div { +/// button { y: 2, "click me" } +/// } +/// } +/// ``` +/// +/// This macro allows the creation of a root element that can contain other elements. +#[macro_export] +macro_rules! rsx { + ($($inner:tt)*) => {{ + osui::rsx_elem!( osui::ui::div { $($inner)* } ) + }}; +} + +/// Macro for defining CSS styles in OSUI. +/// +/// # Example +/// ``` +/// css!(Style; color: "red"; background: "white"); +/// ``` +/// +/// This macro creates a new style based on the provided properties, applying default values +/// for any unspecified fields. +#[macro_export] +macro_rules! css { + ( + $style:path; + $($inner:tt)* + ) => {{ + $style { + $($inner)* + ..Default::default() + } + }}; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2a8a2f4..38e045b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ -use osui::{rsx, ui::*, App}; +use osui::{rsx, ui::*}; fn main() { - let element = rsx! { - button { "Hello, World!" } - }; + osui::app::run(&mut app()); +} - let mut app_screen = App::from(element); - app_screen.run(); +fn app() -> Box { + rsx! { + text { "Hello, World!" } + } } diff --git a/src/ui/elements.rs b/src/ui/elements.rs index a9f3665..faf9d4f 100644 --- a/src/ui/elements.rs +++ b/src/ui/elements.rs @@ -1,49 +1,90 @@ use std::collections::HashMap; -use crate::{element, key::KeyKind, ui::Style, Element, ElementData, UpdateContext}; +use crate::{ + command, element, key::Key, render_to_frame, ui::Style, Command, Direction, Element, + ElementData, ElementSize, UpdateResponse, +}; element! { + /// A text element for displaying static text in the TUI. + /// + /// The `Text` element displays text and does not respond to user interactions. Text {} defaults {} - fn render(&mut self, tick: usize) -> String { - // self.style.write(&self.text) - self.style.write(&format!("{tick}")) + fn render(&self, _: usize) -> String { + self.text.clone() + } +} +element! { + /// A clickable button element. + /// + /// The `Button` element can be clicked, triggering an `on_click` function. Its appearance changes + /// based on its interaction state, such as being "clicked". + Button { + /// A callback function executed when the button is clicked. + on_click: fn() + } + + defaults { + on_click: || {} + } + + fn render(&self, state: usize) -> String { + let writer = self.style.use_style(&state); + if state == 2 { + return writer.write_clicked(&self.text); + } + writer.write(&self.text) + } + + fn event(&mut self, _state: usize, k: Key) -> UpdateResponse { + if k == Key::Enter { + (self.on_click)(); + return command!( + Command::Render(2), + Command::Sleep(120) + ); + } + UpdateResponse::None } } element! { - Button { - pub binds: HashMap, - pub on_click: fn(&mut Button), - clicked: bool - } - defaults { - binds: HashMap::from([(KeyKind::Enter, String::from("click"))]), - on_click: |_|{}, - clicked: false, + /// A container element that can hold multiple child elements and handle directional key input. + /// + /// The `Div` element serves as a container for other elements, allowing navigation between them + /// using directional keys. + Div { + pub keybinds: HashMap } - fn update(&mut self, ctx: &mut UpdateContext) { - if let Some(v) = self.binds.get(&ctx.key.kind) { - if v == "click" { - self.clicked = true; - self.add_action(ctx.tick+2, "un_click"); - (self.on_click)(self); + defaults { + keybinds: HashMap::from([ + (Key::Up, Direction::Up), + (Key::Down, Direction::Down), + (Key::Left, Direction::Left), + (Key::Right, Direction::Right), + ]) + } + + fn render(&self, state: usize) -> String { + let mut frame = crate::create_frame(self.width, self.height); + for (i, child) in (&self.children).iter().enumerate() { + if i==self.child { + render_to_frame(state, &mut frame, child); + } else { + render_to_frame(0, &mut frame, child); } } + frame.join("\n") } - fn render(&mut self, tick: usize) -> String { - - if self.get_action(tick)=="un_click" { - self.clicked = false; + fn event(&mut self, state: usize, k: Key) -> UpdateResponse { + if let Some(direction) = self.keybinds.get(&k) { + self.child = crate::closest_component(&self.children, self.child, direction.clone()); + } else if let Some(child) = self.get_child() { + return child.event(state, k); } - - if self.clicked { - // return self.style.write(&format!("{tick}")) - return self.style.write_clicked(&self.text); - } - self.style.write(&self.text) + UpdateResponse::None } - } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 929a714..70509a4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,33 +1,46 @@ +/// 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::*; -/// Creates and returns a `Box` containing a new `Text` element. +/// Creates a new `Text` element. /// -/// This function constructs a `Text` element and wraps it in a `Box` to -/// manage heap allocation and ownership. +/// The `Text` element displays static text in the TUI, suitable for labels or headers. /// /// # Returns /// -/// A boxed `Text` element, initialized and ready to be styled or populated -/// with text content. +/// A `Box` containing a new `Text` element instance. pub fn text() -> Box { Box::new(Text::new()) } -/// Creates and returns a `Box` containing a new `Button` element. +/// Creates a new `Button` element with a customized style. /// -/// This function constructs a `Button` element and wraps it in a `Box` to -/// manage heap allocation and ownership. Buttons can be styled and -/// configured for interactive functionality. +/// The `Button` element responds to user clicks, triggering an action. This function +/// customizes the button's clicked background and foreground colors. /// /// # Returns /// -/// A boxed `Button` element, initialized and ready for configuration. +/// A `Box