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

widgets/text.rs

//! The `text` element, and the paragraph it becomes with spans: styled
//! runs shaped and wrapped as one flow — bold, italic and coloured runs
//! and emoji share lines and wrap mid-sentence — plus what a single run
//! can ask for on its own: a font family, decorations (underline,
//! strikethrough, a span background), a line height, `nowrap`,
//! `max_lines` with an `ellipsis`. Shrink the window and every paragraph
//! rewraps live.
//!
//! Nothing here is selectable: that is `features/selection`, which puts
//! a scope over exactly this kind of text.
//!
//! Run: cargo run -p kui-native --example text

use kui_devtools::Example;
use kui_native::{Align, App, FontFamily, NodeSpec, Span, TextStyle, Theme, Ui};

struct Text;

/// A card in the theme's own surface and edge.
fn card(t: &Theme, title: &str, ui: &mut Ui<'_>, body: impl FnOnce(&mut Ui<'_>)) {
    ui.with(
        NodeSpec::column()
            .grow_width()
            .max_width(560.0)
            .pad(24.0)
            .gap(12.0)
            .bg(t.surface)
            .radius(12.0)
            .border(1.0, t.border),
        |ui| {
            ui.text(title, TextStyle::new(12.0).color(t.muted));
            body(ui);
        },
    );
}

impl App for Text {
    fn view(&mut self, ui: &mut Ui<'_>) {
        let t = ui.theme();
        // Scrollable page: when the window is shorter than the content,
        // the wheel scrolls it.
        ui.with(
            NodeSpec::column()
                .fill()
                .cross_align(Align::Center)
                .pad(24.0)
                .gap(16.0)
                .scroll_y(),
            |ui| {
                card(
                    &t,
                    "SPANS · one paragraph, many styles, one shaping flow",
                    ui,
                    |ui| {
                        ui.rich_text(
                            &[
                                Span::new("Spans are "),
                                Span::new("plain data").bold().color(t.accent),
                                Span::new(", so every frontend — Rust, "),
                                Span::new("Lua").color(t.success),
                                Span::new(
                                    ", or C — can build them. The whole paragraph is shaped ",
                                ),
                                Span::new("together").italic(),
                                Span::new(
                                    ", which means wrapping crosses style boundaries correctly \
                                 instead of breaking at every run: ",
                                ),
                                Span::new("bold").bold(),
                                Span::new(" and "),
                                Span::new("italic").italic(),
                                Span::new(" and "),
                                Span::new("bold-italic").bold().italic().color(t.warning),
                                Span::new(
                                    " all sit on the same baselines. Emoji ride along via the \
                                 color-glyph atlas path: 🦀🔥✨",
                                ),
                            ],
                            TextStyle::new(16.0).line_height(26.0),
                        );
                        ui.rich_text(
                            &[
                                Span::new(
                                    "Per-span colour overrides the node colour at glyph level — ",
                                ),
                                Span::new("red").color(t.danger),
                                Span::new(", "),
                                Span::new("green").color(t.success),
                                Span::new(", "),
                                Span::new("blue").color(t.accent),
                                Span::new(" — while unstyled runs inherit it."),
                            ],
                            TextStyle::new(16.0).line_height(26.0).color(t.muted),
                        );
                        // A span in a face and a size of its own: inline
                        // code in the monospace face on a soft rounded
                        // wash, and a larger word that makes its line
                        // taller.
                        ui.rich_text(
                            &[
                                Span::new("Run "),
                                Span::new(" cargo test ")
                                    .mono()
                                    .bg(t.accent_soft)
                                    .bg_radius(4.0),
                                Span::new(" before you push; a span can also be "),
                                Span::new("larger").size(24.0).bold(),
                                Span::new(" than its paragraph, or "),
                                Span::new("smaller").size(11.0),
                                Span::new(", and the line is as tall as its tallest span."),
                            ],
                            TextStyle::new(16.0).line_height(26.0),
                        );
                    },
                );

                card(&t, "DECORATIONS · paint, not layout", ui, |ui| {
                    ui.rich_text(
                        &[
                            Span::new("An "),
                            Span::new("underline").underline(),
                            Span::new(", a "),
                            Span::new("strikethrough").strikethrough(),
                            Span::new(", a "),
                            Span::new("highlight").bg(t.accent_soft),
                            Span::new(" behind a span, and all three at once: "),
                            Span::new("marked")
                                .underline()
                                .strikethrough()
                                .bg(t.warning.with_alpha(0.25)),
                            Span::new(". A decorated span measures like a plain one."),
                        ],
                        TextStyle::new(16.0).line_height(26.0),
                    );
                });

                card(&t, "FAMILIES · the same run in each", ui, |ui| {
                    for (name, family) in [
                        ("sans", FontFamily::Sans),
                        ("serif", FontFamily::Serif),
                        ("mono", FontFamily::Mono),
                    ] {
                        // The row grows so the run can wrap at the card's
                        // edge; in a fit row a growing column has no width.
                        ui.with(
                            NodeSpec::row()
                                .grow_width()
                                .gap(12.0)
                                .cross_align(Align::Center),
                            |ui| {
                                ui.text_in(
                                    NodeSpec::row().width(44.0),
                                    name,
                                    TextStyle::new(11.0).color(t.muted),
                                );
                                ui.text_in(
                                    NodeSpec::column().grow_width(),
                                    "The quick brown fox jumps over the lazy dog 0123456789",
                                    TextStyle::new(15.0).family(family),
                                );
                            },
                        );
                    }
                });

                card(
                    &t,
                    "WRAP · nowrap, and max_lines with an ellipsis",
                    ui,
                    |ui| {
                        ui.text(
                            "A run that may not wrap keeps going past its box, and the box \
                         clips it or does not — nowrap is a promise about lines, not width.",
                            TextStyle::new(14.0).nowrap(),
                        );
                        ui.text(
                            "Two lines at most, and an ellipsis where the second one ends: a \
                         card title that has to stay a title however long the string behind \
                         it grows, which is what a list of files or messages needs from a \
                         label — the layout stays where it was and the text yields.",
                            TextStyle::new(14.0)
                                .line_height(20.0)
                                .max_lines(2)
                                .ellipsis(),
                        );
                        ui.text(
                            "Line height is the run's, too: this paragraph asks for 30 px per \
                         line and gets the air that comes with it, whatever the size of \
                         the glyphs on the line.",
                            TextStyle::new(14.0).line_height(30.0).color(t.muted),
                        );
                    },
                );
            },
        );
    }
}

impl Example for Text {}

kui_devtools::main!(Text);