Skip to Content
ftui-styleStyle API

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:

MethodFlag
.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 fg

underline_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:

  • Colorsself’s Some wins; self’s None inherits from parent.
  • 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

log_styles.rs
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.

Where to go next