From 2d68b21547b8c08cf0726026cf5ad8d233420f9a Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Tue, 14 Feb 2023 21:20:18 -0800 Subject: [PATCH 1/9] Show mouse hover on HotkeyLabels Labels show the hover colour when on_click is set, HotkeyLabels should also do the same when they are clickable. --- library/lua/gui/widgets.lua | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 77c6c2c28..3a6fd8494 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1274,9 +1274,15 @@ function Label:getTextWidth() return self.text_width end +-- Overridden by subclasses that also want to add new mouse handlers, see +-- HotkeyLabel. +function Label:shouldHover() + return self.on_click or self.on_rclick +end + function Label:onRenderBody(dc) local text_pen = self.text_pen - if self:getMousePos() and (self.on_click or self.on_rclick) then + if self:getMousePos() and self:shouldHover() then text_pen = self.text_hpen end render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self)) @@ -1432,6 +1438,11 @@ function HotkeyLabel:setLabel(label) self:initializeLabel() end +function HotkeyLabel:shouldHover() + -- When on_activate is set, text should also hover on mouseover + return HotkeyLabel.super.shouldHover(self) or self.on_activate +end + function HotkeyLabel:initializeLabel() self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, on_activate=self.on_activate}} @@ -1475,6 +1486,12 @@ function CycleHotkeyLabel:init() } end +-- CycleHotkeyLabels are always clickable and therefore should always change +-- color when hovered. +function CycleHotkeyLabel:shouldHover() + return true +end + function CycleHotkeyLabel:cycle(backwards) local old_option_idx = self.option_idx if self.option_idx == #self.options and not backwards then From 0b48471607ba0c74daf147232c5efbca18c9ee5a Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Wed, 15 Feb 2023 21:19:44 -0800 Subject: [PATCH 2/9] Invert brightness of the background as well This required some tinkering. --- library/lua/gui/widgets.lua | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 3a6fd8494..15d9064c5 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1224,9 +1224,23 @@ function Label:init(args) -- 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) + + -- Inverts the brightness of the color + invert = function(color) + return (color + 8) % 16 + end + -- default pen is an inverted foreground/background if not self.text_hpen then - self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16 + local text_pen = dfhack.pen.parse(self.text_pen) + self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = dfhack.pen.parse(self.text_hpen) + hpen_parsed.ch = string.byte(' ') + self.text_hpen = hpen_parsed end local function update_label_scrollbar(label) @@ -1280,6 +1294,14 @@ function Label:shouldHover() return self.on_click or self.on_rclick end +function Label:onRenderFrame(dc, rect) + Label.super.onRenderFrame(self, dc, rect) + -- Fill the background with text_hpen on hover + if self:getMousePos() and self:shouldHover() then + dc:fill(rect, self.text_hpen) + end +end + function Label:onRenderBody(dc) local text_pen = self.text_pen if self:getMousePos() and self:shouldHover() then From 0897ca913a46a61c41f655ed60b66a6f0c827006 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Wed, 15 Feb 2023 22:33:31 -0800 Subject: [PATCH 3/9] Support mouse-hover on lists as well --- library/lua/gui/widgets.lua | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 15d9064c5..ab551be1c 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1226,7 +1226,7 @@ function Label:init(args) self:setText(args.text or self.text) -- Inverts the brightness of the color - invert = function(color) + local invert = function(color) return (color + 8) % 16 end -- default pen is an inverted foreground/background @@ -1604,6 +1604,7 @@ List = defclass(List, Widget) List.ATTRS{ text_pen = COLOR_CYAN, + text_hpen = DEFAULT_NIL, -- pen to render list item when mouse is hovered over; defaults to text_pen with inverted brightness cursor_pen = COLOR_LIGHTCYAN, inactive_pen = DEFAULT_NIL, on_select = DEFAULT_NIL, @@ -1633,6 +1634,23 @@ function List:init(info) end self.last_select_click_ms = 0 -- used to track double-clicking on an item + + -- Inverts the brightness of the color + invert = function(color) + return (color + 8) % 16 + end + -- default pen is an inverted foreground/background + if not self.text_hpen then + local text_pen = dfhack.pen.parse(self.text_pen) + self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) + end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = dfhack.pen.parse(self.text_hpen) + hpen_parsed.ch = string.byte(' ') + self.text_hpen = hpen_parsed end function List:setChoices(choices, selected) @@ -1784,12 +1802,19 @@ function List:onRenderBody(dc) end end + local hoveridx = self:getIdxUnderMouse() for i = top,iend do local obj = choices[i] local current = (i == self.selected) - local cur_pen = self.cursor_pen - local cur_dpen = self.text_pen - local active_pen = current and cur_pen or cur_dpen + local hovered = (i == hoveridx) + local cur_pen = to_pen(self.cursor_pen) + local cur_dpen = to_pen(self.text_pen) + local active_pen = (current and cur_pen or cur_dpen) + + -- when mouse is over, always highlight it + if hovered then + cur_dpen = self.text_hpen + end if not getval(self.active) then cur_pen = self.inactive_pen or self.cursor_pen From 3e8d0f0f1efbe9a3d6ed648aa51195b09bca4e18 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Thu, 16 Feb 2023 21:38:27 -0800 Subject: [PATCH 4/9] Properly reverse BG/FG and apply per letter This puts pen creation deeper into the loop in render_text. Lists are current coloured completely wrong, though, and need fixing (and probably anywhere else where disabled is set). --- library/lua/gui/widgets.lua | 92 +++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index ab551be1c..e0f01701d 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1080,7 +1080,26 @@ local function is_disabled(token) (token.enabled ~= nil and not getval(token.enabled)) end -function render_text(obj,dc,x0,y0,pen,dpen,disabled) +-- Make the hover pen -- that is a pen that should render elements that has the +-- mouse hovering over it. if hpen is specified, it just checks the fields and +-- returns it (in parsed pen form) +local function make_hpen(pen, hpen) + if not hpen then + pen = dfhack.pen.parse(pen) + + -- Swap the foreground and background + hpen = dfhack.pen.make(pen.bg, nil, pen.fg + (pen.bold and 8 or 0)) + end + + -- text_hpen needs a character in order to paint the background using + -- Painter:fill(), so let's make it paint a space to show the background + -- color + local hpen_parsed = dfhack.pen.parse(hpen) + hpen_parsed.ch = string.byte(' ') + return hpen_parsed +end + +function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) local width = 0 for iline = dc and obj.start_line_num or 1, #obj.text_lines do local x, line = 0, obj.text_lines[iline] @@ -1120,16 +1139,25 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled) if dc then local tpen = getval(token.pen) + local dcpen = tpen or pen + + -- If disabled, figure out which dpen to use if disabled or is_disabled(token) then - dc:pen(getval(token.dpen) or tpen or dpen) + dccpen = getval(token.dpen) or tpen or dpen if keypen.fg ~= COLOR_BLACK then keypen.bold = false end - else - dc:pen(tpen or pen) + + -- if hovered *and* disabled, combine both effects + if hovered then + dcpen = make_hpen(dcpen) + end + elseif hovered then + dcpen = make_hpen(dcpen, getval(token.hpen) or hpen) end - end + dc:pen(dcpen) + end local width = getval(token.width) local padstr if width then @@ -1221,26 +1249,9 @@ function Label:init(args) 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) - -- Inverts the brightness of the color - local invert = function(color) - return (color + 8) % 16 - end - -- default pen is an inverted foreground/background - if not self.text_hpen then - local text_pen = dfhack.pen.parse(self.text_pen) - self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) - end - - -- text_hpen needs a character in order to paint the background using - -- Painter:fill(), so let's make it paint a space to show the background - -- color - local hpen_parsed = dfhack.pen.parse(self.text_hpen) - hpen_parsed.ch = string.byte(' ') - self.text_hpen = hpen_parsed + -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end local function update_label_scrollbar(label) @@ -1298,16 +1309,15 @@ function Label:onRenderFrame(dc, rect) Label.super.onRenderFrame(self, dc, rect) -- Fill the background with text_hpen on hover if self:getMousePos() and self:shouldHover() then - dc:fill(rect, self.text_hpen) + local hpen = make_hpen(self.text_pen, self.text_hpen) + dc:fill(rect, hpen) end end function Label:onRenderBody(dc) local text_pen = self.text_pen - if self:getMousePos() and self:shouldHover() then - text_pen = self.text_hpen - end - render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self)) + local hovered = self:getMousePos() and self:shouldHover() + render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self), self.text_hpen, hovered) end function Label:on_scrollbar(scroll_spec) @@ -1635,22 +1645,7 @@ function List:init(info) self.last_select_click_ms = 0 -- used to track double-clicking on an item - -- Inverts the brightness of the color - invert = function(color) - return (color + 8) % 16 - end - -- default pen is an inverted foreground/background - if not self.text_hpen then - local text_pen = dfhack.pen.parse(self.text_pen) - self.text_hpen = dfhack.pen.make(invert(text_pen.fg), nil, invert(text_pen.bg)) - end - - -- text_hpen needs a character in order to paint the background using - -- Painter:fill(), so let's make it paint a space to show the background - -- color - local hpen_parsed = dfhack.pen.parse(self.text_hpen) - hpen_parsed.ch = string.byte(' ') - self.text_hpen = hpen_parsed + -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end function List:setChoices(choices, selected) @@ -1807,15 +1802,12 @@ function List:onRenderBody(dc) local obj = choices[i] local current = (i == self.selected) local hovered = (i == hoveridx) + -- cur_pen and cur_dpen can't be integers or background colors get + -- messed up in render_text for subsequent renders local cur_pen = to_pen(self.cursor_pen) local cur_dpen = to_pen(self.text_pen) local active_pen = (current and cur_pen or cur_dpen) - -- when mouse is over, always highlight it - if hovered then - cur_dpen = self.text_hpen - end - if not getval(self.active) then cur_pen = self.inactive_pen or self.cursor_pen end @@ -1828,7 +1820,7 @@ function List:onRenderBody(dc) paint_icon(icon, obj) end - render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current) + render_text(obj, dc, iw or 0, y, cur_pen, cur_dpen, not current, self.text_hpen, hovered) local ip = dc.width From 94ae9973cf00c5f88b228ea0d254cb92214b6e3a Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Thu, 16 Feb 2023 21:43:03 -0800 Subject: [PATCH 5/9] Re-add the invert_color function As requested, but it's not used anymore. --- library/lua/gui.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/library/lua/gui.lua b/library/lua/gui.lua index 4a30d947a..9a5038e69 100644 --- a/library/lua/gui.lua +++ b/library/lua/gui.lua @@ -987,4 +987,10 @@ function FramedScreen:onRenderFrame(dc, rect) paint_frame(dc,rect,self.frame_style,self.frame_title) end +-- Inverts the brightness of the color, optionally taking a "bold" parameter, +-- which you should include if you're reading the fg color of a pen. +function invert_color(color, bold) + color = bold and (color + 8) or color + return (color + 8) % 16 +end return _ENV From 61227eeca1a59f13fb228864517f6955526529e1 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Thu, 16 Feb 2023 21:54:44 -0800 Subject: [PATCH 6/9] Fix use of pens in render_text If you ever pass in a number to `dc:pen` rather than a pen table, it will assume the old pen's other attributes, such as `bg` and `bold`. To workaround this, we just never pass in a number, and always call `to_pen` aka `dfhack.pen.parse` first. --- library/lua/gui/widgets.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index e0f01701d..142e928d3 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1139,11 +1139,11 @@ function render_text(obj,dc,x0,y0,pen,dpen,disabled,hpen,hovered) if dc then local tpen = getval(token.pen) - local dcpen = tpen or pen + local dcpen = to_pen(tpen or pen) -- If disabled, figure out which dpen to use if disabled or is_disabled(token) then - dccpen = getval(token.dpen) or tpen or dpen + dcpen = to_pen(getval(token.dpen) or tpen or dpen) if keypen.fg ~= COLOR_BLACK then keypen.bold = false end From 697f15224c8b3f421bcbf5a667a00bff47d88363 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Sat, 18 Feb 2023 16:06:03 -0800 Subject: [PATCH 7/9] Address PR comments, and remove BG fill BG fill eats up a lot of cycles anyway, and there's not a real tangible benefit in all cases, as it relies on the text label being sized appropriately (width-wise) to the container, or would otherwise require padding. --- docs/dev/Lua API.rst | 16 +++++++++++++++- library/lua/gui/widgets.lua | 17 ++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 02260513f..43c9d0cb0 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4662,7 +4662,9 @@ It has the following attributes: :text_pen: Specifies the pen for active text. :text_dpen: Specifies the pen for disabled text. -:text_hpen: Specifies the pen for text hovered over by the mouse, if a click handler is registered. +:text_hpen: Specifies the pen for text hovered over by the mouse, if a click + handler is registered. By default, this will invert the foreground + and background colors. :disabled: Boolean or a callback; if true, the label is disabled. :enabled: Boolean or a callback; if false, the label is disabled. :auto_height: Sets self.frame.h from the text height. @@ -4769,6 +4771,18 @@ The Label widget implements the following methods: ``+halfpage``, ``-halfpage``, ``home``, or ``end``. It returns the number of lines that were actually scrolled (negative for scrolling up). +* ``label:shouldHover()`` + + This method returns whether or not this widget should show a hover effect, + generally you want to return ``true`` if there is some type of mouse handler + present. For example, for a ``HotKeyLabel``:: + + function HotkeyLabel:shouldHover() + -- When on_activate is set, text should also hover on mouseover + return HotkeyLabel.super.shouldHover(self) or self.on_activate + end + + WrappedLabel class ------------------ diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 142e928d3..6a1726115 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1232,7 +1232,7 @@ Label = defclass(Label, Widget) Label.ATTRS{ text_pen = COLOR_WHITE, text_dpen = COLOR_DARKGREY, -- disabled - text_hpen = DEFAULT_NIL, -- highlight - default is text_pen with reversed brightness + text_hpen = DEFAULT_NIL, -- hover - default is to invert the fg/bg colors disabled = DEFAULT_NIL, enabled = DEFAULT_NIL, auto_height = true, @@ -1250,8 +1250,6 @@ function Label:init(args) self:addviews{self.scrollbar} self:setText(args.text or self.text) - - -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end local function update_label_scrollbar(label) @@ -1305,15 +1303,6 @@ function Label:shouldHover() return self.on_click or self.on_rclick end -function Label:onRenderFrame(dc, rect) - Label.super.onRenderFrame(self, dc, rect) - -- Fill the background with text_hpen on hover - if self:getMousePos() and self:shouldHover() then - local hpen = make_hpen(self.text_pen, self.text_hpen) - dc:fill(rect, hpen) - end -end - function Label:onRenderBody(dc) local text_pen = self.text_pen local hovered = self:getMousePos() and self:shouldHover() @@ -1614,7 +1603,7 @@ List = defclass(List, Widget) List.ATTRS{ text_pen = COLOR_CYAN, - text_hpen = DEFAULT_NIL, -- pen to render list item when mouse is hovered over; defaults to text_pen with inverted brightness + text_hpen = DEFAULT_NIL, -- hover color, defaults to inverting the FG/BG pens for each text object cursor_pen = COLOR_LIGHTCYAN, inactive_pen = DEFAULT_NIL, on_select = DEFAULT_NIL, @@ -1644,8 +1633,6 @@ function List:init(info) end self.last_select_click_ms = 0 -- used to track double-clicking on an item - - -- self.text_hpen = make_hpen(self.text_pen, self.text_hpen) end function List:setChoices(choices, selected) From d18700c96462049f381eefc66e9ad447f4377aef Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Sat, 18 Feb 2023 16:15:16 -0800 Subject: [PATCH 8/9] Update List docs as well. --- docs/dev/Lua API.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 43c9d0cb0..1786c9028 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -4917,6 +4917,8 @@ item to call the ``on_submit`` callback for that item. It has the following attributes: :text_pen: Specifies the pen for deselected list entries. +:text_hpen: Specifies the pen for entries that the mouse is hovered over. + Defaults to swapping the background/foreground colors. :cursor_pen: Specifies the pen for the selected entry. :inactive_pen: If specified, used for the cursor when the widget is not active. :icon_pen: Default pen for icons. From 3c24e67a9ae600139b96a48f395e0da6fd9916f2 Mon Sep 17 00:00:00 2001 From: Kelvie Wong Date: Wed, 22 Feb 2023 17:22:04 -0800 Subject: [PATCH 9/9] Address additional PR comments on_activate is likely to happen first so we shouldn't need to check the other. --- docs/dev/Lua API.rst | 6 ++++++ library/lua/gui/widgets.lua | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 1786c9028..7ead7e374 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -3757,6 +3757,12 @@ Misc Wraps ``dfhack.screen.getKeyDisplay`` in order to allow using strings for the keycode argument. +* ``invert_color(color, bold)`` + + This inverts the brightness of ``color``. If this color is coming from a pen's + foreground color, include ``pen.bold`` in ``bold`` for this to work properly. + + ViewRect class -------------- diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 6a1726115..3ef27a651 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -1461,7 +1461,7 @@ end function HotkeyLabel:shouldHover() -- When on_activate is set, text should also hover on mouseover - return HotkeyLabel.super.shouldHover(self) or self.on_activate + return self.on_activate or HotkeyLabel.super.shouldHover(self) end function HotkeyLabel:initializeLabel()