1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
|
# 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-2023 -- leonerd@leonerd.org.uk
use v5.20;
use warnings;
use Object::Pad 0.807;
package Tickit::Widget::Frame 0.42;
class Tickit::Widget::Frame :strict(params);
inherit Tickit::ContainerWidget;
apply Tickit::WidgetRole::SingleChildContainer;
use Tickit::Style;
use Tickit::WidgetRole::Alignable name => "title_align";
use Carp;
use Tickit::Pen;
use Tickit::Utils qw( textwidth substrwidth );
use Tickit::RenderBuffer qw( LINE_SINGLE LINE_DOUBLE LINE_THICK CAP_START CAP_END );
=head1 NAME
C<Tickit::Widget::Frame> - draw a frame around another widget
=head1 SYNOPSIS
use Tickit;
use Tickit::Widget::Frame;
use Tickit::Widget::Static;
my $frame = Tickit::Widget::Frame->new(
style => { linetype => "single" },
)
->set_child(
Tickit::Widget::Static->new(
text => "Hello, world",
align => "centre",
valign => "middle",
)
);
Tickit->new( root => $frame )->run;
=head1 DESCRIPTION
This container widget draws a frame around a single child widget.
=head1 STYLE
The default style pen is used as the widget pen. The following style pen
prefixes are also used:
=over 4
=item frame => PEN
The pen used to render the frame lines
=back
The following style keys are used:
=over 4
=item linetype => STRING
Controls the type of line characters used to draw the frame. Must be one of
the following names:
ascii single double thick solid_inside solid_outside
The C<ascii> linetype is default, and uses only the C<-|+> ASCII characters.
Other linetypes use Unicode box-drawing characters. These may not be supported
by all terminals or fonts.
=item linetype_top => STRING
=item linetype_bottom => STRING
=item linetype_left => STRING
=item linetype_right => STRING
Overrides the C<linetype> attribute for each side of the frame specifically.
If two line-drawing styles meet at corners they should be drawn correctly if
C<Tickit::RenderBuffer> can combine the line segments, but in other
circumstances the corners are drawn as extensions of the top or bottom line,
and the left and right lines do not meet it.
Any edge's linetype may be set to C<none> to cause that edge not to have a
line at all; no extra space will be consumed on that side.
=back
=cut
style_definition base =>
linetype => "ascii";
style_redraw_keys qw( linetype linetype_top linetype_bottom linetype_left linetype_right );
use constant WIDGET_PEN_FROM_STYLE => 1;
=head1 CONSTRUCTOR
=cut
=head2 new
$frame = Tickit::Widget::Frame->new( %args );
Constructs a new C<Tickit::Widget::Static> object.
Takes the following named arguments in addition to those taken by the base
L<Tickit::SingleChildWidget> constructor:
=over 8
=item title => STRING
Optional.
=item title_align => FLOAT|STRING
Optional. Defaults to C<0.0> if unspecified.
=back
For more details see the accessors below.
=cut
field $_title :reader :param = undef;
field %_has_edge;
ADJUST :params (
:$title_align = 0,
) {
$self->set_title_align( $title_align );
# Prepopulate has_* caches
$self->on_style_changed_values;
}
=head1 ACCESSORS
=cut
method on_style_changed_values
{
my %values = @_;
my $reshape = 0;
my $linetype = $values{linetype}[1] // $self->get_style_values( "linetype" );
# Cache these
foreach (qw( top bottom left right )) {
no warnings 'uninitialized'; # treat undef as false
my $new = ( $values{"linetype_$_"}[1] // $self->get_style_values( "linetype_$_") // $linetype )
ne "none";
$reshape = 1 if $_has_edge{$_} != $new;
$_has_edge{$_} = $new;
}
$self->reshape if $reshape;
}
method lines
{
my $child = $self->child;
return ( $child ? $child->requested_lines : 0 ) + $_has_edge{top} + $_has_edge{bottom};
}
method cols
{
my $child = $self->child;
return ( $child ? $child->requested_cols : 0 ) + $_has_edge{left} + $_has_edge{right};
}
use constant {
TOP => 0,
BOTTOM => 1,
LEFT => 2,
RIGHT => 3,
CORNER_TL => 4,
CORNER_TR => 5,
CORNER_BL => 6,
CORNER_BR => 7,
};
# Character numbers from
# http://en.wikipedia.org/wiki/Box-drawing_characters
my %LINECHARS = ( # TOP BOTTOM LEFT RIGHT TL TR BL BR
ascii => [ '-', '-', '|', '|', '+', '+', '+', '+' ],
solid_inside => [ map chr, 0x2584, 0x2580, 0x2590, 0x258C, 0x2597, 0x2596, 0x259D, 0x2598 ],
solid_outside => [ map chr, 0x2580, 0x2584, 0x258C, 0x2590, 0x259B, 0x259C, 0x2599, 0x259F ],
);
my %LINESTYLES = (
single => LINE_SINGLE,
double => LINE_DOUBLE,
thick => LINE_THICK,
);
=head2 title
$title = $frame->title;
=cut
# generated accessor
=head2 set_title
$frame->set_title( $title );
Accessor for the C<title> property, a string written in the top of the
frame.
=cut
method set_title
{
( $_title ) = @_;
$self->redraw;
}
=head2 title_align
=head2 set_title_align
$title_align = $frame->title_align;
$frame->set_title_align( $title_align );
Accessor for the C<title_align> property. Gives a vlaue in the range C<0.0> to
C<1.0> to align the title in the top of the frame.
See also L<Tickit::WidgetRole::Alignable>.
=cut
## This should come from Tickit::ContainerWidget
method children_changed { $self->reshape }
method reshape
{
my $window = $self->window or return;
my $child = $self->child or return;
my $lines = $window->lines;
my $cols = $window->cols;
my $extra_lines = $_has_edge{top} + $_has_edge{bottom};
my $extra_cols = $_has_edge{left} + $_has_edge{right};
if( $lines > $extra_lines and $cols > $extra_cols ) {
my @geom = ( $_has_edge{top}, $_has_edge{left}, $lines - $extra_lines, $cols - $extra_cols );
if( my $childwin = $child->window ) {
$childwin->change_geometry( @geom );
}
else {
my $childwin = $window->make_sub( @geom );
$child->set_window( $childwin );
}
}
else {
if( $child->window ) {
$child->set_window( undef );
}
}
}
method render_to_rb
{
my ( $rb, $rect ) = @_;
$rb->setpen( $self->get_style_pen( "frame" ) );
my $cols = $self->window->cols;
my $lines = $self->window->lines;
my $right = $cols - 1;
my $bottom = $lines - 1;
my $linetype = $self->get_style_values( "linetype" );
my $linetype_top = $self->get_style_values( "linetype_top" ) // $linetype;
my $linetype_bottom = $self->get_style_values( "linetype_bottom" ) // $linetype;
my $linetype_left = $self->get_style_values( "linetype_left" ) // $linetype;
my $linetype_right = $self->get_style_values( "linetype_right" ) // $linetype;
my $top_is_line = defined $LINESTYLES{$linetype_top};
my $bottom_is_line = defined $LINESTYLES{$linetype_bottom};
my $left_is_line = defined $LINESTYLES{$linetype_left};
my $right_is_line = defined $LINESTYLES{$linetype_right};
my $h_caps = ( $left_is_line ? 0 : CAP_START ) | ( $right_is_line ? 0 : CAP_END );
my $v_caps = ( $top_is_line ? 0 : CAP_START ) | ( $bottom_is_line ? 0 : CAP_END );
my $v_start = $top_is_line ? 0 : $_has_edge{top};
my $v_end = $bottom_is_line ? $bottom : $bottom - $_has_edge{bottom};
my $linechars;
my $style;
# Top
if( $rect->top == 0 ) {
if( $linechars = $LINECHARS{$linetype_top} ) {
$rb->goto( 0, 0 );
$rb->text( $linechars->[$linetype_top eq $linetype_left ? CORNER_TL : TOP] );
$rb->text( $linechars->[TOP] x ($cols - 2) ) if $cols > 2;
$rb->text( $linechars->[$linetype_top eq $linetype_right ? CORNER_TR : TOP] ) if $cols > 1;
}
elsif( $style = $LINESTYLES{$linetype_top} ) {
$rb->hline_at( 0, 0, $right, $style, undef, $h_caps );
}
if( defined( my $title = $self->title ) ) {
my $cols = $self->window->cols;
# At most we can fit $cols-4 columns of title
my ( $left, $titlewidth, $right ) = $self->_title_align_allocation( textwidth( $title ), $cols - 4 );
$rb->goto( 0, 1 + $left );
$rb->text( " " );
$rb->text( $title );
$rb->text( " " );
}
}
# Left
if( $rect->left == 0 ) {
if( $linechars = $LINECHARS{$linetype_left} ) {
$rb->text_at( $_, 0, $linechars->[LEFT] ) for 1 .. $bottom-1;
}
elsif( $style = $LINESTYLES{$linetype_left} ) {
$rb->vline_at( $v_start, $v_end, 0, $style, undef, $v_caps );
}
}
# Right
if( $rect->right == $cols and $cols > 1 ) {
if( $linechars = $LINECHARS{$linetype_right} ) {
$rb->text_at( $_, $right, $linechars->[RIGHT] ) for 1 .. $bottom-1;
}
elsif( $style = $LINESTYLES{$linetype_right} ) {
$rb->vline_at( $v_start, $v_end, $right, $style, undef, $v_caps );
}
}
# Bottom
if( $rect->bottom == $lines and $lines > 1 ) {
if( $linechars = $LINECHARS{$linetype_bottom} ) {
$rb->goto( $bottom, 0 );
$rb->text( $linechars->[$linetype_bottom eq $linetype_left ? CORNER_BL : BOTTOM] );
$rb->text( $linechars->[BOTTOM] x ($cols - 2) ) if $cols > 2;
$rb->text( $linechars->[$linetype_bottom eq $linetype_right ? CORNER_BR : BOTTOM] ) if $cols > 1;
}
elsif( $style = $LINESTYLES{$linetype_bottom} ) {
$rb->hline_at( $bottom, 0, $right, $style, undef, $h_caps );
}
}
}
=head1 TODO
=over 4
=item *
Specific pen for title. Layered on top of frame pen.
=item *
Caption at the bottom of the frame as well. Identical to title.
=item *
Consider if it's useful to provide accessors to apply extra padding inside the
frame, surrounding the child window.
=back
=head1 AUTHOR
Paul Evans <leonerd@leonerd.org.uk>
=cut
0x55AA;
|