summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrej Shadura <andrewsh@debian.org>2019-06-23 18:40:19 -0500
committerAndrej Shadura <andrewsh@debian.org>2019-06-23 18:40:19 -0500
commit67711d917d4666579e7d1b0647f54519ce0d0be6 (patch)
tree7beeda12084a396a513804a05869aea1c6f34f08
Import original source of Tickit-Widget-ScrollBox 0.07
-rw-r--r--Build.PL30
-rw-r--r--Changes43
-rw-r--r--LICENSE379
-rw-r--r--MANIFEST21
-rw-r--r--META.json50
-rw-r--r--META.yml30
-rw-r--r--Makefile.PL19
-rw-r--r--README151
-rw-r--r--examples/demo-gridbox.pl25
-rw-r--r--examples/demo-smart.pl60
-rw-r--r--examples/demo.pl25
-rw-r--r--lib/Tickit/Widget/ScrollBox.pm690
-rw-r--r--lib/Tickit/Widget/ScrollBox/Extent.pm233
-rw-r--r--t/00use.t10
-rw-r--r--t/01scrollbox-horizontal.t98
-rw-r--r--t/01scrollbox-vertical.t100
-rw-r--r--t/02input-key.t79
-rw-r--r--t/03input-mouse.t101
-rw-r--r--t/04on_demand.t74
-rw-r--r--t/05smart.t102
-rw-r--r--t/99pod.t11
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;
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..bc01f32
--- /dev/null
+++ b/Changes
@@ -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.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8c48302
--- /dev/null
+++ b/LICENSE
@@ -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' => {}
+)
+;
diff --git a/README b/README
new file mode 100644
index 0000000..87ec8bb
--- /dev/null
+++ b/README
@@ -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();