From 24232e894a17e6b6d293ec8413d37929d266a311 Mon Sep 17 00:00:00 2001 From: myk002 Date: Thu, 6 Oct 2022 11:13:16 -0700 Subject: [PATCH 1/3] create Scrollbar widget and integrate with List --- docs/Lua API.rst | 41 ++++++++--- docs/changelog.txt | 2 + library/lua/gui/widgets.lua | 138 +++++++++++++++++++++++++++++++++--- 3 files changed, 165 insertions(+), 16 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index d8a76dd28..b9c09c673 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -4032,6 +4032,38 @@ following keyboard hotkeys: - Ctrl-Left/Right arrow: move the cursor one word to the left or right. - Alt-Left/Right arrow: move the cursor to the beginning/end of the text. +Scrollbar class +--------------- + +This Widget subclass implements mouse-interactive scrollbars whose bar sizes +represent the amount of content currently visible in an associated display +widget (like a `Label class`_ or a `List class`_). By default they are styled +like scrollbars used in the vanilla DF help screens, but they are configurable. + +Scrollbars have the following attributes: + +:fg: Specifies the pen for the scroll icons and the active part of the bar. Default is ``COLOR_LIGHTGREEN``. +:bg: Specifies the pen for the background part of the scrollbar. Default is ``COLOR_CYAN``. +:on_scroll: A callback called when the scrollbar is scrolled. It will be called with a single string parameter with a value of "up_large", "down_large", "up_small", or "down_small". + +The Scrollbar widget implements the following methods: + +* ``scrollbar:update(top_elem, elems_per_page, num_elems)`` + + Updates the info about the widget that the scrollbar is paired with. + The ``top_elem`` param is the (one-based) index of the first visible element. + The ``elems_per_page`` param is the maximum number of elements that can be + shown at one time. The ``num_elems`` param is the total number of elements + that the paried widget can scroll through. The scrollbar will adjust its + scrollbar size and position accordingly. + +Clicking on the arrows at the top or the bottom of a scrollbar will scroll an +associated widget by a small amount. Clicking on the unfilled portion of the +scrollbar above or below the filled area will scroll by a larger amount in that +direction. The amount of scrolling done in each case in determined by the +associated widget, and after scrolling is complete, the associated widget must +call ``scrollbar:update()`` with updated new display info. + Label class ----------- @@ -4056,13 +4088,7 @@ It has the following attributes: icons next to the text in an additional column (``frame_inset`` is adjusted to have ``.r`` or ``.l`` greater than ``0``), ``nil`` same as ``'right'`` but changes ``frame_inset`` only if a scroll icon is actually necessary (if ``getTextHeight()`` is greater than ``frame_body.height``). Default is ``nil``. -:scrollbar_fg: Specifies the pen for the scroll icons and the active part of the bar. Default is ``COLOR_LIGHTGREEN`` (the same as the native DF help screens). -:scrollbar_bg: Specifies the pen for the background part of the scrollbar. Default is ``COLOR_CYAN`` (the same as the native DF help screens). - -If the scrollbar is shown, it will react to mouse clicks on the scrollbar itself. -Clicking on the arrows at the top or the bottom will scroll by one line, and -clicking on the unfilled portion of the scrollbar will scroll by a half page in -that direction. +:scrollbar: The table of attributes to pass to the `Scrollbar class`_. The text itself is represented as a complex structure, and passed to the object via the ``text`` argument of the constructor, or via @@ -4283,7 +4309,6 @@ Every list item may be specified either as a string, or as a lua table with the following fields: :text: Specifies the label text in the same format as the Label text. -:caption, [1]: Deprecated legacy aliases for **text**. :text_*: Reserved for internal use. :key: Specifies a keybinding that acts as a shortcut for the specified item. :icon: Specifies an icon string, or a pen to paint a single character. May be a callback. diff --git a/docs/changelog.txt b/docs/changelog.txt index eba15cfb4..b1d5300a9 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -41,12 +41,14 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `ls`: indent tag listings and wrap them in the right column for better readability - `ls`: new ``--exclude`` option for hiding matched scripts from the output. this can be especially useful for modders who don't want their mod scripts to be included in ``ls`` output. - `digtype`: new ``-z`` option for digtype to restrict designations to the current z-level and down +- UX: List widgets now have mouse-interactive scrollbars ## Documentation ## API ## Lua +- ``widgets.Scrollbar``: new scrollbar widget that can be paired with an associated scrollable widget. Integrated with ``widgets.Label`` and ``widgets.List``. # 0.47.05-r7 diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 7076570b9..20aff7aa4 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -359,6 +359,96 @@ function EditField:onInput(keys) return self.modal end +--------------- +-- Scrollbar -- +--------------- + +Scrollbar = defclass(Scrollbar, Widget) + +Scrollbar.ATTRS{ + fg = COLOR_LIGHTGREEN, + bg = COLOR_CYAN, + on_scroll = DEFAULT_NIL, +} + +function Scrollbar:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.w = init_table.frame.w or 1 +end + +function Scrollbar:init() + self:update(1, 1, 1) +end + +-- calculate and cache the number of tiles of empty space above the top of the +-- scrollbar and the number of tiles the scrollbar should occupy to represent +-- the percentage of text that is on the screen. +-- if elems_per_page or num_elems are not specified, the last values passed to +-- Scrollbar:update() are used. +function Scrollbar:update(top_elem, elems_per_page, num_elems) + if not top_elem then error('must specify index of new top element') end + elems_per_page = elems_per_page or self.elems_per_page + num_elems = num_elems or self.num_elems + + local frame_height = self.frame_body and self.frame_body.height or 3 + local scrollbar_body_height = frame_height - 2 + local height = math.max(1, math.floor( + (math.min(elems_per_page, num_elems) * scrollbar_body_height) / + num_elems)) + + local max_pos = scrollbar_body_height - height + local pos = math.ceil(((top_elem-1) * max_pos) / + (num_elems - elems_per_page)) + + self.top_elem = top_elem + self.elems_per_page, self.num_elems = elems_per_page, num_elems + self.bar_offset, self.bar_height = pos, height +end + +local UP_ARROW_CHAR = string.char(24) +local DOWN_ARROW_CHAR = string.char(25) +local NO_ARROW_CHAR = string.char(32) +local BAR_CHAR = string.char(7) +local BAR_BG_CHAR = string.char(179) + +function Scrollbar:onRenderBody(dc) + -- don't draw if all elements are visible + if self.elems_per_page >= self.num_elems then return end + -- render up arrow if we're not at the top + dc:seek(0, 0):char( + self.top_elem == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR, self.fg, self.bg) + -- render scrollbar body + local starty = self.bar_offset + 1 + local endy = self.bar_offset + self.bar_height + for y=1,dc.height-2 do + dc:seek(0, y) + if y >= starty and y <= endy then + dc:char(BAR_CHAR, self.fg) + else + dc:char(BAR_BG_CHAR, self.bg) + end + end + -- render down arrow if we're not at the bottom + local last_visible_el = self.top_elem + self.elems_per_page - 1 + dc:seek(0, dc.height-1):char( + last_visible_el >= self.num_elems and NO_ARROW_CHAR or DOWN_ARROW_CHAR, + self.fg, self.bg) +end + +function Scrollbar:onInput(keys) + if not keys._MOUSE_L_DOWN or not self.on_scroll then return false end + local _,y = self:getMousePos() + if not y then return false end + local scroll = nil + if y == 0 then scroll = 'up_small' + elseif y == self.frame_body.height - 1 then scroll = 'down_small' + elseif y <= self.bar_offset then scroll = 'up_large' + elseif y > self.bar_offset + self.bar_height then scroll = 'down_large' + end + if scroll then self.on_scroll(scroll) end + return true +end + ----------- -- Label -- ----------- @@ -610,12 +700,6 @@ local function get_scrollbar_pos_and_height(label) return pos, height end -local UP_ARROW_CHAR = string.char(24) -local DOWN_ARROW_CHAR = string.char(25) -local NO_ARROW_CHAR = string.char(32) -local BAR_CHAR = string.char(7) -local BAR_BG_CHAR = string.char(179) - function Label:render_scrollbar(dc, x, y1, y2) -- render up arrow if we're not at the top dc:seek(x, y1):char( @@ -953,6 +1037,11 @@ List.ATTRS{ function List:init(info) self.page_top = 1 self.page_size = 1 + self.scrollbar = Scrollbar{ + frame={r=0}, + on_scroll=self:callback('on_scrollbar')} + + self:addviews{self.scrollbar} if info.choices then self:setChoices(info.choices, info.selected) @@ -1017,13 +1106,17 @@ function List:postComputeFrame(body) self:moveCursor(0) end +local function update_list_scrollbar(list) + self.scrollbar:update(list.page_top, list.page_size, #list.choices) +end + function List:moveCursor(delta, force_cb) - local page = math.max(1, self.page_size) local cnt = #self.choices if cnt < 1 then self.page_top = 1 self.selected = 1 + update_list_scrollbar(self) if force_cb and self.on_select then self.on_select(nil,nil) end @@ -1046,14 +1139,40 @@ function List:moveCursor(delta, force_cb) end end + local buffer = 1 + math.min(4, math.floor(self.page_size/10)) + self.selected = 1 + off % cnt - self.page_top = 1 + page * math.floor((self.selected-1) / page) + if (self.selected - buffer) < self.page_top then + self.page_top = math.max(1, self.selected - buffer) + elseif (self.selected + buffer + 1) > (self.page_top + self.page_size) then + local max_page_top = cnt - self.page_size + 1 + self.page_top = math.max(1, + math.min(max_page_top, self.selected - self.page_size + buffer + 1)) + end + update_list_scrollbar(self) if (force_cb or delta ~= 0) and self.on_select then self.on_select(self:getSelected()) end end +function List:on_scrollbar(scroll_spec) + local v = 0 + if scroll_spec == 'down_large' then + v = math.floor(self.page_size / 2) + elseif scroll_spec == 'up_large' then + v = -math.floor(self.page_size / 2) + elseif scroll_spec == 'down_small' then + v = 1 + elseif scroll_spec == 'up_small' then + v = -1 + end + + local max_page_top = math.max(1, #self.choices - self.page_size + 1) + self.page_top = math.max(1, math.min(max_page_top, self.page_top + v)) + update_list_scrollbar(self) +end + function List:onRenderBody(dc) local choices = self.choices local top = self.page_top @@ -1122,6 +1241,9 @@ function List:submit2() end function List:onInput(keys) + if self:inputToSubviews(keys) then + return true + end if self.on_submit and keys.SELECT then self:submit() return true From 5722d6914b2ee080890ce44b095806d11b7223d8 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 7 Oct 2022 12:45:43 -0700 Subject: [PATCH 2/3] transition Label to use the new generic Scrollbar --- docs/Lua API.rst | 8 +- library/lua/gui/widgets.lua | 171 ++++++++--------------------- test/library/gui/widgets.Label.lua | 64 ----------- 3 files changed, 49 insertions(+), 194 deletions(-) diff --git a/docs/Lua API.rst b/docs/Lua API.rst index b9c09c673..ddb00c61a 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -4084,11 +4084,6 @@ It has the following attributes: keys to the number of lines to scroll as positive or negative integers or one of the keywords supported by the ``scroll`` method. The default is up/down arrows scrolling by one line and page up/down scrolling by one page. -:show_scrollbar: Controls scrollbar display: ``false`` for no scrollbar, ``'right'`` or ``'left'`` for - icons next to the text in an additional column (``frame_inset`` is adjusted to have ``.r`` or ``.l`` greater than ``0``), - ``nil`` same as ``'right'`` but changes ``frame_inset`` only if a scroll icon is actually necessary - (if ``getTextHeight()`` is greater than ``frame_body.height``). Default is ``nil``. -:scrollbar: The table of attributes to pass to the `Scrollbar class`_. The text itself is represented as a complex structure, and passed to the object via the ``text`` argument of the constructor, or via @@ -4181,7 +4176,8 @@ The Label widget implements the following methods: This method takes the number of lines to scroll as positive or negative integers or one of the following keywords: ``+page``, ``-page``, - ``+halfpage``, or ``-halfpage``. + ``+halfpage``, or ``-halfpage``. It returns the number of lines that were + actually scrolled (negative for scrolling up). WrappedLabel class ------------------ diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 20aff7aa4..3ac9d3c8b 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -636,12 +636,15 @@ Label.ATTRS{ on_click = DEFAULT_NIL, on_rclick = DEFAULT_NIL, scroll_keys = STANDARDSCROLL, - show_scrollbar = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false - scrollbar_fg = COLOR_LIGHTGREEN, - scrollbar_bg = COLOR_CYAN } function Label:init(args) + self.scrollbar = Scrollbar{ + frame={r=0}, + on_scroll=self:callback('on_scrollbar')} + + self:addviews{self.scrollbar} + -- use existing saved text if no explicit text was specified. this avoids -- overwriting pre-formatted text that subclasses may have already set self:setText(args.text or self.text) @@ -650,6 +653,12 @@ function Label:init(args) end end +local function update_label_scrollbar(label) + local body_height = label.frame_body and label.frame_body.height or 1 + label.scrollbar:update(label.start_line_num, body_height, + label:getTextHeight()) +end + function Label:setText(text) self.start_line_num = 1 self.text = text @@ -659,81 +668,8 @@ function Label:setText(text) self.frame = self.frame or {} self.frame.h = self:getTextHeight() end -end -function Label:update_scroll_inset() - if self.show_scrollbar == nil then - self._show_scrollbar = self:getTextHeight() > self.frame_body.height and 'right' or false - else - self._show_scrollbar = self.show_scrollbar - end - if self._show_scrollbar then - -- here self._show_scrollbar can only be either - -- 'left' or any true value which we interpret as right - local l,t,r,b = gui.parse_inset(self.frame_inset) - if self._show_scrollbar == 'left' and l <= 0 then - l = 1 - elseif r <= 0 then - r = 1 - end - self.frame_inset = {l=l,t=t,r=r,b=b} - end -end - --- the position is the number of tiles of empty space above the top of the --- scrollbar, and the height is the number of tiles the scrollbar should occupy --- to represent the percentage of text that is on the screen. -local function get_scrollbar_pos_and_height(label) - local first_visible_line = label.start_line_num - local text_height = label:getTextHeight() - local last_visible_line = first_visible_line + label.frame_body.height - 1 - local scrollbar_body_height = label.frame_body.height - 2 - local displayed_lines = last_visible_line - first_visible_line - - local height = math.floor(((displayed_lines-1) * scrollbar_body_height) / - text_height) - - local max_pos = scrollbar_body_height - height - local pos = math.ceil(((first_visible_line-1) * max_pos) / - (text_height - label.frame_body.height)) - - return pos, height -end - -function Label:render_scrollbar(dc, x, y1, y2) - -- render up arrow if we're not at the top - dc:seek(x, y1):char( - self.start_line_num == 1 and NO_ARROW_CHAR or UP_ARROW_CHAR, - self.scrollbar_fg, self.scrollbar_bg) - -- render scrollbar body - local pos, height = get_scrollbar_pos_and_height(self) - local starty = y1 + pos + 1 - local endy = y1 + pos + height - for y=y1+1,y2-1 do - if y >= starty and y <= endy then - dc:seek(x, y):char(BAR_CHAR, self.scrollbar_fg) - else - dc:seek(x, y):char(BAR_BG_CHAR, self.scrollbar_bg) - end - end - -- render down arrow if we're not at the bottom - local last_visible_line = self.start_line_num + self.frame_body.height - 1 - dc:seek(x, y2):char( - last_visible_line >= self:getTextHeight() and - NO_ARROW_CHAR or DOWN_ARROW_CHAR, - self.scrollbar_fg, self.scrollbar_bg) -end - -function Label:computeFrame(parent_rect) - local frame_rect,body_rect = Label.super.computeFrame(self, parent_rect) - - self.frame_rect = frame_rect - self.frame_body = parent_rect:viewport(body_rect or frame_rect) - - self:update_scroll_inset() -- frame_body is now set - - -- recalc with updated frame_inset - return Label.super.computeFrame(self, parent_rect) + update_label_scrollbar(self) end function Label:preUpdateLayout() @@ -743,6 +679,10 @@ function Label:preUpdateLayout() end end +function Label:postUpdateLayout() + update_label_scrollbar(self) +end + function Label:itemById(id) if self.text_ids then return self.text_ids[id] @@ -766,44 +706,19 @@ function Label:onRenderBody(dc) render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self)) end -function Label:onRenderFrame(dc, rect) - if self._show_scrollbar then - local x = self._show_scrollbar == 'left' - and self.frame_body.x1-dc.x1-1 - or self.frame_body.x2-dc.x1+1 - self:render_scrollbar(dc, - x, - self.frame_body.y1-dc.y1, - self.frame_body.y2-dc.y1 - ) - end -end - -function Label:click_scrollbar() - if not self._show_scrollbar then return end - local rect = self.frame_body - local x, y = dscreen.getMousePos() - - if self._show_scrollbar == 'left' and x ~= rect.x1-1 or x ~= rect.x2+1 then - return - end - if y < rect.y1 or y > rect.y2 then - return +function Label:on_scrollbar(scroll_spec) + local v = 0 + if scroll_spec == 'down_large' then + v = '+halfpage' + elseif scroll_spec == 'up_large' then + v = '-halfpage' + elseif scroll_spec == 'down_small' then + v = 1 + elseif scroll_spec == 'up_small' then + v = -1 end - if y == rect.y1 then - return -1 - elseif y == rect.y2 then - return 1 - else - local pos, height = get_scrollbar_pos_and_height(self) - if y <= rect.y1 + pos then - return '-halfpage' - elseif y > rect.y1 + pos + height then - return '+halfpage' - end - end - return nil + self:scroll(v) end function Label:scroll(nlines) @@ -824,24 +739,28 @@ function Label:scroll(nlines) local n = self.start_line_num + nlines n = math.min(n, self:getTextHeight() - self.frame_body.height + 1) n = math.max(n, 1) + nlines = n - self.start_line_num self.start_line_num = n + update_label_scrollbar(self) return nlines end function Label:onInput(keys) if is_disabled(self) then return false end - if keys._MOUSE_L_DOWN then - if not self:scroll(self:click_scrollbar()) and - self:getMousePos() and self.on_click then - self:on_click() - end + if self:inputToSubviews(keys) then + return true + end + if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then + self:on_click() + return true end if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then self:on_rclick() + return true end for k,v in pairs(self.scroll_keys) do - if keys[k] then - self:scroll(v) + if keys[k] and 0 ~= self:scroll(v) then + return true end end return check_text_keys(self, keys) @@ -871,7 +790,7 @@ end -- we can't set the text in init() since we may not yet have a frame that we -- can get wrapping bounds from. function WrappedLabel:postComputeFrame() - local wrapped_text = self:getWrappedText(self.frame_body.width) + local wrapped_text = self:getWrappedText(self.frame_body.width-1) if not wrapped_text then return end local text = {} for _,line in ipairs(wrapped_text:split(NEWLINE)) do @@ -1107,7 +1026,11 @@ function List:postComputeFrame(body) end local function update_list_scrollbar(list) - self.scrollbar:update(list.page_top, list.page_size, #list.choices) + list.scrollbar:update(list.page_top, list.page_size, #list.choices) +end + +function List:postUpdateLayout() + update_list_scrollbar(self) end function List:moveCursor(delta, force_cb) @@ -1159,9 +1082,9 @@ end function List:on_scrollbar(scroll_spec) local v = 0 if scroll_spec == 'down_large' then - v = math.floor(self.page_size / 2) + v = math.ceil(self.page_size / 2) elseif scroll_spec == 'up_large' then - v = -math.floor(self.page_size / 2) + v = -math.ceil(self.page_size / 2) elseif scroll_spec == 'down_small' then v = 1 elseif scroll_spec == 'up_small' then diff --git a/test/library/gui/widgets.Label.lua b/test/library/gui/widgets.Label.lua index c43b5e886..4693d3d0d 100644 --- a/test/library/gui/widgets.Label.lua +++ b/test/library/gui/widgets.Label.lua @@ -11,70 +11,6 @@ fs.ATTRS = { focus_path = 'test-framed-screen', } -function test.correct_frame_body_with_scroll_icons() - local t = {} - for i = 1, 12 do - t[#t+1] = tostring(i) - t[#t+1] = NEWLINE - end - - function fs:init() - self:addviews{ - widgets.Label{ - view_id = 'text', - frame_inset = 0, - text = t, - }, - } - end - - local o = fs{} - expect.eq(o.subviews.text.frame_body.width, 9, "Label's frame_body.x2 and .width should be one smaller because of show_scrollbar.") -end - -function test.correct_frame_body_with_few_text_lines() - local t = {} - for i = 1, 10 do - t[#t+1] = tostring(i) - t[#t+1] = NEWLINE - end - - function fs:init() - self:addviews{ - widgets.Label{ - view_id = 'text', - frame_inset = 0, - text = t, - }, - } - end - - local o = fs{} - expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.") -end - -function test.correct_frame_body_without_show_scrollbar() - local t = {} - for i = 1, 12 do - t[#t+1] = tostring(i) - t[#t+1] = NEWLINE - end - - function fs:init() - self:addviews{ - widgets.Label{ - view_id = 'text', - frame_inset = 0, - text = t, - show_scrollbar = false, - }, - } - end - - local o = fs{} - expect.eq(o.subviews.text.frame_body.width, 10, "Label's frame_body.x2 and .width should not change with show_scrollbar = false.") -end - function test.scroll() local t = {} for i = 1, 12 do From 2bff70a2905c211133e2150885f43aed47ffbc78 Mon Sep 17 00:00:00 2001 From: myk002 Date: Fri, 7 Oct 2022 13:14:52 -0700 Subject: [PATCH 3/3] add unit tests for widgets.Scrollbar --- library/lua/gui/widgets.lua | 5 +- test/library/gui/widgets.Scrollbar.lua | 93 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 test/library/gui/widgets.Scrollbar.lua diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 3ac9d3c8b..2eaf80577 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -397,8 +397,9 @@ function Scrollbar:update(top_elem, elems_per_page, num_elems) num_elems)) local max_pos = scrollbar_body_height - height - local pos = math.ceil(((top_elem-1) * max_pos) / - (num_elems - elems_per_page)) + local pos = (num_elems == elems_per_page) and 0 or + math.ceil(((top_elem-1) * max_pos) / + (num_elems - elems_per_page)) self.top_elem = top_elem self.elems_per_page, self.num_elems = elems_per_page, num_elems diff --git a/test/library/gui/widgets.Scrollbar.lua b/test/library/gui/widgets.Scrollbar.lua new file mode 100644 index 000000000..dbe033ba4 --- /dev/null +++ b/test/library/gui/widgets.Scrollbar.lua @@ -0,0 +1,93 @@ +local gui = require('gui') +local widgets = require('gui.widgets') + +function test.update() + local s = widgets.Scrollbar{} + s.frame_body = {height=100} -- give us some space to work with + + -- initial defaults + expect.eq(1, s.top_elem) + expect.eq(1, s.elems_per_page) + expect.eq(1, s.num_elems) + expect.eq(0, s.bar_offset) + expect.eq(1, s.bar_height) + + -- top_elem, elems_per_page, num_elems + s:update(1, 10, 0) + expect.eq(1, s.top_elem) + expect.eq(10, s.elems_per_page) + expect.eq(0, s.num_elems) + expect.eq(0, s.bar_offset) + expect.eq(1, s.bar_height) + + -- first 10 of 50 shown + s:update(1, 10, 50) + expect.eq(1, s.top_elem) + expect.eq(10, s.elems_per_page) + expect.eq(50, s.num_elems) + expect.eq(0, s.bar_offset) + expect.eq(19, s.bar_height) + + -- bottom 10 of 50 shown + s:update(41, 10, 50) + expect.eq(41, s.top_elem) + expect.eq(10, s.elems_per_page) + expect.eq(50, s.num_elems) + expect.eq(79, s.bar_offset) + expect.eq(19, s.bar_height) + + -- ~middle 10 of 50 shown + s:update(23, 10, 50) + expect.eq(23, s.top_elem) + expect.eq(10, s.elems_per_page) + expect.eq(50, s.num_elems) + expect.eq(44, s.bar_offset) + expect.eq(19, s.bar_height) +end + +function test.onInput() + local spec = nil + local mock_on_scroll = function(scroll_spec) spec = scroll_spec end + local s = widgets.Scrollbar{on_scroll=mock_on_scroll} + s.frame_body = {height=100} -- give us some space to work with + local y = nil + s.getMousePos = function() return 0, y end + + -- put scrollbar somewhere in the middle so we can click above and below it + s:update(23, 10, 50) + + expect.false_(s:onInput{}, 'no mouse down') + expect.false_(s:onInput{_MOUSE_L_DOWN=true}, 'no y coord') + + spec, y = nil, 0 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.eq('up_small', spec, 'on up arrow') + + spec, y = nil, 1 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.eq('up_large', spec, 'on body above bar') + + spec, y = nil, 44 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.eq('up_large', spec, 'on body just above bar') + + spec, y = nil, 45 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.nil_(spec, 'on top of bar') + + spec, y = nil, 63 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.nil_(spec, 'on bottom of bar') + + spec, y = nil, 64 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.eq('down_large', spec, 'on body just below bar') + + spec, y = nil, 98 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.eq('down_large', spec, 'on body below bar') + + spec, y = nil, 99 + expect.true_(s:onInput{_MOUSE_L_DOWN=true}) + expect.eq('down_small', spec, 'on down arrow') +end