Added TabControl Element
Added ComboBox Element Added/Updated Switch Element Fixed TextBox Syntax Highlighting Bug
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user