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

Text and typography

In this chapter, you will:

  • Display text using text() for static content and text! for reactive, localized strings
  • Style text with semantic fonts, weights, colors, and decorations
  • Build rich text with StyledStr, including Markdown and concatenation
  • Add syntax highlighting for source code

Text is the most fundamental building block in any user interface. Whether you are showing a headline, a label beside a toggle, or a paragraph of help text, you reach for the Text component first. WaterUI gives you a small two-function API: text() for plain content that does not change, and the text! macro for reactive strings that interpolate captured bindings and (optionally) consult a translation catalog.

Static text with text()

The simplest way to display text is the text() function. It accepts anything that converts into Text — a &'static str becomes localized through the catalog, while String and Str are used verbatim:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn greeting() -> impl View {
    text("Hello, World!")
}
}

Text sizes itself to fit its content and never stretches to fill extra space. When the available width is limited, it wraps to multiple lines automatically.

Layout behavior

Here is what you need to know about how Text participates in layout:

  • Sizing: fits its content naturally, like a label.
  • In stacks: takes only the space it needs, leaving room for siblings.
  • Wrapping: wraps when width is constrained — for example by a parent Frame.
#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn row() -> impl View {
    // Push two labels apart in a row.
    hstack((text("Name"), spacer(), text("Value")))
}
}

Tip: Because Text never stretches on its own, you can safely place it in any stack without worrying about it gobbling up space from sibling views.

Reactive text with text!

Static strings are fine for fixed labels, but most apps need text that updates in response to state. The text! macro captures any named placeholder from the surrounding scope and re-evaluates whenever those bindings change. When a i18n/<locale>.toml catalog is present, the same call site also resolves the matching translation:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn counter_label(count: &Binding<i32>) -> impl View {
    // Captures `count` from scope; the rendered text updates on every change.
    text!("Count: {count}")
}
}

The macro only accepts named placeholders. Either name a binding directly ({count}), or alias an expression with name = expr:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn welcome(get_name: impl Fn() -> String) -> impl View {
    text!("Hello, {name}", name = get_name())
}
}

Warning: text! does not accept positional {} placeholders. Writing text!("Count: {}", count) will not compile. Use a named placeholder ({count}) and capture the binding from scope, or pass an explicit alias (name = expr).

Why not .get() and format!?

Calling .get() on a binding inside a view body reads the current value once and never re-runs. The text would freeze at construction time. Always express formatting through text! so the framework tracks the dependency:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn show(value: &Binding<f64>) -> impl View {
    // Reactive: updates whenever `value` changes.
    text!("Value: {value:.2}")
}
}

Displaying arbitrary values

Any signal whose output type implements Display can be rendered with Text::display:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn show_price(price: &Binding<f64>) -> impl View {
    Text::display(price.clone())
}
}

Text::display maps the signal through to_string() internally, so the text updates whenever the signal does.

Locale-aware formatting

For specialised formatting — locale-specific dates or numbers — use Text::format:

#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::text::locale::Formatter;

fn formatted<T: 'static + Clone>(value: &Binding<T>, fmt: impl Formatter<T> + 'static) -> impl View {
    Text::format(value.clone(), fmt)
}
}

Implement the Formatter<T> trait for any type whose presentation depends on the active locale.

Translation files for text!

If your app supports multiple languages, place TOML files under i18n/ in the crate root:

# i18n/en.toml
"Count: {count}" = "Count: {count}"

# i18n/zh.toml
"Count: {count}" = "计数:{count}"

The macro picks the right translation based on the active Locale in the environment. Missing translation files are not an error — text! falls back to the format string itself.

Note: Plural forms use {#count} syntax in the format string and a TOML table with one/other keys. See waterui/macros/src/locale.rs for the full grammar.

Font system

WaterUI provides a semantic font system with six built-in presets. Each preset resolves to a platform-appropriate size and weight through the environment, so a “Larger Text” accessibility setting cascades into your screen automatically:

PresetDefault SizeDefault Weight
Body16ptNormal
Title24ptSemiBold
Headline32ptBold
Subheadline20ptSemiBold
Caption12ptNormal
Footnote10ptLight

Use the convenience methods on Text. They work on values produced by both text() and text!:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn typography() -> impl View {
    vstack((
        text("Page Title").title(),
        text("Main heading").headline(),
        text("Section header").sub_headline(),
        text("Body content").body(),
        text("Small note").caption(),
        text("Legal text").footnote(),
    ))
}
}

Custom font configuration

For fine-grained control, build a Font value and pass it to .font():

#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::text::font::{Font, FontWeight};

fn custom() -> impl View {
    text("Custom").font(
        Font::default()
            .size(18.0)
            .weight(FontWeight::Medium)
            .family("monospace"),
    )
}
}

Font weights

The FontWeight enum provides nine standard weights:

#![allow(unused)]
fn main() {
pub enum FontWeight {
    Thin,       // 100
    UltraLight, // 200
    Light,      // 300
    Normal,     // 400 (default)
    Medium,     // 500
    SemiBold,   // 600
    Bold,       // 700
    UltraBold,  // 800
    Black,      // 900
}
}

Size, weight, italic shortcuts

You do not always need to construct a Font. Text provides direct shortcuts. .size() and .weight() accept signals, so they can react:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn highlight(emphasised: &Binding<bool>) -> impl View {
    vstack((
        text("Large bold text").size(28.0).bold(),
        // Italic toggles reactively from the binding.
        text("May be italic").italic(emphasised.clone()),
    ))
}
}

Color

Colors are zero-sized marker types you can pass into .color() or .foreground(). The built-in palette includes Red, Blue, Green, Orange, Purple, Cyan, Yellow, Pink, and Grey:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn status() -> impl View {
    vstack((
        text("Error message").color(Red),
        text("Success").color(Green),
        text("Highlighted").background_color(Yellow),
    ))
}
}

Note: .color() on Text sets an explicit foreground color for that specific text view. The more general .foreground() modifier from ViewExt sets the inherited foreground for an entire view subtree, so children respect the cascade.

Text decorations

Underline

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn link_label(highlighted: &Binding<bool>) -> impl View {
    text("Click here").underline(highlighted.clone())
}
}

.underline() accepts any IntoSignal<bool>, so the decoration can toggle reactively.

Strikethrough

Strikethrough lives on StyledStr, not on Text:

#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::text::styled::StyledStr;

fn deprecated() -> impl View {
    text(StyledStr::plain("Deprecated").strikethrough(true))
}
}

Concatenating text

Sometimes you need mixed styles within a single line. Text implements Add and AddAssign, so styled fragments compose with +:

#![allow(unused)]
fn main() {
use waterui::prelude::*;

fn name_row() -> impl View {
    text("Name: ").bold() + text("Alice")
}
}

The resulting Text preserves the styling of each fragment.

Rich text with StyledStr

For full control over rich text, build a StyledStr directly. Each chunk carries its own Style, which includes font, foreground color, background color, italic, underline, and strikethrough:

#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::text::styled::{Style, StyledStr};

fn intro() -> impl View {
    let mut styled = StyledStr::empty();
    styled.push("Bold intro: ", Style::default().bold());
    styled.push("normal continuation", Style::default());
    text(styled)
}
}

Markdown shorthand

StyledStr::from_markdown parses a small subset of Markdown — headings, bold, italic, strikethrough, inline code, and paragraphs — into a styled string in one step:

#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::text::styled::StyledStr;

fn release_notes() -> impl View {
    text(StyledStr::from_markdown("**Bold** and *italic* with `code`"))
}
}

Syntax highlighting

If your app displays source code, WaterUI ships a syntect-backed highlighter. highlight_text is synchronous: it consumes a borrowed source string and a mutable highlighter, and returns a fully styled StyledStr:

#![allow(unused)]
fn main() {
use waterui::prelude::*;
use waterui::text::highlight::{DefaultHighlighter, Language, highlight_text};

fn code_view(source: &str) -> impl View {
    let mut highlighter = DefaultHighlighter::default();
    text(highlight_text(Language::Rust, source, &mut highlighter))
}
}

The Language enum covers Rust, Swift, Python, TypeScript, and many others. Each chunk in the resulting StyledStr carries the appropriate syntax color.

Quick reference

Method / FunctionPurpose
text("...")Static text, localized for &'static str
text!("Count: {n}")Reactive, localized text capturing n
Text::display(sig)Render any Signal<Output: Display>
Text::format(v, fmt)Locale-aware formatted text
.title()Apply the Title font preset
.headline()Apply the Headline font preset
.sub_headline()Apply the Subheadline font preset
.body()Apply the Body font preset
.caption()Apply the Caption font preset
.footnote()Apply the Footnote font preset
.size(f64)Set a custom font size (accepts signals)
.bold()Set font weight to Bold
.weight(w)Set a specific font weight (accepts signals)
.italic(sig)Toggle italic styling reactively
.color(c)Set the text foreground color
.background_color(c)Set the text background color
.underline(sig)Toggle underline reactively
.font(f)Apply a fully custom Font

Now that you can display and style text, it is time to learn how to arrange views on screen. In the next chapter, you will explore stacks, frames, grids, and the rest of the layout system.