@@ -25,9 +25,14 @@ TabControl.defineProperty(TabControl, "headerBackground", {default = colors.gray
2525TabControl .defineProperty (TabControl , " activeTabBackground" , {default = colors .white , type = " color" , canTriggerRender = true })
2626--- @property activeTabTextColor color Foreground color for the active tab text
2727TabControl .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
2933TabControl .defineEvent (TabControl , " mouse_click" )
3034TabControl .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
206277end
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
328400end
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+
330420function 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
340432end
341433
434+
342435--- @shortDescription Sets the cursor position; accounts for tab header offset when delegating to parent
343436function TabControl :setCursor (x , y , blink , color )
344437 local tabH = self :_getHeaderMetrics ().headerHeight
@@ -360,19 +453,31 @@ end
360453--- @protected
361454function 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
426531end
427532
428- return TabControl
533+ return TabControl
0 commit comments