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 frameThree things worth knowing:
Lines align individually. A wrapped paragraph under
TextCentercomes 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
Selkie::Align ā
TextAlign, and the container-levelCrossAlignSelkie::Widget::RichText ā styled spans within one block of text
Selkie::Widget::TextStream ā append-only log with ring buffer and auto-scroll
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 MuReplace the displayed text. Re-wraps and marks the widget dirty.
method set-style
method set-style(
Selkie::Style $s
) returns MuReplace 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 MuReplace the semantic theme slot used when style is not set.
method set-align
method set-align(
TextAlign:D $a
) returns NilChange 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 UIntThe 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 UIntNumber 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 MuRender 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 NilWrite 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.