Version 0.1.1
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# Building a Comprehensive Demo Application
|
||||
|
||||
This guide walks through the `src/demos/mod.rs` file provided in the OSUI source, explaining how various features are combined to create a multi-page interactive application. This demo showcases reactive state, input handling, custom elements, and layout components.
|
||||
|
||||
## Overview of `src/demos/mod.rs`
|
||||
|
||||
The `demos::app` function returns an `Rsx` tree, which is the declarative representation of our UI. This `Rsx` tree is then drawn to the `Screen`.
|
||||
|
||||
```rust
|
||||
// src/demos/mod.rs
|
||||
use std::sync::Arc;
|
||||
use osui::prelude::*;
|
||||
|
||||
pub fn app(screen: Arc<Screen>) -> Rsx {
|
||||
// 1. Reactive State
|
||||
let count = use_state(0);
|
||||
|
||||
// Spawn a background thread to increment the counter
|
||||
std::thread::spawn({
|
||||
let count = count.clone();
|
||||
move || loop {
|
||||
**count.get() += 1; // Increment and mark as changed
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Main UI Definition
|
||||
rsx! {
|
||||
// Global event handler for Escape key to close the app
|
||||
@Handler::new({
|
||||
let screen = screen.clone();
|
||||
move |_, e: &crossterm::event::Event| {
|
||||
if let crossterm::event::Event::Key(crossterm::event::KeyEvent { code, .. }) = e {
|
||||
if *code == crossterm::event::KeyCode::Esc {
|
||||
screen.close();
|
||||
}
|
||||
}
|
||||
}});
|
||||
// Keep the root Paginator widget always focused
|
||||
@AlwaysFocused;
|
||||
Paginator { // Main page manager
|
||||
// --- Page 1: Welcome and Instructions ---
|
||||
FlexRow { // Horizontal layout for elements on this page
|
||||
Heading, smooth: false, { "OSUI" } // Large ASCII art title
|
||||
"Welcome to the OSUI demo!"
|
||||
"Press tab to switch to the next page or shift+tab to the previous page"
|
||||
}
|
||||
|
||||
// --- Page 2: Divs with different outlines ---
|
||||
FlexCol, gap: 3, { // Vertical layout for elements on this page
|
||||
@Transform::new().padding(2, 2);
|
||||
@Style { foreground: None, background: Background::RoundedOutline(0x00ff00) };
|
||||
Div {
|
||||
"This is text inside a div with rounded outlines"
|
||||
}
|
||||
|
||||
@Transform::new().padding(2, 2);
|
||||
@Style { foreground: None, background: Background::Outline(0x00ff00) };
|
||||
Div {
|
||||
"This is text inside a div with square outlines"
|
||||
}
|
||||
}
|
||||
|
||||
// --- Page 3: Reactive Counter & Input Fields ---
|
||||
FlexRow, gap: 1, {
|
||||
%count // This section depends on the 'count' state
|
||||
"This will increment every second: {count}"
|
||||
|
||||
FlexRow { // Nested FlexRow for Username input
|
||||
"Username"
|
||||
@Transform::new().padding(1, 1).dimensions(40, 1);
|
||||
@Style { foreground: Some(0xffffff), background: Background::RoundedOutline(0xff0000) };
|
||||
@Focused; // Initially focus this input field
|
||||
Input { }
|
||||
}
|
||||
|
||||
@Transform::new().margin(0, 1); // Add a margin below previous element
|
||||
FlexRow { // Nested FlexRow for Password input
|
||||
"Password"
|
||||
@Transform::new().padding(1, 1).dimensions(40, 1);
|
||||
@Style { foreground: Some(0xffffff), background: Background::RoundedOutline(0xffff00) };
|
||||
Input { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dissecting the Demo
|
||||
|
||||
### 1. Central `Screen` and Extensions
|
||||
|
||||
* In `src/main.rs`, the `Screen` is initialized, and `InputExtension` and `RelativeFocusExtension` are registered.
|
||||
* `InputExtension` is vital as it enables raw mode terminal input and dispatches `crossterm::event::Event`s throughout the OSUI system. Without it, keyboard input won't work.
|
||||
* `RelativeFocusExtension` manages which widget is "focused," which is essential for `Input` elements to receive typing events. It also provides navigation between focusable elements (by default, using arrow keys when Shift is held).
|
||||
* The `demos::app(screen.clone()).draw(&screen);` line is the entry point for rendering the entire UI structure.
|
||||
|
||||
### 2. Global Event Handling (`@Handler` and `Esc` Key)
|
||||
|
||||
* The very first element in `rsx!` (outside the `Paginator`) is a `Handler` component attached to an implicit root widget.
|
||||
* `@Handler::new(...)` creates an event handler that listens for `crossterm::event::Event`s.
|
||||
* The closure checks if the event is a `KeyCode::Esc` key press. If so, it calls `screen.close()`, which gracefully exits the OSUI application, restoring terminal state.
|
||||
* `@AlwaysFocused` ensures this root handler always receives events, regardless of which sub-widget might have specific input focus.
|
||||
|
||||
### 3. Page Navigation with `Paginator`
|
||||
|
||||
* The top-level element is a `Paginator`. This element acts as a simple tab-like interface.
|
||||
* Each direct child of `Paginator` becomes a "page." The `Paginator` displays only one child at a time.
|
||||
* Pressing `Tab` (or `Shift+Tab`) cycles through the `Paginator`'s children, switching the active page. This behavior is built into the `Paginator` element's `event` method.
|
||||
|
||||
### 4. Layout with `FlexRow` and `FlexCol`
|
||||
|
||||
* Each "page" within the `Paginator` uses either `FlexRow` or `FlexCol` to organize its content.
|
||||
* `FlexRow { ... }`: Arranges children horizontally. Used for the welcome message and the counter/input fields.
|
||||
* `FlexCol, gap: 3, { ... }`: Arranges children vertically. Used for the Div examples, with a `gap` of 3 cells between each child.
|
||||
* These flex containers automatically calculate their own size based on their children and distribute space. They are "ghost" elements, meaning they don't draw anything themselves but define layout for their children.
|
||||
|
||||
### 5. Styling and Layout Components (`@Transform`, `@Style`)
|
||||
|
||||
* **`@Transform::new().padding(2, 2)`**: Sets internal spacing of 2 cells on all sides for the `Div` elements.
|
||||
* **`@Style { background: Background::RoundedOutline(0x00ff00) }`**: Applies a green rounded outline background to the first `Div`. `0x00ff00` is a 24-bit RGB hexadecimal color code.
|
||||
* **`@Style { background: Background::Outline(0x00ff00) }`**: Applies a green square outline background to the second `Div`.
|
||||
* **`@Transform::new().dimensions(40, 1)`**: Sets the `Input` fields to a fixed width of 40 cells and a height of 1 cell.
|
||||
* **`@Style { foreground: Some(0xffffff), background: Background::RoundedOutline(0xff0000) }`**: Styles the username input with white foreground text and a red rounded outline.
|
||||
|
||||
### 6. Reactive State and Text Interpolation (`%count`)
|
||||
|
||||
* `let count = use_state(0);` initializes a reactive state variable.
|
||||
* The `std::thread::spawn` block modifies `count` every second (`**count.get() += 1;`). This modification automatically marks `count` as "changed."
|
||||
* `%count` (on the `FlexRow` containing the counter text) declares a dependency. When `count` changes, this `FlexRow` (and its children, including the text) is rebuilt and re-rendered, displaying the updated value.
|
||||
* The text `"This will increment every second: {count}"` directly interpolates the `count` state's value. This works because `State<T>` implements `Display`.
|
||||
|
||||
### 7. Interactive Input (`Input` Element)
|
||||
|
||||
* `Input { }` creates a text input field.
|
||||
* `@Focused` on the "Username" `Input` ensures it receives keyboard focus when that page is active, allowing the user to type immediately. The `RelativeFocusExtension` helps manage this focus.
|
||||
* When an `Input` is focused, typing characters modifies its internal `State<String>`, and it redraws to show the updated text and cursor.
|
||||
|
||||
This demo illustrates a comprehensive use of OSUI's features, demonstrating how declarative UI, reactive state, event handling, and layout work together to build a functional TUI application.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Common Built-in Elements
|
||||
|
||||
OSUI provides a set of essential UI elements that serve as building blocks for your terminal interfaces. These elements range from simple text to complex layout containers and interactive input fields.
|
||||
|
||||
## 1. Text (`String` Element)
|
||||
|
||||
The simplest element is a `String`. Any string literal or `format!` macro output directly within `rsx!` will be treated as a renderable text element.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
"Hello, World!" // Renders "Hello, World!"
|
||||
format!("The current count is: {}", 123) // Dynamic text
|
||||
}
|
||||
```
|
||||
**Behavior**: Draws the string at its calculated position. It's a "ghost" element (`is_ghost` returns `true`), meaning it primarily contributes content for layout but doesn't have its own background or border (unless a `Style` component is explicitly attached to its parent that applies to its area). Its size contributes to `Dimension::Content` calculations of its parent.
|
||||
|
||||
## 2. `Div` (Generic Container)
|
||||
|
||||
The `Div` element is a versatile, transparent container. It doesn't draw anything itself but is crucial for grouping other elements and applying layout and style components to a collection of children.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
@Transform::new().padding(2, 2);
|
||||
@Style { background: Background::Solid(0x222222) };
|
||||
Div {
|
||||
"This text is inside a gray Div with padding."
|
||||
FlexRow { "Another", "element", "can", "be", "nested" }
|
||||
}
|
||||
}
|
||||
```
|
||||
**Behavior**: `Div` is a "ghost" element. Its `after_render` method iterates through its children and renders them. It automatically calculates its own `width` and `height` to encompass all its children, plus any `padding` specified in its `Transform`.
|
||||
|
||||
## 3. Flex Containers (`FlexRow` and `FlexCol`)
|
||||
|
||||
Flex containers are powerful layout elements that automatically arrange their children either horizontally (`FlexRow`) or vertically (`FlexCol`), with optional gaps between them.
|
||||
|
||||
### `FlexRow`
|
||||
|
||||
Arranges children in a row (horizontally).
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
@Transform::new().padding(1, 1).dimensions(Full, Content);
|
||||
@Style { background: Background::Outline(0x00FF00) };
|
||||
FlexRow, gap: 2, { // `gap` property sets spacing between children
|
||||
"Item 1"
|
||||
"Item 2"
|
||||
@Style { foreground: Some(0xFFFF00) };
|
||||
"Item 3 (Yellow)"
|
||||
Div { "Nested div as item 4" }
|
||||
}
|
||||
}
|
||||
```
|
||||
**Behavior**: `FlexRow` is a "ghost" element. Its `after_render` dynamically positions each child next to the previous one, accounting for the `gap` property. The `FlexRow`'s `width` will be the sum of its children's widths (plus gaps and padding), and its `height` will be the height of its tallest child.
|
||||
|
||||
### `FlexCol`
|
||||
|
||||
Arranges children in a column (vertically).
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
@Transform::new().padding(1, 1).dimensions(Content, Full);
|
||||
@Style { background: Background::Outline(0xFF00FF) };
|
||||
FlexCol, gap: 1, {
|
||||
"First Line"
|
||||
"Second Line"
|
||||
Input { } // An input field on a new line
|
||||
Div { "A div below the input" }
|
||||
}
|
||||
}
|
||||
```
|
||||
**Behavior**: `FlexCol` is a "ghost" element. Its `after_render` dynamically positions each child below the previous one, accounting for the `gap` property. The `FlexCol`'s `height` will be the sum of its children's heights (plus gaps and padding), and its `width` will be the width of its widest child.
|
||||
|
||||
## 4. `Heading` (ASCII Art Text)
|
||||
|
||||
The `Heading` element uses the `figlet-rs` library to render large, ASCII art text. It's great for titles and banners.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
// Heading with default (standard) font
|
||||
Heading { "OSUI" }
|
||||
|
||||
// Heading with smooth characters (replaces hyphens and pipes)
|
||||
Heading, smooth: true, { "Awesome" }
|
||||
}
|
||||
```
|
||||
**Properties**:
|
||||
* `font: FIGfont`: The font to use (defaults to `standard`). You can load other FIGlet fonts.
|
||||
* `smooth: bool`: If `true`, replaces common characters like `-` and `|` with Unicode line drawing characters for a smoother appearance.
|
||||
|
||||
**Behavior**: `Heading` is a "ghost" element. It takes a single `String` child (or any element that can be downcast to `String`) and converts it to ASCII art before drawing it. Its size contributes to its parent's `Dimension::Content` calculation.
|
||||
|
||||
## 5. `Input` (Text Input Field)
|
||||
|
||||
The `Input` element provides a basic interactive text input field.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
FlexCol, gap: 1, {
|
||||
"Enter Username:"
|
||||
@Transform::new().dimensions(30, 1).padding(0,0);
|
||||
@Style { background: Background::Outline(0xAAAAAA) };
|
||||
@Focused; // Mark this input as initially focused for keyboard interaction
|
||||
Input { }
|
||||
|
||||
"Enter Password:"
|
||||
@Transform::new().dimensions(30, 1).padding(0,0);
|
||||
@Style { background: Background::Outline(0xAAAAAA) };
|
||||
Input { } // This input will not be focused initially
|
||||
}
|
||||
}
|
||||
```
|
||||
**Properties**:
|
||||
* `state: State<String>`: The internal `State` that holds the input string. You can access this `State` to get or set the input value.
|
||||
* `cursor: usize`: Internal cursor position within the input string.
|
||||
|
||||
**Behavior**:
|
||||
* When focused (via the `@Focused` component or navigation), it captures keyboard input.
|
||||
* Supports typing characters, `Backspace`, `Delete`, `Left` arrow, `Right` arrow.
|
||||
* The currently typed text is drawn, and a "inverted" character at the cursor position indicates focus.
|
||||
* The `Input` element automatically creates its own internal `State<String>`. To access this state, you would need to store a reference to the `Input` element (e.g., if you were creating it programmatically without `rsx!`) or manage your own state and inject it.
|
||||
|
||||
## 6. `Paginator` (Page Navigation)
|
||||
|
||||
The `Paginator` element manages a collection of child widgets, displaying only one at a time and allowing navigation between them (typically with Tab/Shift+Tab).
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
@Transform::new().center().dimensions(50, 20); // Paginator needs fixed size to contain pages
|
||||
@Style { background: Background::Solid(0x333333) };
|
||||
Paginator {
|
||||
// First page content
|
||||
FlexCol { "This is Page 1" "Press TAB for next page, Shift+TAB for previous" }
|
||||
|
||||
// Second page content
|
||||
Div { "This is Page 2, with some input:" Input { } }
|
||||
|
||||
// Third page content
|
||||
Heading { "Last Page!" }
|
||||
}
|
||||
}
|
||||
```
|
||||
**Properties**:
|
||||
* `index: usize`: The current page index being displayed (defaults to 0).
|
||||
|
||||
**Behavior**:
|
||||
* `Paginator` is a "ghost" element. It draws only its child at the current `index`.
|
||||
* It listens for `KeyCode::Tab` and `KeyCode::BackTab` events to cycle through its children.
|
||||
* Its `width` and `height` are determined by its currently displayed child.
|
||||
|
||||
These built-in elements provide a solid foundation for building diverse and interactive terminal user interfaces with OSUI.
|
||||
@@ -0,0 +1,215 @@
|
||||
# Creating Custom Widgets
|
||||
|
||||
While OSUI provides a rich set of built-in elements, you'll often need to create your own custom widgets to encapsulate specific UI logic or appearance. This guide walks you through implementing the `Element` trait and integrating your custom widget into the OSUI ecosystem.
|
||||
|
||||
## The `Element` Trait Revisited
|
||||
|
||||
As discussed in [Concepts: Widget Model](../concepts/widget-model.md), the `Element` trait is the contract for anything that can be rendered.
|
||||
|
||||
```rust
|
||||
pub trait Element: Send + Sync {
|
||||
// Draw commands for the element itself
|
||||
fn render(&mut self, scope: &mut RenderScope, render_context: &RenderContext);
|
||||
|
||||
// Logic after self-rendering, typically for rendering children
|
||||
fn after_render(&mut self, scope: &mut RenderScope, render_context: &RenderContext);
|
||||
|
||||
// Used by rsx! to register children
|
||||
fn draw_child(&mut self, element: &Arc<Widget>);
|
||||
|
||||
// Handle incoming events
|
||||
fn event(&mut self, event: &dyn Event);
|
||||
|
||||
// Is this element purely a logical/layout container (e.g., Div, FlexRow)?
|
||||
fn is_ghost(&mut self) -> bool;
|
||||
|
||||
// Required for downcasting, usually implemented boilerplate
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
```
|
||||
|
||||
## Example: A Simple `ClickableBox`
|
||||
|
||||
Let's create a box that displays a message and changes its background color when clicked.
|
||||
|
||||
### 1. Define the Element Struct
|
||||
|
||||
Our `ClickableBox` will hold its message, current color, and a list of children.
|
||||
|
||||
```rust
|
||||
// src/elements/clickable_box.rs (or anywhere you organize your custom elements)
|
||||
|
||||
use std::sync::Arc;
|
||||
use crossterm::event::{Event as CrosstermEvent, MouseEvent, MouseEventKind};
|
||||
use crate::{prelude::*, NoRenderRoot}; // Import necessary OSUI items
|
||||
|
||||
pub struct ClickableBox {
|
||||
message: String,
|
||||
current_color: u32,
|
||||
default_color: u32,
|
||||
clicked_color: u32,
|
||||
children: Vec<Arc<Widget>>, // To hold potential nested elements
|
||||
// We'll track the size it renders to, important for parents
|
||||
size: (u16, u16),
|
||||
}
|
||||
|
||||
impl ClickableBox {
|
||||
pub fn new(message: &str) -> Self {
|
||||
Self {
|
||||
message: message.to_string(),
|
||||
current_color: 0x0055AA, // Default blue
|
||||
default_color: 0x0055AA,
|
||||
clicked_color: 0xAA5500, // Orange when clicked
|
||||
children: Vec::new(),
|
||||
size: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// A method to change color, useful for event handlers
|
||||
pub fn set_clicked(&mut self, clicked: bool) {
|
||||
self.current_color = if clicked { self.clicked_color } else { self.default_color };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Implement the `Element` Trait
|
||||
|
||||
Now, let's implement the core `Element` trait methods.
|
||||
|
||||
```rust
|
||||
// Continue in src/elements/clickable_box.rs
|
||||
|
||||
impl Element for ClickableBox {
|
||||
fn render(
|
||||
&mut self,
|
||||
scope: &mut RenderScope,
|
||||
render_context: &RenderContext,
|
||||
) {
|
||||
// Draw the background rectangle based on current_color
|
||||
scope.draw_rect(0, 0, scope.get_transform().width, scope.get_transform().height, self.current_color);
|
||||
|
||||
// Draw the message text, centered
|
||||
let (msg_w, msg_h) = utils::str_size(&self.message);
|
||||
let text_x = (scope.get_transform().width.saturating_sub(msg_w)) / 2;
|
||||
let text_y = (scope.get_transform().height.saturating_sub(msg_h)) / 2;
|
||||
scope.draw_text_colored(text_x, text_y, &self.message, 0xFFFFFF); // White text
|
||||
|
||||
// Use the area based on the message size if dimensions are `Content`
|
||||
scope.use_area(msg_w, msg_h);
|
||||
|
||||
// Update the size for the after_render pass and parent's layout
|
||||
self.size = (scope.get_transform().width, scope.get_transform().height);
|
||||
}
|
||||
|
||||
fn after_render(
|
||||
&mut self,
|
||||
scope: &mut RenderScope,
|
||||
render_context: &RenderContext,
|
||||
) {
|
||||
// This element is a container, so we need to render its children.
|
||||
// We'll pass them a new RenderScope nested within this box's area.
|
||||
let mut transform = scope.get_transform().clone();
|
||||
let mut child_renderer = DivRenderer(&mut transform); // Use DivRenderer helper for children
|
||||
|
||||
let (parent_w, parent_h) = scope.get_parent_size(); // Store parent size
|
||||
scope.set_parent_size(self.size.0, self.size.1); // Set current element's size as parent for children
|
||||
|
||||
for widget in &self.children {
|
||||
// Render each child widget using the context and the specialized renderer
|
||||
scope.render_widget(&mut child_renderer, render_context.get_context(), widget);
|
||||
}
|
||||
|
||||
// Restore parent size for subsequent sibling elements
|
||||
scope.set_parent_size(parent_w, parent_h);
|
||||
// The `DivRenderer` updates `transform.width/height` to encompass children.
|
||||
// We need to propagate this up to our element's own calculated size.
|
||||
self.size = (transform.width, transform.height);
|
||||
}
|
||||
|
||||
fn event(&mut self, event: &dyn Event) {
|
||||
// Listen for Crossterm Mouse Events
|
||||
if let Some(crossterm_event) = event.get::<CrosstermEvent>() {
|
||||
if let CrosstermEvent::Mouse(MouseEvent { kind: MouseEventKind::Down(_), column, row, .. }) = crossterm_event {
|
||||
// Check if the click occurred within our widget's bounds
|
||||
// This would require knowing the widget's absolute position on screen.
|
||||
// For simplicity here, we'll just toggle color on any click to demonstrate.
|
||||
// In a real app, you'd get the widget's RawTransform from an extension
|
||||
// or a global layout manager to check bounds.
|
||||
self.set_clicked(!self.current_color == self.clicked_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_child(&mut self, element: &Arc<Widget>) {
|
||||
// When a child is added (e.g., via rsx! nesting), store it.
|
||||
// Inject NoRenderRoot so OSUI's main loop doesn't try to render it directly.
|
||||
element.inject(|w| w.component(NoRenderRoot));
|
||||
self.children.push(element.clone());
|
||||
}
|
||||
|
||||
fn is_ghost(&mut self) -> bool {
|
||||
// This element draws its own background, so it's not a ghost.
|
||||
false
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Integrate with `mod.rs` (Optional but Recommended)
|
||||
|
||||
Add your new element to `src/elements/mod.rs` so it's easily accessible via `osui::prelude::*`.
|
||||
|
||||
```rust
|
||||
// src/elements/mod.rs (partial)
|
||||
pub mod div;
|
||||
// ... other elements
|
||||
pub mod clickable_box; // Your new module!
|
||||
|
||||
pub use div::*;
|
||||
// ... other elements
|
||||
pub use clickable_box::*; // Export it for prelude
|
||||
// ... String impl (already there)
|
||||
```
|
||||
|
||||
### 4. Use Your Custom Widget in `rsx!`
|
||||
|
||||
Now you can use `ClickableBox` just like any other built-in element:
|
||||
|
||||
```rust
|
||||
// src/main.rs (or your demo app)
|
||||
use osui::prelude::*;
|
||||
use osui::elements::ClickableBox; // Explicitly import if not using prelude
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
screen.extension(InputExtension); // For mouse events
|
||||
screen.extension(RelativeFocusExtension::new()); // Optional, but good practice
|
||||
// You'd also need a MouseExtension if you want raw mouse events on widgets.
|
||||
// For this basic example, `crossterm::event::Event` captures all.
|
||||
|
||||
rsx! {
|
||||
// A ClickableBox with fixed dimensions and some nested text
|
||||
@Transform::new().dimensions(30, 5).center();
|
||||
ClickableBox::new("Click Me!") {
|
||||
// Nested content will be drawn by ClickableBox's after_render
|
||||
@Transform::new().y(3); // Position child text within the box
|
||||
"Nested content inside the box"
|
||||
}
|
||||
}
|
||||
.draw(&screen);
|
||||
|
||||
screen.run()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
**Note on mouse events**: For a true click-detection, you'd need to compare `MouseEvent.column` and `MouseEvent.row` against the widget's actual rendered `RawTransform` coordinates. This typically involves an extension collecting widget positions or a global event dispatcher that routes events to widgets based on their bounds. The `Input` element handles focus and key events because it's built to capture those when focused.
|
||||
|
||||
By following this pattern, you can extend OSUI with a wide variety of custom UI components tailored to your application's specific needs.
|
||||
@@ -0,0 +1,116 @@
|
||||
# Extending OSUI
|
||||
|
||||
OSUI's architecture is designed for extensibility, allowing you to add custom behaviors, event listeners, and rendering logic through the `Extension` trait. Extensions are global listeners and modifiers that hook into the OSUI application lifecycle.
|
||||
|
||||
## The `Extension` Trait
|
||||
|
||||
The core of OSUI's extensibility is the `Extension` trait:
|
||||
|
||||
```rust
|
||||
pub trait Extension {
|
||||
fn init(&mut self, _ctx: &Context) {}
|
||||
fn event(&mut self, _ctx: &Context, _event: &dyn Event) {}
|
||||
fn on_close(&mut self) {}
|
||||
fn render(&mut self, _ctx: &Context, _scope: &mut RenderScope) {}
|
||||
fn render_widget(&mut self, _ctx: &Context, _scope: &mut RenderScope, _widget: &Arc<Widget>) {}
|
||||
fn after_render_widget(
|
||||
&mut self,
|
||||
_ctx: &Context,
|
||||
_scope: &mut RenderScope,
|
||||
_widget: &Arc<Widget>,
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
Each method provides a hook into a specific part of the OSUI application lifecycle:
|
||||
|
||||
* **`init(&mut self, ctx: &Context)`**: Called once when the extension is registered with the `Screen` and before the main rendering loop starts. Use this for setup tasks, like spawning background threads or initializing global resources.
|
||||
* **`event(&mut self, ctx: &Context, event: &dyn Event)`**: Called whenever any `Event` is dispatched across the OSUI system. This is where extensions can listen for and react to keyboard input, custom events, or events from other extensions.
|
||||
* **`on_close(&mut self)`**: Called when `screen.close()` is invoked. Use this for cleanup, like disabling raw terminal mode or saving state.
|
||||
* **`render(&mut self, ctx: &Context, scope: &mut RenderScope)`**: Called *before* any individual widgets are rendered for a given frame. This `RenderScope` represents the *entire screen*. Useful for drawing global elements or backgrounds that span the whole terminal.
|
||||
* **`render_widget(&mut self, ctx: &Context, scope: &mut RenderScope, widget: &Arc<Widget>)`**: Called *before* a specific `widget`'s `Element::render` method is called. Extensions can inject drawing commands into the `scope` or modify the `widget` at this stage.
|
||||
* **`after_render_widget(&mut self, ctx: &Context, scope: &mut RenderScope, widget: &Arc<Widget>)`**: Called *after* a specific `widget`'s `Element::after_render` method and its drawing commands have been processed. Useful for post-processing, debugging, or recording widget positions (e.g., `RelativeFocusExtension`).
|
||||
|
||||
## The `Context` Object
|
||||
|
||||
The `Context` struct (`src/extensions/mod.rs`) is passed to most `Extension` hooks and provides access to core OSUI functionalities:
|
||||
|
||||
* **`Context::new(screen: Arc<Screen>)`**: Creates a new `Context` linked to the main `Screen`.
|
||||
* **`context.event<E: Event + Clone + 'static>(&self, e: &E)`**: Dispatches an event `e` to all widgets (via their `event` method and `Handler<E>` components) and all registered extensions. This is how extensions can inject new events into the system.
|
||||
* **`context.get_widgets()`**: Provides a `MutexGuard` to the `Vec<Arc<Widget>>` currently managed by the `Screen`. This allows extensions to iterate over and potentially modify widgets.
|
||||
* **`context.iter_components<C, F>()` / `context.get_components<C>()`**: Helper methods to easily iterate over or collect specific components attached to widgets.
|
||||
|
||||
## Creating a Custom Extension
|
||||
|
||||
Let's create a simple extension that logs every keyboard key pressed to standard output.
|
||||
|
||||
```rust
|
||||
// src/my_extension.rs
|
||||
use std::io::{self, Write};
|
||||
use osui::prelude::*; // Import OSUI prelude
|
||||
|
||||
// Define our custom extension struct
|
||||
pub struct KeyLoggerExtension;
|
||||
|
||||
// Implement the Extension trait for our struct
|
||||
impl Extension for KeyLoggerExtension {
|
||||
// Implement the event hook to listen for events
|
||||
fn event(&mut self, _ctx: &Context, event: &dyn Event) {
|
||||
// Attempt to downcast the generic `event` to a `crossterm::event::Event`.
|
||||
// The `InputExtension` dispatches these events.
|
||||
if let Some(crossterm_event) = event.get::<crossterm::event::Event>() {
|
||||
match crossterm_event {
|
||||
crossterm::event::Event::Key(key_event) => {
|
||||
// Log the key event to stdout.
|
||||
let _ = writeln!(io::stdout(), "Key Pressed: {:?}", key_event);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
_ => {} // Ignore other crossterm events (mouse, resize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Registering Your Extension
|
||||
|
||||
To make your extension active, you must register it with the `Screen` instance in your `main.rs` or application setup:
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
mod my_extension; // Declare your new module
|
||||
mod demos; // Assuming you have a demos module as in guides
|
||||
|
||||
use osui::prelude::*;
|
||||
use my_extension::KeyLoggerExtension; // Import your extension
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
|
||||
// Register OSUI's built-in InputExtension (essential for keyboard input)
|
||||
screen.extension(InputExtension);
|
||||
// Register OSUI's built-in RelativeFocusExtension (for Tab/Shift+Tab navigation)
|
||||
screen.extension(RelativeFocusExtension::new());
|
||||
|
||||
// Register your custom KeyLoggerExtension
|
||||
screen.extension(KeyLoggerExtension);
|
||||
|
||||
demos::app(screen.clone()).draw(&screen);
|
||||
|
||||
screen.run()
|
||||
}
|
||||
```
|
||||
|
||||
Now, when you run your application and press keys, you will see `Key Pressed: ...` messages in your terminal's output stream (though they might be mixed with the TUI output due to how `crossterm` operates).
|
||||
|
||||
## Built-in Extensions
|
||||
|
||||
OSUI includes several powerful built-in extensions that you've already encountered in examples:
|
||||
|
||||
* **`InputExtension`**: Handles raw terminal input from `crossterm`.
|
||||
* **`RelativeFocusExtension`**: Manages focus between widgets based on their rendered positions, enabling navigation via keyboard (e.g., arrow keys). It introduces `Focused` and `AlwaysFocused` components.
|
||||
* **`IdExtension`**: Provides a way to assign unique IDs to widgets and retrieve them later by ID. It introduces the `Id` component.
|
||||
* **`TickExtension`**: Dispatches `TickEvent`s at a configurable interval, useful for animations or periodic updates.
|
||||
* **`VelocityExtension`**: Provides simple animation by automatically moving widgets with a `Velocity` component.
|
||||
|
||||
By implementing custom extensions, you can integrate external libraries, create complex behaviors not covered by simple components, or debug your UI in powerful ways.
|
||||
@@ -0,0 +1,157 @@
|
||||
# Getting Started with OSUI Example
|
||||
|
||||
This guide expands on the basic "Hello, OSUI!" example to demonstrate a slightly more interactive application with keyboard input and reactive state.
|
||||
|
||||
## 1. Project Setup (Review)
|
||||
|
||||
Ensure you have a new Rust project set up and `osui` added to your `Cargo.toml`. Refer to the [Getting Started](../intro/getting-started.md) guide if you haven't done this already.
|
||||
|
||||
## 2. The `main.rs` File
|
||||
|
||||
We'll use a `main.rs` file that directly calls into a `demos::app` function, similar to how the OSUI examples are structured. This separates the application logic from the basic setup.
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
mod demos; // Declare the demos module
|
||||
|
||||
use osui::prelude::*;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
|
||||
// Register extensions for input handling and relative focus navigation.
|
||||
// InputExtension is crucial for keyboard events.
|
||||
screen.extension(InputExtension);
|
||||
// RelativeFocusExtension enables navigation between widgets using arrow keys.
|
||||
screen.extension(RelativeFocusExtension::new());
|
||||
|
||||
// Call the `app` function from the `demos` module,
|
||||
// which defines our UI and draws it to the screen.
|
||||
demos::app(screen.clone()).draw(&screen);
|
||||
|
||||
// Run the main event loop. This will block until the application closes.
|
||||
screen.run()
|
||||
}
|
||||
```
|
||||
|
||||
## 3. The `demos` Module (`src/demos/mod.rs`)
|
||||
|
||||
This is where the main application logic and UI definition reside. We'll create a simple counter that increments every second and allows the user to exit using the `Esc` key.
|
||||
|
||||
```rust
|
||||
// src/demos/mod.rs
|
||||
use std::sync::Arc;
|
||||
use osui::prelude::*;
|
||||
|
||||
// This function takes an Arc<Screen> to interact with the main TUI context.
|
||||
pub fn app(screen: Arc<Screen>) -> Rsx {
|
||||
// 1. Create a reactive state variable for our counter.
|
||||
let count = use_state(0);
|
||||
|
||||
// 2. Spawn a background thread to update the counter every second.
|
||||
// We clone `count` (which is an Arc internally) to move it into the thread.
|
||||
std::thread::spawn({
|
||||
let count = count.clone();
|
||||
move || loop {
|
||||
// Acquire a mutable lock on the state and increment the value.
|
||||
// DerefMut implementation on Inner<T> automatically marks the state as "changed".
|
||||
**count.get() += 1;
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Define the UI using the rsx! macro.
|
||||
rsx! {
|
||||
// @Handler::new: Attaches an event handler component to the root widget.
|
||||
// This handler listens for `crossterm::event::Event`s.
|
||||
@Handler::new({
|
||||
let screen = screen.clone(); // Clone screen to move into the closure
|
||||
move |_, e: &crossterm::event::Event| {
|
||||
// Check if the event is a Key event and if the key is 'Esc'.
|
||||
if let crossterm::event::Event::Key(crossterm::event::KeyEvent { code, .. }) = e {
|
||||
if *code == crossterm::event::KeyCode::Esc {
|
||||
// If Esc is pressed, close the screen, terminating the application.
|
||||
screen.close();
|
||||
}
|
||||
}
|
||||
}});
|
||||
// @AlwaysFocused: A component from RelativeFocusExtension that keeps this widget focused
|
||||
// even when other widgets are navigated to. Useful for root containers or global handlers.
|
||||
@AlwaysFocused;
|
||||
Paginator { // Paginator is a simple page management element
|
||||
// FlexRow organizes children horizontally
|
||||
FlexRow {
|
||||
// Heading element for large text (uses figlet-rs)
|
||||
Heading, smooth: false, { "OSUI" } // Sets `smooth` property on Heading
|
||||
"Welcome to the OSUI demo!"
|
||||
"Press tab to switch to the next page or shift+tab to the previous page"
|
||||
}
|
||||
|
||||
// FlexCol organizes children vertically
|
||||
FlexCol, gap: 3, { // Sets `gap` property on FlexCol
|
||||
// @Transform: Component to define layout (position, dimensions, padding)
|
||||
@Transform::new().padding(2, 2);
|
||||
// @Style: Component to define visual style (background, foreground)
|
||||
@Style { foreground: None, background: Background::RoundedOutline(0x00ff00) };
|
||||
Div { // Div is a basic container element
|
||||
"This is text inside a div"
|
||||
}
|
||||
|
||||
@Transform::new().padding(2, 2);
|
||||
@Style { foreground: None, background: Background::Outline(0x00ff00) };
|
||||
Div {
|
||||
"This is text inside a div with square outlines"
|
||||
}
|
||||
}
|
||||
|
||||
FlexRow, gap: 1, {
|
||||
// %count: Declares this widget depends on the `count` state variable.
|
||||
// When `count` changes, this widget (and its children) will re-render.
|
||||
%count
|
||||
// String interpolation directly from state.
|
||||
"This will increment every second: {count}"
|
||||
|
||||
FlexRow { // Nested FlexRow for username input
|
||||
"Username"
|
||||
@Transform::new().padding(1, 1).dimensions(40, 1);
|
||||
@Style { foreground: Some(0xffffff), background: Background::RoundedOutline(0xff0000) };
|
||||
@Focused; // This component marks the Input as initially focused.
|
||||
Input { } // Input element allows text input
|
||||
}
|
||||
|
||||
@Transform::new().margin(0, 1);
|
||||
FlexRow { // Nested FlexRow for password input
|
||||
"Password"
|
||||
@Transform::new().padding(1, 1).dimensions(40, 1);
|
||||
@Style { foreground: Some(0xffffff), background: Background::RoundedOutline(0xffff00) };
|
||||
Input { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **`Screen` Initialization**: The `Screen` is the central orchestrator. It holds your UI widgets and extensions.
|
||||
2. **Extensions**:
|
||||
* `InputExtension`: Crucial for any interactive app. It enables raw mode for terminal input and continuously reads keyboard events, dispatching them to the `Screen`'s event bus.
|
||||
* `RelativeFocusExtension`: Manages which widget is "focused," allowing elements like `Input` to respond to typing. It also provides navigation (e.g., Tab key or arrow keys with Shift).
|
||||
3. **State Management (`use_state` and `%count`)**:
|
||||
* `use_state(0)` creates a `State<i32>` variable initialized to `0`. `State` is an `Arc<Mutex<T>>` internally, making it safe to share across threads.
|
||||
* The `std::thread::spawn` block continuously increments this `count` every second. `**count.get() += 1;` uses `DerefMut` on the `MutexGuard` obtained from `get()`. This `DerefMut` implementation *automatically marks the state as changed*.
|
||||
* In the `rsx!` macro, `%count` tells OSUI that the enclosing `Div` (and its children) depends on `count`.
|
||||
* Whenever `count` is marked as changed, the `DynWidget` associated with that `Div` automatically rebuilds its content and triggers a re-render in the next frame. This is why the number updates in the UI.
|
||||
4. **`rsx!` Macro**:
|
||||
* The `rsx!` macro provides a declarative way to define your UI tree. It creates a hierarchy of `Widget`s, each potentially containing an `Element` and various `Component`s.
|
||||
* `Paginator`, `FlexRow`, `FlexCol`, `Div`, `Heading`, `Input` are built-in [elements](../reference/elements/index.md) that define structure and appearance.
|
||||
* `@Transform` and `@Style` are [components](../reference/style.md) that attach layout and visual properties to elements.
|
||||
* `@Handler` is a [component](../reference/extensions.md) that allows a widget to listen for specific events (here, `crossterm::event::Event`).
|
||||
* `@Focused` and `@AlwaysFocused` are [components](../reference/extensions/focus.md) from `RelativeFocusExtension` that manage input focus.
|
||||
5. **`screen.run()`**: This method starts the main application loop. It continuously:
|
||||
* Renders all widgets.
|
||||
* Processes events from extensions.
|
||||
* Calls `auto_refresh` on dynamic widgets to check dependencies and re-render if needed.
|
||||
|
||||
This example showcases how OSUI integrates reactive state, event handling, and declarative UI definition to create interactive terminal applications.
|
||||
@@ -0,0 +1,147 @@
|
||||
# Layout and Styling with OSUI
|
||||
|
||||
OSUI provides a robust system for controlling the position, size, and visual appearance of your UI elements using `Transform` and `Style` components.
|
||||
|
||||
## 1. Controlling Layout with `Transform`
|
||||
|
||||
The `Transform` component defines how a widget is positioned and sized relative to its parent. It's designed for flexibility, allowing both fixed values and dynamic rules.
|
||||
|
||||
```rust
|
||||
// Defined using the component! macro in src/style.rs
|
||||
component!(Transform {
|
||||
pub x: Position,
|
||||
pub y: Position,
|
||||
pub mx: i32,
|
||||
pub my: i32,
|
||||
pub px: u16,
|
||||
pub py: u16,
|
||||
pub width: Dimension,
|
||||
pub height: Dimension,
|
||||
});
|
||||
```
|
||||
|
||||
* **`x`, `y`**: Horizontal and vertical position, defined by the `Position` enum.
|
||||
* **`mx`, `my`**: Margins (offsets) from the calculated position. Can be positive or negative.
|
||||
* **`px`, `py`**: Padding (internal spacing) around the content within the widget's bounds.
|
||||
* **`width`, `height`**: Sizing rules for dimensions, defined by the `Dimension` enum.
|
||||
|
||||
### `Position` Enum
|
||||
|
||||
Controls horizontal or vertical alignment:
|
||||
|
||||
* **`Const(u16)`**: Fixed position in cells from the origin (top-left).
|
||||
* `x: Position::Const(10)` or simply `x: 10` (due to `From<u16> for Position` impl).
|
||||
* **`Center`**: Centers the widget in the parent's available space.
|
||||
* **`End`**: Aligns the widget to the end (right or bottom) of the parent.
|
||||
|
||||
### `Dimension` Enum
|
||||
|
||||
Controls sizing:
|
||||
|
||||
* **`Full`**: Fills the available space from the parent.
|
||||
* **`Content`**: Automatically sized to fit content. This is the default. The element's `render` method will typically update the `RenderScope`'s size to match its content.
|
||||
* **`Const(u16)`**: Fixed size in cells.
|
||||
* `width: Dimension::Const(50)` or simply `width: 50`.
|
||||
|
||||
### Fluent API for `Transform`
|
||||
|
||||
`Transform` provides a fluent builder pattern for easy configuration:
|
||||
|
||||
* **`Transform::new()`**: Creates a default transform: top-left aligned (`x: 0`, `y: 0`), content sizing (`width: Content`, `height: Content`), no margins or padding.
|
||||
* **`Transform::center()`**: Shortcut for `x: Center`, `y: Center`.
|
||||
* **`transform.bottom()`**: Sets `y: End`.
|
||||
* **`transform.right()`**: Sets `x: End`.
|
||||
* **`transform.margin(x, y)`**: Sets `mx` and `my`.
|
||||
* **`transform.padding(x, y)`**: Sets `px` and `py`.
|
||||
* **`transform.dimensions(width, height)`**: Sets `width: Const(width)` and `height: Const(height)`.
|
||||
|
||||
### Examples of `Transform` Usage
|
||||
|
||||
Attach `Transform` as a component to any element using the `@` syntax in `rsx!`:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
// Center a Div on the screen
|
||||
@Transform::center();
|
||||
Div { "I am centered." }
|
||||
|
||||
// Place a Div at (10, 5) with fixed dimensions and padding
|
||||
@Transform::new().x(10).y(5).dimensions(30, 7).padding(2, 1);
|
||||
Div { "Fixed size box with padding." }
|
||||
|
||||
// Align a Div to the bottom-right with margin
|
||||
@Transform::new().bottom().right().margin(-5, -3); // Negative margin pulls it closer to edge
|
||||
Div { "Bottom right with margin." }
|
||||
|
||||
// A FlexRow that takes full width and automatically sizes height
|
||||
@Transform::new().width(Full);
|
||||
FlexRow { "Full width content." }
|
||||
}
|
||||
```
|
||||
|
||||
The `transform!` macro provides a more concise way to create and set properties on a `Transform`:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
// Equivalent to Transform::new().x(10).y(5).dimensions(30, 7).padding(2, 1);
|
||||
@transform!(x: 10, y: 5, width: 30, height: 7, px: 2, py: 1);
|
||||
Div { "Shorter syntax for transform." }
|
||||
|
||||
// Using Position and Dimension enums directly
|
||||
@transform!(x: Center, y: End, width: Full);
|
||||
Div { "Centered horizontally, at bottom, full width." }
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Defining Style with `Style`
|
||||
|
||||
The `Style` component defines the visual appearance, specifically background and foreground colors.
|
||||
|
||||
```rust
|
||||
// Defined using the component! macro in src/style.rs
|
||||
component!(Style {
|
||||
pub background: Background,
|
||||
pub foreground: Option<u32>,
|
||||
});
|
||||
|
||||
pub enum Background {
|
||||
NoBackground, // No background drawn
|
||||
Outline(u32), // Draws a basic rectangular outline
|
||||
RoundedOutline(u32),// Draws a rounded rectangular outline
|
||||
Solid(u32), // Fills the background with a solid color
|
||||
}
|
||||
```
|
||||
|
||||
* **`background`**: Determines how the background of the widget's calculated area is rendered.
|
||||
* **`foreground`**: An `Option<u32>` where `u32` represents a 24-bit RGB color (e.g., `0xFF0000` for red, `0x00FF00` for green). If `None`, the default terminal foreground color is used.
|
||||
|
||||
### Examples of `Style` Usage
|
||||
|
||||
Attach `Style` as a component:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
// A div with a solid blue background and white text
|
||||
@Transform::new().dimensions(25, 3);
|
||||
@Style { background: Background::Solid(0x0000FF), foreground: Some(0xFFFFFF) };
|
||||
Div { "Hello, Blue World!" }
|
||||
|
||||
// A div with a green rounded outline
|
||||
@Transform::new().dimensions(25, 3).margin(0, 4);
|
||||
@Style { background: Background::RoundedOutline(0x00FF00), foreground: Some(0xFFFFFF) };
|
||||
Div { "Rounded green box." }
|
||||
|
||||
// A div with a red square outline
|
||||
@Transform::new().dimensions(25, 3).margin(0, 8);
|
||||
@Style { background: Background::Outline(0xFF0000), foreground: Some(0xFFFFFF) };
|
||||
Div { "Square red box." }
|
||||
}
|
||||
```
|
||||
|
||||
By combining `Transform` and `Style`, you can precisely control both the layout and visual aesthetics of your OSUI applications.
|
||||
@@ -0,0 +1,135 @@
|
||||
# State and Reactivity in OSUI
|
||||
|
||||
OSUI's reactivity model enables your UI to automatically update when underlying data changes, eliminating the need for manual DOM manipulation. This is achieved through the `State<T>` struct and its integration with `DynWidget`s via the `DependencyHandler` trait.
|
||||
|
||||
## The Problem: Dynamic UIs
|
||||
|
||||
Imagine you have a counter that increments over time, and you want to display its current value in your TUI. Without a reactive system, you would manually:
|
||||
1. Get the new count.
|
||||
2. Clear the old count from the screen.
|
||||
3. Draw the new count at the correct position.
|
||||
4. Manage redraws if other elements shift.
|
||||
|
||||
This becomes complex quickly, especially with multiple, interconnected pieces of dynamic data.
|
||||
|
||||
## The OSUI Solution: `State<T>`
|
||||
|
||||
`State<T>` is a generic type that wraps your data `T` and provides mechanisms to signal when `T` has changed. This signal then triggers a re-render of any UI widget that depends on that `State`.
|
||||
|
||||
### 1. Creating Reactive State: `use_state()`
|
||||
|
||||
The `use_state` function is the primary way to create a new `State<T>` instance:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
screen.extension(InputExtension); // Needed for event loop
|
||||
|
||||
// Create a new State<i32> initialized with 0
|
||||
let counter = use_state(0);
|
||||
|
||||
// ... UI definition and screen.run()
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
`State<T>` internally uses `Arc<Mutex<Inner<T>>>`, making it `Send + Sync` and safely shareable across threads and widgets.
|
||||
|
||||
### 2. Modifying State and Triggering Updates
|
||||
|
||||
The most common way to interact with `State<T>` is via its `get()` method, which returns a `MutexGuard<'_, Inner<T>>`. The `Inner<T>` struct implements `Deref` and `DerefMut` for `T`, allowing you to treat `state.get()` much like a direct mutable reference to your data.
|
||||
|
||||
**Key Point**: When you use `DerefMut` (e.g., `*state.get() = ...` or `*state.get() += 1`), the `State` automatically registers that it has changed. This is critical for reactivity.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
screen.extension(InputExtension); // Required for terminal setup
|
||||
|
||||
let counter = use_state(0);
|
||||
|
||||
// Spawn a new thread to increment the counter every second.
|
||||
// We clone `counter` (which is cheap due to Arc) to move it into the thread.
|
||||
thread::spawn({
|
||||
let counter_clone = counter.clone(); // Clone the Arc for the new thread
|
||||
move || {
|
||||
loop {
|
||||
// Get a mutable lock on the counter state
|
||||
// The DerefMut implementation on `Inner<i32>` will mark the state as changed
|
||||
// (setting `inner.changed = inner.dependencies`)
|
||||
*counter_clone.get() += 1;
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
// Declare that this Div widget depends on the `counter` state.
|
||||
// The `%counter` syntax is a critical part of connecting state to UI.
|
||||
%counter
|
||||
Div {
|
||||
// Access the value of the state using `.get()`.
|
||||
// `format!` macro will automatically call `Display` for `State<T>`.
|
||||
format!("Current Count: {}", counter.get())
|
||||
}
|
||||
}
|
||||
.draw(&screen);
|
||||
|
||||
screen.run()?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
In this example:
|
||||
* `*counter_clone.get() += 1;` modifies the `i32` value and simultaneously tells `State<i32>` that it has been updated.
|
||||
* Because the `Div` element was declared with `%counter` in `rsx!`, it becomes a `DynWidget` and registers `counter` as one of its dependencies.
|
||||
* In the main rendering loop (inside `screen.run()`), `DynWidget`s repeatedly call `dependency.check()`. When `counter.check()` returns `true` (because it was marked as changed), the `DynWidget` rebuilds its internal `Element` and components, refreshing its content with the new `counter` value.
|
||||
|
||||
### 3. Declaring Dependencies in `rsx!`
|
||||
|
||||
The `%variable_name` syntax in the `rsx!` macro is how you declare that a widget depends on a `State<T>` variable (or any type implementing `DependencyHandler`).
|
||||
|
||||
```rust
|
||||
let my_data = use_state("Initial".to_string());
|
||||
// ...
|
||||
rsx! {
|
||||
%my_data // This Div will re-render when `my_data` changes
|
||||
Div {
|
||||
format!("Data: {}", my_data.get())
|
||||
}
|
||||
}
|
||||
```
|
||||
You can declare multiple dependencies: `%state1 %state2 Div { ... }`.
|
||||
|
||||
### When to use `State::set()` or `State::update()`
|
||||
|
||||
* **`State::set(new_value)`**: Use this when you want to entirely replace the `T` value within the `State` and mark it as changed.
|
||||
```rust
|
||||
// Instead of: *my_state.get() = "New".to_string();
|
||||
my_state.set("New".to_string());
|
||||
```
|
||||
* **`State::update()`**: Use this if you modify the inner `T` value through a path that doesn't trigger `DerefMut` (e.g., if `T` is a complex struct and you modify one of its fields after getting a `&mut T` but without re-assigning the whole `T`). This explicitly tells `State` that it has changed.
|
||||
```rust
|
||||
// If MyComplexData has an internal field modified, and you only got `&mut MyComplexData`
|
||||
// but didn't reassign the whole struct.
|
||||
let mut data_lock = my_complex_data.get();
|
||||
data_lock.some_internal_field.modify();
|
||||
// After modifying, you might need to manually update if DerefMut didn't catch it
|
||||
// (though in most simple cases, DerefMut handles it automatically).
|
||||
my_complex_data.update();
|
||||
```
|
||||
|
||||
### Cloning `State<T>`
|
||||
|
||||
Since `State<T>` uses `Arc`, cloning a `State<T>` instance is very cheap. It only increments the reference count of the shared `Arc`. This is how you pass `State` into closures or other threads without moving ownership.
|
||||
|
||||
```rust
|
||||
let original_state = use_state(0);
|
||||
let cloned_state = original_state.clone(); // This is just an Arc clone
|
||||
```
|
||||
|
||||
By leveraging `State<T>` and dependency tracking, OSUI enables you to build dynamic, responsive terminal UIs with a clear separation of concerns between data and presentation.
|
||||
Reference in New Issue
Block a user