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
minifborpixels), 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
DrawInstructions 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()).
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.
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
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:
MyCustomExecutor: ImplementsCommandExecutor. It handles the built-incommands::Stopand our newCustomCommand. It also keeps a log of received custom commands for verification.MyHeadlessEngine: ImplementsEngine.- It takes
MyCustomExecutoras its command executor. runmethod establishes a basic loop that continues as long asexecutor.get_status()istrue.renderorchestrates therender_viewanddraw_contextcalls.render_viewexecutes the componentViewand collectsDrawInstructions.draw_contextiterates throughDrawInstructions, logging them instead of actually drawing to a terminal. It recursively handlesDrawInstruction::ViewandDrawInstruction::Child.
- It takes
MyAppComponent:- Uses
use_statefor a counter. - Uses
use_effectto periodically increment the counter. - When the counter reaches 3, it
executes ourCustomCommandand thenstop()s the engine.
- Uses
mainFunction:- 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.
- Instantiates
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.