File Coverage

blib/lib/Acme/TextLayout.pm
Criterion Covered Total %
statement 269 285 94.3
branch 77 92 83.7
condition 26 33 78.7
subroutine 33 35 94.2
pod 15 15 100.0
total 420 460 91.3


line stmt bran cond sub pod time code
1             package Acme::TextLayout;
2              
3 2     2   61359 use warnings;
  2         4  
  2         72  
4 2     2   12 use strict;
  2         6  
  2         70  
5 2     2   1761 use Perl6::Attributes;
  2         74337  
  2         11  
6 2     2   12284 use FileHandle;
  2         8823  
  2         12  
7 2     2   2022 use Data::Dumper;
  2         8553  
  2         7009  
8              
9              
10             =head1 NAME
11              
12             Acme::TextLayout - Layout things in a grid, as described textually
13              
14             =head1 VERSION
15              
16             Version 0.01
17              
18             =cut
19              
20             our $VERSION = '0.01';
21              
22             =head1 SYNOPSIS
23              
24             $tl = Acme::TextLayout->new;
25             $tl->instantiate(text => $pattern);
26              
27             =head1 DESCRIPTION
28              
29             For a GUI, controlling layout (especially on resize) can be
30             difficult, especially if your layout is complex. When looking
31             at a GUI, I came to the realization that I could express the
32             layout nicely like this:
33              
34             AAAAAAAAAAAAAAAA
35             BBBBxxxxxxxxxxxx
36             BBBBxxxxxxxxxxxx
37             DDDDDDDDDDDDDDDD
38             DDDDDDDDDDDDDDDD
39             DDDDDDDDDDDDDDDD
40             %%%%%%%%%%%%%GGG
41              
42             Where each group of contiguous, like characters specifies a screen
43             region.
44              
45             B: space is not legal. Nor should you use "-", trust
46             me. A space (" ") will cause you to die, but a "-" is accepted,
47             but is used by other modules for other things. BEWARE!
48              
49             To me, this gives an easy-to-grasp pictorial of the GUI
50             layout, as long as one notes WTF the letters and symbols represent.
51             The only caveat is that the collection of like characters/symbols
52             making the pattern must be adjacent, and must be rectangular. And
53             the overall pattern must be rectangular.
54              
55             Note that this textual arrangement can be as big as you want.
56             It's all relative. Although it might not look like it on
57             the screen in your editor of choice, all spacing is assummed to
58             be the same in X and Y. Thus, the aspect ratio of the above
59             pattern is 16/7 (width/height).
60              
61             To be useful for a GUI, one must be able to map this goofy space
62             into screen coordinates. That's what the B function is
63             for (see below).
64              
65             Now, I know what you must be thinking: is this guy nuts? Why not
66             use brand-X fancy GUI layout tool? Well, the fact is that those
67             are nice and easy for the initial layout, but they generally generate
68             code with precise XY coordinates in them, which makes resizing almost
69             impossible.
70              
71             The idea here is that we use the above textual layout to specify
72             all the relative positions of things, then map this to a real
73             coordinate system, preserving the spatial relativity and size
74             associations.
75              
76             I wrote this for use in a GUI application, but figured it might have
77             use elsewhere. Hence, this class. If you find a novel use for it,
78             please let me know what it is (email address in this document).
79              
80              
81             =head1 METHODS
82              
83             =cut
84              
85             =head2 B
86              
87             $tl = Acme::TextLayout->new([%opts]);
88              
89             Create an instance of this class. See B to do anything useful.
90              
91             =cut
92              
93             sub new {
94 9     9 1 1402 my $class = shift;
95 9         23 my %opts = @_;
96 9         20 my $self = \%opts;
97 9         32 bless $self, $class;
98 9         31 $.Class = $class;
99 9         38 return $self;
100             }
101              
102             =head2 B
103              
104             $tl->instantiate(text => ??);
105             -or-
106             $tl->instantiate(file => ??);
107              
108             Specify the textual layout pattern we are interested in, either
109             from a text string or a file.
110              
111             Returns undef if something wrong with your input.
112              
113             =cut
114              
115             sub instantiate {
116 12     12 1 1005 my ($self, %opts) = @_;
117 12         30 my $file = $opts{file};
118 12         25 my $text = $opts{text};
119              
120             # reset state on new instantiation
121 12         33 $.textRef = [];
122 12         30 $.Ranges = {};
123 12         34 $.widest = undef;
124 12         25 $.chars = {};
125 12         54 $.Above = $.Below = $.Left = $.Right = undef;
126              
127 12 100       52 if (defined $file) {
    50          
128 1         11 my $fh = FileHandle->new($file);
129 1 50       111 return unless defined $fh;
130 1         35 my @text = <$fh>;
131 1         7 $fh->close;
132 1         42 chomp foreach @text;
133 1         7 s/^\s+// foreach @text;
134 1         4 $text = [ @text ];
135 1         3 ./_widest(\@text);
136             }
137             elsif (defined $text) {
138 11         60 my @text = split(/\n{1}/, $text);
139 11         89 s/^\s+// foreach @text;
140 11         33 $text = [ @text ];
141 11         32 ./_widest(\@text);
142             }
143             else {
144 0         0 return undef;
145             }
146              
147 12         43 ./_whats_in_there($text);
148 12         70 ./_widest($text);
149 12         38 $.textRef = $text;
150 33 100       132 map {
151 12         52 return undef unless length($_) == $.widest;
152 12         27 } @{$.textRef};
153              
154 11         24 my %Ranges;
155 11         19 my %chars = %.chars;
  11         104  
156 38         85 map {
157 11         37 my $C = $_;
158 38         140 my @d = ./range($C);
159 37         215 $Ranges{$C} = \@d;
160             } keys(%chars);
161              
162 10         35 $.Ranges = \%Ranges;
163 10 100       44 print STDERR "Pattern appears disjoint\n" if ./_disjoint();
164 10 100       36 return undef if ./_disjoint();
165             # signify OK if we got here
166 8         89 return 1;
167             }
168              
169             # not a complete test, but tests for the obvious
170             sub _disjoint {
171 20     20   35 my ($self) = @_;
172 20         29 my @text = @{$.textRef};
  20         71  
173 20         56 my @chars = ./characters();
174 20         42 my $ok = 1;
175 58         77 map {
176 20         32 my $line = $_;
177 288         1291 map {
178 58         87 my $n = 0;
179 288         1678 my $t = $line;
180 288         7064 $n++ while $t =~ s/$_{1,}//;
181 288 100       1844 $ok = 0 if $n > 1;
182             } @chars;
183             } @text;
184 20         88 my $width = ./width();
185 20         67 for (my $i=0; $i < $width; $i++) {
186 136         159 my @new;
187 136         1053 push(@new, substr($_, $i, 1)) foreach @text;
188 136         447 my $line = join('', @new);
189 704         793 map {
190 136         200 my $n = 0;
191 704         1231 my $t = $line;
192 704         11842 $n++ while $t =~ s/$_{1,}//;
193 704 100       2994 $ok = 0 if $n > 1;
194             } @chars;
195             }
196              
197 20 100       226 return $ok ? 0 : 1;
198             }
199              
200             sub _widest {
201 76     76   130 my ($self, $textRef) = @_;
202 76         2608 my @text = @$textRef;
203 76         141 my $widest = length($text[0]);
204 257         336 map {
205 76         123 my $len = length($_);
206 257 50       978 $widest = $len if $len > $widest;
207             } @text;
208 76         4384 $.widest = $widest;
209             }
210              
211             sub _height {
212 19     19   32 my ($self, $textRef) = @_;
213 19         45 my @text = @$textRef;
214 19         43 return scalar(@text);
215             }
216              
217             # figure out all characters in our pattern
218             sub _whats_in_there {
219 12     12   21 my ($self, $aref) = @_;
220 12         34 my @text = @$aref;
221             #print "@text", "\n";
222 12         18 my %chars;
223 3060         4248 map {
224 12         39 my $c = $_;
225 3060         5117 my $C = chr($c);
226 8415         10410 map {
227 3060         6424 my $n;
228 8415 100       42033 $chars{$C} = 1 if $_ =~ /\Q$C\E/;
229 8415 50 66     34857 die "$.Class - space unacceptable in pattern\n"
      33        
230             if $C eq " " && defined $chars{$C} && $chars{$C} == 1;
231             } @text;
232             } 1 .. 255;
233              
234             # preserve our character set
235 12         85 $.chars = \%chars;
236             }
237              
238             sub _right {
239 0     0   0 my ($self, $text, $char) = @_;
240 0         0 my @text = split(//, $text);
241 0         0 my $first;
242             my $last;
243 0 0       0 if ($text =~ /$char/) {
244 0         0 $first = pos($text);
245 0         0 $last = rindex $text, $char;
246             }
247 0         0 return ($first, $last);
248             }
249              
250             # determine vertical range of a specific character in our pattern
251             sub _vrange {
252 48     48   70 my ($self, $textRef, $char) = @_;
253 48         60 my $top;
254             my $bottom;
255 48         55 my $n = 0;
256 182 100 100     1176 map {
257 48         87 $top = $n if $_ =~ /$char/ && !defined $top;
258 182 100       989 $bottom = $n if $_ =~ /$char/;
259 182         387 $n++;
260             } @$textRef;
261 48         121 return ($top, $bottom);
262             }
263              
264             sub _first {
265 48     48   86 my ($self, $textRef, $char) = @_;
266 48         119 my @text = @$textRef;
267 48         91 my $first;
268 182         311 map {
269 48         66 my $n = index $_, $char;
270 182 100       546 unless (defined $first) {
271 94 100       207 $first = $n if $n >= 0;
272             }
273 182 100 100     925 if (defined $first && $n >= 0) {
274 80 50       389 die "$.Class - char $char appears misaligned\n"
275             if $n < $first;
276             }
277             } @text;
278 48         132 return $first;
279             }
280              
281             sub _last {
282 48     48   72 my ($self, $textRef, $char) = @_;
283 48         115 my @text = @$textRef;
284 48         85 my $last;
285 182         303 map {
286 48         70 my $n = rindex $_, $char;
287 182 100       383 unless (defined $last) {
288 94 100       236 $last = $n if $n >= 0;
289             }
290 182 100 100     803 if (defined $last && $n >= 0) {
291 80 100       439 die "$.Class - char $char appears misaligned\n"
292             if $n > $last;
293             }
294             } @text;
295 47         144 return $last;
296             }
297              
298             sub _range {
299 48     48   88 my ($self, $textRef, $char) = @_;
300 48         114 my ($top, $bottom) = ./_vrange($textRef, $char);
301 48         150 my $left = ./_first($textRef, $char);
302 48         106 my $right = ./_last($textRef, $char);
303 47         194 return ($top, $bottom, $left, $right);
304             }
305              
306             # simple equation to map char ranges to something else
307             sub _stretch_offset {
308 12     12   52 my ($self, $i1, $i2, $o1, $o2) = @_;
309             # handle single characters
310 12 50       24 $i2 = $i1 + 1 if $i1 == $i2;
311 12         20 my $stretch = ($o2-$o1)/($i2-$i1);
312 12         17 my $offset = $o1-($i1*$stretch);
313 12         36 return ($stretch, $offset);
314             }
315              
316             =head2 B
317              
318             ($ymin, $ymax, $xmin, $xmax) = $tl->range($char);
319              
320             The range of positions for the specified character. B
321             order of arguments> returned.
322              
323             =cut
324              
325             sub range {
326 48     48 1 765 my ($self, $char) = @_;
327             #return () unless defined $.Ranges{$char};
328 48         177 return ./_range($.textRef, $char);
329             }
330              
331             =head2 B
332              
333             @chars = $tl->characters();
334              
335             Return list of all of the unique characters in our pattern.
336              
337             =cut
338              
339             sub characters {
340 21     21 1 329 my ($self) = @_;
341 21         25 return sort keys %.Ranges;
  21         234  
342             }
343              
344             =head2 B
345              
346             ($width, $height) = $tl->text_size();
347              
348             Find width & height of our pattern in character units. This may
349             be important since the user of a GUI is free to resize in a way
350             that messes up the relative aspect ratio as you defined in the
351             pattern. And you may want to correct this awful situation.
352              
353             =cut
354              
355             sub text_size {
356 0     0 1 0 my ($self) = @_;
357 0         0 my $h = ./_height($.textRef);
358 0         0 my $w = ./_widest($.textRef);
359 0         0 return ($w, $h);
360             }
361              
362             =head2 B
363              
364             $tl->width();
365              
366             Return width of our pattern (in # characters).
367              
368             =cut
369              
370             sub width {
371 46     46 1 76 my ($self) = @_;
372 46         128 my $w = ./_widest($.textRef);
373 46         136 return $w;
374             }
375              
376             =head2 B
377              
378             $tl->height();
379              
380             Return height of our pattern (in # characters).
381              
382             =cut
383              
384             sub height {
385 13     13 1 291 my ($self) = @_;
386 13         44 my $h = ./_height($.textRef);
387 13         52 return $h;
388             }
389              
390             =head2 B
391              
392             @bbox = $tl->map_range($width, $height, $char);
393              
394             Map the relative position and size of the indicated character ($char)
395             region in our pattern to a real XY coordinate space.
396              
397             @bbox is the bounding box, returned as ($x1, $y1, $x2, $y2), where
398             $x1, $y1 is the upper left corner, and $x2, $y2 is the lower right.
399              
400             Because this was written (primarily) to interface to a GUI,
401             the origin is assumed
402             to be 0,0 in the upper left corner, with x bigger to the right, and
403             y bigger down. Adjust as necessary to fit your problem domain.
404              
405             =cut
406              
407             sub map_range {
408 6     6 1 3217 my ($self, $width, $height, $char) = @_;
409 6         57 my @r = @{$.Ranges{$char}};
  6         20  
410 6         18 my $h = ./_height($.textRef);
411 6         16 my $w = ./_widest($.textRef);
412 6         20 my ($xs, $xo) = ./_stretch_offset(0, $w, 0, $width);
413 6         16 my ($ys, $yo) = ./_stretch_offset(0, $h, 0, $height);
414 6     12   25 my $xEqn = sub { my ($x) = @_; my $y = $xs*$x + $xo; return $y; };
  12         23  
  12         23  
  12         17  
415 6     12   23 my $yEqn = sub { my ($y) = @_; my $x = $ys*$y + $yo; return $x; };
  12         13  
  12         18  
  12         23  
416 6         16 my $xmin = $xEqn->($r[2]);
417 6         16 my $ymin = $yEqn->($r[0]),
418             my $xmax = $xEqn->($r[3]-$r[2]+1)+$xmin;
419 6         16 my $ymax = $yEqn->($r[1]-$r[0]+1)+$ymin;
420 6         14 my @bbox = ($xmin, $ymin, $xmax-1, $ymax-1);
421 6         51 return @bbox;
422             }
423              
424             # find out if there is overlap; $c0 and $c1 are array references
425             sub _check_overlap {
426 224     224   338 my ($self, $c0, $c1) = @_;
427 224         221 my %x;
428 224         436 my @x0 = @$c0;
429 224         318 my @x1 = @$c1;
430 224         1443 $x{$_} = 1 foreach $x0[0] .. $x0[1];
431 224         1349 $x{$_} += 1 foreach $x1[0] .. $x1[1];
432 224         304 my $status;
433 1315 100       4304 map {
434 224         586 $status = 1 if $x{$_} > 1;
435             } keys(%x);
436 224 100       2489 return defined $status ? 1 : 0;
437             }
438              
439             # are they in same x range?
440             sub _in_x {
441 112     112   173 my ($self, $me, $other) = @_;
442 112         229 my @x = ($me->[2], $me->[3]);
443 112         200 my @xo = ($other->[2], $other->[3]);
444 112         256 return ./_check_overlap(\@x, \@xo);
445             }
446              
447             # are they in same y range?
448             sub _in_y {
449 112     112   201 my ($self, $me, $other) = @_;
450 112         228 my @y = ($me->[0], $me->[1]);
451 112         191 my @yo = ($other->[0], $other->[1]);
452 112         256 return ./_check_overlap(\@y, \@yo);
453             }
454              
455             =head2 B
456              
457             @r = $tl->above($char);
458              
459             Return a list (possibly empty) of each of the characters
460             above (and adjacent) to the specified character.
461              
462             =cut
463              
464             sub above {
465 8     8 1 3005 my ($self, $char) = @_;
466 8         11 my @r = @{$.Ranges{$char}};
  8         27  
467 8 100       36 return () if $r[0] == 0;
468 5 50       24 return @{$.Above{$char}} if defined $.Above{$char};
  0         0  
469 5         5 my @keys = keys(%.Ranges);
  5         26  
470 5         11 my @d;
471             map {
472 5 100       9 if ($_ ne $char) {
  45         116  
473             #print "Comparing $_ ";
474 40         46 my @other = @{$.Ranges{$_}};
  40         125  
475 40 100 66     98 push(@d, $_) if ./_in_x(\@r, \@other) &&
      66        
476             ($other[0] == ($r[0]-1) || $other[1] == ($r[0]-1));
477             }
478             } @keys;
479 5         14 $.Above{$char} = \@d;
480             #print "Above $char @d\n";
481 5         32 return @d;
482             }
483              
484             =head2 B
485              
486             @r = $tl->below($char);
487              
488             Return a list (possibly empty) of each of the characters
489             below (and adjacent) to the specified character.
490              
491             =cut
492              
493             sub below {
494 9     9 1 7276 my ($self, $char) = @_;
495 9         15 my @r = @{$.Ranges{$char}};
  9         35  
496 9 50       31 return () if $r[1] == ./width();
497 9 50       42 return @{$.Below{$char}} if defined $.Below{$char};
  0         0  
498 9         14 my @keys = keys(%.Ranges);
  9         53  
499 9         20 my @d;
500             map {
501 9 100       16 if ($_ ne $char) {
  81         205  
502 72         79 my @other = @{$.Ranges{$_}};
  72         221  
503 72 100 66     193 push(@d, $_) if ./_in_x(\@r, \@other) &&
      66        
504             ($other[0] == ($r[0]+1) || $other[1] == ($r[0]+1));
505             }
506             } @keys;
507 9         28 $.Below{$char} = \@d;
508 9         59 return @d;
509             }
510              
511             =head2 B
512              
513             @r = $tl->left($char);
514              
515             Return a list (possibly empty) of each of the characters to
516             the left (and adjacent) to the specified character.
517              
518             =cut
519              
520             sub left {
521 9     9 1 2533 my ($self, $char) = @_;
522 9         15 my @r = @{$.Ranges{$char}};
  9         33  
523 9 100       84 return () if $r[2] == 0;
524 6 50       21 return @{$.Left{$char}} if defined $.Left{$char};
  0         0  
525 6         7 my @keys = keys(%.Ranges);
  6         28  
526 6         14 my @d;
527             map {
528 6 100       8 if ($_ ne $char) {
  54         131  
529 48         54 my @other = @{$.Ranges{$_}};
  48         149  
530 48 100 100     114 push(@d, $_) if ./_in_y(\@r, \@other) &&
531             ($other[3] == ($r[2]-1));
532             }
533             } @keys;
534 6         15 $.Left{$char} = \@d;
535 6         37 return @d;
536             }
537              
538             =head2 B
539              
540             @r = $tl->right($char);
541              
542             Return a list (possibly empty) of each of the characters to
543             the right (and adjacent) to the specified character.
544              
545             =cut
546              
547             sub right {
548 8     8 1 2508 my ($self, $char) = @_;
549 8         11 my @r = @{$.Ranges{$char}};
  8         27  
550 8 50       35 return () if $r[2] == ./width();
551 8 50       24 return @{$.Right{$char}} if defined $.Right{$char};
  0         0  
552 8         9 my @keys = keys(%.Ranges);
  8         39  
553 8         15 my @d;
554             map {
555 8 100       10 if ($_ ne $char) {
  72         191  
556 64         69 my @other = @{$.Ranges{$_}};
  64         183  
557 64 100 100     143 push(@d, $_) if ./_in_y(\@r, \@other) &&
558             ($other[2] == ($r[3]+1));
559             }
560             } @keys;
561 8         23 $.Right{$char} = \@d;
562 8         44 return @d;
563             }
564              
565             =head2 B
566              
567             ($xpercent, $ypercent) = $tl->range_as_percent($char);
568              
569             Returns the percentage of x and y that this character consumes
570             in the I. Number returned for each is <= 1.0.
571              
572             =cut
573              
574             sub range_as_percent {
575 6     6 1 1318 my ($self, $char) = @_;
576 6         18 my ($ymin, $ymax, $xmin, $xmax) = ./range($char);
577 6         18 my $width = ./width();
578 6         18 my $height = ./height();
579 6         76 return (($xmax-$xmin+1)/$width, ($ymax-$ymin+1)/$height);
580             }
581              
582             =head2 B
583              
584             @chars = $tl->order([$line]);
585              
586             Return the order of the characters encountered on line $line
587             (zero-based). $line defaults to zero if not specified.
588              
589             =cut
590              
591             sub order {
592 4     4 1 633 my ($self, $line) = @_;
593 4 100       27 $line = 0 unless defined $line;
594 4 100       15 die "$.Class - in order, line $line is too big!\n"
595             unless $line < ./height();
596 3         6 my $text = $.textRef[$line];
597 3 50       12 return unless defined $text;
598 3         43 my %Chars;
599             my @Chars;
600 3         18 my @chars = split('', $text);
601             map {
602 3 100       6 unless (defined $Chars{$_}) {
  19         58  
603 6         12 push(@Chars, $_);
604 6         24 $Chars{$_} = 1;
605             }
606             } @chars;
607 3         23 return @Chars;
608             }
609              
610             =head2 B
611              
612             $stat = $tl->only_one();
613              
614             Returns 1 if there is only a single character in your pattern,
615             0 if there are more.
616              
617             =cut
618              
619             sub only_one {
620 2     2 1 5 my ($self) = @_;
621 2         9 return ./order() == 1;
622             }
623              
624             =head1 AUTHOR
625              
626             X Cramps, C<< >>
627              
628             =head1 BUGS
629              
630             There shouldn't be any. But I am a human, and do mess up sometimes.
631              
632             Please report any bugs or feature requests to C
633             at rt.cpan.org>, or through
634             the web interface at
635             L.
636             I will be notified, and then you'll
637             automatically be notified of progress on your bug as I make changes.
638              
639             =head1 SUPPORT
640              
641             You can find documentation for this module with the perldoc command.
642              
643             perldoc Acme::TextLayout
644              
645             You can also look for information at:
646              
647             =over 4
648              
649             =item * RT: CPAN's request tracker
650              
651             L
652              
653             =item * AnnoCPAN: Annotated CPAN documentation
654              
655             L
656              
657             =item * CPAN Ratings
658              
659             L
660              
661             =item * Search CPAN
662              
663             L
664              
665             =back
666              
667             =head1 ACKNOWLEDGEMENTS
668              
669             Captain Beefheart and Ella Guru. So there.
670              
671             =head1 COPYRIGHT & LICENSE
672              
673             Copyright 2009 X Cramps, all rights reserved.
674              
675             This program is free software; you can redistribute it and/or modify it
676             under the same terms as Perl itself.
677              
678              
679             =cut
680              
681             1; # End of Acme::TextLayout