Large structural changes
This commit is contained in:
+2
-1
@@ -17,4 +17,5 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
crossterm = "0.28.1"
|
crossterm = "0.28.1"
|
||||||
regex = "*"
|
regex = "*"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
|
dyn-clone = "1.0.17"
|
||||||
|
|||||||
+136
@@ -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
@@ -1,51 +1,89 @@
|
|||||||
use std::io::{self, Read};
|
use std::io::{self, Read};
|
||||||
|
|
||||||
|
/// Represents different key inputs that can be detected from the terminal.
|
||||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||||
pub enum KeyKind {
|
pub enum Key {
|
||||||
|
/// Represents the Enter key (carriage return).
|
||||||
Enter,
|
Enter,
|
||||||
|
/// Represents the Tab key.
|
||||||
Tab,
|
Tab,
|
||||||
|
/// Represents the Shift+Tab key combination.
|
||||||
ShiftTab,
|
ShiftTab,
|
||||||
|
/// Represents the Escape key.
|
||||||
Escape,
|
Escape,
|
||||||
|
/// Represents the Up arrow key.
|
||||||
Up,
|
Up,
|
||||||
|
/// Represents the Down arrow key.
|
||||||
Down,
|
Down,
|
||||||
|
/// Represents the Left arrow key.
|
||||||
Left,
|
Left,
|
||||||
|
/// Represents the Right arrow key.
|
||||||
Right,
|
Right,
|
||||||
Char(String),
|
/// Represents any other character or sequence that does not match a predefined key.
|
||||||
}
|
Other(String),
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
|
||||||
pub struct Key {
|
|
||||||
pub kind: KeyKind,
|
|
||||||
pub raw: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Key {
|
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 {
|
pub fn new(k: String) -> Key {
|
||||||
Key {
|
match k.as_str() {
|
||||||
raw: k.clone(),
|
"\r" => Key::Enter,
|
||||||
kind: match k.as_str() {
|
"\t" => Key::Tab,
|
||||||
"\r" => KeyKind::Enter,
|
"\x1b[Z" => Key::ShiftTab,
|
||||||
"\t" => KeyKind::Tab,
|
"\x1b" => Key::Escape,
|
||||||
"\x1b[Z" => KeyKind::ShiftTab,
|
"\x1b[A" => Key::Up,
|
||||||
"\x1b" => KeyKind::Escape,
|
"\x1b[B" => Key::Down,
|
||||||
"\x1b[A" => KeyKind::Up,
|
"\x1b[C" => Key::Right,
|
||||||
"\x1b[B" => KeyKind::Down,
|
"\x1b[D" => Key::Left,
|
||||||
"\x1b[C" => KeyKind::Right,
|
_ => Key::Other(k),
|
||||||
"\x1b[D" => KeyKind::Left,
|
|
||||||
|
|
||||||
_ => KeyKind::Char(k.clone()),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
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];
|
let mut buffer = vec![0; 3];
|
||||||
io::stdin().read(&mut buffer).unwrap();
|
io::stdin().read(&mut buffer).unwrap();
|
||||||
Key::new(
|
String::from_utf8(buffer)
|
||||||
String::from_utf8(buffer)
|
.unwrap()
|
||||||
.unwrap()
|
.trim_matches('\0')
|
||||||
.trim_matches('\0')
|
.to_string()
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+129
-145
@@ -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 key;
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub trait Element: std::fmt::Debug {
|
pub use element::*;
|
||||||
fn get_child(&mut self) -> Option<&mut Box<dyn Element>>;
|
pub use utils::*;
|
||||||
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) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub mod app {
|
||||||
pub struct ElementData {
|
//! Application entry point and main event loop for OSUI.
|
||||||
pub x: usize,
|
//!
|
||||||
pub y: usize,
|
//! Provides functions to render and update UI elements based on keyboard
|
||||||
pub width: usize,
|
//! input. Manages cursor visibility, terminal size, and controls UI behavior
|
||||||
pub height: usize,
|
//! using custom commands such as rendering, updating, or exiting.
|
||||||
pub style: ui::styles::Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
use crate::{
|
||||||
pub enum UpdateResponse {
|
clear, create_frame, flush, get_term_size, hide_cursor,
|
||||||
Exit,
|
key::{read_key, Key},
|
||||||
Done,
|
render_to_frame, show_cursor, Command, Element, ElementSize, UpdateResponse,
|
||||||
None,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UpdateContext {
|
/// Renders a single frame of the UI to the terminal.
|
||||||
pub key: key::Key,
|
///
|
||||||
pub tick: usize,
|
/// Sets up a new frame based on the terminal's current size, updates
|
||||||
pub response: UpdateResponse,
|
/// the element dimensions, and renders the UI element to the frame.
|
||||||
}
|
///
|
||||||
|
/// # Arguments
|
||||||
pub struct App {
|
///
|
||||||
element: Box<dyn Element>,
|
/// * `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) {
|
||||||
impl App {
|
let (width, height) = get_term_size();
|
||||||
/// Creates a new screen to render components
|
elem.update_data(width, height);
|
||||||
pub fn new() -> App {
|
let mut frame: Vec<String> =
|
||||||
App {
|
create_frame(ElementSize::Custom(width), ElementSize::Custom(height));
|
||||||
element: ui::text(),
|
render_to_frame(state, &mut frame, elem);
|
||||||
}
|
clear();
|
||||||
}
|
|
||||||
|
|
||||||
/// 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();
|
|
||||||
print!("{}", frame.join(""));
|
print!("{}", frame.join(""));
|
||||||
utils::flush();
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &mut UpdateContext) {
|
/// Updates the UI element based on keyboard input and issues any commands in response.
|
||||||
self.element.update(ctx);
|
///
|
||||||
match ctx.response {
|
/// Processes the result of `Element::update` to handle commands like rendering,
|
||||||
UpdateResponse::Exit => {
|
/// updating, and exiting. Commands can be a single action or a list of actions.
|
||||||
crossterm::terminal::disable_raw_mode().unwrap();
|
///
|
||||||
utils::clear();
|
/// # Arguments
|
||||||
utils::show_cursor();
|
///
|
||||||
println!("");
|
/// * `elem` - A mutable reference to a boxed UI element.
|
||||||
return;
|
/// * `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.
|
||||||
/// Run the screen
|
fn update(elem: &mut Box<dyn Element>, state: usize, k: Key) -> bool {
|
||||||
pub fn run(&mut self) {
|
match elem.event(state, k.clone()) {
|
||||||
// Initialize
|
UpdateResponse::Command(command) => run_command(command, elem, k.clone()),
|
||||||
utils::hide_cursor();
|
UpdateResponse::CommandList(commands) => {
|
||||||
utils::clear();
|
for command in commands {
|
||||||
let mut stdout = std::io::stdout();
|
if run_command(command, elem, k.clone()) {
|
||||||
stdout
|
return true;
|
||||||
.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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
if let Some(remaining) = tick_duration.checked_sub(elapsed) {
|
_ => false,
|
||||||
std::thread::sleep(remaining);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
@@ -1,175 +1,222 @@
|
|||||||
#[macro_export]
|
//! # Macros Module
|
||||||
macro_rules! __rsx {
|
//!
|
||||||
() => {
|
//! This module defines various macros for creating and manipulating OSUI elements.
|
||||||
(String::new(), Vec::new())
|
//! The macros provide a clean and concise syntax for defining UI components and
|
||||||
};
|
//! handling their parameters, commands, and rendering.
|
||||||
|
|
||||||
($p:expr;) => {{
|
/// Macro for defining an OSUI `Element`.
|
||||||
(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)```
|
|
||||||
///
|
///
|
||||||
/// Create a frame for rendering multiple components
|
/// This macro generates a new element type with specified parameters and default values.
|
||||||
macro_rules! create_frame {
|
///
|
||||||
($width:expr, $height:expr) => {
|
/// # Example
|
||||||
vec![" ".repeat($width); $height]
|
/// ```
|
||||||
};
|
/// element! {
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
///```component!{
|
|
||||||
/// MyElem {}
|
/// MyElem {}
|
||||||
/// defaults {}
|
/// 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 {
|
macro_rules! element {
|
||||||
(
|
(
|
||||||
|
$(#[$meta:meta])*
|
||||||
$name:ident {
|
$name:ident {
|
||||||
$( $inner:tt )*
|
$( $inner:tt )*
|
||||||
}
|
}
|
||||||
defaults {$( $defaults:tt )*}
|
defaults {$( $defaults:tt )*}
|
||||||
$( $functions:tt )*
|
$( $functions:tt )*
|
||||||
) => {
|
) => {
|
||||||
#[derive(Debug)]
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct $name {
|
pub struct $name {
|
||||||
pub x: usize,
|
pub x: usize,
|
||||||
pub y: usize,
|
pub y: usize,
|
||||||
pub width: usize,
|
pub width: ElementSize,
|
||||||
pub height: usize,
|
pub height: ElementSize,
|
||||||
pub children: Vec<Box<dyn Element>>,
|
pub children: Vec<Box<dyn Element>>,
|
||||||
pub child: usize,
|
pub child: usize,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub style: Style,
|
pub style: Style,
|
||||||
pub tick_line: std::collections::HashMap<usize, String>,
|
|
||||||
$( $inner )*
|
$( $inner )*
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for $name {
|
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 {
|
fn get_data(&self) -> ElementData {
|
||||||
ElementData {
|
ElementData {
|
||||||
x: self.x,
|
x: self.x,
|
||||||
y: self.y,
|
y: self.y,
|
||||||
width: self.width,
|
width: self.width.clone(),
|
||||||
height: self.height,
|
height: self.height.clone(),
|
||||||
style: self.style.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_data(&mut self, data: ElementData) {
|
fn update_data(&mut self, width: usize, height: usize) {
|
||||||
self.x = data.x;
|
self.width.try_set_size(width);
|
||||||
self.y = data.y;
|
self.height.try_set_size(height);
|
||||||
self.width = data.width;
|
for child in &mut self.children {
|
||||||
self.height = data.height;
|
child.update_data(width, height);
|
||||||
self.style = data.style;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_ticks(&mut self) {
|
|
||||||
self.tick_line.clear();
|
|
||||||
}
|
}
|
||||||
$( $functions )*
|
$( $functions )*
|
||||||
}
|
}
|
||||||
|
|
||||||
impl $name {
|
impl $name {
|
||||||
|
/// Creates a new instance of the element with default values.
|
||||||
pub fn new() -> $name {
|
pub fn new() -> $name {
|
||||||
$name {
|
$name {
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 0,
|
width: ElementSize::Default(0),
|
||||||
height: 0,
|
height: ElementSize::Default(0),
|
||||||
child: 0,
|
child: 0,
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
style: Style::default(),
|
style: Style::default(),
|
||||||
tick_line: std::collections::HashMap::new(),
|
|
||||||
$( $defaults )*
|
$( $defaults )*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_action(&self, tick: usize) -> String {
|
/// Retrieves a mutable reference to the current child element, if any.
|
||||||
match self.tick_line.get(&tick) {
|
///
|
||||||
Some(action) => action.clone(),
|
/// # Returns
|
||||||
None => String::new(),
|
/// 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)
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
@@ -1,10 +1,11 @@
|
|||||||
use osui::{rsx, ui::*, App};
|
use osui::{rsx, ui::*};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let element = rsx! {
|
osui::app::run(&mut app());
|
||||||
button { "Hello, World!" }
|
}
|
||||||
};
|
|
||||||
|
|
||||||
let mut app_screen = App::from(element);
|
fn app() -> Box<dyn osui::Element> {
|
||||||
app_screen.run();
|
rsx! {
|
||||||
|
text { "Hello, World!" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-30
@@ -1,49 +1,90 @@
|
|||||||
use std::collections::HashMap;
|
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! {
|
element! {
|
||||||
|
/// A text element for displaying static text in the TUI.
|
||||||
|
///
|
||||||
|
/// The `Text` element displays text and does not respond to user interactions.
|
||||||
Text {}
|
Text {}
|
||||||
defaults {}
|
defaults {}
|
||||||
fn render(&mut self, tick: usize) -> String {
|
fn render(&self, _: usize) -> String {
|
||||||
// self.style.write(&self.text)
|
self.text.clone()
|
||||||
self.style.write(&format!("{tick}"))
|
}
|
||||||
|
}
|
||||||
|
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! {
|
element! {
|
||||||
Button {
|
/// A container element that can hold multiple child elements and handle directional key input.
|
||||||
pub binds: HashMap<KeyKind, String>,
|
///
|
||||||
pub on_click: fn(&mut Button),
|
/// The `Div` element serves as a container for other elements, allowing navigation between them
|
||||||
clicked: bool
|
/// using directional keys.
|
||||||
}
|
Div {
|
||||||
defaults {
|
pub keybinds: HashMap<Key, Direction>
|
||||||
binds: HashMap::from([(KeyKind::Enter, String::from("click"))]),
|
|
||||||
on_click: |_|{},
|
|
||||||
clicked: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &mut UpdateContext) {
|
defaults {
|
||||||
if let Some(v) = self.binds.get(&ctx.key.kind) {
|
keybinds: HashMap::from([
|
||||||
if v == "click" {
|
(Key::Up, Direction::Up),
|
||||||
self.clicked = true;
|
(Key::Down, Direction::Down),
|
||||||
self.add_action(ctx.tick+2, "un_click");
|
(Key::Left, Direction::Left),
|
||||||
(self.on_click)(self);
|
(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 {
|
fn event(&mut self, state: usize, k: Key) -> UpdateResponse {
|
||||||
|
if let Some(direction) = self.keybinds.get(&k) {
|
||||||
if self.get_action(tick)=="un_click" {
|
self.child = crate::closest_component(&self.children, self.child, direction.clone());
|
||||||
self.clicked = false;
|
} else if let Some(child) = self.get_child() {
|
||||||
|
return child.event(state, k);
|
||||||
}
|
}
|
||||||
|
UpdateResponse::None
|
||||||
if self.clicked {
|
|
||||||
// return self.style.write(&format!("{tick}"))
|
|
||||||
return self.style.write_clicked(&self.text);
|
|
||||||
}
|
|
||||||
self.style.write(&self.text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-10
@@ -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 mod styles;
|
||||||
pub use styles::*;
|
pub use styles::*;
|
||||||
pub mod elements;
|
pub mod elements;
|
||||||
pub use 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
|
/// The `Text` element displays static text in the TUI, suitable for labels or headers.
|
||||||
/// manage heap allocation and ownership.
|
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A boxed `Text` element, initialized and ready to be styled or populated
|
/// A `Box<Text>` containing a new `Text` element instance.
|
||||||
/// with text content.
|
|
||||||
pub fn text() -> Box<Text> {
|
pub fn text() -> Box<Text> {
|
||||||
Box::new(Text::new())
|
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
|
/// The `Button` element responds to user clicks, triggering an action. This function
|
||||||
/// manage heap allocation and ownership. Buttons can be styled and
|
/// customizes the button's clicked background and foreground colors.
|
||||||
/// configured for interactive functionality.
|
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A boxed `Button` element, initialized and ready for configuration.
|
/// A `Box<Button>` containing a new `Button` element instance.
|
||||||
pub fn button() -> Box<Button> {
|
pub fn button() -> Box<Button> {
|
||||||
let mut btn = Button::new();
|
let mut btn = Button::new();
|
||||||
btn.style.clicked_bg = styles::Color::White;
|
btn.style.clicked_bg = styles::Color::White;
|
||||||
btn.style.clicked_fg = styles::Color::Black;
|
btn.style.clicked_fg = styles::Color::Black;
|
||||||
Box::new(btn)
|
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
@@ -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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
|
/// Background color of the element.
|
||||||
pub bg: Color,
|
pub bg: Color,
|
||||||
|
/// Foreground color of the element.
|
||||||
pub fg: Color,
|
pub fg: Color,
|
||||||
|
/// Outline color of the element.
|
||||||
pub outline: Color,
|
pub outline: Color,
|
||||||
|
/// Font style for the element.
|
||||||
pub font: Font,
|
pub font: Font,
|
||||||
|
|
||||||
|
/// Background color when the element is hovered.
|
||||||
pub hover_bg: Color,
|
pub hover_bg: Color,
|
||||||
|
/// Foreground color when the element is hovered.
|
||||||
pub hover_fg: Color,
|
pub hover_fg: Color,
|
||||||
|
/// Outline color when the element is hovered.
|
||||||
pub hover_outline: Color,
|
pub hover_outline: Color,
|
||||||
|
/// Font style when the element is hovered.
|
||||||
pub hover_font: Font,
|
pub hover_font: Font,
|
||||||
|
/// Foreground color for the cursor when hovered.
|
||||||
pub hover_cursor_fg: Color,
|
pub hover_cursor_fg: Color,
|
||||||
|
/// Background color for the cursor when hovered.
|
||||||
pub hover_cursor_bg: Color,
|
pub hover_cursor_bg: Color,
|
||||||
|
|
||||||
|
/// Background color when the element is clicked.
|
||||||
pub clicked_bg: Color,
|
pub clicked_bg: Color,
|
||||||
|
/// Foreground color when the element is clicked.
|
||||||
pub clicked_fg: Color,
|
pub clicked_fg: Color,
|
||||||
|
/// Outline color when the element is clicked.
|
||||||
pub clicked_outline: Color,
|
pub clicked_outline: Color,
|
||||||
|
/// Font style when the element is clicked.
|
||||||
pub clicked_font: Font,
|
pub clicked_font: Font,
|
||||||
|
|
||||||
|
/// Background color when the element is selected.
|
||||||
pub selected_bg: Color,
|
pub selected_bg: Color,
|
||||||
|
/// Foreground color when the element is selected.
|
||||||
pub selected_fg: Color,
|
pub selected_fg: Color,
|
||||||
|
/// Font style when the element is selected.
|
||||||
pub selected_font: Font,
|
pub selected_font: Font,
|
||||||
|
|
||||||
|
/// Foreground color for the cursor.
|
||||||
pub cursor_fg: Color,
|
pub cursor_fg: Color,
|
||||||
|
/// Background color for the cursor.
|
||||||
pub cursor_bg: Color,
|
pub cursor_bg: Color,
|
||||||
|
|
||||||
|
/// Indicates if the style is active, affecting its state-based styling.
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Style {
|
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 {
|
fn default() -> Style {
|
||||||
Style {
|
Style {
|
||||||
bg: Color::None,
|
bg: Color::None,
|
||||||
fg: Color::None,
|
fg: Color::None,
|
||||||
outline: Color::None,
|
outline: Color::None,
|
||||||
font: Font::None,
|
font: Font::None,
|
||||||
|
|
||||||
hover_bg: Color::None,
|
hover_bg: Color::None,
|
||||||
hover_fg: Color::None,
|
hover_fg: Color::None,
|
||||||
hover_outline: Color::None,
|
hover_outline: Color::None,
|
||||||
hover_font: Font::None,
|
hover_font: Font::None,
|
||||||
hover_cursor_fg: Color::None,
|
hover_cursor_fg: Color::None,
|
||||||
hover_cursor_bg: Color::None,
|
hover_cursor_bg: Color::None,
|
||||||
|
|
||||||
clicked_bg: Color::None,
|
clicked_bg: Color::None,
|
||||||
clicked_fg: Color::None,
|
clicked_fg: Color::None,
|
||||||
clicked_outline: Color::None,
|
clicked_outline: Color::None,
|
||||||
clicked_font: Font::None,
|
clicked_font: Font::None,
|
||||||
|
|
||||||
selected_bg: Color::None,
|
selected_bg: Color::None,
|
||||||
selected_fg: Color::None,
|
selected_fg: Color::None,
|
||||||
selected_font: Font::None,
|
selected_font: Font::None,
|
||||||
cursor_fg: Color::None,
|
cursor_fg: Color::None,
|
||||||
cursor_bg: Color::None,
|
cursor_bg: Color::None,
|
||||||
|
|
||||||
is_active: false,
|
is_active: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Style {
|
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 {
|
pub fn get(&self) -> String {
|
||||||
if self.is_active {
|
if self.is_active {
|
||||||
format!(
|
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 {
|
pub fn get_outline(&self) -> String {
|
||||||
if self.is_active {
|
if self.is_active {
|
||||||
self.outline.prioritize(&self.hover_outline).ansi()
|
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 {
|
pub fn get_clicked(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
@@ -94,6 +119,7 @@ impl Style {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the selected style ANSI sequence for the element.
|
||||||
pub fn get_selected(&self) -> String {
|
pub fn get_selected(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
@@ -103,6 +129,7 @@ impl Style {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the cursor style ANSI sequence for the element, considering hover.
|
||||||
pub fn get_cursor(&self) -> String {
|
pub fn get_cursor(&self) -> String {
|
||||||
if self.is_active {
|
if self.is_active {
|
||||||
format!(
|
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 {
|
pub fn write(&self, s: &str) -> String {
|
||||||
format!("{}{}\x1b[0m", self.get(), s)
|
format!("{}{}\x1b[0m", self.get(), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes a string with the outline color applied.
|
||||||
pub fn write_outline(&self, s: &str) -> String {
|
pub fn write_outline(&self, s: &str) -> String {
|
||||||
format!("{}{}\x1b[0m", self.get_outline(), s)
|
format!("{}{}\x1b[0m", self.get_outline(), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes a string with the clicked style applied.
|
||||||
pub fn write_clicked(&self, s: &str) -> String {
|
pub fn write_clicked(&self, s: &str) -> String {
|
||||||
format!("{}{}\x1b[0m", self.get_clicked(), s)
|
format!("{}{}\x1b[0m", self.get_clicked(), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes a string with the selected style applied.
|
||||||
pub fn write_selected(&self, s: &str) -> String {
|
pub fn write_selected(&self, s: &str) -> String {
|
||||||
format!("{}{}\x1b[0m", self.get_selected(), s)
|
format!("{}{}\x1b[0m", self.get_selected(), s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Writes a string with the cursor style applied.
|
||||||
pub fn write_cursor(&self, s: &str) -> String {
|
pub fn write_cursor(&self, s: &str) -> String {
|
||||||
format!("{}{}\x1b[0m", self.get_cursor(), s)
|
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Font {
|
pub enum Font {
|
||||||
None,
|
None,
|
||||||
@@ -144,10 +184,11 @@ pub enum Font {
|
|||||||
Italic,
|
Italic,
|
||||||
Reverse,
|
Reverse,
|
||||||
Strike,
|
Strike,
|
||||||
Mul(Vec<Font>),
|
Mul(Vec<Font>), // Allows combining multiple font styles.
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
|
/// Converts the font style to an ANSI sequence.
|
||||||
pub fn ansi(&self) -> String {
|
pub fn ansi(&self) -> String {
|
||||||
String::from(match self {
|
String::from(match self {
|
||||||
Font::None => "",
|
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 {
|
pub fn prioritize<'a>(&'a self, secondary: &'a Font) -> &Font {
|
||||||
if secondary == &Font::None {
|
if secondary == &Font::None {
|
||||||
self
|
self
|
||||||
@@ -175,10 +218,11 @@ impl Font {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents color options for elements, supporting both named colors and RGB values.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Color {
|
pub enum Color {
|
||||||
None,
|
None,
|
||||||
Rgb(u8, u8, u8),
|
Rgb(u8, u8, u8), // RGB color representation.
|
||||||
Black,
|
Black,
|
||||||
Red,
|
Red,
|
||||||
Green,
|
Green,
|
||||||
@@ -190,6 +234,7 @@ pub enum Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Color {
|
impl Color {
|
||||||
|
/// Converts the color to an ANSI foreground sequence.
|
||||||
pub fn ansi(&self) -> String {
|
pub fn ansi(&self) -> String {
|
||||||
String::from(match self {
|
String::from(match self {
|
||||||
Color::None => "",
|
Color::None => "",
|
||||||
@@ -207,6 +252,7 @@ impl Color {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts the color to an ANSI background sequence.
|
||||||
pub fn ansi_bg(&self) -> String {
|
pub fn ansi_bg(&self) -> String {
|
||||||
String::from(match self {
|
String::from(match self {
|
||||||
Color::None => "",
|
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 {
|
pub fn prioritize<'a>(&'a self, secondary: &'a Color) -> &Color {
|
||||||
if secondary == &Color::None {
|
if secondary == &Color::None {
|
||||||
self
|
self
|
||||||
@@ -231,4 +279,4 @@ impl Color {
|
|||||||
secondary
|
secondary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-11
@@ -10,14 +10,6 @@ lazy_static! {
|
|||||||
pub static ref ANSI: Regex = Regex::new(r"(\x1b\[([0-9;]*)[a-zA-Z])+").unwrap();
|
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
|
/// Compress a string by a regex pattern
|
||||||
pub fn compress_string(input: &str, re: &Regex) -> (String, HashMap<usize, String>) {
|
pub fn compress_string(input: &str, re: &Regex) -> (String, HashMap<usize, String>) {
|
||||||
let mut matches_map = HashMap::new();
|
let mut matches_map = HashMap::new();
|
||||||
@@ -82,9 +74,9 @@ fn merge_line(frame_: &str, line_: &str, x: usize) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render to a frame
|
/// 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();
|
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() {
|
if (data.y + i) < frame.len() {
|
||||||
let frame_line = frame.get_mut(data.y + i).unwrap();
|
let frame_line = frame.get_mut(data.y + i).unwrap();
|
||||||
*frame_line = merge_line(&frame_line, line, data.x);
|
*frame_line = merge_line(&frame_line, line, data.x);
|
||||||
@@ -103,7 +95,7 @@ pub fn hide_cursor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn show_cursor() {
|
pub fn show_cursor() {
|
||||||
print!("\x1b[?25H");
|
print!("\x1B[?25h");
|
||||||
stdout().flush().unwrap();
|
stdout().flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +103,7 @@ pub fn flush() {
|
|||||||
stdout().flush().unwrap();
|
stdout().flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
Left,
|
Left,
|
||||||
Right,
|
Right,
|
||||||
@@ -144,3 +137,12 @@ pub fn closest_component(
|
|||||||
.map(|(index, _)| index) // Return the index of 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
|
.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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user