NAME
====

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

SYNOPSIS
========

```raku
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](Selkie--Align.md) `TextAlign` and defaults to `TextLeft`, which is where `Text` has always put its lines:

```raku
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](Selkie--Align.md).

EXAMPLES
========

A header and footer
-------------------

```raku
$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:

```raku
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
-----------------------------------------

```raku
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](Selkie--Align.md) — `TextAlign`, and the container-level `CrossAlign`

  * [Selkie::Widget::RichText](Selkie--Widget--RichText.md) — styled spans within one block of text

  * [Selkie::Widget::TextStream](Selkie--Widget--TextStream.md) — 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](Selkie--Align.md).

### 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

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

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

### method set-style

```raku
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

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

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

### method set-align

```raku
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

```raku
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

```raku
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

```raku
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

```raku
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.

