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',
);