Updated to 0.2.0

This commit is contained in:
2026-01-31 20:52:02 +01:00
parent a30c1ac52a
commit 0b7a72dc5d
56 changed files with 7365 additions and 0 deletions
@@ -0,0 +1,261 @@
markdown
---
sidebar_position: 1
title: Customizing the Engine
---
# Customizing the Engine
OSUI's `Engine` and `CommandExecutor` traits are designed to be highly extensible. While the `Console` engine provides `crossterm`-based terminal rendering, you might want to create a custom engine for various reasons:
* **Different Rendering Backend**: Render to a graphical window (e.g., using `minifb` or `pixels`), a web canvas, or a specific hardware display.
* **Headless Testing**: Create a dummy engine that doesn't render anything but processes all commands and component logic, useful for fast unit or integration tests.
* **Logging/Debugging**: An engine that logs all `DrawInstruction`s to a file for analysis.
* **Specialized Behavior**: Implement custom render loops, input handling, or command processing.
This guide will walk you through the process of implementing your own `Engine` and `CommandExecutor`.
## Implementing `CommandExecutor`
First, let's define a custom `CommandExecutor`. This trait is responsible for processing commands issued by components (e.g., `cx.stop()`).
```rust
use osui::prelude::*;
use std::{
any::Any,
sync::{Arc, Mutex},
};
// Define a custom command
#[derive(Debug, Clone)]
pub struct CustomCommand(pub String);
impl Command for CustomCommand {
fn as_any(&self) -> &dyn Any {
self
}
}
pub struct MyCustomExecutor {
running: Mutex<bool>,
received_commands: Mutex<Vec<CustomCommand>>,
}
impl MyCustomExecutor {
pub fn new() -> Arc<Self> {
Arc::new(Self {
running: Mutex::new(true),
received_commands: Mutex::new(Vec::new()),
})
}
pub fn stop_engine(&self) -> crate::Result<()> {
*self.running.lock()? = false;
Ok(())
}
pub fn get_status(&self) -> bool {
*self.running.lock().unwrap()
}
pub fn get_received_commands(&self) -> Vec<CustomCommand> {
self.received_commands.lock().unwrap().clone()
}
}
impl CommandExecutor for MyCustomExecutor {
fn execute_command(&self, command: &Arc<dyn Command>) -> crate::Result<()> {
let command_any = command.as_any();
// Handle built-in Stop command
if let Some(commands::Stop) = command_any.downcast_ref::<commands::Stop>() {
println!("MyCustomExecutor: Received Stop command.");
return self.stop_engine();
}
// Handle our custom command
if let Some(custom_cmd) = command_any.downcast_ref::<CustomCommand>() {
println!("MyCustomExecutor: Received CustomCommand: {:?}", custom_cmd);
self.received_commands.lock().unwrap().push(custom_cmd.clone());
return Ok(());
}
println!("MyCustomExecutor: Unhandled command.");
Ok(())
}
}
```
## Implementing `Engine`
Now, let's create a simple "headless" engine that doesn't draw to the terminal but just logs rendering events.
```rust
use osui::prelude::*;
use std::sync::Arc;
pub struct MyHeadlessEngine {
executor: Arc<MyCustomExecutor>,
log_output: Mutex<Vec<String>>,
}
impl MyHeadlessEngine {
pub fn new() -> Self {
Self {
executor: MyCustomExecutor::new(),
log_output: Mutex::new(Vec::new()),
}
}
// Helper to log messages
fn log(&self, msg: &str) {
self.log_output.lock().unwrap().push(msg.to_string());
}
pub fn get_log(&self) -> Vec<String> {
self.log_output.lock().unwrap().clone()
}
}
impl Engine for MyHeadlessEngine {
fn run<C: ComponentImpl + 'static>(&self, component: C) -> crate::Result<()> {
self.log("Engine: Initializing component...");
let cx = self.init(component);
while self.executor.get_status() {
self.log("Engine: Starting render cycle...");
self.render(&cx);
self.log("Engine: Render cycle complete. Delaying...");
self.render_delay(); // Use default delay or implement custom
}
self.log("Engine: Application stopped.");
Ok(())
}
fn init<C: ComponentImpl + 'static>(&self, component: C) -> Arc<Context> {
// Perform any setup needed for your custom engine
self.log("Engine: Component initialized.");
let cx = Context::new(component, self.executor.clone());
cx.refresh(); // Initial render of the component
cx
}
fn render(&self, cx: &Arc<Context>) {
let area = Area { x: 0, y: 0, width: 80, height: 24 }; // Define a virtual screen size
let draw_ctx = self.render_view(&area, &cx.get_view());
self.draw_context(&draw_ctx);
}
// No actual delay for headless, or keep default for testing loop speed
fn render_delay(&self) {
// crate::sleep(16); // Uncomment for actual delay
}
fn render_view(&self, area: &Area, view: &View) -> DrawContext {
self.log(&format!("Engine: Rendering view in area: {:?}", area));
let mut context = DrawContext::new(area.clone());
view(&mut context); // Execute the View closure to populate DrawContext
context
}
fn draw_context(&self, ctx: &DrawContext) {
self.log(&format!("Engine: Drawing context with {} instructions.", ctx.drawing.len()));
for inst in &ctx.drawing {
match inst {
DrawInstruction::Text(point, text) => self.log(&format!(" Draw Text at {:?}: '{}'", point, text)),
DrawInstruction::View(area, view) => {
self.log(&format!(" Draw Child View in area: {:?}", area));
self.draw_context(&self.render_view(area, view)); // Recursively render child views
},
DrawInstruction::Child(point, child_ctx) => {
self.log(&format!(" Draw Child DrawContext at {:?}.", point));
self.draw_context(child_ctx); // Recursively draw child contexts
},
}
}
}
fn executor(&self) -> Arc<dyn CommandExecutor> {
self.executor.clone()
}
}
```
## Using Your Custom Engine
```rust
use osui::prelude::*;
use std::sync::Arc;
// (Include MyHeadlessEngine, MyCustomExecutor, CustomCommand definitions here)
#[component]
fn MyApp(cx: &Arc<Context>) -> View {
let counter = use_state(0);
use_effect(
{
let cx = cx.clone();
let counter = counter.clone();
move || {
// Periodically increment counter and emit custom command
loop {
sleep(200);
let new_val = *counter.get() + 1;
counter.set(new_val);
if new_val >= 3 {
cx.execute(CustomCommand(format!("Counter reached {}", new_val))).expect("Cmd failed");
cx.stop().expect("Stop failed");
break;
}
}
}
},
&[], // Run once on mount
);
rsx! {
format!("Counter value: {}", counter.get_dl())
}.view(&cx)
}
fn main() {
let my_engine = MyHeadlessEngine::new();
let executor = my_engine.executor.clone(); // Get a reference to the executor
my_engine.run(MyApp {}).expect("Failed to run custom engine");
println!("\n--- Engine Log ---");
for line in my_engine.get_log() {
println!("{}", line);
}
println!("\n--- Received Commands ---");
for cmd in executor.get_received_commands() {
println!("{:?}", cmd);
}
}
```
### Explanation:
1. **`MyCustomExecutor`**: Implements `CommandExecutor`. It handles the built-in `commands::Stop` and our new `CustomCommand`. It also keeps a log of received custom commands for verification.
2. **`MyHeadlessEngine`**: Implements `Engine`.
* It takes `MyCustomExecutor` as its command executor.
* `run` method establishes a basic loop that continues as long as `executor.get_status()` is `true`.
* `render` orchestrates the `render_view` and `draw_context` calls.
* `render_view` executes the component `View` and collects `DrawInstruction`s.
* `draw_context` iterates through `DrawInstruction`s, logging them instead of actually drawing to a terminal. It recursively handles `DrawInstruction::View` and `DrawInstruction::Child`.
3. **`MyApp` Component**:
* Uses `use_state` for a counter.
* Uses `use_effect` to periodically increment the counter.
* When the counter reaches 3, it `execute`s our `CustomCommand` and then `stop()`s the engine.
4. **`main` Function**:
* Instantiates `MyHeadlessEngine`.
* Calls `my_engine.run(MyApp {})`.
* After the engine stops, it prints the internal log and received commands from the executor, allowing you to verify that component logic and commands were processed correctly.
By following this pattern, you can integrate OSUI's powerful component and state management system with virtually any rendering or execution environment you desire.
**Next:** Dive into the internals of OSUI's macro system in [Internals: Macros](./02-internals-macros.md).