Selkie.git | lib/Selkie/ | BorderStyle.rakumod
=begin pod
=head1 NAME
Selkie::BorderStyle - Box-drawing glyph sets and title alignment for framed widgets
=head1 SYNOPSIS
=begin code :lang<raku>
use Selkie::BorderStyle;
use Selkie::Widget::Border;
# Pick a stock glyph set by kind.
my $panel = Selkie::Widget::Border.new(
title => 'Characters',
border-style => BorderRounded,
title-align => TitleCenter,
);
# Or hand it a bespoke glyph table.
my $dashed = Selkie::BorderStyle::BorderGlyphs.new(
top-left => '.', top-right => '.',
bottom-left => "'", bottom-right => "'",
horizontal => '-', vertical => ':',
);
$panel.set-border-glyphs($dashed);
=end code
=head1 DESCRIPTION
Two enums and a value class, shared by every widget that paints a frame.
C<BorderKind> names the five stock glyph sets; C<BorderGlyphs> is the
six-glyph table one of those names resolves to (or that you build
yourself); C<TitleAlign> says where a title sits along its edge.
The glyph tables, in C<top-left top-right bottom-left bottom-right
horizontal vertical> order:
=begin code
BorderSingle ┌ ┐ └ ┘ ─ │ ┌────────┐
BorderRounded ╭ ╮ ╰ ╯ ─ │ │ single │
BorderDouble ╔ ╗ ╚ ╝ ═ ║ └────────┘
BorderHeavy ┏ ┓ ┗ ┛ ━ ┃
BorderAscii + + + + - | ╭────────╮
│rounded │
╰────────╯
=end code
=head2 No automatic downgrade
Selkie never inspects the locale, C<TERM>, or the terminal's reported
capabilities to swap a Unicode glyph set for an ASCII one. Auto-downgrade
would make rendering — and therefore snapshot tests — depend on the
environment the process happens to run in, which is exactly the class of
bug snapshot tests exist to catch.
C<BorderAscii> is the escape hatch. If your app must run somewhere the
box-drawing block isn't available, decide that yourself (a config flag,
a C<--ascii> switch, a C<%*ENV> probe you own) and pass C<BorderAscii>:
=begin code :lang<raku>
my $kind = %*ENV<MYAPP_ASCII> ?? BorderAscii !! BorderRounded;
Selkie::Widget::Border.new(:title('Log'), border-style => $kind);
=end code
=head2 Custom glyph tables
Every glyph is a C<Str>, not a codepoint, so multi-codepoint clusters
work. Keep each one a single I<column> wide though — the frame painter
assumes one cell per glyph, and a double-width glyph (CJK, most emoji)
will push the frame out of alignment. Emitting a run of C<cols - 2>
horizontals is a single C<putstr>, so a wide horizontal glyph overflows
the right corner rather than being clipped.
=head1 EXAMPLES
=head2 Resolving a kind to its table
=begin code :lang<raku>
my $g = Selkie::BorderStyle::BorderGlyphs.for(BorderDouble);
say $g.top-left; # ╔
say $g.horizontal; # ═
# Resolution is cached — the same kind always returns the same object.
say BorderGlyphs.for(BorderDouble) === BorderGlyphs.for(BorderDouble); # True
=end code
=head2 Deriving a table from a stock one
C<BorderGlyphs> is immutable; C<clone> is the way to vary one glyph.
=begin code :lang<raku>
my $studded = BorderGlyphs.for(BorderSingle).clone(
top-left => '◤', top-right => '◥',
);
=end code
=head2 Aligning titles
=begin code :lang<raku>
$panel.set-title-align(TitleCenter);
$panel.set-bottom-title('↑/↓ scroll q quit');
$panel.set-bottom-title-align(TitleRight);
=end code
=head1 SEE ALSO
=item L<Selkie::Widget::Border> — the widget that consumes all of this
=item L<Selkie::Theme> — C<border> / C<border-focused> slots pick the frame's I<colours>; the glyph set is orthogonal
=end pod
unit module Selkie::BorderStyle;
#|( The five stock box-drawing glyph sets.
C<BorderSingle> is the default everywhere and the only one that was
available before Selkie 0.11. C<BorderRounded> is the same weight
with arc corners. C<BorderDouble> and C<BorderHeavy> read as
emphasis. C<BorderAscii> uses nothing outside 7-bit ASCII and is the
manual escape hatch for terminals or fonts without the box-drawing
block — Selkie never selects it for you (see the module Pod). )
enum BorderKind is export (
BorderSingle => 'single',
BorderRounded => 'rounded',
BorderDouble => 'double',
BorderHeavy => 'heavy',
BorderAscii => 'ascii',
);
#|( Where a title sits along the edge it's drawn on.
C<TitleLeft> is the historical (and default) placement: two columns
in from the left corner. C<TitleCenter> centres the decorated title
across the full width. C<TitleRight> ends it two columns short of
the right corner. All three are clamped so the corner glyphs are
never overwritten. )
enum TitleAlign is export (
TitleLeft => 'left',
TitleCenter => 'center',
TitleRight => 'right',
);
#|( The six glyphs it takes to paint a box: four corners, one
horizontal, one vertical.
Build one with C<.for(BorderKind)> for a stock set, C<.new> for a
bespoke table, or C<.clone> off a stock set to vary a glyph or two.
Instances are immutable value objects and safe to share between
widgets. )
class BorderGlyphs is export {
#| Top-left corner glyph, e.g. C<┌>.
has Str:D $.top-left is required;
#| Top-right corner glyph, e.g. C<┐>.
has Str:D $.top-right is required;
#| Bottom-left corner glyph, e.g. C<└>.
has Str:D $.bottom-left is required;
#| Bottom-right corner glyph, e.g. C<┘>.
has Str:D $.bottom-right is required;
#| Glyph tiled along the top and bottom edges, e.g. C<─>.
has Str:D $.horizontal is required;
#| Glyph drawn down the left and right edges, e.g. C<│>.
has Str:D $.vertical is required;
# Resolution cache. Stock sets are immutable, so handing out the
# same instance for repeated .for() calls is safe and keeps the
# per-render lookup in Border free of allocation.
my %cache{BorderKind};
#| The C<┌┐└┘─│> set. Selkie's default, and byte-identical to what
#| C<Border> painted before glyph sets existed.
method single(--> BorderGlyphs:D) {
BorderGlyphs.new(
top-left => '┌', top-right => '┐',
bottom-left => '└', bottom-right => '┘',
horizontal => '─', vertical => '│',
);
}
#| The C<╭╮╰╯─│> set — single-weight edges, arc corners.
method rounded(--> BorderGlyphs:D) {
BorderGlyphs.new(
top-left => '╭', top-right => '╮',
bottom-left => '╰', bottom-right => '╯',
horizontal => '─', vertical => '│',
);
}
#| The C<╔╗╚╝═║> set — double-ruled edges.
method double(--> BorderGlyphs:D) {
BorderGlyphs.new(
top-left => '╔', top-right => '╗',
bottom-left => '╚', bottom-right => '╝',
horizontal => '═', vertical => '║',
);
}
#| The C<┏┓┗┛━┃> set — heavy-weight edges.
method heavy(--> BorderGlyphs:D) {
BorderGlyphs.new(
top-left => '┏', top-right => '┓',
bottom-left => '┗', bottom-right => '┛',
horizontal => '━', vertical => '┃',
);
}
#| The C<+ + + + - |> set — 7-bit ASCII only.
method ascii(--> BorderGlyphs:D) {
BorderGlyphs.new(
top-left => '+', top-right => '+',
bottom-left => '+', bottom-right => '+',
horizontal => '-', vertical => '|',
);
}
#|( Resolve a C<BorderKind> to its glyph table. Cached: the same
kind always yields the very same object, so callers can compare
with C<===> and render loops don't allocate. )
method for(BorderKind:D $kind --> BorderGlyphs:D) {
%cache{$kind} //= do given $kind {
when BorderSingle { BorderGlyphs.single }
when BorderRounded { BorderGlyphs.rounded }
when BorderDouble { BorderGlyphs.double }
when BorderHeavy { BorderGlyphs.heavy }
when BorderAscii { BorderGlyphs.ascii }
default {
die "Selkie::BorderStyle: no glyph table for BorderKind '$kind'";
}
};
}
}