Skip to content

Commit 52e52c4

Browse files
committed
TabControl scroll feature
1 parent f868e03 commit 52e52c4

File tree

1 file changed

+131
-26
lines changed

1 file changed

+131
-26
lines changed

src/elements/TabControl.lua

Lines changed: 131 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ TabControl.defineProperty(TabControl, "headerBackground", {default = colors.gray
2525
TabControl.defineProperty(TabControl, "activeTabBackground", {default = colors.white, type = "color", canTriggerRender = true})
2626
---@property activeTabTextColor color Foreground color for the active tab text
2727
TabControl.defineProperty(TabControl, "activeTabTextColor", {default = colors.black, type = "color", canTriggerRender = true})
28+
---@property scrollableTab boolean Enables scroll mode for tabs if they exceed width
29+
TabControl.defineProperty(TabControl, "scrollableTab", {default = false, type = "boolean", canTriggerRender = true})
30+
---@property tabScrollOffset number Current scroll offset for tabs in scrollable mode
31+
TabControl.defineProperty(TabControl, "tabScrollOffset", {default = 0, type = "number", canTriggerRender = true})
2832

2933
TabControl.defineEvent(TabControl, "mouse_click")
3034
TabControl.defineEvent(TabControl, "mouse_up")
35+
TabControl.defineEvent(TabControl, "mouse_scroll")
3136

3237
--- @shortDescription Creates a new TabControl instance
3338
--- @return TabControl self The created instance
@@ -183,28 +188,95 @@ function TabControl:_getHeaderMetrics()
183188
local tabs = self.get("tabs") or {}
184189
local width = self.get("width") or 1
185190
local minTabH = self.get("tabHeight") or 1
191+
local scrollable = self.get("scrollableTab")
186192

187193
local positions = {}
188-
local line = 1
189-
local cursorX = 1
190-
for i, tab in ipairs(tabs) do
191-
local tabWidth = #tab.title + 2
192-
if tabWidth > width then
193-
tabWidth = width
194+
195+
if scrollable then
196+
local scrollOffset = self.get("tabScrollOffset") or 0
197+
local actualX = 1
198+
local totalWidth = 0
199+
200+
for i, tab in ipairs(tabs) do
201+
local tabWidth = #tab.title + 2
202+
if tabWidth > width then
203+
tabWidth = width
204+
end
205+
206+
local visualX = actualX - scrollOffset
207+
local startClip = 0
208+
local endClip = 0
209+
210+
if visualX < 1 then
211+
startClip = 1 - visualX
212+
end
213+
214+
if visualX + tabWidth - 1 > width then
215+
endClip = (visualX + tabWidth - 1) - width
216+
end
217+
218+
if visualX + tabWidth > 1 and visualX <= width then
219+
local displayX = math.max(1, visualX)
220+
local displayWidth = tabWidth - startClip - endClip
221+
222+
table.insert(positions, {
223+
id = tab.id,
224+
title = tab.title,
225+
line = 1,
226+
x1 = displayX,
227+
x2 = displayX + displayWidth - 1,
228+
width = tabWidth,
229+
displayWidth = displayWidth,
230+
actualX = actualX,
231+
startClip = startClip,
232+
endClip = endClip
233+
})
234+
end
235+
236+
actualX = actualX + tabWidth
194237
end
195-
if cursorX + tabWidth - 1 > width then
196-
line = line + 1
197-
cursorX = 1
238+
239+
totalWidth = actualX - 1
240+
241+
return {
242+
headerHeight = 1,
243+
lines = 1,
244+
positions = positions,
245+
totalWidth = totalWidth,
246+
scrollOffset = scrollOffset,
247+
maxScroll = math.max(0, totalWidth - width)
248+
}
249+
else
250+
local line = 1
251+
local cursorX = 1
252+
253+
for i, tab in ipairs(tabs) do
254+
local tabWidth = #tab.title + 2
255+
if tabWidth > width then
256+
tabWidth = width
257+
end
258+
if cursorX + tabWidth - 1 > width then
259+
line = line + 1
260+
cursorX = 1
261+
end
262+
table.insert(positions, {
263+
id = tab.id,
264+
title = tab.title,
265+
line = line,
266+
x1 = cursorX,
267+
x2 = cursorX + tabWidth - 1,
268+
width = tabWidth
269+
})
270+
cursorX = cursorX + tabWidth
198271
end
199-
table.insert(positions, {id = tab.id, title = tab.title, line = line, x1 = cursorX, x2 = cursorX + tabWidth - 1, width = tabWidth})
200-
cursorX = cursorX + tabWidth
201-
end
202272

203-
local computedLines = line
204-
local headerHeight = math.max(minTabH, computedLines)
205-
return {headerHeight = headerHeight, lines = computedLines, positions = positions}
273+
local computedLines = line
274+
local headerHeight = math.max(minTabH, computedLines)
275+
return {headerHeight = headerHeight, lines = computedLines, positions = positions}
276+
end
206277
end
207278

279+
208280
--- @shortDescription Handles mouse click events for tab switching
209281
--- @param button number The button that was clicked
210282
--- @param x number The x position of the click (global)
@@ -327,18 +399,39 @@ function TabControl:mouse_drag(button, x, y)
327399
return false
328400
end
329401

402+
---Scrolls the tab header left or right if scrollableTab is enabled
403+
--- @shortDescription Scrolls the tab header left or right if scrollableTab is enabled
404+
--- @param direction number -1 to scroll left, 1 to scroll right
405+
--- @return TabControl self For method chaining
406+
function TabControl:scrollTabs(direction)
407+
if not self.get("scrollableTab") then return self end
408+
409+
local metrics = self:_getHeaderMetrics()
410+
local currentOffset = self.get("tabScrollOffset") or 0
411+
local maxScroll = metrics.maxScroll or 0
412+
413+
local newOffset = currentOffset + (direction * 5)
414+
newOffset = math.max(0, math.min(maxScroll, newOffset))
415+
416+
self.set("tabScrollOffset", newOffset)
417+
return self
418+
end
419+
330420
function TabControl:mouse_scroll(direction, x, y)
331421
if VisualElement.mouse_scroll(self, direction, x, y) then
332-
local baseRelX, baseRelY = VisualElement.getRelativePosition(self, x, y)
333-
local headerH = self:_getHeaderMetrics().headerHeight
334-
if baseRelY <= headerH then
422+
local headerH = self:_getHeaderMetrics().headerHeight
423+
424+
if self.get("scrollableTab") and y == self.get("y") then
425+
self:scrollTabs(direction)
335426
return true
336427
end
428+
337429
return Container.mouse_scroll(self, direction, x, y)
338430
end
339431
return false
340432
end
341433

434+
342435
--- @shortDescription Sets the cursor position; accounts for tab header offset when delegating to parent
343436
function TabControl:setCursor(x, y, blink, color)
344437
local tabH = self:_getHeaderMetrics().headerHeight
@@ -360,19 +453,31 @@ end
360453
--- @protected
361454
function TabControl:render()
362455
VisualElement.render(self)
363-
364456
local width = self.get("width")
365-
366457
local metrics = self:_getHeaderMetrics()
367458
local headerH = metrics.headerHeight or 1
368-
VisualElement.multiBlit(self, 1, 1, width, headerH, " ", tHex[self.get("foreground")], tHex[self.get("headerBackground")])
369459

460+
VisualElement.multiBlit(self, 1, 1, width, headerH, " ", tHex[self.get("foreground")], tHex[self.get("headerBackground")])
370461
local activeTab = self.get("activeTab")
462+
371463
for _, pos in ipairs(metrics.positions) do
372-
local bgColor = (pos.id == activeTab) and self.get("activeTabBackground") or self.get("headerBackground")
373-
local fgColor = (pos.id == activeTab) and self.get("activeTabTextColor") or self.get("foreground")
374-
VisualElement.multiBlit(self, pos.x1, pos.line, pos.width, 1, " ", tHex[self.get("foreground")], tHex[bgColor])
375-
VisualElement.textFg(self, pos.x1 + 1, pos.line, pos.title, fgColor)
464+
local bgColor = (pos.id == activeTab) and self.get("activeTabBackground") or self.get("headerBackground")
465+
local fgColor = (pos.id == activeTab) and self.get("activeTabTextColor") or self.get("foreground")
466+
467+
VisualElement.multiBlit(self, pos.x1, pos.line, pos.displayWidth or (pos.x2 - pos.x1 + 1), 1, " ", tHex[self.get("foreground")], tHex[bgColor])
468+
469+
local displayTitle = pos.title
470+
local textStartInTitle = 1 + (pos.startClip or 0)
471+
local textLength = #pos.title - (pos.startClip or 0) - (pos.endClip or 0)
472+
473+
if textLength > 0 then
474+
displayTitle = pos.title:sub(textStartInTitle, textStartInTitle + textLength - 1)
475+
local textX = pos.x1
476+
if (pos.startClip or 0) == 0 then
477+
textX = textX + 1
478+
end
479+
VisualElement.textFg(self, textX, pos.line, displayTitle, fgColor)
480+
end
376481
end
377482

378483
if not self.get("childrenSorted") then
@@ -425,4 +530,4 @@ function TabControl:sortChildrenEvents(eventName)
425530
return self
426531
end
427532

428-
return TabControl
533+
return TabControl

0 commit comments

Comments
 (0)