# 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](/docs/0.1.1/concepts/widget-model), 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); // 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>, // 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::() { 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) { // 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.