Custom Widgets
Escape hatch for user-defined character-cell rendering via the Widget trait.
use telex::widget::Widget;
use telex::buffer::{Buffer, Rect};
struct Sparkline {
data: Vec<u8>,
}
impl Widget for Sparkline {
fn render(&self, area: Rect, buf: &mut Buffer) {
let bars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
for (i, &val) in self.data.iter().enumerate() {
if i as u16 >= area.width { break; }
let idx = (val as usize * 7) / 100;
buf.set(area.x + i as u16, area.y, bars[idx],
crossterm::style::Color::Cyan, crossterm::style::Color::Reset);
}
}
}
View::custom(Rc::new(RefCell::new(sparkline)))
Run with: cargo run -p telex-tui --example 38_custom_widget
The Widget trait
pub trait Widget {
/// Draw into the buffer within the given area
fn render(&self, area: Rect, buf: &mut Buffer);
/// Whether this widget can receive keyboard focus (default: false)
fn focusable(&self) -> bool { false }
/// Preferred height given available width (default: None)
fn height_hint(&self, _width: u16) -> Option<u16> { None }
/// Preferred width (default: None)
fn width_hint(&self) -> Option<u16> { None }
}
render
The only required method. Called each frame with:
area— ARect { x, y, width, height }defining the available spacebuf— ABufferto write characters into
Use buf.set(x, y, char, fg, bg) to place characters. Stay within the area bounds.
focusable
Return true if the widget should participate in tab navigation.
height_hint / width_hint
Return Some(n) to suggest a preferred size. The layout engine uses these as hints but may allocate different dimensions.
Embedding in your UI
Wrap your widget in Rc<RefCell<_>> and pass to View::custom():
use std::rc::Rc;
use std::cell::RefCell;
let widget = Sparkline { data: vec![10, 50, 80, 30, 60] };
View::custom(Rc::new(RefCell::new(widget)))
Custom widgets compose with all other views — put them in stacks, boxes, tabs, or anywhere a View is accepted.
Example: spectrum visualizer
struct Spectrum {
levels: Vec<f64>,
}
impl Widget for Spectrum {
fn render(&self, area: Rect, buf: &mut Buffer) {
for (i, &level) in self.levels.iter().enumerate() {
if i as u16 >= area.width { break; }
let height = (level * area.height as f64) as u16;
for row in 0..area.height {
let y = area.y + area.height - 1 - row;
let ch = if row < height { '█' } else { ' ' };
buf.set(area.x + i as u16, y, ch,
crossterm::style::Color::Green,
crossterm::style::Color::Reset);
}
}
}
fn height_hint(&self, _width: u16) -> Option<u16> {
Some(10)
}
fn width_hint(&self) -> Option<u16> {
Some(self.levels.len() as u16)
}
}
Tips
Stay in bounds — Only write to cells within area.x..area.x+area.width and area.y..area.y+area.height. Writing outside may corrupt other widgets.
RefCell for mutation — Since widgets are behind Rc<RefCell<_>>, you can mutate widget state between renders.
Combine with state — Create the widget from state values each render, or store the widget in state and update it.
Performance — The widget's render is called every frame. Keep it fast — no I/O, no allocations if possible.