TabControl scroll feature

This commit is contained in:
Robert Jelic
2025-10-07 00:15:44 +02:00
parent f868e03f85
commit 52e52c4c76

View File

@@ -25,9 +25,14 @@ TabControl.defineProperty(TabControl, "headerBackground", {default = colors.gray
TabControl.defineProperty(TabControl, "activeTabBackground", {default = colors.white, type = "color", canTriggerRender = true})
---@property activeTabTextColor color Foreground color for the active tab text
TabControl.defineProperty(TabControl, "activeTabTextColor", {default = colors.black, type = "color", canTriggerRender = true})
---@property scrollableTab boolean Enables scroll mode for tabs if they exceed width
TabControl.defineProperty(TabControl, "scrollableTab", {default = false, type = "boolean", canTriggerRender = true})
---@property tabScrollOffset number Current scroll offset for tabs in scrollable mode
TabControl.defineProperty(TabControl, "tabScrollOffset", {default = 0, type = "number", canTriggerRender = true})
TabControl.defineEvent(TabControl, "mouse_click")
TabControl.defineEvent(TabControl, "mouse_up")
TabControl.defineEvent(TabControl, "mouse_scroll")
--- @shortDescription Creates a new TabControl instance
--- @return TabControl self The created instance
@@ -183,28 +188,95 @@ function TabControl:_getHeaderMetrics()
local tabs = self.get("tabs") or {}
local width = self.get("width") or 1
local minTabH = self.get("tabHeight") or 1
local scrollable = self.get("scrollableTab")
local positions = {}
local line = 1
local cursorX = 1
for i, tab in ipairs(tabs) do
local tabWidth = #tab.title + 2
if tabWidth > width then
tabWidth = width
end
if cursorX + tabWidth - 1 > width then
line = line + 1
cursorX = 1
end
table.insert(positions, {id = tab.id, title = tab.title, line = line, x1 = cursorX, x2 = cursorX + tabWidth - 1, width = tabWidth})
cursorX = cursorX + tabWidth
end
local computedLines = line
local headerHeight = math.max(minTabH, computedLines)
return {headerHeight = headerHeight, lines = computedLines, positions = positions}
if scrollable then
local scrollOffset = self.get("tabScrollOffset") or 0
local actualX = 1
local totalWidth = 0
for i, tab in ipairs(tabs) do
local tabWidth = #tab.title + 2
if tabWidth > width then
tabWidth = width
end
local visualX = actualX - scrollOffset
local startClip = 0
local endClip = 0
if visualX < 1 then
startClip = 1 - visualX
end
if visualX + tabWidth - 1 > width then
endClip = (visualX + tabWidth - 1) - width
end
if visualX + tabWidth > 1 and visualX <= width then
local displayX = math.max(1, visualX)
local displayWidth = tabWidth - startClip - endClip
table.insert(positions, {
id = tab.id,
title = tab.title,
line = 1,
x1 = displayX,
x2 = displayX + displayWidth - 1,
width = tabWidth,
displayWidth = displayWidth,
actualX = actualX,
startClip = startClip,
endClip = endClip
})
end
actualX = actualX + tabWidth
end
totalWidth = actualX - 1
return {
headerHeight = 1,
lines = 1,
positions = positions,
totalWidth = totalWidth,
scrollOffset = scrollOffset,
maxScroll = math.max(0, totalWidth - width)
}
else
local line = 1
local cursorX = 1
for i, tab in ipairs(tabs) do
local tabWidth = #tab.title + 2
if tabWidth > width then
tabWidth = width
end
if cursorX + tabWidth - 1 > width then
line = line + 1
cursorX = 1
end
table.insert(positions, {
id = tab.id,
title = tab.title,
line = line,
x1 = cursorX,
x2 = cursorX + tabWidth - 1,
width = tabWidth
})
cursorX = cursorX + tabWidth
end
local computedLines = line
local headerHeight = math.max(minTabH, computedLines)
return {headerHeight = headerHeight, lines = computedLines, positions = positions}
end
end
--- @shortDescription Handles mouse click events for tab switching
--- @param button number The button that was clicked
--- @param x number The x position of the click (global)
@@ -327,18 +399,39 @@ function TabControl:mouse_drag(button, x, y)
return false
end
---Scrolls the tab header left or right if scrollableTab is enabled
--- @shortDescription Scrolls the tab header left or right if scrollableTab is enabled
--- @param direction number -1 to scroll left, 1 to scroll right
--- @return TabControl self For method chaining
function TabControl:scrollTabs(direction)
if not self.get("scrollableTab") then return self end
local metrics = self:_getHeaderMetrics()
local currentOffset = self.get("tabScrollOffset") or 0
local maxScroll = metrics.maxScroll or 0
local newOffset = currentOffset + (direction * 5)
newOffset = math.max(0, math.min(maxScroll, newOffset))
self.set("tabScrollOffset", newOffset)
return self
end
function TabControl:mouse_scroll(direction, x, y)
if VisualElement.mouse_scroll(self, direction, x, y) then
local baseRelX, baseRelY = VisualElement.getRelativePosition(self, x, y)
local headerH = self:_getHeaderMetrics().headerHeight
if baseRelY <= headerH then
local headerH = self:_getHeaderMetrics().headerHeight
if self.get("scrollableTab") and y == self.get("y") then
self:scrollTabs(direction)
return true
end
return Container.mouse_scroll(self, direction, x, y)
end
return false
end
--- @shortDescription Sets the cursor position; accounts for tab header offset when delegating to parent
function TabControl:setCursor(x, y, blink, color)
local tabH = self:_getHeaderMetrics().headerHeight
@@ -360,19 +453,31 @@ end
--- @protected
function TabControl:render()
VisualElement.render(self)
local width = self.get("width")
local metrics = self:_getHeaderMetrics()
local headerH = metrics.headerHeight or 1
VisualElement.multiBlit(self, 1, 1, width, headerH, " ", tHex[self.get("foreground")], tHex[self.get("headerBackground")])
VisualElement.multiBlit(self, 1, 1, width, headerH, " ", tHex[self.get("foreground")], tHex[self.get("headerBackground")])
local activeTab = self.get("activeTab")
for _, pos in ipairs(metrics.positions) do
local bgColor = (pos.id == activeTab) and self.get("activeTabBackground") or self.get("headerBackground")
local fgColor = (pos.id == activeTab) and self.get("activeTabTextColor") or self.get("foreground")
VisualElement.multiBlit(self, pos.x1, pos.line, pos.width, 1, " ", tHex[self.get("foreground")], tHex[bgColor])
VisualElement.textFg(self, pos.x1 + 1, pos.line, pos.title, fgColor)
local bgColor = (pos.id == activeTab) and self.get("activeTabBackground") or self.get("headerBackground")
local fgColor = (pos.id == activeTab) and self.get("activeTabTextColor") or self.get("foreground")
VisualElement.multiBlit(self, pos.x1, pos.line, pos.displayWidth or (pos.x2 - pos.x1 + 1), 1, " ", tHex[self.get("foreground")], tHex[bgColor])
local displayTitle = pos.title
local textStartInTitle = 1 + (pos.startClip or 0)
local textLength = #pos.title - (pos.startClip or 0) - (pos.endClip or 0)
if textLength > 0 then
displayTitle = pos.title:sub(textStartInTitle, textStartInTitle + textLength - 1)
local textX = pos.x1
if (pos.startClip or 0) == 0 then
textX = textX + 1
end
VisualElement.textFg(self, textX, pos.line, displayTitle, fgColor)
end
end
if not self.get("childrenSorted") then
@@ -425,4 +530,4 @@ function TabControl:sortChildrenEvents(eventName)
return self
end
return TabControl
return TabControl