File Coverage

blib/lib/Tickit/Widget/SegmentDisplay.pm
Criterion Covered Total %
statement 11 11 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 15 15 100.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2013-2015 -- leonerd@leonerd.org.uk
5              
6             package Tickit::Widget::SegmentDisplay;
7              
8 1     1   881 use strict;
  1         2  
  1         29  
9 1     1   5 use warnings;
  1         1  
  1         29  
10 1     1   31 use 5.010; # //
  1         3  
11 1     1   5 use base qw( Tickit::Widget );
  1         1  
  1         892  
12             use Tickit::Style;
13              
14             use utf8;
15              
16             our $VERSION = '0.04';
17              
18             use Carp;
19              
20             # The 7 segments are
21             # AAA
22             # F B
23             # F B
24             # GGG
25             # E C
26             # E C
27             # DDD
28             #
29             # B,C,E,F == 2cols wide
30             # A,D,G == 1line tall
31              
32             =encoding UTF-8
33              
34             =head1 NAME
35              
36             C - show a single character like a segmented display
37              
38             =head1 DESCRIPTION
39              
40             This class provides a widget that immitates a segmented LED or LCD display. It
41             shows a single character by lighting or shading fixed rectangular bars.
42              
43             =head1 STYLE
44              
45             The default style pen is used as the widget pen, though only the background
46             colour will actually matter as the widget does not directly display text.
47              
48             The following style keys are used:
49              
50             =over 4
51              
52             =item lit => COLOUR
53              
54             =item unlit => COLOUR
55              
56             Colour descriptions (index or name) for the lit and unlight segments of the
57             display.
58              
59             =back
60              
61             =cut
62              
63             style_definition base =>
64             lit => "red",
65             unlit => 16+36;
66              
67             use constant WIDGET_PEN_FROM_STYLE => 1;
68              
69             =head1 CONSTRUCTOR
70              
71             =cut
72              
73             =head2 new
74              
75             $segmentdisplay = Tickit::Widget::SegmentDisplay->new( %args )
76              
77             Constructs a new C object.
78              
79             Takes the following named arguments
80              
81             =over 8
82              
83             =item value => STR
84              
85             Sets an initial value.
86              
87             =item type => STR
88              
89             The type of display. Supported types are:
90              
91             =over 4
92              
93             =item seven
94              
95             A 7-segment bar display. The display can also be blanked with the value " ".
96              
97             =item seven_dp
98              
99             A 7-segment bar display with decimal-point. To light the decimal point, append
100             the value with ".".
101              
102             =item colon
103              
104             A static C<:>
105              
106             =item symb
107              
108             A unit, prefix symbol or other character. The following characters are
109             recognised:
110              
111             V A W Ω F H s
112             G M k m µ n p
113             + - %
114              
115             Each will be drawn in a style approximately to fit the general LED shape
116             display, by drawing lines of erased cells. Note however that some more
117             intricate shapes may not be very visible on smaller scales.
118              
119             =back
120              
121             =item use_unicode => BOOL
122              
123             If true, use Unicode block-drawing characters. If false, use only coloured
124             erase cells using the background colour.
125              
126             =item use_halfline => BOOL
127              
128             If true, vertical resolution of rendered block characters is effectively
129             doubled by using half-filled Unicode block-drawing characters. Setting this
130             option implies C.
131              
132             =item thickness => INT
133              
134             Gives the number of columns wide and half-lines tall that LED bars will be
135             drawn in. Note that unless C is set, this value ought to be an
136             even number. Defaults to 2.
137              
138             =back
139              
140             =cut
141              
142             my %types = (
143             seven => [qw( 7 )],
144             seven_dp => [qw( 7. )],
145             colon => [qw( : )],
146             symb => [],
147             );
148              
149             sub new
150             {
151             my $class = shift;
152             my %args = @_;
153             my $self = $class->SUPER::new( %args );
154              
155             my $type = $args{type} // "seven";
156             my $method;
157             foreach my $typename ( keys %types ) {
158             $type eq $typename and $method = $typename, last;
159             $type eq $_ and $method = $typename, last for @{ $types{$typename} };
160             }
161             defined $method or croak "Unrecognised type name '$type'";
162              
163             $self->{reshape_method} = $self->can( "reshape_${method}" );
164             $self->{render_method} = $self->can( "render_${method}_to_rb" );
165              
166             my $use_halfline = $args{use_halfline};
167             $self->{use_halfline} = $use_halfline;
168              
169             my $use_unicode = $args{use_unicode};
170              
171             $self->{flush_method} = $self->can(
172             $use_halfline ? "flush_halfline" :
173             $use_unicode ? "flush_unicode" :
174             "flush" );
175              
176             $self->{thickness} = $args{thickness} // 2;
177              
178             $self->{value} = $args{value} // "";
179              
180             $self->on_style_changed_values(
181             lit => [ undef, $self->get_style_values( "lit" ) ],
182             unlit => [ undef, $self->get_style_values( "unlit" ) ],
183             );
184              
185             return $self;
186             }
187              
188             # ADG + atleast 1 line each for FB and EC
189             sub lines { 3 + 2 }
190              
191             # FE, BC + atleast 2 columns for AGD
192             sub cols { 4 + 2 }
193              
194             =head1 ACCESSORS
195              
196             =cut
197              
198             =head2 value
199              
200             $value = $segmentdisplay->value
201              
202             $segmentdisplay->set_value( $value )
203              
204             Return or set the character on display
205              
206             =cut
207              
208             sub value
209             {
210             my $self = shift;
211             return $self->{value};
212             }
213              
214             sub set_value
215             {
216             my $self = shift;
217             ( $self->{value} ) = @_;
218             $self->redraw;
219             }
220              
221             sub on_style_changed_values
222             {
223             my $self = shift;
224             my %values = @_;
225              
226             $self->{lit_pen} = Tickit::Pen::Immutable->new( fg => $values{lit}[1] ) if $values{lit};
227             $self->{unlit_pen} = Tickit::Pen::Immutable->new( fg => $values{unlit}[1] ) if $values{unlit};
228             }
229              
230             sub reshape
231             {
232             my $self = shift;
233             my $win = $self->window or return;
234              
235             my $linescale = 1 + !!$self->{use_halfline};
236              
237             $self->{reshape_method}->( $self, $win->lines * $linescale, $win->cols, 0, 0 );
238             }
239              
240             use constant {
241             LIT => 0x01,
242             UNLIT => 0x02,
243             };
244              
245             sub render_to_rb
246             {
247             my $self = shift;
248             my ( $rb, $rect ) = @_;
249              
250             my @buff;
251              
252             # TODO: sizing?
253             $self->{render_method}->( $self, \@buff );
254              
255             $rb->eraserect( $rect );
256              
257             $self->{flush_method}->( $self, \@buff, $rb, $rect );
258             }
259              
260             sub flush
261             {
262             my $self = shift;
263             my ( $buff, $rb, $rect ) = @_;
264              
265             my $lit_pen = Tickit::Pen::Immutable->new( bg => $self->{lit_pen}->getattr( "fg" ) );
266             my $unlit_pen = Tickit::Pen::Immutable->new( bg => $self->{unlit_pen}->getattr( "fg" ) );
267              
268             foreach my $line ( $rect->linerange ) {
269             next unless defined( my $cells = $buff->[$line] );
270             foreach my $col ( $rect->left .. $rect->right - 1 ) {
271             my $val = vec( $cells, $col, 2 ) or next;
272             $rb->setpen( $val == LIT ? $lit_pen : $unlit_pen );
273             $rb->erase_at( $line, $col, 1 );
274             }
275             }
276             }
277              
278             use constant {
279             U_FULL => 0x2588,
280             U_UPPER => 0x2580,
281             U_LOWER => 0x2584,
282             };
283              
284             sub flush_unicode
285             {
286             my $self = shift;
287             my ( $buff, $rb, $rect ) = @_;
288              
289             my $lit_pen = $self->{lit_pen};
290             my $unlit_pen = $self->{unlit_pen};
291              
292             foreach my $line ( $rect->linerange ) {
293             next unless defined( my $cells = $buff->[$line] );
294             foreach my $col ( $rect->left .. $rect->right - 1 ) {
295             my $val = vec( $cells, $col, 2 ) or next;
296             $rb->setpen( $val == LIT ? $lit_pen : $unlit_pen );
297             $rb->char_at( $line, $col, U_FULL );
298             }
299             }
300             }
301              
302             sub flush_halfline
303             {
304             my $self = shift;
305             my ( $buff, $rb, $rect ) = @_;
306              
307             my $lit_pen = $self->{lit_pen};
308             my $unlit_pen = $self->{unlit_pen};
309              
310             my $both_pen = Tickit::Pen::Immutable->new(
311             fg => $lit_pen->getattr( 'fg' ),
312             bg => $unlit_pen->getattr( 'fg' ),
313             );
314              
315             foreach my $phyline ( $rect->linerange ) {
316             my $hicells = $buff->[$phyline*2];
317             my $locells = $buff->[$phyline*2 + 1];
318              
319             next unless defined $hicells or defined $locells;
320              
321             $hicells //= "";
322             $locells //= "";
323              
324             foreach my $col ( $rect->left .. $rect->right - 1 ) {
325             my $hival = vec( $hicells, $col, 2 );
326             my $loval = vec( $locells, $col, 2 );
327              
328             $hival or $loval or next;
329              
330             if( $hival == $loval ) {
331             $rb->setpen( ( $hival || $loval ) == LIT ? $lit_pen : $unlit_pen );
332             $rb->char_at( $phyline, $col, U_FULL );
333             }
334             elsif( !$hival or !$loval ) {
335             $rb->setpen( ( $hival || $loval ) == LIT ? $lit_pen : $unlit_pen );
336             $rb->char_at( $phyline, $col, $hival ? U_UPPER : U_LOWER );
337             }
338             else {
339             # Half lit, half unlit
340             $rb->setpen( $both_pen );
341             $rb->char_at( $phyline, $col, $hival == LIT ? U_UPPER : U_LOWER );
342             }
343             }
344             }
345             }
346              
347             sub _fill
348             {
349             my $self = shift;
350             my ( $buff, $startline, $endline, $startcol, $endcol, $val ) = @_;
351             $val //= LIT;
352              
353             my $thickness = $self->{thickness};
354             my @colrange = ( $startcol .. $endcol + $thickness - 1 );
355              
356             $thickness /= 2 unless $self->{use_halfline};
357             my @linerange = ( $startline .. $endline + $thickness - 1 );
358              
359             foreach my $line ( @linerange ) {
360             vec( $buff->[$line], $_, 2 ) = $val for @colrange;
361             }
362             }
363              
364             sub _dot
365             {
366             my $self = shift;
367             my ( $buff, $line, $col, $val ) = @_;
368             $self->_fill( $buff, $line, $line, $col, $col, $val );
369             }
370              
371             # 7-Segment
372             my %segments = (
373             ' ' => " ",
374             0 => "ABCDEF ",
375             1 => " BC ",
376             2 => "AB DE G",
377             3 => "ABCD G",
378             4 => " BC FG",
379             5 => "A CD FG",
380             6 => "A CDEFG",
381             7 => "ABC ",
382             8 => "ABCDEFG",
383             9 => "ABCD FG",
384             );
385              
386             sub _val_for_seg
387             {
388             my $self = shift;
389             my ( $segment ) = @_;
390              
391             my $segments = $segments{$self->value} or return UNLIT;
392              
393             my $lit = substr( $segments, ord($segment) - ord("A"), 1 ) ne " ";
394             return $lit ? LIT : UNLIT;
395             }
396              
397             sub reshape_seven
398             {
399             my $self = shift;
400             my ( $lines, $cols, $top, $left ) = @_;
401              
402             my $thickness = $self->{thickness};
403              
404             my $right = $left + $cols - $thickness;
405              
406             $self->{FE_col} = $left;
407             $self->{AGD_startcol} = $left + $thickness;
408             $self->{AGD_endcol} = $right - $thickness;
409             $self->{BC_col} = $right;
410              
411             $thickness /= 2 unless $self->{use_halfline};
412              
413             my $bottom = $top + $lines - $thickness;
414             my $mid = int( ( $top + $bottom ) / 2 );
415              
416             $self->{A_line} = $top;
417             $self->{BF_startline} = $top + $thickness;
418             $self->{BF_endline} = $mid - $thickness;
419             $self->{G_line} = $mid;
420             $self->{CE_startline} = $mid + $thickness;
421             $self->{CE_endline} = $bottom - $thickness;
422             $self->{D_line} = $bottom;
423             }
424              
425             sub render_seven_to_rb
426             {
427             my $self = shift;
428             my ( $buff ) = @_;
429              
430             $self->_fill( $buff, ( $self->{A_line} ) x 2, $self->{AGD_startcol}, $self->{AGD_endcol}, $self->_val_for_seg( "A" ) );
431             $self->_fill( $buff, ( $self->{G_line} ) x 2, $self->{AGD_startcol}, $self->{AGD_endcol}, $self->_val_for_seg( "G" ) );
432             $self->_fill( $buff, ( $self->{D_line} ) x 2, $self->{AGD_startcol}, $self->{AGD_endcol}, $self->_val_for_seg( "D" ) );
433              
434             $self->_fill( $buff, $self->{BF_startline}, $self->{BF_endline}, ( $self->{FE_col} ) x 2, $self->_val_for_seg( "F" ) );
435             $self->_fill( $buff, $self->{BF_startline}, $self->{BF_endline}, ( $self->{BC_col} ) x 2, $self->_val_for_seg( "B" ) );
436             $self->_fill( $buff, $self->{CE_startline}, $self->{CE_endline}, ( $self->{FE_col} ) x 2, $self->_val_for_seg( "E" ) );
437             $self->_fill( $buff, $self->{CE_startline}, $self->{CE_endline}, ( $self->{BC_col} ) x 2, $self->_val_for_seg( "C" ) );
438             }
439              
440             # 7-Segment with DP
441             sub reshape_seven_dp
442             {
443             my $self = shift;
444             my ( $lines, $cols, $top, $left ) = @_;
445              
446             $self->reshape_seven( $lines, $cols - 2, $top, $left );
447              
448             $self->{DP_line} = $top + $lines - 1;
449             $self->{DP_col} = $left + $cols - 2;
450             }
451              
452             sub render_seven_dp_to_rb
453             {
454             my $self = shift;
455             my ( $buff ) = @_;
456              
457             my $value = $self->{value};
458             my $dp;
459             local $self->{value};
460              
461             if( $value =~ m/^(\d?)(\.?)/ ) {
462             $self->{value} = $1;
463             $dp = length $2;
464             }
465             else {
466             $self->{value} = $value;
467             }
468              
469             $self->render_seven_to_rb( $buff );
470              
471             $self->_dot( $buff, $self->{DP_line}, $self->{DP_col}, $dp ? LIT : UNLIT );
472             }
473              
474             # Static double-dot colon
475             sub reshape_colon
476             {
477             my $self = shift;
478             my ( $lines, $cols, $top, $left ) = @_;
479             my $bottom = $top + $lines - 1;
480              
481             $self->{colon_col} = 2 + int( ( $cols - 4 ) / 2 );
482              
483             my $ofs = int( ( $lines - 1 + 0.5 ) / 4 );
484              
485             $self->{A_line} = $top + $ofs;
486             $self->{B_line} = $bottom - $ofs;
487             }
488              
489             sub render_colon_to_rb
490             {
491             my $self = shift;
492             my ( $buff ) = @_;
493              
494             my $col = $self->{colon_col};
495             $self->_dot( $buff, $self->{A_line}, $col );
496             $self->_dot( $buff, $self->{B_line}, $col );
497             }
498              
499             # Symbol drawing
500             #
501             # Each symbol is drawn as a series of erase calls on the RB to draw 'lines'.
502              
503             my %symbol_strokes = do {
504             no warnings 'qw'; # Quiet the 'Possible attempt to separate words with commas' warning
505              
506             # Letters likely to be used for units
507             V => [ [qw( 0,0 50,100 100,0 )] ],
508             A => [ [qw( 0,100 50,0 100,100 )], [qw( 20,70 80,70)] ],
509             W => [ [qw( 0,0 25,100 50,50 75,100 100,0)] ],
510             Ω => [ [qw( 0,100 25,100 25,75 10,60 0,50 0,20 20,0 80,0 100,20 100,50 90,60 75,75 75,100 100,100 ) ] ],
511             F => [ [qw( 0,100 0,0 100,0 )], [qw( 0,50 80,50 )] ],
512             H => [ [qw( 0,0 0,100 )], [qw( 0,50 100,50 )], [qw( 100,0 100,100 )] ],
513             s => [ [qw( 100,50 75,40 25,40 0,50 0,60 25,70 75,70 100,80 100,90 75,100 25,100 0,90 )] ],
514              
515             # Symbols likely to be used as SI prefixes
516             G => [ [qw( 100,25 65,0 35,0 0,25 0,75 35,100 65,100 100,75 100,50 55,50 )] ],
517             M => [ [qw( 0,100 0,0 50,50 100,0 100,100 )] ],
518             k => [ [qw( 10,0 10,100 )], [qw( 90,40 10,70 90,100 )] ],
519             m => [ [qw( 0,100 0,50 10,40 40,40 50,50 50,100 )], [qw( 50,50 60,40 90,40 100,50 100,100 )] ],
520             µ => [ [qw( 0,100 0,40 )], [qw( 0,80 70,80 80,75 90,60 100,40 )] ],
521             n => [ [qw( 0,100 0,40 )], [qw( 0,50 30,40 70,40 100,50 100,100 )] ],
522             p => [ [qw( 0,100 0,40 )], [qw( 0,55 30,40 70,40 100,55 100,60 70,80 30,80 0,60 )] ],
523              
524             # Mathematical symbols
525             '+' => [ [qw( 10,50 90,50 )], [qw( 50,30 50,70 )] ],
526             '-' => [ [qw( 10,50 90,50 )] ],
527             '%' => [ [qw( 10,10 10,30 30,30 30,10 10,10 )], [qw( 20,100 80,00 )], [qw( 70,70 70,90 90,90 90,70 70,70 )] ],
528             };
529              
530             sub reshape_symb
531             {
532             my $self = shift;
533             my ( $lines, $cols, $top, $left ) = @_;
534              
535             $self->{mid_line} = int( ( $lines - 1 ) / 2 );
536             $self->{mid_col} = int( ( $cols - 2 ) / 2 );
537              
538             $self->{Y_to_line} = ( $lines - 1 ) / 100;
539             $self->{X_to_col} = ( $cols - 2 ) / 100;
540             }
541              
542             sub _roundpos
543             {
544             my $self = shift;
545             my ( $l, $c ) = @_;
546              
547             # Round away from the centre of the widget
548             return
549             int($l) + ( $l > int($l) && $l > $self->{mid_line} ),
550             int($c) + ( $c > int($c) && $c > $self->{mid_col} );
551             }
552              
553             sub render_symb_to_rb
554             {
555             my $self = shift;
556             my ( $buff ) = @_;
557              
558             my $strokes = $symbol_strokes{$self->value} or return;
559              
560             my $Y_to_line = $self->{Y_to_line};
561             my $X_to_col = $self->{X_to_col};
562              
563             foreach my $stroke ( @$strokes ) {
564             my ( $start, @points ) = @$stroke;
565             $start =~ m/^(\d+),(\d+)$/;
566             my ( $atL, $atC ) = $self->_roundpos( $2 * $Y_to_line, $1 * $X_to_col );
567              
568             foreach ( @points ) {
569             m/^(\d+),(\d+)$/;
570             my ( $toL, $toC ) = $self->_roundpos( $2 * $Y_to_line, $1 * $X_to_col );
571              
572             if( $toL == $atL ) {
573             my ( $c, $limC ) = $toC > $atC ? ( $atC, $toC ) : ( $toC, $atC );
574             $self->_fill( $buff, $atL, $atL, $c, $limC );
575             }
576             elsif( $toC == $atC ) {
577             my ( $l, $limL ) = $toL > $atL ? ( $atL, $toL ) : ( $toL, $atL );
578             $self->_fill( $buff, $l, $limL, $atC, $atC );
579             }
580             else {
581             my ( $sL, $eL, $sC, $eC ) = $toL > $atL ? ( $atL, $toL, $atC, $toC )
582             : ( $toL, $atL, $toC, $atC );
583             # Maths is all easier if we use exclusive coords.
584             $eL++;
585             $eC > $sC ? $eC++ : $eC--;
586              
587             my $dL = $eL - $sL;
588             my $dC = $eC - $sC;
589              
590             if( $dL >= abs $dC ) {
591             my $c = $sC;
592             my $err = 0;
593              
594             for( my $l = $sL; $l != $eL; $l++ ) {
595             $c++, $err -= $dL if $err > $dL;
596             $c--, $err += $dL if -$err > $dL;
597              
598             $self->_dot( $buff, $l, $c );
599              
600             $err += $dC;
601             }
602             }
603             else {
604             my $l = $sL;
605             my $err = 0;
606             my $adC = abs $dC;
607              
608             for( my $c = $sC; $c != $eC; $c += ( $eC > $sC ) ? 1 : -1 ) {
609             $l++, $err -= $adC if $err > $adC;
610             $l--, $err += $adC if -$err > $adC;
611              
612             $self->_dot( $buff, $l, $c );
613              
614             $err += $dL;
615             }
616             }
617             }
618              
619             $atL = $toL;
620             $atC = $toC;
621             }
622             }
623             }
624              
625             =head1 AUTHOR
626              
627             Paul Evans
628              
629             =cut
630              
631             0x55AA;