Large structural changes

This commit is contained in:
2024-11-05 17:26:19 +01:00
parent 196c6094d2
commit 4f9a143008
10 changed files with 672 additions and 361 deletions
+136
View File
@@ -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<Command>),
}
/// 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),
}
+66 -28
View File
@@ -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()
}
+129 -145
View File
@@ -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<dyn Element>>;
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<dyn Element>,
}
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<dyn Element>) -> App {
let mut app = App::new();
app.set_component(elem);
app
}
/// Sets a component
pub fn set_component(&mut self, element: Box<dyn Element>) {
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<String> = 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<dyn Element>, state: usize) {
let (width, height) = get_term_size();
elem.update_data(width, height);
let mut frame: Vec<String> =
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<dyn Element>, 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<dyn Element>, 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<dyn Element>) {
// 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;
}
}
}
+170 -123
View File
@@ -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<dyn osui::Element>])
}};
($p:expr; $($rest:tt)+) => {{
let mut comps: Vec<Box<dyn osui::Element>> = osui::__rsx!($($rest)+).1;
comps.insert(0, $p as Box<dyn osui::Element>);
(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<Box<dyn osui::Element>> = 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<Box<dyn osui::Element>> = 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<dyn osui::Element>
}};
( $tag:path { $($k:ident: $v:expr),* } ) => {{
let mut c = $tag();
$(
c.$k = $v;
)*
c as Box<dyn osui::Element>
}};
( $tag:path { $($inner:tt)* } ) => {{
let mut c = $tag();
let a = osui::__rsx!($($inner)*);
c.text = a.0;
c.children = a.1;
c as Box<dyn osui::Element>
}};
( $p:expr; ) => {{
$p as Box<dyn osui::Element>
}};
}
#[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<Box<dyn Element>>,
pub child: usize,
pub text: String,
pub style: Style,
pub tick_line: std::collections::HashMap<usize, String>,
$( $inner )*
}
impl Element for $name {
fn get_child(&mut self) -> Option<&mut Box<dyn Element>> {
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<dyn Element>> {
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<dyn osui::Element>
}};
}
/// 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()
}
}};
}
+7 -6
View File
@@ -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<dyn osui::Element> {
rsx! {
text { "Hello, World!" }
}
}
+71 -30
View File
@@ -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<KeyKind, String>,
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<Key, Direction>
}
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
}
}
+23 -10
View File
@@ -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<Text>` containing a new `Text` element instance.
pub fn text() -> Box<Text> {
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<Button>` containing a new `Button` element instance.
pub fn button() -> Box<Button> {
let mut btn = Button::new();
btn.style.clicked_bg = styles::Color::White;
btn.style.clicked_fg = styles::Color::Black;
Box::new(btn)
}
/// Creates a new `Div` container element.
///
/// The `Div` element serves as a container for other UI elements, enabling navigation
/// and grouping of child elements. It supports directional navigation using key bindings.
///
/// # Returns
///
/// A `Box<Div>` containing a new `Div` element instance.
pub fn div() -> Box<Div> {
Box::new(Div::new())
}
+55 -7
View File
@@ -1,64 +1,87 @@
/// Represents a style configuration for TUI elements, including background,
/// foreground, outline colors, and font settings for different element states
/// (hovered, clicked, selected).
#[derive(Debug, Clone, PartialEq)]
pub struct Style {
/// Background color of the element.
pub bg: Color,
/// Foreground color of the element.
pub fg: Color,
/// Outline color of the element.
pub outline: Color,
/// Font style for the element.
pub font: Font,
/// Background color when the element is hovered.
pub hover_bg: Color,
/// Foreground color when the element is hovered.
pub hover_fg: Color,
/// Outline color when the element is hovered.
pub hover_outline: Color,
/// Font style when the element is hovered.
pub hover_font: Font,
/// Foreground color for the cursor when hovered.
pub hover_cursor_fg: Color,
/// Background color for the cursor when hovered.
pub hover_cursor_bg: Color,
/// Background color when the element is clicked.
pub clicked_bg: Color,
/// Foreground color when the element is clicked.
pub clicked_fg: Color,
/// Outline color when the element is clicked.
pub clicked_outline: Color,
/// Font style when the element is clicked.
pub clicked_font: Font,
/// Background color when the element is selected.
pub selected_bg: Color,
/// Foreground color when the element is selected.
pub selected_fg: Color,
/// Font style when the element is selected.
pub selected_font: Font,
/// Foreground color for the cursor.
pub cursor_fg: Color,
/// Background color for the cursor.
pub cursor_bg: Color,
/// Indicates if the style is active, affecting its state-based styling.
pub is_active: bool,
}
impl Default for Style {
/// Creates a default `Style` instance with `None` for all color and font fields
/// and inactive (`is_active` set to false).
fn default() -> Style {
Style {
bg: Color::None,
fg: Color::None,
outline: Color::None,
font: Font::None,
hover_bg: Color::None,
hover_fg: Color::None,
hover_outline: Color::None,
hover_font: Font::None,
hover_cursor_fg: Color::None,
hover_cursor_bg: Color::None,
clicked_bg: Color::None,
clicked_fg: Color::None,
clicked_outline: Color::None,
clicked_font: Font::None,
selected_bg: Color::None,
selected_fg: Color::None,
selected_font: Font::None,
cursor_fg: Color::None,
cursor_bg: Color::None,
is_active: false,
}
}
}
impl Style {
/// Generates the ANSI code for the current style, using the active color
/// and font if `is_active` is true.
pub fn get(&self) -> String {
if self.is_active {
format!(
@@ -77,6 +100,7 @@ impl Style {
}
}
/// Returns the outline color in ANSI format, applying hover style if active.
pub fn get_outline(&self) -> String {
if self.is_active {
self.outline.prioritize(&self.hover_outline).ansi()
@@ -85,6 +109,7 @@ impl Style {
}
}
/// Retrieves the clicked style ANSI sequence for the element.
pub fn get_clicked(&self) -> String {
format!(
"{}{}{}",
@@ -94,6 +119,7 @@ impl Style {
)
}
/// Retrieves the selected style ANSI sequence for the element.
pub fn get_selected(&self) -> String {
format!(
"{}{}{}",
@@ -103,6 +129,7 @@ impl Style {
)
}
/// Retrieves the cursor style ANSI sequence for the element, considering hover.
pub fn get_cursor(&self) -> String {
if self.is_active {
format!(
@@ -115,27 +142,40 @@ impl Style {
}
}
/// Writes the given string with the active style's ANSI sequences.
pub fn write(&self, s: &str) -> String {
format!("{}{}\x1b[0m", self.get(), s)
}
/// Writes a string with the outline color applied.
pub fn write_outline(&self, s: &str) -> String {
format!("{}{}\x1b[0m", self.get_outline(), s)
}
/// Writes a string with the clicked style applied.
pub fn write_clicked(&self, s: &str) -> String {
format!("{}{}\x1b[0m", self.get_clicked(), s)
}
/// Writes a string with the selected style applied.
pub fn write_selected(&self, s: &str) -> String {
format!("{}{}\x1b[0m", self.get_selected(), s)
}
/// Writes a string with the cursor style applied.
pub fn write_cursor(&self, s: &str) -> String {
format!("{}{}\x1b[0m", self.get_cursor(), s)
}
/// Clones the style and sets `is_active` based on the given state.
pub fn use_style(&self, state: &usize) -> Style {
let mut style = self.clone();
style.is_active = *state == 1;
style
}
}
/// Represents different font styles that can be applied to TUI elements.
#[derive(Debug, Clone, PartialEq)]
pub enum Font {
None,
@@ -144,10 +184,11 @@ pub enum Font {
Italic,
Reverse,
Strike,
Mul(Vec<Font>),
Mul(Vec<Font>), // Allows combining multiple font styles.
}
impl Font {
/// Converts the font style to an ANSI sequence.
pub fn ansi(&self) -> String {
String::from(match self {
Font::None => "",
@@ -166,6 +207,8 @@ impl Font {
})
}
/// Returns the prioritized font between `self` and `secondary`, favoring `secondary`
/// if it is not `None`.
pub fn prioritize<'a>(&'a self, secondary: &'a Font) -> &Font {
if secondary == &Font::None {
self
@@ -175,10 +218,11 @@ impl Font {
}
}
/// Represents color options for elements, supporting both named colors and RGB values.
#[derive(Debug, Clone, PartialEq)]
pub enum Color {
None,
Rgb(u8, u8, u8),
Rgb(u8, u8, u8), // RGB color representation.
Black,
Red,
Green,
@@ -190,6 +234,7 @@ pub enum Color {
}
impl Color {
/// Converts the color to an ANSI foreground sequence.
pub fn ansi(&self) -> String {
String::from(match self {
Color::None => "",
@@ -207,6 +252,7 @@ impl Color {
})
}
/// Converts the color to an ANSI background sequence.
pub fn ansi_bg(&self) -> String {
String::from(match self {
Color::None => "",
@@ -224,6 +270,8 @@ impl Color {
})
}
/// Returns the prioritized color between `self` and `secondary`, favoring `secondary`
/// if it is not `None`.
pub fn prioritize<'a>(&'a self, secondary: &'a Color) -> &Color {
if secondary == &Color::None {
self
@@ -231,4 +279,4 @@ impl Color {
secondary
}
}
}
}
+13 -11
View File
@@ -10,14 +10,6 @@ lazy_static! {
pub static ref ANSI: Regex = Regex::new(r"(\x1b\[([0-9;]*)[a-zA-Z])+").unwrap();
}
#[derive(Debug, PartialEq, Clone)]
pub enum Value {
String(String),
Int(i32),
Float(f64),
Bool(bool),
}
/// Compress a string by a regex pattern
pub fn compress_string(input: &str, re: &Regex) -> (String, HashMap<usize, String>) {
let mut matches_map = HashMap::new();
@@ -82,9 +74,9 @@ fn merge_line(frame_: &str, line_: &str, x: usize) -> String {
}
/// Render to a frame
pub fn render_to_frame(tick: usize, frame: &mut Vec<String>, element: &mut Box<dyn Element>) {
pub fn render_to_frame(state: usize, frame: &mut Vec<String>, element: &Box<dyn Element>) {
let data = element.get_data();
for (i, line) in element.render(tick).split('\n').enumerate() {
for (i, line) in element.render(state).split('\n').enumerate() {
if (data.y + i) < frame.len() {
let frame_line = frame.get_mut(data.y + i).unwrap();
*frame_line = merge_line(&frame_line, line, data.x);
@@ -103,7 +95,7 @@ pub fn hide_cursor() {
}
pub fn show_cursor() {
print!("\x1b[?25H");
print!("\x1B[?25h");
stdout().flush().unwrap();
}
@@ -111,6 +103,7 @@ pub fn flush() {
stdout().flush().unwrap();
}
#[derive(Debug, Clone)]
pub enum Direction {
Left,
Right,
@@ -144,3 +137,12 @@ pub fn 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: crate::ElementSize, height: crate::ElementSize) -> Vec<String> {
vec![" ".repeat(width.get_size()); height.get_size()]
}
pub fn get_term_size() -> (usize, usize) {
let (width, height) = crossterm::terminal::size().unwrap();
(width as usize, height as usize)
}