File Coverage

blib/lib/Tickit/RenderContext.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


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 -- leonerd@leonerd.org.uk
5              
6             package Tickit::RenderContext;
7              
8 1     1   895 use strict;
  1         2  
  1         32  
9 1     1   5 use warnings;
  1         1  
  1         34  
10 1     1   14 use feature qw( switch );
  1         2  
  1         115  
11              
12             our $VERSION = '0.09';
13              
14 1     1   5 use Carp;
  1         2  
  1         117  
15 1     1   6 use Scalar::Util qw( refaddr );
  1         2  
  1         114  
16              
17 1     1   398 use Tickit::Utils qw( textwidth substrwidth );
  0            
  0            
18             use Tickit::Rect;
19             use Tickit::Pen 0.31;
20              
21             # Exported API constants
22             use Exporter 'import';
23             our @EXPORT_OK = qw(
24             LINE_SINGLE LINE_DOUBLE LINE_THICK
25             CAP_START CAP_END CAP_BOTH
26             );
27             use constant {
28             LINE_SINGLE => 0x01,
29             LINE_DOUBLE => 0x02,
30             LINE_THICK => 0x03,
31             };
32             use constant {
33             CAP_START => 0x01,
34             CAP_END => 0x02,
35             CAP_BOTH => 0x03,
36             };
37              
38             # cell states
39             use constant {
40             SKIP => 0,
41             TEXT => 1,
42             ERASE => 2,
43             CONT => 3,
44             LINE => 4,
45             CHAR => 5,
46             };
47              
48             =head1 NAME
49              
50             C - efficiently render text and linedrawing on
51             L windows
52              
53             =head1 SYNOPSIS
54              
55             package Tickit::Widget::Something;
56             ...
57              
58             sub render
59             {
60             my $self = shift;
61             my %args = @_;
62             my $win = $self->window or return;
63              
64             my $rc = Tickit::RenderContext->new(
65             lines => $win->lines,
66             cols => $win->cols,
67             );
68             $rc->clip( $args{rect} );
69              
70             $rc->text_at( 2, 2, "Hello, world!", $self->pen );
71              
72             $rc->flush_to_window( $win );
73             }
74              
75             =head1 DESCRIPTION
76              
77             Provides a buffer of pending rendering operations to apply to a Window. The
78             buffer is modified by rendering operations performed by the widget, and
79             flushed to the widget's window when complete.
80              
81             This provides the following advantages:
82              
83             =over 2
84              
85             =item *
86              
87             Changes can be made in any order, and will be flushed in top-to-bottom,
88             left-to-right order, minimising cursor movements.
89              
90             =item *
91              
92             Buffered content can be overwritten or partly erased once stored, simplifying
93             some styles of drawing operation. Large areas can be erased, and then redrawn
94             with text or lines, without causing a double-drawing flicker on the output
95             terminal.
96              
97             =item *
98              
99             The buffer supports line-drawing, complete with merging of line segments that
100             meet in a character cell. Boxes, grids, and other shapes can be easily formed
101             by drawing separate line segments, and the render context will handle the
102             corners and other junctions formed.
103              
104             =back
105              
106             Drawing methods come in two forms; absolute, and cursor-relative:
107              
108             =over 2
109              
110             =item *
111              
112             Absolute methods, identified by their name having a suffixed C<_at>, operate
113             on a position within the buffer specified by their argument.
114              
115             =item *
116              
117             Cursor-relative methods, identified by their lack of C<_at> suffix, operate at
118             and update the position of the "virtual cursor". This is a position within the
119             buffer that can be set using the C method. The position of the virtual
120             cursor is not affected by the absolute-position methods.
121              
122             =back
123              
124             This code is still in the experiment stage. At some future point it may be
125             merged into the main L distribution, and reimplemented in efficient XS
126             or C code. As such, recommendations and best-practices are still subject to
127             change and evolution as the code progresses.
128              
129             =head2 State Stack
130              
131             The render context stores a stack of saved state. The state of the context can
132             be stored using the C method, so that changes can be made, before
133             finally restoring back to that state using C. The following items of
134             state are saved:
135              
136             =over 2
137              
138             =item *
139              
140             The virtual cursor position
141              
142             =item *
143              
144             The clipping rectangle
145              
146             =item *
147              
148             The render pen
149              
150             =item *
151              
152             The translation offset
153              
154             =back
155              
156             When the state is saved to the stack, the render pen is remembered and merged
157             with any pen set using the C method.
158              
159             The queued content to render is not part of the state stack. It is intended
160             that the state stack be used to implement recursive delegation of drawing
161             operations down a tree of code, allowing child contexts to be created by
162             saving state and modifying it, to later restore it again afterwards.
163              
164             =cut
165              
166             require XSLoader;
167             XSLoader::load( __PACKAGE__, $VERSION );
168              
169             use Struct::Dumb;
170              
171             struct State => [qw( line col clip pen xlate_line xlate_col )];
172             struct StatePen => [qw( pen )];
173              
174             =head1 CONSTRUCTOR
175              
176             =cut
177              
178             =head2 $rc = Tickit::RenderContext->new( %args )
179              
180             Returns a new instance of a C.
181              
182             Takes the following named arguments:
183              
184             =over 8
185              
186             =item lines => INT
187              
188             =item cols => INT
189              
190             The size of the buffer area.
191              
192             =back
193              
194             =cut
195              
196             sub new
197             {
198             my $class = shift;
199             my %args = @_;
200              
201             my $lines = $args{lines};
202             my $cols = $args{cols};
203              
204             my $self = bless {
205             lines => $lines,
206             cols => $cols,
207             pen => undef,
208             xlate_line => 0,
209             xlate_col => 0,
210             }, $class;
211             $self->_xs_new;
212              
213             $self->reset;
214              
215             return $self;
216             }
217              
218             sub DESTROY
219             {
220             my $self = shift;
221              
222             $self->_xs_destroy;
223             }
224              
225             =head1 METHODS
226              
227             =cut
228              
229             =head2 $lines = $rc->lines
230              
231             =head2 $cols = $rc->cols
232              
233             Returns the size of the buffer area
234              
235             =cut
236              
237             sub lines { shift->{lines} }
238             sub cols { shift->{cols} }
239              
240             sub _xlate_and_clip
241             {
242             my $self = shift;
243             my ( $line, $col, $len ) = @_;
244              
245             $line += $self->{xlate_line};
246             $col += $self->{xlate_col};
247              
248             my $clip = $self->{clip} or return; # undef means totally invisible
249              
250             return if $line < $clip->top or
251             $line >= $clip->bottom or
252             $col >= $clip->right;
253              
254             my $startcol = 0;
255             if( $col < $clip->left ) {
256             $len += $col - $clip->left;
257             $startcol -= $col - $clip->left;
258             $col = $clip->left;
259             }
260             return if $len <= 0;
261              
262             if( $len > $clip->right - $col ) {
263             $len = $clip->right - $col;
264             }
265              
266             return ( $line, $col, $len, $startcol );
267             }
268              
269             =head2 $rc->save
270              
271             Pushes a new state-saving context to the stack, which can later be returned to
272             by the C method.
273              
274             =cut
275              
276             sub save
277             {
278             my $self = shift;
279              
280             my $savepen = defined $self->{pen} ? Tickit::Pen::Immutable->new( $self->{pen}->getattrs )
281             : undef;
282              
283             push @{ $self->{stack} }, State(
284             $self->{line}, $self->{col}, $self->{clip}, $savepen, $self->{xlate_line}, $self->{xlate_col},
285             );
286             }
287              
288             =head2 $rc->savepen
289              
290             Pushes a new state-saving context to the stack that only stores the pen. This
291             can later be returned to by the C method, but will only restore the
292             pen. Other attributes such as the virtual cursor position will be unaffected.
293              
294             This may be more efficient for rendering runs of text in a different pen, than
295             multiple calls to C or C using the same pen. For a single call it
296             is better just to pass a different pen directly.
297              
298             =cut
299              
300             sub savepen
301             {
302             my $self = shift;
303              
304             my $savepen = defined $self->{pen} ? Tickit::Pen::Immutable->new( $self->{pen}->getattrs )
305             : undef;
306              
307             push @{ $self->{stack} }, StatePen( $savepen );
308             }
309              
310             =head2 $rc->restore
311              
312             Pops and restores a saved state previously created with C.
313              
314             =cut
315              
316             sub restore
317             {
318             my $self = shift;
319              
320             my $state = pop @{ $self->{stack} };
321              
322             $self->{pen} = $state->pen;
323              
324             if( $state->isa( "Tickit::RenderContext::State" ) ) {
325             $self->{line} = $state->line;
326             $self->{col} = $state->col;
327             $self->{clip} = $state->clip;
328             $self->{xlate_line} = $state->xlate_line;
329             $self->{xlate_col} = $state->xlate_col;
330             }
331             }
332              
333             =head2 $rc->clip( $rect )
334              
335             Restricts the clipping rectangle of drawing operations to be no further than
336             the limits of the given rectangle. This will apply to subsequent rendering
337             operations but does not affect existing content, nor the actual rendering to
338             the window.
339              
340             Clipping rectangles cumulative; each call further restricts the drawing
341             region. To revert back to a larger drawing area, use the C and
342             C stack.
343              
344             =cut
345              
346             sub clip
347             {
348             my $self = shift;
349             my ( $rect ) = @_;
350              
351             # $self->{clip} is always in output coordinates
352              
353             $self->{clip} = $self->{clip}->intersect( $rect->translate( $self->{xlate_line}, $self->{xlate_col} ) );
354              
355             # There's a chance clip is now undef; but that's OK - that means we're totally invisible
356             }
357              
358             =head2 $rc->translate( $downward, $rightward )
359              
360             Applies a translation to the coordinate system used by C and the
361             absolute-position methods C<*_at>. After this call, all positions used will be
362             offset by the given amount.
363              
364             =cut
365              
366             sub translate
367             {
368             my $self = shift;
369             my ( $downward, $rightward ) = @_;
370              
371             $self->{xlate_line} += $downward;
372             $self->{xlate_col} += $rightward;
373             }
374              
375             =head2 $rc->reset
376              
377             Removes any pending changes and reverts the render context to its default
378             empty state. Undefines the virtual cursor position, resets the clipping
379             rectangle, and clears the stack of saved state.
380              
381             =cut
382              
383             sub reset
384             {
385             my $self = shift;
386              
387             $self->_xs_reset;
388              
389             $self->{texts} = [];
390              
391             $self->{stack} = [];
392              
393             undef $self->{line};
394             undef $self->{col};
395              
396             $self->{clip} = Tickit::Rect->new(
397             top => 0, left => 0, lines => $self->lines, cols => $self->cols,
398             );
399             }
400              
401             # Methods to alter cell states
402              
403             =head2 $rc->clear( $pen )
404              
405             Resets every cell in the buffer to an erased state.
406             A shortcut to calling C for every line.
407              
408             =cut
409              
410             sub clear
411             {
412             my $self = shift;
413             my ( $pen ) = @_;
414              
415             # Since we're about to kill all the content, we should empty the text
416             # buffers first
417             undef @{ $self->{texts} };
418              
419             foreach my $line ( 0 .. $self->lines - 1 ) {
420             $self->erase_at( $line, 0, $self->cols, $pen );
421             }
422             }
423              
424             =head2 $rc->goto( $line, $col )
425              
426             Sets the position of the virtual cursor.
427              
428             =cut
429              
430             sub goto
431             {
432             my $self = shift;
433             @{$self}{qw( line col )} = @_;
434             }
435              
436             =head2 $rc->setpen( $pen )
437              
438             Sets the rendering pen to use for drawing operations. If a pen is set then a
439             C<$pen> argument is optional to any of the drawing methods. If a pen argument
440             is supplied as well as having a stored pen, then the attributes are merged,
441             with the directly-applied pen taking precedence.
442              
443             Successive calls to this method will replace the active pen used, but if there
444             is a saved state on the stack it will be merged with the rendering pen of the
445             most recent saved state.
446              
447             This method may be preferrable to passing pens into multiple C or
448             C calls as it may be more efficient than merging the same pen on every
449             call. If the original pen is still required afterwards, the C /
450             C pair may be useful.
451              
452             =cut
453              
454             sub _merge_pen
455             {
456             my $self = shift;
457             my ( $direct_pen ) = @_;
458              
459             return $self->{pen} unless $direct_pen;
460             return $direct_pen unless $self->{pen};
461              
462             return $direct_pen->clone->default_from( $self->{pen} );
463             }
464              
465             sub setpen
466             {
467             my $self = shift;
468             my ( $pen ) = @_;
469              
470             if( @{ $self->{stack} } and defined( my $prevpen = $self->{stack}[-1]->pen ) ) {
471             $self->{pen} = Tickit::Pen::Immutable->new( $prevpen->getattrs, $pen->getattrs );
472             }
473             elsif( defined $pen ) {
474             $self->{pen} = Tickit::Pen::Immutable->new( $pen->getattrs );
475             }
476             else {
477             undef $self->{pen};
478             }
479             }
480              
481             =head2 $rc->skip_at( $line, $col, $len )
482              
483             Sets the range of cells given to a skipped state. No content will be drawn
484             here, nor will any content existing on the window be erased.
485              
486             Initially, or after calling C, all cells are set to this state.
487              
488             =cut
489              
490             sub skip_at
491             {
492             my $self = shift;
493             my ( $line, $col, $len ) = @_;
494             ( $line, $col, $len ) = $self->_xlate_and_clip( $line, $col, $len ) or return;
495              
496             $self->_xs_make_span( $line, $col, $len )->SKIP()
497             }
498              
499             =head2 $rc->skip( $len )
500              
501             Sets the range of cells at the virtual cursor position to a skipped state, and
502             updates the position.
503              
504             =cut
505              
506             sub skip
507             {
508             my $self = shift;
509             my ( $len ) = @_;
510             defined $self->{line} or croak "Cannot ->skip without a virtual cursor position";
511             $self->skip_at( $self->{line}, $self->{col}, $len );
512             $self->{col} += $len;
513             }
514              
515             =head2 $rc->skip_to( $col )
516              
517             Sets the range of cells from the virtual cursor position until before the
518             given column to a skipped state, and updates the position to the column.
519              
520             If the position is already past this column then the cursor is moved backwards
521             and no buffer changes are made.
522              
523             =cut
524              
525             sub skip_to
526             {
527             my $self = shift;
528             my ( $col ) = @_;
529             defined $self->{line} or croak "Cannot ->skip_to without a virtual cursor position";
530              
531             if( $self->{col} < $col ) {
532             $self->skip_at( $self->{line}, $self->{col}, $col - $self->{col} );
533             }
534              
535             $self->{col} = $col;
536             }
537              
538             sub _text_at
539             {
540             my $self = shift;
541             my ( $line, $col, $text, $len, $pen ) = @_;
542             ( $line, $col, $len, my $startcol ) = $self->_xlate_and_clip( $line, $col, $len ) or return;
543             $pen = $self->_merge_pen( $pen );
544              
545             push @{ $self->{texts} }, $text;
546             my $textidx = $#{$self->{texts}};
547              
548             $self->_xs_make_span( $line, $col, $len )->TEXT(
549             $pen->as_immutable, $textidx, $startcol
550             );
551             }
552              
553             =head2 $rc->text_at( $line, $col, $text, $pen )
554              
555             Sets the range of cells starting at the given position, to render the given
556             text in the given pen.
557              
558             =cut
559              
560             sub text_at
561             {
562             my $self = shift;
563             my ( $line, $col, $text, $pen ) = @_;
564             $self->_text_at( $line, $col, $text, textwidth( $text ), $pen );
565             }
566              
567             =head2 $rc->text( $text, $pen )
568              
569             Sets the range of cells at the virtual cursor position to render the given
570             text in the given pen, and updates the position.
571              
572             =cut
573              
574             sub text
575             {
576             my $self = shift;
577             my ( $text, $pen ) = @_;
578             defined $self->{line} or croak "Cannot ->text without a virtual cursor position";
579             my $len = textwidth( $text );
580             $self->_text_at( $self->{line}, $self->{col}, $text, $len, $pen );
581             $self->{col} += $len;
582             }
583              
584             =head2 $rc->erase_at( $line, $col, $len, $pen )
585              
586             Sets the range of cells given to erase with the given pen.
587              
588             =cut
589              
590             sub erase_at
591             {
592             my $self = shift;
593             my ( $line, $col, $len, $pen ) = @_;
594             ( $line, $col, $len ) = $self->_xlate_and_clip( $line, $col, $len ) or return;
595             $pen = $self->_merge_pen( $pen );
596              
597             $self->_xs_make_span( $line, $col, $len )->ERASE(
598             $pen->as_immutable
599             );
600             }
601              
602             =head2 $rc->erase( $len, $pen )
603              
604             Sets the range of cells at the virtual cursor position to erase with the given
605             pen, and updates the position.
606              
607             =cut
608              
609             sub erase
610             {
611             my $self = shift;
612             my ( $len, $pen ) = @_;
613             defined $self->{line} or croak "Cannot ->erase without a virtual cursor position";
614             $self->erase_at( $self->{line}, $self->{col}, $len, $pen );
615             $self->{col} += $len;
616             }
617              
618             =head2 $rc->erase_to( $col, $pen )
619              
620             Sets the range of cells from the virtual cursor position until before the
621             given column to erase with the given pen, and updates the position to the
622             column.
623              
624             If the position is already past this column then the cursor is moved backwards
625             and no buffer changes are made.
626              
627             =cut
628              
629             sub erase_to
630             {
631             my $self = shift;
632             my ( $col, $pen ) = @_;
633             defined $self->{line} or croak "Cannot ->erase_to without a virtual cursor position";
634              
635             if( $self->{col} < $col ) {
636             $self->erase_at( $self->{line}, $self->{col}, $col - $self->{col}, $pen );
637             }
638              
639             $self->{col} = $col;
640             }
641              
642             =head1 LINE DRAWING
643              
644             The render context buffer supports storing line-drawing characters in cells,
645             and can merge line segments where they meet, attempting to draw the correct
646             character for the segments that meet in each cell.
647              
648             There are three exported constants giving supported styles of line drawing:
649              
650             =over 4
651              
652             =item * LINE_SINGLE
653              
654             A single, thin line
655              
656             =item * LINE_DOUBLE
657              
658             A pair of double, thin lines
659              
660             =item * LINE_THICK
661              
662             A single, thick line
663              
664             =back
665              
666             Note that linedrawing is performed by Unicode characters, and not every
667             possible combination of line segments of differing styles meeting in a cell is
668             supported by Unicode. The following sets of styles may be relied upon:
669              
670             =over 4
671              
672             =item *
673              
674             Any possible combination of only C segments, C segments, or
675             both.
676              
677             =item *
678              
679             Any combination of only C segments, except cells that only have one of
680             the four borders occupied.
681              
682             =item *
683              
684             Any combination of C and C segments except where the style
685             changes between C to C on a vertical or horizontal run.
686              
687             =back
688              
689             Other combinations are not directly supported (i.e. any combination of
690             C and C in the same cell, or any attempt to change from
691             C to C in either the vertical or horizontal direction). To
692             handle these cases, a cell may be rendered with a substitution character which
693             replaces a C or C segment with a C one within that
694             cell. The effect will be the overall shape of the line is retained, but close
695             to the edge or corner it will have the wrong segment type.
696              
697             Conceptually, every cell involved in line drawing has a potential line segment
698             type at each of its four borders to its neighbours. Horizontal lines are drawn
699             though the vertical centre of each cell, and vertical lines are drawn through
700             the horizontal centre.
701              
702             There is a choice of how to handle the ends of line segments, as to whether
703             the segment should go to the centre of each cell, or should continue through
704             the entire body of the cell and stop at the boundary. By default line segments
705             will start and end at the centre of the cells, so that horizontal and vertical
706             lines meeting in a cell will form a neat corner. When drawing isolated lines
707             such as horizontal or vertical rules, it is preferrable that the line go right
708             through the cells at the start and end. To control this behaviour, the
709             C<$caps> bitmask is used. C and C state that the line
710             should consume the whole of the start or end cell, respectively; C
711             is a convenient shortcut specifying both behaviours.
712              
713             A rectangle may be formed by combining two C and two C
714             calls, without end caps:
715              
716             $rc->hline_at( $top, $left, $right, $style, $pen );
717             $rc->hline_at( $bottom, $left, $right, $style, $pen );
718             $rc->vline_at( $top, $bottom, $left, $style, $pen );
719             $rc->vline_at( $top, $bottom, $right, $style, $pen );
720              
721             =cut
722              
723             # Various parts of this code borrowed from Tom Molesworth's Tickit::Canvas
724              
725             # Bitmasks on Cell linemask
726             use constant {
727             # Connections to the next cell upwards
728             NORTH => 0x03,
729             NORTH_SINGLE => 0x01,
730             NORTH_DOUBLE => 0x02,
731             NORTH_THICK => 0x03,
732             NORTH_SHIFT => 0,
733              
734             # Connections to the next cell to the right
735             EAST => 0x0C,
736             EAST_SINGLE => 0x04,
737             EAST_DOUBLE => 0x08,
738             EAST_THICK => 0x0C,
739             EAST_SHIFT => 2,
740              
741             # Connections to the next cell downwards
742             SOUTH => 0x30,
743             SOUTH_SINGLE => 0x10,
744             SOUTH_DOUBLE => 0x20,
745             SOUTH_THICK => 0x30,
746             SOUTH_SHIFT => 4,
747              
748             # Connections to the next cell to the left
749             WEST => 0xC0,
750             WEST_SINGLE => 0x40,
751             WEST_DOUBLE => 0x80,
752             WEST_THICK => 0xC0,
753             WEST_SHIFT => 6,
754             };
755              
756             my @linechars;
757             {
758             local $_;
759             while( ) {
760             chomp;
761             my ( $char, $spec ) = split( m/\s+=>\s+/, $_, 2 );
762              
763             my $mask = 0;
764             $mask |= __PACKAGE__->$_ for $spec =~ m/([A-Z_]+)/g;
765              
766             $linechars[$mask] = $char;
767             }
768              
769             close DATA;
770              
771             # Fill in the gaps
772             foreach my $mask ( 1 .. 255 ) {
773             next if defined $linechars[$mask];
774              
775             # Try with SINGLE instead of THICK, so mask away 0xAA
776             if( my $char = $linechars[$mask & 0xAA] ) {
777             $linechars[$mask] = $char;
778             next;
779             }
780              
781             # The only ones left now are awkward mixes of single/double
782             # Turn DOUBLE into SINGLE
783             my $singlemask = $mask;
784             foreach my $dir (qw( NORTH EAST SOUTH WEST )) {
785             my $dirmask = __PACKAGE__->$dir;
786             my $dirshift = __PACKAGE__->${\"${dir}_SHIFT"};
787              
788             my $dirsingle = LINE_SINGLE << $dirshift;
789             my $dirdouble = LINE_DOUBLE << $dirshift;
790              
791             $singlemask = ( $singlemask & ~$dirmask ) | $dirsingle
792             if ( $singlemask & $dirmask ) == $dirdouble;
793             }
794              
795             if( my $char = $linechars[$singlemask] ) {
796             $linechars[$mask] = $char;
797             next;
798             }
799              
800             die sprintf "TODO: Couldn't find a linechar for %02x\n", $mask;
801             }
802             }
803              
804             sub linecell
805             {
806             my $self = shift;
807             my ( $line, $col, $bits, $pen ) = @_;
808             ( $line, $col ) = $self->_xlate_and_clip( $line, $col, 1 ) or return;
809              
810             my $cell = $self->_xs_getcell( $line, $col );
811             if( $cell->state != LINE ) {
812             $self->_xs_make_span( $line, $col, 1 )->LINE( $pen->as_immutable );
813             }
814              
815             if( !$cell->pen->equiv( $pen ) ) {
816             warn "Pen collision for line cell ($line,$col)\n";
817             $cell->LINE( $pen->as_immutable );
818             }
819              
820             $cell->LINE_more( $bits );
821             }
822              
823             =head2 $rc->hline_at( $line, $startcol, $endcol, $style, $pen, $caps )
824              
825             Draws a horizontal line between the given columns (both are inclusive), in the
826             given line style, with the given pen.
827              
828             =cut
829              
830             sub hline_at
831             {
832             my $self = shift;
833             my ( $line, $startcol, $endcol, $style, $pen, $caps ) = @_;
834             $pen = $self->_merge_pen( $pen );
835             $caps ||= 0;
836              
837             # TODO: _xs_make_span first for efficiency
838             my $east = $style << EAST_SHIFT;
839             my $west = $style << WEST_SHIFT;
840              
841             $self->linecell( $line, $startcol, $east | ($caps & CAP_START ? $west : 0), $pen );
842             foreach my $col ( $startcol+1 .. $endcol-1 ) {
843             $self->linecell( $line, $col, $east | $west, $pen );
844             }
845             $self->linecell( $line, $endcol, $west | ($caps & CAP_END ? $east : 0), $pen );
846             }
847              
848             =head2 $rc->vline_at( $startline, $endline, $col, $style, $pen, $caps )
849              
850             Draws a vertical line between the centres of the given lines (both are
851             inclusive), in the given line style, with the given pen.
852              
853             =cut
854              
855             sub vline_at
856             {
857             my $self = shift;
858             my ( $startline, $endline, $col, $style, $pen, $caps ) = @_;
859             $pen = $self->_merge_pen( $pen );
860             $caps ||= 0;
861              
862             my $south = $style << SOUTH_SHIFT;
863             my $north = $style << NORTH_SHIFT;
864              
865             $self->linecell( $startline, $col, $south | ($caps & CAP_START ? $north : 0), $pen );
866             foreach my $line ( $startline+1 .. $endline-1 ) {
867             $self->linecell( $line, $col, $north | $south, $pen );
868             }
869             $self->linecell( $endline, $col, $north | ($caps & CAP_END ? $south : 0), $pen );
870             }
871              
872             =head2 $rc->char_at( $line, $col, $codepoint, $pen )
873              
874             Sets the given cell to render the given Unicode character (as given by
875             codepoint number, not character string) in the given pen.
876              
877             While this is also achieveable by the C method, this method is
878             implemented without storing a text segment, so can be more efficient than many
879             single-column wide C calls. It will also be more efficient in the C
880             library rewrite.
881              
882             =cut
883              
884             sub char_at
885             {
886             my $self = shift;
887             my ( $line, $col, $codepoint, $pen ) = @_;
888             ( $line, $col ) = $self->_xlate_and_clip( $line, $col, 1 ) or return;
889             $pen = $self->_merge_pen( $pen );
890              
891             $self->_xs_make_span( $line, $col, 1 )->CHAR( $codepoint, $pen->as_immutable );
892             }
893              
894             =head2 $rc->flush_to_window( $win )
895              
896             Renders the stored content to the given L. After this, the
897             context will be cleared and reset back to initial state.
898              
899             =cut
900              
901             # Legacy name
902             *render_to_window = \&flush_to_window;
903             sub flush_to_window
904             {
905             my $self = shift;
906             my ( $win ) = @_;
907              
908             foreach my $line ( 0 .. $self->lines-1 ) {
909             my $phycol;
910              
911             for ( my $col = 0; $col < $self->cols ; ) {
912             my $cell = $self->_xs_getcell( $line, $col );
913              
914             $col += $cell->len, next if $cell->state == SKIP;
915              
916             if( !defined $phycol or $phycol < $col ) {
917             $win->goto( $line, $col );
918             }
919             $phycol = $col;
920              
921             given( $cell->state ) {
922             when( TEXT ) {
923             my $text = $self->{texts}[$cell->textidx];
924             $win->print( substrwidth( $text, $cell->textoffs, $cell->len ), $cell->pen );
925             $phycol += $cell->len;
926             }
927             when( ERASE ) {
928             # No need to set moveend=true to erasech unless we actually
929             # have more content;
930             my $moveend = $col + $cell->len < $self->cols &&
931             $self->_xs_getcell( $line, $col + $cell->len )->state != SKIP;
932              
933             $win->erasech( $cell->len, $moveend || undef, $cell->pen );
934             $phycol += $cell->len;
935             undef $phycol unless $moveend;
936             }
937             when( LINE ) {
938             # This is more efficient and works better with unit testing in
939             # the Perl case but in the C version this is easier just done a
940             # cell at a time
941             my $pen = $cell->pen;
942             my $chars = "";
943             do {
944             $chars .= $linechars[$cell->linemask];
945             $col++;
946             } while( $col < $self->cols and
947             $cell = $self->_xs_getcell( $line, $col ) and
948             $cell->state == LINE and
949             $cell->pen->equiv( $pen ) );
950              
951             $phycol += $win->print( $chars, $pen )->columns;
952              
953             next;
954             }
955             when( CHAR ) {
956             $win->print( chr $cell->codepoint, $cell->pen );
957             $phycol += $cell->len;
958             }
959             default {
960             die "TODO: cell in state ". $cell->state;
961             }
962             }
963              
964             $col += $cell->len;
965             }
966             }
967              
968             $self->reset;
969             }
970              
971             =head1 TODO
972              
973             As this code is still experimental, there are many planned features it
974             currently lacks:
975              
976             =over 2
977              
978             =item *
979              
980             Hole regions, to directly support shadows made by floating windows.
981              
982             =item *
983              
984             Direct rendering to a L instead of a Window.
985              
986             =back
987              
988             =cut
989              
990             =head1 AUTHOR
991              
992             Paul Evans
993              
994             =cut
995              
996             0x55AA;
997              
998             use utf8;
999             __DATA__