File Coverage

blib/lib/Tickit/Widget/SegmentDisplay.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.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-2014 -- leonerd@leonerd.org.uk
5              
6             package Tickit::Widget::SegmentDisplay;
7              
8 1     1   508 use strict;
  1         1  
  1         32  
9 1     1   4 use warnings;
  1         1  
  1         24  
10 1     1   22 use 5.010; # //
  1         3  
  1         26  
11 1     1   3 use base qw( Tickit::Widget );
  1         1  
  1         290  
12 1     1   162 use Tickit::Style;
  0            
  0            
13              
14             use utf8;
15              
16             our $VERSION = '0.03';
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 $segmentdisplay = Tickit::Widget::SegmentDisplay->new( %args )
74              
75             Constructs a new C object.
76              
77             Takes the following named arguments
78              
79             =over 8
80              
81             =item value => STR
82              
83             Sets an initial value.
84              
85             =item type => STR
86              
87             The type of display. Supported types are:
88              
89             =over 4
90              
91             =item seven
92              
93             A 7-segment bar display
94              
95             =item seven_dp
96              
97             A 7-segment bar display with decimal-point. To light the decimal point, append
98             the value with ".".
99              
100             =item colon
101              
102             A static C<:>
103              
104             =item symb
105              
106             A unit or prefix symbol character. The following characters are recognised
107              
108             V A W Ω
109             M k m µ
110              
111             Each will be drawn in a style approximately to fit the general LED shape
112             display, by drawing lines of erased cells. Note however that some more
113             intricate shapes may not be very visible on smaller scales.
114              
115             =back
116              
117             =back
118              
119             =cut
120              
121             my %types = (
122             seven => [qw( 7 )],
123             seven_dp => [qw( 7. )],
124             colon => [qw( : )],
125             symb => [],
126             );
127              
128             sub new
129             {
130             my $class = shift;
131             my %args = @_;
132             my $self = $class->SUPER::new( %args );
133              
134             my $type = $args{type} // "seven";
135             my $method;
136             foreach my $typename ( keys %types ) {
137             $type eq $typename and $method = $typename, last;
138             $type eq $_ and $method = $typename, last for @{ $types{$typename} };
139             }
140             defined $method or croak "Unrecognised type name '$type'";
141              
142             $self->{reshape_method} = $self->can( "reshape_${method}" );
143             $self->{render_method} = $self->can( "render_${method}_to_rb" );
144              
145             $self->{value} = $args{value} // "";
146              
147             $self->on_style_changed_values(
148             lit => [ undef, $self->get_style_values( "lit" ) ],
149             unlit => [ undef, $self->get_style_values( "unlit" ) ],
150             );
151              
152             return $self;
153             }
154              
155             # ADG + atleast 1 line each for FB and EC
156             sub lines { 3 + 2 }
157              
158             # FE, BC + atleast 2 columns for AGD
159             sub cols { 4 + 2 }
160              
161             =head1 ACCESSORS
162              
163             =cut
164              
165             =head2 $value = $segmentdisplay->value
166              
167             =head2 $segmentdisplay->set_value( $value )
168              
169             Return or set the character on display
170              
171             =cut
172              
173             sub value
174             {
175             my $self = shift;
176             return $self->{value};
177             }
178              
179             sub set_value
180             {
181             my $self = shift;
182             ( $self->{value} ) = @_;
183             $self->redraw;
184             }
185              
186             sub on_style_changed_values
187             {
188             my $self = shift;
189             my %values = @_;
190              
191             $self->{lit_pen} = Tickit::Pen::Immutable->new( bg => $values{lit}[1] ) if $values{lit};
192             $self->{unlit_pen} = Tickit::Pen::Immutable->new( bg => $values{unlit}[1] ) if $values{unlit};
193             }
194              
195             sub reshape
196             {
197             my $self = shift;
198             my $win = $self->window or return;
199              
200             $self->{reshape_method}->( $self, $win->lines, $win->cols, 0, 0 );
201             }
202              
203             sub render_to_rb
204             {
205             my $self = shift;
206             my ( $rb, $rect ) = @_;
207              
208             $rb->eraserect( $rect );
209              
210             $self->{render_method}->( $self, $rb, $rect );
211             }
212              
213             # 7-Segment
214             my %segments = (
215             0 => "ABCDEF ",
216             1 => " BC ",
217             2 => "AB DE G",
218             3 => "ABCD G",
219             4 => " BC FG",
220             5 => "A CD FG",
221             6 => "A CDEFG",
222             7 => "ABC ",
223             8 => "ABCDEFG",
224             9 => "ABCD FG",
225             );
226              
227             sub _pen_for_seg
228             {
229             my $self = shift;
230             my ( $segment ) = @_;
231              
232             my $segments = $segments{$self->value} or return $self->{unlit_pen};
233              
234             my $lit = substr( $segments, ord($segment) - ord("A"), 1 ) ne " ";
235             return $lit ? $self->{lit_pen} : $self->{unlit_pen};
236             }
237              
238             sub reshape_seven
239             {
240             my $self = shift;
241             my ( $lines, $cols, $top, $left ) = @_;
242              
243             $self->{AGD_col} = $left + 2;
244             $self->{AGD_width} = $cols - 4;
245              
246             $self->{FE_col} = $left;
247             $self->{BC_col} = $left + $cols - 2;
248              
249             $self->{A_line} = $top;
250             $self->{G_line} = $top + int( ( $lines - 1 + 0.5 ) / 2 );
251             $self->{D_line} = $top + $lines - 1;
252             }
253              
254             sub render_seven_to_rb
255             {
256             my $self = shift;
257             my ( $rb ) = @_;
258              
259             $rb->erase_at( $self->{A_line}, $self->{AGD_col}, $self->{AGD_width}, $self->_pen_for_seg( "A" ) );
260             $rb->erase_at( $self->{G_line}, $self->{AGD_col}, $self->{AGD_width}, $self->_pen_for_seg( "G" ) );
261             $rb->erase_at( $self->{D_line}, $self->{AGD_col}, $self->{AGD_width}, $self->_pen_for_seg( "D" ) );
262              
263             my ( $F_pen, $B_pen ) = ( $self->_pen_for_seg( "F" ), $self->_pen_for_seg( "B" ) );
264             foreach my $line ( $self->{A_line}+1 .. $self->{G_line}-1 ) {
265             $rb->erase_at( $line, $self->{FE_col}, 2, $F_pen );
266             $rb->erase_at( $line, $self->{BC_col}, 2, $B_pen );
267             }
268              
269             my ( $E_pen, $C_pen ) = ( $self->_pen_for_seg( "E" ), $self->_pen_for_seg( "C" ) );
270             foreach my $line ( $self->{G_line}+1 .. $self->{D_line}-1 ) {
271             $rb->erase_at( $line, $self->{FE_col}, 2, $E_pen );
272             $rb->erase_at( $line, $self->{BC_col}, 2, $C_pen );
273             }
274             }
275              
276             # 7-Segment with DP
277             sub reshape_seven_dp
278             {
279             my $self = shift;
280             my ( $lines, $cols, $top, $left ) = @_;
281              
282             $self->reshape_seven( $lines, $cols - 2, $top, $left );
283              
284             $self->{DP_line} = $top + $lines - 1;
285             $self->{DP_col} = $left + $cols - 2;
286             }
287              
288             sub render_seven_dp_to_rb
289             {
290             my $self = shift;
291             my ( $rb ) = @_;
292              
293             my $value = $self->{value};
294             my $dp;
295             local $self->{value};
296              
297             if( $value =~ m/^(\d?)(\.?)/ ) {
298             $self->{value} = $1;
299             $dp = length $2;
300             }
301             else {
302             $self->{value} = $value;
303             }
304              
305             $self->render_seven_to_rb( $rb );
306              
307             my $dp_pen = $dp ? $self->{lit_pen} : $self->{unlit_pen};
308             $rb->erase_at( $self->{DP_line}, $self->{DP_col}, 2, $dp_pen );
309             }
310              
311             # Static double-dot colon
312             sub reshape_colon
313             {
314             my $self = shift;
315             my ( $lines, $cols, $top, $left ) = @_;
316             my $bottom = $top + $lines - 1;
317              
318             $self->{colon_col} = 2 + int( ( $cols - 4 ) / 2 );
319              
320             my $ofs = int( ( $lines - 1 + 0.5 ) / 4 );
321              
322             $self->{A_line} = $top + $ofs;
323             $self->{B_line} = $bottom - $ofs;
324             }
325              
326             sub render_colon_to_rb
327             {
328             my $self = shift;
329             my ( $rb ) = @_;
330              
331             my $col = $self->{colon_col};
332             $rb->erase_at( $self->{A_line}, $col, 2, $self->{lit_pen} );
333             $rb->erase_at( $self->{B_line}, $col, 2, $self->{lit_pen} );
334             }
335              
336             # Symbol drawing
337             #
338             # Each symbol is drawn as a series of erase calls on the RB to draw 'lines'.
339              
340             my %symbol_strokes = do {
341             no warnings 'qw'; # Quiet the 'Possible attempt to separate words with commas' warning
342              
343             # Letters likely to be used for units
344             V => [ [qw( 0,0 50,100 100,0 )] ],
345             A => [ [qw( 0,100 50,0 100,100 )], [qw( 20,70 80,70)] ],
346             W => [ [qw( 0,0 25,100 50,50 75,100 100,0)] ],
347             Ω => [ [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 ) ] ],
348              
349             # Symbols likely to be used as SI prefixes
350             M => [ [qw( 0,100 0,0 50,50 100,0 100,100 )] ],
351             k => [ [qw( 10,0 10,100 )], [qw( 90,40 10,70 90,100 )] ],
352             m => [ [qw( 0,100 0,50 )], [qw( 10,40 40,40 )], [qw( 50,50 50,100 )], [qw( 60,40 90,40 )], [qw( 90,50 100,100 )] ],
353             µ => [ [qw( 0,100 0,40 )], [qw( 0,80 70,80 80,75 90,60 100,40 )] ],
354             };
355              
356             sub reshape_symb
357             {
358             my $self = shift;
359             my ( $lines, $cols, $top, $left ) = @_;
360              
361             $self->{mid_line} = int( ( $lines - 1 ) / 2 );
362             $self->{mid_col} = int( ( $cols - 2 ) / 2 );
363              
364             $self->{Y_to_line} = ( $lines - 1 ) / 100;
365             $self->{X_to_col} = ( $cols - 2 ) / 100;
366             }
367              
368             sub _roundpos
369             {
370             my $self = shift;
371             my ( $l, $c ) = @_;
372              
373             # Round away from the centre of the widget
374             return
375             int($l) + ( $l > int($l) && $l > $self->{mid_line} ),
376             int($c) + ( $c > int($c) && $c > $self->{mid_col} );
377             }
378              
379             sub render_symb_to_rb
380             {
381             my $self = shift;
382             my ( $rb ) = @_;
383              
384             my $strokes = $symbol_strokes{$self->value} or return;
385              
386             $rb->setpen( $self->{lit_pen} );
387              
388             my $Y_to_line = $self->{Y_to_line};
389             my $X_to_col = $self->{X_to_col};
390              
391             foreach my $stroke ( @$strokes ) {
392             my ( $start, @points ) = @$stroke;
393             $start =~ m/^(\d+),(\d+)$/;
394             my ( $atL, $atC ) = $self->_roundpos( $2 * $Y_to_line, $1 * $X_to_col );
395              
396             foreach ( @points ) {
397             m/^(\d+),(\d+)$/;
398             my ( $toL, $toC ) = $self->_roundpos( $2 * $Y_to_line, $1 * $X_to_col );
399              
400             if( $toL == $atL ) {
401             my ( $c, $limC ) = $toC > $atC ? ( $atC, $toC ) : ( $toC, $atC );
402             $rb->erase_at( $atL, $c, $limC - $c + 2 );
403             }
404             elsif( $toC == $atC ) {
405             my ( $l, $limL ) = $toL > $atL ? ( $atL, $toL ) : ( $toL, $atL );
406             $rb->erase_at( $_, $atC, 2 ) for $l .. $limL;
407             }
408             else {
409             my ( $sL, $eL, $sC, $eC ) = $toL > $atL ? ( $atL, $toL, $atC, $toC )
410             : ( $toL, $atL, $toC, $atC );
411             # Maths is all easier if we use exclusive coords.
412             $eL++;
413             $eC > $sC ? $eC++ : $eC--;
414              
415             my $dL = $eL - $sL;
416             my $dC = $eC - $sC;
417              
418             if( $dL >= abs $dC ) {
419             my $c = $sC;
420             my $err = 0;
421              
422             for( my $l = $sL; $l != $eL; $l++ ) {
423             $c++, $err -= $dL if $err > $dL;
424             $c--, $err += $dL if -$err > $dL;
425              
426             $rb->erase_at( $l, $c, 2 );
427              
428             $err += $dC;
429             }
430             }
431             else {
432             my $l = $sL;
433             my $err = 0;
434             my $adC = abs $dC;
435              
436             for( my $c = $sC; $c != $eC; $c += ( $eC > $sC ) ? 1 : -1 ) {
437             $l++, $err -= $adC if $err > $adC;
438             $l--, $err += $adC if -$err > $adC;
439              
440             $rb->erase_at( $l, $c, 2 );
441              
442             $err += $dL;
443             }
444             }
445             }
446              
447             $atL = $toL;
448             $atC = $toC;
449             }
450             }
451             }
452              
453             =head1 AUTHOR
454              
455             Paul Evans
456              
457             =cut
458              
459             0x55AA;