216 lines
8.1 KiB
Markdown
216 lines
8.1 KiB
Markdown
# Creating Custom Widgets
|
|
|
|
While OSUI provides a rich set of built-in elements, you'll often need to create your own custom widgets to encapsulate specific UI logic or appearance. This guide walks you through implementing the `Element` trait and integrating your custom widget into the OSUI ecosystem.
|
|
|
|
## The `Element` Trait Revisited
|
|
|
|
As discussed in [Concepts: Widget Model](../concepts/widget-model.md), the `Element` trait is the contract for anything that can be rendered.
|
|
|
|
```rust
|
|
pub trait Element: Send + Sync {
|
|
// Draw commands for the element itself
|
|
fn render(&mut self, scope: &mut RenderScope, render_context: &RenderContext);
|
|
|
|
// Logic after self-rendering, typically for rendering children
|
|
fn after_render(&mut self, scope: &mut RenderScope, render_context: &RenderContext);
|
|
|
|
// Used by rsx! to register children
|
|
fn draw_child(&mut self, element: &Arc<Widget>);
|
|
|
|
// Handle incoming events
|
|
fn event(&mut self, event: &dyn Event);
|
|
|
|
// Is this element purely a logical/layout container (e.g., Div, FlexRow)?
|
|
fn is_ghost(&mut self) -> bool;
|
|
|
|
// Required for downcasting, usually implemented boilerplate
|
|
fn as_any(&self) -> &dyn Any;
|
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
|
}
|
|
```
|
|
|
|
## Example: A Simple `ClickableBox`
|
|
|
|
Let's create a box that displays a message and changes its background color when clicked.
|
|
|
|
### 1. Define the Element Struct
|
|
|
|
Our `ClickableBox` will hold its message, current color, and a list of children.
|
|
|
|
```rust
|
|
// src/elements/clickable_box.rs (or anywhere you organize your custom elements)
|
|
|
|
use std::sync::Arc;
|
|
use crossterm::event::{Event as CrosstermEvent, MouseEvent, MouseEventKind};
|
|
use crate::{prelude::*, NoRenderRoot}; // Import necessary OSUI items
|
|
|
|
pub struct ClickableBox {
|
|
message: String,
|
|
current_color: u32,
|
|
default_color: u32,
|
|
clicked_color: u32,
|
|
children: Vec<Arc<Widget>>, // To hold potential nested elements
|
|
// We'll track the size it renders to, important for parents
|
|
size: (u16, u16),
|
|
}
|
|
|
|
impl ClickableBox {
|
|
pub fn new(message: &str) -> Self {
|
|
Self {
|
|
message: message.to_string(),
|
|
current_color: 0x0055AA, // Default blue
|
|
default_color: 0x0055AA,
|
|
clicked_color: 0xAA5500, // Orange when clicked
|
|
children: Vec::new(),
|
|
size: (0, 0),
|
|
}
|
|
}
|
|
|
|
// A method to change color, useful for event handlers
|
|
pub fn set_clicked(&mut self, clicked: bool) {
|
|
self.current_color = if clicked { self.clicked_color } else { self.default_color };
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Implement the `Element` Trait
|
|
|
|
Now, let's implement the core `Element` trait methods.
|
|
|
|
```rust
|
|
// Continue in src/elements/clickable_box.rs
|
|
|
|
impl Element for ClickableBox {
|
|
fn render(
|
|
&mut self,
|
|
scope: &mut RenderScope,
|
|
render_context: &RenderContext,
|
|
) {
|
|
// Draw the background rectangle based on current_color
|
|
scope.draw_rect(0, 0, scope.get_transform().width, scope.get_transform().height, self.current_color);
|
|
|
|
// Draw the message text, centered
|
|
let (msg_w, msg_h) = utils::str_size(&self.message);
|
|
let text_x = (scope.get_transform().width.saturating_sub(msg_w)) / 2;
|
|
let text_y = (scope.get_transform().height.saturating_sub(msg_h)) / 2;
|
|
scope.draw_text_colored(text_x, text_y, &self.message, 0xFFFFFF); // White text
|
|
|
|
// Use the area based on the message size if dimensions are `Content`
|
|
scope.use_area(msg_w, msg_h);
|
|
|
|
// Update the size for the after_render pass and parent's layout
|
|
self.size = (scope.get_transform().width, scope.get_transform().height);
|
|
}
|
|
|
|
fn after_render(
|
|
&mut self,
|
|
scope: &mut RenderScope,
|
|
render_context: &RenderContext,
|
|
) {
|
|
// This element is a container, so we need to render its children.
|
|
// We'll pass them a new RenderScope nested within this box's area.
|
|
let mut transform = scope.get_transform().clone();
|
|
let mut child_renderer = DivRenderer(&mut transform); // Use DivRenderer helper for children
|
|
|
|
let (parent_w, parent_h) = scope.get_parent_size(); // Store parent size
|
|
scope.set_parent_size(self.size.0, self.size.1); // Set current element's size as parent for children
|
|
|
|
for widget in &self.children {
|
|
// Render each child widget using the context and the specialized renderer
|
|
scope.render_widget(&mut child_renderer, render_context.get_context(), widget);
|
|
}
|
|
|
|
// Restore parent size for subsequent sibling elements
|
|
scope.set_parent_size(parent_w, parent_h);
|
|
// The `DivRenderer` updates `transform.width/height` to encompass children.
|
|
// We need to propagate this up to our element's own calculated size.
|
|
self.size = (transform.width, transform.height);
|
|
}
|
|
|
|
fn event(&mut self, event: &dyn Event) {
|
|
// Listen for Crossterm Mouse Events
|
|
if let Some(crossterm_event) = event.get::<CrosstermEvent>() {
|
|
if let CrosstermEvent::Mouse(MouseEvent { kind: MouseEventKind::Down(_), column, row, .. }) = crossterm_event {
|
|
// Check if the click occurred within our widget's bounds
|
|
// This would require knowing the widget's absolute position on screen.
|
|
// For simplicity here, we'll just toggle color on any click to demonstrate.
|
|
// In a real app, you'd get the widget's RawTransform from an extension
|
|
// or a global layout manager to check bounds.
|
|
self.set_clicked(!self.current_color == self.clicked_color);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn draw_child(&mut self, element: &Arc<Widget>) {
|
|
// When a child is added (e.g., via rsx! nesting), store it.
|
|
// Inject NoRenderRoot so OSUI's main loop doesn't try to render it directly.
|
|
element.inject(|w| w.component(NoRenderRoot));
|
|
self.children.push(element.clone());
|
|
}
|
|
|
|
fn is_ghost(&mut self) -> bool {
|
|
// This element draws its own background, so it's not a ghost.
|
|
false
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
|
self
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Integrate with `mod.rs` (Optional but Recommended)
|
|
|
|
Add your new element to `src/elements/mod.rs` so it's easily accessible via `osui::prelude::*`.
|
|
|
|
```rust
|
|
// src/elements/mod.rs (partial)
|
|
pub mod div;
|
|
// ... other elements
|
|
pub mod clickable_box; // Your new module!
|
|
|
|
pub use div::*;
|
|
// ... other elements
|
|
pub use clickable_box::*; // Export it for prelude
|
|
// ... String impl (already there)
|
|
```
|
|
|
|
### 4. Use Your Custom Widget in `rsx!`
|
|
|
|
Now you can use `ClickableBox` just like any other built-in element:
|
|
|
|
```rust
|
|
// src/main.rs (or your demo app)
|
|
use osui::prelude::*;
|
|
use osui::elements::ClickableBox; // Explicitly import if not using prelude
|
|
|
|
fn main() -> std::io::Result<()> {
|
|
let screen = Screen::new();
|
|
screen.extension(InputExtension); // For mouse events
|
|
screen.extension(RelativeFocusExtension::new()); // Optional, but good practice
|
|
// You'd also need a MouseExtension if you want raw mouse events on widgets.
|
|
// For this basic example, `crossterm::event::Event` captures all.
|
|
|
|
rsx! {
|
|
// A ClickableBox with fixed dimensions and some nested text
|
|
@Transform::new().dimensions(30, 5).center();
|
|
ClickableBox::new("Click Me!") {
|
|
// Nested content will be drawn by ClickableBox's after_render
|
|
@Transform::new().y(3); // Position child text within the box
|
|
"Nested content inside the box"
|
|
}
|
|
}
|
|
.draw(&screen);
|
|
|
|
screen.run()?;
|
|
Ok(())
|
|
}
|
|
```
|
|
**Note on mouse events**: For a true click-detection, you'd need to compare `MouseEvent.column` and `MouseEvent.row` against the widget's actual rendered `RawTransform` coordinates. This typically involves an extension collecting widget positions or a global event dispatcher that routes events to widgets based on their bounds. The `Input` element handles focus and key events because it's built to capture those when focused.
|
|
|
|
By following this pattern, you can extend OSUI with a wide variety of custom UI components tailored to your application's specific needs.
|