v0.1.0 with spark
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# Building UIs with RSX
|
||||
|
||||
OSUI leverages a declarative syntax, similar to JSX in web development, to define your UI elements. This is primarily facilitated by the `rsx!` macro. This guide explains how to use `rsx!` to construct your UI tree, manage properties, attach components, and handle reactive updates.
|
||||
|
||||
## The `rsx!` Macro: Declarative UI Definition
|
||||
|
||||
The `rsx!` macro provides a concise way to declare a hierarchy of UI elements. It processes a block of UI definitions and expands them into an `Rsx` object, which can then be drawn onto the `Screen`.
|
||||
|
||||
A basic `rsx!` block looks like this:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
fn my_ui_function(my_state: State<String>) -> Rsx {
|
||||
rsx! {
|
||||
// A simple string is treated as a text element
|
||||
"Hello, RSX!"
|
||||
|
||||
// An element with properties and children
|
||||
Div {
|
||||
// Another text element
|
||||
"I am a child of the Div."
|
||||
}
|
||||
|
||||
// A dynamic element with a state dependency
|
||||
%my_state
|
||||
Div {
|
||||
"Current state value: {my_state}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Element Types
|
||||
|
||||
1. **Text Literals**: A string literal directly within `rsx!` creates a simple text element. OSUI automatically converts `String` and `(String, u32)` (for colored text) into renderable `Element`s.
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
"This is plain text."
|
||||
("This text is colored", 0xFF00FF) // Pink text
|
||||
}
|
||||
```
|
||||
|
||||
2. **Built-in Elements**: OSUI provides several pre-defined elements like `Div`, `FlexRow`, `FlexCol`, `Input`, `Paginator`, and `Heading`. You reference them by their struct name.
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
Div {
|
||||
"Content inside a div."
|
||||
}
|
||||
FlexCol, gap: 1, {
|
||||
"Item 1"
|
||||
"Item 2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Properties and Configuration
|
||||
|
||||
Elements can be configured by setting their public fields. This is done by listing the field names and their values after the element type, separated by commas.
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
// Setting `gap` for FlexRow
|
||||
FlexRow, gap: 2, {
|
||||
"First item"
|
||||
"Second item"
|
||||
}
|
||||
|
||||
// Setting `smooth` for Heading
|
||||
Heading, smooth: true, { "My Title" }
|
||||
}
|
||||
```
|
||||
|
||||
If an element has no properties to set, or you are using default values, you can omit the property list:
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
Div { "No custom properties needed here." }
|
||||
}
|
||||
```
|
||||
|
||||
## Nesting Elements (Children)
|
||||
|
||||
Elements can contain other elements as children. This creates the UI tree. Children are defined within curly braces `{}` immediately following the element declaration.
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
Div {
|
||||
"Parent Div"
|
||||
Div { // Nested Div
|
||||
"Child Div"
|
||||
"Another child"
|
||||
}
|
||||
FlexCol { // Another child, a FlexCol
|
||||
"Flex item 1"
|
||||
"Flex item 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Attaching Components
|
||||
|
||||
Components are Rust structs that implement the `Component` trait. They can be attached to any widget to extend its behavior or provide additional data (like styling or transformation). In `rsx!`, components are attached using the `@` prefix.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
// Assume MyComponent is defined via `component!(MyComponent { /* ... */ });`
|
||||
// and Transform and Style are imported from `osui::style`.
|
||||
|
||||
rsx! {
|
||||
// Attach a Transform component to control position/size
|
||||
@Transform::new().center().padding(1, 1);
|
||||
// Attach a Style component for background and foreground colors
|
||||
@Style { background: Background::Solid(0x333333), foreground: Some(0xFFFFFF) };
|
||||
Div {
|
||||
"This div is centered, padded, and has a dark background with white text."
|
||||
}
|
||||
|
||||
// Attach a custom component
|
||||
@MyComponent { my_prop: "value".to_string() };
|
||||
Div {
|
||||
"This div has MyComponent attached."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can attach multiple components to a single element. They are applied in the order they are declared.
|
||||
|
||||
## Dynamic vs. Static Widgets
|
||||
|
||||
OSUI distinguishes between static and dynamic widgets for performance and reactivity.
|
||||
|
||||
### Static Widgets (`static` keyword)
|
||||
|
||||
A widget declared with the `static` keyword means that the root `Element` instance itself will be created only once. Its children, however, can still be dynamic. This is suitable for parts of your UI that do not change their fundamental structure or the root `Element` type.
|
||||
|
||||
```rust
|
||||
rsx! {
|
||||
static Heading, smooth: true, { "Static Title" }
|
||||
static Div { "This div's root element is static." }
|
||||
}
|
||||
```
|
||||
|
||||
* **When to use `static`**: For elements whose `Element` trait implementation doesn't change and doesn't depend on external reactive state to rebuild itself. This can include simple text, static containers, or complex elements whose internal state is managed purely by their own logic (not OSUI's `State` system).
|
||||
* **Performance**: Generally more performant as they avoid re-evaluating the element creation closure on every refresh cycle.
|
||||
|
||||
### Dynamic Widgets (Default)
|
||||
|
||||
By default, any element declared without `static` is considered dynamic. This means its creation closure (`FnMut() -> WidgetLoad`) will be re-evaluated whenever its declared dependencies change or when a manual `refresh()` is triggered.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
let my_counter = use_state(0);
|
||||
rsx! {
|
||||
// This Div is dynamic because it depends on `my_counter`
|
||||
%my_counter
|
||||
Div {
|
||||
"Counter: {my_counter}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **When to use Dynamic**: For any element whose content or type changes based on reactive state or other external factors that necessitate a rebuild of the underlying `Element`.
|
||||
* **Reactivity**: Essential for building interactive UIs that respond to state changes.
|
||||
|
||||
## Reactive Updates with State (`%` prefix)
|
||||
|
||||
OSUI's reactivity system allows you to automatically re-render parts of your UI when specific `State` variables change. This is achieved by declaring a dependency using the `%` prefix followed by the state variable's identifier.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
screen.extension(InputExtension); // Needed for input to trigger updates
|
||||
|
||||
let click_count = use_state(0);
|
||||
|
||||
// Increment counter on any key press
|
||||
screen.draw_dyn({
|
||||
let click_count = click_count.clone();
|
||||
move || {
|
||||
WidgetLoad::new(String::new())
|
||||
.component(Handler::new(move |_, e: &crossterm::event::Event| {
|
||||
if let crossterm::event::Event::Key(_) = e {
|
||||
**click_count.get() += 1;
|
||||
}
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
// This Div will re-render whenever `click_count` changes
|
||||
%click_count
|
||||
Div {
|
||||
"You have pressed a key {click_count} times."
|
||||
}
|
||||
}.draw(&screen);
|
||||
|
||||
screen.run()
|
||||
}
|
||||
```
|
||||
|
||||
When `**click_count.get() += 1;` is called, it marks `click_count` as changed. During the next render cycle, any `DynWidget` (like our `Div` above) that declares `click_count` as a dependency will be automatically refreshed (rebuilt), reflecting the new value.
|
||||
|
||||
Multiple dependencies can be declared:
|
||||
|
||||
```rust
|
||||
let state_a = use_state(0);
|
||||
let state_b = use_state(false);
|
||||
|
||||
rsx! {
|
||||
%state_a %state_b
|
||||
Div {
|
||||
"A: {state_a}, B: {state_b}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `rsx!` macro automatically clones the `Arc<State<T>>` for each declared dependency and passes it into the widget's creation closure, ensuring the closure can capture and use the state without ownership issues.
|
||||
|
||||
## Expanding RSX Blocks (`=>`)
|
||||
|
||||
You can compose `rsx!` blocks by calling a function that returns `Rsx` and using the `=>` operator. This is useful for breaking down complex UIs into smaller, manageable functions.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
fn my_header() -> Rsx {
|
||||
rsx! {
|
||||
Heading, smooth: true, { "My App" }
|
||||
}
|
||||
}
|
||||
|
||||
fn my_content(data: State<String>) -> Rsx {
|
||||
rsx! {
|
||||
%data
|
||||
Div {
|
||||
"Data: {data}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsx! {
|
||||
my_header => () // Call my_header function to insert its elements
|
||||
my_content => (my_state_variable.clone()) // Pass arguments if needed
|
||||
Div { "Footer" }
|
||||
}
|
||||
```
|
||||
|
||||
The `rsx_inner!` macro, which `rsx!` expands to, handles the recursive insertion of `RsxElement`s from the called function.
|
||||
|
||||
## Summary
|
||||
|
||||
The `rsx!` macro is the cornerstone of building UIs in OSUI. By understanding how to define elements, set properties, attach components, and manage reactivity with `State`, you can construct powerful and interactive terminal applications with a clean and declarative syntax.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
# Building Custom Elements
|
||||
|
||||
OSUI's component-based architecture allows you to create your own UI elements by implementing the `Element` trait. This guide details how to define custom elements, render them, handle their children, and process events.
|
||||
|
||||
## The `Element` Trait
|
||||
|
||||
The `Element` trait is the core interface for any renderable UI component in OSUI. It defines methods that the rendering engine calls during the UI lifecycle.
|
||||
|
||||
```rust
|
||||
pub trait Element: Send + Sync {
|
||||
/// Called to perform rendering for the element.
|
||||
fn render(&mut self, scope: &mut RenderScope);
|
||||
|
||||
/// Called after rendering, for follow-up logic or cleanup.
|
||||
fn after_render(&mut self, scope: &mut RenderScope);
|
||||
|
||||
/// Called to draw child widgets, if any.
|
||||
fn draw_child(&mut self, element: &Arc<Widget>);
|
||||
|
||||
/// Called when an event occurs.
|
||||
fn event(&mut self, event: &dyn Event);
|
||||
|
||||
/// Returns a type-erased reference to this object.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
/// Returns a mutable type-erased reference to this object.
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
```
|
||||
|
||||
## Defining a Simple Custom Element
|
||||
|
||||
Let's create a very basic `Box` element that just draws a rectangle and its text content.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct MyBox {
|
||||
// Children are typically stored if your element acts as a container
|
||||
children: Vec<Arc<Widget>>,
|
||||
// Internal state for managing the box's size, if not determined by children
|
||||
calculated_size: (u16, u16),
|
||||
pub border_color: u32, // Public field for RSX properties
|
||||
pub fill_color: u32, // Public field for RSX properties
|
||||
}
|
||||
|
||||
impl MyBox {
|
||||
// Constructor for use with `rsx!`
|
||||
pub fn new() -> Self {
|
||||
MyBox {
|
||||
children: Vec::new(),
|
||||
calculated_size: (0, 0),
|
||||
border_color: 0xAAAAAA, // Default gray border
|
||||
fill_color: 0x333333, // Default dark fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for MyBox {
|
||||
// Called when the element needs to render its own content.
|
||||
fn render(&mut self, scope: &mut RenderScope) {
|
||||
// Draw the background rectangle first
|
||||
scope.draw_rect(
|
||||
0,
|
||||
0,
|
||||
scope.get_transform().width,
|
||||
scope.get_transform().height,
|
||||
self.fill_color,
|
||||
);
|
||||
|
||||
// Draw a border (outline)
|
||||
// Note: RenderScope's draw_rect doesn't draw outlines directly,
|
||||
// so we'd typically rely on `Style::Background::Outline` applied
|
||||
// as a component to the widget containing this element.
|
||||
// For a simple filled box, we can just draw the background.
|
||||
// If we wanted a distinct border *inside* the box, it'd be more complex.
|
||||
// For simplicity, we'll assume `Style` component handles outlines.
|
||||
|
||||
// Get the current accumulated size from the scope,
|
||||
// which might have been influenced by child rendering in `after_render`.
|
||||
let (width, height) = scope.get_size_or(self.calculated_size.0, self.calculated_size.1);
|
||||
// Ensure the scope tracks at least this area for its own calculations.
|
||||
scope.use_area(width, height);
|
||||
}
|
||||
|
||||
// Called after the element's `render` method and its children's `render` methods.
|
||||
// This is where container elements typically render their children.
|
||||
fn after_render(&mut self, scope: &mut RenderScope) {
|
||||
// Store the original parent size for restoration later
|
||||
let (original_parent_width, original_parent_height) = scope.get_parent_size();
|
||||
|
||||
// Pass this element's resolved size as the parent size for its children.
|
||||
// This is crucial for children to correctly calculate their `Full` or `Center` dimensions.
|
||||
let self_transform = scope.get_transform().clone(); // Clone resolved transform for current element
|
||||
scope.set_parent_size(self_transform.width, self_transform.height);
|
||||
|
||||
// Track max dimensions used by children for this box's overall size
|
||||
let mut max_child_width = 0;
|
||||
let mut max_child_height = 0;
|
||||
|
||||
for child_widget in &self.children {
|
||||
// Children marked with NoRender or NoRenderRoot are handled by their direct parent
|
||||
// or the screen, not by this specific element's `after_render`.
|
||||
// This prevents double-rendering if they are also top-level widgets.
|
||||
if child_widget.get::<NoRender>().is_some() {
|
||||
continue;
|
||||
}
|
||||
|
||||
scope.clear(); // Clear the scope for each child's rendering context
|
||||
|
||||
// Apply any Transform or Style components attached directly to the child widget.
|
||||
if let Some(child_style) = child_widget.get() {
|
||||
scope.set_style(child_style);
|
||||
}
|
||||
if let Some(child_transform_comp) = child_widget.get() {
|
||||
scope.set_transform(&child_transform_comp);
|
||||
}
|
||||
|
||||
// Render the child's own content. This fills its render_stack.
|
||||
child_widget.get_elem().render(scope);
|
||||
|
||||
// Re-apply the transform *after* child render to ensure any content-based sizing
|
||||
// (Dimension::Content) is reflected in the child's `raw_transform.width/height`.
|
||||
if let Some(child_transform_comp) = child_widget.get() {
|
||||
scope.set_transform(&child_transform_comp);
|
||||
}
|
||||
|
||||
// Get the child's now-resolved raw transform (position, size, padding)
|
||||
let child_raw_transform = scope.get_transform_mut();
|
||||
|
||||
// Offset the child's actual drawing coordinates by the parent's position and padding.
|
||||
// This ensures children are drawn relative to their parent's content area.
|
||||
child_raw_transform.x += self_transform.x + self_transform.px;
|
||||
child_raw_transform.y += self_transform.y + self_transform.py;
|
||||
child_raw_transform.px += self_transform.px; // Accumulate padding
|
||||
child_raw_transform.py += self_transform.py; // Accumulate padding
|
||||
|
||||
// Update the parent's (MyBox's) effective size based on its children.
|
||||
// This is crucial for MyBox to "auto-size" if it's `Dimension::Content`.
|
||||
max_child_width = max_child_width.max(
|
||||
child_raw_transform.x
|
||||
+ child_raw_transform.width
|
||||
+ (child_raw_transform.px * 2)
|
||||
- self_transform.x
|
||||
- self_transform.px, // Relative to parent content area
|
||||
);
|
||||
max_child_height = max_child_height.max(
|
||||
child_raw_transform.y
|
||||
+ child_raw_transform.height
|
||||
+ (child_raw_transform.py * 2)
|
||||
- self_transform.y
|
||||
- self_transform.py, // Relative to parent content area
|
||||
);
|
||||
|
||||
|
||||
scope.draw(); // Draw the child's accumulated render stack to the terminal.
|
||||
child_widget.get_elem().after_render(scope); // Recursively call after_render for child
|
||||
}
|
||||
|
||||
// After all children are processed, update MyBox's calculated size.
|
||||
// Add back MyBox's own padding to the children's max extent.
|
||||
self.calculated_size = (
|
||||
max_child_width + (self_transform.px * 2),
|
||||
max_child_height + (self_transform.py * 2),
|
||||
);
|
||||
// Ensure the scope's own transform reflects the newly calculated size
|
||||
scope.use_area(self.calculated_size.0, self.calculated_size.1);
|
||||
|
||||
// Restore the parent size for subsequent elements at this level.
|
||||
scope.set_parent_size(original_parent_width, original_parent_height);
|
||||
}
|
||||
|
||||
// Called when a child widget is added to this element via `rsx!`.
|
||||
fn draw_child(&mut self, element: &Arc<Widget>) {
|
||||
// Mark the child as `NoRenderRoot` so the Screen doesn't try to render it directly.
|
||||
// This indicates that its rendering will be managed by this parent element's `after_render`.
|
||||
element.inject(|w| w.component(NoRenderRoot));
|
||||
self.children.push(element.clone());
|
||||
}
|
||||
|
||||
// Handles incoming events for this element.
|
||||
fn event(&mut self, event: &dyn Event) {
|
||||
// You can check for specific event types
|
||||
if let Some(key_event) = event.get::<crossterm::event::Event>() {
|
||||
// Example: Respond to a key press
|
||||
if let crossterm::event::Event::Key(ke) = key_event {
|
||||
// println!("MyBox received key: {:?}", ke.code); // For debugging
|
||||
}
|
||||
}
|
||||
// You might also want to pass events to children,
|
||||
// though OSUI's default event system often dispatches globally.
|
||||
}
|
||||
|
||||
// Required for downcasting the trait object.
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using Your Custom Element in `rsx!`
|
||||
|
||||
After defining `MyBox`, you can use it just like any other built-in element:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
// In your main or app function:
|
||||
rsx! {
|
||||
@Transform::new().dimensions(50, 10); // Set a fixed size for the box
|
||||
MyBox, border_color: 0xFF0000, fill_color: 0x0000FF, {
|
||||
("Hello from inside MyBox!", 0xFFFFFF)
|
||||
Div {
|
||||
"Another nested div!"
|
||||
}
|
||||
}
|
||||
}.draw(&screen);
|
||||
```
|
||||
|
||||
## Key Considerations for Custom Elements
|
||||
|
||||
* **`render()` vs. `after_render()`**:
|
||||
* `render()`: Use this for drawing the element's *own* content (e.g., text, background shapes). It should use the `RenderScope` to queue drawing commands.
|
||||
* `after_render()`: Use this for container logic, specifically iterating through `self.children` and rendering them. It needs to manage the `RenderScope`'s parent size and transform for each child.
|
||||
* **`draw_child()`**: This method is called by the `rsx!` macro when you nest elements inside your custom element. You *must* store the `Arc<Widget>` in a `Vec` or similar structure.
|
||||
* Crucially, you should also call `element.inject(|w| w.component(NoRenderRoot));`. This tells the main `Screen` loop to *not* render this child directly at the root level, as its rendering will be managed by its parent (`MyBox` in this case).
|
||||
* **`as_any()` / `as_any_mut()`**: These are boilerplate methods required for downcasting trait objects, allowing you to retrieve specific `Element` or `Component` types from a `Box<dyn Element>` or `Box<dyn Component>`.
|
||||
* **`RenderScope` Usage**:
|
||||
* `scope.set_transform(&t)`: Applies a `Transform` component's rules to calculate the absolute position and size (`RawTransform`) for the current scope, based on its parent's size.
|
||||
* `scope.get_transform()` / `scope.get_transform_mut()`: Accesses the `RawTransform` that represents the current element's resolved position and size.
|
||||
* `scope.set_parent_size(width, height)`: Critical for nested elements. Before rendering a child, set the `scope`'s parent size to *this element's* resolved size so the child can correctly resolve its `Dimension::Full` or `Position::Center` values. Remember to restore the original parent size after processing all children.
|
||||
* `scope.use_area(width, height)`: In your `render` method, if your element's size depends on its content or a fixed size, use this to tell the `RenderScope` what minimum area your element occupies. This helps when the element's `Dimension` is `Content`.
|
||||
* **State Management**: If your custom element needs to hold dynamic data, consider using `osui::state::State<T>` for reactive updates, especially if you want your element to trigger re-renders of itself or its children when its internal data changes.
|
||||
|
||||
By following these guidelines, you can create sophisticated and well-integrated custom UI elements that extend OSUI's capabilities to fit your application's unique needs.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Handling Input
|
||||
|
||||
Interactive terminal applications rely heavily on input handling. OSUI provides a flexible event system to capture and respond to user input, primarily keyboard events. This guide explains how to integrate and use OSUI's input capabilities.
|
||||
|
||||
## The `InputExtension`
|
||||
|
||||
The core of OSUI's input system is the `InputExtension`. This extension is responsible for:
|
||||
|
||||
1. Enabling `crossterm`'s raw mode, which allows for detailed, non-buffered input events.
|
||||
2. Spawning a dedicated thread to continuously read terminal events.
|
||||
3. Dispatching these events to all active widgets on the `Screen`.
|
||||
|
||||
### Registering the `InputExtension`
|
||||
|
||||
To start receiving input, you must register the `InputExtension` with your `Screen` instance:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
// Register the InputExtension once at startup
|
||||
screen.extension(InputExtension);
|
||||
|
||||
// ... your rsx! or draw calls ...
|
||||
|
||||
screen.run()
|
||||
}
|
||||
```
|
||||
|
||||
Once registered, the extension will manage raw mode and event polling. When your application closes (e.g., via `screen.close()`), the `InputExtension`'s `on_close` method will automatically disable raw mode, returning the terminal to its normal state.
|
||||
|
||||
## The `Event` Trait and `Handler` Component
|
||||
|
||||
OSUI uses a generic `Event` trait and a `Handler` component to enable widgets to subscribe to and process events.
|
||||
|
||||
* **`Event` Trait**: A marker trait that identifies types that can be dispatched as events. It requires `Send + Sync` and `as_any()` for type erasure. `crossterm::event::Event` already implements this within OSUI.
|
||||
* **`Handler<E>` Component**: A wrapper component that holds a closure (`FnMut(&Arc<Widget>, &E)`) which will be called when an event of type `E` is dispatched to the widget it's attached to.
|
||||
|
||||
### Attaching an Event Handler
|
||||
|
||||
You attach `Handler` components to widgets using the `@` syntax in `rsx!` or by calling `widget.component(Handler::new(...))` directly.
|
||||
|
||||
The `Handler`'s closure receives two arguments:
|
||||
1. An `Arc<Widget>` representing the widget the handler is attached to. This allows the handler to interact with its own widget, e.g., by getting or setting components on it.
|
||||
2. A reference to the event (`&E`).
|
||||
|
||||
#### Example: Handling Keyboard Input
|
||||
|
||||
To handle `crossterm::event::Event` (which includes `KeyEvent`, `MouseEvent`, `ResizeEvent`, etc.), you'll typically downcast the incoming event to a `KeyEvent`.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
use crossterm::event::{KeyCode, KeyEvent, Event as CrosstermEvent}; // Alias to avoid conflict with osui::event!
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
screen.extension(InputExtension);
|
||||
|
||||
// This widget will capture all keyboard events.
|
||||
// We attach the Handler directly to the root widget drawn on the screen.
|
||||
rsx! {
|
||||
@Handler::new({
|
||||
let screen = screen.clone(); // Clone Arc for the closure
|
||||
move |current_widget, event: &CrosstermEvent| {
|
||||
// Check if the event is a KeyEvent
|
||||
if let CrosstermEvent::Key(key_event) = event {
|
||||
match key_event.code {
|
||||
KeyCode::Char('q') => {
|
||||
// Close the screen if 'q' is pressed
|
||||
println!("Quitting application...");
|
||||
screen.close();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Example: Increment a counter on Enter
|
||||
if let Some(mut my_state_comp) = current_widget.get::<State<i32>>() {
|
||||
**my_state_comp.get_mut() += 1;
|
||||
println!("Enter pressed! Count: {}", my_state_comp.get_dl());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Handle other keys or print them for debugging
|
||||
// println!("Key pressed: {:?}", key_event.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Div {
|
||||
"Press 'q' to quit, 'Enter' to increment a hidden counter (check console)."
|
||||
}
|
||||
}.draw(&screen);
|
||||
|
||||
screen.run()
|
||||
}
|
||||
```
|
||||
|
||||
In the example above, the `Handler` is attached to the root `Div`. Because the `InputExtension` dispatches events to *all* widgets managed by the screen, this root widget will receive every `crossterm::event::Event`.
|
||||
|
||||
### Element-Specific Event Handling
|
||||
|
||||
Some elements, like `Input` and `Paginator`, implement the `Element::event` method internally to handle specific events relevant to their functionality.
|
||||
|
||||
* **`Input` Element**: Manages text input, cursor movement (left/right), backspace, and delete based on `KeyEvent`s.
|
||||
* **`Paginator` Element**: Switches pages on `KeyCode::Tab` and `KeyCode::BackTab` (`Shift+Tab`).
|
||||
|
||||
When you use these elements, their internal `event` method is automatically called by the `Screen`'s rendering loop. You can still attach your own `Handler` components to these elements for additional, custom event logic that doesn't interfere with their built-in behavior.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
Paginator {
|
||||
// This handler will be called *in addition* to Paginator's default tab handling.
|
||||
@Handler::new(|_, e: &crossterm::event::Event| {
|
||||
if let crossterm::event::Event::Key(KeyEvent { code: KeyCode::Char('p'), .. }) = e {
|
||||
// Do something when 'p' is pressed on the Paginator
|
||||
println!("Paginator received 'p' key!");
|
||||
}
|
||||
});
|
||||
Div { "Page 1" }
|
||||
Div { "Page 2" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Events
|
||||
|
||||
Beyond `crossterm` events, you can define and dispatch your own custom event types using the `event!` macro. This is useful for communication between different parts of your application or custom extensions.
|
||||
|
||||
See the [Advanced: Custom Events](../advanced/custom_events.md) guide for details.
|
||||
|
||||
## Summary
|
||||
|
||||
By understanding how to register the `InputExtension` and attach `Handler` components to your widgets, you gain full control over user interaction in your OSUI applications. This robust event system allows for building highly responsive and interactive terminal UIs.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# Layout and Styling
|
||||
|
||||
OSUI provides a robust layout and styling system to control the appearance and positioning of your UI elements. This system is built around several key concepts: `Transform`, `Position`, `Dimension`, `Style`, and `Background`.
|
||||
|
||||
## 1. `Transform`: Positioning and Sizing Rules
|
||||
|
||||
The `Transform` component is used to define how a widget should be positioned and sized relative to its parent. It specifies declarative rules rather than absolute pixel values, allowing for flexible and responsive layouts.
|
||||
|
||||
You typically create and attach a `Transform` using the `@` component syntax in `rsx!` or by calling `widget.component(Transform::new()...)`.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
// Default transform: (0,0) position, content-sized
|
||||
let default_transform = Transform::new();
|
||||
|
||||
// Centered horizontally and vertically, content-sized
|
||||
let centered_transform = Transform::center();
|
||||
|
||||
rsx! {
|
||||
@Transform::new().padding(2, 1).dimensions(30, 5);
|
||||
Div { "A fixed-size div with padding." }
|
||||
|
||||
@Transform::new().right().margin(5, 0);
|
||||
Div { "Aligned to the right with a 5-cell horizontal margin." }
|
||||
|
||||
@Transform::new().bottom().margin(0, 2);
|
||||
Div { "Aligned to the bottom with a 2-cell vertical margin." }
|
||||
}
|
||||
```
|
||||
|
||||
### `Transform` Fields:
|
||||
|
||||
* `x: Position`: Horizontal position relative to the parent.
|
||||
* `y: Position`: Vertical position relative to the parent.
|
||||
* `mx: i32`: Horizontal margin (offset) from the calculated `x` position. Can be negative for overlap.
|
||||
* `my: i32`: Vertical margin (offset) from the calculated `y` position. Can be negative for overlap.
|
||||
* `px: u16`: Horizontal padding (internal spacing) around the content.
|
||||
* `py: u16`: Vertical padding (internal spacing) around the content.
|
||||
* `width: Dimension`: Rule for the widget's width.
|
||||
* `height: Dimension`: Rule for the widget's height.
|
||||
|
||||
### Chainable Methods for `Transform`
|
||||
|
||||
`Transform` provides several convenient chainable methods for common layout patterns:
|
||||
|
||||
* `Transform::new()`: Creates a default transform at `(0,0)` with `Content` dimensions and no padding/margin.
|
||||
* `Transform::center()`: Creates a transform centered both horizontally and vertically.
|
||||
* `Transform::bottom(self)`: Sets `y` to `Position::End`.
|
||||
* `Transform::right(self)`: Sets `x` to `Position::End`.
|
||||
* `Transform::margin(self, x: i32, y: i32)`: Sets `mx` and `my`.
|
||||
* `Transform::padding(self, x: u16, y: u16)`: Sets `px` and `py`.
|
||||
* `Transform::dimensions(self, width: u16, height: u16)`: Sets `width` and `height` to `Dimension::Const`.
|
||||
|
||||
## 2. `Position`: Horizontal and Vertical Alignment
|
||||
|
||||
`Position` defines how an element is placed along an axis relative to its parent's boundaries.
|
||||
|
||||
```rust
|
||||
pub enum Position {
|
||||
/// Fixed position in cells from the origin (top-left).
|
||||
Const(u16),
|
||||
/// Centered in the parent.
|
||||
Center,
|
||||
/// Aligned to the end (right for x, bottom for y) of the parent.
|
||||
End,
|
||||
}
|
||||
```
|
||||
|
||||
* `Position::Const(value)`: Sets an exact coordinate from the top-left (0,0). You can also use `u16` directly, thanks to `impl From<u16> for Position`.
|
||||
```rust
|
||||
@Transform { x: 5, y: 10 }; // Same as x: Position::Const(5), y: Position::Const(10)
|
||||
```
|
||||
* `Position::Center`: Centers the element within the available space of its parent on that axis.
|
||||
```rust
|
||||
@Transform { x: Position::Center, y: Position::Center };
|
||||
```
|
||||
* `Position::End`: Aligns the element to the right (for `x`) or bottom (for `y`) edge of its parent.
|
||||
```rust
|
||||
@Transform { x: Position::End, y: Position::Const(0) }; // Top-right aligned
|
||||
```
|
||||
|
||||
## 3. `Dimension`: Sizing Rules
|
||||
|
||||
`Dimension` defines how an element's width or height is determined.
|
||||
|
||||
```rust
|
||||
pub enum Dimension {
|
||||
/// Fills the available space from the parent.
|
||||
Full,
|
||||
/// Automatically sized to fit content.
|
||||
Content,
|
||||
/// Fixed size in cells.
|
||||
Const(u16),
|
||||
}
|
||||
```
|
||||
|
||||
* `Dimension::Full`: The element will take up 100% of the available space on that axis within its parent.
|
||||
* `Dimension::Content`: The element's size will be determined by its content. For container elements (like `Div`, `FlexRow`, `FlexCol`), this means their size will expand to encompass their children. For text elements, it will be the size of the text. This is the default.
|
||||
* `Dimension::Const(value)`: The element will have a fixed size in cells. You can also use `u16` directly, thanks to `impl From<u16> for Dimension`.
|
||||
```rust
|
||||
@Transform { width: 20, height: 5 }; // Same as width: Dimension::Const(20), height: Dimension::Const(5)
|
||||
```
|
||||
|
||||
## 4. `Style`: Visual Appearance
|
||||
|
||||
The `Style` component defines the background and foreground colors of a widget.
|
||||
|
||||
```rust
|
||||
pub struct Style {
|
||||
pub background: Background,
|
||||
pub foreground: Option<u32>,
|
||||
}
|
||||
```
|
||||
|
||||
* `background: Background`: Specifies the widget's background appearance.
|
||||
* `foreground: Option<u32>`: Sets the text color. `None` means the default terminal foreground color.
|
||||
|
||||
You attach `Style` using the `@` component syntax:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
@Style { background: Background::Solid(0x222222), foreground: Some(0xFFFFFF) };
|
||||
Div {
|
||||
"This text is white on a dark gray background."
|
||||
}
|
||||
|
||||
@Style { background: Background::Outline(0x00FF00), foreground: Some(0x00FF00) };
|
||||
Div {
|
||||
"This div has a green outline and green text."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `Background`: Background Appearance Options
|
||||
|
||||
`Background` defines various ways a widget's background can be drawn. Colors are specified as `u32` hex values (e.g., `0xFF0000` for red).
|
||||
|
||||
```rust
|
||||
pub enum Background {
|
||||
/// Transparent / no background.
|
||||
NoBackground,
|
||||
/// Draws a basic outline using the given color.
|
||||
Outline(u32),
|
||||
/// Draws a rounded outline using the given color.
|
||||
RoundedOutline(u32),
|
||||
/// Fills the background with the specified color.
|
||||
Solid(u32),
|
||||
}
|
||||
```
|
||||
|
||||
## The `transform!` Macro
|
||||
|
||||
For even more concise `Transform` definitions, you can use the `transform!` macro:
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
|
||||
rsx! {
|
||||
@transform!{ x: 10, y: Center, width: Full, padding: (1, 1) };
|
||||
Div {
|
||||
"This div is at x=10, centered vertically, full width, with 1 unit of padding."
|
||||
}
|
||||
}
|
||||
```
|
||||
This macro automatically converts `u16` values to `Position::Const` or `Dimension::Const` where appropriate.
|
||||
|
||||
## How Layout and Styling are Applied
|
||||
|
||||
When the `Screen` renders a widget, it performs the following steps (simplified):
|
||||
|
||||
1. **Retrieve Components**: It fetches the `Transform` and `Style` components attached to the widget.
|
||||
2. **Resolve Transform**: The `Transform`'s `use_dimensions` and `use_position` methods are called. These methods take the *parent's* resolved size (from the `RenderScope`) and convert the declarative `Position` and `Dimension` rules into concrete `u16` values (absolute `x`, `y`, `width`, `height`) within a `RawTransform` structure.
|
||||
3. **Apply Style**: The `Style` component is passed to the `RenderScope`.
|
||||
4. **Element Rendering**: The widget's `Element::render` method is called. This method uses the now-resolved `RawTransform` and `Style` from the `RenderScope` to queue its drawing instructions (text, rectangles). It might also update `RenderScope`'s size based on content.
|
||||
5. **Parent Rendering (`after_render`)**: For container elements (like `Div`, `FlexRow`, `FlexCol`), their `Element::after_render` method then takes over. They iterate through their children, set the `RenderScope`'s "parent size" to *their own* resolved size, and recursively trigger the rendering process for each child.
|
||||
6. **Drawing to Terminal**: Finally, `RenderScope::draw()` is called, which takes all accumulated drawing instructions and applies them to the terminal using `crossterm` and ANSI escape codes. Backgrounds and outlines are drawn first, then text and other content.
|
||||
|
||||
This multi-stage process ensures that layout rules are correctly interpreted from parent to child, allowing for adaptive and well-positioned UI elements.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# Using Extensions
|
||||
|
||||
OSUI features an extensible architecture that allows you to add global behaviors, custom rendering logic, or integrate third-party functionalities by implementing the `Extension` trait. This guide explains what extensions are, how to implement them, and how to register them with your `Screen`.
|
||||
|
||||
## What are Extensions?
|
||||
|
||||
Extensions are separate modules or structs that provide lifecycle hooks for the `Screen` and can interact with widgets at a global level. They are ideal for:
|
||||
|
||||
* **Global Event Handling**: Such as processing keyboard or mouse input across all widgets (e.g., `InputExtension`).
|
||||
* **Periodic Tasks**: Running logic on a fixed interval (e.g., `TickExtension`, `VelocityExtension`).
|
||||
* **Custom Rendering Overrides**: Injecting logic before or after a widget's rendering.
|
||||
* **Managing Global State**: Maintaining state accessible to multiple widgets or other extensions.
|
||||
* **Debugging or Logging**: Observing the UI tree or rendering process.
|
||||
|
||||
## The `Extension` Trait
|
||||
|
||||
The `Extension` trait defines the interface for all extensions:
|
||||
|
||||
```rust
|
||||
pub trait Extension {
|
||||
/// Called once when the screen starts running.
|
||||
fn init(&mut self, _screen: Arc<Screen>) {}
|
||||
|
||||
/// Called when the screen is being closed.
|
||||
fn on_close(&mut self, _screen: Arc<Screen>) {}
|
||||
|
||||
/// Called for each widget before its `render` method is invoked.
|
||||
fn render_widget(&mut self, _scope: &mut RenderScope, _widget: &Arc<Widget>) {}
|
||||
}
|
||||
```
|
||||
|
||||
All methods have default empty implementations, meaning you only need to implement the hooks relevant to your extension's functionality.
|
||||
|
||||
## Implementing a Custom Extension
|
||||
|
||||
Let's create a simple extension that logs when the screen initializes and when a widget is rendered.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct MyLoggerExtension;
|
||||
|
||||
impl Extension for MyLoggerExtension {
|
||||
fn init(&mut self, screen: Arc<Screen>) {
|
||||
println!("MyLoggerExtension: Screen initialized!");
|
||||
// You could store the screen Arc if needed for later interaction
|
||||
// self.screen = Some(screen);
|
||||
}
|
||||
|
||||
fn on_close(&mut self, screen: Arc<Screen>) {
|
||||
println!("MyLoggerExtension: Screen closing!");
|
||||
}
|
||||
|
||||
fn render_widget(&mut self, scope: &mut RenderScope, widget: &Arc<Widget>) {
|
||||
// This hook is called for *every* widget about to be rendered.
|
||||
// It's useful for debugging or applying global styles/transforms.
|
||||
// Note: The widget here is the Arc<Widget>, not its inner Element.
|
||||
// You can use `widget.get_elem()` to access the Element.
|
||||
|
||||
// For simplicity, we'll just log the widget's address and a component if it has one.
|
||||
if let Some(t) = widget.get::<Transform>() {
|
||||
println!(
|
||||
"MyLoggerExtension: Rendering widget at {:p} with transform: x={:?} y={:?}",
|
||||
Arc::as_ptr(widget), t.x, t.y
|
||||
);
|
||||
} else {
|
||||
println!("MyLoggerExtension: Rendering widget at {:p}", Arc::as_ptr(widget));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Registering an Extension
|
||||
|
||||
To activate your extension, you must register it with the `Screen` instance using the `screen.extension()` method. This is typically done at the beginning of your `main` function.
|
||||
|
||||
```rust
|
||||
use osui::prelude::*;
|
||||
use std::sync::Arc; // Needed for Arc<Screen> in the main function
|
||||
|
||||
// ... (MyLoggerExtension definition from above) ...
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let screen = Screen::new();
|
||||
|
||||
// Register your custom extension
|
||||
screen.extension(MyLoggerExtension);
|
||||
|
||||
// Register other built-in extensions if needed
|
||||
screen.extension(InputExtension);
|
||||
screen.extension(TickExtension(10)); // Example: Tick every 10ms
|
||||
|
||||
rsx! {
|
||||
Div { "Hello, OSUI!" }
|
||||
}.draw(&screen);
|
||||
|
||||
screen.run()
|
||||
}
|
||||
```
|
||||
|
||||
Once registered, the `Screen` will call the appropriate lifecycle methods of your extension at the right times during its operation.
|
||||
|
||||
## Built-in Extensions
|
||||
|
||||
OSUI comes with several useful built-in extensions:
|
||||
|
||||
* [`InputExtension`](../reference/extensions_api.md#inputextension): Handles keyboard input and dispatches `crossterm::event::Event`s. **Crucial for interactive applications.**
|
||||
* [`TickExtension`](../reference/extensions_api.md#tickextension): Dispatches `TickEvent`s at a specified rate, useful for animations or periodic updates.
|
||||
* [`VelocityExtension`](../reference/extensions_api.md#velocityextension): Automatically updates the `Transform` of widgets that have a `Velocity` component, causing them to move.
|
||||
* [`IdExtension`](../reference/extensions_api.md#idextension): Provides a way to retrieve specific widgets by a unique `Id` component. (Note: The current `IdExtension` implementation only *stores* a screen reference but doesn't actively do anything unless you manually call its `get_element` method.)
|
||||
|
||||
You use these built-in extensions by simply calling `screen.extension(...)` with an instance of them, just like `MyLoggerExtension`.
|
||||
|
||||
## Interaction between Extensions and Widgets
|
||||
|
||||
Extensions can interact with widgets in various ways:
|
||||
|
||||
* **Reading Components**: Within `render_widget` or other hooks, an extension can use `widget.get::<C>()` to read components (like `Transform` or `Style`) attached to a widget, influencing how it renders or behaves.
|
||||
* **Setting Components**: An extension can use `widget.set_component(c)` to dynamically add or modify components on a widget. For example, `VelocityExtension` modifies `Transform` components.
|
||||
* **Dispatching Events**: Extensions can dispatch custom events to widgets using `widget.event(&my_custom_event)`.
|
||||
* **Modifying RenderScope**: In `render_widget`, an extension can directly modify the `RenderScope` (e.g., apply a global offset or filter) before the widget's `render` method is called.
|
||||
|
||||
By leveraging extensions, you can keep your core UI logic clean and declarative, while offloading cross-cutting concerns or global features into reusable and modular units.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user