Group
Extension

Term-Fabulous/lib/Term/Fabulous/Widget/Button.pm

package Term::Fabulous::Widget::Button;

use v5.32;
use warnings;
use feature 'signatures';
no warnings 'experimental::signatures';

use Object::Pad 0.825;

use Clay::UI::Role::Interaction::Disableable;
use Clay::UI::Role::Interaction::Focusable;
use Clay::UI::Role::Interaction::Hoverable;
use Clay::UI::Role::Interaction::Pressable;
use Term::Fabulous::Widget::Box;

our $VERSION = '0.05';

class Term::Fabulous::Widget::Button
	:isa(Term::Fabulous::Widget::Box)
	:does(Clay::UI::Role::Interaction::Focusable)
	:does(Clay::UI::Role::Interaction::Pressable)
	:does(Clay::UI::Role::Interaction::Hoverable)
	:does(Clay::UI::Role::Interaction::Disableable)
	:strict(params)
{
	use Clay::UI::Enum::Result;
	use Scalar::Util qw(refaddr);
	use Term::Fabulous::Check qw(color optional);
	use Term::Fabulous::Event::Activate;

	use constant REVERSE_VIDEO => 'reverse';

	my %ACTIVATES = map { $_ => 1 } qw(Enter Space);

	ADJUST {
		my $continue = Clay::UI::Enum::Result->CONTINUE;
		$self->on(
			OnRelease => sub ( $self, $event ) {
				$self->activate if refaddr( $event->target ) == refaddr($self) && $self->is_enabled;
				return $continue;
			},
			owner => $self
		);
		$self->on(
			KeyPress => sub ( $self, $event ) {
				return $continue unless $ACTIVATES{ $event->main_key_name // '' } && $self->is_enabled;
				$self->activate;
				return;
			},
			owner => $self
		);
	}

	# The pressed look: 'reverse', a color, or undef for no change.
	sub _pressed_look ( $owner, $name, $value ) {
		return REVERSE_VIDEO if defined $value && !ref $value && $value eq REVERSE_VIDEO;
		return optional( \&color, $owner, $name, $value );
	}

	method layout_properties :common () {
		return (
			$class->SUPER::layout_properties,
			can_focus => 'boolean',
			disabled  => 'boolean',
		);
	}

	# ---------------------------------------------------------------------
	# The theme: the button family, whose border, background and text
	# follow the state. A disabled Button is neither pressed nor focused.
	# ---------------------------------------------------------------------

	method theme_family :common () {
		return 'button';
	}

	# The looks of a focused, a pressed and a disabled Button come from the
	# theme unless given: focus_border_color (a color, or undef for no
	# focus look), pressed_background_color ('reverse', a color, or undef
	# for no change) and disabled_color.
	method themed_params :common () {
		return (
			$class->SUPER::themed_params,
			focus_border_color       => [ 'border.color', 'focused',  'optional_color' ],
			pressed_background_color => [ 'background',   'pressed',  \&_pressed_look ],
			disabled_color           => [ 'border.color', 'disabled', 'color' ],
		);
	}

	method look_state :override () {
		return
			 !$self->is_enabled ? 'disabled'
			: $self->is_pressed ? 'pressed'
			: $self->is_focused ? 'focused'
			: $self->is_hovered ? 'hovered'
			:                     'normal';
	}

	method disabled_color (@new) {
		return $self->look_value('disabled_color') unless @new;
		return $self->set_look( disabled_color => $new[0] );
	}

	# The color the Text widgets inside the Button are drawn in: the
	# disabled color while it is disabled (the given one grays the text as
	# well as the border), otherwise the text's own color or the theme's
	# for the button in its state (see Term::Fabulous::Widget::Text).
	method child_text_color ($explicit) {
		return $self->has_look_override('disabled_color') ? $self->look_value('disabled_color') : $self->look( 'text', 'disabled' ) unless $self->is_enabled;
		return $explicit // $self->look( 'text', $self->look_state );
	}

	method focus_border_color (@new) {
		return $self->look_value('focus_border_color') unless @new;
		return $self->set_look( focus_border_color => $new[0] );
	}

	method pressed_background_color (@new) {
		return $self->look_value('pressed_background_color') unless @new;
		return $self->set_look( pressed_background_color => $new[0] );
	}

	method reverse_video :override () {
		my $pressed = $self->look_value('pressed_background_color');
		return defined $pressed && !ref $pressed && $self->is_pressed ? 1 : 0;
	}

	method activate () {
		return $self->fire_event( Term::Fabulous::Event::Activate->new );
	}

}

1;

__END__

=head1 NAME

Term::Fabulous::Widget::Button - A box that can be clicked, focused and
activated

=head1 SYNOPSIS

	use Term::Fabulous::Widget::Button;
	use Term::Fabulous::Widget::Text;
	use Term::Fabulous::Enum::BorderStyle;
	use Clay::XS qw(sizing_fit);

	my $save = Term::Fabulous::Widget::Button->new(
		id               => 'save',
		background_color => [ 40, 60, 90, 255 ],
		bordered         => 1,
		border_color     => [ 90, 110, 140, 255 ],
		border_style     => Term::Fabulous::Enum::BorderStyle->Round,
		layout           => {
			sizing  => { width => sizing_fit(), height => sizing_fit() },
			padding => { left => 1, right => 1 },
		},
	);
	$save->add_child( Term::Fabulous::Widget::Text->new( text => 'Save', text_color => [ 255, 255, 255, 255 ] ) );

	# A click, or Enter or Space while the button has the focus.
	$save->on( Activate => sub ($event) { save_document(); return } );

=begin html

<p><img src="https://raw.githubusercontent.com/davenonymous/perl-term-fabulous/v0.05/screenshots/widget-button.svg" alt="Save, Cancel, Delete and Archive buttons: Save focused with a blue border, Archive disabled and drawn in gray, and the line Save was pressed"></p>

=end html

The program is F<examples/widgets/button.pl>.

=head1 DESCRIPTION

A Button is a L<Term::Fabulous::Widget::Box> that can take the keyboard
focus, notices when the mouse button is pressed and released over it,
and tracks whether the mouse pointer is over it. Put a
L<Term::Fabulous::Widget::Text> (or anything else) inside it as its
label.

A Button fires one event for everything that counts as pressing it:
C<Activate> (L<Term::Fabulous::Event::Activate>), for a click (the left
mouse button pressed and released over the Button) and for C<Enter> or
C<Space> while it has the focus. The lower-level events are still
there: C<OnPress> and C<OnRelease> for the mouse, C<KeyPress> for
every key.

A Button shows its state by itself:

=over

=item * while it is pressed (the left mouse button held down over it),
it is drawn in reverse video: the foreground and background colors of
every cell it paints, label included, are swapped. C<pressed_background_color>
replaces that with a background color, or switches it off;

=item * while it has the focus, its border is drawn in
C<focus_border_color>, the blue the input widgets use for their accent.
A Button without a border shows nothing: give it
C<bordered =E<gt> 1> (or let the theme give every button a border with
C<button.border.enabled>), and the theme supplies the style (C<Round>
in the built-in themes) and the color, or change its background in an
C<OnFocus> listener;

=item * hovering changes nothing by default. C<is_hovered> and the
C<OnHoverStart> and C<OnHoverStopped> events follow the pointer, so a
hover look is one listener away;

=item * while it is disabled (L</disabled>), its border and the
L<Term::Fabulous::Widget::Text> widgets inside it are drawn in
C<disabled_color>, the gray a disabled input draws its text in, and it
is never drawn focused or pressed.

=back

Under L<Term::Fabulous>, Tab and Shift+Tab move the focus to and from
Buttons, and pressing the left mouse button on a cell the Button paints
(its background or its border) focuses it; see L</MOUSE>.

=head1 CONSTRUCTOR

=head2 new

	my $button = Term::Fabulous::Widget::Button->new(%parameters);

All parameters are optional; unknown parameters die. A Button takes
every parameter of L<Term::Fabulous::Widget::Box> (C<id>, C<layout>,
C<background_color>, C<bordered>, C<border_color>, C<border_style>,
...; see L<Term::Fabulous::Widget/new>) plus:

=over

=item C<can_focus>

A boolean, stored as 1 or 0. Default: 1. With 0 the Button is skipped by Tab and
Shift+Tab, a click does not focus it, and
C<< $ui->interaction->set_focused_widget($button) >> dies. Mouse
clicks still fire C<OnPress>, C<OnRelease> and C<Activate>.

=item C<disabled>

A boolean, stored as 1 or 0. Default: 0. A disabled Button ignores
clicks, C<Enter> and C<Space> (no C<OnPress>, C<OnRelease> or
C<Activate>), cannot take the focus and is drawn disabled; see
L</disabled>.

=item C<disabled_color>

The color of the border and of the text of a disabled Button, in any
format L<Term::Fabulous::Color> accepts. Default: the theme's
C<button.border.color> in the C<disabled> state (the text takes
C<button.text> in that state), C<[ 108, 112, 120, 255 ]> in the
built-in dark theme. See L<Term::Fabulous::Manual::Looks/THEMES>.

=item C<focus_border_color>

The color of the border while the Button has the focus, in any format
L<Term::Fabulous::Color> accepts, or C<undef> for no focus look.
Default: the theme's C<button.border.color> in the C<focused> state,
the accent C<[ 97, 175, 239, 255 ]> in the built-in dark theme. It
only shows on the sides the Button has a border on (C<bordered>, or
the theme's C<button.border.enabled>) whose style, the theme's or the
widget's own, is not C<Blank>.

=item C<pressed_background_color>

What the Button looks like while it is pressed: the string C<reverse>
draws it in reverse video, a color in any format
L<Term::Fabulous::Color> accepts replaces the background with that
color, and C<undef> leaves the Button unchanged while pressed.
Default: the theme's C<button.background> in the C<pressed> state,
C<reverse> in the built-in themes.

The background, the border color and the border style of the Button
itself come from the theme's C<button> family when they are not
given (see L<Term::Fabulous::Widget/new>); so does the color of the
Text widgets inside it that have no C<text_color>. The three looks
above return to the theme with L<Term::Fabulous::Widget/reset_look>.

=back

=head1 METHODS

A Button has all methods of L<Term::Fabulous::Widget> plus these:

=head2 activate

	$button->activate;

Fires C<Activate> on the Button, as a click or C<Enter> would, and
returns what C<fire_event> returns. Use it to trigger a button from
code, for example from an application shortcut. It fires also while the
Button is disabled: only the user's clicks and keys are ignored then.

=head2 focus_border_color

	$button->focus_border_color('#ffffff');
	$button->focus_border_color(undef);

Accessor for the constructor parameter of the same name. Without an
argument it returns the color in use, the given one or the theme's,
as C<[r, g, b, a]> (or C<undef> for no focus look); with an argument
it sets the value and returns the stored form. An invalid color dies.

=head2 pressed_background_color

	$button->pressed_background_color('reverse');
	$button->pressed_background_color( [ 60, 90, 160, 255 ] );
	$button->pressed_background_color(undef);

Accessor for the constructor parameter of the same name. Returns the
look in use, the given one or the theme's: the string C<reverse>, a
C<[r, g, b, a]>, or C<undef>.

=head2 reverse_video

	my $swapped = $button->reverse_video;

1 while the Button is pressed and C<pressed_background_color> is
C<reverse>, 0 otherwise. The renderer calls it for every widget; see
L<Term::Fabulous::Manual::CustomWidgets/A box that takes the focus and reacts to the mouse>.

=head2 disabled

	$button->disabled(1);
	if ( $button->is_enabled ) { ... }

Accessor from L<Clay::UI::Role::Interaction::Disableable>. Returns 1 or
0; a write takes any plain boolean value and returns the new value.
Disabling a Button takes the focus away from it if it has it, and ends
a press that is in progress; C<is_enabled> is the opposite. The Button
also has the derived state C<disabled>.

=head2 disabled_color

	$button->disabled_color('#555555');

Accessor for the constructor parameter of the same name; returns the
color in use, the given one or the theme's, as C<[r, g, b, a]>. An
invalid color dies.

=head2 child_text_color

	my $color = $button->child_text_color($explicit_color);

The color a Text widget inside the Button is drawn in, given the color
the Text was given (or C<undef>): the disabled color while the Button
is disabled, otherwise the given color or the theme's C<button.text>
for the Button's state. L<Term::Fabulous::Widget::Text> asks its
nearest ancestor that has this method.

=head2 can_focus

	$button->can_focus(0);

Accessor. Reads whether the Button can take the focus now: 1 when the
last value written (through C<new>, a layout file or this accessor) was
true and the Button is enabled. A write records the value and returns
what reading returns now; turning it off takes the focus away from a
Button that has it. From L<Clay::UI::Role::Interaction::Focusable>.

=head2 is_focused

	if ( $button->is_focused ) { ... }

1 while the Button has the keyboard focus, 0 otherwise (also while it is
not part of a L<Term::Fabulous>). To give it the focus, call
C<< $ui->interaction->set_focused_widget($button) >>.

=head2 is_hovered

	if ( $button->is_hovered ) { ... }

1 while the mouse pointer is over the Button, as of the last frame.

=head2 is_pressed

	if ( $button->is_pressed ) { ... }

1 while the left mouse button, pressed over this Button, is held down
and the pointer is still over it.

=head1 EVENTS

All events are delivered to listeners registered with
C<< $button->on( $name => sub ($event) { ... } ) >>; see
L<Term::Fabulous::Manual::Events/EVENTS> for how listener return values decide
whether an event continues to the Button's ancestors.

=over

=item C<Activate> (L<Term::Fabulous::Event::Activate>)

The user activated the Button: a click (C<OnRelease> over the Button
after C<OnPress> on it), or C<Enter> (also the keypad's Enter) or
C<Space> without modifiers while the Button has the focus.
C<< $event->target >> is the Button. Fired by the Button itself, from its
own C<OnRelease> and C<KeyPress> listeners, which run before any
listener you add. So a listener on an ancestor sees C<Activate> before
the C<OnRelease> it came from bubbles up to it.

=item C<OnPress> (L<Clay::UI::Events::OnPress>)

The left mouse button went down over the Button. When Buttons are
nested, only the innermost one under the pointer gets it. C<< $event->x >>
and C<< $event->y >> are the pointer position in cells, as the center of
the cell (column + 0.5, row + 0.5). The event is fired while the next
frame is drawn, up to 1/30 second after the click.

=item C<OnRelease> (L<Clay::UI::Events::OnRelease>)

The left mouse button went up over the Button after it was pressed over
it: a completed click. Releasing elsewhere fires nothing. Carries C<x>
and C<y> like C<OnPress>.

=item C<OnFocus>, C<OnBlur> (L<Clay::UI::Events::OnFocus>, L<Clay::UI::Events::OnBlur>)

The Button got or lost the keyboard focus.

=item C<OnHoverStart>, C<OnHoverStopped> (L<Clay::UI::Events::OnHoverStart>, L<Clay::UI::Events::OnHoverStopped>)

The pointer entered or left the Button. These events do not bubble.

=item C<KeyPress> (L<Term::Fabulous::Event::KeyPress>)

A key was pressed while the Button had the focus. C<Enter> and
C<Space> are used by the Button (they fire C<Activate> and do not
bubble); every other key bubbles on to the ancestors.

=item C<Mouse> (L<Term::Fabulous::Event::Mouse>)

Any mouse event over the Button (press, release, drag, wheel), when the
Button paints the cell under the pointer (it needs a background color
or a border there).

=back

=head1 KEYS

C<Enter> (also the keypad's Enter) and C<Space> activate the focused
Button, unless it is disabled. With a modifier (C<Ctrl+Enter>,
C<Shift+Space>, ...) they bubble on like every other key. A disabled
Button cannot have the focus; keys fired at it from code bubble on. Tab and Shift+Tab always move the focus
away (see L<Term::Fabulous::Manual::Events/FOCUS>).

=head1 MOUSE

Pressing the left button on a cell the Button paints (its background
or its border) focuses the Button, unless C<can_focus> is 0. A disabled
Button is neither focused nor pressed by the mouse. A Button
without a background color paints only its border (or nothing), so the
cells in between are transparent: there the C<Mouse> event and the
focus go to the widget behind the Button. C<OnPress>, C<OnRelease> and
C<Activate> do not depend on painting: they fire for any cell inside
the Button's box.

Pressing fires C<OnPress>; releasing over the Button fires
C<OnRelease> and then C<Activate>. Releasing anywhere else cancels the
click. While the button is held with the pointer dragged off the
Button, C<is_pressed> is 0 and the pressed look disappears; dragging
back onto it makes it 1 again, and releasing there still fires
C<OnRelease> and C<Activate>.

=head1 KDL PROPERTIES

The properties of L<Term::Fabulous::Widget::Box/KDL PROPERTIES>, plus
C<can_focus> and C<disabled> (C<#true> or C<#false>),
C<focus_border_color> (a color string, or C<#null> for no focus look),
C<pressed_background_color> (a color string, C<"reverse">, or C<#null>
for no pressed look) and C<disabled_color> (a color string):

=for highlighter language=kdl

	use Term::Fabulous::Widget::Button as Button
	use Term::Fabulous::Widget::Text as Text

	Button "save" {
		background_color "#283c5a"
		border style=Round color="#5a6b8c"
		bordered #true
		padding left=1 right=1
		pressed_background_color "#3d5a85"
		Text { text "Save"; text_color "#ffffff"; }
	}

Listeners cannot be given in KDL; attach them in Perl after building
the layout.

=head1 SEE ALSO

L<Term::Fabulous::Widget::Box>, L<Term::Fabulous::Event::Activate>,
L<Term::Fabulous::Manual::Events/FOCUS>, L<Term::Fabulous::Manual::Events/MOUSE>,
L<Clay::UI::Role::Interaction::Pressable>,
L<Clay::UI::Role::Interaction::Focusable>,
L<Clay::UI::Role::Interaction::Hoverable>,
L<Term::Fabulous::Cookbook::KeyboardAndMouse/Add buttons for the mouse and the keyboard (Button)>,
the example programs F<examples/widgets/button.pl> and
F<examples/buttons-and-keys.pl>.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.