Selkie.git | lib/Selkie/ | Align.rakumod


=begin pod

=head1 NAME

Selkie::Align - Text alignment and box cross-axis alignment enums

=head1 SYNOPSIS

=begin code :lang<raku>

use Selkie::Align;
use Selkie::Widget::Text;
use Selkie::Layout::VBox;
use Selkie::Sizing;

# Centre a banner's text inside its own plane.
my $banner = Selkie::Widget::Text.new(
    text   => 'Selkie',
    align  => TextCenter,
    sizing => Sizing.fixed(1),
);
$banner.set-align(TextRight);

# Centre a fixed-width card inside a full-width column.
my $col = Selkie::Layout::VBox.new(sizing => Sizing.flex, align-items => CrossCenter);
$col.add: Selkie::Widget::Text.new(
    text         => 'a narrow card',
    cross-sizing => Sizing.fixed(20),   # 20 columns, centred in the VBox
    sizing       => Sizing.fixed(3),
);

=end code

=head1 DESCRIPTION

Two enums, kept in one module so every widget that positions something
inside a wider slot speaks the same language.

C<TextAlign> is about glyphs I<within> a widget's own plane:
L<Selkie::Widget::Text> offsets each wrapped line by
C<Text.align-column>. C<CrossAlign> is about widgets I<within> a
container: L<Selkie::Layout::VBox> and L<Selkie::Layout::HBox> place
each child along the axis they do B<not> stack on.

The two are orthogonal, and so is C<gap>: C<gap> reserves cells along
the I<main> axis (between children), alignment moves a child along the
I<cross> axis. A C<VBox> with C<gap =E<gt> 1> and
C<align-items =E<gt> CrossCenter> gets both, independently.

=head2 The cross axis

A C<VBox> stacks children top to bottom, so its main axis is rows and
its cross axis is B<columns>. An C<HBox> is the mirror image: main axis
columns, cross axis B<rows>. "Cross-axis alignment" therefore means
horizontal placement in a VBox and vertical placement in an HBox.

Two attributes drive it, and they answer different questions:

=item B<How big is the child on the cross axis?> C<Widget.cross-sizing>
— a L<Selkie::Sizing> resolved against the container's cross extent.
Undefined (the default) means "as big as the container", which is what
Selkie has always done.

=item B<Where does that size sit?> C<CrossAlign> — on the container as
C<align-items>, overridable per child as C<Widget.align-self>.

=begin code :lang<raku>

my $row = Selkie::Layout::HBox.new(sizing => Sizing.flex, align-items => CrossStart);

# Both are 1 row tall; the second opts out of the container's rule.
$row.add: $stamp;                     # cross-sizing => Sizing.fixed(1) — top
$row.add: $badge;                     # …and align-self => CrossEnd — bottom
$badge.set-align-self(CrossEnd);

=end code

=head2 Defaults are the old behaviour

C<CrossFill> plus an undefined C<cross-sizing> is exactly what VBox and
HBox did before alignment existed: every child gets the container's
full cross extent at offset 0. C<TextLeft> likewise puts every line at
column 0. Nothing in an existing app moves by a cell.

=head1 EXAMPLES

=head2 A centred, fixed-width login form

=begin code :lang<raku>

my $screen = Selkie::Layout::VBox.new(
    sizing      => Sizing.flex,
    gap         => 1,
    align-items => CrossCenter,
);
$screen.add: Selkie::Widget::Text.new(
    text         => 'Sign in',
    align        => TextCenter,
    cross-sizing => Sizing.fixed(40),
    sizing       => Sizing.fixed(1),
);
$screen.add: $username-row;    # cross-sizing => Sizing.fixed(40)
$screen.add: $password-row;    # cross-sizing => Sizing.fixed(40)

=end code

=head2 A right-aligned status column

=begin code :lang<raku>

my $col = Selkie::Layout::VBox.new(sizing => Sizing.flex, align-items => CrossEnd);
$col.add: Selkie::Widget::Text.new(
    text         => '3 unread',
    cross-sizing => Sizing.percent(50),   # half the column, flush right
    sizing       => Sizing.fixed(1),
);

=end code

Note the difference between the two ways of pushing text to the right:
C<TextRight> moves the I<glyphs> inside a full-width widget, while
C<CrossEnd> moves the I<widget> inside a wider container. Reach for
C<TextRight> when the widget's own background should span the row, and
for C<CrossEnd> when it shouldn't.

=head1 SEE ALSO

=item L<Selkie::Widget::Text> — C<align> / C<align-column>
=item L<Selkie::Layout::Allocate> — C<resolve-cross-extent>, C<cross-axis-offset>, C<effective-cross-align>
=item L<Selkie::Layout::VBox> / L<Selkie::Layout::HBox> — C<align-items>
=item L<Selkie::Sizing> — the fixed/percent/flex model C<cross-sizing> reuses
=item L<Selkie::BorderStyle> — C<TitleAlign>, the same idea for a frame's title

=end pod

unit module Selkie::Align;

#|( Horizontal placement of a line of text inside its widget's plane.

    C<TextLeft> is the default and the historical behaviour: every line
    starts at column 0. C<TextCenter> and C<TextRight> shift each line
    independently, so a wrapped paragraph comes out ragged-left rather
    than block-justified.

    Alignment is an I<offset>, never padding — Selkie does not write
    spaces on either side of the line. That keeps the untouched cells
    showing the plane's base cell, which is what makes aligned text
    work over a scrim or a gradient. )
enum TextAlign is export (
    TextLeft   => 'left',
    TextCenter => 'center',
    TextRight  => 'right',
);

#|( Placement of a child along its container's cross axis — columns in
    a C<VBox>, rows in an C<HBox>.

    C<CrossFill> is the default: the child spans the container's whole
    cross extent (unless it declares a C<cross-sizing>, which always
    wins on size; C<CrossFill> then places it at offset 0, like
    C<CrossStart>). C<CrossStart> is flush left / top, C<CrossEnd>
    flush right / bottom, and C<CrossCenter> splits the leftover space,
    rounding the leading side down. )
enum CrossAlign is export (
    CrossFill   => 'fill',
    CrossStart  => 'start',
    CrossCenter => 'center',
    CrossEnd    => 'end',
);