Style API
ftui_style::Style is the single struct every render surface in
FrankenTUI uses to describe “how should this text look?”. It’s
Copy, 28 bytes, no allocations, and combines CSS-style with a
merge() call.
The struct
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Style {
pub fg: Option<PackedRgba>,
pub bg: Option<PackedRgba>,
pub attrs: Option<StyleFlags>,
pub underline_color: Option<PackedRgba>,
}Four optional fields. None means “inherit from parent”; Some
means “force this value.” This is the exact model CSS uses for
properties like color and font-weight.
Builder methods
Style::new()
.fg(PackedRgba::rgb(200, 0, 0))
.bg(PackedRgba::rgb(0, 0, 0))
.bold()
.italic()
.underline();Every builder method takes self and returns Self. No intermediate
state; each call returns a new Style value.
Attribute flags — StyleFlags
pub struct StyleFlags(pub u16);
impl StyleFlags {
pub const BOLD: StyleFlags;
pub const ITALIC: StyleFlags;
pub const UNDERLINE: StyleFlags;
pub const DIM: StyleFlags;
pub const REVERSE: StyleFlags;
pub const STRIKETHROUGH: StyleFlags;
pub const BLINK: StyleFlags;
pub const HIDDEN: StyleFlags;
pub const DOUBLE_UNDERLINE: StyleFlags;
pub const CURLY_UNDERLINE: StyleFlags;
}Each builder method adds one flag:
| Method | Flag |
|---|---|
.bold() | BOLD |
.italic() | ITALIC |
.underline() | UNDERLINE |
.double_underline() | DOUBLE_UNDERLINE |
.curly_underline() | CURLY_UNDERLINE |
.dim() | DIM |
.reverse() | REVERSE (swap fg/bg) |
.strikethrough() | STRIKETHROUGH |
.blink() | BLINK |
.hidden() | HIDDEN (conceal — passwords) |
You can also set flags directly:
Style::new().attrs(StyleFlags::BOLD | StyleFlags::ITALIC);Colors — fg, bg, underline_color
Style::new()
.fg(PackedRgba::rgb(100, 200, 255)) // foreground
.bg(PackedRgba::rgb(0, 0, 0)) // background
.underline_color(PackedRgba::rgb(200, 0, 0)); // decoupled from fgunderline_color is separate so underlines can be a different color
than the text (useful for spell-check squiggles, hyperlink underlines,
etc.).
See color for the difference between Color,
PackedRgba, and Ansi16, and how downgrade works on older
terminals.
The cascade — merge
// child.merge(parent) — child wins on colors, attrs OR together
impl Style {
pub fn merge(self, parent: Style) -> Style;
}The contract:
- Colors —
self’sSomewins;self’sNoneinherits fromparent. - Attribute flags — union (bitwise OR). A parent’s bold survives a child that doesn’t mention bold.
let parent = Style::new()
.fg(PackedRgba::rgb(255, 255, 255))
.bold();
let child = Style::new()
.fg(PackedRgba::rgb(200, 0, 0))
.italic();
let merged = child.merge(parent);
// fg = red (child wins); attrs = BOLD | ITALIC (union).Diff — diff
impl Style {
pub fn diff(&self, other: &Style) -> Option<StyleDiff>;
}Computes the minimal set of ANSI changes to go from self to
other. The presenter uses this to avoid emitting redundant SGR
sequences when neighboring cells share attributes.
Worked example — a log-line styler
use ftui_render::cell::PackedRgba;
use ftui_style::Style;
fn style_for_level(level: &str) -> Style {
match level {
"error" => Style::new()
.fg(PackedRgba::rgb(240, 80, 80))
.bold(),
"warn" => Style::new()
.fg(PackedRgba::rgb(240, 200, 80)),
"info" => Style::new()
.fg(PackedRgba::rgb(100, 180, 240)),
"debug" => Style::new()
.fg(PackedRgba::rgb(160, 160, 160))
.dim(),
_ => Style::new(),
}
}Combine with a surrounding cascade:
let frame_default = Style::new().fg(PackedRgba::rgb(220, 220, 220));
let effective = style_for_level(level).merge(frame_default);Interaction with the render layer
Style values flow into ftui-render’s Cell through the render
layer’s encoders. A Cell packs the PackedRgba fg / bg, a StyleFlags
payload, and a pointer to the grapheme into 16 bytes total. Because
Style is already Copy and PackedRgba is already u32, the hot
path never touches the heap.
Pitfalls
Style is Copy, so .bold() on a borrowed value does not
modify it. If you write let s = original_style; s.bold(); the
mutation is discarded. Chain: let s = original_style.bold();.
hidden() conceals text but still counts cells. It does not
shrink layout. Passwords render as equivalent-width blank space with
the same cursor behavior as the underlying text.
reverse() swaps at render time. If you set both reverse and
explicit fg/bg, the renderer still swaps. Use one mechanism.