diff --git a/docs/Lua API.rst b/docs/Lua API.rst index a84458ee8..b97c1b314 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -4039,13 +4039,17 @@ 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_scroll_icons: Controls scroll icons' behaviour: ``false`` for no icons, ``'right'`` or ``'left'`` for +: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``. -:up_arrow_icon: The symbol for scroll up arrow. Default is ``string.char(24)`` (``↑``). -:down_arrow_icon: The symbol for scroll down arrow. Default is ``string.char(25)`` (``↓``). -:scroll_icon_pen: Specifies the pen for scroll icons. Default is ``COLOR_LIGHTCYAN``. +: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. The text itself is represented as a complex structure, and passed to the object via the ``text`` argument of the constructor, or via diff --git a/docs/changelog.txt b/docs/changelog.txt index 3ef25f5dd..5abae3dec 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -64,6 +64,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: - `seedwatch`: ``seedwatch all`` now adds all plants with seeds to the watchlist, not just the "basic" crops. - UX: You can now move the cursor around in DFHack text fields in ``gui/`` scripts (e.g. `gui/blueprint`, `gui/quickfort`, or `gui/gm-editor`). You can move the cursor by clicking where you want it to go with the mouse or using the Left/Right arrow keys. Ctrl+Left/Right will move one word at a time, and Alt+Left/Right will move to the beginning/end of the text. - UX: You can now click on the hotkey hint text in many ``gui/`` script windows to activate the hotkey, like a button. Not all scripts have been updated to use the clickable widget yet, but you can try it in `gui/blueprint` or `gui/quickfort`. +- UX: Label widget scroll icons are replaced with scrollbars that represent the percentage of text on the screen and move with the position of the visible text, just like web browser scrollbars. - `quickfort`: `Dreamfort ` blueprint set improvements: set traffic designations to encourage dwarves to eat cooked food instead of raw ingredients ## Documentation diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index d05749417..d81fbf6b9 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -544,12 +544,10 @@ Label.ATTRS{ auto_width = false, on_click = DEFAULT_NIL, on_rclick = DEFAULT_NIL, - -- scroll_keys = STANDARDSCROLL, - show_scroll_icons = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false - up_arrow_icon = string.char(24), - down_arrow_icon = string.char(25), - scroll_icon_pen = COLOR_LIGHTCYAN, + show_scrollbar = DEFAULT_NIL, -- DEFAULT_NIL, 'right', 'left', false + scrollbar_fg = COLOR_LIGHTGREEN, + scrollbar_bg = COLOR_CYAN } function Label:init(args) @@ -573,16 +571,16 @@ function Label:setText(text) end function Label:update_scroll_inset() - if self.show_scroll_icons == nil then - self._show_scroll_icons = self:getTextHeight() > self.frame_body.height and 'right' or false + if self.show_scrollbar == nil then + self._show_scrollbar = self:getTextHeight() > self.frame_body.height and 'right' or false else - self._show_scroll_icons = self.show_scroll_icons + self._show_scrollbar = self.show_scrollbar end - if self._show_scroll_icons then - -- here self._show_scroll_icons can only be either + 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_scroll_icons == 'left' and l <= 0 then + if self._show_scrollbar == 'left' and l <= 0 then l = 1 elseif r <= 0 then r = 1 @@ -591,14 +589,54 @@ function Label:update_scroll_inset() end end -function Label:render_scroll_icons(dc, x, y1, y2) - if self.start_line_num ~= 1 then - dc:seek(x, y1):char(self.up_arrow_icon, self.scroll_icon_pen) +-- 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 + +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( + 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 - if last_visible_line < self:getTextHeight() then - dc:seek(x, y2):char(self.down_arrow_icon, self.scroll_icon_pen) - end + 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) @@ -644,13 +682,11 @@ function Label:onRenderBody(dc) end function Label:onRenderFrame(dc, rect) - if self._show_scroll_icons - and self:getTextHeight() > self.frame_body.height - then - local x = self._show_scroll_icons == 'left' + 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_scroll_icons(dc, + self:render_scrollbar(dc, x, self.frame_body.y1-dc.y1, self.frame_body.y2-dc.y1 @@ -658,7 +694,35 @@ function Label:onRenderFrame(dc, rect) 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 + 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 +end + function Label:scroll(nlines) + if not nlines then return end if type(nlines) == 'string' then if nlines == '+page' then nlines = self.frame_body.height @@ -676,12 +740,16 @@ function Label:scroll(nlines) n = math.min(n, self:getTextHeight() - self.frame_body.height + 1) n = math.max(n, 1) self.start_line_num = n + return nlines end function Label:onInput(keys) if is_disabled(self) then return false end - if keys._MOUSE_L_DOWN and self:getMousePos() and self.on_click then - self:on_click() + 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 end if keys._MOUSE_R_DOWN and self:getMousePos() and self.on_rclick then self:on_rclick() diff --git a/test/library/gui/widgets.Label.lua b/test/library/gui/widgets.Label.lua index 6b0097d1e..c43b5e886 100644 --- a/test/library/gui/widgets.Label.lua +++ b/test/library/gui/widgets.Label.lua @@ -29,7 +29,7 @@ function test.correct_frame_body_with_scroll_icons() 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_scroll_icons.") + 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() @@ -50,10 +50,10 @@ function test.correct_frame_body_with_few_text_lines() 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_scroll_icons = false.") + 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_scroll_icons() +function test.correct_frame_body_without_show_scrollbar() local t = {} for i = 1, 12 do t[#t+1] = tostring(i) @@ -66,13 +66,13 @@ function test.correct_frame_body_without_show_scroll_icons() view_id = 'text', frame_inset = 0, text = t, - show_scroll_icons = false, + 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_scroll_icons = false.") + 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()