v0.1.0 with spark

This commit is contained in:
2025-08-03 13:05:42 -05:00
parent 4bf347d26c
commit d35cc6408d
28 changed files with 4057 additions and 0 deletions
@@ -0,0 +1,64 @@
# Contributing to OSUI
We welcome contributions to OSUI! Whether it's reporting bugs, suggesting features, improving documentation, or submitting code, your help is valuable. This guide outlines the process for contributing.
## How to Contribute
1. **Report Bugs**: If you find a bug, please open an issue on the [GitHub repository](https://github.com/osui-rs/osui/issues). Provide a clear description of the bug, steps to reproduce it, and your environment (OS, Rust version, terminal emulator).
2. **Suggest Features**: Have an idea for a new feature or improvement? Open an issue on GitHub to discuss it.
3. **Improve Documentation**: Spotted a typo, unclear explanation, or missing example? Feel free to submit a pull request with your changes, or open an issue.
4. **Submit Code (Pull Requests)**: If you'd like to contribute code, follow the guidelines below.
## Code Contribution Guidelines
1. **Fork the Repository**: Start by forking the [OSUI GitHub repository](https://github.com/osui-rs/osui).
2. **Clone Your Fork**:
```bash
git clone https://github.com/your-username/osui.git
cd osui
```
3. **Create a New Branch**: Create a descriptive branch for your changes.
```bash
git checkout -b feature/my-new-feature
# or
git checkout -b bugfix/fix-some-bug
```
4. **Make Your Changes**:
* **Code Style**: Adhere to the existing Rust code style. Run `cargo fmt` before committing.
* **Clippy**: Ensure your code passes Clippy lints: `cargo clippy --all-targets --all-features -- -D warnings`.
* **Tests**: If you're adding new functionality, please include unit tests. If fixing a bug, consider adding a regression test.
* **Documentation**: Update relevant documentation (API reference, guides) for any new features or changes. Add comments to your code where necessary.
* **Examples/Demos**: If your feature adds significant new functionality, consider adding a small example to the `src/demos` directory.
5. **Commit Your Changes**: Write clear, concise commit messages.
```bash
git commit -m "feat: Add new awesome feature"
```
6. **Push to Your Fork**:
```bash
git push origin feature/my-new-feature
```
7. **Open a Pull Request**:
* Go to the [OSUI repository on GitHub](https://github.com/osui-rs/osui).
* You should see a prompt to open a pull request from your recently pushed branch.
* Provide a clear title and description for your pull request. Explain what problem it solves and how.
* Reference any related issues (e.g., `Fixes #123`, `Closes #456`).
8. **Review Process**:
* Project maintainers will review your pull request.
* Be prepared to address feedback and make further changes if requested.
* Once approved, your changes will be merged into the `master` branch.
## Development Environment Setup
* **Rust Toolchain**: Make sure you have a recent stable Rust toolchain installed.
```bash
rustup update
```
* **Dependencies**: Ensure all project dependencies are installed by running `cargo build`.
* **Testing**: Run tests with `cargo test`.
* **Linting**: Use `cargo clippy --all-targets --all-features -- -D warnings` to check for common code issues.
* **Formatting**: Use `cargo fmt` to automatically format your code according to Rust's standard style.
Thank you for considering contributing to OSUI! Your efforts help make this project better for everyone.
[**Return to Overview**](../intro/overview.md)
@@ -0,0 +1,213 @@
# Custom Events
Beyond `crossterm` events, OSUI's event system is designed to be extensible, allowing you to define and dispatch your own custom event types. This is essential for building complex applications where different parts of your UI or internal logic need to communicate.
## 1. Defining a Custom Event
You define custom events using the `event!` macro, which automatically implements the necessary `Event` trait for your struct.
### Example: A `UserLoginEvent`
```rust
// In your `events.rs` or `lib.rs` file
use osui::prelude::*; // Import prelude for `event!` macro
event!(UserLoginEvent {
username: String,
success: bool,
});
// A simple unit event
event!(UserLoggedOut);
```
These events automatically get `Debug` and `Clone` derives, and implement the `osui::extensions::Event` trait.
## 2. Dispatching a Custom Event
Events are dispatched to widgets using the `widget.event(&my_event)` method. The `Screen`'s main loop automatically dispatches `crossterm::event::Event`s to all top-level widgets. For custom events, you will typically dispatch them manually from:
* **Event Handlers**: A `Handler<crossterm::event::Event>` that captures input can then dispatch your custom event.
* **Background Threads**: A thread performing some work can dispatch an event to the UI when its work is done.
* **Custom Elements**: An element's internal logic might dispatch an event based on user interaction or state changes.
* **Extensions**: An `Extension` might dispatch events to trigger behavior across multiple widgets.
To dispatch an event, you need an `Arc<Widget>` reference.
### Example: Dispatching from an `Input` handler
Let's imagine you have an `Input` widget where, when the user presses Enter, you want to dispatch a `UserLoginEvent`.
```rust
use osui::prelude::*;
use std::sync::Arc;
use crossterm::event::{KeyCode, KeyEvent, Event as CrosstermEvent};
event!(UserLoginEvent {
username: String,
success: bool,
});
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension);
let username_input_state = use_state(String::new());
rsx! {
// This handler is attached to the root widget, listening for ALL input events.
// It will then dispatch a custom UserLoginEvent.
@Handler::new({
let screen = screen.clone(); // Clone screen to get all widgets
let username_input_state = username_input_state.clone(); // Clone state to read username
move |current_widget, event: &CrosstermEvent| {
if let CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, .. }) = event {
let entered_username = username_input_state.get_dl();
// Simulate login logic
let login_success = entered_username == "admin";
// Create and dispatch the custom event to ALL widgets on the screen
// This is inefficient; normally you'd dispatch to specific widgets or use state updates
let login_event = UserLoginEvent {
username: entered_username,
success: login_success,
};
for w in screen.widgets.lock().unwrap().iter() {
w.event(&login_event);
}
}
}
});
Div {
"Enter username (type 'admin' for success):"
@Transform::new().y(1).dimensions(20, 1);
@Style { background: Background::Outline(0x888888) };
Input, state: username_input_state, { }
}
}.draw(&screen);
screen.run()
}
```
## 3. Handling a Custom Event
Widgets (or other entities) can subscribe to your custom event types using the `Handler<E>` component.
### Example: A `LoginStatusDisplay` Widget
Now, let's create a widget that reacts to our `UserLoginEvent`:
```rust
use osui::prelude::*;
use std::sync::Arc;
// Make sure UserLoginEvent is defined and visible
event!(UserLoginEvent {
username: String,
success: bool,
});
pub struct LoginStatusDisplay {
status_text: State<String>,
}
impl LoginStatusDisplay {
pub fn new() -> Self {
Self {
status_text: use_state("Awaiting login...".to_string()),
}
}
}
impl Element for LoginStatusDisplay {
fn render(&mut self, scope: &mut RenderScope) {
scope.draw_text(0, 0, &self.status_text.get_dl());
}
// Implement as_any, as_any_mut, draw_child, after_render as needed for container logic
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
}
fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension);
let username_input_state = use_state(String::new());
let login_status_display = Arc::new(Widget::new_static(Box::new(LoginStatusDisplay::new())));
// Attach the handler for UserLoginEvent to the LoginStatusDisplay widget
login_status_display.component(Handler::new({
let status_state = login_status_display.get::<LoginStatusDisplay>().unwrap().status_text.clone();
move |_, event: &UserLoginEvent| {
if event.success {
status_state.set(format!("Welcome, {}!", event.username));
} else {
status_state.set(format!("Login failed for {}!", event.username));
}
}
}));
// Draw the LoginStatusDisplay and the Input field
screen.draw_widget(login_status_display.clone());
rsx! {
// This handler is now attached to a separate, root element.
@Handler::new({
let screen = screen.clone();
let username_input_state = username_input_state.clone();
move |_, event: &CrosstermEvent| {
if let CrosstermEvent::Key(KeyEvent { code: KeyCode::Enter, .. }) = event {
let entered_username = username_input_state.get_dl();
let login_success = entered_username == "admin";
let login_event = UserLoginEvent {
username: entered_username,
success: login_success,
};
// Dispatch to the specific login_status_display widget
// This is more efficient than iterating all screen widgets.
if let Some(w) = screen.widgets.lock().unwrap().iter().find(|w| {
// A more robust way to find the target widget, perhaps by an ID component
w.get_elem().as_any().is::<LoginStatusDisplay>()
}) {
w.event(&login_event);
}
}
}
});
@Transform::new().y(0); // Position the input above the status
Div {
"Enter username (type 'admin' for success):"
@Transform::new().y(1).dimensions(20, 1);
@Style { background: Background::Outline(0x888888) };
Input, state: username_input_state, { }
}
}.draw(&screen); // Add the input via rsx!
screen.run()
}
```
In this enhanced example:
1. The `Input` field (`rsx!`) has its own `Handler` for `crossterm::event::Event`.
2. When `Enter` is pressed in the `Input`'s handler, it constructs a `UserLoginEvent`.
3. Instead of iterating all widgets on the screen, it tries to find the `LoginStatusDisplay` widget (e.g., by checking its inner `Element` type, though using an `Id` component is more robust for production).
4. It dispatches the `UserLoginEvent` directly to that specific `login_status_display` widget.
5. The `Handler<UserLoginEvent>` attached to `login_status_display` then updates its internal `status_text` `State`.
6. Because `status_text` is a `State`, and `LoginStatusDisplay` is a `StaticWidget` whose `render` method reads `status_text`, the display updates automatically.
## Event Propagation (Important)
OSUI's current event system is primarily a **global dispatch model**.
* `InputExtension` (and `TickExtension`) dispatches events to *all* top-level widgets (`Screen.widgets.lock().unwrap().iter()`).
* `Widget::event()` then checks for `Handler` components and calls the `Element::event` method.
This means if you have multiple `Handler`s for the *same event type* on different widgets, they will all be called. For more complex scenarios, you might need to build your own event routing or bubbling system on top of this, or prefer using `State` updates for communication over direct event dispatch between deeply nested components.
However, for simple communication like the `UserLoginEvent` example, dispatching directly to the target widget (once found) is efficient.
Custom events are a powerful tool for decoupling concerns and enabling clear communication between different parts of your OSUI application, allowing you to build more complex and modular UIs.
@@ -0,0 +1,89 @@
# Performance Considerations
Building performant Terminal User Interfaces (TUIs) requires careful attention, as direct terminal manipulation can be slower than native graphical UIs. OSUI is designed with performance in mind, offering mechanisms to optimize rendering and reactivity.
## 1. `DynWidget` vs. `StaticWidget`
This is perhaps the most crucial performance decision in OSUI.
* **`StaticWidget`**:
* **Creation**: The `Element` instance is created only *once* when the widget is initially loaded.
* **Rendering**: During each `Screen::render()` cycle, the `Element::render` and `Element::after_render` methods are called directly on the existing `Element` instance. The `Element`'s internal state (if any) is mutated directly.
* **Overhead**: Minimal. No re-allocation or re-evaluation of closures per frame.
* **When to Use**: For any part of your UI that does not change its fundamental structure or the type of its root `Element` instance. This includes static text, fixed layouts, or elements whose internal state changes but doesn't require a full rebuild of the `Element` itself.
* **In `rsx!`**: Use the `static` keyword: `static Div { "Hello" }`.
* **`DynWidget`**:
* **Creation**: Holds a closure (`FnMut() -> WidgetLoad`) that *rebuilds* the `Element` and its initial components whenever `refresh()` is called.
* **Rendering**: During `Screen::render()`, if `auto_refresh()` determines that a dependency has changed, the widget's internal `Element` is entirely replaced by re-executing the creation closure. Then, the `render` methods are called on this *new* `Element` instance.
* **Overhead**: Higher. Involves re-allocations for the new `Element` and `HashMap` of components, plus the cost of re-evaluating the closure and potentially re-parsing text for elements like `format!()` strings.
* **When to Use**: For parts of your UI that *must* change their `Element` type, or whose content is deeply tied to reactive `State` that necessitates a full rebuild to reflect changes. Use it for dynamic text, lists that grow/shrink, or components that switch between different visual representations.
* **In `rsx!`**: Default behavior or using `%dependency`: `%my_state Div { "Count: {my_state}" }`.
**Recommendation**: Favor `static` widgets whenever possible. Break down your UI into the smallest possible `DynWidget`s to isolate reactive updates and minimize the scope of re-renders.
## 2. `State<T>` Usage and Granularity
OSUI's `State<T>` is efficient for simple values, but consider its implications for larger data structures.
* **Modification Cost**: When `**my_state.get() = ...` or `my_state.set(...)` is called, it marks `inner.changed = inner.dependencies`. This means *all* `DynWidget`s listening to that specific `State<T>` will be rebuilt on the next `auto_refresh` cycle.
* **Large `State` Objects**: If `T` in `State<T>` is a large struct or `Vec`, and you only modify a small part of it, the entire `DynWidget` (and its children) listening to it will still rebuild.
* **Optimization**:
* **Splitting State**: If a complex data structure has independent parts that change, consider splitting it into multiple `State` objects.
```rust
// Instead of:
struct UserProfile { name: String, email: String, settings: Settings }
let profile = use_state(UserProfile { /* ... */ });
// And updating `profile.get().name = ...` which rebuilds everything.
// Consider:
let user_name = use_state(String::new());
let user_email = use_state(String::new());
let user_settings = use_state(Settings::new());
// Then, only widgets depending on `user_name` re-render when `user_name` changes.
```
* **Smart `Element` Implementations**: For complex data, a custom `Element` can internally manage its own `State` and handle partial updates without requiring a full rebuild of the `Element` itself. For instance, an `Element` could hold a `State<Vec<Item>>` and only re-render the changed `Item`s internally, or update specific `Widget` children based on diffing logic, rather than relying on `DynWidget` to rebuild the entire `Element`.
## 3. Terminal I/O Overhead
Every character printed to the terminal, especially with color or cursor positioning, incurs overhead due to ANSI escape code processing and actual screen updates by the terminal emulator.
* **Full Screen Clear**: `utils::clear()` (used by `Screen::render`) clears the entire screen. This is a common TUI practice to avoid artifacts but is also a performance bottleneck for very high frame rates or remote connections. OSUI currently performs a full clear every frame.
* **Minimize Redraws**: OSUI's reactive system already helps minimize *what* is rebuilt, but the `RenderScope` then draws the *entire* content of each widget. Terminal emulators often optimize partial updates, but minimizing the total area of change is always beneficial.
* **Batching**: `RenderScope` implicitly batches drawing commands before `draw()` is called. Avoid manual, unbuffered `print!` calls in tight loops.
## 4. Expensive Operations in `Element::render` or `FnMut() -> WidgetLoad` Closures
* **Avoid Heavy Computation**: Do not perform computationally intensive tasks (e.g., complex data processing, network requests, large file I/O) directly within `Element::render` or the `FnMut() -> WidgetLoad` closure of a `DynWidget`. These are called frequently (every frame for `render`, or every dependency change for the closure).
* **Offload**: If such operations are necessary, offload them to separate `std::thread::spawn` threads or use asynchronous runtime if your application supports it. Update `State<T>` from these background threads, and your UI will react.
```rust
// BAD (expensive in render/build closure):
// rsx! { Div { format!("Result: {}", expensive_computation()) } }
// GOOD:
let computation_result = use_state("Calculating...".to_string());
std::thread::spawn({
let computation_result = computation_result.clone();
move || {
let result = expensive_computation(); // Runs in background
computation_result.set(format!("Result: {}", result)); // Updates state, triggers UI refresh
}
});
rsx! {
%computation_result
Div { "{computation_result}" }
}
```
## 5. `Mutex` Contention
OSUI uses `Mutex`es extensively (`Arc<Mutex<T>>`) for thread-safe access to widgets, elements, components, and state.
* **Minimize Lock Duration**: When you call `my_state.get()` or `widget.get_elem()`, you acquire a `MutexGuard`. Keep the duration for which you hold this lock as short as possible. Perform your read/write operation, then `drop` the guard or let it go out of scope quickly.
* **Avoid Nested Locks**: Do not acquire a `Mutex` lock and then, while holding it, try to acquire another `Mutex` that could be held by a different thread trying to acquire your first lock. This leads to deadlocks. `State::get_dl()` is useful for avoiding this, as it releases the lock immediately after cloning.
By being mindful of these performance considerations, you can ensure your OSUI applications are responsive and efficient, even when handling complex UIs or frequent updates.