diff options
Diffstat (limited to 'lib/Tickit/Widget/Scroller.pm')
-rw-r--r-- | lib/Tickit/Widget/Scroller.pm | 1143 |
1 files changed, 1143 insertions, 0 deletions
diff --git a/lib/Tickit/Widget/Scroller.pm b/lib/Tickit/Widget/Scroller.pm new file mode 100644 index 0000000..a63c5ef --- /dev/null +++ b/lib/Tickit/Widget/Scroller.pm @@ -0,0 +1,1143 @@ +# You may distribute under the terms of either the GNU General Public License +# or the Artistic License (the same terms as Perl itself) +# +# (C) Paul Evans, 2011-2016 -- leonerd@leonerd.org.uk + +package Tickit::Widget::Scroller; + +use strict; +use warnings; +use base qw( Tickit::Widget ); +use Tickit::Style; +Tickit::Widget->VERSION( '0.35' ); +Tickit::Window->VERSION( '0.57' ); # ->bind_event + +use Tickit::Window; +use Tickit::Utils qw( textwidth ); +use Tickit::RenderBuffer; + +our $VERSION = '0.23'; + +use Carp; + +=head1 NAME + +C<Tickit::Widget::Scroller> - a widget displaying a scrollable collection of +items + +=head1 SYNOPSIS + + use Tickit; + use Tickit::Widget::Scroller; + use Tickit::Widget::Scroller::Item::Text; + + my $tickit = Tickit->new; + + my $scroller = Tickit::Widget::Scroller->new; + + $scroller->push( + Tickit::Widget::Scroller::Item::Text->new( "Hello world" ), + Tickit::Widget::Scroller::Item::Text->new( "Here are some lines" ), + map { Tickit::Widget::Scroller::Item::Text->new( "<Line $_>" ) } 1 .. 50, + ); + + $tickit->set_root_widget( $scroller ); + + $tickit->run + +=head1 DESCRIPTION + +This class provides a widget which displays a scrollable list of items. The +view of the items is scrollable, able to display only a part of the list. + +A Scroller widget stores a list of instances implementing the +C<Tickit::Widget::Scroller::Item> interface. + +=head1 STYLE + +The default style pen is used as the widget pen. + +The following style pen prefixes are used: + +=over 4 + +=item indicator => PEN + +The pen used for the scroll position indicators at the top or bottom of the +display + +=back + +=cut + +style_definition base => + indicator_rv => 1; + +use constant WIDGET_PEN_FROM_STYLE => 1; + +=head1 KEYBINDINGS + +The following keys are bound + +=over 2 + +=item * Down + +Scroll one line down + +=item * Up + +Scroll one line up + +=item * PageDown + +Scroll half a window down + +=item * PageUp + +Scroll half a window up + +=item * Ctrl-Home + +Scroll to the top + +=item * Ctrl-End + +Scroll to the bottom + +=back + +=cut + +=head1 CONSTRUCTOR + +=cut + +=head2 new + + $scroller = Tickit::Widget::Scroller->new( %args ) + +Constructs a new C<Tickit::Widget::Scroller> object. The new object will start +with an empty list of items. + +Takes the following named arguments: + +=over 8 + +=item gravity => STRING + +Optional. If given the value C<bottom>, resize events and the C<push> method +will attempt to preserve the item at the bottom of the screen. Otherwise, will +preserve the top. + +=item gen_top_indicator => CODE + +=item gen_bottom_indicator => CODE + +Optional. Generator functions for the top and bottom indicators. See also +C<set_gen_top_indicator> and C<set_gen_bottom_indicator>. + +=back + +=cut + +sub new +{ + my $class = shift; + my %args = @_; + + my $gravity = delete $args{gravity} || "top"; + + my $self = $class->SUPER::new( %args ); + + # We're going to cache window height because we need pre-resize height + # during resize event + $self->{window_lines} = undef; + + $self->{items} = []; + + $self->{start_item} = 0; + $self->{start_partial} = 0; + + $self->{gravity_bottom} = $gravity eq "bottom"; + + $self->set_on_scrolled( $args{on_scrolled} ) if $args{on_scrolled}; + + $self->set_gen_top_indicator( $args{gen_top_indicator} ); + $self->set_gen_bottom_indicator( $args{gen_bottom_indicator} ); + + return $self; +} + +=head1 METHODS + +=cut + +sub cols { 1 } +sub lines { 1 } + +sub _item +{ + my $self = shift; + my ( $idx ) = @_; + return $self->{items}[$idx]; +} + +sub _itemheight +{ + my $self = shift; + my ( $idx ) = @_; + return $self->{itemheights}[$idx] if defined $self->{itemheights}[$idx]; + return $self->{itemheights}[$idx] = $self->_item( $idx )->height_for_width( $self->window->cols ); +} + +sub reshape +{ + my $self = shift; + + my ( $itemidx, $itemline ) = $self->line2item( $self->{gravity_bottom} ? -1 : 0 ); + $itemline -= $self->_itemheight( $itemidx ) if $self->{gravity_bottom} and defined $itemidx; + + $self->SUPER::reshape; + + $self->{window_lines} = $self->window->lines; + + if( !defined $self->{window_cols} or $self->{window_cols} != $self->window->cols ) { + $self->{window_cols} = $self->window->cols; + + undef $self->{itemheights}; + $self->resized; + } + + if( defined $itemidx ) { + $self->scroll_to( $self->{gravity_bottom} ? -1 : 0, $itemidx, $itemline ); + } + elsif( $self->{gravity_bottom} ) { + $self->scroll_to_bottom; + } + else { + $self->scroll_to_top; + } + + $self->update_indicators; +} + +sub window_lost +{ + my $self = shift; + $self->SUPER::window_lost( @_ ); + + my ( $line, $offscreen ) = $self->item2line( -1, -1 ); + + $self->{pending_scroll_to_bottom} = 1 if defined $line; + + undef $self->{window_lines}; +} + +sub window_gained +{ + my $self = shift; + my ( $win ) = @_; + + $self->{window_lines} = $win->lines; + + $self->SUPER::window_gained( $win ); + + if( delete $self->{pending_scroll_to_bottom} ) { + $self->scroll_to_bottom; + } +} + +=head2 on_scrolled + +=head2 set_on_scrolled + + $on_scrolled = $scroller->on_scrolled + + $scroller->set_on_scrolled( $on_scrolled ) + +Return or set the CODE reference to be called when the scroll position is +adjusted. + + $on_scrolled->( $scroller, $delta ) + +This is invoked by the C<scroll> method, including the C<scroll_to>, +C<scroll_to_top> and C<scroll_to_bottom>. In normal cases it will be given the +delta offset that C<scroll> itself was invoked with, though this may be +clipped if this would scroll past the beginning or end of the display. + +=cut + +sub on_scrolled +{ + my $self = shift; + return $self->{on_scrolled}; +} + +sub set_on_scrolled +{ + my $self = shift; + ( $self->{on_scrolled} ) = @_; +} + +=head2 push + + $scroller->push( @items ) + +Append the given items to the end of the list. + +If the Scroller is already at the tail (that is, the last line of the last +item is on display) and the gravity mode is C<bottom>, the newly added items +will be displayed, possibly by scrolling downward if required. While the +scroller isn't adjusted by using any of the C<scroll> methods, it will remain +following the tail of the items, scrolling itself downwards as more are added. + +=cut + +sub push +{ + my $self = shift; + + my $items = $self->{items}; + + my $oldsize = @$items; + + push @$items, @_; + + if( my $win = $self->window and $self->window->is_visible ) { + my $added = 0; + $added += $self->_itemheight( $_ ) for $oldsize .. $#$items; + + my $lines = $self->{window_lines}; + + my $oldlast = $oldsize ? $self->item2line( $oldsize-1, -1 ) : -1; + + # Previous tail is on screen if $oldlast is defined and less than $lines + # If not, don't bother drawing or scrolling + return unless defined $oldlast and $oldlast < $lines; + + my $new_start = $oldlast + 1; + my $new_stop = $new_start + $added; + + if( $self->{gravity_bottom} ) { + # If there were enough spare lines, render them, otherwise scroll + if( $new_stop <= $lines ) { + $self->render_lines( $new_start, $new_stop ); + } + else { + $self->render_lines( $new_start, $lines ) if $new_start < $lines; + $self->scroll( $new_stop - $lines ); + } + } + else { + # If any new lines of content are now on display, render them + $new_stop = $lines if $new_stop > $lines; + if( $new_stop > $new_start ) { + $self->render_lines( $new_start, $new_stop ); + } + } + } + + $self->update_indicators; +} + +=head2 unshift + + $scroller->unshift( @items ) + +Prepend the given items to the beginning of the list. + +If the Scroller is already at the head (that is, the first line of the first +item is on display) and the gravity mode is C<top>, the newly added items will +be displayed, possibly by scrolling upward if required. While the scroller +isn't adjusted by using any of the C<scroll> methods, it will remain following +the head of the items, scrolling itself upwards as more are added. + +=cut + +sub unshift :method +{ + my $self = shift; + + my $items = $self->{items}; + + my $oldsize = @$items; + + my $oldfirst = $oldsize ? $self->item2line( 0, 0 ) : 0; + my $oldlast = $oldsize ? $self->item2line( -1, -1 ) : -1; + + unshift @$items, @_; + unshift @{ $self->{itemheights} }, ( undef ) x @_; + $self->{start_item} += @_; + + if( my $win = $self->window and $self->window->is_visible ) { + my $added = 0; + $added += $self->_itemheight( $_ ) for 0 .. $#_; + + # Previous head is on screen if $oldfirst is defined and non-negative + # If not, don't bother drawing or scrolling + return unless defined $oldfirst and $oldfirst >= 0; + + my $lines = $self->{window_lines}; + + if( $self->{gravity_bottom} ) { + # If the display wasn't yet full, scroll it down to display any new + # lines that are visible + my $first_blank = $oldlast + 1; + my $scroll_delta = $lines - $first_blank; + $scroll_delta = $added if $scroll_delta > $added; + if( $oldsize ) { + $self->scroll( -$scroll_delta ); + } + else { + $self->{start_item} = 0; + # TODO: if $added > $lines, need special handling + $self->render_lines( 0, $added ); + } + } + else { + # Scroll down by the amount added + if( $oldsize ) { + $self->scroll( -$added ); + } + else { + my $new_stop = $added; + $new_stop = $lines if $new_stop > $lines; + $self->{start_item} = 0; + $self->render_lines( 0, $new_stop ); + } + } + } + + $self->update_indicators; +} + +=head2 shift + + @items = $scroller->shift( $count ) + +Remove the given number of items from the start of the list and returns them. + +If any of the items are on display, the Scroller will be scrolled upwards an +amount sufficient to close the gap, ensuring the first remaining item is now +at the top of the display. + +The returned items may be re-used by adding them back into the scroller again +either by C<push> or C<unshift>, or may be discarded. + +=cut + +sub shift :method +{ + my $self = shift; + my ( $count ) = @_; + + defined $count or $count = 1; + + my $items = $self->{items}; + + croak '$count out of bounds' if $count <= 0; + croak '$count out of bounds' if $count > @$items; + + my ( $lastline, $offscreen ) = $self->item2line( $count - 1, -1 ); + + if( defined $lastline ) { + $self->scroll( $lastline + 1, allow_gap => 1 ); + # ->scroll implies $win->restore + } + + my @ret = splice @$items, 0, $count; + splice @{ $self->{itemheights} }, 0, $count; + $self->{start_item} -= $count; + + if( !defined $lastline and defined $offscreen and $offscreen eq "below" ) { + $self->scroll_to_top; + # ->scroll implies $win->restore + } + + $self->update_indicators; + + return @ret; +} + +=head2 pop + + @items = $scroller->pop( $count ) + +Remove the given number of items from the end of the list and returns them. + +If any of the items are on display, the Scroller will be scrolled downwards an +amount sufficient to close the gap, ensuring the last remaining item is now at +the bottom of the display. + +The returned items may be re-used by adding them back into the scroller again +either by C<push> or C<unshift>, or may be discarded. + +=cut + +sub pop :method +{ + my $self = shift; + my ( $count ) = @_; + + defined $count or $count = 1; + + my $items = $self->{items}; + + croak '$count out of bounds' if $count <= 0; + croak '$count out of bounds' if $count > @$items; + + my ( $firstline, $offscreen ) = $self->item2line( -$count, 0 ); + + if( defined $firstline ) { + $self->scroll( $firstline - $self->window->lines ); + } + + my @ret = splice @$items, -$count, $count; + splice @{ $self->{itemheights} }, -$count, $count; + + if( !defined $firstline and defined $offscreen and $offscreen eq "above" ) { + $self->scroll_to_bottom; + } + + $self->update_indicators; + + return @ret; +} + +=head2 scroll + + $scroller->scroll( $delta ) + +Move the display up or down by the given C<$delta> amount; with positive +moving down. This will be a physical count of displayed lines; if some items +occupy multiple lines, then fewer items may be scrolled than lines. + +=cut + +sub scroll +{ + my $self = shift; + my ( $delta, %opts ) = @_; + + return unless $delta; + + my $window = $self->window; + my $items = $self->{items}; + @$items or return; + + my $itemidx = $self->{start_item}; + my $partial = $self->{start_partial}; + my $scroll_amount = 0; + +REDO: + if( $partial > 0 ) { + $delta += $partial; + $scroll_amount -= $partial; + $partial = 0; + } + + while( $delta ) { + my $itemheight = $self->_itemheight( $itemidx ); + + if( $delta >= $itemheight ) { + $partial = $itemheight - 1, last if $itemidx == $#$items; + + $delta -= $itemheight; + $scroll_amount += $itemheight; + + $itemidx++; + } + elsif( $delta < 0 ) { + $partial = 0, last if $itemidx == 0; + $itemidx--; + + $itemheight = $self->_itemheight( $itemidx ); + + $delta += $itemheight; + $scroll_amount -= $itemheight; + } + else { + $partial = $delta; + $scroll_amount += $delta; + + $delta = 0; + } + } + + return if $itemidx == $self->{start_item} and + $partial == $self->{start_partial}; + + my $lines = $self->{window_lines}; + + if( $scroll_amount > 0 and !$opts{allow_gap} ) { + # We scrolled down. See if we've gone too far + my $line = -$partial; + my $idx = $itemidx; + + while( $line < $lines && $idx < @$items ) { + $line += $self->_itemheight( $idx ); + $idx++; + } + + if( $line < $lines ) { + my $spare = $lines - $line; + + $delta = -$spare; + goto REDO; + } + } + + $self->{start_item} = $itemidx; + $self->{start_partial} = $partial; + + if( abs( $scroll_amount ) < $lines ) { + $window->scroll( $scroll_amount, 0 ); + } + else { + $self->redraw; + } + + if( my $on_scrolled = $self->{on_scrolled} ) { + $self->$on_scrolled( $scroll_amount ); + } + + $self->update_indicators; +} + +=head2 scroll_to + + $scroller->scroll_to( $line, $itemidx, $itemline ) + +Moves the display up or down so that display line C<$line> contains line +C<$itemline> of item C<$itemidx>. Any of these counts may be negative to count +backwards from the display lines, items, or lines within the item. + +=cut + +sub scroll_to +{ + my $self = shift; + my ( $line, $itemidx, $itemline ) = @_; + + my $window = $self->window or return; + my $lines = $self->{window_lines}; + + my $items = $self->{items}; + @$items or return; + + if( $line < 0 ) { + $line += $lines; + + croak '$line out of bounds' if $line < 0; + } + else { + croak '$line out of bounds' if $line >= $lines; + } + + if( $itemidx < 0 ) { + $itemidx += @$items; + + croak '$itemidx out of bounds' if $itemidx < 0; + } + else { + croak '$itemidx out of bounds' if $itemidx >= @$items; + } + + my $itemheight = $self->_itemheight( $itemidx ); + + if( $itemline < 0 ) { + $itemline += $itemheight; + + croak '$itemline out of bounds' if $itemline < 0; + } + else { + croak '$itemline out of bounds' if $itemline >= $itemheight; + } + + $line -= $itemline; # now ignore itemline + + while( $line > 0 ) { + if( $itemidx == 0 ) { + $line = 0; + last; + } + + $itemheight = $self->_itemheight( --$itemidx ); + + $line -= $itemheight; + } + $itemline = -$line; # $line = 0; + + # Now we want $itemidx line $itemline to be on physical line 0 + + # Work out how far away that is + my $delta = 0; + my $i = $self->{start_item}; + + $delta -= $self->{start_partial}; + while( $itemidx > $i ) { + $delta += $self->_itemheight( $i ); + $i++; + } + while( $itemidx < $i ) { + $i--; + $delta -= $self->_itemheight( $i ); + } + $delta += $itemline; + + return if !$delta; + + $self->scroll( $delta ); +} + +=head2 scroll_to_top + + $scroller->scroll_to_top( $itemidx, $itemline ) + +Shortcut for C<scroll_to> to set the top line of display; where C<$line> is 0. +If C<$itemline> is undefined, it will be passed as 0. If C<$itemidx> is also +undefined, it will be passed as 0. Calling this method with no arguments, +therefore scrolls to the very top of the display. + +=cut + +sub scroll_to_top +{ + my $self = shift; + my ( $itemidx, $itemline ) = @_; + + defined $itemidx or $itemidx = 0; + defined $itemline or $itemline = 0; + + $self->scroll_to( 0, $itemidx, $itemline ); +} + +=head2 scroll_to_bottom + + $scroller->scroll_to_bottom( $itemidx, $itemline ) + +Shortcut for C<scroll_to> to set the bottom line of display; where C<$line> is +-1. If C<$itemline> is undefined, it will be passed as -1. If C<$itemidx> is +also undefined, it will be passed as -1. Calling this method with no +arguments, therefore scrolls to the very bottom of the display. + +=cut + +sub scroll_to_bottom +{ + my $self = shift; + my ( $itemidx, $itemline ) = @_; + + defined $itemidx or $itemidx = -1; + defined $itemline or $itemline = -1; + + $self->scroll_to( -1, $itemidx, $itemline ); +} + +=head2 line2item + + $itemidx = $scroller->line2item( $line ) + + ( $itemidx, $itemline ) = $scroller->line2item( $line ) + +Returns the item index currently on display at the given line of the window. +In list context, also returns the line number within item. If no window has +been set, or there is no item on display at that line, C<undef> or an empty +list are returned. C<$line> may be negative to count backward from the last +line on display; the last line taking C<-1>. + +=cut + +sub line2item +{ + my $self = shift; + my ( $line ) = @_; + + my $window = $self->window or return; + my $lines = $self->{window_lines}; + + my $items = $self->{items}; + + if( $line < 0 ) { + $line += $lines; + + croak '$line out of bounds' if $line < 0; + } + else { + croak '$line out of bounds' if $line >= $lines; + } + + my $itemidx = $self->{start_item}; + $line += $self->{start_partial}; + + while( $itemidx < @$items ) { + my $itemheight = $self->_itemheight( $itemidx ); + if( $line < $itemheight ) { + return $itemidx, $line if wantarray; + return $itemidx; + } + + $line -= $itemheight; + $itemidx++; + } + + return; +} + +=head2 item2line + + $line = $scroller->item2line( $itemidx, $itemline ) + + ( $line, $offscreen ) = $scroller->item2line( $itemidx, $itemline, $count_offscreen ) + +Returns the display line in the window of the given line of the item at the +given index. C<$itemidx> may be given negative, to count backwards from the +last item. C<$itemline> may be negative to count backward from the last line +of the item. + +In list context, also returns a value describing the offscreen nature of the +item. For items fully on display, this value is C<undef>. If the given line of +the given item is not on display because it is scrolled off either the top or +bottom of the window, this value will be either C<"above"> or C<"below"> +respectively. If C<$count_offscreen> is true, then the returned C<$line> value +will always be defined, even if the item line is offscreen. This will be +negative for items C<"above">, and a value equal or greater than the number of +lines in the scroller's window for items C<"below">. + +=cut + +sub item2line +{ + my $self = shift; + my ( $want_itemidx, $want_itemline, $count_offscreen ) = @_; + + my $window = $self->window or return; + my $lines = $self->{window_lines}; + + my $items = $self->{items}; + @$items or return; + + if( $want_itemidx < 0 ) { + $want_itemidx += @$items; + + croak '$itemidx out of bounds' if $want_itemidx < 0; + } + else { + croak '$itemidx out of bounds' if $want_itemidx >= @$items; + } + + my $itemheight = $self->_itemheight( $want_itemidx ); + + defined $want_itemline or $want_itemline = 0; + if( $want_itemline < 0 ) { + $want_itemline += $itemheight; + + croak '$itemline out of bounds' if $want_itemline < 0; + } + else { + croak '$itemline out of bounds' if $want_itemline >= $itemheight; + } + + my $itemidx = $self->{start_item}; + + my $line = -$self->{start_partial}; + + if( $want_itemidx < $itemidx or + $want_itemidx == $itemidx and $want_itemline < $self->{start_partial} ) { + if( wantarray and $count_offscreen ) { + while( $itemidx >= 0 ) { + if( $want_itemidx == $itemidx ) { + $line += $want_itemline; + last; + } + + $itemidx--; + $line -= $self->_itemheight( $itemidx ); + } + return ( $line, "above" ); + } + return ( undef, "above" ) if wantarray; + return; + } + + while( $itemidx < @$items and ( $line < $lines or $count_offscreen ) ) { + if( $want_itemidx == $itemidx ) { + $line += $want_itemline; + + last if $line >= $lines; + return $line; + } + + $line += $self->_itemheight( $itemidx ); + $itemidx++; + } + + return ( undef, "below" ) if wantarray and !$count_offscreen; + return ( $line, "below" ) if wantarray and $count_offscreen; + return; +} + +=head2 lines_above + + $count = $scroller->lines_above + +Returns the number of lines of content above the scrolled display. + +=cut + +sub lines_above +{ + my $self = shift; + my ( $line, $offscreen ) = $self->item2line( 0, 0, 1 ); + return 0 unless $offscreen; + return -$line; +} + +=head2 lines_below + + $count = $scroller->lines_below + +Returns the number of lines of content below the scrolled display. + +=cut + +sub lines_below +{ + my $self = shift; + my ( $line, $offscreen ) = $self->item2line( -1, -1, 1 ); + return 0 unless $offscreen; + return $line - $self->window->lines + 1; +} + +sub render_lines +{ + my $self = shift; + my ( $startline, $endline ) = @_; + + my $win = $self->window or return; + $win->expose( Tickit::Rect->new( + top => $startline, + bottom => $endline, + left => 0, + right => $win->cols, + ) ); +} + +sub render_to_rb +{ + my $self = shift; + my ( $rb, $rect ) = @_; + + my $win = $self->window; + my $cols = $win->cols; + + my $items = $self->{items}; + + my $line = 0; + my $itemidx = $self->{start_item}; + + if( my $partial = $self->{start_partial} ) { + $line -= $partial; + } + + my $startline = $rect->top; + my $endline = $rect->bottom; + + while( $line < $endline and $itemidx < @$items ) { + my $item = $self->_item( $itemidx ); + my $itemheight = $self->_itemheight( $itemidx ); + + my $top = $line; + my $firstline = ( $startline > $line ) ? $startline - $top : 0; + + $itemidx++; + $line += $itemheight; + + next if $firstline >= $itemheight; + + $rb->save; + { + my $lastline = ( $endline < $line ) ? $endline - $top : $itemheight; + + $rb->translate( $top, 0 ); + $rb->clip( Tickit::Rect->new( + top => $firstline, + bottom => $lastline, + left => 0, + cols => $cols, + ) ); + + $item->render( $rb, + top => 0, + firstline => $firstline, + lastline => $lastline - 1, + width => $cols, + height => $itemheight, + ); + + } + $rb->restore; + } + + while( $line < $endline ) { + $rb->goto( $line, 0 ); + $rb->erase( $cols ); + $line++; + } +} + +my %bindings = ( + Down => sub { $_[0]->scroll( +1 ) }, + Up => sub { $_[0]->scroll( -1 ) }, + + PageDown => sub { $_[0]->scroll( +int( $_[0]->window->lines / 2 ) ) }, + PageUp => sub { $_[0]->scroll( -int( $_[0]->window->lines / 2 ) ) }, + + 'C-Home' => sub { $_[0]->scroll_to_top }, + 'C-End' => sub { $_[0]->scroll_to_bottom }, +); + +sub on_key +{ + my $self = shift; + my ( $ev ) = @_; + + if( $ev->type eq "key" and my $code = $bindings{$ev->str} ) { + $code->( $self ); + return 1; + } + + return 0; +} + +sub on_mouse +{ + my $self = shift; + my ( $ev ) = @_; + + return unless $ev->type eq "wheel"; + + $self->scroll( 5 ) if $ev->button eq "down"; + $self->scroll( -5 ) if $ev->button eq "up"; +} + +=head2 set_gen_top_indicator + +=head2 set_gen_bottom_indicator + + $scroller->set_gen_top_indicator( $method ) + + $scroller->set_gen_bottom_indicator( $method ) + +Accessors for the generators for the top and bottom indicator text. If set, +each should be a CODE reference or method name on the scroller which will be +invoked after any operation that changes the contents of the window, such as +scrolling or adding or removing items. It should return a text string which, +if defined and non-empty, will be displayed in an indicator window. This will +be a small one-line window displayed at the top right or bottom right corner +of the Scroller's window. + + $text = $scroller->$method() + +The ability to pass method names allows subclasses to easily implement custom +logic as methods without having to capture a closure. + +=cut + +sub set_gen_top_indicator +{ + my $self = shift; + ( $self->{gen_top_indicator} ) = @_; + + $self->update_indicators; +} + +sub set_gen_bottom_indicator +{ + my $self = shift; + ( $self->{gen_bottom_indicator} ) = @_; + + $self->update_indicators; +} + +=head2 update_indicators + + $scroller->update_indicators + +Calls any defined generators for indicator text, and updates the indicator +windows with the returned text. This may be useful if the functions would +return different text now. + +=cut + +sub update_indicators +{ + my $self = shift; + + my $win = $self->window or return; + + for my $edge (qw( top bottom )) { + my $text_field = "${edge}_indicator_text"; + + my $text = $self->{"gen_${edge}_indicator"} ? $self->${ \$self->{"gen_${edge}_indicator"} } + : undef; + $text //= ""; + next if $text eq ( $self->{$text_field} // "" ); + + $self->{$text_field} = $text; + + if( !length $text ) { + $self->{"${edge}_indicator_win"}->hide if $self->{"${edge}_indicator_win"}; + undef $self->{"${edge}_indicator_win"}; + next; + } + + my $textwidth = textwidth $text; + my $line = $edge eq "top" ? 0 + : $win->lines - 1; + + my $floatwin; + if( $floatwin = $self->{"${edge}_indicator_win"} ) { + $floatwin->change_geometry( $line, $win->cols - $textwidth, 1, $textwidth ); + } + elsif( $self->window ) { + $floatwin = $win->make_float( $line, $win->cols - $textwidth, 1, $textwidth ); + $floatwin->bind_event( expose => sub { + my ( $win, undef, $info ) = @_; + $info->rb->text_at( 0, 0, + $self->{$text_field}, + $self->get_style_pen( "indicator" ) + ); + } ); + $self->{"${edge}_indicator_win"} = $floatwin; + } + + $floatwin->expose; + } +} + +=head1 TODO + +=over 4 + +=item * + +Abstract away the "item storage model" out of the actual widget. Implement +more storage models, such as database-driven ones.. more dynamic. + +=item * + +Keybindings + +=back + +=cut + +=head1 AUTHOR + +Paul Evans <leonerd@leonerd.org.uk> + +=cut + +0x55AA; |