Merge pull request #2911 from kelvie/label-hover

Usability: Show mouse hover on all clickable Labels/Lists
develop
Myk 2023-02-22 20:39:02 -08:00 committed by GitHub
commit 3fa9babb48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 91 additions and 20 deletions

@ -3757,6 +3757,12 @@ Misc
Wraps ``dfhack.screen.getKeyDisplay`` in order to allow using strings for the keycode argument. 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 ViewRect class
-------------- --------------
@ -4662,7 +4668,9 @@ It has the following attributes:
:text_pen: Specifies the pen for active text. :text_pen: Specifies the pen for active text.
:text_dpen: Specifies the pen for disabled 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. :disabled: Boolean or a callback; if true, the label is disabled.
:enabled: Boolean or a callback; if false, 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. :auto_height: Sets self.frame.h from the text height.
@ -4769,6 +4777,18 @@ The Label widget implements the following methods:
``+halfpage``, ``-halfpage``, ``home``, or ``end``. It returns the number of ``+halfpage``, ``-halfpage``, ``home``, or ``end``. It returns the number of
lines that were actually scrolled (negative for scrolling up). 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 WrappedLabel class
------------------ ------------------
@ -4903,6 +4923,8 @@ item to call the ``on_submit`` callback for that item.
It has the following attributes: It has the following attributes:
:text_pen: Specifies the pen for deselected list entries. :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. :cursor_pen: Specifies the pen for the selected entry.
:inactive_pen: If specified, used for the cursor when the widget is not active. :inactive_pen: If specified, used for the cursor when the widget is not active.
:icon_pen: Default pen for icons. :icon_pen: Default pen for icons.

@ -987,4 +987,10 @@ function FramedScreen:onRenderFrame(dc, rect)
paint_frame(dc,rect,self.frame_style,self.frame_title) paint_frame(dc,rect,self.frame_style,self.frame_title)
end 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 return _ENV

@ -1080,7 +1080,26 @@ local function is_disabled(token)
(token.enabled ~= nil and not getval(token.enabled)) (token.enabled ~= nil and not getval(token.enabled))
end 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 local width = 0
for iline = dc and obj.start_line_num or 1, #obj.text_lines do for iline = dc and obj.start_line_num or 1, #obj.text_lines do
local x, line = 0, obj.text_lines[iline] 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 if dc then
local tpen = getval(token.pen) local tpen = getval(token.pen)
local dcpen = to_pen(tpen or pen)
-- If disabled, figure out which dpen to use
if disabled or is_disabled(token) then if disabled or is_disabled(token) then
dc:pen(getval(token.dpen) or tpen or dpen) dcpen = to_pen(getval(token.dpen) or tpen or dpen)
if keypen.fg ~= COLOR_BLACK then if keypen.fg ~= COLOR_BLACK then
keypen.bold = false keypen.bold = false
end end
else
dc:pen(tpen or pen) -- if hovered *and* disabled, combine both effects
if hovered then
dcpen = make_hpen(dcpen)
end 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 width = getval(token.width)
local padstr local padstr
if width then if width then
@ -1204,7 +1232,7 @@ Label = defclass(Label, Widget)
Label.ATTRS{ Label.ATTRS{
text_pen = COLOR_WHITE, text_pen = COLOR_WHITE,
text_dpen = COLOR_DARKGREY, -- disabled 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, disabled = DEFAULT_NIL,
enabled = DEFAULT_NIL, enabled = DEFAULT_NIL,
auto_height = true, auto_height = true,
@ -1221,12 +1249,7 @@ function Label:init(args)
self:addviews{self.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) self:setText(args.text or self.text)
if not self.text_hpen then
self.text_hpen = ((tonumber(self.text_pen) or tonumber(self.text_pen.fg) or 0) + 8) % 16
end
end end
local function update_label_scrollbar(label) local function update_label_scrollbar(label)
@ -1274,12 +1297,16 @@ function Label:getTextWidth()
return self.text_width return self.text_width
end 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) function Label:onRenderBody(dc)
local text_pen = self.text_pen local text_pen = self.text_pen
if self:getMousePos() and (self.on_click or self.on_rclick) then local hovered = self:getMousePos() and self:shouldHover()
text_pen = self.text_hpen render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self), self.text_hpen, hovered)
end
render_text(self,dc,0,0,text_pen,self.text_dpen,is_disabled(self))
end end
function Label:on_scrollbar(scroll_spec) function Label:on_scrollbar(scroll_spec)
@ -1432,6 +1459,11 @@ function HotkeyLabel:setLabel(label)
self:initializeLabel() self:initializeLabel()
end end
function HotkeyLabel:shouldHover()
-- When on_activate is set, text should also hover on mouseover
return self.on_activate or HotkeyLabel.super.shouldHover(self)
end
function HotkeyLabel:initializeLabel() function HotkeyLabel:initializeLabel()
self:setText{{key=self.key, key_sep=self.key_sep, text=self.label, self:setText{{key=self.key, key_sep=self.key_sep, text=self.label,
on_activate=self.on_activate}} on_activate=self.on_activate}}
@ -1475,6 +1507,12 @@ function CycleHotkeyLabel:init()
} }
end end
-- CycleHotkeyLabels are always clickable and therefore should always change
-- color when hovered.
function CycleHotkeyLabel:shouldHover()
return true
end
function CycleHotkeyLabel:cycle(backwards) function CycleHotkeyLabel:cycle(backwards)
local old_option_idx = self.option_idx local old_option_idx = self.option_idx
if self.option_idx == #self.options and not backwards then if self.option_idx == #self.options and not backwards then
@ -1565,6 +1603,7 @@ List = defclass(List, Widget)
List.ATTRS{ List.ATTRS{
text_pen = COLOR_CYAN, text_pen = COLOR_CYAN,
text_hpen = DEFAULT_NIL, -- hover color, defaults to inverting the FG/BG pens for each text object
cursor_pen = COLOR_LIGHTCYAN, cursor_pen = COLOR_LIGHTCYAN,
inactive_pen = DEFAULT_NIL, inactive_pen = DEFAULT_NIL,
on_select = DEFAULT_NIL, on_select = DEFAULT_NIL,
@ -1745,12 +1784,16 @@ function List:onRenderBody(dc)
end end
end end
local hoveridx = self:getIdxUnderMouse()
for i = top,iend do for i = top,iend do
local obj = choices[i] local obj = choices[i]
local current = (i == self.selected) local current = (i == self.selected)
local cur_pen = self.cursor_pen local hovered = (i == hoveridx)
local cur_dpen = self.text_pen -- cur_pen and cur_dpen can't be integers or background colors get
local active_pen = current and cur_pen or cur_dpen -- 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)
if not getval(self.active) then if not getval(self.active) then
cur_pen = self.inactive_pen or self.cursor_pen cur_pen = self.inactive_pen or self.cursor_pen
@ -1764,7 +1807,7 @@ function List:onRenderBody(dc)
paint_icon(icon, obj) paint_icon(icon, obj)
end 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 local ip = dc.width