Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Your own theme

A theme is a ThemeSpec you resolve. The minimum is a name, a mode and one colour:

extern crate tuile;
use tuile::prelude::*;
fn main() {
let mine = ThemeSpec::new("acme", true, Rgb::hex(0x7c3aed)).resolve();
theme::set(mine);
theme::set_by_name("textual-dark");
}

Everything else is derived. That is usually too uniform for a real product, so add the colours that carry meaning in your domain and leave the rest:

extern crate tuile;
use tuile::prelude::*;
fn main() {
let spec = ThemeSpec::new("acme", true, Rgb::hex(0x7c3aed))
    .secondary(Rgb::hex(0x2dd4bf))
    .accent(Rgb::hex(0xfbbf24))
    .success(Rgb::hex(0x22c55e))
    .warning(Rgb::hex(0xf59e0b))
    .error(Rgb::hex(0xef4444))
    .background(Rgb::hex(0x0f0f17))
    .surface(Rgb::hex(0x1a1a24));
theme::set(spec.resolve());
theme::set_by_name("textual-dark");
}

Every builder method is const, so a theme can be a constant:

extern crate tuile;
use tuile::prelude::*;
const ACME: ThemeSpec = ThemeSpec::new("acme", true, Rgb::hex(0x7c3aed))
    .accent(Rgb::hex(0xfbbf24));
fn main() { let _ = ACME.resolve(); }

Colour construction

extern crate tuile;
use tuile::prelude::*;
fn main() {
let a = Rgb::hex(0x7c3aed);
let b = Rgb(124, 58, 237);
assert_eq!(a, b);

let lighter = a.shade(1);        // +15% L* in CIE-Lab
let darker = a.shade(-2);        // -30%
let mixed = a.blend(b, 0.5);     // linear interpolation
let on_top = a.text_on(0.87);    // readable text for this background
let _ = (lighter, darker, mixed, on_top);
}

Rgb::hex takes the same 0xRRGGBB you would paste from a design tool. There is no CSS string parser, and no named colours.

Starting from a built-in

Copy a palette and change what you need:

extern crate tuile;
use tuile::prelude::*;
fn main() {
let mut spec = *theme::BUILTIN.iter().find(|s| s.name == "gruvbox").expect("ships with the crate");
spec.name = "gruvbox-hot";
spec.error = Some(Rgb::hex(0xff0033));
theme::set(spec.resolve());
theme::set_by_name("textual-dark");
}

ThemeSpec is Copy with public fields, so this needs no builder call at all.

Per-widget themes

Every builder takes .theme(&Theme), which overrides the global for that widget:

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
fn demo(area: Rect, buf: &mut Buffer, st: &mut ButtonState) {
let danger = ThemeSpec::new("danger", true, Rgb::hex(0xef4444)).resolve();
Button::new("Delete").theme(&danger).render(area, buf, st);
}
fn main() {}

Resolve the theme once and keep it in your app struct. Resolving it every frame repeats the whole Lab expansion for no benefit.

A per-widget theme is the right tool for a panel that must stand apart, a preview pane showing another palette, or a diff view with its own colours. For anything larger, set the global theme.

Letting users pick

Offer theme::theme_names() in a select and call set_by_name on change:

extern crate tuile;
extern crate ratatui_core;
use tuile::prelude::*;
struct Prefs { picker: SelectState }
impl Prefs {
fn on_theme_picked(&mut self, name: &str) {
    if !theme::set_by_name(name) {
        // unknown name, keep the current theme
    }
}
}
fn main() {}

set_by_name returns false rather than panicking or falling back silently, so a stale name in a config file leaves the user with the previous theme instead of a surprise.

To persist a custom theme, store the spec’s ten colours, not the resolved theme. The resolution rules can change between versions; the palette you chose will not.

What you cannot theme

The derivation is fixed. You choose the base colours; you do not choose that text_muted is 60% alpha, or that hover is an 8% tint. Widgets read named roles, so changing a role means changing every widget that reads it.

If you need one role to differ, build a Theme by resolving a spec and then assigning the field directly. Theme is a plain struct with public fields:

extern crate tuile;
use tuile::prelude::*;
fn main() {
let mut th = ThemeSpec::new("acme", true, Rgb::hex(0x7c3aed)).resolve();
th.hover_bg = Rgb::hex(0x2a2a3a);       // stronger hover than the 8% default
theme::set(th);
theme::set_by_name("textual-dark");
}

That is supported and occasionally the right answer. Be aware you are stepping outside the derivation, so the override will not follow when you change the base palette.