unit class Terminal::UI::Frame; use Terminal::UI::Pane; use Terminal::ANSI; use Terminal::ANSI::OO :t; use Terminal::UI::Utils; use Log::Async; logger.untapped-ok = True; method pod { $=pod } subset UInt of Int where * >= 0; #| Offset from the top of the screen. A maximized frame has top == 1. has UInt $.top = 1; #| Offset from the left of the screen. A maximized frame has left == 1. has UInt $.left = 1; #| Number of rows, including the top and bottom borders has UInt $.height is required; #| Number of columns has UInt $.width; #| List of rows with dividers in the frame. has UInt @.dividers; #| The panes for the frame. has Terminal::UI::Pane @.panes; #| The screen associated with the frame. has $.screen; #| Characters for drawing the frame border. has %.border = { indicator => '╢', side => '║', # '│', top => '═', # '─', bottom => '═', # '─', divider => '─', corners => < ╔ ╗ ╝ ╚ >, # <┌ ┐ ┘ └>, edges => <╟ ╢>, # < ├ ┤ > }; #| A function to compute an array of heights of panes, given the screen height. has $.height-computer; #| The focused pane has Terminal::UI::Pane $.focused; #| A name for this frame (optional) has $.name = 'unnamed'; #| The row of the bottom (top + height - 1) method bottom { $.top + $!height - 1; } #| The rightmost column (left + width - 1) method right { $.left + $.width - 1; } #| Add a divider to the frame at the given row (between 1 and height) method add-divider(UInt $line) { die "bad divider line ($line)" unless 1 < $line < $!height; @.dividers.push: $line; } #| Validate that the heights of the panes + the dividers add up method check(@panes) { my $pane-heights = sum @panes.map: *.height; if @panes > 1 && @.dividers != @panes - 1 { return "wrong number of dividers ({+@.dividers}) for panes ({+@panes})"; } if @panes == 1 && @.dividers > 0 { return "Only one pane, but there are dividers"; } if @panes > 1 { my $total = sum @panes.map: *.height; if $total + @.dividers + 2 != $.height { abort("height ($.height) does not match sum of pane heights ($total) + dividers ({+@.dividers}) + borders (2)"); } } return Nil } has $.border-color is rw = t.bright-white; method !color($str = '') { t.text-reset ~ $.border-color ~ $str; } #| Draw or refresh this frame method draw { atomically { print-at($.top, $.left, self!color); print-at($.top, $.left + 1, (%.border x ($.width - 2) )); print-at($.bottom, $.left + 1, (%.border x ($.width - 2) )); self.draw-side($.top + $_) for 1 .. $.height - 2; print-at($.top, $.left, (%.border[0])); print-at($.top, $.right, (%.border[1])); print-at($.bottom, $.right, (%.border[2])); print-at($.bottom, $.left, (%.border[3])); for @.dividers -> $d { print-at($.top + $d,$.left, (%.border x $.width)); print-at($.top + $d,$.left, (%.border[0])); print-at($.top + $d,$.right, (%.border[1])); } } } #| Draw only the sides, of a particular row method draw-side($h, Bool :$hl = False) { if $hl { print-at($h, $.left, self!color(%.border)); } else { print-at($h, $.left, self!color(%.border)); } print-at($h, $.right,self!color(%.border)); } #| Print a single line of output in the frame, including the borders. method print-line($h, $str) { print-at $h, self.left + 1, t.text-reset ~ (" " x self.width - 1); print-at $h, self.left + 1, $str; self.draw-side($h); } #| Create a single pane for this frame method add-pane { self.add-panes(heights => [ self.height - 2 ]); @.panes.tail; } #| Number of dividers. Will be number of panes - 1, when panes are added. has $.number-of-dividers is rw; #| Add multiple panes with the given height ratios multi method add-panes(:$ratios!, :$height-computer) { my $n = $ratios.elems; my $s = $ratios.cache.sum; debug "adding panes, ratios: {$ratios.raku}, sum $s"; $!number-of-dividers = $n - 1; $!height-computer = $height-computer // -> $available-rows { debug "computing heights, to add up to $available-rows"; my $base = $available-rows div $s; my @h = ($base xx $n) >>*>> @$ratios; debug "base $base heights are {@h.join(' ')}"; while @h.sum < $available-rows { @h[$++ % @h.elems]++; debug "adjusting. base $base heights are now {@h.join(' ')}"; } @h; } my @heights = $!height-computer(self.available-rows); debug "heights are now {@heights}"; self.add-panes(heights => @heights, :$height-computer) } method make-height-computer($in) { my $hc = -> |c { debug "computing!"; my $heights = $in.clone; my $have = $heights.grep(Int).sum; my $want = self.height - $heights.elems - 1; my $left = $want - $have; my $frs = $heights.grep(Pair).map(*.value).sum; my $fr-base = $left div $frs; my @changed; for @$heights.kv -> $i, $h is rw { if $h ~~ Pair { @changed.push: $i; $h = $fr-base * $h.value; } } while @$heights.sum < $want { @$heights[@changed[ $++ ]]++; } @$heights; } } #| Add multiple panes with the given heights, and optionally a callback for computing heights multi method add-panes(:$heights! is copy) { if $heights.grep(Pair) { my $hc = self.make-height-computer($heights); $heights = $hc(); $!height-computer = $hc; } $!number-of-dividers //= $heights.elems - 1; my $have = $heights.sum + $!number-of-dividers + 2; unless $have == self.height { abort("heights don't add up. heights: {$heights.join(' + ')} dividers: $!number-of-dividers + 2 borders = $have, != {self.height}"); } my @panes; my $at = 1; for @$heights -> $height { debug "Adding pane of height $height"; @panes.push: Terminal::UI::Pane.new(:frame(self), :$height, :top(self.top + $at)); $at += $height; debug "Adding divider at $at"; self.add-divider($at) if $at < self.height - 1; $at += 1; } self.check(@panes) andthen warning "$_"; @!panes = @panes; $!number-of-dividers = @panes - 1; @panes; } #| Number of available rows: height - 2 - (number of dividers - 1) method available-rows { exit note "Please set number-of-dividers" without self.number-of-dividers; my $a = $.height - 2 - (self.number-of-dividers); debug "available rows in frame $.height - 2 - {self.number-of-dividers} = $a"; $a; } #| Change focus to a particular pane in this frame method focus(Terminal::UI::Pane $pane) { $!focused = $pane; for @.panes -> $p { if $p === $pane { $p.focus; } else { $p.unfocus; } } $pane; } #| Handle a resize of the screen method handle-resize(:$from-width, :$from-height, :$to-width, :$to-height) { debug "resize from $from-width x $from-height to $to-width x $to-height"; $!height += $to-height - $from-height; $!width += $to-width - $from-width; without $!height-computer { debug "no height-computer"; for @.panes -> $p { $p.set-size( $p.width + ($to-width - $from-width) , $p.height ); } return; } my $at = 1; @.dividers = (); my @pane-heights = $!height-computer(self.available-rows); debug "recomputed heights: {@pane-heights.join(' ')}"; for @pane-heights -> $h { my $b = @.panes[$++]; $b.set-size( $b.width + ($to-width - $from-width) , $h ); $b.set-top( self.top + $at ); $at += $h; self.add-divider($at) if $at < self.height - 1; $at += 1; } self.check(@!panes) andthen warning "$_"; for @!panes { .reformat; .redraw; } self.draw; } #| If there is only one pane, return it. method pane { abort "ambiguous call to pane: {@.panes.elems} panes" unless @.panes==1; @.panes.first; } =NAME Terminal::UI::Frame -- A border, which may have several panes =begin DESCRIPTION A frame is like a window frame -- it represents the border, and may have several panes within it. =end DESCRIPTION