diff options
author | Andrej Shadura <andrewsh@debian.org> | 2019-06-23 18:40:19 -0500 |
---|---|---|
committer | Andrej Shadura <andrewsh@debian.org> | 2019-06-23 18:40:19 -0500 |
commit | 67711d917d4666579e7d1b0647f54519ce0d0be6 (patch) | |
tree | 7beeda12084a396a513804a05869aea1c6f34f08 |
Import original source of Tickit-Widget-ScrollBox 0.07
-rw-r--r-- | Build.PL | 30 | ||||
-rw-r--r-- | Changes | 43 | ||||
-rw-r--r-- | LICENSE | 379 | ||||
-rw-r--r-- | MANIFEST | 21 | ||||
-rw-r--r-- | META.json | 50 | ||||
-rw-r--r-- | META.yml | 30 | ||||
-rw-r--r-- | Makefile.PL | 19 | ||||
-rw-r--r-- | README | 151 | ||||
-rw-r--r-- | examples/demo-gridbox.pl | 25 | ||||
-rw-r--r-- | examples/demo-smart.pl | 60 | ||||
-rw-r--r-- | examples/demo.pl | 25 | ||||
-rw-r--r-- | lib/Tickit/Widget/ScrollBox.pm | 690 | ||||
-rw-r--r-- | lib/Tickit/Widget/ScrollBox/Extent.pm | 233 | ||||
-rw-r--r-- | t/00use.t | 10 | ||||
-rw-r--r-- | t/01scrollbox-horizontal.t | 98 | ||||
-rw-r--r-- | t/01scrollbox-vertical.t | 100 | ||||
-rw-r--r-- | t/02input-key.t | 79 | ||||
-rw-r--r-- | t/03input-mouse.t | 101 | ||||
-rw-r--r-- | t/04on_demand.t | 74 | ||||
-rw-r--r-- | t/05smart.t | 102 | ||||
-rw-r--r-- | t/99pod.t | 11 |
21 files changed, 2331 insertions, 0 deletions
diff --git a/Build.PL b/Build.PL new file mode 100644 index 0000000..e0e5ad2 --- /dev/null +++ b/Build.PL @@ -0,0 +1,30 @@ +use strict; +use warnings; + +use Module::Build; + +my $build = Module::Build->new( + module_name => 'Tickit::Widget::ScrollBox', + requires => { + 'Tickit::SingleChildWidget' => '0.36', + 'Tickit::Style' => '0.35', + 'Tickit::Window' => '0.39', + }, + build_requires => { + 'Test::More' => '0.88', # done_testing + 'Test::Refcount' => 0, + 'Tickit::Test' => '0.38', # bugfix to ICH/DCH scrollrect + }, + auto_configure_requires => 0, # Don't add M::B to configure_requires + license => 'perl', + create_makefile_pl => 'traditional', + create_license => 1, + create_readme => 1, + meta_merge => { + resources => { + x_IRC => "irc://irc.freenode.net/#tickit", + }, + }, +); + +$build->create_build_script; @@ -0,0 +1,43 @@ +Revision history for Tickit-Widget-ScrollBox + +0.07 2016/01/06 19:38:39 + [CHANGES] + * Updates for latest Tickit: + + no longer necessary to set $win->expose_after_scroll + * Updated module documentation style to use =head2 barenames + +0.06 2015/04/21 21:43:42 + [BUGFIXES] + * Declare WIDGET_PEN_FROM_STYLE in inline widget classes used in unit + tests to keep Tickit 0.51 deprecation warnings happy (RT103864) + +0.05 2014/10/08 14:52:45 + [BUGFIXES] + * Better handling of content smaller than viewport + +0.04 2014/09/18 21:03:56 + [CHANGES] + * Accept Alt+wheel to scroll horizontally + * Better smart scrolling protocol: have the child widget call + $extent->set_total() rather than abusing the lines/cols protocol + + [BUGFIXES] + * Don't crash on attempts to scroll in non-existent directions + * Ensure scrollbars get redrawn when smart child calls ->set_total + * Ensure key handler methods return a true value to stop event + propagation + +0.03 CHANGES: + * Allow smart-scrolling with a scroll-aware child widget + * Use $win->scroll_with_children to implement non-smart scrolling + more efficiency + * Remember to set WIDGET_PEN_FROM_STYLE + +0.02 CHANGES: + * Allow horizontal scrolling + * Allow display of scrollbars on-demand + * Updated visual style; use line-drawing for scrollbars + * Hide arrow buttons when scrollbar is at full end stop + +0.01 First version, released on an unsuspecting world. + @@ -0,0 +1,379 @@ +This software is copyright (c) 2016 by Paul Evans <leonerd@leonerd.org.uk>. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. + +Terms of the Perl programming language system itself + +a) the GNU General Public License as published by the Free + Software Foundation; either version 1, or (at your option) any + later version, or +b) the "Artistic License" + +--- The GNU General Public License, Version 1, February 1989 --- + +This software is Copyright (c) 2016 by Paul Evans <leonerd@leonerd.org.uk>. + +This is free software, licensed under: + + The GNU General Public License, Version 1, February 1989 + + GNU GENERAL PUBLIC LICENSE + Version 1, February 1989 + + Copyright (C) 1989 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The license agreements of most software companies try to keep users +at the mercy of those companies. By contrast, our General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. The +General Public License applies to the Free Software Foundation's +software and to any other program whose authors commit to using it. +You can use it for your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Specifically, the General Public License is designed to make +sure that you have the freedom to give away or sell copies of free +software, that you receive source code or can get it if you want it, +that you can change the software or use pieces of it in new free +programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of a such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must tell them their rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any program or other work which +contains a notice placed by the copyright holder saying it may be +distributed under the terms of this General Public License. The +"Program", below, refers to any such program or work, and a "work based +on the Program" means either the Program or any work containing the +Program or a portion of it, either verbatim or with modifications. Each +licensee is addressed as "you". + + 1. You may copy and distribute verbatim copies of the Program's source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this +General Public License and to the absence of any warranty; and give any +other recipients of the Program a copy of this General Public License +along with the Program. You may charge a fee for the physical act of +transferring a copy. + + 2. You may modify your copy or copies of the Program or any portion of +it, and copy and distribute such modifications under the terms of Paragraph +1 above, provided that you also do the following: + + a) cause the modified files to carry prominent notices stating that + you changed the files and the date of any change; and + + b) cause the whole of any work that you distribute or publish, that + in whole or in part contains the Program or any part thereof, either + with or without modifications, to be licensed at no charge to all + third parties under the terms of this General Public License (except + that you may choose to grant warranty protection to some or all + third parties, at your option). + + c) If the modified program normally reads commands interactively when + run, you must cause it, when started running for such interactive use + in the simplest and most usual way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this General + Public License. + + d) You may charge a fee for the physical act of transferring a + copy, and you may at your option offer warranty protection in + exchange for a fee. + +Mere aggregation of another independent work with the Program (or its +derivative) on a volume of a storage or distribution medium does not bring +the other work under the scope of these terms. + + 3. You may copy and distribute the Program (or a portion or derivative of +it, under Paragraph 2) in object code or executable form under the terms of +Paragraphs 1 and 2 above provided that you also do one of the following: + + a) accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of + Paragraphs 1 and 2 above; or, + + b) accompany it with a written offer, valid for at least three + years, to give any third party free (except for a nominal charge + for the cost of distribution) a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of + Paragraphs 1 and 2 above; or, + + c) accompany it with the information you received as to where the + corresponding source code may be obtained. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form alone.) + +Source code for a work means the preferred form of the work for making +modifications to it. For an executable file, complete source code means +all the source code for all modules it contains; but, as a special +exception, it need not include source code for modules which are standard +libraries that accompany the operating system on which the executable +file runs, or for standard header files or definitions files that +accompany that operating system. + + 4. You may not copy, modify, sublicense, distribute or transfer the +Program except as expressly provided under this General Public License. +Any attempt otherwise to copy, modify, sublicense, distribute or transfer +the Program is void, and will automatically terminate your rights to use +the Program under this License. However, parties who have received +copies, or rights to use copies, from you under this General Public +License will not have their licenses terminated so long as such parties +remain in full compliance. + + 5. By copying, distributing or modifying the Program (or any work based +on the Program) you indicate your acceptance of this license to do so, +and all its terms and conditions. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the original +licensor to copy, distribute or modify the Program subject to these +terms and conditions. You may not impose any further restrictions on the +recipients' exercise of the rights granted herein. + + 7. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of the license which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +the license, you may choose any version ever published by the Free Software +Foundation. + + 8. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to humanity, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + + To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) 19yy <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 1, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19xx name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the +appropriate parts of the General Public License. Of course, the +commands you use may be called something other than `show w' and `show +c'; they could even be mouse-clicks or menu items--whatever suits your +program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (a program to direct compilers to make passes + at assemblers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +That's all there is to it! + + +--- The Artistic License 1.0 --- + +This software is Copyright (c) 2016 by Paul Evans <leonerd@leonerd.org.uk>. + +This is free software, licensed under: + + The Artistic License 1.0 + +The Artistic License + +Preamble + +The intent of this document is to state the conditions under which a Package +may be copied, such that the Copyright Holder maintains some semblance of +artistic control over the development of the package, while giving the users of +the package the right to use and distribute the Package in a more-or-less +customary fashion, plus the right to make reasonable modifications. + +Definitions: + + - "Package" refers to the collection of files distributed by the Copyright + Holder, and derivatives of that collection of files created through + textual modification. + - "Standard Version" refers to such a Package if it has not been modified, + or has been modified in accordance with the wishes of the Copyright + Holder. + - "Copyright Holder" is whoever is named in the copyright or copyrights for + the package. + - "You" is you, if you're thinking about copying or distributing this Package. + - "Reasonable copying fee" is whatever you can justify on the basis of media + cost, duplication charges, time of people involved, and so on. (You will + not be required to justify it to the Copyright Holder, but only to the + computing community at large as a market that must bear the fee.) + - "Freely Available" means that no fee is charged for the item itself, though + there may be fees involved in handling the item. It also means that + recipients of the item may redistribute it under the same conditions they + received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications derived +from the Public Domain or from the Copyright Holder. A Package modified in such +a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided that +you insert a prominent notice in each changed file stating how and when you +changed that file, and provided that you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or an + equivalent medium, or placing the modifications on a major archive site + such as ftp.uu.net, or by allowing the Copyright Holder to include your + modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict with + standard executables, which must also be provided, and provide a separate + manual page for each non-standard executable that clearly documents how it + differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or executable +form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where to + get the Standard Version. + + b) accompany the distribution with the machine-readable source of the Package + with your modifications. + + c) accompany any non-standard executables with their corresponding Standard + Version executables, giving the non-standard executables non-standard + names, and clearly documenting the differences in manual pages (or + equivalent), together with instructions on where to get the Standard + Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this Package. You +may not charge a fee for this Package itself. However, you may distribute this +Package in aggregate with other (possibly commercial) programs as part of a +larger (possibly commercial) software distribution provided that you do not +advertise this Package as a product of your own. + +6. The scripts and library files supplied as input to or produced as output +from the programs of this Package do not automatically fall under the copyright +of this Package, but belong to whomever generated them, and may be sold +commercially, and may be aggregated with this Package. + +7. C or perl subroutines supplied by you and linked into this Package shall not +be considered part of this Package. + +8. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +The End + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..daaf3a6 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,21 @@ +Build.PL +Changes +examples/demo-gridbox.pl +examples/demo-smart.pl +examples/demo.pl +lib/Tickit/Widget/ScrollBox.pm +lib/Tickit/Widget/ScrollBox/Extent.pm +LICENSE +Makefile.PL +MANIFEST This list of files +META.json +META.yml +README +t/00use.t +t/01scrollbox-horizontal.t +t/01scrollbox-vertical.t +t/02input-key.t +t/03input-mouse.t +t/04on_demand.t +t/05smart.t +t/99pod.t diff --git a/META.json b/META.json new file mode 100644 index 0000000..f56bcef --- /dev/null +++ b/META.json @@ -0,0 +1,50 @@ +{ + "abstract" : "allow a single child widget to be scrolled", + "author" : [ + "Paul Evans <leonerd@leonerd.org.uk>" + ], + "dynamic_config" : 1, + "generated_by" : "Module::Build version 0.4211", + "license" : [ + "perl_5" + ], + "meta-spec" : { + "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", + "version" : "2" + }, + "name" : "Tickit-Widget-ScrollBox", + "prereqs" : { + "build" : { + "requires" : { + "Test::More" : "0.88", + "Test::Refcount" : "0", + "Tickit::Test" : "0.38" + } + }, + "runtime" : { + "requires" : { + "Tickit::SingleChildWidget" : "0.36", + "Tickit::Style" : "0.35", + "Tickit::Window" : "0.39" + } + } + }, + "provides" : { + "Tickit::Widget::ScrollBox" : { + "file" : "lib/Tickit/Widget/ScrollBox.pm", + "version" : "0.07" + }, + "Tickit::Widget::ScrollBox::Extent" : { + "file" : "lib/Tickit/Widget/ScrollBox/Extent.pm", + "version" : "0.07" + } + }, + "release_status" : "stable", + "resources" : { + "license" : [ + "http://dev.perl.org/licenses/" + ], + "x_IRC" : "irc://irc.freenode.net/#tickit" + }, + "version" : "0.07" +} diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..dfa220f --- /dev/null +++ b/META.yml @@ -0,0 +1,30 @@ +--- +abstract: 'allow a single child widget to be scrolled' +author: + - 'Paul Evans <leonerd@leonerd.org.uk>' +build_requires: + Test::More: '0.88' + Test::Refcount: '0' + Tickit::Test: '0.38' +dynamic_config: 1 +generated_by: 'Module::Build version 0.4211, CPAN::Meta::Converter version 2.150001' +license: perl +meta-spec: + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: '1.4' +name: Tickit-Widget-ScrollBox +provides: + Tickit::Widget::ScrollBox: + file: lib/Tickit/Widget/ScrollBox.pm + version: '0.07' + Tickit::Widget::ScrollBox::Extent: + file: lib/Tickit/Widget/ScrollBox/Extent.pm + version: '0.07' +requires: + Tickit::SingleChildWidget: '0.36' + Tickit::Style: '0.35' + Tickit::Window: '0.39' +resources: + IRC: irc://irc.freenode.net/#tickit + license: http://dev.perl.org/licenses/ +version: '0.07' diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..c5f3ce9 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,19 @@ +# Note: this file was auto-generated by Module::Build::Compat version 0.4211 +use ExtUtils::MakeMaker; +WriteMakefile +( + 'NAME' => 'Tickit::Widget::ScrollBox', + 'VERSION_FROM' => 'lib/Tickit/Widget/ScrollBox.pm', + 'PREREQ_PM' => { + 'Test::More' => '0.88', + 'Test::Refcount' => 0, + 'Tickit::SingleChildWidget' => '0.36', + 'Tickit::Style' => '0.35', + 'Tickit::Test' => '0.38', + 'Tickit::Window' => '0.39' + }, + 'INSTALLDIRS' => 'site', + 'EXE_FILES' => [], + 'PL_FILES' => {} +) +; @@ -0,0 +1,151 @@ +NAME + `Tickit::Widget::ScrollBox' - allow a single child widget to be scrolled + +SYNOPSIS + use Tickit; + use Tickit::Widget::ScrollBox; + use Tickit::Widget::Static; + + my $scrollbox = Tickit::Widget::ScrollBox->new( + child => Tickit::Widget::Static->new( + text => join( "\n", map { "The content for line $_" } 1 .. 100 ), + ), + ); + + Tickit->new( root => $scrollbox )->run; + +DESCRIPTION + This container widget draws a scrollbar beside a single child widget and + allows a portion of it to be displayed by scrolling. + +STYLE + Th following style pen prefixes are used: + + scrollbar => PEN + The pen used to render the background of the scroll bar + + scrollmark => PEN + The pen used to render the active scroll position in the scroll bar + + arrow => PEN + The pen used to render the scrolling arrow buttons + + The following style keys are used: + + arrow_up => STRING + arrow_down => STRING + arrow_left => STRING + arrow_right => STRING + Each should be a single character to use for the scroll arrow + buttons. + + The following style actions are used: + + up_1 (<Up>) + down_1 (<Down>) + left_1 (<Left>) + right_1 (<Right>) + Scroll by 1 line + + up_half (<PageUp>) + down_half (<PageDown>) + left_half (<C-Left>) + right_half (<C-Right>) + Scroll by half of the viewport + + to_top (<C-Home>) + to_bottom (<C-End>) + to_leftmost (<Home>) + to_rightmost (<End>) + Scroll to the edge of the area + +CONSTRUCTOR + new + $scrollbox = Tickit::Widget::ScrollBox->new( %args ) + + Constructs a new `Tickit::Widget::ScrollBox' object. + + Takes the following named arguments in addition to those taken by the + base Tickit::SingleChildWidget constructor: + + vertical => BOOL or "on_demand" + horizontal => BOOL or "on_demand" + Whether to apply a scrollbar in the vertical or horizontal + directions. If not given, these default to vertical only. + + If given as the string `on_demand' then the scrollbar will be + optionally be displayed only if needed; if the space given to + the widget is smaller than the child content necessary to + display. + +ACCESSORS + vextent + $vextent = $scrollbox->vextent + + Returns the Tickit::Widget::ScrollBox::Extent object representing the + box's vertical scrolling extent. + + hextent + $hextent = $scrollbox->hextent + + Returns the Tickit::Widget::ScrollBox::Extent object representing the + box's horizontal scrolling extent. + +METHODS + scroll + $scrollbox->scroll( $downward, $rightward ) + + Requests the content be scrolled downward a number of lines and + rightward a number of columns (either of which which may be negative). + + scroll_to + $scrollbox->scroll_to( $top, $left ) + + Requests the content be scrolled such that the given line and column + number of the child's content is the topmost visible in the container. + +SMART SCROLLING + If the child widget declares it supports smart scrolling, then the + ScrollBox will not implement content scrolling on its behalf. Extra + methods are used to co-ordinate the scroll position between the + scrolling-aware child widget and the containing ScrollBox. This is + handled by the following methods on the child widget. + + If smart scrolling is enabled for the child, then its window will be set + to the viewport directly, and the child widget must offset its content + within the window as appropriate. The child must indicate the range of + its scrolling ability by using the `set_total' method on the extent + object it is given. + + $smart = $child->CAN_SCROLL + If this method exists and returns a true value, the ScrollBox will use + smart scrolling. This method must return a true value for this to work, + allowing the method to itself be a proxy, for example, to proxy + scrolling information through a single child widget container. + + $child->set_scrolling_extents( $vextent, $hextent ) + Gives the child widget the vertical and horizontal scrolling extents. + The child widget should save thes values, and inspect the `start' value + of them any time it needs these to implement content offset position + when rendering. + + $child->scrolled( $downward, $rightward, $h_or_v ) + Informs the child widget that one of the scroll positions has changed. + It passes the delta (which may be negative) of each position, and a + string which will be either `"h"' or `"v"' to indicate whether it was an + adjustment of the horizontal or vertical scrollbar. The extent objects + will already have been updated by this point, so the child may also + inspect the `start' value of them to obtain the new absolute offsets. + +TODO + * Choice of left/right and top/bottom bar positions. + + * Click-and-hold on arrow buttons for auto-repeat + + * Allow smarter cooperation with a scrolling-aware child widget; + likely by setting extent objects on the child if it declares to be + supported, and use that instead of an offset child window. + +AUTHOR + Paul Evans <leonerd@leonerd.org.uk> + diff --git a/examples/demo-gridbox.pl b/examples/demo-gridbox.pl new file mode 100644 index 0000000..b897f0d --- /dev/null +++ b/examples/demo-gridbox.pl @@ -0,0 +1,25 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Tickit; +use Tickit::Widget::ScrollBox; +use Tickit::Widget::GridBox; +use Tickit::Widget::Static; + +my $scrollbox = Tickit::Widget::ScrollBox->new( + horizontal => "on_demand", + vertical => "on_demand", + + child => Tickit::Widget::GridBox->new( + children => [ + map { my $row = $_; + [ map { Tickit::Widget::Static->new( text => "Row $row Col $_" ) } 1 .. 10 ] + } 1 .. 10 ], + row_spacing => 1, + col_spacing => 2, + ), +); + +Tickit->new( root => $scrollbox )->run; diff --git a/examples/demo-smart.pl b/examples/demo-smart.pl new file mode 100644 index 0000000..a7c1e42 --- /dev/null +++ b/examples/demo-smart.pl @@ -0,0 +1,60 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Tickit; +use Tickit::Widget::ScrollBox; +use Tickit::Widget::Border; + +my $border = Tickit::Widget::Border->new( + h_border => 6, + v_border => 2, + style => { bg => "green" }, + child => Tickit::Widget::ScrollBox->new( + child => ScrollableWidget->new, + style => { bg => "black" }, + ), +); + +Tickit->new( root => $border )->run; + +package ScrollableWidget; +use base qw( Tickit::Widget ); + +sub lines { 1 } +sub cols { 1 } + +use constant CAN_SCROLL => 1; + +sub set_scrolling_extents +{ + my $self = shift; + ( $self->{vextent}, $self->{hextent} ) = @_; + $self->{vextent}->set_total( 100 ) if $self->{vextent}; + $self->{hextent}->set_total( 50 ) if $self->{hextent}; +} + +sub scrolled +{ + my $self = shift; + my ( $downward, $rightward, $id ) = @_; + + $self->redraw; +} + +sub vextent { shift->{vextent} } +sub hextent { shift->{hextent} } + +sub render_to_rb +{ + my $self = shift; + my ( $rb, $rect ) = @_; + + $rb->clear; + + my $vstart = $self->vextent ? $self->vextent->start : 0; + my $hstart = $self->hextent ? $self->hextent->start : 0; + + $rb->text_at( 1, 1, "Render with vstart=$vstart hstart=$hstart" ); +} diff --git a/examples/demo.pl b/examples/demo.pl new file mode 100644 index 0000000..30814c4 --- /dev/null +++ b/examples/demo.pl @@ -0,0 +1,25 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Tickit; +use Tickit::Widget::ScrollBox; +use Tickit::Widget::Static; + +use Getopt::Long; + +GetOptions( + 'lines|l=i' => \(my $LINES = 100), +) or exit 1; + +my $scrollbox = Tickit::Widget::ScrollBox->new( + horizontal => "on_demand", + vertical => "on_demand", + + child => Tickit::Widget::Static->new( + text => join( "\n", map { "The content for line $_ " x 3 } 1 .. $LINES ), + ), +); + +Tickit->new( root => $scrollbox )->run; diff --git a/lib/Tickit/Widget/ScrollBox.pm b/lib/Tickit/Widget/ScrollBox.pm new file mode 100644 index 0000000..93dbae2 --- /dev/null +++ b/lib/Tickit/Widget/ScrollBox.pm @@ -0,0 +1,690 @@ +# You may distribute under the terms of either the GNU General Public License +# or the Artistic License (the same terms as Perl itself) +# +# (C) Paul Evans, 2013-2016 -- leonerd@leonerd.org.uk + +package Tickit::Widget::ScrollBox; + +use strict; +use warnings; +use base qw( Tickit::SingleChildWidget ); +Tickit::Window->VERSION( '0.39' ); # ->scroll_with_children, default expose_after_scroll +use Tickit::Style; + +our $VERSION = '0.07'; + +use Carp; + +use List::Util qw( max ); + +use Tickit::Widget::ScrollBox::Extent; +use Tickit::RenderBuffer qw( LINE_DOUBLE CAP_BOTH ); + +=head1 NAME + +C<Tickit::Widget::ScrollBox> - allow a single child widget to be scrolled + +=head1 SYNOPSIS + + use Tickit; + use Tickit::Widget::ScrollBox; + use Tickit::Widget::Static; + + my $scrollbox = Tickit::Widget::ScrollBox->new( + child => Tickit::Widget::Static->new( + text => join( "\n", map { "The content for line $_" } 1 .. 100 ), + ), + ); + + Tickit->new( root => $scrollbox )->run; + +=head1 DESCRIPTION + +This container widget draws a scrollbar beside a single child widget and +allows a portion of it to be displayed by scrolling. + +=head1 STYLE + +Th following style pen prefixes are used: + +=over 4 + +=item scrollbar => PEN + +The pen used to render the background of the scroll bar + +=item scrollmark => PEN + +The pen used to render the active scroll position in the scroll bar + +=item arrow => PEN + +The pen used to render the scrolling arrow buttons + +=back + +The following style keys are used: + +=over 4 + +=item arrow_up => STRING + +=item arrow_down => STRING + +=item arrow_left => STRING + +=item arrow_right => STRING + +Each should be a single character to use for the scroll arrow buttons. + +=back + +The following style actions are used: + +=over 4 + +=item up_1 (<Up>) + +=item down_1 (<Down>) + +=item left_1 (<Left>) + +=item right_1 (<Right>) + +Scroll by 1 line + +=item up_half (<PageUp>) + +=item down_half (<PageDown>) + +=item left_half (<C-Left>) + +=item right_half (<C-Right>) + +Scroll by half of the viewport + +=item to_top (<C-Home>) + +=item to_bottom (<C-End>) + +=item to_leftmost (<Home>) + +=item to_rightmost (<End>) + +Scroll to the edge of the area + +=back + +=cut + +style_definition base => + scrollbar_fg => "blue", + scrollmark_bg => "blue", + arrow_rv => 1, + arrow_up => chr 0x25B4, # U+25B4 == Black up-pointing small triangle + arrow_down => chr 0x25BE, # U+25BE == Black down-pointing small triangle + arrow_left => chr 0x25C2, # U+25C2 == Black left-pointing small triangle + arrow_right => chr 0x25B8, # U+25B8 == Black right-pointing small triangle + '<Up>' => "up_1", + '<Down>' => "down_1", + '<Left>' => "left_1", + '<Right>' => "right_1", + '<PageUp>' => "up_half", + '<PageDown>' => "down_half", + '<C-Left>' => "left_half", + '<C-Right>' => "right_half", + '<C-Home>' => "to_top", + '<C-End>' => "to_bottom", + '<Home>' => "to_leftmost", + '<End>' => "to_rightmost", + ; + +use constant WIDGET_PEN_FROM_STYLE => 1; +use constant KEYPRESSES_FROM_STYLE => 1; + +=head1 CONSTRUCTOR + +=cut + +=head2 new + + $scrollbox = Tickit::Widget::ScrollBox->new( %args ) + +Constructs a new C<Tickit::Widget::ScrollBox> object. + +Takes the following named arguments in addition to those taken by the base +L<Tickit::SingleChildWidget> constructor: + +=over 8 + +=item vertical => BOOL or "on_demand" + +=item horizontal => BOOL or "on_demand" + +Whether to apply a scrollbar in the vertical or horizontal directions. If not +given, these default to vertical only. + +If given as the string C<on_demand> then the scrollbar will be optionally be +displayed only if needed; if the space given to the widget is smaller than the +child content necessary to display. + +=back + +=cut + +sub new +{ + my $class = shift; + my %args = @_; + + my $vertical = delete $args{vertical} // 1; + my $horizontal = delete $args{horizontal}; + + my $child = delete $args{child}; + + my $self = $class->SUPER::new( %args ); + + $self->{vextent} = Tickit::Widget::ScrollBox::Extent->new( $self, "v" ) if $vertical; + $self->{hextent} = Tickit::Widget::ScrollBox::Extent->new( $self, "h" ) if $horizontal; + + $self->{v_on_demand} = $vertical ||'' eq "on_demand"; + $self->{h_on_demand} = $horizontal||'' eq "on_demand"; + + $self->add( $child ) if $child; + + return $self; +} + +=head1 ACCESSORS + +=cut + +sub lines +{ + my $self = shift; + return $self->child->lines + ( $self->hextent ? 1 : 0 ); +} + +sub cols +{ + my $self = shift; + return $self->child->cols + ( $self->vextent ? 1 : 0 ); +} + +=head2 vextent + + $vextent = $scrollbox->vextent + +Returns the L<Tickit::Widget::ScrollBox::Extent> object representing the box's +vertical scrolling extent. + +=cut + +sub vextent +{ + my $self = shift; + return $self->{vextent}; +} + +sub _v_visible +{ + my $self = shift; + return 0 unless my $vextent = $self->{vextent}; + return 1 unless $self->{v_on_demand}; + return $vextent->limit > 0; +} + +=head2 hextent + + $hextent = $scrollbox->hextent + +Returns the L<Tickit::Widget::ScrollBox::Extent> object representing the box's +horizontal scrolling extent. + +=cut + +sub hextent +{ + my $self = shift; + return $self->{hextent}; +} + +sub _h_visible +{ + my $self = shift; + return 0 unless my $hextent = $self->{hextent}; + return 1 unless $self->{h_on_demand}; + return $hextent->limit > 0; +} + +=head1 METHODS + +=cut + +sub children_changed +{ + my $self = shift; + if( my $child = $self->child ) { + my $scrollable = $self->{child_is_scrollable} = $child->can( "CAN_SCROLL" ) && $child->CAN_SCROLL; + + if( $scrollable ) { + foreach my $method (qw( set_scrolling_extents scrolled )) { + $child->can( $method ) or croak "ScrollBox child cannot ->$method - do you implement it?"; + } + + my $vextent = $self->vextent; + my $hextent = $self->hextent; + + $child->set_scrolling_extents( $vextent, $hextent ); + defined $vextent->real_total or croak "ScrollBox child did not set vextent->total" if $vextent; + defined $hextent->real_total or croak "ScrollBox child did not set hextent->total" if $hextent; + } + } + $self->SUPER::children_changed; +} + +sub reshape +{ + my $self = shift; + + my $window = $self->window or return; + my $child = $self->child or return; + + my $vextent = $self->vextent; + my $hextent = $self->hextent; + + if( !$self->{child_is_scrollable} ) { + $vextent->set_total( $child->lines ) if $vextent; + $hextent->set_total( $child->cols ) if $hextent; + } + + my $v_spare = ( $vextent ? $vextent->real_total : $window->lines-1 ) - $window->lines; + my $h_spare = ( $hextent ? $hextent->real_total : $window->cols-1 ) - $window->cols; + + # visibility of each bar might depend on the visibility of the other, if it + # it was exactly at limit + $v_spare++ if $v_spare == 0 and $h_spare > 0; + $h_spare++ if $h_spare == 0 and $v_spare > 0; + + my $v_visible = $vextent && ( !$self->{v_on_demand} || $v_spare > 0 ); + my $h_visible = $hextent && ( !$self->{h_on_demand} || $h_spare > 0 ); + + my @viewportgeom = ( 0, 0, + $window->lines - ( $h_visible ? 1 : 0 ), + $window->cols - ( $v_visible ? 1 : 0 ) ); + + my $viewport; + if( $viewport = $self->{viewport} ) { + $viewport->change_geometry( @viewportgeom ); + } + else { + $viewport = $window->make_sub( @viewportgeom ); + $self->{viewport} = $viewport; + } + + $vextent->set_viewport( $viewport->lines ) if $vextent; + $hextent->set_viewport( $viewport->cols ) if $hextent; + + if( $self->{child_is_scrollable} ) { + $child->set_window( $viewport ) unless $child->window; + } + else { + my ( $childtop, $childlines ) = + $vextent ? ( -$vextent->start, $vextent->total ) + : ( 0, max( $child->lines, $viewport->lines ) ); + + my ( $childleft, $childcols ) = + $hextent ? ( -$hextent->start, $hextent->total ) + : ( 0, max( $child->cols, $viewport->cols ) ); + + my @childgeom = ( $childtop, $childleft, $childlines, $childcols ); + + if( my $childwin = $child->window ) { + $childwin->change_geometry( @childgeom ); + } + else { + $childwin = $viewport->make_sub( @childgeom ); + $child->set_window( $childwin ); + } + } +} + +sub window_lost +{ + my $self = shift; + $self->SUPER::window_lost( @_ ); + + $self->{viewport}->close if $self->{viewport}; + + undef $self->{viewport}; +} + +=head2 scroll + + $scrollbox->scroll( $downward, $rightward ) + +Requests the content be scrolled downward a number of lines and rightward a +number of columns (either of which which may be negative). + +=cut + +sub scroll +{ + my $self = shift; + my ( $downward, $rightward ) = @_; + $self->vextent->scroll( $downward ) if $self->vextent and defined $downward; + $self->hextent->scroll( $rightward ) if $self->hextent and defined $rightward; +} + +=head2 scroll_to + + $scrollbox->scroll_to( $top, $left ) + +Requests the content be scrolled such that the given line and column number of +the child's content is the topmost visible in the container. + +=cut + +sub scroll_to +{ + my $self = shift; + my ( $top, $left ) = @_; + $self->vextent->scroll_to( $top ) if $self->vextent and defined $top; + $self->hextent->scroll_to( $left ) if $self->hextent and defined $left; +} + +sub _extent_scrolled +{ + my $self = shift; + my ( $id, $delta, $value ) = @_; + + my $vextent = $self->vextent; + my $hextent = $self->hextent; + + if( my $win = $self->window ) { + if( $id eq "v" ) { + $win->expose( Tickit::Rect->new( + top => 0, lines => $win->lines, + left => $win->cols - 1, cols => 1, + ) ); + } + elsif( $id eq "h" ) { + $win->expose( Tickit::Rect->new( + top => $win->lines - 1, lines => 1, + left => 0, cols => $win->cols, + ) ); + } + } + + # Extents use $delta = 0 to just request a redraw e.g. on change of total + return if $delta == 0; + + my $child = $self->child or return; + + my ( $downward, $rightward ) = ( 0, 0 ); + if( $id eq "v" ) { + $downward = $delta; + } + elsif( $id eq "h" ) { + $rightward = $delta; + } + + if( $self->{child_is_scrollable} ) { + $child->scrolled( $downward, $rightward, $id ); + } + else { + my $childwin = $child->window or return; + + $childwin->reposition( $vextent ? -$vextent->start : 0, + $hextent ? -$hextent->start : 0 ); + + my $viewport = $self->{viewport}; + $viewport->scroll_with_children( $downward, $rightward ); + } +} + +sub render_to_rb +{ + my $self = shift; + my ( $rb, $rect ) = @_; + my $win = $self->window or return; + + my $lines = $win->lines; + my $cols = $win->cols; + + my $scrollbar_pen = $self->get_style_pen( "scrollbar" ); + my $scrollmark_pen = $self->get_style_pen( "scrollmark" ); + my $arrow_pen = $self->get_style_pen( "arrow" ); + + my $v_visible = $self->_v_visible; + my $h_visible = $self->_h_visible; + + if( $v_visible and $rect->right == $cols ) { + my $vextent = $self->vextent; + my ( $bar_top, $mark_top, $mark_bottom, $bar_bottom ) = + $vextent->scrollbar_geom( 1, $lines - 2 - ( $h_visible ? 1 : 0 ) ); + my $start = $vextent->start; + + $rb->text_at ( 0, $cols-1, + $start > 0 ? $self->get_style_values( "arrow_up" ) : " ", $arrow_pen ); + $rb->vline_at( $bar_top, $mark_top-1, $cols-1, LINE_DOUBLE, $scrollbar_pen, CAP_BOTH ) if $mark_top > $bar_top; + $rb->erase_at( $_, $cols-1, 1, $scrollmark_pen ) for $mark_top .. $mark_bottom-1; + $rb->vline_at( $mark_bottom, $bar_bottom-1, $cols-1, LINE_DOUBLE, $scrollbar_pen, CAP_BOTH ) if $bar_bottom > $mark_bottom; + $rb->text_at ( $bar_bottom, $cols-1, + $start < $vextent->limit ? $self->get_style_values( "arrow_down" ) : " ", $arrow_pen ); + } + + if( $h_visible and $rect->bottom == $lines ) { + my $hextent = $self->hextent; + + my ( $bar_left, $mark_left, $mark_right, $bar_right ) = + $hextent->scrollbar_geom( 1, $cols - 2 - ( $v_visible ? 1 : 0 ) ); + my $start = $hextent->start; + + $rb->goto( $lines-1, 0 ); + + $rb->text_at( $lines-1, 0, + $start > 0 ? $self->get_style_values( "arrow_left" ) : " ", $arrow_pen ); + $rb->hline_at( $lines-1, $bar_left, $mark_left-1, LINE_DOUBLE, $scrollbar_pen, CAP_BOTH ) if $mark_left > $bar_left; + $rb->erase_at( $lines-1, $mark_left, $mark_right - $mark_left, $scrollmark_pen ); + $rb->hline_at( $lines-1, $mark_right, $bar_right-1, LINE_DOUBLE, $scrollbar_pen, CAP_BOTH ) if $bar_right > $mark_right; + $rb->text_at( $lines-1, $bar_right, + $start < $hextent->limit ? $self->get_style_values( "arrow_right" ) : " ", $arrow_pen ); + + $rb->erase_at( $lines-1, $cols-1, 1 ) if $v_visible; + } +} + +sub key_up_1 { my $vextent = shift->vextent or return; $vextent->scroll( -1 ); 1 } +sub key_down_1 { my $vextent = shift->vextent or return; $vextent->scroll( +1 ); 1 } +sub key_left_1 { my $hextent = shift->hextent or return; $hextent->scroll( -1 ); 1 } +sub key_right_1 { my $hextent = shift->hextent or return; $hextent->scroll( +1 ); 1 } + +sub key_up_half { my $vextent = shift->vextent or return; $vextent->scroll( -int( $vextent->viewport / 2 ) ); 1 } +sub key_down_half { my $vextent = shift->vextent or return; $vextent->scroll( +int( $vextent->viewport / 2 ) ); 1 } +sub key_left_half { my $hextent = shift->hextent or return; $hextent->scroll( -int( $hextent->viewport / 2 ) ); 1 } +sub key_right_half { my $hextent = shift->hextent or return; $hextent->scroll( +int( $hextent->viewport / 2 ) ); 1 } + +sub key_to_top { my $vextent = shift->vextent or return; $vextent->scroll_to( 0 ); 1 } +sub key_to_bottom { my $vextent = shift->vextent or return; $vextent->scroll_to( $vextent->limit ); 1 } +sub key_to_leftmost { my $hextent = shift->hextent or return; $hextent->scroll_to( 0 ); 1 } +sub key_to_rightmost { my $hextent = shift->hextent or return; $hextent->scroll_to( $hextent->limit ); 1 } + +sub on_mouse +{ + my $self = shift; + my ( $args ) = @_; + + my $type = $args->type; + my $button = $args->button; + + my $lines = $self->window->lines; + my $cols = $self->window->cols; + + my $vextent = $self->vextent; + my $hextent = $self->hextent; + + my $vlen = $lines - 2 - ( $self->_h_visible ? 1 : 0 ); + my $hlen = $cols - 2 - ( $self->_v_visible ? 1 : 0 ); + + if( $type eq "press" and $button == 1 ) { + if( $vextent and $args->col == $cols-1 ) { + # Click in vertical scrollbar + my ( undef, $mark_top, $mark_bottom, $bar_bottom ) = $vextent->scrollbar_geom( 1, $vlen ); + my $line = $args->line; + + if( $line == 0 ) { # up arrow + $vextent->scroll( -1 ); + } + elsif( $line < $mark_top ) { # above area + $vextent->scroll( -int( $vextent->viewport / 2 ) ); + } + elsif( $line < $mark_bottom ) { + # press in mark - ignore for now - TODO: prelight? + } + elsif( $line < $bar_bottom ) { # below area + $vextent->scroll( +int( $vextent->viewport / 2 ) ); + } + elsif( $line == $bar_bottom ) { # down arrow + $vextent->scroll( +1 ); + } + return 1; + } + if( $hextent and $args->line == $lines-1 ) { + # Click in horizontal scrollbar + my ( undef, $mark_left, $mark_right, $bar_right ) = $hextent->scrollbar_geom( 1, $hlen ); + my $col = $args->col; + + if( $col == 0 ) { # left arrow + $hextent->scroll( -1 ); + } + elsif( $col < $mark_left ) { # above area + $hextent->scroll( -int( $hextent->viewport / 2 ) ); + } + elsif( $col < $mark_right ) { + # press in mark - ignore for now - TODO: prelight + } + elsif( $col < $bar_right ) { # below area + $hextent->scroll( +int( $hextent->viewport / 2 ) ); + } + elsif( $col == $bar_right ) { # right arrow + $hextent->scroll( +1 ); + } + return 1; + } + } + elsif( $type eq "drag_start" and $button == 1 ) { + if( $vextent and $args->col == $cols-1 ) { + # Drag in vertical scrollbar + my ( undef, $mark_top, $mark_bottom ) = $vextent->scrollbar_geom( 1, $vlen ); + my $line = $args->line; + + if( $line >= $mark_top and $line < $mark_bottom ) { + $self->{drag_offset} = $line - $mark_top; + $self->{drag_bar} = "v"; + return 1; + } + } + if( $hextent and $args->line == $lines-1 ) { + # Drag in horizontal scrollbar + my ( undef, $mark_left, $mark_right ) = $hextent->scrollbar_geom( 1, $hlen ); + my $col = $args->col; + + if( $col >= $mark_left and $col < $mark_right ) { + $self->{drag_offset} = $col - $mark_left; + $self->{drag_bar} = "h"; + return 1; + } + } + } + elsif( $type eq "drag" and $button == 1 and defined( $self->{drag_offset} ) ) { + if( $self->{drag_bar} eq "v" ) { + my $want_bar_top = $args->line - $self->{drag_offset} - 1; + my $want_top = int( $want_bar_top * $vextent->total / $vlen + 0.5 ); + $vextent->scroll_to( $want_top ); + } + if( $self->{drag_bar} eq "h" ) { + my $want_bar_left = $args->col - $self->{drag_offset} - 1; + my $want_left = int( $want_bar_left * $hextent->total / $hlen + 0.5 ); + $hextent->scroll_to( $want_left ); + } + } + elsif( $type eq "drag_stop" ) { + undef $self->{drag_offset}; + } + elsif( $type eq "wheel" ) { + # Alt-wheel for horizontal + my $extent = $args->mod & 2 ? $self->hextent : $self->vextent; + $extent->scroll( -5 ) if $extent and $button eq "up"; + $extent->scroll( +5 ) if $extent and $button eq "down"; + return 1; + } +} + +=head1 SMART SCROLLING + +If the child widget declares it supports smart scrolling, then the ScrollBox +will not implement content scrolling on its behalf. Extra methods are used to +co-ordinate the scroll position between the scrolling-aware child widget and +the containing ScrollBox. This is handled by the following methods on the +child widget. + +If smart scrolling is enabled for the child, then its window will be set to +the viewport directly, and the child widget must offset its content within the +window as appropriate. The child must indicate the range of its scrolling +ability by using the C<set_total> method on the extent object it is given. + +=head2 $smart = $child->CAN_SCROLL + +If this method exists and returns a true value, the ScrollBox will use smart +scrolling. This method must return a true value for this to work, allowing the +method to itself be a proxy, for example, to proxy scrolling information +through a single child widget container. + +=head2 $child->set_scrolling_extents( $vextent, $hextent ) + +Gives the child widget the vertical and horizontal scrolling extents. The +child widget should save thes values, and inspect the C<start> value of them +any time it needs these to implement content offset position when +rendering. + +=head2 $child->scrolled( $downward, $rightward, $h_or_v ) + +Informs the child widget that one of the scroll positions has changed. It +passes the delta (which may be negative) of each position, and a string which +will be either C<"h"> or C<"v"> to indicate whether it was an adjustment of +the horizontal or vertical scrollbar. The extent objects will already have +been updated by this point, so the child may also inspect the C<start> value +of them to obtain the new absolute offsets. + +=cut + +=head1 TODO + +=over 4 + +=item * + +Choice of left/right and top/bottom bar positions. + +=item * + +Click-and-hold on arrow buttons for auto-repeat + +=item * + +Allow smarter cooperation with a scrolling-aware child widget; likely by +setting extent objects on the child if it declares to be supported, and use +that instead of an offset child window. + +=back + +=cut + +=head1 AUTHOR + +Paul Evans <leonerd@leonerd.org.uk> + +=cut + +0x55AA; diff --git a/lib/Tickit/Widget/ScrollBox/Extent.pm b/lib/Tickit/Widget/ScrollBox/Extent.pm new file mode 100644 index 0000000..fb0d2a1 --- /dev/null +++ b/lib/Tickit/Widget/ScrollBox/Extent.pm @@ -0,0 +1,233 @@ +# You may distribute under the terms of either the GNU General Public License +# or the Artistic License (the same terms as Perl itself) +# +# (C) Paul Evans, 2013-2016 -- leonerd@leonerd.org.uk + +package Tickit::Widget::ScrollBox::Extent; + +use strict; +use warnings; + +our $VERSION = '0.07'; + +use Scalar::Util qw( weaken ); + +=head1 NAME + +C<Tickit::Widget::ScrollBox::Extent> - represents the range of scrolling extent + +=head1 DESCRIPTION + +This small utility object stores the effective scrolling range for a +L<Tickit::Widget::ScrollBox>. They are not constructed directly, but instead +returned by the C<hextent> and C<vextent> methods of the associated ScrollBox. + +=cut + +sub new +{ + my $class = shift; + my ( $scrollbox, $id ) = @_; + + my $self = bless { + start => 0, + id => $id, + }, $class; + + weaken( $self->{scrollbox} = $scrollbox ); + return $self; +} + +sub _clamp +{ + my $self = shift; + + my $limit = $self->total - $self->viewport; + $self->{start} = $limit if $self->{start} > $limit; +} + +# Internal; used by T:W:ScrollBox +sub set_viewport +{ + my $self = shift; + my ( $viewport ) = @_; + + $self->{viewport} = $viewport; + $self->_clamp if defined $self->{total}; +} + +=head1 ACCESSORS + +=cut + +=head2 viewport + + $viewport = $extent->viewport + +Returns the size of the viewable portion of the scrollable area (the +"viewport"). + +=cut + +sub viewport +{ + my $self = shift; + return $self->{viewport}; +} + +=head2 total + + $total = $extent->total + +Returns the total size of the scrollable area; which is always at least the +size of the viewport. + +=head2 set_total + + $extent->set_total( $total ) + +Sets the total size of the scrollable area. This method should only be used by +the child widget, when it is performing smart scrolling. + +=cut + +sub total +{ + my $self = shift; + my $viewport = $self->{viewport}; + my $total = $self->{total}; + $total = $viewport if $viewport > $total; + return $total; +} + +sub real_total +{ + my $self = shift; + return $self->{total}; +} + +sub set_total +{ + my $self = shift; + my ( $total ) = @_; + + return if defined $self->{total} and $self->{total} == $total; + + $self->{total} = $total; + $self->_clamp if defined $self->{viewport}; + + $self->{scrollbox}->_extent_scrolled( $self->{id}, 0, undef ); +} + +=head2 limit + + $limit = $extent->limit + +Returns the limit of the offset; the largest value the start offset may be. +This is simply C<$total - $viewport>, with a limit applied so that it returns +zero rather than a negative value, in the case that the viewport is larger +than the total. + +=cut + +sub limit +{ + my $self = shift; + my $limit = $self->{total} - $self->{viewport}; + $limit = 0 if $limit < 0; + return $limit; +} + +=head2 start + + $start = $extent->start + +Returns the start position offset of the viewport within the total area. This +is always at least zero, and no greater than the limit. + +=cut + +sub start +{ + my $self = shift; + return $self->{start}; +} + +=head1 METHODS + +=cut + +=head2 scroll + + $extent->scroll( $delta ) + +Requests to move the start by the amount given. This will be clipped if it +moves outside the allowed range. + +=cut + +sub scroll +{ + my $self = shift; + $self->scroll_to( $self->start + $_[0] ); +} + +=head2 scroll_to + + $extent->scroll_to( $new_start ) + +Requests to move the start to that given. This will be clipped if it is +outside the allowed range. + +=cut + +sub scroll_to +{ + my $self = shift; + my ( $start ) = @_; + + my $limit = $self->limit; + $start = $limit if $start > $limit; + + $start = 0 if $start < 0; + + return if $self->{start} == $start; + + my $delta = $start - $self->{start}; + + $self->{start} = $start; + $self->{scrollbox}->_extent_scrolled( $self->{id}, $delta, $start ); +} + +=head2 scrollbar_geom + + ( $bar_top, $mark_top, $mark_bottom, $bar_bottom ) = $extent->scrollbar_geom( $top, $length ) + +Calculates the start and end positions of a scrollbar and the mark within it +to represent the position of the extent. Returns four integer indexes within +C<$length>. + +=cut + +sub scrollbar_geom +{ + my $self = shift; + my ( $top, $length ) = @_; + + my $total = $self->total; + + my $bar_length = int( $self->viewport * $length / $total + 0.5 ); + $bar_length = 1 if $bar_length < 1; + + my $bar_start = $top + int( $self->start * $length / $total + 0.5 ); + + return ( $top, $bar_start, $bar_start + $bar_length, $top + $length ); +} + +=head1 AUTHOR + +Paul Evans <leonerd@leonerd.org.uk> + +=cut + +0x55AA; diff --git a/t/00use.t b/t/00use.t new file mode 100644 index 0000000..115770a --- /dev/null +++ b/t/00use.t @@ -0,0 +1,10 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; + +use_ok( 'Tickit::Widget::ScrollBox' ); + +done_testing; diff --git a/t/01scrollbox-horizontal.t b/t/01scrollbox-horizontal.t new file mode 100644 index 0000000..67a8f52 --- /dev/null +++ b/t/01scrollbox-horizontal.t @@ -0,0 +1,98 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use utf8; + +use Test::More; +use Test::Refcount; + +use Tickit::Test; + +use Tickit::Widget::Static; +use Tickit::Widget::ScrollBox; + +my $win = mk_window; + +my $static = Tickit::Widget::Static->new( + text => join "\n", map { "Content on line $_:" . join ",", 1 .. 50 } 1 .. 10 +); + +my $widget = Tickit::Widget::ScrollBox->new( + horizontal => 1, + vertical => 0, +); + +ok( defined $widget, 'defined $widget' ); +is_oneref( $widget, '$widget has refcount 1 initially' ); + +$widget->add( $static ); + +is_oneref( $widget, '$widget has refcount 1 after adding child' ); + +is( $widget->lines, 11, '$widget wants 11 lines' ); +is( $widget->cols, 159, '$widget wants 159 cols' ); + +my $hextent = $widget->hextent; + +ok( defined $hextent, '$widget has ->hextent' ); + +$widget->set_window( $win ); + +ok( defined $static->window, '$static has window after $widget->set_window' ); + +is( $static->window->top, 0, '$static window starts on line 0' ); +is( $static->window->left, 0, '$static window starts on column 0' ); +is( $static->window->lines, 24, '$static given 24 line window' ); +is( $static->window->cols, 159, '$static given 159 column window' ); + +is( $hextent->total, 159, '$hextent->total is 159' ); +is( $hextent->viewport, 80, '$hextent->viewport is 80' ); +is( $hextent->start, 0, '$hextent->start is 0' ); + +flush_tickit; + +is_display( [ ( map +[TEXT("Content on line $_:" . join( ",", 1 .. 24 ) )], 1 .. 9 ), + ( map +[TEXT("Content on line $_:" . join( ",", 1 .. 23 ) . ",2" )], 10 ), + BLANKLINES(14), + [TEXT(" ",rv=>1), + BLANK(39,bg=>4), + TEXT("═"x39,fg=>4), + TEXT("\x{25B8}",rv=>1)] ], + 'Display initially' ); + +$widget->scroll( undef, +10 ); +flush_tickit; + +is( $static->window->left, -10, '$static window starts on column -10 after ->scroll +10' ); +is( $hextent->start, 10, '$hextent->start is now 10 after ->scroll +10' ); + +is_display( [ ( map +[TEXT(" line $_:" . join( ",", 1 .. 27 ) . "," )], 1 .. 9 ), + ( map +[TEXT(" line $_:" . join( ",", 1 .. 27 ) )], 10 ), + BLANKLINES(14), + [TEXT("\x{25C2}",rv=>1), + TEXT("═"x5,fg=>4), + BLANK(39,bg=>4), + TEXT("═"x34,fg=>4), + TEXT("\x{25B8}",rv=>1)] ], + 'Display after scroll +10' ); + +$hextent->scroll_to( 25 ); +flush_tickit; + +is( $static->window->left, -25, '$static window starts on column -10 after ->scroll_to 25' ); +is( $hextent->start, 25, '$hextent->start is now 10 after ->scroll_to 25' ); + +is_display( [ ( map +[TEXT("," . join( ",", 5 .. 32 ) . "," )], 1 .. 9 ), + ( map +[TEXT(join( ",", 4 .. 32 ) )], 10 ), + BLANKLINES(14), + [TEXT("\x{25C2}",rv=>1), + TEXT("═"x12,fg=>4), + BLANK(39,bg=>4), + TEXT("═"x27,fg=>4), + TEXT("\x{25B8}",rv=>1)] ], + 'Display after $vextent->scroll_to 25' ); + +is_oneref( $widget, '$widget has refcount 1 at EOF' ); + +done_testing; diff --git a/t/01scrollbox-vertical.t b/t/01scrollbox-vertical.t new file mode 100644 index 0000000..90e6f2f --- /dev/null +++ b/t/01scrollbox-vertical.t @@ -0,0 +1,100 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use utf8; + +use Test::More; +use Test::Refcount; + +use Tickit::Test; + +use Tickit::Widget::Static; +use Tickit::Widget::ScrollBox; + +my $win = mk_window; + +my $static = Tickit::Widget::Static->new( + text => join "\n", map { "Content on line $_" } 1 .. 100 +); + +my $widget = Tickit::Widget::ScrollBox->new; + +ok( defined $widget, 'defined $widget' ); +is_oneref( $widget, '$widget has refcount 1 initially' ); + +$widget->add( $static ); + +is_oneref( $widget, '$widget has refcount 1 after adding child' ); + +is( $widget->lines, 100, '$widget wants 100 lines' ); +is( $widget->cols, 20, '$widget wants 20 cols' ); + +my $vextent = $widget->vextent; + +ok( defined $vextent, '$widget has ->vextent' ); + +$widget->set_window( $win ); + +ok( defined $static->window, '$static has window after $widget->set_window' ); + +is( $static->window->top, 0, '$static window starts on line 0' ); +is( $static->window->left, 0, '$static window starts on column 0' ); +is( $static->window->lines, 100, '$static given 100 line window' ); +is( $static->window->cols, 79, '$static given 79 column window' ); + +is( $vextent->total, 100, '$vextent->total is 100' ); +is( $vextent->viewport, 25, '$vextent->viewport is 25' ); +is( $vextent->start, 0, '$vextent->start is 0' ); + +flush_tickit; + +is_display( [ [TEXT("Content on line 1"), BLANK(62), + TEXT(" ",rv=>1)], + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT(" ",bg=>4) ], 2 .. 7 ), + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT("║",fg=>4) ], 8 .. 24 ), + [TEXT("Content on line 25"), BLANK(61), + TEXT("\x{25BE}",rv=>1)] ], + 'Display initially' ); + +$widget->scroll( +10 ); +flush_tickit; + +is( $static->window->top, -10, '$static window starts on line -10 after ->scroll +10' ); +is( $vextent->start, 10, '$vextent->start is now 10 after ->scroll +10' ); + +is_display( [ [TEXT("Content on line 11"), BLANK(61), + TEXT("\x{25B4}",rv=>1)], + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT("║",fg=>4) ], 12 .. 13 ), + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT(" ",bg=>4) ], 14 .. 19 ), + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT("║",fg=>4) ], 20 .. 34 ), + [TEXT("Content on line 35"), BLANK(61), + TEXT("\x{25BE}",rv=>1)] ], + 'Display after scroll +10' ); + +$vextent->scroll_to( 25 ); +flush_tickit; + +is( $static->window->top, -25, '$static window starts on line -25 after ->scroll_to 25' ); +is( $vextent->start, 25, '$vextent->start is now 25 after ->scroll_to 25' ); + +is_display( [ [TEXT("Content on line 26"), BLANK(61), + TEXT("\x{25B4}",rv=>1)], + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT("║",fg=>4) ], 27 .. 32 ), + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT(" ",bg=>4) ], 33 .. 38 ), + ( map +[TEXT("Content on line $_"), BLANK(63-length$_), + TEXT("║",fg=>4) ], 39 .. 49 ), + [TEXT("Content on line 50"), BLANK(61), + TEXT("\x{25BE}",rv=>1)] ], + 'Display after $vextent->scroll_to 25' ); + +is_oneref( $widget, '$widget has refcount 1 at EOF' ); + +done_testing; diff --git a/t/02input-key.t b/t/02input-key.t new file mode 100644 index 0000000..3000cdf --- /dev/null +++ b/t/02input-key.t @@ -0,0 +1,79 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; + +use Tickit::Test; + +use Tickit::Widget::Static; +use Tickit::Widget::ScrollBox; + +my $win = mk_window; + +my $static = Tickit::Widget::Static->new( + text => join "\n", map { "X" x 200 } 1 .. 100 +); + +my $widget = Tickit::Widget::ScrollBox->new( + horizontal => 1, + vertical => 1, +); + +$widget->add( $static ); +$widget->set_window( $win ); + +my $vextent = $widget->vextent; +my $hextent = $widget->hextent; + +# We won't use the is_display tests here because they're annoying to write. +# Having asserted that the Extent objects do the right thing in earlier tests, +# we'll just check the input events have the right effect on those. + +is( $vextent->start, 0, 'start is 0 initially' ); +is( $hextent->start, 0, 'hextent start is 0 initially' ); + +# vertical +{ + presskey( key => "Down" ); + is( $vextent->start, 1, 'start moves down +1 after <Down>' ); + + presskey( key => "PageDown" ); + is( $vextent->start, 13, 'start moves down +12 after <PageDown>' ); + + presskey( key => "Up" ); + is( $vextent->start, 12, 'start moves up -1 after <Up>' ); + + presskey( key => "PageUp" ); + is( $vextent->start, 0, 'start moves up -12 after <PageUp>' ); + + presskey( key => "C-End", 0x04 ); + is( $vextent->start, 76, 'start moves to 76 after <C-End>' ); + + presskey( key => "C-Home", 0x04 ); + is( $vextent->start, 0, 'start moves to 0 after <C-Home>' ); +} + +# horizontal +{ + presskey( key => "Right" ); + is( $hextent->start, 1, 'start moves right +1 after <Right>' ); + + presskey( key => "C-Right", 0x4 ); + is( $hextent->start, 40, 'start moves right +39 after <C-Right>' ); + + presskey( key => "Left" ); + is( $hextent->start, 39, 'start moves up -1 after <Left>' ); + + presskey( key => "C-Left", 0x4 ); + is( $hextent->start, 0, 'start moves up -39 after <C-Left>' ); + + presskey( key => "End" ); + is( $hextent->start, 121, 'start moves to 121 after <End>' ); + + presskey( key => "Home" ); + is( $hextent->start, 0, 'start moves to 0 after <Home>' ); +} + +done_testing; diff --git a/t/03input-mouse.t b/t/03input-mouse.t new file mode 100644 index 0000000..32fce62 --- /dev/null +++ b/t/03input-mouse.t @@ -0,0 +1,101 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; + +use Tickit::Test; + +use Tickit::Widget::Static; +use Tickit::Widget::ScrollBox; + +my $win = mk_window; + +my $static = Tickit::Widget::Static->new( + text => join "\n", map { "X" x 200 } 1 .. 100 +); + +my $widget = Tickit::Widget::ScrollBox->new( + horizontal => 1, + vertical => 1, +); + +$widget->add( $static ); +$widget->set_window( $win ); + +my $vextent = $widget->vextent; +my $hextent = $widget->hextent; + +# We won't use the is_display tests here because they're annoying to write. +# Having asserted that the Extent objects do the right thing in earlier tests, +# we'll just check the input events have the right effect on those. + +is( $vextent->start, 0, 'vextent start is 0 initially' ); +is( $hextent->start, 0, 'hextent start is 0 initially' ); + +# vertical +{ + # down arrow + pressmouse( press => 1, 23, 79 ); + pressmouse( release => 1, 23, 79 ); + is( $vextent->start, 1, 'start moves down +1 after mouse click down arrow' ); + + # 'after' area + pressmouse( press => 1, 21, 79 ); + pressmouse( release => 1, 21, 79 ); + is( $vextent->start, 13, 'start moves down +12 after mouse click after area' ); + + # up arrow + pressmouse( press => 1, 0, 79 ); + pressmouse( release => 1, 0, 79 ); + is( $vextent->start, 12, 'start moves up -1 after mouse click up arrow' ); + + # 'before' area + pressmouse( press => 1, 1, 79 ); + pressmouse( release => 1, 1, 79 ); + is( $vextent->start, 0, 'start moves up -12 after mouse click up arrow' ); + + # click-drag + pressmouse( press => 1, 5, 79 ); + pressmouse( drag => 1, 10, 79 ); + pressmouse( release => 1, 10, 79 ); + is( $vextent->start, 23, 'start is 22 after mouse drag' ); + + # wheel - doesn't have to be in scrollbar + pressmouse( wheel => 'down', 13, 40 ); + is( $vextent->start, 28, 'start moves down +5 after wheel down' ); + pressmouse( wheel => 'up', 13, 40 ); + is( $vextent->start, 23, 'start moves up -5 after wheel up' ); +} + +# horizontal +{ + # right arrow + pressmouse( press => 1, 24, 78 ); + pressmouse( release => 1, 24, 78 ); + is( $hextent->start, 1, 'start moves right +1 after mouse click right arrow' ); + + # 'after' area + pressmouse( press => 1, 24, 72 ); + pressmouse( release => 1, 24, 72 ); + is( $hextent->start, 40, 'start moves right +39 after mouse click after area' ); + + # left arrow + pressmouse( press => 1, 24, 0 ); + pressmouse( release => 1, 24, 0 ); + is( $hextent->start, 39, 'start moves left -1 after mouse click left arrow' ); + + # 'before' area + pressmouse( press => 1, 24, 5 ); + pressmouse( release => 1, 24, 5 ); + is( $hextent->start, 0, 'start moves left -39 after mouse click before area' ); + + # click-drag + pressmouse( press => 1, 24, 20 ); + pressmouse( drag => 1, 24, 30 ); + pressmouse( release => 1, 24, 30 ); + is( $hextent->start, 26, 'start is 26 after mouse drag' ); +} + +done_testing; diff --git a/t/04on_demand.t b/t/04on_demand.t new file mode 100644 index 0000000..16f45a3 --- /dev/null +++ b/t/04on_demand.t @@ -0,0 +1,74 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; + +use Tickit::Test; + +use Tickit::Widget::Static; +use Tickit::Widget::ScrollBox; + +my $win = mk_window; + +# Needs 40x20 +my $static = Tickit::Widget::Static->new( + text => join "\n", map { $_ x 40 } 'A' .. 'T' +); + +my $widget = Tickit::Widget::ScrollBox->new( + horizontal => "on_demand", + vertical => "on_demand", + child => $static, +); + +$widget->set_window( $win ); +flush_tickit; + +# Oversized at 80x25 +{ + ok( !$widget->_h_visible, 'H invisible at 80x25' ); + ok( !$widget->_v_visible, 'V invisible at 80x25' ); +} + +# Undersized vertically at 80x15 +{ + $win->resize( 15, 80 ); + ok( !$widget->_h_visible, 'H invisible at 80x15' ); + ok( $widget->_v_visible, 'V visible at 80x15' ); +} + +# Undersized horizontally at 30x25 +{ + $win->resize( 25, 30 ); + ok( $widget->_h_visible, 'H visible at 30x25' ); + ok( !$widget->_v_visible, 'V invisible at 30x25' ); +} + +# Undersized at 30x15 +{ + $win->resize( 15, 30 ); + ok( $widget->_h_visible, 'H visible at 30x15' ); + ok( $widget->_v_visible, 'V visible at 30x15' ); +} + +# Exactly at limits +{ + $win->resize( 20, 40 ); + ok( !$widget->_h_visible, 'H invisible at 40x20' ); + ok( !$widget->_v_visible, 'V invisible at 40x20' ); +} + +# Making either scrollbar visible forces the other when at-limit +{ + $win->resize( 20, 39 ); + ok( $widget->_h_visible, 'H visible at 39x20' ); + ok( $widget->_v_visible, 'V visible at 39x20' ); + + $win->resize( 19, 40 ); + ok( $widget->_h_visible, 'H visible at 40x19' ); + ok( $widget->_v_visible, 'V visible at 40x19' ); +} + +done_testing; diff --git a/t/05smart.t b/t/05smart.t new file mode 100644 index 0000000..54e3ef9 --- /dev/null +++ b/t/05smart.t @@ -0,0 +1,102 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use utf8; + +use Test::More; + +use Tickit::Test; + +use Tickit::Widget::ScrollBox; + +my $win = mk_window; + +my ( $vextent, $hextent ); +my ( $downward, $rightward ) = (0) x 2; +{ + package ScrollableWidget; + use base qw( Tickit::Widget ); + + use constant CAN_SCROLL => 1; + use constant WIDGET_PEN_FROM_STYLE => 1; + + sub lines { 1 } + sub cols { 1 } + + sub set_scrolling_extents + { + shift; + ( $vextent, $hextent ) = @_; + $vextent->set_total( 100 ); + $hextent->set_total( 50 ) + } + + sub scrolled + { + shift; + $downward += $_[0]; + $rightward += $_[1]; + } + + sub render_to_rb {} +} + +my $child = ScrollableWidget->new; + +my $widget = Tickit::Widget::ScrollBox->new( + child => $child, + horizontal => 1, vertical => 1, +); + +$widget->set_window( $win ); +flush_tickit; + +ok( defined $vextent, '$vextent set' ); +ok( defined $hextent, '$hextent set' ); + +ok( defined $child->window, '$child has window after $widget->set_window' ); + +is( $child->window->top, 0, '$child window starts on line 0' ); +is( $child->window->left, 0, '$child window starts on column 0' ); +is( $child->window->lines, 25, '$child given 25 line window' ); +is( $child->window->cols, 79, '$child given 79 column window' ); + +is_display( [ [ BLANK(79), TEXT(" ",rv=>1)], + ([BLANK(79), TEXT(" ",bg=>4)]) x 6, + ([BLANK(79), TEXT("║",fg=>4)]) x 17, + [ BLANK(79), TEXT("▾",rv=>1)] ], + 'Display initially' ); + +$widget->scroll( +10 ); +flush_tickit; + +is( $downward, 10, '$child informed of scroll +10' ); +$downward = 0; + +is( $child->window->top, 0, '$child window still starts on line 0 after scroll +10' ); + +$widget->scroll_to( 25 ); +flush_tickit; + +is( $downward, 15, '$child informed of scroll_to 25' ); + +is( $child->window->top, 0, '$child window still starts on line 0 after scroll_to 25' ); + +is_display( [ [ BLANK(79), TEXT("▴",rv=>1)], + ([BLANK(79), TEXT("║",fg=>4)]) x 6, + ([BLANK(79), TEXT(" ",bg=>4)]) x 6, + ([BLANK(79), TEXT("║",fg=>4)]) x 11, + [ BLANK(79), TEXT("▾",rv=>1)] ], + 'Display after scrolls' ); + +$vextent->set_total( 50 ); +flush_tickit; + +is_display( [ [ BLANK(79), TEXT("▴",rv=>1)], + ([BLANK(79), TEXT("║",fg=>4)]) x 12, + ([BLANK(79), TEXT(" ",bg=>4)]) x 11, + [ BLANK(79), TEXT(" ",rv=>1)] ], + 'Display after ->set_total 50' ); + +done_testing; diff --git a/t/99pod.t b/t/99pod.t new file mode 100644 index 0000000..eb319fb --- /dev/null +++ b/t/99pod.t @@ -0,0 +1,11 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; + +eval "use Test::Pod 1.00"; +plan skip_all => "Test::Pod 1.00 required for testing POD" if $@; + +all_pod_files_ok(); |