Skip to content

Commit 9e04b22

Browse files
authored
Merge pull request #5022 from robob27/robob27/scrolling-tabbar
Scrolling `TabBar`
2 parents bccaea4 + 78d6a2f commit 9e04b22

File tree

4 files changed

+821
-8
lines changed

4 files changed

+821
-8
lines changed

docs/changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Template for new versions:
8686
## Lua
8787

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

9091
## Removed
9192
- UI focus strings for squad panel flows combined into a single tree: ``dwarfmode/SquadEquipment`` -> ``dwarfmode/Squads/Equipment``, ``dwarfmode/SquadSchedule`` -> ``dwarfmode/Squads/Schedule``

docs/dev/Lua API.rst

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6194,9 +6194,16 @@ TabBar class
61946194
------------
61956195

61966196
This widget implements a set of one or more tabs to allow navigation between groups
6197-
of content. Tabs automatically wrap on the width of the window and will continue
6198-
rendering on the next line(s) if all tabs cannot fit on a single line.
6199-
6197+
of content.
6198+
6199+
:wrap: If true, tabs automatically wrap on the width of the window and will
6200+
continue rendering on the next line(s) if all tabs cannot fit on a single line.
6201+
If false, tabs will be truncated and can be scrolled using ``scroll_key``
6202+
and ``scroll_key_back``, mouse wheel or by clicking on the scroll labels
6203+
that will automatically appear on the left and right sides of the tab bar
6204+
as needed. When clicking on a tab or using ``key`` or ``key_back`` to switch tabs,
6205+
the selected tab will be scrolled into view if it is not already visible.
6206+
Defaults to true.
62006207
:key: Specifies a keybinding that can be used to switch to the next tab.
62016208
Defaults to ``CUSTOM_CTRL_T``.
62026209
:key_back: Specifies a keybinding that can be used to switch to the previous
@@ -6222,6 +6229,28 @@ rendering on the next line(s) if all tabs cannot fit on a single line.
62226229
itself as the second. The default implementation, which will handle most
62236230
situations, returns ``self.active_tab_pens``, if ``self.get_cur_page() == idx``,
62246231
otherwise returns ``self.inactive_tab_pens``.
6232+
:scroll_key: Specifies a keybinding that can be used to scroll the tabs to the right.
6233+
Defaults to ``CUSTOM_ALT_T``.
6234+
:scroll_key_back: Specifies a keybinding that can be used to scroll the tabs to the left.
6235+
Defaults to ``CUSTOM_ALT_Y``.
6236+
:scroll_left_text: The text to display on the left scroll label.
6237+
Defaults to "<<<".
6238+
:scroll_right_text: The text to display on the right scroll label.
6239+
Defaults to ">>>".
6240+
:scroll_label_text_pen: The pen to use for the scroll label text.
6241+
Defaults to ``Label`` default.
6242+
:scroll_label_text_hpen: The pen to use for the scroll label text when hovered.
6243+
Defaults to ``scroll_label_text_pen`` with the background
6244+
and foreground colors swapped.
6245+
:scroll_step: The number of units to scroll tabs by.
6246+
Defaults to 10.
6247+
:fast_scroll_multiplier: The multiplier for fast scrolling (holding shift).
6248+
Defaults to 3.
6249+
:scroll_into_view_offset: After a selected tab is scrolled into view, this offset
6250+
is added to the scroll position to ensure the tab is
6251+
not flush against the edge of the tab bar, allowing
6252+
some space for the user to see the next tab.
6253+
Defaults to 5.
62256254

62266255
Tab class
62276256
---------

library/lua/gui/widgets/tab_bar.lua

Lines changed: 233 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
local Widget = require('gui.widgets.widget')
22
local ResizingPanel = require('gui.widgets.containers.resizing_panel')
3+
local Label = require('gui.widgets.labels.label')
4+
local Panel = require('gui.widgets.containers.panel')
5+
local utils = require('utils')
36

47
local to_pen = dfhack.pen.parse
58

@@ -131,6 +134,14 @@ end
131134
---@field get_pens? fun(index: integer, tabbar: self): widgets.TabPens
132135
---@field key string
133136
---@field key_back string
137+
---@field wrap boolean
138+
---@field scroll_step integer
139+
---@field scroll_key string
140+
---@field scroll_key_back string
141+
---@field fast_scroll_modifier integer
142+
---@field scroll_into_view_offset integer
143+
---@field scroll_label_text_pen dfhack.pen
144+
---@field scroll_label_text_hpen dfhack.pen
134145

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

@@ -151,17 +162,41 @@ TabBar.ATTRS{
151162
get_pens=DEFAULT_NIL,
152163
key='CUSTOM_CTRL_T',
153164
key_back='CUSTOM_CTRL_Y',
165+
wrap = true,
166+
scroll_step = 10,
167+
scroll_key = 'CUSTOM_ALT_T',
168+
scroll_key_back = 'CUSTOM_ALT_Y',
169+
fast_scroll_modifier = 3,
170+
scroll_into_view_offset = 5,
171+
scroll_label_text_pen = DEFAULT_NIL,
172+
scroll_label_text_hpen = DEFAULT_NIL,
154173
}
155174

175+
local TO_THE_RIGHT = string.char(16)
176+
local TO_THE_LEFT = string.char(17)
177+
156178
---@param self widgets.TabBar
157179
function TabBar:init()
180+
self.scrollable = false
181+
self.scroll_offset = 0
182+
self.first_render = true
183+
184+
local panel = Panel{
185+
view_id='TabBar__tabs',
186+
frame={t=0, l=0, h=2},
187+
frame_inset={l=0},
188+
}
189+
158190
for idx,label in ipairs(self.labels) do
159-
self:addviews{
191+
panel:addviews{
160192
Tab{
161193
frame={t=0, l=0},
162194
id=idx,
163195
label=label,
164-
on_select=self.on_select,
196+
on_select=function()
197+
self.scrollTabIntoView(self, idx)
198+
self.on_select(idx)
199+
end,
165200
get_pens=self.get_pens and function()
166201
return self.get_pens(idx, self)
167202
end or function()
@@ -174,32 +209,225 @@ function TabBar:init()
174209
}
175210
}
176211
end
212+
213+
self:addviews{panel}
214+
215+
if not self.wrap then
216+
self:addviews{
217+
Label{
218+
view_id='TabBar__scroll_left',
219+
frame={t=0, l=0, w=1},
220+
text_pen=self.scroll_label_text_pen,
221+
text_hpen=self.scroll_label_text_hpen,
222+
text=TO_THE_LEFT,
223+
visible = false,
224+
on_click=function()
225+
self:scrollLeft()
226+
end,
227+
},
228+
Label{
229+
view_id='TabBar__scroll_right',
230+
frame={t=0, l=0, w=1},
231+
text_pen=self.scroll_label_text_pen,
232+
text_hpen=self.scroll_label_text_hpen,
233+
text=TO_THE_RIGHT,
234+
visible = false,
235+
on_click=function()
236+
self:scrollRight()
237+
end,
238+
},
239+
}
240+
end
241+
end
242+
243+
function TabBar:updateScrollElements()
244+
self:showScrollLeft()
245+
self:showScrollRight()
246+
self:updateTabPanelPosition()
247+
end
248+
249+
function TabBar:leftScrollVisible()
250+
return self.scroll_offset < 0
251+
end
252+
253+
function TabBar:showScrollLeft()
254+
if self.wrap then return end
255+
self:scrollLeftElement().visible = self:leftScrollVisible()
256+
end
257+
258+
function TabBar:rightScrollVisible()
259+
return self.scroll_offset > self.offset_to_show_last_tab
260+
end
261+
262+
function TabBar:showScrollRight()
263+
if self.wrap then return end
264+
self:scrollRightElement().visible = self:rightScrollVisible()
265+
end
266+
267+
function TabBar:updateTabPanelPosition()
268+
self:tabsElement().frame_inset.l = self.scroll_offset
269+
self:tabsElement():updateLayout(self.frame_body)
270+
end
271+
272+
function TabBar:tabsElement()
273+
return self.subviews.TabBar__tabs
274+
end
275+
276+
function TabBar:scrollLeftElement()
277+
return self.subviews.TabBar__scroll_left
278+
end
279+
280+
function TabBar:scrollRightElement()
281+
return self.subviews.TabBar__scroll_right
282+
end
283+
284+
function TabBar:scrollTabIntoView(idx)
285+
if self.wrap or not self.scrollable then return end
286+
287+
local tab = self:tabsElement().subviews[idx]
288+
local tab_l = tab.frame.l
289+
local tab_r = tab.frame.l + tab.frame.w
290+
local tabs_l = self:tabsElement().frame.l
291+
local tabs_r = tabs_l + self.frame_body.width
292+
local scroll_offset = self.scroll_offset
293+
294+
if tab_l < tabs_l - scroll_offset then
295+
self.scroll_offset = tabs_l - tab_l + self.scroll_into_view_offset
296+
elseif tab_r > tabs_r - scroll_offset then
297+
self.scroll_offset = self.scroll_offset - (tab_r - tabs_r) - self.scroll_into_view_offset
298+
end
299+
300+
self:capScrollOffset()
301+
self:updateScrollElements()
302+
end
303+
304+
function TabBar:capScrollOffset()
305+
if self.scroll_offset > 0 then
306+
self.scroll_offset = 0
307+
elseif self.scroll_offset < self.offset_to_show_last_tab then
308+
self.scroll_offset = self.offset_to_show_last_tab
309+
end
310+
end
311+
312+
function TabBar:scrollRight(alternate_step)
313+
if not self:scrollRightElement().visible then return end
314+
315+
self.scroll_offset = self.scroll_offset - (alternate_step and alternate_step or self.scroll_step)
316+
317+
self:capScrollOffset()
318+
self:updateScrollElements()
319+
end
320+
321+
function TabBar:scrollLeft(alternate_step)
322+
if not self:scrollLeftElement().visible then return end
323+
324+
self.scroll_offset = self.scroll_offset + (alternate_step and alternate_step or self.scroll_step)
325+
326+
self:capScrollOffset()
327+
self:updateScrollElements()
328+
end
329+
330+
function TabBar:isMouseOver()
331+
for _, sv in ipairs(self:tabsElement().subviews) do
332+
if utils.getval(sv.visible) and sv:getMouseFramePos() then return true end
333+
end
177334
end
178335

179336
function TabBar:postComputeFrame(body)
337+
local all_tabs_width = 0
338+
180339
local t, l, width = 0, 0, body.width
181-
for _,tab in ipairs(self.subviews) do
340+
self.scrollable = false
341+
342+
self.last_post_compute_width = self.post_compute_width or 0
343+
self.post_compute_width = width
344+
345+
local tab_rows = 1
346+
for _,tab in ipairs(self:tabsElement().subviews) do
347+
tab.visible = true
182348
if l > 0 and l + tab.frame.w > width then
183-
t = t + 2
184-
l = 0
349+
if self.wrap then
350+
t = t + 2
351+
l = 0
352+
tab_rows = tab_rows + 1
353+
else
354+
self.scrollable = true
355+
end
185356
end
186357
tab.frame.t = t
187358
tab.frame.l = l
188359
l = l + tab.frame.w
360+
all_tabs_width = all_tabs_width + tab.frame.w
189361
end
362+
363+
self.offset_to_show_last_tab = -(all_tabs_width - self.post_compute_width)
364+
365+
if self.scrollable and not self.wrap then
366+
self:scrollRightElement().frame.l = width - 1
367+
if self.last_post_compute_width ~= self.post_compute_width then
368+
self.scroll_offset = 0
369+
end
370+
end
371+
372+
if self.first_render then
373+
self.first_render = false
374+
if not self.wrap then
375+
self:scrollTabIntoView(self.get_cur_page())
376+
end
377+
end
378+
379+
-- we have to calculate the height of this based on the number of tab rows we will have
380+
-- so that autoarrange_subviews will work correctly
381+
self:tabsElement().frame.h = tab_rows * 2
382+
383+
self:updateScrollElements()
384+
end
385+
386+
function TabBar:fastStep()
387+
return self.scroll_step * self.fast_scroll_modifier
190388
end
191389

192390
function TabBar:onInput(keys)
193391
if TabBar.super.onInput(self, keys) then return true end
392+
if not self.wrap then
393+
if self:isMouseOver() then
394+
if keys.CONTEXT_SCROLL_UP then
395+
self:scrollLeft()
396+
return true
397+
end
398+
if keys.CONTEXT_SCROLL_DOWN then
399+
self:scrollRight()
400+
return true
401+
end
402+
if keys.CONTEXT_SCROLL_PAGEUP then
403+
self:scrollLeft(self:fastStep())
404+
return true
405+
end
406+
if keys.CONTEXT_SCROLL_PAGEDOWN then
407+
self:scrollRight(self:fastStep())
408+
return true
409+
end
410+
end
411+
if self.scroll_key and keys[self.scroll_key] then
412+
self:scrollRight()
413+
return true
414+
end
415+
if self.scroll_key_back and keys[self.scroll_key_back] then
416+
self:scrollLeft()
417+
return true
418+
end
419+
end
194420
if self.key and keys[self.key] then
195421
local zero_idx = self.get_cur_page() - 1
196422
local next_zero_idx = (zero_idx + 1) % #self.labels
423+
self.scrollTabIntoView(self, next_zero_idx + 1)
197424
self.on_select(next_zero_idx + 1)
198425
return true
199426
end
200427
if self.key_back and keys[self.key_back] then
201428
local zero_idx = self.get_cur_page() - 1
202429
local prev_zero_idx = (zero_idx - 1) % #self.labels
430+
self.scrollTabIntoView(self, prev_zero_idx + 1)
203431
self.on_select(prev_zero_idx + 1)
204432
return true
205433
end

0 commit comments

Comments
 (0)