# 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, 2009-2022 -- leonerd@leonerd.org.uk use Object::Pad 0.70 ':experimental(adjust_params)'; package Tickit::Widget 0.56; class Tickit::Widget :repr(HASH); use experimental 'postderef'; use Carp; use Scalar::Util qw( blessed weaken ); use List::Util 1.33 qw( all ); use Tickit::Pen; use Tickit::Style; use Tickit::Utils qw( textwidth ); use Tickit::Window 0.57; # $win->bind_event use Tickit::Event 0.66; # $info->type newapi use constant PEN_ATTR_MAP => { map { $_ => 1 } @Tickit::Pen::ALL_ATTRS }; use constant KEYPRESSES_FROM_STYLE => 0; use constant CAN_FOCUS => 0; =head1 NAME C - abstract base class for on-screen widgets =head1 DESCRIPTION This class acts as an abstract base class for on-screen widget objects. It provides the lower-level machinery required by most or all widget types. Objects cannot be directly constructed in this class. Instead, a subclass of this class which provides a suitable implementation of the C and other provided methods is derived. Instances in that class are then constructed. See the C section below. The core F distribution only contains a couple of simple widget classes. Many more widget types are available on CPAN. Almost certainly for any widget-based program you will want to at least install the L distribution, which provides many of the basic UI types of widget. =head1 STYLE The following style tags are used on all widget classes that use Style: =over 4 =item :focus Set when this widget has the input focus =back The following style actions are used: =over 4 =item focus_next_before () =item focus_next_after () Requests the focus move to the next or previous focusable widget in display order. =back =cut style_definition base => '' => "focus_next_after", '' => "focus_next_before"; =head1 CONSTRUCTOR =cut =head2 new $widget = Tickit::Widget->new( %args ) Constructs a new C object. Must be called on a subclass that implements the required methods; see the B section below. Any pen attributes present in C<%args> will be used to set the default values on the widget's pen object, other than the following: =over 8 =item class => STRING =item classes => ARRAY of STRING If present, gives the C class name or names applied to this widget. =item style => HASH If present, gives a set of "direct applied" style to the Widget. This is treated as an extra set of style definitions that apply more directly than any of the style classes or the default definitions. The hash should contain style keys, optionally suffixed by style tags, giving values. style => { 'fg' => 3, 'fg:active' => 5, } =back =cut field @_style_classes; field $_style_direct; field %_style_tag; ADJUST { my $class = ref $self; foreach my $method (qw( lines cols render_to_rb )) { $class->can( $method ) or croak "$class cannot ->$method - do you subclass and implement it?"; } } ADJUST :params ( :$class = undef, :$classes = [ $class ], ) { @_style_classes = $classes->@*; } ADJUST :params ( :$style = undef, %params ) { # Legacy direct-applied-style argument support foreach my $attr ( @Tickit::Pen::ALL_ATTRS ) { next unless defined( my $val = delete $params{$attr} ); carp "Applying legacy direct pen attribute '$attr' for ${\ref $self}"; $style->{$attr} = $val; } if( $style ) { my $tagset = $_style_direct = Tickit::Style::_Tagset->new; foreach my $key ( keys %$style ) { $tagset->add( $key, $style->{$key} ); } } $self->_update_pen( $self->get_style_pen ); } field $_parent :reader; field $_window :reader; field $_pen :reader; field $_focus_pending; field %_event_ids; =head1 METHODS =cut =head2 style_classes @classes = $widget->style_classes Returns a list of the style class names this Widget has. =cut method style_classes { return @_style_classes; } =head2 set_style_tag $widget->set_style_tag( $tag, $value ) Sets the (boolean) state of the named style tag. After calling this method, the C methods may return different results. No resizing or redrawing is necessarily performed; but the widget can use C, C or C to declare which style keys should cause automatic reshaping or redrawing. In addition it can override the C method to inspect the changes and decide for itself. =cut # This is cached, so will need invalidating on style loads my %KEYS_BY_TYPE_CLASS_TAG; Tickit::Style::on_style_load( sub { undef %KEYS_BY_TYPE_CLASS_TAG } ); method set_style_tag { my ( $tag, $value ) = @_; # Early-return on no change return if !$_style_tag{$tag} == !$value; # Work out what style keys might depend on this tag my %values; if( $_style_direct ) { KEYSET: foreach my $keyset ( $_style_direct->keysets ) { $keyset->tags->{$tag} or next KEYSET; $values{$_} ||= [] for keys $keyset->style->%*; } } my $type = $self->_widget_style_type; foreach my $class ( $self->style_classes, undef ) { my $keys = $KEYS_BY_TYPE_CLASS_TAG{$type}{$class//""}{$tag} ||= do { my $tagset = Tickit::Style::_ref_tagset( $type, $class ); my %keys; KEYSET: foreach my $keyset ( $tagset->keysets ) { $keyset->tags->{$tag} or next KEYSET; $keys{$_}++ for keys $keyset->style->%*; } [ keys %keys ]; }; $values{$_} ||= [] for @$keys; } my @keys = keys %values; my @old_values = $self->get_style_values( @keys ); $values{$keys[$_]}[0] = $old_values[$_] for 0 .. $#keys; $_style_tag{$tag} = !!$value; $self->_style_changed_values( \%values ); } method _style_tags { return join "|", sort grep { $_style_tag{$_} } keys %_style_tag; } =head2 get_style_values @values = $widget->get_style_values( @keys ) $value = $widget->get_style_values( $key ) Returns a list of values for the given keys of the currently-applied style. For more detail see the L documentation. Returns just one value in scalar context. =cut field %_style_cache; method get_style_values { my @keys = @_; my $type = $self->_widget_style_type; my @set = ( 0 ) x @keys; my @values = ( undef ) x @keys; my $cache = $_style_cache{$self->_style_tags} ||= {}; foreach my $i ( 0 .. $#keys ) { next unless exists $cache->{$keys[$i]}; $set[$i] = 1; $values[$i] = $cache->{$keys[$i]}; } my @classes = ( $self->style_classes, undef ); my $tagset = $_style_direct; while( !all { $_ } @set and @classes ) { # First time around this uses the direct style, if set. Thereafter uses # the style classes in order, finally the unclassed base. defined $tagset or $tagset = Tickit::Style::_ref_tagset( $type, shift @classes ); KEYSET: foreach my $keyset ( $tagset->keysets ) { $_style_tag{$_} or next KEYSET for keys $keyset->tags->%*; my $style = $keyset->style; foreach ( 0 .. $#keys ) { exists $style->{$keys[$_]} or next; $set[$_] and next; $values[$_] = $style->{$keys[$_]}; $set[$_] = 1; } } undef $tagset; # After all the classes, try again with type as "*" if( $type ne "*" and !@classes ) { $type = "*"; @classes = ( $self->style_classes, undef ); } } foreach my $i ( 0 .. $#keys ) { next if exists $cache->{$keys[$i]}; $cache->{$keys[$i]} = $values[$i]; } return @values if wantarray; return $values[0]; } =head2 get_style_pen $pen = $widget->get_style_pen( $prefix ) A shortcut to calling C to collect up the pen attributes, and form a L object from them. If C<$prefix> is supplied, it will be prefixed on the pen attribute names with an underscore (which would be read from the stylesheet file as a hyphen). Note that the returned pen instance is immutable, and may be cached. =cut field %_style_pen_cache; method get_style_pen { my $class = ref $self; my ( $prefix ) = @_; return $_style_pen_cache{$self->_style_tags}{$prefix//""} ||= do { my @keys = map { defined $prefix ? "${prefix}_$_" : $_ } @Tickit::Pen::ALL_ATTRS; my %attrs; @attrs{@Tickit::Pen::ALL_ATTRS} = $self->get_style_values( @keys ); Tickit::Pen::Immutable->new( %attrs ); }; } =head2 get_style_text $text = $widget->get_style_text A shortcut to calling C for a single key called C<"text">. =cut method get_style_text { my $class = ref $self; return $self->get_style_values( "text" ) // croak "$class style does not define text"; } =head2 set_style $widget->set_style( %defs ) Changes the widget's direct-applied style. C<%defs> should contain style keys optionally suffixed with tags in the same form as that given to the C