Improved docs

This commit is contained in:
2026-01-31 21:01:30 +01:00
parent 6fe25f0320
commit 37d2de41ce
54 changed files with 2 additions and 54 deletions
@@ -0,0 +1,123 @@
---
sidebar_position: 0
title: Creating Components
---
# Creating Components
Components are the building blocks of any OSUI application. They encapsulate UI logic, state, and rendering instructions, making your code modular and reusable. This guide explains how to define and use components effectively.
## The `#[component]` Attribute
OSUI uses the `#[component]` procedural macro to transform a regular Rust function into an OSUI component. This macro handles the boilerplate necessary for prop handling and integrating the function into the component tree.
### Basic Structure
A component function generally looks like this:
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
pub fn MyComponent(cx: &Arc<Context>) -> View {
// Component logic goes here
rsx! {
"Hello from MyComponent!"
}.view(&cx)
}
```
**Key requirements:**
1. **`#[component]`**: Always annotate your component function with this attribute.
2. **Function Signature**:
* It must take `cx: &Arc<Context>` as its *first* argument. The `Context` is essential for managing state, events, and child components.
* It must return a `View`. A `View` is an `Arc<dyn Fn(&mut DrawContext) + Send + Sync>`, essentially a closure that contains the drawing instructions for your component.
3. **Return Value**: The most common way to return a `View` is by using the `rsx!` macro followed by `.view(&cx)`.
### Component Props
Components become truly powerful when they can receive data from their parents. These are called "props" (properties). To define props for your component, simply add more parameters to your component function after `cx: &Arc<Context>`.
OSUI's `#[component]` macro automatically generates a struct for your component based on these parameters.
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
pub fn GreetUser(cx: &Arc<Context>, name: &str, age: &u8) -> View {
// Access props directly by their parameter names
rsx! {
format!("Hello, {}! You are {} years old.", name, age)
}.view(&cx)
}
#[component]
pub fn App(cx: &Arc<Context>) -> View {
let user_name = "Alice".to_string();
let user_age = 30;
rsx! {
// Instantiate GreetUser and pass props
GreetUser {
name: user_name, // Prop name matches the parameter name
age: user_age,
}
}.view(&cx)
}
```
**Prop Rules:**
* **Parameter Names**: The names of your function parameters (e.g., `name`, `age`) become the names of the props you use when instantiating the component in `rsx!`.
* **Reference Types**: Props are typically passed as references (e.g., `&str`, `&u8`). The `#[component]` macro automatically "strips" the reference when generating the internal component struct, storing the owned type. This means you don't need to manually clone values unless you intend to move them into a closure or `State`.
* **`children` Prop**: As seen in the [previous guide](../intro/03-example-basic-component.md), any content nested inside a component's `rsx!` invocation is implicitly passed as a `children: &Rsx` prop. This allows for flexible content composition.
### Example: Component with `children` and custom props
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
pub fn Card(cx: &Arc<Context>, title: &str, children: &Rsx) -> View {
rsx! {
format!("--- {} ---", title) // Render the title prop
@{children} // Render the children passed to Card
"----------------"
}.view(&cx)
}
#[component]
pub fn App(cx: &Arc<Context>) -> View {
rsx! {
Card {
title: "My Awesome Card", // Pass a custom prop
// The content below is passed as the `children` prop
rsx! {
"This is the content inside the card."
"It can span multiple lines or include other components."
}
}
Card {
title: "Another Card",
"Just some simple text here." // Even a single string literal can be children
}
}.view(&cx)
}
```
In this example, the `Card` component takes a `title` prop and a `children` prop. It renders the title, then its children, and finally a footer.
## When to Create a Component
* **Reusability**: If you find yourself writing the same UI structure multiple times, extract it into a component.
* **Separation of Concerns**: When a part of your UI has its own state or complex logic, it's a good candidate for a component.
* **Readability**: Breaking down large `rsx!` blocks into smaller components improves the readability and maintainability of your code.
* **Performance (Reactivity)**: Components, especially when using state hooks, allow OSUI to efficiently re-render only the parts of the UI that have changed.
By following these guidelines, you can build well-structured and scalable OSUI applications.
**Next:** Deep dive into the `rsx!` macro and its full capabilities in the [RSX Syntax Guide](./01-rsx-syntax.md).
@@ -0,0 +1,192 @@
---
sidebar_position: 1
title: RSX Syntax Guide
---
# RSX Syntax Guide
The `rsx!` macro is the cornerstone of OSUI's declarative UI system. Inspired by React's JSX, it provides an ergonomic way to define your component hierarchies directly in Rust code. This guide covers all the features of the `rsx!` syntax.
## Basic Structure
The `rsx!` macro produces an `Rsx` object, which is a collection of renderable nodes. You typically call `.view(&cx)` on the `Rsx` object to convert it into a `View` that your component returns.
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
// Your UI elements go here
"Hello, RSX!" // A simple text literal
}.view(&cx)
}
```
## Types of Nodes
`rsx!` supports several types of nodes:
### 1. Text Literals
Plain string literals are rendered as text.
```rust
rsx! {
"This is some text."
"This is another line of text."
}
```
### 2. Rust Expressions: `@{expr}`
You can embed any Rust expression that evaluates to a renderable type (one that implements `ToRsx`) using the `@{...}` syntax. This is useful for dynamic content or rendering other `Rsx` objects.
```rust
let dynamic_text = format!("The current time is: {:?}", std::time::SystemTime::now());
let other_rsx = rsx! { "Some nested content" };
rsx! {
@{dynamic_text} // Renders the string from the expression
@{other_rsx} // Renders another Rsx object
@{123 + 456} // Renders the result of the arithmetic operation (as a string)
}
```
:::tip
Any type that implements `std::fmt::Display` (like `String`, `&str`, `i32`, `f64`, etc.) automatically implements `ToRsx` and can be used directly within `rsx!`.
:::
### 3. Component Instantiation: `ComponentName { prop: value, ... }`
To use another component, specify its name (path) followed by an optional braced block containing its props and children.
```rust
#[component]
fn MyButton(cx: &Arc<Context>, text: &str) -> View {
rsx! {
format!("[ {} ]", text)
}.view(&cx)
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
MyButton { text: "Click Me" } // Component with a prop
MyButton { "Another Button" } // Component with children (which becomes `text` if `children: &Rsx` is also defined)
}.view(&cx)
}
```
* **Props**: Key-value pairs (`prop_name: value`) inside the braces. The `prop_name` must match a parameter name in the target component's function signature.
* **Children**: Any `rsx!` content (text, expressions, other components) directly nested inside the component's braces after props will be collected into the special `children: &Rsx` prop, if defined by the component.
### 4. Conditional Rendering: `@if condition { ... }`
Conditionally render parts of your UI based on a boolean expression.
```rust
let show_message = true;
let is_admin = false;
rsx! {
@if show_message {
"This message is always shown."
}
@if is_admin {
"Admin panel access granted."
} else {
"Access denied." // `else` is optional
}
}
```
* The `condition` must be a Rust expression that evaluates to a `bool`.
* The content inside the `{...}` block is an `rsx!` fragment that will be rendered if the condition is true.
* An optional `else { ... }` block can follow for false conditions.
#### Reactivity with Dependencies: `%$dep @if condition { ... }`
For conditional rendering to react to state changes, you need to explicitly declare dependencies using the `%$dep` syntax.
```rust
let count = use_state(0); // A reactive state
rsx! {
%count @if *count.get() > 0 { // This block re-renders if `count` changes
format!("Count is: {}", count.get_dl())
} else {
"Count is zero."
}
}
```
* **`%count`**: Declares `count` as a dependency for this `if` block. When `count`'s value changes (via `count.set()` or `*count.get_mut()`), this entire `if` block will be re-evaluated and re-rendered.
* You can declare multiple dependencies: `%dep1, dep2, dep3 @if ...`
* You can also rename dependencies for clarity: `%original_name as new_name @if ...`
### 5. Loop Rendering: `@for pattern in expr { ... }`
Render a list of items by iterating over a collection.
```rust
let items = vec!["Apple", "Banana", "Cherry"];
rsx! {
"Fruits:"
@for item in items { // Loops over the `items` vector
format!("- {}", item)
}
"Numbers:"
@for i in (0..3) {
format!("Number: {}", i)
}
}
```
* `pattern` is a standard Rust `for` loop pattern (e.g., `item`, `(index, item)`, `_`).
* `expr` is a Rust expression that evaluates to an `IntoIterator`.
* The content inside the `{...}` block is an `rsx!` fragment that will be rendered for each iteration.
#### Reactivity with Dependencies: `%$dep @for pattern in expr { ... }`
Similar to `@if`, `@for` loops also support dependency tracking for reactive updates.
```rust
let my_list = use_state(vec!["One".to_string(), "Two".to_string()]);
// Later, you might update my_list.set(new_vec);
// or my_list.get_mut().push("Three");
rsx! {
"My Dynamic List:"
%my_list @for item in my_list.get_dl() { // This block re-renders if `my_list` changes
format!("- {}", item)
}
}
```
* **`%my_list`**: Declares `my_list` as a dependency. When `my_list` is updated, the loop will be re-executed, rendering the new list.
### 6. Mount Hook: `!mount_hook_instance`
This syntax is used to explicitly "mount" a component's lifecycle hook. This is specifically for `use_mount_manual`.
```rust
let my_mount_hook = use_mount_manual();
rsx! {
"This text is always visible."
!my_mount_hook // Explicitly triggers the mount effects for `my_mount_hook`
}
```
* When `!my_mount_hook` is encountered in the `rsx!` output, it calls `my_mount_hook.mount()`, triggering any `use_effect` callbacks registered with that specific `Mount` instance.
## Summary
The `rsx!` macro is a powerful tool for building declarative user interfaces in OSUI. By combining text, expressions, components, conditionals, and loops with reactive dependencies, you can create complex and dynamic TUIs with a clean and familiar syntax.
**Next:** Learn how to manage component data over time with OSUI's [State Management hooks](./02-state-management.md).
@@ -0,0 +1,195 @@
---
sidebar_position: 2
title: State Management
---
# State Management
Effective state management is crucial for building interactive and dynamic TUI applications. OSUI provides a React-like hook system that enables components to hold mutable state and react to changes efficiently. This guide covers the core state management hooks: `use_state` and `use_effect`.
## `use_state`: Managing Component-Local State
The `use_state` hook allows your components to declare and manage mutable, reactive state. When state managed by `use_state` changes, OSUI automatically re-renders affected parts of your UI.
### Basic Usage
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
fn Counter(cx: &Arc<Context>) -> View {
// 1. Initialize state with `use_state`
// `count` is a `State<i32>`, initialized to 0.
let count = use_state(0);
// 2. Define a button that increments the count
// This is a placeholder for actual interactive elements.
// In a real app, an event handler would trigger `count.set()` or `*count.get_mut()`.
let increment_button_simulated = {
let count = count.clone(); // Clone the State handle to move into the closure
move || {
// Option 1: Using `set()` for direct replacement
// count.set(*count.get() + 1);
// Option 2: Using `get()` for mutable access (recommended for complex changes)
*count.get() += 1; // `Inner` guard automatically calls `update()` on drop
}
};
// Simulate clicking the button once per render for demonstration
// In a real app, this would be triggered by user input or other events.
increment_button_simulated();
rsx! {
// 3. Display the state value
// `count.get_dl()` gets a cloned, deadlock-less copy of the value.
// `count.get()` returns a guard for mutable access, also deadlock-less.
format!("Count: {}", count.get_dl())
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(Counter {}).expect("Failed to run Counter app");
}
```
In this example, the `Counter` component's state (`count`) is incremented on each render cycle (simulated). The `rsx!` macro automatically updates to reflect the new `count` value because the `Counter` component is re-rendered by the engine and the `count` state is used.
### `State<T>` and `Inner<'a, T>`
* **`State<T>`**: This is the primary handle to your reactive state. It's an `Arc<Mutex<T>>` internally, allowing safe shared ownership and mutation across threads. When you call `use_state(value)`, you get a `State<T>`.
* **`count.get()`**: This method returns an `Inner<'a, T>` guard. `Inner` implements `Deref` and `DerefMut`, so you can treat it almost like a direct reference to your data (`*count.get()`). The `Inner` guard is crucial because it automatically triggers updates to dependents when it's dropped *if* the value was mutated.
* **`count.set(value)`**: A convenience method to replace the entire state value and then trigger updates.
* **`count.get_dl()`**: Returns a cloned copy of the state value. The "dl" stands for "deadlock-less", as it doesn't hold the mutex lock for an extended period, making it safer for quick reads. Use this when you only need to read the value and cloning is cheap.
* **`count.update()`**: Manually notifies all dependents that the state *might* have changed, even if you didn't use `get_mut()` or `set()`. Useful if you modify the internal `Arc<Mutex<T>>` directly (not recommended) or a complex part of `T` without triggering the `DerefMut` auto-update.
### Important Considerations for `State<T>`:
* **Cloning `State` handles**: `State<T>` itself can be cloned (`count.clone()`). This creates a new `Arc` reference to the *same* underlying state. This is essential when moving `State` into closures or child components.
* **`Send + Sync`**: The type `T` held by `State<T>` must implement `Send` and `Sync` for thread-safe access.
* **Reactivity**: `use_state` makes state reactive. When the value changes, any `use_effect` or `rsx!` dynamic scope (`%state @if...` or `%state @for...`) that declared this `State` as a dependency will be re-evaluated.
## `use_effect`: Performing Side Effects
The `use_effect` hook allows you to perform side effects (e.g., logging, network requests, setting up event listeners) in response to state changes or component mounting.
### Basic Usage
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
fn EffectExample(cx: &Arc<Context>) -> View {
let count = use_state(0);
let message = use_state("Initial message".to_string());
// Effect 1: Logs when `count` changes
use_effect(
{
let count = count.clone(); // Clone for the closure
move || {
println!("Effect 1: Count changed to {}", count.get_dl());
}
},
&[&count], // Dependencies: &[&dyn HookDependency]
);
// Effect 2: Logs when `message` changes (and also on initial render)
use_effect(
{
let message = message.clone();
move || {
println!("Effect 2: Message is now '{}'", message.get_dl());
}
},
&[&message], // Dependencies: &[&dyn HookDependency]
);
// Simulate state changes (e.g., from user input or timers)
let _ = {
let count = count.clone();
let message = message.clone();
std::thread::spawn(move || {
sleep(500); // Wait 0.5 seconds
*count.get() += 1; // Triggers Effect 1
sleep(500);
message.set("Updated message!".to_string()); // Triggers Effect 2
sleep(500);
*count.get() += 1; // Triggers Effect 1 again
});
};
rsx! {
format!("Count: {}", count.get_dl())
format!("Message: {}", message.get_dl())
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(EffectExample {}).expect("Failed to run EffectExample app");
}
```
### `use_effect` Parameters:
1. **`f: F`**: A closure (`FnMut() + Send + Sync + 'static`) that represents the side effect. This closure will be executed when any of its dependencies change. The closure is run in a separate spawned thread to avoid blocking the main rendering loop.
2. **`dependencies: &[&dyn HookDependency]`**: A slice of references to objects that implement the `HookDependency` trait. These are typically `State<T>` instances. The effect closure will be called whenever any of these dependencies notify an update.
### Understanding Dependencies:
* **Empty Dependency List (`&[]`)**: If you pass an empty slice (`&[]`), the effect will only run once when the component is first mounted. This is useful for setup logic like initializing global resources or subscriptions.
* **Specific Dependencies**: When you list `State` objects as dependencies, the effect will run:
* Once, immediately when `use_effect` is called (during the initial component render).
* Again, whenever any of the listed `State` objects trigger an update.
* **`HookDependency` Trait**: This trait defines how an object can register an `HookEffect` to be called upon update. `State<T>` and `Mount` both implement this trait, making them usable as dependencies.
## Lifecycle Hooks: `use_mount` and `use_mount_manual`
These hooks are special cases of `use_effect` for managing actions tied to a component's "mounting" lifecycle.
* **`use_mount()`**: Returns a `Mount` instance that automatically calls `mount()` immediately upon its creation. Effects registered with this `Mount` instance will run once when the component is first rendered.
* **`use_mount_manual()`**: Returns a `Mount` instance that starts in an unmounted state. Effects registered with it will *only* run when you explicitly call `mount_instance.mount()`. This is useful for controlling the mount event from specific `rsx!` nodes (`!mount_instance`) or from other logic.
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
fn LifecycleExample(cx: &Arc<Context>) -> View {
let mount_hook = use_mount(); // Automatically mounted
let manual_mount_hook = use_mount_manual(); // Manual mount needed
use_effect(
move || {
println!("Component mounted (automatic hook)!");
},
&[&mount_hook],
);
use_effect(
move || {
println!("Component manually mounted!");
},
&[&manual_mount_hook],
);
rsx! {
"Lifecycle example"
// This will trigger the `manual_mount_hook`'s effects
!manual_mount_hook
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(LifecycleExample {}).expect("Failed to run LifecycleExample app");
}
```
This guide has laid the foundation for managing state and side effects in your OSUI applications. By mastering `use_state` and `use_effect`, you can build sophisticated and responsive TUI experiences.
**Next:** Learn how to handle user interactions and other events with OSUI's [Event Handling system](./03-event-handling.md).
@@ -0,0 +1,284 @@
---
sidebar_position: 3
title: Event Handling
---
# Event Handling
OSUI provides a flexible and type-safe event system that allows components to react to various occurrences, such as user input, internal state changes, or custom application-specific events. This guide explains how to define, emit, and listen for events using `on_event`, `emit_event`, and `emit_event_threaded`.
## Event Propagation Model
In OSUI, events are propagated downwards through the component tree. When an event is emitted from a `Context`, it first triggers all registered handlers on that `Context`, and then recursively calls `emit_event` on all its child `Context`s.
## Defining Custom Events
Any Rust type that implements `Send + Sync + Any + 'static` can be used as an event. Typically, you'll define custom `struct`s or `enum`s for your events to carry specific data.
```rust
// Define a custom event
#[derive(Debug, Clone)]
pub struct ButtonClickEvent {
pub button_id: usize,
pub timestamp: std::time::Instant,
}
// Another custom event
#[derive(Debug, Clone)]
pub enum CustomAppEvent {
Tick,
ReloadData,
}
```
## Listening for Events: `cx.on_event()`
Components can register event handlers using `cx.on_event()` to respond to specific event types.
```rust
use osui::prelude::*;
use std::sync::Arc;
use std::time::Instant;
// Custom event definition
#[derive(Debug, Clone)]
pub struct MyCustomEvent {
pub value: String,
}
#[component]
fn EventListener(cx: &Arc<Context>) -> View {
let received_events = use_state(Vec::<String>::new());
// Register an event handler for `MyCustomEvent`
cx.on_event({
let received_events = received_events.clone(); // Clone State for the closure
move |_ctx, event: &MyCustomEvent| {
// This closure runs when MyCustomEvent is emitted
let mut events_guard = received_events.get();
events_guard.push(format!("Received: {}", event.value));
// No need to call `update()` manually, `Inner` guard handles it on drop
}
});
rsx! {
"Event Listener Component"
@for msg in received_events.get_dl() {
format!("- {}", msg)
}
}.view(&cx)
}
```
* `cx.on_event<T: Any + 'static, F: Fn(&Arc<Self>, &T) + Send + Sync + 'static>(self: &Arc<Self>, handler: F)`:
* `T`: The type of event you want to listen for (e.g., `MyCustomEvent`).
* `F`: A closure that takes `&Arc<Context>` (the component's context) and `&T` (a reference to the event data).
* You pass a closure that captures the necessary state (`received_events` in this case) and logic to execute when the event fires.
## Emitting Events: `cx.emit_event()` and `cx.emit_event_threaded()`
Components or other parts of your application can send events using `cx.emit_event()` or `cx.emit_event_threaded()`.
### `cx.emit_event()` (Synchronous)
`emit_event` processes event handlers sequentially in the current thread.
```rust
// ... (MyCustomEvent and EventListener component definitions from above)
#[component]
fn EventSender(cx: &Arc<Context>) -> View {
let button_clicks = use_state(0);
// Simulate an action that emits an event
let emit_click_event = {
let cx = cx.clone(); // Clone Context for the closure
let button_clicks = button_clicks.clone();
move || {
let current_clicks = *button_clicks.get();
*button_clicks.get() += 1;
let event = MyCustomEvent {
value: format!("Button clicked {} times", current_clicks + 1),
};
cx.emit_event(event); // Emit the event
}
};
// Simulate clicking the button every second
use_effect(
{
let emit_click_event = emit_click_event.clone();
move || {
loop {
sleep(1000); // Wait 1 second
emit_click_event();
}
}
},
&[], // No dependencies, run once on mount
);
rsx! {
format!("Button clicks: {}", button_clicks.get_dl())
}.view(&cx)
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
// EventSender and EventListener are siblings in the tree.
// Events emitted by EventSender will propagate down to EventListener.
EventSender {}
EventListener {}
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run App");
}
```
### `cx.emit_event_threaded()` (Asynchronous)
`emit_event_threaded` spawns a new thread for each registered event handler. This is useful for long-running or potentially blocking event handlers, preventing them from freezing your UI.
```rust
// ... (MyCustomEvent, EventListener definitions)
#[component]
fn ThreadedEventSender(cx: &Arc<Context>) -> View {
let cx_clone = cx.clone();
// Emit an event using `emit_event_threaded` on mount
use_effect(
move || {
println!("Emitting threaded event on mount!");
let event = MyCustomEvent {
value: "Threaded mount event".to_string(),
};
cx_clone.emit_event_threaded(&event); // Notice the `&event` for threaded
},
&[], // Run once on mount
);
rsx! {
"Threaded Event Sender"
}.view(&cx)
}
#[component]
fn AppWithThreaded(cx: &Arc<Context>) -> View {
rsx! {
ThreadedEventSender {}
EventListener {} // This listener will receive the threaded event
}.view(&cx)
}
// To run: engine.run(AppWithThreaded {})
```
* `cx.emit_event_threaded<E: Any + Send + Sync + Clone + 'static>(self: &Arc<Self>, event: &E)`:
* Takes `&E` (a reference to the event), which must implement `Clone` because each spawned thread receives a clone of the event data.
* Each handler for `E` will be executed in its own `std::thread::spawn`.
## Realistic Usage Scenario: Interactive Counter
Let's create a more interactive counter that responds to keyboard input.
```rust
use osui::prelude::*;
use std::sync::Arc;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
// Define a custom event for counter actions
#[derive(Debug, Clone)]
pub enum CounterAction {
Increment,
Decrement,
}
#[component]
fn InteractiveCounter(cx: &Arc<Context>) -> View {
let count = use_state(0);
// Listen for CounterAction events
cx.on_event({
let count = count.clone();
move |_ctx, action: &CounterAction| {
let mut count_guard = count.get();
match action {
CounterAction::Increment => *count_guard += 1,
CounterAction::Decrement => *count_guard -= 1,
}
}
});
rsx! {
"Press 'q' to quit, '+' to increment, '-' to decrement."
format!("Current Count: {}", count.get_dl())
}.view(&cx)
}
// A component (or `main` function logic) to poll keyboard input
// and emit CounterAction events.
#[component]
fn KeyboardInputHandler(cx: &Arc<Context>) -> View {
// This effect runs once on mount to start the keyboard polling thread
use_effect(
{
let cx = cx.clone();
move || {
let _ = crossterm::terminal::enable_raw_mode();
loop {
if event::poll(std::time::Duration::from_millis(50)).unwrap() {
if let Event::Key(key_event) = event::read().unwrap() {
if key_event.kind == KeyEventKind::Press {
match key_event.code {
KeyCode::Char('+') => cx.emit_event(CounterAction::Increment),
KeyCode::Char('-') => cx.emit_event(CounterAction::Decrement),
KeyCode::Char('q') => {
let _ = crossterm::terminal::disable_raw_mode();
cx.stop().expect("Failed to stop engine");
break;
},
_ => {}
}
}
}
}
}
}
},
&[], // Empty deps: run once on mount
);
// This component doesn't render anything visible itself.
// Its purpose is purely to handle input and emit events.
rsx! { "" }.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(AppWithKeyboardInput {}).expect("Failed to run interactive app");
let _ = crossterm::terminal::disable_raw_mode(); // Ensure raw mode is disabled on exit
}
#[component]
fn AppWithKeyboardInput(cx: &Arc<Context>) -> View {
rsx! {
KeyboardInputHandler {} // Handles input and emits events
InteractiveCounter {} // Listens for events and updates UI
}.view(&cx)
}
```
In this example:
* `KeyboardInputHandler` runs in a separate thread (due to `use_effect`'s spawning behavior).
* It polls for keyboard events using `crossterm`.
* Upon detecting `+`, `-`, or `q`, it emits a `CounterAction` or `Stop` command to its `Context`.
* `InteractiveCounter` listens for `CounterAction` events and updates its internal `count` state, which then causes it to re-render.
* The `stop()` command, emitted by `KeyboardInputHandler`, instructs the `Console` engine to terminate its rendering loop.
This showcases a full interaction loop using custom events for communication between components.
**Next:** Explore specialized lifecycle management with `use_mount` and `use_mount_manual` in [Lifecycle Hooks](./04-lifecycle-hooks.md).
@@ -0,0 +1,149 @@
---
sidebar_position: 4
title: Lifecycle Hooks
---
# Lifecycle Hooks
In OSUI, components have a lifecycle, meaning they go through various stages from creation to destruction. While OSUI doesn't expose a full set of lifecycle methods like some frameworks, it provides powerful hooks for managing effects related to a component's "mounting" phase: `use_mount` and `use_mount_manual`. These are special applications of the `use_effect` hook, designed for setup logic.
## The "Mounted" Concept
A component is considered "mounted" when it has been rendered for the first time and is part of the active component tree. Lifecycle hooks allow you to run code precisely at this point.
## `use_mount()`: Automatic Mounting
The `use_mount()` hook provides a `Mount` instance that is automatically marked as "mounted" upon its creation. Any `use_effect` that depends on this `Mount` instance will execute its effect closure immediately when the component renders.
### When to use `use_mount()`:
* **Initial Setup**: Performing actions that should only happen once when the component first appears on screen, such as fetching initial data, setting up global event listeners, or initializing complex external resources.
* **No Cleanup Required**: For effects that don't require any cleanup logic. If cleanup is needed, ensure your effect closure handles it or consider using `use_effect` with an empty dependency array (which is similar in behavior for initial execution, but offers more control over subsequent runs if dependencies are added).
### Example: Initial Data Fetch
```rust
use osui::prelude::*;
use std::sync::Arc;
use std::time::Duration;
// Simulate a data fetching operation
async fn fetch_data_async() -> String {
// In a real application, this would be an actual network request or disk read.
tokio::time::sleep(Duration::from_secs(1)).await; // Simulate network delay
"Data loaded successfully!".to_string()
}
#[component]
fn DataLoader(cx: &Arc<Context>) -> View {
let data_state = use_state("Loading data...".to_string());
let mount_hook = use_mount(); // Automatically initializes as "mounted"
// Use `use_effect` with `mount_hook` as a dependency
use_effect(
{
let data_state = data_state.clone();
move || {
// This effect runs once when `mount_hook` is initialized (component mounts)
println!("DataLoader: Component mounted, starting data fetch...");
// Spawn a new async task (requires a runtime like tokio)
tokio::spawn(async move {
let data = fetch_data_async().await;
data_state.set(data); // Update state, triggering re-render
println!("DataLoader: Data fetch complete.");
});
}
},
&[&mount_hook], // Effect runs when `mount_hook` updates (i.e., on mount)
);
rsx! {
format!("Status: {}", data_state.get_dl())
}.view(&cx)
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
DataLoader {}
}.view(&cx)
}
pub fn main() {
// For async operations, you need a runtime like tokio.
// Add `tokio = { version = "1", features = ["full"] }` to your Cargo.toml
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let engine = Console::new();
engine.run(App {}).expect("Failed to run async app");
});
}
```
## `use_mount_manual()`: Explicit Mounting
The `use_mount_manual()` hook provides a `Mount` instance that starts in an "unmounted" state. Its associated `use_effect` callbacks will *not* run until you explicitly call the `mount()` method on that specific `Mount` instance. This offers finer control over when initial setup logic executes.
### When to use `use_mount_manual()`:
* **Conditional Mounting**: When you want to delay the "mount" logic until a specific condition is met or an interaction occurs.
* **`rsx!`-driven Mounting**: You can trigger the `mount()` method directly from your `rsx!` template using the `!mount_hook_instance` syntax. This is useful if the mount event is tied to the presence of a specific element or a complex rendering path.
### Example: Delayed Mount with `rsx!`
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
fn DelayedSetup(cx: &Arc<Context>) -> View {
let setup_status = use_state("Waiting for manual mount...".to_string());
let manual_mount_hook = use_mount_manual(); // Starts unmounted
use_effect(
{
let setup_status = setup_status.clone();
move || {
// This effect will only run when `manual_mount_hook.mount()` is called.
println!("DelayedSetup: Manual mount triggered, performing setup!");
setup_status.set("Setup complete!".to_string());
}
},
&[&manual_mount_hook], // Depends on the manual mount hook
);
rsx! {
format!("Status: {}", setup_status.get_dl())
// The `!manual_mount_hook` in RSX triggers `manual_mount_hook.mount()`
// which then causes the `use_effect` to run.
!manual_mount_hook
"This component is now mounted."
}.view(&cx)
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
DelayedSetup {}
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run delayed mount app");
}
```
In this example, the "Setup complete!" message will appear only after the `!manual_mount_hook` line is processed by the `rsx!` renderer, explicitly calling `mount_hook.mount()`.
## Summary of Lifecycle Hooks
* `use_mount()`: For effects that should run once immediately when the component is initially rendered.
* `use_mount_manual()`: For effects that you want to explicitly control when they run, either through programmatic calls to `.mount()` or via the `!mount_hook_instance` syntax in `rsx!`.
These hooks, combined with `use_effect`, give you fine-grained control over when side effects are performed during your component's lifetime.
**Next:** Understand how to synchronize component state with events for sophisticated data flow patterns in [Data Flow and Sync](./05-data-flow-and-sync.md).
@@ -0,0 +1,215 @@
---
sidebar_position: 5
title: Data Flow and Sync
---
# Data Flow and Synchronization
OSUI's reactive state system and event handling capabilities provide a robust foundation for managing data flow within your application. The `use_sync_state` and `use_sync_effect` hooks offer powerful patterns for synchronizing component state with external events and vice versa, enabling sophisticated inter-component communication and data management.
## `use_sync_state`: State from Events
`use_sync_state` allows a component's internal `State` to be automatically updated whenever a specific type of event is emitted to its `Context`. This is a powerful way to inject external data or changes into a component's reactive state.
### How it works:
It combines `use_state` and `cx.on_event`.
1. You provide an initial value for the state.
2. You specify the event type (`E`) and a `decoder` function.
3. The `decoder` function takes an `&E` event and returns a new value `T` for the state.
4. Whenever an event of type `E` is emitted to the current `Context`, the `decoder` runs, and the internal `State<T>` is updated.
```rust
use osui::prelude::*;
use std::sync::Arc;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
// Define a simple event to change a message
#[derive(Debug, Clone)]
pub struct MessageChangeEvent(pub String);
#[component]
fn MessageDisplay(cx: &Arc<Context>) -> View {
// Use `use_sync_state` to update the message based on `MessageChangeEvent`
let message = use_sync_state(
cx,
"Initial Message".to_string(), // Initial state value
|event: &MessageChangeEvent| event.0.clone(), // Decoder: extract string from event
);
rsx! {
"Received Message:"
format!(" {}", message.get_dl())
}.view(&cx)
}
#[component]
fn MessageInput(cx: &Arc<Context>) -> View {
// Simulate input by reacting to key presses and emitting MessageChangeEvent
use_effect(
{
let cx = cx.clone();
move || {
let _ = crossterm::terminal::enable_raw_mode();
loop {
if event::poll(std::time::Duration::from_millis(50)).unwrap() {
if let Event::Key(key_event) = event::read().unwrap() {
if key_event.kind == KeyEventKind::Press {
match key_event.code {
KeyCode::Char(c) => {
let msg = format!("Typed: {}", c);
cx.emit_event(MessageChangeEvent(msg));
},
KeyCode::Enter => {
cx.emit_event(MessageChangeEvent("Enter pressed!".to_string()));
},
KeyCode::Esc => {
let _ = crossterm::terminal::disable_raw_mode();
cx.stop().expect("Failed to stop engine");
break;
},
_ => {}
}
}
}
}
}
}
},
&[], // Run once on mount
);
rsx! {
"Type something to change the message (Esc to quit):"
}.view(&cx)
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
MessageInput {} // Emits MessageChangeEvent
MessageDisplay {} // Synchronizes its state with MessageChangeEvent
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run app");
let _ = crossterm::terminal::disable_raw_mode();
}
```
In this example, `MessageInput` emits `MessageChangeEvent`s based on keyboard input. `MessageDisplay` automatically updates its `message` state whenever it receives one of these events from its parent `Context`, thanks to `use_sync_state`.
## `use_sync_effect`: Events from State
`use_sync_effect` allows changes in a component's internal `State` to automatically trigger the emission of a specific type of event to its `Context`. This is useful for communicating state changes upwards or to sibling components.
### How it works:
It combines `use_effect` and `cx.emit_event`.
1. You provide an `State<T>` instance you want to monitor.
2. You specify an `encoder` function and optional dependencies.
3. The `encoder` function takes a `&State<T>` and returns an event `Ev`.
4. Whenever the `State<T>` changes (or any specified dependencies), the `encoder` runs, and the generated event `Ev` is emitted to the current `Context`.
```rust
use osui::prelude::*;
use std::sync::Arc;
use std::collections::HashMap;
// Event to signal a counter has changed
#[derive(Debug, Clone)]
pub struct CounterUpdatedEvent {
pub id: usize,
pub new_value: i32,
}
#[component]
fn ChildCounter(cx: &Arc<Context>, id: &usize, initial_value: &i32) -> View {
let count = use_state(*initial_value);
// Use `use_sync_effect` to emit `CounterUpdatedEvent` when `count` changes
use_sync_effect(
cx,
&count, // Monitor this state
move |state_ref: &State<i32>| {
// Encoder: create an event from the state
CounterUpdatedEvent {
id: *id,
new_value: state_ref.get_dl(),
}
},
&[&count], // Effect runs when `count` changes
);
// Simulate incrementing the counter periodically
use_effect(
{
let count = count.clone();
move || {
loop {
sleep(1000); // Increment every second
*count.get() += 1;
}
}
},
&[], // Run once on mount
);
rsx! {
format!("Counter {}: {}", id, count.get_dl())
}.view(&cx)
}
#[component]
fn ParentDashboard(cx: &Arc<Context>) -> View {
let all_counts = use_state(HashMap::<usize, i32>::new());
// Listen for `CounterUpdatedEvent` from children
cx.on_event({
let all_counts = all_counts.clone();
move |_ctx, event: &CounterUpdatedEvent| {
let mut counts_guard = all_counts.get();
counts_guard.insert(event.id, event.new_value);
}
});
rsx! {
"Dashboard Overview:"
@for (id, value) in all_counts.get_dl() {
format!(" Counter {}: {}", id, value)
}
"---"
ChildCounter { id: 1, initial_value: 0 }
ChildCounter { id: 2, initial_value: 10 }
}.view(&cx)
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
ParentDashboard {}
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run app");
}
```
In this example:
* `ChildCounter` uses `use_sync_effect` to emit a `CounterUpdatedEvent` every time its internal `count` state changes.
* `ParentDashboard` listens for these `CounterUpdatedEvent`s (which bubble up from its children) using `cx.on_event` and updates its own `all_counts` `HashMap` state. This `HashMap` then drives the display in the dashboard.
## When to use `use_sync_state` and `use_sync_effect`:
* **Inter-component Communication**: When components need to communicate beyond simple prop passing. Events are excellent for sibling-to-sibling or child-to-ancestor communication without prop drilling.
* **Centralized State Management**: You can have a central "store" component that emits events, and other components `use_sync_state` to react to those events.
* **External System Integration**: When your TUI needs to react to external system events (e.g., file changes, network updates) by mapping them to internal `State`.
* **Decoupling**: They help decouple components, as they don't need direct references to each other, only awareness of event types.
By combining `use_state`, `use_effect`, `use_sync_state`, and `use_sync_effect` with OSUI's event system, you can build powerful and maintainable data flow architectures for your TUI applications.
**Next:** Learn how to arrange your components visually using OSUI's rendering primitives in [Building Complex Layouts](./06-building-complex-layouts.md).
@@ -0,0 +1,155 @@
---
sidebar_position: 6
title: Building Complex Layouts
---
# Building Complex Layouts
OSUI provides foundational primitives for rendering text and components, allowing you to compose them into complex layouts. While OSUI doesn't include a built-in layout engine (like flexbox or grid), it exposes the `DrawContext` and geometric types (`Point`, `Area`, `Size`) that enable you to manually position elements or build your own layout components.
## The Rendering Pipeline
At its core, OSUI's rendering works by accumulating `DrawInstruction`s into a `DrawContext`. The engine then takes this `DrawContext` and executes the instructions to draw to the terminal.
1. **`View`**: A component returns a `View`, which is essentially a closure that takes a mutable `DrawContext` and adds drawing instructions to it.
2. **`DrawContext`**: This is the canvas for your component. It has an `area` (the total space available to the current component) and an `allocated` area (the space currently used by drawing instructions within that component).
3. **`DrawInstruction`**: The actual commands to draw, like `Text`, `View` (for child components), or `Child` (for nested `DrawContext`s).
## Core Rendering Primitives
These types, found in the `osui::render` module, are essential for manual layout.
* ### `Point`
Represents a position `(x, y)` in terminal coordinates. `x` is column, `y` is row.
```rust
pub struct Point {
pub x: u16,
pub y: u16,
}
```
* ### `Size`
Represents `width` and `height` in terminal columns and rows.
```rust
pub struct Size {
pub width: u16,
pub height: u16,
}
```
* ### `Area`
Combines `Point` and `Size` to define a rectangular region.
```rust
pub struct Area {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
```
## `DrawContext`: Your Drawing Canvas
Inside a `View` closure, you receive a mutable `DrawContext`. This is how you interact with the rendering system.
```rust
use osui::prelude::*;
use std::sync::Arc;
#[component]
fn MyCustomLayout(cx: &Arc<Context>, children: &Rsx) -> View {
let children_view = children.view(&cx); // Convert Rsx children to a View
Arc::new(move |ctx: &mut DrawContext| {
// `ctx.area` gives you the total available space for this component.
let available_width = ctx.area.width;
let available_height = ctx.area.height;
// --- Manual layout example: Two columns ---
// Allocate space for the left column
let left_col_area = ctx.allocate(
ctx.area.x,
ctx.area.y,
available_width / 2,
available_height,
);
// Draw some text in the left column
ctx.draw_text(
Point { x: left_col_area.x + 1, y: left_col_area.y + 1 },
"Left Panel",
);
ctx.draw_text(
Point { x: left_col_area.x + 1, y: left_col_area.y + 2 },
&format!("Available: {}x{}", left_col_area.width, left_col_area.height),
);
// Allocate space for the right column
let right_col_area = ctx.allocate(
ctx.area.x + available_width / 2, // Start x at half width
ctx.area.y,
available_width / 2,
available_height,
);
// Draw the children (passed to MyCustomLayout) into the right column
// This effectively "moves" the children's rendering into this specific area.
ctx.draw_view(
right_col_area, // Children will render relative to this new area
children_view.clone(),
);
// Draw more text in the right column
ctx.draw_text(
Point { x: right_col_area.x + 1, y: right_col_area.y + 1 },
"Right Panel (Children Area)",
);
})
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
MyCustomLayout {
rsx! {
"Hello from the child content!"
"This text should appear in the right panel."
}
}
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run app");
}
```
### Key `DrawContext` Methods:
* **`ctx.area`**: The `Area` that the *current component* has been allocated by its parent. All drawing coordinates are relative to `ctx.area.x` and `ctx.area.y`.
* **`ctx.allocate(x, y, width, height)`**: This method marks a region within `ctx.area` as "used". It takes coordinates relative to `ctx.area`'s top-left corner (0,0 of its own space) and returns a new `Area` representing the allocated sub-region. It also updates `ctx.allocated` to be the union of all allocations so far within this `DrawContext`.
* **`ctx.draw_text(point, text)`**: Adds a `Text` instruction. `point` is relative to `ctx.area`.
* **`ctx.draw_view(area, view)`**: Adds a `View` instruction. This is how you tell the renderer to draw a child component (or another `View`) within a specific `area`. The `area` here is also relative to `ctx.area`. The child view will then receive this `area` as its own `ctx.area`.
### Building a Simple Layout Component
The `MyCustomLayout` component above demonstrates a basic two-column layout. You can create more sophisticated layout components by:
1. **Calculating Sub-Regions**: Based on `ctx.area.width` and `ctx.area.height`, divide the space into logical sub-regions (e.g., header, footer, sidebar, main content).
2. **Allocating Space**: Use `ctx.allocate()` to define these sub-regions.
3. **Drawing Content**:
* For static text or background elements, use `ctx.draw_text()`.
* For child components, call `child_rsx.view(&cx)` to get their `View`, and then use `ctx.draw_view(sub_area, child_view)` to render them in their designated space.
### Tips for Layouts:
* **Relative Positioning**: Always think of `Point` and `Area` coordinates as being *relative* to the `ctx.area` of the current `View` being rendered. The `Console` engine handles translating these relative coordinates to absolute terminal coordinates.
* **No Overlapping**: Be mindful of overlapping areas. If you draw two things to the same `Point`, the last one drawn will overwrite the first. OSUI does not automatically manage Z-ordering.
* **Responsive Design**: Consider how your layouts will adapt to different terminal sizes. You can use `ctx.area.width` and `ctx.area.height` to make calculations dynamic.
* **Composition**: Layout components can themselves be children of other layout components, allowing you to build complex nested structures.
While implementing a full layout system like CSS Flexbox is beyond the scope of OSUI's core, these primitives empower you to craft highly customized and visually rich terminal interfaces by manually managing space and component placement.
**Next:** Explore the detailed [API Reference](../reference/00-crate-structure.md) for all OSUI modules and types.