Added TabControl Element
Added ComboBox Element Added/Updated Switch Element Fixed TextBox Syntax Highlighting Bug
This commit is contained in:
35
examples/TabControl.lua
Normal file
35
examples/TabControl.lua
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
local basalt = require("basalt")
|
||||||
|
|
||||||
|
local frame = basalt.getMainFrame()
|
||||||
|
|
||||||
|
local tabs = frame:addTabControl({x = 2,
|
||||||
|
y = 2,
|
||||||
|
width = 35,
|
||||||
|
height = 12,
|
||||||
|
headerBackground = colors.black,
|
||||||
|
foreground = colors.lightGray
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
-- create three tabs; addTab now returns a proxy for adding elements into the new tab
|
||||||
|
local overview = tabs:newTab("Overview")
|
||||||
|
local editor = tabs:newTab("Editor")
|
||||||
|
local settings = tabs:newTab("Settings")
|
||||||
|
|
||||||
|
-- Overview tab: add a label and a button
|
||||||
|
overview:addLabel({x = 2, y = 2, width = 46}):setText("Welcome to the demo")
|
||||||
|
overview:addButton({x = 2, y = 4, width = 12, height = 3}):setText("Click me")
|
||||||
|
:setBackground("{self.clicked and colors.green or colors.black}")
|
||||||
|
:setForeground("{self.clicked and colors.black or colors.lightGray}")
|
||||||
|
|
||||||
|
-- Editor tab: textbox with some sample text
|
||||||
|
editor:addTextBox({x = 2, y = 2, width = 12, height = 8, background=colors.black, foreground=colors.white}):setText("Type here...\nLine 2\nLine 3")
|
||||||
|
|
||||||
|
-- Settings tab: show some inputs
|
||||||
|
settings:addLabel({x = 2, y = 2, width = 20}):setText("Settings")
|
||||||
|
settings:addLabel({x = 2, y = 4, width = 20}):setText("Username:")
|
||||||
|
settings:addLabel({x = 2, y = 6, width = 20}):setText("Password:")
|
||||||
|
settings:addInput({x = 12, y = 4, width = 20, background=colors.black, foreground=colors.white})
|
||||||
|
settings:addInput({x = 12, y = 6, width = 20, background=colors.black, foreground=colors.white})
|
||||||
|
|
||||||
|
basalt.run()
|
||||||
423
src/elements/ComboBox.lua
Normal file
423
src/elements/ComboBox.lua
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
local VisualElement = require("elements/VisualElement")
|
||||||
|
local Dropdown = require("elements/Dropdown")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
|
||||||
|
---@configDescription A ComboBox that combines dropdown selection with editable text input
|
||||||
|
---@configDefault false
|
||||||
|
|
||||||
|
--- This is the ComboBox class. It extends the dropdown functionality with editable text input,
|
||||||
|
--- allowing users to either select from a list or type their own custom text.
|
||||||
|
--- @usage local ComboBox = main:addCombobox()
|
||||||
|
--- @usage ComboBox:setEditable(true)
|
||||||
|
--- @usage ComboBox:setItems({
|
||||||
|
--- @usage {text = "Option 1"},
|
||||||
|
--- @usage {text = "Option 2"},
|
||||||
|
--- @usage {text = "Option 3"},
|
||||||
|
--- @usage })
|
||||||
|
--- @usage ComboBox:setText("Custom input...")
|
||||||
|
---@class ComboBox : Dropdown
|
||||||
|
local ComboBox = setmetatable({}, Dropdown)
|
||||||
|
ComboBox.__index = ComboBox
|
||||||
|
|
||||||
|
---@property editable boolean true Whether the ComboBox allows text input
|
||||||
|
ComboBox.defineProperty(ComboBox, "editable", {default = true, type = "boolean", canTriggerRender = true})
|
||||||
|
---@property text string "" The current text content of the ComboBox
|
||||||
|
ComboBox.defineProperty(ComboBox, "text", {default = "", type = "string", canTriggerRender = true})
|
||||||
|
---@property cursorPos number 1 The current cursor position in the text
|
||||||
|
ComboBox.defineProperty(ComboBox, "cursorPos", {default = 1, type = "number"})
|
||||||
|
---@property viewOffset number 0 The horizontal scroll offset for viewing long text
|
||||||
|
ComboBox.defineProperty(ComboBox, "viewOffset", {default = 0, type = "number", canTriggerRender = true})
|
||||||
|
---@property placeholder string "..." Text to display when input is empty
|
||||||
|
ComboBox.defineProperty(ComboBox, "placeholder", {default = "...", type = "string"})
|
||||||
|
---@property placeholderColor color gray Color of the placeholder text
|
||||||
|
ComboBox.defineProperty(ComboBox, "placeholderColor", {default = colors.gray, type = "color"})
|
||||||
|
---@property focusedBackground color blue Background color when ComboBox is focused
|
||||||
|
ComboBox.defineProperty(ComboBox, "focusedBackground", {default = colors.blue, type = "color"})
|
||||||
|
---@property focusedForeground color white Foreground color when ComboBox is focused
|
||||||
|
ComboBox.defineProperty(ComboBox, "focusedForeground", {default = colors.white, type = "color"})
|
||||||
|
---@property autoComplete boolean false Whether to enable auto-complete filtering when typing
|
||||||
|
ComboBox.defineProperty(ComboBox, "autoComplete", {default = false, type = "boolean"})
|
||||||
|
---@property manuallyOpened boolean false Whether the dropdown was manually opened (not by auto-complete)
|
||||||
|
ComboBox.defineProperty(ComboBox, "manuallyOpened", {default = false, type = "boolean"})
|
||||||
|
|
||||||
|
--- Creates a new ComboBox instance
|
||||||
|
--- @shortDescription Creates a new ComboBox instance
|
||||||
|
--- @return ComboBox self The newly created ComboBox instance
|
||||||
|
function ComboBox.new()
|
||||||
|
local self = setmetatable({}, ComboBox):__init()
|
||||||
|
self.class = ComboBox
|
||||||
|
self.set("width", 16)
|
||||||
|
self.set("height", 1)
|
||||||
|
self.set("z", 8)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Initializes the ComboBox instance
|
||||||
|
--- @param props table The properties to initialize the element with
|
||||||
|
--- @param basalt table The basalt instance
|
||||||
|
--- @return ComboBox self The initialized instance
|
||||||
|
--- @protected
|
||||||
|
function ComboBox:init(props, basalt)
|
||||||
|
Dropdown.init(self, props, basalt)
|
||||||
|
self.set("type", "ComboBox")
|
||||||
|
|
||||||
|
self.set("cursorPos", 1)
|
||||||
|
self.set("viewOffset", 0)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets the text content of the ComboBox
|
||||||
|
--- @shortDescription Sets the text content
|
||||||
|
--- @param text string The text to set
|
||||||
|
--- @return ComboBox self
|
||||||
|
function ComboBox:setText(text)
|
||||||
|
if text == nil then text = "" end
|
||||||
|
self.set("text", tostring(text))
|
||||||
|
self.set("cursorPos", #self.get("text") + 1)
|
||||||
|
self:updateViewport()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets the current text content
|
||||||
|
--- @shortDescription Gets the text content
|
||||||
|
--- @return string text The current text
|
||||||
|
function ComboBox:getText()
|
||||||
|
return self.get("text")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Sets whether the ComboBox is editable
|
||||||
|
--- @shortDescription Sets editable state
|
||||||
|
--- @param editable boolean Whether the ComboBox should be editable
|
||||||
|
--- @return ComboBox self
|
||||||
|
function ComboBox:setEditable(editable)
|
||||||
|
self.set("editable", editable)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Filters items based on current text for auto-complete
|
||||||
|
--- @shortDescription Filters items for auto-complete
|
||||||
|
--- @private
|
||||||
|
function ComboBox:getFilteredItems()
|
||||||
|
local allItems = self.get("items") or {}
|
||||||
|
local currentText = self.get("text"):lower()
|
||||||
|
|
||||||
|
if not self.get("autoComplete") or #currentText == 0 then
|
||||||
|
return allItems
|
||||||
|
end
|
||||||
|
|
||||||
|
local filteredItems = {}
|
||||||
|
for _, item in ipairs(allItems) do
|
||||||
|
local itemText = ""
|
||||||
|
if type(item) == "string" then
|
||||||
|
itemText = item:lower()
|
||||||
|
elseif type(item) == "table" and item.text then
|
||||||
|
itemText = item.text:lower()
|
||||||
|
end
|
||||||
|
|
||||||
|
if itemText:find(currentText, 1, true) then
|
||||||
|
table.insert(filteredItems, item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return filteredItems
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Updates the dropdown with filtered items
|
||||||
|
--- @shortDescription Updates dropdown with filtered items
|
||||||
|
--- @private
|
||||||
|
function ComboBox:updateFilteredDropdown()
|
||||||
|
if not self.get("autoComplete") then return end
|
||||||
|
|
||||||
|
local filteredItems = self:getFilteredItems()
|
||||||
|
local shouldOpen = #filteredItems > 0 and #self.get("text") > 0
|
||||||
|
|
||||||
|
if shouldOpen then
|
||||||
|
self.set("isOpen", true)
|
||||||
|
self.set("manuallyOpened", false)
|
||||||
|
local dropdownHeight = self.get("dropdownHeight") or 5
|
||||||
|
local actualHeight = math.min(dropdownHeight, #filteredItems)
|
||||||
|
self.set("height", 1 + actualHeight)
|
||||||
|
else
|
||||||
|
self.set("isOpen", false)
|
||||||
|
self.set("manuallyOpened", false)
|
||||||
|
self.set("height", 1)
|
||||||
|
end
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
--- @shortDescription Updates the viewport
|
||||||
|
--- @private
|
||||||
|
function ComboBox:updateViewport()
|
||||||
|
local text = self.get("text")
|
||||||
|
local cursorPos = self.get("cursorPos")
|
||||||
|
local width = self.get("width")
|
||||||
|
local dropSymbol = self.get("dropSymbol")
|
||||||
|
|
||||||
|
local textWidth = width - #dropSymbol
|
||||||
|
if textWidth < 1 then textWidth = 1 end
|
||||||
|
|
||||||
|
local viewOffset = self.get("viewOffset")
|
||||||
|
|
||||||
|
if cursorPos - viewOffset > textWidth then
|
||||||
|
viewOffset = cursorPos - textWidth
|
||||||
|
elseif cursorPos - 1 < viewOffset then
|
||||||
|
viewOffset = math.max(0, cursorPos - 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.set("viewOffset", viewOffset)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handles character input when editable
|
||||||
|
--- @shortDescription Handles character input
|
||||||
|
--- @param char string The character that was typed
|
||||||
|
function ComboBox:char(char)
|
||||||
|
if not self.get("editable") then return end
|
||||||
|
if not self.get("focused") then return end
|
||||||
|
|
||||||
|
local text = self.get("text")
|
||||||
|
local cursorPos = self.get("cursorPos")
|
||||||
|
|
||||||
|
local newText = text:sub(1, cursorPos - 1) .. char .. text:sub(cursorPos)
|
||||||
|
self.set("text", newText)
|
||||||
|
self.set("cursorPos", cursorPos + 1)
|
||||||
|
self:updateViewport()
|
||||||
|
|
||||||
|
if self.get("autoComplete") then
|
||||||
|
self:updateFilteredDropdown()
|
||||||
|
else
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handles key input when editable
|
||||||
|
--- @shortDescription Handles key input
|
||||||
|
--- @param key number The key code that was pressed
|
||||||
|
--- @param held boolean Whether the key is being held
|
||||||
|
function ComboBox:key(key, held)
|
||||||
|
if not self.get("editable") then return end
|
||||||
|
if not self.get("focused") then return end
|
||||||
|
|
||||||
|
local text = self.get("text")
|
||||||
|
local cursorPos = self.get("cursorPos")
|
||||||
|
|
||||||
|
if key == keys.left then
|
||||||
|
self.set("cursorPos", math.max(1, cursorPos - 1))
|
||||||
|
self:updateViewport()
|
||||||
|
elseif key == keys.right then
|
||||||
|
self.set("cursorPos", math.min(#text + 1, cursorPos + 1))
|
||||||
|
self:updateViewport()
|
||||||
|
elseif key == keys.backspace then
|
||||||
|
if cursorPos > 1 then
|
||||||
|
local newText = text:sub(1, cursorPos - 2) .. text:sub(cursorPos)
|
||||||
|
self.set("text", newText)
|
||||||
|
self.set("cursorPos", cursorPos - 1)
|
||||||
|
self:updateViewport()
|
||||||
|
|
||||||
|
if self.get("autoComplete") then
|
||||||
|
self:updateFilteredDropdown()
|
||||||
|
else
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif key == keys.delete then
|
||||||
|
if cursorPos <= #text then
|
||||||
|
local newText = text:sub(1, cursorPos - 1) .. text:sub(cursorPos + 1)
|
||||||
|
self.set("text", newText)
|
||||||
|
self:updateViewport()
|
||||||
|
|
||||||
|
if self.get("autoComplete") then
|
||||||
|
self:updateFilteredDropdown()
|
||||||
|
else
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif key == keys.home then
|
||||||
|
self.set("cursorPos", 1)
|
||||||
|
self:updateViewport()
|
||||||
|
elseif key == keys["end"] then
|
||||||
|
self.set("cursorPos", #text + 1)
|
||||||
|
self:updateViewport()
|
||||||
|
elseif key == keys.enter then
|
||||||
|
self.set("isOpen", not self.get("isOpen"))
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handles mouse clicks
|
||||||
|
--- @shortDescription Handles mouse clicks
|
||||||
|
--- @param button number The mouse button (1 = left, 2 = right, 3 = middle)
|
||||||
|
--- @param x number The x coordinate of the click
|
||||||
|
--- @param y number The y coordinate of the click
|
||||||
|
--- @return boolean handled Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function ComboBox:mouse_click(button, x, y)
|
||||||
|
if not VisualElement.mouse_click(self, button, x, y) then return false end
|
||||||
|
|
||||||
|
local relX, relY = self:getRelativePosition(x, y)
|
||||||
|
local width = self.get("width")
|
||||||
|
local dropSymbol = self.get("dropSymbol")
|
||||||
|
|
||||||
|
if relY == 1 then
|
||||||
|
if relX >= width - #dropSymbol + 1 and relX <= width then
|
||||||
|
|
||||||
|
local isCurrentlyOpen = self.get("isOpen")
|
||||||
|
self.set("isOpen", not isCurrentlyOpen)
|
||||||
|
|
||||||
|
if self.get("isOpen") then
|
||||||
|
local allItems = self.get("items") or {}
|
||||||
|
local dropdownHeight = self.get("dropdownHeight") or 5
|
||||||
|
local actualHeight = math.min(dropdownHeight, #allItems)
|
||||||
|
self.set("height", 1 + actualHeight)
|
||||||
|
self.set("manuallyOpened", true)
|
||||||
|
else
|
||||||
|
self.set("height", 1)
|
||||||
|
self.set("manuallyOpened", false)
|
||||||
|
end
|
||||||
|
self:updateRender()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
if relX <= width - #dropSymbol and self.get("editable") then
|
||||||
|
local text = self.get("text")
|
||||||
|
local viewOffset = self.get("viewOffset")
|
||||||
|
local maxPos = #text + 1
|
||||||
|
local targetPos = math.min(maxPos, viewOffset + relX)
|
||||||
|
|
||||||
|
self.set("cursorPos", targetPos)
|
||||||
|
self:updateRender()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return true
|
||||||
|
elseif self.get("isOpen") and relY > 1 and self.get("selectable") then
|
||||||
|
local itemIndex = (relY - 1) + self.get("offset")
|
||||||
|
local items = self.get("items")
|
||||||
|
|
||||||
|
if itemIndex <= #items then
|
||||||
|
local item = items[itemIndex]
|
||||||
|
if type(item) == "string" then
|
||||||
|
item = {text = item}
|
||||||
|
items[itemIndex] = item
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.get("multiSelection") then
|
||||||
|
for _, otherItem in ipairs(items) do
|
||||||
|
if type(otherItem) == "table" then
|
||||||
|
otherItem.selected = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
item.selected = true
|
||||||
|
|
||||||
|
if item.text then
|
||||||
|
self:setText(item.text)
|
||||||
|
end
|
||||||
|
self.set("isOpen", false)
|
||||||
|
self.set("height", 1)
|
||||||
|
self:updateRender()
|
||||||
|
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Renders the ComboBox
|
||||||
|
--- @shortDescription Renders the ComboBox
|
||||||
|
function ComboBox:render()
|
||||||
|
VisualElement.render(self)
|
||||||
|
|
||||||
|
local text = self.get("text")
|
||||||
|
local width = self.get("width")
|
||||||
|
local dropSymbol = self.get("dropSymbol")
|
||||||
|
local isFocused = self.get("focused")
|
||||||
|
local isOpen = self.get("isOpen")
|
||||||
|
local viewOffset = self.get("viewOffset")
|
||||||
|
local placeholder = self.get("placeholder")
|
||||||
|
|
||||||
|
local bg = isFocused and self.get("focusedBackground") or self.get("background")
|
||||||
|
local fg = isFocused and self.get("focusedForeground") or self.get("foreground")
|
||||||
|
|
||||||
|
local displayText = text
|
||||||
|
local textWidth = width - #dropSymbol
|
||||||
|
|
||||||
|
if #text == 0 and not isFocused and #placeholder > 0 then
|
||||||
|
displayText = placeholder
|
||||||
|
fg = self.get("placeholderColor")
|
||||||
|
end
|
||||||
|
|
||||||
|
if #displayText > 0 then
|
||||||
|
displayText = displayText:sub(viewOffset + 1, viewOffset + textWidth)
|
||||||
|
end
|
||||||
|
|
||||||
|
displayText = displayText .. string.rep(" ", textWidth - #displayText)
|
||||||
|
|
||||||
|
local fullText = displayText .. (isOpen and "\31" or "\17")
|
||||||
|
|
||||||
|
self:blit(1, 1, fullText,
|
||||||
|
string.rep(tHex[fg], width),
|
||||||
|
string.rep(tHex[bg], width))
|
||||||
|
|
||||||
|
if isFocused and self.get("editable") then
|
||||||
|
local cursorPos = self.get("cursorPos")
|
||||||
|
local cursorX = cursorPos - viewOffset
|
||||||
|
if cursorX >= 1 and cursorX <= textWidth then
|
||||||
|
self:setCursor(cursorX, 1, true, self.get("foreground"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if isOpen then
|
||||||
|
local items
|
||||||
|
if self.get("autoComplete") and not self.get("manuallyOpened") then
|
||||||
|
items = self:getFilteredItems()
|
||||||
|
else
|
||||||
|
items = self.get("items")
|
||||||
|
end
|
||||||
|
|
||||||
|
local dropdownHeight = math.min(self.get("dropdownHeight"), #items)
|
||||||
|
if dropdownHeight > 0 then
|
||||||
|
local offset = self.get("offset")
|
||||||
|
|
||||||
|
for i = 1, dropdownHeight do
|
||||||
|
local itemIndex = i + offset
|
||||||
|
if items[itemIndex] then
|
||||||
|
local item = items[itemIndex]
|
||||||
|
local itemText = item.text or ""
|
||||||
|
local isSelected = item.selected or false
|
||||||
|
|
||||||
|
local itemBg = isSelected and self.get("selectedBackground") or self.get("background")
|
||||||
|
local itemFg = isSelected and self.get("selectedForeground") or self.get("foreground")
|
||||||
|
|
||||||
|
if #itemText > width then
|
||||||
|
itemText = itemText:sub(1, width)
|
||||||
|
end
|
||||||
|
|
||||||
|
itemText = itemText .. string.rep(" ", width - #itemText)
|
||||||
|
self:blit(1, i + 1, itemText,
|
||||||
|
string.rep(tHex[itemFg], width),
|
||||||
|
string.rep(tHex[itemBg], width))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Called when the ComboBox gains focus
|
||||||
|
--- @shortDescription Called when gaining focus
|
||||||
|
function ComboBox:focus()
|
||||||
|
Dropdown.focus(self)
|
||||||
|
-- Additional focus logic for input if needed
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Called when the ComboBox loses focus
|
||||||
|
--- @shortDescription Called when losing focus
|
||||||
|
function ComboBox:blur()
|
||||||
|
Dropdown.blur(self)
|
||||||
|
self.set("isOpen", false)
|
||||||
|
self.set("height", 1)
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
|
||||||
|
return ComboBox
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
local elementManager = require("elementManager")
|
local elementManager = require("elementManager")
|
||||||
local VisualElement = elementManager.getElement("VisualElement")
|
local VisualElement = elementManager.getElement("VisualElement")
|
||||||
---@cofnigDescription The Switch is a standard Switch element with click handling and state management.
|
local tHex = require("libraries/colorHex")
|
||||||
|
---@configDescription The Switch is a standard Switch element with click handling and state management.
|
||||||
|
|
||||||
--- The Switch is a standard Switch element with click handling and state management.
|
--- The Switch is a standard Switch element with click handling and state management.
|
||||||
---@class Switch : VisualElement
|
---@class Switch : VisualElement
|
||||||
@@ -9,6 +10,14 @@ Switch.__index = Switch
|
|||||||
|
|
||||||
---@property checked boolean Whether switch is checked
|
---@property checked boolean Whether switch is checked
|
||||||
Switch.defineProperty(Switch, "checked", {default = false, type = "boolean", canTriggerRender = true})
|
Switch.defineProperty(Switch, "checked", {default = false, type = "boolean", canTriggerRender = true})
|
||||||
|
---@property text string Text to display next to switch
|
||||||
|
Switch.defineProperty(Switch, "text", {default = "", type = "string", canTriggerRender = true})
|
||||||
|
---@property autoSize boolean Whether to automatically size the element to fit switch and text
|
||||||
|
Switch.defineProperty(Switch, "autoSize", {default = false, type = "boolean"})
|
||||||
|
---@property onBackground number Background color when ON
|
||||||
|
Switch.defineProperty(Switch, "onBackground", {default = colors.green, type = "number", canTriggerRender = true})
|
||||||
|
---@property offBackground number Background color when OFF
|
||||||
|
Switch.defineProperty(Switch, "offBackground", {default = colors.red, type = "number", canTriggerRender = true})
|
||||||
|
|
||||||
Switch.defineEvent(Switch, "mouse_click")
|
Switch.defineEvent(Switch, "mouse_click")
|
||||||
Switch.defineEvent(Switch, "mouse_up")
|
Switch.defineEvent(Switch, "mouse_up")
|
||||||
@@ -22,6 +31,7 @@ function Switch.new()
|
|||||||
self.set("width", 2)
|
self.set("width", 2)
|
||||||
self.set("height", 1)
|
self.set("height", 1)
|
||||||
self.set("z", 5)
|
self.set("z", 5)
|
||||||
|
self.set("backgroundEnabled", true)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -34,10 +44,38 @@ function Switch:init(props, basalt)
|
|||||||
self.set("type", "Switch")
|
self.set("type", "Switch")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Handles mouse click events
|
||||||
|
--- @param button number The button that was clicked
|
||||||
|
--- @param x number The x position of the click
|
||||||
|
--- @param y number The y position of the click
|
||||||
|
--- @return boolean Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function Switch:mouse_click(button, x, y)
|
||||||
|
if VisualElement.mouse_click(self, button, x, y) then
|
||||||
|
self.set("checked", not self.get("checked"))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
--- @shortDescription Renders the Switch
|
--- @shortDescription Renders the Switch
|
||||||
--- @protected
|
--- @protected
|
||||||
function Switch:render()
|
function Switch:render()
|
||||||
VisualElement.render(self)
|
local checked = self.get("checked")
|
||||||
|
local text = self.get("text")
|
||||||
|
local switchWidth = self.get("width")
|
||||||
|
local switchHeight = self.get("height")
|
||||||
|
|
||||||
|
local bgColor = checked and self.get("onBackground") or self.get("offBackground")
|
||||||
|
self:multiBlit(1, 1, switchWidth, switchHeight, " ", tHex[self.get("foreground")], tHex[bgColor])
|
||||||
|
|
||||||
|
local sliderSize = math.floor(switchWidth / 2)
|
||||||
|
local sliderStart = checked and (switchWidth - sliderSize + 1) or 1
|
||||||
|
self:multiBlit(sliderStart, 1, sliderSize, switchHeight, " ", tHex[self.get("foreground")], tHex[self.get("background")])
|
||||||
|
|
||||||
|
if text ~= "" then
|
||||||
|
self:textFg(switchWidth + 2, 1, text, self.get("foreground"))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return Switch
|
return Switch
|
||||||
|
|||||||
427
src/elements/TabControl.lua
Normal file
427
src/elements/TabControl.lua
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
local elementManager = require("elementManager")
|
||||||
|
local VisualElement = require("elements/VisualElement")
|
||||||
|
local Container = elementManager.getElement("Container")
|
||||||
|
local tHex = require("libraries/colorHex")
|
||||||
|
local log = require("log")
|
||||||
|
---@configDescription A TabControl element that provides tabbed interface with multiple content areas.
|
||||||
|
|
||||||
|
--- The TabControl is a container that provides tabbed interface functionality
|
||||||
|
---@class TabControl : Container
|
||||||
|
local TabControl = setmetatable({}, Container)
|
||||||
|
TabControl.__index = TabControl
|
||||||
|
|
||||||
|
---@property activeTab number The currently active tab ID
|
||||||
|
TabControl.defineProperty(TabControl, "activeTab", {default = nil, type = "number", allowNil = true, canTriggerRender = true, setter = function(self, value)
|
||||||
|
return value
|
||||||
|
end})
|
||||||
|
---@property tabHeight number Height of the tab header area
|
||||||
|
TabControl.defineProperty(TabControl, "tabHeight", {default = 1, type = "number", canTriggerRender = true})
|
||||||
|
---@property tabs table List of tab definitions
|
||||||
|
TabControl.defineProperty(TabControl, "tabs", {default = {}, type = "table"})
|
||||||
|
|
||||||
|
---@property headerBackground color Background color for the tab header area
|
||||||
|
TabControl.defineProperty(TabControl, "headerBackground", {default = colors.gray, type = "color", canTriggerRender = true})
|
||||||
|
---@property activeTabBackground color Background color for the active tab
|
||||||
|
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})
|
||||||
|
|
||||||
|
TabControl.defineEvent(TabControl, "tabChanged")
|
||||||
|
|
||||||
|
--- @shortDescription Creates a new TabControl instance
|
||||||
|
--- @return TabControl self The created instance
|
||||||
|
--- @private
|
||||||
|
function TabControl.new()
|
||||||
|
local self = setmetatable({}, TabControl):__init()
|
||||||
|
self.class = TabControl
|
||||||
|
self.set("width", 20)
|
||||||
|
self.set("height", 10)
|
||||||
|
self.set("z", 10)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Initializes the TabControl instance
|
||||||
|
--- @param props table The properties to initialize the element with
|
||||||
|
--- @param basalt table The basalt instance
|
||||||
|
--- @protected
|
||||||
|
function TabControl:init(props, basalt)
|
||||||
|
Container.init(self, props, basalt)
|
||||||
|
self.set("type", "TabControl")
|
||||||
|
end
|
||||||
|
|
||||||
|
--- returns a proxy for adding elements to the tab
|
||||||
|
--- @shortDescription Creates a new tab handler proxy
|
||||||
|
--- @param title string The title of the tab
|
||||||
|
--- @return table tabHandler The tab handler proxy for adding elements to the new tab
|
||||||
|
function TabControl:newTab(title)
|
||||||
|
local tabs = self.get("tabs") or {}
|
||||||
|
local tabId = #tabs + 1
|
||||||
|
|
||||||
|
table.insert(tabs, {
|
||||||
|
id = tabId,
|
||||||
|
title = tostring(title or ("Tab " .. tabId))
|
||||||
|
})
|
||||||
|
|
||||||
|
self.set("tabs", tabs)
|
||||||
|
|
||||||
|
if not self.get("activeTab") then
|
||||||
|
self.set("activeTab", tabId)
|
||||||
|
end
|
||||||
|
self:updateTabVisibility()
|
||||||
|
|
||||||
|
local tabControl = self
|
||||||
|
local proxy = {}
|
||||||
|
setmetatable(proxy, {
|
||||||
|
__index = function(_, key)
|
||||||
|
if type(key) == "string" and key:sub(1,3) == "add" and type(tabControl[key]) == "function" then
|
||||||
|
return function(_, ...)
|
||||||
|
local el = tabControl[key](tabControl, ...)
|
||||||
|
if el then
|
||||||
|
el._tabId = tabId
|
||||||
|
tabControl.set("childrenSorted", false)
|
||||||
|
tabControl.set("childrenEventsSorted", false)
|
||||||
|
tabControl:updateRender()
|
||||||
|
end
|
||||||
|
return el
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local v = tabControl[key]
|
||||||
|
if type(v) == "function" then
|
||||||
|
return function(_, ...)
|
||||||
|
return v(tabControl, ...)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return v
|
||||||
|
end
|
||||||
|
})
|
||||||
|
|
||||||
|
return proxy
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Sets an element to belong to a specific tab
|
||||||
|
--- @param element table The element to assign to a tab
|
||||||
|
--- @param tabId number The ID of the tab to assign the element to
|
||||||
|
--- @return TabControl self For method chaining
|
||||||
|
function TabControl:setTab(element, tabId)
|
||||||
|
element._tabId = tabId
|
||||||
|
self:updateTabVisibility()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Adds an element to the TabControl and assigns it to the active tab
|
||||||
|
--- @param elementType string The type of element to add
|
||||||
|
--- @param tabId number Optional tab ID, defaults to active tab
|
||||||
|
--- @return table element The created element
|
||||||
|
function TabControl:addElement(elementType, tabId)
|
||||||
|
local element = Container.addElement(self, elementType)
|
||||||
|
local targetTab = tabId or self.get("activeTab")
|
||||||
|
if targetTab then
|
||||||
|
element._tabId = targetTab
|
||||||
|
self:updateTabVisibility()
|
||||||
|
end
|
||||||
|
return element
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Overrides Container's addChild to assign new elements to tab 1 by default
|
||||||
|
--- @param child table The child element to add
|
||||||
|
--- @return Container self For method chaining
|
||||||
|
--- @protected
|
||||||
|
function TabControl:addChild(child)
|
||||||
|
Container.addChild(self, child)
|
||||||
|
if not child._tabId then
|
||||||
|
local tabs = self.get("tabs") or {}
|
||||||
|
if #tabs > 0 then
|
||||||
|
child._tabId = 1
|
||||||
|
self:updateTabVisibility()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Updates visibility of tab containers
|
||||||
|
--- @private
|
||||||
|
function TabControl:updateTabVisibility()
|
||||||
|
self.set("childrenSorted", false)
|
||||||
|
self.set("childrenEventsSorted", false)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Sets the active tab
|
||||||
|
--- @param tabId number The ID of the tab to activate
|
||||||
|
function TabControl:setActiveTab(tabId)
|
||||||
|
local oldTab = self.get("activeTab")
|
||||||
|
if oldTab == tabId then return self end
|
||||||
|
self.set("activeTab", tabId)
|
||||||
|
self:updateTabVisibility()
|
||||||
|
self:dispatchEvent("tabChanged", tabId, oldTab)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Checks if a child should be visible (overrides Container)
|
||||||
|
--- @param child table The child element to check
|
||||||
|
--- @return boolean Whether the child should be visible
|
||||||
|
--- @protected
|
||||||
|
function TabControl:isChildVisible(child)
|
||||||
|
if not Container.isChildVisible(self, child) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if child._tabId then
|
||||||
|
return child._tabId == self.get("activeTab")
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Gets the content area Y offset (below tab headers)
|
||||||
|
--- @return number yOffset The Y offset for content
|
||||||
|
--- @protected
|
||||||
|
function TabControl:getContentYOffset()
|
||||||
|
local metrics = self:_getHeaderMetrics()
|
||||||
|
return metrics.headerHeight
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:_getHeaderMetrics()
|
||||||
|
local tabs = self.get("tabs") or {}
|
||||||
|
local width = self.get("width") or 1
|
||||||
|
local minTabH = self.get("tabHeight") or 1
|
||||||
|
|
||||||
|
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}
|
||||||
|
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)
|
||||||
|
--- @param y number The y position of the click (global)
|
||||||
|
--- @return boolean Whether the event was handled
|
||||||
|
--- @protected
|
||||||
|
function TabControl:mouse_click(button, x, y)
|
||||||
|
if not VisualElement.mouse_click(self, button, x, y) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local baseRelX, baseRelY = VisualElement.getRelativePosition(self, x, y)
|
||||||
|
local metrics = self:_getHeaderMetrics()
|
||||||
|
if baseRelY <= metrics.headerHeight then
|
||||||
|
if #metrics.positions == 0 then return true end
|
||||||
|
for _, pos in ipairs(metrics.positions) do
|
||||||
|
if pos.line == baseRelY and baseRelX >= pos.x1 and baseRelX <= pos.x2 then
|
||||||
|
self:setActiveTab(pos.id)
|
||||||
|
self.set("focusedChild", nil)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return Container.mouse_click(self, button, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:getRelativePosition(x, y)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
if x == nil or y == nil then
|
||||||
|
return VisualElement.getRelativePosition(self)
|
||||||
|
else
|
||||||
|
local rx, ry = VisualElement.getRelativePosition(self, x, y)
|
||||||
|
return rx, ry - headerH
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:multiBlit(x, y, width, height, text, fg, bg)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
return Container.multiBlit(self, x, (y or 1) + headerH, width, height, text, fg, bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:textFg(x, y, text, fg)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
return Container.textFg(self, x, (y or 1) + headerH, text, fg)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:textBg(x, y, text, bg)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
return Container.textBg(self, x, (y or 1) + headerH, text, bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:drawText(x, y, text)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
return Container.drawText(self, x, (y or 1) + headerH, text)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:drawFg(x, y, fg)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
return Container.drawFg(self, x, (y or 1) + headerH, fg)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:drawBg(x, y, bg)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
return Container.drawBg(self, x, (y or 1) + headerH, bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:blit(x, y, text, fg, bg)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
return Container.blit(self, x, (y or 1) + headerH, text, fg, bg)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:mouse_up(button, x, y)
|
||||||
|
if not VisualElement.mouse_up(self, button, x, y) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local baseRelX, baseRelY = VisualElement.getRelativePosition(self, x, y)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
if baseRelY <= headerH then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return Container.mouse_up(self, button, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:mouse_release(button, x, y)
|
||||||
|
VisualElement.mouse_release(self, button, x, y)
|
||||||
|
local baseRelX, baseRelY = VisualElement.getRelativePosition(self, x, y)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
if baseRelY <= headerH then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return Container.mouse_release(self, button, x, y)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:mouse_move(_, x, y)
|
||||||
|
if VisualElement.mouse_move(self, _, x, y) then
|
||||||
|
local baseRelX, baseRelY = VisualElement.getRelativePosition(self, x, y)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
if baseRelY <= headerH then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local args = {self:getRelativePosition(x, y)}
|
||||||
|
local success, child = self:callChildrenEvent(true, "mouse_move", table.unpack(args))
|
||||||
|
if success then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function TabControl:mouse_drag(button, x, y)
|
||||||
|
if VisualElement.mouse_drag(self, button, x, y) then
|
||||||
|
local baseRelX, baseRelY = VisualElement.getRelativePosition(self, x, y)
|
||||||
|
local headerH = self:_getHeaderMetrics().headerHeight
|
||||||
|
if baseRelY <= headerH then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return Container.mouse_drag(self, button, x, y)
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
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
|
||||||
|
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
|
||||||
|
if self.parent then
|
||||||
|
local xPos, yPos = self:calculatePosition()
|
||||||
|
local targetX = x + xPos - 1
|
||||||
|
local targetY = y + yPos - 1 + tabH
|
||||||
|
|
||||||
|
if(targetX < 1) or (targetX > self.parent.get("width")) or
|
||||||
|
(targetY < 1) or (targetY > self.parent.get("height")) then
|
||||||
|
return self.parent:setCursor(targetX, targetY, false)
|
||||||
|
end
|
||||||
|
return self.parent:setCursor(targetX, targetY, blink, color)
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @shortDescription Renders the TabControl (header + children)
|
||||||
|
--- @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")])
|
||||||
|
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not self.get("childrenSorted") then
|
||||||
|
self:sortChildren()
|
||||||
|
end
|
||||||
|
if not self.get("childrenEventsSorted") then
|
||||||
|
for eventName in pairs(self._values.childrenEvents or {}) do
|
||||||
|
self:sortChildrenEvents(eventName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, child in ipairs(self.get("visibleChildren") or {}) do
|
||||||
|
if child == self then error("CIRCULAR REFERENCE DETECTED!") return end
|
||||||
|
child:render()
|
||||||
|
child:postRender()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @protected
|
||||||
|
function TabControl:sortChildrenEvents(eventName)
|
||||||
|
local childrenEvents = self._values.childrenEvents and self._values.childrenEvents[eventName]
|
||||||
|
if childrenEvents then
|
||||||
|
local visibleChildrenEvents = {}
|
||||||
|
for _, child in ipairs(childrenEvents) do
|
||||||
|
if self:isChildVisible(child) then
|
||||||
|
table.insert(visibleChildrenEvents, child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 2, #visibleChildrenEvents do
|
||||||
|
local current = visibleChildrenEvents[i]
|
||||||
|
local currentZ = current.get("z")
|
||||||
|
local j = i - 1
|
||||||
|
while j > 0 do
|
||||||
|
local compare = visibleChildrenEvents[j].get("z")
|
||||||
|
if compare > currentZ then
|
||||||
|
visibleChildrenEvents[j + 1] = visibleChildrenEvents[j]
|
||||||
|
j = j - 1
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
visibleChildrenEvents[j + 1] = current
|
||||||
|
end
|
||||||
|
|
||||||
|
self._values.visibleChildrenEvents = self._values.visibleChildrenEvents or {}
|
||||||
|
self._values.visibleChildrenEvents[eventName] = visibleChildrenEvents
|
||||||
|
end
|
||||||
|
self.set("childrenEventsSorted", true)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
return TabControl
|
||||||
@@ -57,13 +57,35 @@ end
|
|||||||
--- Adds a new syntax highlighting pattern
|
--- Adds a new syntax highlighting pattern
|
||||||
--- @shortDescription Adds a new syntax highlighting pattern
|
--- @shortDescription Adds a new syntax highlighting pattern
|
||||||
--- @param pattern string The regex pattern to match
|
--- @param pattern string The regex pattern to match
|
||||||
--- @param color colors The color to apply
|
--- @param color number The color to apply
|
||||||
--- @return TextBox self The TextBox instance
|
--- @return TextBox self The TextBox instance
|
||||||
function TextBox:addSyntaxPattern(pattern, color)
|
function TextBox:addSyntaxPattern(pattern, color)
|
||||||
table.insert(self.get("syntaxPatterns"), {pattern = pattern, color = color})
|
table.insert(self.get("syntaxPatterns"), {pattern = pattern, color = color})
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Removes a syntax pattern by index (1-based)
|
||||||
|
--- @param index number The index of the pattern to remove
|
||||||
|
--- @return TextBox self
|
||||||
|
function TextBox:removeSyntaxPattern(index)
|
||||||
|
local patterns = self.get("syntaxPatterns") or {}
|
||||||
|
if type(index) ~= "number" then return self end
|
||||||
|
if index >= 1 and index <= #patterns then
|
||||||
|
table.remove(patterns, index)
|
||||||
|
self.set("syntaxPatterns", patterns)
|
||||||
|
self:updateRender()
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Clears all syntax highlighting patterns
|
||||||
|
--- @return TextBox self
|
||||||
|
function TextBox:clearSyntaxPatterns()
|
||||||
|
self.set("syntaxPatterns", {})
|
||||||
|
self:updateRender()
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
local function insertChar(self, char)
|
local function insertChar(self, char)
|
||||||
local lines = self.get("lines")
|
local lines = self.get("lines")
|
||||||
local cursorX = self.get("cursorX")
|
local cursorX = self.get("cursorX")
|
||||||
@@ -223,12 +245,15 @@ function TextBox:mouse_click(button, x, y)
|
|||||||
local scrollX = self.get("scrollX")
|
local scrollX = self.get("scrollX")
|
||||||
local scrollY = self.get("scrollY")
|
local scrollY = self.get("scrollY")
|
||||||
|
|
||||||
local targetY = relY + scrollY
|
local targetY = (relY or 0) + (scrollY or 0)
|
||||||
local lines = self.get("lines")
|
local lines = self.get("lines") or {}
|
||||||
|
|
||||||
if targetY <= #lines then
|
-- clamp and validate before indexing to avoid nil errors
|
||||||
|
if targetY < 1 then targetY = 1 end
|
||||||
|
if targetY <= #lines and lines[targetY] ~= nil then
|
||||||
self.set("cursorY", targetY)
|
self.set("cursorY", targetY)
|
||||||
self.set("cursorX", math.min(relX + scrollX, #lines[targetY] + 1))
|
local lineLen = #tostring(lines[targetY])
|
||||||
|
self.set("cursorX", math.min((relX or 1) + (scrollX or 0), lineLen + 1))
|
||||||
end
|
end
|
||||||
self:updateRender()
|
self:updateRender()
|
||||||
return true
|
return true
|
||||||
@@ -286,8 +311,15 @@ local function applySyntaxHighlighting(self, line)
|
|||||||
while true do
|
while true do
|
||||||
local s, e = text:find(syntax.pattern, start)
|
local s, e = text:find(syntax.pattern, start)
|
||||||
if not s then break end
|
if not s then break end
|
||||||
colors = colors:sub(1, s-1) .. string.rep(tHex[syntax.color], e-s+1) .. colors:sub(e+1)
|
local matchLen = e - s + 1
|
||||||
start = e + 1
|
if matchLen <= 0 then
|
||||||
|
-- avoid infinite loops for zero-length matches: color one char and advance
|
||||||
|
colors = colors:sub(1, s-1) .. string.rep(tHex[syntax.color], 1) .. colors:sub(s+1)
|
||||||
|
start = s + 1
|
||||||
|
else
|
||||||
|
colors = colors:sub(1, s-1) .. string.rep(tHex[syntax.color], matchLen) .. colors:sub(e+1)
|
||||||
|
start = e + 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -310,13 +342,18 @@ function TextBox:render()
|
|||||||
for y = 1, height do
|
for y = 1, height do
|
||||||
local lineNum = y + scrollY
|
local lineNum = y + scrollY
|
||||||
local line = lines[lineNum] or ""
|
local line = lines[lineNum] or ""
|
||||||
local visibleText = line:sub(scrollX + 1, scrollX + width)
|
|
||||||
if #visibleText < width then
|
local fullText, fullColors = applySyntaxHighlighting(self, line)
|
||||||
visibleText = visibleText .. string.rep(" ", width - #visibleText)
|
local text = fullText:sub(scrollX + 1, scrollX + width)
|
||||||
|
local colors = fullColors:sub(scrollX + 1, scrollX + width)
|
||||||
|
|
||||||
|
local padLen = width - #text
|
||||||
|
if padLen > 0 then
|
||||||
|
text = text .. string.rep(" ", padLen)
|
||||||
|
colors = colors .. string.rep(tHex[self.get("foreground")], padLen)
|
||||||
end
|
end
|
||||||
|
|
||||||
local text, colors = applySyntaxHighlighting(self, visibleText)
|
self:blit(1, y, text, colors, string.rep(bg, #text))
|
||||||
self:blit(1, y, text, colors, string.rep(bg, #visibleText))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if self.get("focused") then
|
if self.get("focused") then
|
||||||
|
|||||||
Reference in New Issue
Block a user