Skip to content

Commit

Permalink
Merge pull request #5022 from robob27/robob27/scrolling-tabbar
Browse files Browse the repository at this point in the history
Scrolling `TabBar`
  • Loading branch information
myk002 authored Nov 28, 2024
2 parents bccaea4 + 78d6a2f commit 9e04b22
Show file tree
Hide file tree
Showing 4 changed files with 821 additions and 8 deletions.
1 change: 1 addition & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ Template for new versions:
## Lua

- ``dfhack.units``: new function ``setPathGoal``
- ``widgets.TabBar``: updated to allow for horizontal scrolling of tabs when there are too many to fit in the available space

## Removed
- UI focus strings for squad panel flows combined into a single tree: ``dwarfmode/SquadEquipment`` -> ``dwarfmode/Squads/Equipment``, ``dwarfmode/SquadSchedule`` -> ``dwarfmode/Squads/Schedule``
Expand Down
35 changes: 32 additions & 3 deletions docs/dev/Lua API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6194,9 +6194,16 @@ TabBar class
------------

This widget implements a set of one or more tabs to allow navigation between groups
of content. Tabs automatically wrap on the width of the window and will continue
rendering on the next line(s) if all tabs cannot fit on a single line.

of content.

:wrap: If true, tabs automatically wrap on the width of the window and will
continue rendering on the next line(s) if all tabs cannot fit on a single line.
If false, tabs will be truncated and can be scrolled using ``scroll_key``
and ``scroll_key_back``, mouse wheel or by clicking on the scroll labels
that will automatically appear on the left and right sides of the tab bar
as needed. When clicking on a tab or using ``key`` or ``key_back`` to switch tabs,
the selected tab will be scrolled into view if it is not already visible.
Defaults to true.
:key: Specifies a keybinding that can be used to switch to the next tab.
Defaults to ``CUSTOM_CTRL_T``.
:key_back: Specifies a keybinding that can be used to switch to the previous
Expand All @@ -6222,6 +6229,28 @@ rendering on the next line(s) if all tabs cannot fit on a single line.
itself as the second. The default implementation, which will handle most
situations, returns ``self.active_tab_pens``, if ``self.get_cur_page() == idx``,
otherwise returns ``self.inactive_tab_pens``.
:scroll_key: Specifies a keybinding that can be used to scroll the tabs to the right.
Defaults to ``CUSTOM_ALT_T``.
:scroll_key_back: Specifies a keybinding that can be used to scroll the tabs to the left.
Defaults to ``CUSTOM_ALT_Y``.
:scroll_left_text: The text to display on the left scroll label.
Defaults to "<<<".
:scroll_right_text: The text to display on the right scroll label.
Defaults to ">>>".
:scroll_label_text_pen: The pen to use for the scroll label text.
Defaults to ``Label`` default.
:scroll_label_text_hpen: The pen to use for the scroll label text when hovered.
Defaults to ``scroll_label_text_pen`` with the background
and foreground colors swapped.
:scroll_step: The number of units to scroll tabs by.
Defaults to 10.
:fast_scroll_multiplier: The multiplier for fast scrolling (holding shift).
Defaults to 3.
:scroll_into_view_offset: After a selected tab is scrolled into view, this offset
is added to the scroll position to ensure the tab is
not flush against the edge of the tab bar, allowing
some space for the user to see the next tab.
Defaults to 5.

Tab class
---------
Expand Down
238 changes: 233 additions & 5 deletions library/lua/gui/widgets/tab_bar.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
local Widget = require('gui.widgets.widget')
local ResizingPanel = require('gui.widgets.containers.resizing_panel')
local Label = require('gui.widgets.labels.label')
local Panel = require('gui.widgets.containers.panel')
local utils = require('utils')

local to_pen = dfhack.pen.parse

Expand Down Expand Up @@ -131,6 +134,14 @@ end
---@field get_pens? fun(index: integer, tabbar: self): widgets.TabPens
---@field key string
---@field key_back string
---@field wrap boolean
---@field scroll_step integer
---@field scroll_key string
---@field scroll_key_back string
---@field fast_scroll_modifier integer
---@field scroll_into_view_offset integer
---@field scroll_label_text_pen dfhack.pen
---@field scroll_label_text_hpen dfhack.pen

---@class widgets.TabBar.attrs.partial: widgets.TabBar.attrs

Expand All @@ -151,17 +162,41 @@ TabBar.ATTRS{
get_pens=DEFAULT_NIL,
key='CUSTOM_CTRL_T',
key_back='CUSTOM_CTRL_Y',
wrap = true,
scroll_step = 10,
scroll_key = 'CUSTOM_ALT_T',
scroll_key_back = 'CUSTOM_ALT_Y',
fast_scroll_modifier = 3,
scroll_into_view_offset = 5,
scroll_label_text_pen = DEFAULT_NIL,
scroll_label_text_hpen = DEFAULT_NIL,
}

local TO_THE_RIGHT = string.char(16)
local TO_THE_LEFT = string.char(17)

---@param self widgets.TabBar
function TabBar:init()
self.scrollable = false
self.scroll_offset = 0
self.first_render = true

local panel = Panel{
view_id='TabBar__tabs',
frame={t=0, l=0, h=2},
frame_inset={l=0},
}

for idx,label in ipairs(self.labels) do
self:addviews{
panel:addviews{
Tab{
frame={t=0, l=0},
id=idx,
label=label,
on_select=self.on_select,
on_select=function()
self.scrollTabIntoView(self, idx)
self.on_select(idx)
end,
get_pens=self.get_pens and function()
return self.get_pens(idx, self)
end or function()
Expand All @@ -174,32 +209,225 @@ function TabBar:init()
}
}
end

self:addviews{panel}

if not self.wrap then
self:addviews{
Label{
view_id='TabBar__scroll_left',
frame={t=0, l=0, w=1},
text_pen=self.scroll_label_text_pen,
text_hpen=self.scroll_label_text_hpen,
text=TO_THE_LEFT,
visible = false,
on_click=function()
self:scrollLeft()
end,
},
Label{
view_id='TabBar__scroll_right',
frame={t=0, l=0, w=1},
text_pen=self.scroll_label_text_pen,
text_hpen=self.scroll_label_text_hpen,
text=TO_THE_RIGHT,
visible = false,
on_click=function()
self:scrollRight()
end,
},
}
end
end

function TabBar:updateScrollElements()
self:showScrollLeft()
self:showScrollRight()
self:updateTabPanelPosition()
end

function TabBar:leftScrollVisible()
return self.scroll_offset < 0
end

function TabBar:showScrollLeft()
if self.wrap then return end
self:scrollLeftElement().visible = self:leftScrollVisible()
end

function TabBar:rightScrollVisible()
return self.scroll_offset > self.offset_to_show_last_tab
end

function TabBar:showScrollRight()
if self.wrap then return end
self:scrollRightElement().visible = self:rightScrollVisible()
end

function TabBar:updateTabPanelPosition()
self:tabsElement().frame_inset.l = self.scroll_offset
self:tabsElement():updateLayout(self.frame_body)
end

function TabBar:tabsElement()
return self.subviews.TabBar__tabs
end

function TabBar:scrollLeftElement()
return self.subviews.TabBar__scroll_left
end

function TabBar:scrollRightElement()
return self.subviews.TabBar__scroll_right
end

function TabBar:scrollTabIntoView(idx)
if self.wrap or not self.scrollable then return end

local tab = self:tabsElement().subviews[idx]
local tab_l = tab.frame.l
local tab_r = tab.frame.l + tab.frame.w
local tabs_l = self:tabsElement().frame.l
local tabs_r = tabs_l + self.frame_body.width
local scroll_offset = self.scroll_offset

if tab_l < tabs_l - scroll_offset then
self.scroll_offset = tabs_l - tab_l + self.scroll_into_view_offset
elseif tab_r > tabs_r - scroll_offset then
self.scroll_offset = self.scroll_offset - (tab_r - tabs_r) - self.scroll_into_view_offset
end

self:capScrollOffset()
self:updateScrollElements()
end

function TabBar:capScrollOffset()
if self.scroll_offset > 0 then
self.scroll_offset = 0
elseif self.scroll_offset < self.offset_to_show_last_tab then
self.scroll_offset = self.offset_to_show_last_tab
end
end

function TabBar:scrollRight(alternate_step)
if not self:scrollRightElement().visible then return end

self.scroll_offset = self.scroll_offset - (alternate_step and alternate_step or self.scroll_step)

self:capScrollOffset()
self:updateScrollElements()
end

function TabBar:scrollLeft(alternate_step)
if not self:scrollLeftElement().visible then return end

self.scroll_offset = self.scroll_offset + (alternate_step and alternate_step or self.scroll_step)

self:capScrollOffset()
self:updateScrollElements()
end

function TabBar:isMouseOver()
for _, sv in ipairs(self:tabsElement().subviews) do
if utils.getval(sv.visible) and sv:getMouseFramePos() then return true end
end
end

function TabBar:postComputeFrame(body)
local all_tabs_width = 0

local t, l, width = 0, 0, body.width
for _,tab in ipairs(self.subviews) do
self.scrollable = false

self.last_post_compute_width = self.post_compute_width or 0
self.post_compute_width = width

local tab_rows = 1
for _,tab in ipairs(self:tabsElement().subviews) do
tab.visible = true
if l > 0 and l + tab.frame.w > width then
t = t + 2
l = 0
if self.wrap then
t = t + 2
l = 0
tab_rows = tab_rows + 1
else
self.scrollable = true
end
end
tab.frame.t = t
tab.frame.l = l
l = l + tab.frame.w
all_tabs_width = all_tabs_width + tab.frame.w
end

self.offset_to_show_last_tab = -(all_tabs_width - self.post_compute_width)

if self.scrollable and not self.wrap then
self:scrollRightElement().frame.l = width - 1
if self.last_post_compute_width ~= self.post_compute_width then
self.scroll_offset = 0
end
end

if self.first_render then
self.first_render = false
if not self.wrap then
self:scrollTabIntoView(self.get_cur_page())
end
end

-- we have to calculate the height of this based on the number of tab rows we will have
-- so that autoarrange_subviews will work correctly
self:tabsElement().frame.h = tab_rows * 2

self:updateScrollElements()
end

function TabBar:fastStep()
return self.scroll_step * self.fast_scroll_modifier
end

function TabBar:onInput(keys)
if TabBar.super.onInput(self, keys) then return true end
if not self.wrap then
if self:isMouseOver() then
if keys.CONTEXT_SCROLL_UP then
self:scrollLeft()
return true
end
if keys.CONTEXT_SCROLL_DOWN then
self:scrollRight()
return true
end
if keys.CONTEXT_SCROLL_PAGEUP then
self:scrollLeft(self:fastStep())
return true
end
if keys.CONTEXT_SCROLL_PAGEDOWN then
self:scrollRight(self:fastStep())
return true
end
end
if self.scroll_key and keys[self.scroll_key] then
self:scrollRight()
return true
end
if self.scroll_key_back and keys[self.scroll_key_back] then
self:scrollLeft()
return true
end
end
if self.key and keys[self.key] then
local zero_idx = self.get_cur_page() - 1
local next_zero_idx = (zero_idx + 1) % #self.labels
self.scrollTabIntoView(self, next_zero_idx + 1)
self.on_select(next_zero_idx + 1)
return true
end
if self.key_back and keys[self.key_back] then
local zero_idx = self.get_cur_page() - 1
local prev_zero_idx = (zero_idx - 1) % #self.labels
self.scrollTabIntoView(self, prev_zero_idx + 1)
self.on_select(prev_zero_idx + 1)
return true
end
Expand Down
Loading

0 comments on commit 9e04b22

Please sign in to comment.