
Ratatui
- 2 installs
- 4 repo stars
- Updated January 19, 2026
- zhanghandong/ratatui-skills
Write and troubleshoot Rust ratatui terminal UI code covering terminal init, layout, widgets, and styling for ratatui 0.30.
About
Provides expert guidance and code generation for the Rust ratatui TUI crate, organized into basics, layout, widgets, and styling modules. A developer uses it when building or debugging terminal user interfaces with ratatui.
- Modular reference for terminal init, layout, widgets, and styling
- Targets ratatui 0.30 with edition 2024 and crossterm backend defaults
Ratatui by the numbers
- 2 all-time installs (skills.sh)
- Ranked #101 of 121 Rust skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zhanghandong/ratatui-skills --skill ratatuiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 4 |
| Last updated | January 19, 2026 |
| Repository | zhanghandong/ratatui-skills ↗ |
What it does
Write and troubleshoot Rust ratatui terminal UI code covering terminal init, layout, widgets, and styling for ratatui 0.30.
Files
Ratatui TUI Library
Version: ratatui 0.30.0 | Last Updated: 2026-01-19
>
Check for updates: https://crates.io/crates/ratatui
You are an expert at the Rust ratatui crate. Help users by:
- Writing code: Generate Rust code following the patterns below
- Answering questions: Explain concepts, troubleshoot issues, reference documentation
Code Generation Rules
IMPORTANT: Before generating any Rust code, read `./references/_shared/rust-defaults.md` for shared rules.
Key rules:
- Use
edition = "2024"in Cargo.toml (NOT 2021) - Use latest ratatui version:
ratatui = "0.30" - Use crossterm backend by default (cross-platform)
Module Navigation
This skill is organized into focused sub-modules. For detailed information, refer to:
| Module | File | Topics |
|---|---|---|
| Basics | ./skills/basics/SKILL.md | Terminal init, app structure, event loop |
| Layout | ./skills/layout/SKILL.md | Constraint, Rect, Flex, split areas |
| Widgets | ./skills/widgets/SKILL.md | Block, List, Table, Gauge, custom widgets |
| Styling | ./skills/styling/SKILL.md | Color, Style, Modifier, Text/Span/Line |
Key Concepts
Ratatui uses immediate rendering with intermediate buffers:
- Each frame, render all widgets to a buffer
- Terminal compares current/previous buffers
- Only changed cells are written to terminal
Quick Reference
Simplest App
use crossterm::event;
fn main() -> std::io::Result<()> {
ratatui::run(|mut terminal| {
loop {
terminal.draw(|frame| {
frame.render_widget("Hello World!", frame.area());
})?;
if event::read()?.is_key_press() {
break Ok(());
}
}
})
}App with Layout
use ratatui::layout::{Constraint, Layout};
use ratatui::widgets::{Block, Paragraph};
fn render(frame: &mut Frame) {
let [header, body, footer] = Layout::vertical([
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(1),
]).areas(frame.area());
frame.render_widget(
Paragraph::new("Header").block(Block::bordered()),
header,
);
frame.render_widget(
Paragraph::new("Body content"),
body,
);
frame.render_widget(
Paragraph::new("Footer"),
footer,
);
}Styled Text
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};
let line = Line::from(vec![
"Normal ".into(),
"bold".bold(),
" and ".into(),
"red".red(),
]);List with Selection
use ratatui::widgets::{Block, List, ListItem, ListState};
use ratatui::style::Stylize;
let items: Vec<ListItem> = vec![
ListItem::new("Item 1"),
ListItem::new("Item 2"),
];
let list = List::new(items)
.block(Block::bordered().title("List"))
.highlight_style(Style::new().reversed())
.highlight_symbol("> ");
let mut state = ListState::default();
state.select(Some(0));
frame.render_stateful_widget(list, area, &mut state);API Reference Table
| Function/Type | Description | Example |
|---|---|---|
ratatui::run(f) | Run app with auto init/restore | `ratatui::run(\ |
ratatui::init() | Initialize terminal | let mut term = ratatui::init(); |
ratatui::restore() | Restore terminal state | ratatui::restore(); |
terminal.draw(f) | Draw a frame | `terminal.draw(\ |
Layout::vertical([...]) | Create vertical layout | Layout::vertical([Length(3), Fill(1)]) |
Layout::horizontal([...]) | Create horizontal layout | Layout::horizontal([Percentage(50); 2]) |
frame.render_widget(w, a) | Render widget | frame.render_widget(para, area); |
frame.render_stateful_widget(w, a, s) | Render with state | frame.render_stateful_widget(list, area, &mut state); |
Constraint Types
| Constraint | Description |
|---|---|
Length(n) | Exactly n cells |
Min(n) | At least n cells |
Max(n) | At most n cells |
Percentage(n) | n% of available |
Ratio(a, b) | a/b of available |
Fill(n) | Fill with weight n |
Built-in Widgets
| Widget | State Type | Description |
|---|---|---|
Block | - | Container with borders/title |
Paragraph | - | Text display with wrapping |
List | ListState | Selectable list items |
Table | TableState | Rows and columns |
Tabs | - | Tab bar |
Gauge | - | Progress bar |
Scrollbar | ScrollbarState | Scroll indicator |
Chart | - | Line/scatter charts |
BarChart | - | Bar charts |
Canvas | - | Custom drawing |
When Writing Code
1. Use ratatui::run() for simple apps - handles init/restore automatically 2. Use Layout::vertical/horizontal() with areas() for compile-time known layouts 3. Wrap content widgets with Block for borders and titles 4. Handle KeyEventKind::Press to avoid duplicate key events on Windows 5. Use crossterm backend by default (works on all platforms) 6. Implement Widget for &MyWidget for reusable custom widgets
When Answering Questions
1. Ratatui is immediate mode - rebuild UI every frame 2. Widgets are consumed when rendered (implement on &Widget for reuse) 3. Layout uses Cassowary constraint solver algorithm 4. Event handling is separate from ratatui - use crossterm/termion directly 5. Stateful widgets require external state management
Ratatui Skills
English | 中文
Rust ratatui TUI ライブラリのスキルコレクション
ディレクトリ構成
ratatui-skills/
├── SKILL.md # メインエントリ(概要 + クイックリファレンス)
├── skills/ # サブモジュールスキル
│ ├── basics/ # ターミナル初期化, アプリ構造, イベントループ
│ ├── layout/ # Constraint, Rect, Flex, エリア分割
│ ├── widgets/ # Block, List, Table, カスタムウィジェット
│ └── styling/ # 色, スタイル, 修飾子, Text/Span/Line
└── references/ # 詳細ドキュメント
├── _shared/ # 共有ルール (rust-defaults.md)
├── basics/ # アプリ構造, バックエンド
├── layout/ # 制約タイプ, Flex モード
├── widgets/ # 組み込みウィジェット, カスタムウィジェット
└── styling/ # 色, テキストスタイリング使用方法
Claude Code スキルとして使用
このディレクトリを Claude Code スキルディレクトリにコピーまたはシンボリックリンク:
# 方法 1: シンボリックリンク
ln -s /path/to/ratatui-skills ~/.claude/skills/ratatui
# 方法 2: コピー
cp -r /path/to/ratatui-skills ~/.claude/skills/ratatuiトリガーキーワード
以下のキーワードでスキルが有効化:
ratatui,TUI,terminal ui,ratatui::run,ratatui::initLayout,Constraint,Rect,Flex,horizontal,verticalBlock,Paragraph,List,Table,Gauge,ChartStyle,Color,Stylize,Span,Line,TextWidget,StatefulWidget,ListState,TableState
モジュール
| モジュール | 用途 | 主要 API |
|---|---|---|
| basics | ターミナル設定 | ratatui::run(), init(), restore(), DefaultTerminal |
| layout | 画面レイアウト | Layout, Constraint, Rect, Flex |
| widgets | UI コンポーネント | Block, List, Table, Gauge, Scrollbar |
| styling | 色とテキスト | Style, Color, Stylize, Span, Line, Text |
バージョン
- ratatui: 0.30.0
- Rust edition: 2024
- 最終更新: 2026-01-19
Ratatui Skills
English | 日本語
Rust ratatui TUI 库技能集合
目录结构
ratatui-skills/
├── SKILL.md # 主入口(概览 + 快速参考)
├── skills/ # 子模块技能
│ ├── basics/ # 终端初始化, 应用结构, 事件循环
│ ├── layout/ # 约束, Rect, Flex, 区域分割
│ ├── widgets/ # Block, List, Table, 自定义组件
│ └── styling/ # 颜色, 样式, 修饰符, Text/Span/Line
└── references/ # 详细文档
├── _shared/ # 共享规则 (rust-defaults.md)
├── basics/ # 应用结构, 后端
├── layout/ # 约束类型, Flex 模式
├── widgets/ # 内置组件, 自定义组件
└── styling/ # 颜色, 文本样式使用方法
作为 Claude Code 技能使用
将此目录复制或符号链接到 Claude Code 技能目录:
# 方式 1: 符号链接
ln -s /path/to/ratatui-skills ~/.claude/skills/ratatui
# 方式 2: 复制
cp -r /path/to/ratatui-skills ~/.claude/skills/ratatui触发关键词
技能在以下关键词时激活:
ratatui,TUI,terminal ui,ratatui::run,ratatui::initLayout,Constraint,Rect,Flex,horizontal,verticalBlock,Paragraph,List,Table,Gauge,ChartStyle,Color,Stylize,Span,Line,TextWidget,StatefulWidget,ListState,TableState- 中文关键词:
终端界面,布局,组件,样式等
模块说明
| 模块 | 用途 | 核心 API |
|---|---|---|
| basics | 终端设置 | ratatui::run(), init(), restore(), DefaultTerminal |
| layout | 屏幕布局 | Layout, Constraint, Rect, Flex |
| widgets | UI 组件 | Block, List, Table, Gauge, Scrollbar |
| styling | 颜色与文本 | Style, Color, Stylize, Span, Line, Text |
版本信息
- ratatui: 0.30.0
- Rust edition: 2024
- 最后更新: 2026-01-19
Ratatui Skills
中文 | 日本語
Comprehensive skills collection for Rust's ratatui TUI library.
Structure
ratatui-skills/
├── SKILL.md # Main entry point (overview + quick reference)
├── skills/ # Sub-module skills
│ ├── basics/ # Terminal init, app structure, event loop
│ ├── layout/ # Constraint, Rect, Flex, split areas
│ ├── widgets/ # Block, List, Table, custom widgets
│ └── styling/ # Color, Style, Modifier, Text/Span/Line
└── references/ # Detailed documentation
├── _shared/ # Shared rules (rust-defaults.md)
├── basics/ # App structure, backends
├── layout/ # Constraints, flex modes
├── widgets/ # Built-in widgets, custom widgets
└── styling/ # Colors, text stylingUsage
As Claude Code Skills
Copy or symlink this directory to your Claude Code skills directory:
# Option 1: Symlink
ln -s /path/to/ratatui-skills ~/.claude/skills/ratatui
# Option 2: Copy
cp -r /path/to/ratatui-skills ~/.claude/skills/ratatuiTrigger Keywords
The skill activates on keywords like:
ratatui,TUI,terminal ui,ratatui::run,ratatui::initLayout,Constraint,Rect,Flex,horizontal,verticalBlock,Paragraph,List,Table,Gauge,ChartStyle,Color,Stylize,Span,Line,TextWidget,StatefulWidget,ListState,TableState
Modules
| Module | Purpose | Key APIs |
|---|---|---|
| basics | Terminal setup | ratatui::run(), init(), restore(), DefaultTerminal |
| layout | Screen layout | Layout, Constraint, Rect, Flex |
| widgets | UI components | Block, List, Table, Gauge, Scrollbar |
| styling | Colors & text | Style, Color, Stylize, Span, Line, Text |
Version
- ratatui: 0.30.0
- Rust edition: 2024
- Last updated: 2026-01-19
Rust Code Generation Defaults
Shared rules for all Rust-related skills. Symlink this file to your skill's references/ directory.
Cargo.toml Defaults
[package]
edition = "2024" # ALWAYS use 2024, NOT 2021
[dependencies]
# Use latest stable versionsCommon Dependencies (Latest Versions)
| Crate | Version | Features |
|---|---|---|
| tokio | 1.49 | ["full"] |
| serde | 1.0 | ["derive"] |
| anyhow | 1.0 | - |
| thiserror | 2.0 | - |
| tracing | 0.1 | - |
| axum | 0.8 | - |
| sqlx | 0.8 | ["runtime-tokio", "postgres"] |
Code Style
- Prefer explicit error handling over
.unwrap()in production code - Use
?operator for error propagation - Add
#![warn(clippy::all)]to lib.rs/main.rs - Use
rustfmtdefault settings
Error Handling
- Libraries: Use
thiserrorfor custom error types - Applications: Use
anyhowfor convenient error handling - Never use
.unwrap()on user input or external data
Async Code
- Prefer
tokioruntime for async applications - Use
JoinSetoverfutures::join_allfor task management - Always handle task cancellation gracefully
Ratatui Application Structure
Overview
Ratatui applications follow a common pattern: 1. Initialize terminal 2. Run main loop (draw + handle events) 3. Restore terminal
Initialization Methods
Method 1: ratatui::run() (Recommended for simple apps)
use crossterm::event;
fn main() -> std::io::Result<()> {
ratatui::run(|mut terminal| {
loop {
terminal.draw(|frame| {
// render widgets
})?;
if event::read()?.is_key_press() {
break Ok(());
}
}
})
}Benefits:
- Automatic terminal setup and teardown
- Panic hooks installed automatically
- Simplest approach for basic apps
Method 2: init() / restore() (More control)
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let result = run(&mut terminal);
ratatui::restore();
result
}
fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
loop {
terminal.draw(render)?;
if should_quit()? {
break Ok(());
}
}
}Important: Use a separate function for the main loop to ensure restore() is always called.
Method 3: Manual Backend Construction
use std::io::stdout;
use ratatui::{backend::CrosstermBackend, Terminal};
fn main() -> std::io::Result<()> {
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
// Manual setup
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(
stdout(),
crossterm::terminal::EnterAlternateScreen
)?;
let result = run(&mut terminal);
// Manual teardown
crossterm::terminal::disable_raw_mode()?;
crossterm::execute!(
stdout(),
crossterm::terminal::LeaveAlternateScreen
)?;
result
}Application Patterns
Pattern 1: Functional Style
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let mut state = AppState::default();
loop {
terminal.draw(|frame| render(frame, &state))?;
if let Some(action) = handle_events()? {
match action {
Action::Quit => break,
Action::Increment => state.counter += 1,
Action::Decrement => state.counter -= 1,
}
}
}
ratatui::restore();
Ok(())
}Pattern 2: App Struct with Methods
struct App {
counter: i32,
should_quit: bool,
}
impl App {
fn new() -> Self {
Self { counter: 0, should_quit: false }
}
fn run(&mut self, terminal: &mut DefaultTerminal) -> std::io::Result<()> {
while !self.should_quit {
terminal.draw(|frame| self.render(frame))?;
self.handle_events()?;
}
Ok(())
}
fn render(&self, frame: &mut Frame) {
// ...
}
fn handle_events(&mut self) -> std::io::Result<()> {
// ...
Ok(())
}
}Pattern 3: App as Widget
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
// Compose child widgets
let layout = Layout::vertical([
Constraint::Length(3),
Constraint::Fill(1),
]);
let [header, body] = layout.areas(area);
Paragraph::new("Header").render(header, buf);
self.render_body(body, buf);
}
}
// Usage
terminal.draw(|frame| {
frame.render_widget(&app, frame.area());
})?;Event Handling
Basic Event Loop
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
fn handle_events() -> std::io::Result<bool> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
match key.code {
KeyCode::Char('q') => return Ok(true),
KeyCode::Up => { /* handle up */ }
KeyCode::Down => { /* handle down */ }
_ => {}
}
}
Event::Resize(width, height) => {
// Terminal resized - next draw() will use new size
}
_ => {}
}
Ok(false)
}Non-blocking Events with Timeout
use std::time::Duration;
fn handle_events() -> std::io::Result<bool> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
return Ok(true);
}
}
}
Ok(false)
}Terminal Methods
| Method | Description |
|---|---|
terminal.draw(f) | Draw frame, returns CompletedFrame |
terminal.clear() | Clear terminal screen |
terminal.size() | Get terminal size as Size |
terminal.get_frame() | Get current frame (rarely needed) |
terminal.insert_before(n, f) | Insert lines before viewport |
terminal.set_cursor_position(pos) | Move cursor (via Frame) |
Frame Methods
| Method | Description |
|---|---|
frame.area() | Get drawable Rect |
frame.render_widget(w, area) | Render stateless widget |
frame.render_stateful_widget(w, area, state) | Render stateful widget |
frame.set_cursor_position(pos) | Show cursor at position |
frame.buffer_mut() | Get mutable buffer reference |
Ratatui Backends
Overview
Ratatui supports three terminal backends:
- Crossterm (default) - Cross-platform
- Termion - Unix-only
- Termwiz - Terminal emulator toolkit
Backend Comparison
| Feature | Crossterm | Termion | Termwiz |
|---|---|---|---|
| Platform | Linux/Mac/Windows | Linux/Mac | Linux/Mac/Windows |
| Default | Yes | No | No |
| Cargo feature | crossterm | termion | termwiz |
| Async support | Yes | No | Yes |
| Underline color | Yes | No | Yes |
Using Crossterm (Default)
# Cargo.toml
[dependencies]
ratatui = "0.30"
crossterm = "0.29"use ratatui::DefaultTerminal;
fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
// ...
ratatui::restore();
Ok(())
}Crossterm Version Selection
# Use crossterm 0.28.x
ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_28"] }
# Use crossterm 0.29.x (default)
ratatui = { version = "0.30", features = ["crossterm"] }Using Termion
# Cargo.toml
[dependencies]
ratatui = { version = "0.30", default-features = false, features = ["termion"] }
termion = "4"use std::io::{self, stdout};
use ratatui::{backend::TermionBackend, Terminal};
use termion::raw::IntoRawMode;
fn main() -> io::Result<()> {
let stdout = stdout().into_raw_mode()?;
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// ...
Ok(())
}Using Termwiz
# Cargo.toml
[dependencies]
ratatui = { version = "0.30", default-features = false, features = ["termwiz"] }
termwiz = "0.22"use ratatui::{backend::TermwizBackend, Terminal};
use termwiz::terminal::Terminal as TermwizTerminal;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend = TermwizBackend::new()?;
let mut terminal = Terminal::new(backend)?;
// ...
Ok(())
}Backend Crates (v0.30.0+)
The modular workspace provides separate backend crates:
# Only crossterm backend
[dependencies]
ratatui = { version = "0.30", default-features = false }
ratatui-crossterm = "0.30"
# Only termion backend
[dependencies]
ratatui = { version = "0.30", default-features = false }
ratatui-termion = "0.30"Raw Mode and Alternate Screen
What is Raw Mode?
- Disables line buffering
- Disables echo
- Disables special key processing (Ctrl+C, etc.)
What is Alternate Screen?
- Switches to a separate screen buffer
- Original content preserved
- Restored when leaving alternate screen
Manual Setup (Crossterm)
use std::io::stdout;
use crossterm::{
execute,
terminal::{enable_raw_mode, disable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
fn setup() -> std::io::Result<()> {
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
Ok(())
}
fn teardown() -> std::io::Result<()> {
disable_raw_mode()?;
execute!(stdout(), LeaveAlternateScreen)?;
Ok(())
}Panic Handling
ratatui::run() and ratatui::init() install panic hooks automatically.
For manual setup:
use std::panic;
fn install_panic_hook() {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
// Restore terminal before panic message
let _ = ratatui::restore();
original_hook(info);
}));
}TestBackend for Testing
use ratatui::{backend::TestBackend, Terminal};
#[test]
fn test_render() {
let backend = TestBackend::new(80, 24);
let mut terminal = Terminal::new(backend).unwrap();
terminal.draw(|frame| {
// render widgets
}).unwrap();
// Assert on buffer contents
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(0, 0)].symbol(), "H");
}Ratatui Constraints
Constraint Types
Constraint::Length(n)
Fixed size of exactly n cells.
// Header exactly 3 rows tall
Constraint::Length(3)Constraint::Percentage(n)
Relative size as percentage of available space.
// Take 50% of available space
Constraint::Percentage(50)Constraint::Ratio(numerator, denominator)
Proportional size using ratios.
// Take 1/3 of available space
Constraint::Ratio(1, 3)Constraint::Fill(weight)
Fill remaining space proportionally to weight.
// Two areas, second twice as large
Layout::horizontal([
Constraint::Fill(1), // 1/3 of remaining
Constraint::Fill(2), // 2/3 of remaining
])Constraint::Min(n)
Minimum size of at least n cells. Highest priority.
// At least 10 cells, but can grow
Constraint::Min(10)Constraint::Max(n)
Maximum size of at most n cells.
// Up to 50 cells, but can shrink
Constraint::Max(50)Priority Resolution
When constraints conflict, they are resolved by priority:
1. Min - Highest priority, always satisfied first 2. Max - High priority 3. Length/Percentage/Ratio - Medium priority 4. Fill - Lowest priority, takes remaining space
Examples
Fixed Header + Flexible Body + Fixed Footer
let [header, body, footer] = Layout::vertical([
Constraint::Length(3), // Fixed header
Constraint::Fill(1), // Flexible body
Constraint::Length(1), // Fixed footer
]).areas(frame.area());Sidebar with Min/Max
let [sidebar, main] = Layout::horizontal([
Constraint::Min(20).max(40), // 20-40 cells
Constraint::Fill(1), // Rest
]).areas(frame.area());Equal Columns
// Three equal columns
let [a, b, c] = Layout::horizontal([
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]).areas(frame.area());
// Or using Fill
let [a, b, c] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(1),
Constraint::Fill(1),
]).areas(frame.area());
// Or shorthand
let [a, b, c] = Layout::horizontal([Constraint::Fill(1); 3]).areas(frame.area());Weighted Distribution
// Left: 1 part, Middle: 2 parts, Right: 1 part
let [left, middle, right] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(2),
Constraint::Fill(1),
]).areas(frame.area());Mixed Constraints
let [fixed, flex, percent] = Layout::horizontal([
Constraint::Length(20), // Fixed 20
Constraint::Fill(1), // Fill remaining
Constraint::Percentage(25), // 25% of total
]).areas(frame.area());Constraint Chaining
Constraints support method chaining for combining:
// Between 10 and 50 cells
let constraint = Constraint::Min(10).max(50);
// At least 20, but prefer 30
let constraint = Constraint::Min(20).length(30);Dynamic Layouts
When constraint count isn't known at compile time:
fn create_columns(count: usize, area: Rect) -> Vec<Rect> {
let constraints: Vec<Constraint> = (0..count)
.map(|_| Constraint::Fill(1))
.collect();
Layout::horizontal(constraints)
.split(area)
.to_vec()
}Ratatui Flex Modes
Overview
Flex controls how extra space is distributed when constraints are satisfied.
Flex Modes
Flex::Start (Default)
Align content to start, excess space at end.
[Item1][Item2][Item3]Flex::End
Align content to end, excess space at start.
[Item1][Item2][Item3]Flex::Center
Center content, equal space on both sides.
[Item1][Item2][Item3]Flex::SpaceBetween
Distribute space between items, none at edges.
[Item1] [Item2] [Item3]Flex::SpaceAround
Space around each item (edges get half space).
[Item1] [Item2] [Item3]Flex::SpaceEvenly
Equal space everywhere including edges.
[Item1] [Item2] [Item3]Flex::Legacy
Legacy behavior - excess space goes to last element.
[Item1][Item2][Item3 ]Examples
Centered Buttons
use ratatui::layout::{Constraint, Flex, Layout};
let [_, btn1, _, btn2, _] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(10),
Constraint::Length(2), // Gap between buttons
Constraint::Length(10),
Constraint::Fill(1),
]).areas(area);
// Or with Flex
let buttons = Layout::horizontal([
Constraint::Length(10),
Constraint::Length(10),
])
.flex(Flex::Center)
.spacing(2)
.areas(area);Navigation Bar with SpaceBetween
let nav_items = Layout::horizontal([
Constraint::Length(8), // Home
Constraint::Length(10), // Products
Constraint::Length(8), // About
Constraint::Length(10), // Contact
])
.flex(Flex::SpaceBetween)
.areas(nav_area);Card Grid with SpaceEvenly
let cards = Layout::horizontal([
Constraint::Length(20),
Constraint::Length(20),
Constraint::Length(20),
])
.flex(Flex::SpaceEvenly)
.areas(grid_area);Toolbar with Start Alignment
let toolbar = Layout::horizontal([
Constraint::Length(8), // Save
Constraint::Length(8), // Open
Constraint::Length(8), // New
])
.flex(Flex::Start)
.spacing(1)
.areas(toolbar_area);Visual Comparison
Given 3 items of 10 cells each in an 80 cell wide area:
Flex::Start:
[----10----][----10----][----10----]
Flex::End:
[----10----][----10----][----10----]
Flex::Center:
[----10----][----10----][----10----]
Flex::SpaceBetween:
[----10----] [----10----] [----10----]
Flex::SpaceAround:
[----10----] [----10----] [----10----]
Flex::SpaceEvenly:
[----10----] [----10----] [----10----]Combining Flex with Margin and Spacing
let layout = Layout::horizontal([
Constraint::Length(10),
Constraint::Length(10),
Constraint::Length(10),
])
.flex(Flex::Center)
.margin(2) // 2 cells margin on all sides
.spacing(1); // 1 cell between items
let areas = layout.areas(frame.area());When to Use Each
| Mode | Use Case |
|---|---|
Start | Default alignment, left-to-right content |
End | Right-aligned content, action buttons |
Center | Modal dialogs, centered forms |
SpaceBetween | Navigation bars, toolbars |
SpaceAround | Card layouts, icon grids |
SpaceEvenly | Uniform distribution needed |
Legacy | Backward compatibility only |
Ratatui Colors
Color Enum
pub enum Color {
Reset, // Terminal default
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray, // Same as LightBlack
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
LightGray, // Same as White
White,
Rgb(u8, u8, u8), // True color
Indexed(u8), // 256 color palette
}Basic Colors
use ratatui::style::Color;
// Standard 8 colors
Color::Black
Color::Red
Color::Green
Color::Yellow
Color::Blue
Color::Magenta
Color::Cyan
Color::White
// Light/Bright variants
Color::LightBlack // Gray
Color::LightRed
Color::LightGreen
Color::LightYellow
Color::LightBlue
Color::LightMagenta
Color::LightCyan
Color::LightWhite // Bright whiteRGB Colors
True color support (most modern terminals):
// RGB values 0-255
Color::Rgb(255, 0, 0) // Pure red
Color::Rgb(0, 255, 0) // Pure green
Color::Rgb(0, 0, 255) // Pure blue
Color::Rgb(255, 128, 0) // Orange
Color::Rgb(128, 0, 128) // Purple
// From hex
fn hex_to_rgb(hex: u32) -> Color {
Color::Rgb(
((hex >> 16) & 0xFF) as u8,
((hex >> 8) & 0xFF) as u8,
(hex & 0xFF) as u8,
)
}
let coral = hex_to_rgb(0xFF7F50);Indexed Colors (256 Palette)
// 0-7: Standard colors
Color::Indexed(0) // Black
Color::Indexed(1) // Red
Color::Indexed(2) // Green
// ...
// 8-15: Bright colors
Color::Indexed(8) // Bright Black (Gray)
Color::Indexed(9) // Bright Red
// ...
// 16-231: 6x6x6 color cube
// Formula: 16 + 36*r + 6*g + b (r,g,b: 0-5)
Color::Indexed(196) // Bright red
Color::Indexed(46) // Bright green
Color::Indexed(21) // Bright blue
// 232-255: Grayscale (24 shades)
Color::Indexed(232) // Near black
Color::Indexed(243) // Mid gray
Color::Indexed(255) // Near whiteUsing Colors with Style
use ratatui::style::{Color, Style};
// Foreground color
let style = Style::new().fg(Color::Red);
// Background color
let style = Style::new().bg(Color::Blue);
// Both
let style = Style::new()
.fg(Color::White)
.bg(Color::DarkGray);
// Underline color (requires feature)
let style = Style::new()
.underlined()
.underline_color(Color::Red);Using Stylize Shorthand
use ratatui::style::Stylize;
// Foreground colors
"text".black()
"text".red()
"text".green()
"text".yellow()
"text".blue()
"text".magenta()
"text".cyan()
"text".gray()
"text".white()
// Light variants
"text".light_red()
"text".light_green()
// ...
// Background colors (on_*)
"text".on_black()
"text".on_red()
"text".on_blue()
// ...
"text".on_light_blue()Color Conversion (palette feature)
With palette feature enabled:
use palette::{Srgb, Hsv};
use ratatui::style::Color;
// From palette Srgb
let color: Color = Srgb::new(1.0, 0.5, 0.0).into();
// From palette Hsv
let color: Color = Hsv::new(120.0, 1.0, 1.0).into();Serialization (serde feature)
With serde feature enabled:
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Theme {
primary: Color,
secondary: Color,
}
// JSON format
// { "primary": "Red", "secondary": { "Rgb": [255, 128, 0] } }Terminal Compatibility
| Color Type | Support |
|---|---|
| 8 basic colors | All terminals |
| 16 colors (+ light) | Most terminals |
| 256 indexed | Modern terminals |
| RGB true color | Modern terminals |
Check terminal support:
# Check TERM variable
echo $TERM
# Check color support
echo $COLORTERMCommon Color Palettes
Monokai-inspired
let background = Color::Rgb(39, 40, 34);
let foreground = Color::Rgb(248, 248, 242);
let comment = Color::Rgb(117, 113, 94);
let red = Color::Rgb(249, 38, 114);
let green = Color::Rgb(166, 226, 46);
let yellow = Color::Rgb(230, 219, 116);
let blue = Color::Rgb(102, 217, 239);
let purple = Color::Rgb(174, 129, 255);Nord-inspired
let polar_night = Color::Rgb(46, 52, 64);
let snow_storm = Color::Rgb(236, 239, 244);
let frost_blue = Color::Rgb(136, 192, 208);
let aurora_red = Color::Rgb(191, 97, 106);
let aurora_green = Color::Rgb(163, 190, 140);Ratatui Text Styling
Text Hierarchy
Text
├── Line 1
│ ├── Span "Hello "
│ └── Span "World" (styled)
└── Line 2
└── Span "More text"Span
A segment of text with a single style.
use ratatui::text::Span;
use ratatui::style::{Style, Stylize};
// Raw (unstyled)
let span = Span::raw("Hello");
// Styled with Style struct
let span = Span::styled("World", Style::new().bold());
// Using Stylize trait (string -> Span)
let span = "Colored".red().bold();
// From conversion
let span: Span = "Text".into();Span Methods
// Get content
span.content // Cow<'a, str>
// Get style
span.style // Style
// Styled methods return new Span
span.style(Style::new().red())
span.fg(Color::Blue)
span.bg(Color::White)
// Reset style
span.reset_style()Line
A single line of styled text (collection of Spans).
use ratatui::text::Line;
// From string
let line = Line::raw("Simple line");
// From styled string
let line = Line::styled("Styled line", Style::new().blue());
// From spans
let line = Line::from(vec![
Span::raw("Normal "),
Span::styled("bold", Style::new().bold()),
" text".into(),
]);
// Using shorthand
let line = Line::from(vec![
"Hello ".into(),
"world".red(),
"!".into(),
]);
// From string with Stylize
let line: Line = "Full line".yellow().into();Line Methods
// Alignment
line.alignment(Alignment::Center)
line.left_aligned()
line.centered()
line.right_aligned()
// Style the whole line
line.style(Style::new().italic())
// Get spans
line.spans // Vec<Span>
// Get width
line.width() // usizeText
Multiple lines of styled text.
use ratatui::text::Text;
// From string (splits on newlines)
let text = Text::raw("Line 1\nLine 2\nLine 3");
// From lines
let text = Text::from(vec![
Line::from("First line".blue()),
Line::from("Second line".green()),
Line::from(vec!["Mixed ".into(), "styles".red()]),
]);
// Styled text
let text = Text::styled("All italic", Style::new().italic());
// From iterator
let text: Text = ["Line 1", "Line 2", "Line 3"]
.iter()
.map(|s| Line::from(*s))
.collect();Text Methods
// Get lines
text.lines // Vec<Line>
// Get dimensions
text.width() // usize (max line width)
text.height() // usize (number of lines)
// Style all lines
text.style(Style::new().bold())
// Alignment
text.alignment(Alignment::Center)
// Add line
text.push_line(Line::from("New line"))
// Extend with lines
text.extend(other_lines)Stylize Trait
Shorthand methods available on strings and styled types:
use ratatui::style::Stylize;
// Color shortcuts
"text".black()
"text".red()
"text".green()
"text".yellow()
"text".blue()
"text".magenta()
"text".cyan()
"text".gray()
"text".white()
// Light variants
"text".light_red()
"text".light_green()
// ...
// Background (on_*)
"text".on_black()
"text".on_red()
"text".on_blue()
// ...
// Modifiers
"text".bold()
"text".dim()
"text".italic()
"text".underlined()
"text".slow_blink()
"text".rapid_blink()
"text".reversed()
"text".hidden()
"text".crossed_out()
// Reset
"text".reset()
// Not (remove modifier)
"text".not_bold()
"text".not_italic()
// ...Style Struct
For storing and reusing styles:
use ratatui::style::{Color, Modifier, Style};
// Create empty style
let style = Style::default();
let style = Style::new();
// Set foreground
let style = Style::new().fg(Color::Red);
// Set background
let style = Style::new().bg(Color::Blue);
// Add modifiers
let style = Style::new()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::ITALIC);
// Combine modifiers
let style = Style::new()
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED);
// Remove modifier
let style = style.remove_modifier(Modifier::BOLD);
// Set underline color
let style = Style::new()
.underlined()
.underline_color(Color::Red);
// Patch (merge styles)
let base = Style::new().fg(Color::White);
let highlight = base.patch(Style::new().bold());Common Patterns
Highlighted Selection
let normal = Style::default();
let selected = Style::new().reversed();
let items: Vec<Line> = data.iter().enumerate()
.map(|(i, item)| {
let style = if i == selected_idx { selected } else { normal };
Line::styled(item, style)
})
.collect();Status Indicators
fn status_style(status: Status) -> Style {
match status {
Status::Success => Style::new().green(),
Status::Warning => Style::new().yellow(),
Status::Error => Style::new().red().bold(),
Status::Info => Style::new().blue(),
}
}Syntax Highlighting
fn highlight_line(line: &str) -> Line {
let mut spans = Vec::new();
// Parse and create styled spans
// ...
Line::from(spans)
}Theme Structure
struct Theme {
primary: Style,
secondary: Style,
accent: Style,
error: Style,
warning: Style,
success: Style,
}
impl Default for Theme {
fn default() -> Self {
Self {
primary: Style::new().fg(Color::White),
secondary: Style::new().fg(Color::Gray),
accent: Style::new().fg(Color::Cyan).bold(),
error: Style::new().fg(Color::Red),
warning: Style::new().fg(Color::Yellow),
success: Style::new().fg(Color::Green),
}
}
}Ratatui Built-in Widgets
Block
Container widget for borders, titles, and padding.
use ratatui::widgets::{Block, Borders, Padding};
// Simple bordered block
let block = Block::bordered().title("Title");
// Detailed configuration
let block = Block::new()
.borders(Borders::ALL)
.border_style(Style::new().blue())
.title("Top Title")
.title_bottom("Bottom")
.title_alignment(Alignment::Center)
.padding(Padding::horizontal(1));
// Use as container
let paragraph = Paragraph::new("Content").block(block);Paragraph
Display styled and wrapped text.
use ratatui::widgets::{Paragraph, Wrap};
use ratatui::text::Text;
// Simple text
let para = Paragraph::new("Hello World!");
// Styled text
let para = Paragraph::new("Text".red().bold());
// Multi-line
let para = Paragraph::new(vec![
Line::from("Line 1"),
Line::from("Line 2"),
]);
// With wrapping
let para = Paragraph::new(long_text)
.wrap(Wrap { trim: true });
// With scrolling
let para = Paragraph::new(content)
.scroll((offset_y, offset_x));List
Display selectable items.
use ratatui::widgets::{List, ListItem, ListState, ListDirection};
// Create items
let items: Vec<ListItem> = data
.iter()
.map(|s| ListItem::new(s.as_str()))
.collect();
// Configure list
let list = List::new(items)
.block(Block::bordered().title("List"))
.highlight_style(Style::new().reversed())
.highlight_symbol("> ")
.repeat_highlight_symbol(true)
.direction(ListDirection::TopToBottom);
// State management
let mut state = ListState::default();
state.select(Some(0));
// Navigation
fn next(&mut self, len: usize) {
let i = match self.state.selected() {
Some(i) => (i + 1) % len,
None => 0,
};
self.state.select(Some(i));
}
fn previous(&mut self, len: usize) {
let i = match self.state.selected() {
Some(i) => (i + len - 1) % len,
None => 0,
};
self.state.select(Some(i));
}Table
Multi-column data grid.
use ratatui::widgets::{Table, Row, Cell, TableState};
use ratatui::layout::Constraint;
// Create rows
let rows = vec![
Row::new(vec![
Cell::from("Alice"),
Cell::from("alice@example.com"),
Cell::from("Admin").green(),
]),
Row::new(vec![
Cell::from("Bob"),
Cell::from("bob@example.com"),
Cell::from("User"),
]),
];
// Configure table
let widths = [
Constraint::Length(15),
Constraint::Fill(1),
Constraint::Length(10),
];
let table = Table::new(rows, widths)
.block(Block::bordered().title("Users"))
.header(
Row::new(vec!["Name", "Email", "Role"])
.style(Style::new().bold())
.bottom_margin(1)
)
.highlight_style(Style::new().reversed())
.highlight_symbol("> ");
// State
let mut state = TableState::default();
state.select(Some(0));Tabs
Horizontal tab bar.
use ratatui::widgets::Tabs;
let titles = vec!["Home", "Settings", "Help"];
let selected = 0;
let tabs = Tabs::new(titles)
.block(Block::bordered())
.select(selected)
.style(Style::new().white())
.highlight_style(Style::new().yellow().bold())
.divider(" | ");Gauge
Progress bar.
use ratatui::widgets::Gauge;
// Percentage-based
let gauge = Gauge::default()
.block(Block::bordered().title("Download"))
.gauge_style(Style::new().fg(Color::Green).bg(Color::Black))
.percent(75)
.label("75%");
// Ratio-based (0.0 to 1.0)
let gauge = Gauge::default()
.ratio(0.75)
.label(format!("{:.1}%", 0.75 * 100.0));LineGauge
Thin progress line.
use ratatui::widgets::LineGauge;
use ratatui::symbols::line;
let gauge = LineGauge::default()
.block(Block::bordered().title("Progress"))
.ratio(0.5)
.line_set(line::THICK)
.filled_style(Style::new().green())
.unfilled_style(Style::new().dark_gray());Scrollbar
Scroll indicator.
use ratatui::widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState};
let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("↑"))
.end_symbol(Some("↓"))
.thumb_symbol("█")
.track_symbol(Some("│"));
let mut state = ScrollbarState::new(total_items)
.position(current_position);
frame.render_stateful_widget(scrollbar, area, &mut state);Sparkline
Compact data visualization.
use ratatui::widgets::Sparkline;
let data = vec![0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0];
let sparkline = Sparkline::default()
.block(Block::bordered().title("Sparkline"))
.data(&data)
.max(10)
.style(Style::new().green());Chart
Line and scatter charts.
use ratatui::widgets::{Axis, Chart, Dataset, GraphType};
use ratatui::symbols::Marker;
let data = vec![(0.0, 1.0), (1.0, 3.0), (2.0, 2.0), (3.0, 4.0)];
let dataset = Dataset::default()
.name("Data")
.marker(Marker::Braille)
.graph_type(GraphType::Line)
.style(Style::new().green())
.data(&data);
let chart = Chart::new(vec![dataset])
.block(Block::bordered().title("Chart"))
.x_axis(
Axis::default()
.title("X")
.bounds([0.0, 4.0])
.labels(vec!["0", "2", "4"])
)
.y_axis(
Axis::default()
.title("Y")
.bounds([0.0, 5.0])
.labels(vec!["0", "2.5", "5"])
);BarChart
Bar chart visualization.
use ratatui::widgets::{Bar, BarChart, BarGroup};
let data = vec![
("Mon", 10),
("Tue", 20),
("Wed", 15),
("Thu", 25),
("Fri", 30),
];
let bars: Vec<Bar> = data
.iter()
.map(|(label, value)| Bar::default().value(*value).label((*label).into()))
.collect();
let chart = BarChart::default()
.block(Block::bordered().title("Weekly"))
.data(BarGroup::default().bars(&bars))
.bar_width(5)
.bar_gap(2)
.bar_style(Style::new().green())
.value_style(Style::new().bold());Canvas
Drawing shapes.
use ratatui::widgets::canvas::{Canvas, Circle, Line, Rectangle};
let canvas = Canvas::default()
.block(Block::bordered().title("Canvas"))
.x_bounds([0.0, 100.0])
.y_bounds([0.0, 100.0])
.paint(|ctx| {
ctx.draw(&Rectangle {
x: 10.0,
y: 10.0,
width: 30.0,
height: 20.0,
color: Color::Green,
});
ctx.draw(&Circle {
x: 50.0,
y: 50.0,
radius: 20.0,
color: Color::Yellow,
});
ctx.draw(&Line {
x1: 0.0,
y1: 0.0,
x2: 100.0,
y2: 100.0,
color: Color::Red,
});
})
.marker(Marker::Braille);Clear
Clear area for overlays.
use ratatui::widgets::Clear;
// Clear before rendering popup
frame.render_widget(Clear, popup_area);
frame.render_widget(popup_content, popup_area);Calendar (feature: widget-calendar)
Monthly calendar display.
use ratatui::widgets::calendar::{CalendarEventStore, Monthly};
use time::Date;
let events = CalendarEventStore::today(Style::new().red().bold());
let calendar = Monthly::new(
Date::from_calendar_date(2024, time::Month::January, 1).unwrap(),
events,
)
.block(Block::bordered().title("January 2024"))
.show_weekdays_header(Style::new().bold())
.show_month_header(Style::new().bold());Creating Custom Widgets
Widget Trait
The basic trait for stateless widgets:
pub trait Widget {
fn render(self, area: Rect, buf: &mut Buffer);
}Implementation Patterns
Pattern 1: Reference-Based (Recommended)
Implement on &Widget for reusability:
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::Line;
use ratatui::widgets::Widget;
struct Counter {
value: u32,
label: String,
}
impl Widget for &Counter {
fn render(self, area: Rect, buf: &mut Buffer) {
let text = format!("{}: {}", self.label, self.value);
Line::raw(text).render(area, buf);
}
}
// Usage - widget can be reused
let counter = Counter { value: 42, label: "Count".to_string() };
frame.render_widget(&counter, area1);
frame.render_widget(&counter, area2); // Can render againPattern 2: Consuming Widget
Original pattern, widget is consumed:
impl Widget for Counter {
fn render(self, area: Rect, buf: &mut Buffer) {
let text = format!("{}: {}", self.label, self.value);
Line::raw(text).render(area, buf);
}
}
// Usage - widget is consumed
let counter = Counter { value: 42, label: "Count".to_string() };
frame.render_widget(counter, area);
// counter is no longer availablePattern 3: Both Patterns
Support both usage styles:
impl Widget for &Counter {
fn render(self, area: Rect, buf: &mut Buffer) {
// Implementation
}
}
impl Widget for Counter {
fn render(self, area: Rect, buf: &mut Buffer) {
(&self).render(area, buf);
}
}StatefulWidget
For widgets with external state:
use ratatui::widgets::StatefulWidget;
struct ScrollableList {
items: Vec<String>,
}
#[derive(Default)]
struct ScrollableListState {
offset: usize,
selected: Option<usize>,
}
impl StatefulWidget for &ScrollableList {
type State = ScrollableListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let visible_items = self.items
.iter()
.skip(state.offset)
.take(area.height as usize);
for (i, item) in visible_items.enumerate() {
let y = area.y + i as u16;
let style = if Some(state.offset + i) == state.selected {
Style::new().reversed()
} else {
Style::default()
};
buf.set_string(area.x, y, item, style);
}
}
}
// Usage
let list = ScrollableList { items: vec![...] };
let mut state = ScrollableListState::default();
frame.render_stateful_widget(&list, area, &mut state);Mutable Widget Pattern
For widgets that modify internal state during render:
struct AnimatedWidget {
frame_count: u32,
}
impl Widget for &mut AnimatedWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
self.frame_count += 1;
let text = format!("Frame: {}", self.frame_count);
Line::raw(text).render(area, buf);
}
}
// Usage
let mut widget = AnimatedWidget { frame_count: 0 };
frame.render_widget(&mut widget, area);Composing Widgets
Build complex widgets from simpler ones:
struct Panel {
title: String,
content: String,
}
impl Widget for &Panel {
fn render(self, area: Rect, buf: &mut Buffer) {
// Use Block for border
let block = Block::bordered().title(self.title.as_str());
let inner = block.inner(area);
block.render(area, buf);
// Render content inside
Paragraph::new(self.content.as_str()).render(inner, buf);
}
}Widget with Configuration
Builder pattern for configurable widgets:
struct ProgressBar {
progress: f64,
style: Style,
label: Option<String>,
}
impl ProgressBar {
fn new(progress: f64) -> Self {
Self {
progress: progress.clamp(0.0, 1.0),
style: Style::default(),
label: None,
}
}
fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
impl Widget for ProgressBar {
fn render(self, area: Rect, buf: &mut Buffer) {
let filled = (area.width as f64 * self.progress) as u16;
// Draw filled portion
for x in area.x..area.x + filled {
buf[(x, area.y)].set_char('█').set_style(self.style);
}
// Draw empty portion
for x in area.x + filled..area.x + area.width {
buf[(x, area.y)].set_char('░');
}
// Draw label
if let Some(label) = self.label {
let x = area.x + (area.width - label.len() as u16) / 2;
buf.set_string(x, area.y, &label, Style::default());
}
}
}
// Usage
frame.render_widget(
ProgressBar::new(0.75)
.style(Style::new().green())
.label("75%"),
area
);Buffer Operations
Direct buffer manipulation:
impl Widget for &MyWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
// Set single cell
buf[(area.x, area.y)]
.set_char('X')
.set_style(Style::new().red());
// Set string
buf.set_string(area.x, area.y, "Hello", Style::default());
// Set styled spans
buf.set_line(area.x, area.y, &Line::from(vec![
Span::raw("Hello "),
Span::styled("World", Style::new().bold()),
]), area.width);
// Fill area
for y in area.y..area.y + area.height {
for x in area.x..area.x + area.width {
buf[(x, y)].set_char('.');
}
}
}
}Best Practices
1. Implement on references - Use impl Widget for &MyWidget for reusability 2. Compose existing widgets - Build on Block, Paragraph, etc. 3. Use builder pattern - For configurable widgets 4. Handle area bounds - Widgets may receive empty areas 5. Respect area limits - Don't draw outside the given area