Selkie--Widget--Text

NAME

Selkie::Widget::Text - Static styled text with word-wrap

SYNOPSIS

use Selkie::Widget::Text;
use Selkie::Style;
use Selkie::Sizing;

my $header = Selkie::Widget::Text.new(
    text   => ' My App',
    sizing => Sizing.fixed(1),
    style  => Selkie::Style.new(fg => 0x7AA2F7, bold => True),
);

# Mutate later
$header.set-text(' My App — logged in as Alice');

DESCRIPTION

A block of text rendered onto a single plane. Word-wraps automatically when the text exceeds the widget's width — words longer than the line are hard-broken at the character level.

Styled via the optional style attribute. If omitted, inherits the theme's text slot. Pass theme-slot for framework-built text that should follow a semantic theme slot such as overlay-title.

Text implements render-region(offset, height), so it plays correctly with Selkie::Widget::ScrollView for long content.

Alignment

align takes a Selkie::Align TextAlign and defaults to TextLeft, which is where Text has always put its lines:

my $banner = Selkie::Widget::Text.new(
    text   => 'Selkie',
    align  => TextCenter,
    sizing => Sizing.fixed(1),
);
$banner.set-align(TextRight);      # marks dirty; repaints next frame

Three things worth knowing:

  • Lines align individually. A wrapped paragraph under TextCenter comes out centred line by line — ragged on both sides, not block-justified.

  • Alignment is an offset, not padding. Selkie writes the line at a column and leaves the rest of the row untouched, so those cells keep showing the plane's base cell. Padding the line with spaces would paint this widget's background across the whole row, which is exactly wrong under a scrim, a gradient, or any transparent base.

  • A line wider than the widget starts at column 0 and clips on the right, under every alignment. Wrapping normally prevents this; a single-column widget holding multi-column text is the case that gets there.

Widths are counted in characters

Text measures every string with .chars — wrapping, alignment, logical-height, all of it. For Latin, Greek, Cyrillic and the like that is also the column count, so alignment lands where you expect.

It is not the column count for East Asian characters, most emoji, or anything else the terminal draws two cells wide: a centred line of CJK will sit roughly half its width too far left, because Selkie counted 10 characters where the terminal drew 20 columns. Combining marks go the other way — they cost a character but no column.

This is a whole-widget property, not an alignment quirk (wrapping has always had it), and fixing it means a real wcswidth-class width table. If your content is wide-character text and the ragged edge matters, size the widget to the text and let a container's align-items place the widget instead — that arithmetic is in cells, not characters. See Selkie::Align.

EXAMPLES

A header and footer

$vbox.add: Selkie::Widget::Text.new(
    text   => 'Selkie App',
    sizing => Sizing.fixed(1),
    style  => Selkie::Style.new(fg => 0x7AA2F7, bold => True),
);
$vbox.add: $main-content;
$vbox.add: Selkie::Widget::Text.new(
    text   => 'Ctrl+Q: quit  —  ?: help',
    sizing => Sizing.fixed(1),
    style  => Selkie::Style.new(fg => 0x888888),
);

Driven by the store

Set up a subscription that updates the text whenever state changes:

my $status = Selkie::Widget::Text.new(text => '', sizing => Sizing.fixed(1));
$app.store.subscribe-with-callback(
    'status-line',
    -> $s { "{$s.get-in('user', 'name') // 'guest'} — {$s.get-in('messages').elems} unread" },
    -> $text { $status.set-text($text) },
    $status,
);

A centred banner over a themed background

my $title = Selkie::Widget::Text.new(
    text   => 'Selkie',
    align  => TextCenter,
    sizing => Sizing.fixed(1),
    style  => Selkie::Style.new(fg => 0xCBA6F7, bold => True),
);

# The cells either side of the word are never written, so a gradient
# or scrim painted on the plane beneath shows through them.

SEE ALSO

has Str $.text

The text to render. Can include newlines — each line is wrapped independently.

has TextAlign $.align

Horizontal alignment of each wrapped line within the widget's width. Defaults to TextLeft — every line at column 0, the way Text has always rendered. Lines are aligned individually, so wrapped prose comes out ragged-left under TextCenter / TextRight rather than justified. See Selkie::Align.

has Selkie::Style $.style

Optional style override. If undefined, the theme's text slot is used.

has Str $.theme-slot

Optional theme slot name to use when style is not set.

method set-text

method set-text(
    Str:D $t
) returns Mu

Replace the displayed text. Re-wraps and marks the widget dirty.

method set-style

method set-style(
    Selkie::Style $s
) returns Mu

Replace the style override. Pass an undefined Selkie::Style to revert to the theme default.

method set-theme-slot

method set-theme-slot(
    Str $slot
) returns Mu

Replace the semantic theme slot used when style is not set.

method set-align

method set-align(
    TextAlign:D $a
) returns Nil

Change the horizontal alignment of the wrapped lines and mark the widget dirty. No-op when the alignment is unchanged, so calling it from a subscription callback every frame costs nothing.

method align-column

method align-column(
    TextAlign $a,
    Int $line-chars where { ... },
    Int $cols where { ... }
) returns UInt

The column a $line-chars-wide line starts at, in a $cols-wide widget, under alignment $a: =item TextLeft — 0. =item TextCenter — ((cols - chars) / 2).floor; an odd slack puts the extra column on the right. =item TextRight — cols - chars. Slack is floored at 0, so a line wider than the widget (which !rewrap only produces when the widget is one column wide and the text is not) starts at column 0 and clips on the right, rather than being pushed off the left edge. Exposed as a class method — no plane, no instance — so alignment arithmetic is testable and so custom widgets can reuse it: Selkie::Widget::Text.align-column(TextCenter, 5, 11); # 3

method logical-height

method logical-height() returns UInt

Number of lines the text wraps to at the current width. Used by ScrollView to compute scrollable extent.

method render-region

method render-region(
    Int :$offset where { ... },
    Int :$height where { ... }
) returns Mu

Render only a slice of the wrapped lines, starting at offset and going for height rows. Used by ScrollView for partial-viewport rendering.

method put-line

method put-line(
    Int $row where { ... },
    Str $line
) returns Nil

Write one wrapped line at its aligned column. Both render paths go through here so render-region — the slice ScrollView asks for — aligns identically to a full render. Alignment is an offset, never padding: nothing is written to the left or right of the line, so those cells keep showing the plane's base cell. Space-padding would paint this widget's background over them, which is visibly wrong under a scrim, a gradient, or any transparent base. Empty lines therefore write nothing at all.

Selkie v0.11.1

High-level TUI framework built on Notcurses

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

Notcurses::Native:ver<0.4.1+>:auth<zef:apogee>

Test Dependencies

Provides

  • Selkie
  • Selkie::Align
  • Selkie::Alpha
  • Selkie::App
  • Selkie::App::Internal::Animation
  • Selkie::App::Internal::Dispatch
  • Selkie::App::Internal::ErrorLog
  • Selkie::App::Internal::FocusTree
  • Selkie::App::Internal::HitTest
  • Selkie::App::Internal::IdleBudget
  • Selkie::App::Internal::OverlayTree
  • Selkie::App::Internal::RenderLoop
  • Selkie::App::Internal::ScreenModalLifecycle
  • Selkie::App::Internal::Terminal
  • Selkie::App::Internal::TerminalSequences
  • Selkie::BorderStyle
  • Selkie::Container
  • Selkie::EffectiveBounds
  • Selkie::Event
  • Selkie::Gradient
  • Selkie::Layout::Allocate
  • Selkie::Layout::HBox
  • Selkie::Layout::Split
  • Selkie::Layout::VBox
  • Selkie::Plot::Palette
  • Selkie::Plot::Scaler
  • Selkie::Plot::Ticks
  • Selkie::ScreenManager
  • Selkie::Sizing
  • Selkie::Store
  • Selkie::Style
  • Selkie::Test::Focus
  • Selkie::Test::Keys
  • Selkie::Test::Snapshot
  • Selkie::Test::Snapshot::Harness
  • Selkie::Test::Store
  • Selkie::Test::Supply
  • Selkie::Test::Tree
  • Selkie::Theme
  • Selkie::Trace
  • Selkie::Tree
  • Selkie::Tween
  • Selkie::Widget
  • Selkie::Widget::Axis
  • Selkie::Widget::BarChart
  • Selkie::Widget::Border
  • Selkie::Widget::Button
  • Selkie::Widget::CardList
  • Selkie::Widget::Checkbox
  • Selkie::Widget::CommandPalette
  • Selkie::Widget::ConfirmModal
  • Selkie::Widget::FileBrowser
  • Selkie::Widget::FocusableByDefault
  • Selkie::Widget::GradientFill
  • Selkie::Widget::Heatmap
  • Selkie::Widget::HelpOverlay
  • Selkie::Widget::Histogram
  • Selkie::Widget::Image
  • Selkie::Widget::Legend
  • Selkie::Widget::LineChart
  • Selkie::Widget::ListView
  • Selkie::Widget::Modal
  • Selkie::Widget::MultiLineInput
  • Selkie::Widget::PasswordStrength
  • Selkie::Widget::Plot
  • Selkie::Widget::ProgressBar
  • Selkie::Widget::RadioGroup
  • Selkie::Widget::RichText
  • Selkie::Widget::RichText::Span
  • Selkie::Widget::ScatterPlot
  • Selkie::Widget::ScrollView
  • Selkie::Widget::Select
  • Selkie::Widget::Sparkline
  • Selkie::Widget::Spinner
  • Selkie::Widget::TabBar
  • Selkie::Widget::Table
  • Selkie::Widget::Text
  • Selkie::Widget::TextInput
  • Selkie::Widget::TextInput::HighlightSpan
  • Selkie::Widget::TextStream
  • Selkie::Widget::Toast
  • Selkie::Widget::ViewportedCardList

Documentation

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.

Built with Podlite — the markup and publishing tools behind this site.