Files
LeonOS/data/computercraft/lua/rom/apis/lgui.lua
Leonmmcoset c91c989b57 fix(lgui): 安全处理os.pullEvent调用
添加对os.pullEvent函数的类型检查,防止在函数不可用时抛出错误
2025-09-03 14:41:52 +08:00

668 lines
16 KiB
Lua

-- lgui.lua - Simple GUI library for CC Tweaked
-- Version: 0.1
local lgui = {}
-- Dependencies
local term = require("term")
local window = require("window")
local colors = require("colors")
local keys = require("keys")
-- Constants
lgui.colors = {
BLACK = colors.black,
WHITE = colors.white,
RED = colors.red,
GREEN = colors.green,
BLUE = colors.blue,
YELLOW = colors.yellow,
CYAN = colors.cyan,
MAGENTA = colors.magenta,
GRAY = colors.gray,
LIGHT_GRAY = colors.lightGray,
LIGHT_RED = colors.lightRed,
LIGHT_GREEN = colors.lightGreen,
LIGHT_BLUE = colors.lightBlue,
LIGHT_YELLOW = colors.lightYellow,
LIGHT_CYAN = colors.lightCyan,
LIGHT_MAGENTA = colors.lightMagenta
}
-- Base class for UI elements
local UIElement = {}
UIElement.__index = UIElement
function UIElement:new(x, y, width, height)
local obj = setmetatable({}, self)
obj.x = x
obj.y = y
obj.width = width
obj.height = height
obj.visible = true
obj.enabled = true
obj.bgColor = lgui.colors.BLACK
obj.fgColor = lgui.colors.WHITE
obj.parent = nil
return obj
end
function UIElement:setPosition(x, y)
self.x = x
self.y = y
if self.parent then self.parent:requestRedraw() end
end
function UIElement:setSize(width, height)
self.width = width
self.height = height
if self.parent then self.parent:requestRedraw() end
end
function UIElement:setVisible(visible)
self.visible = visible
if self.parent then self.parent:requestRedraw() end
end
function UIElement:setEnabled(enabled)
self.enabled = enabled
if self.parent then self.parent:requestRedraw() end
end
function UIElement:setColors(fg, bg)
self.fgColor = fg or self.fgColor
self.bgColor = bg or self.bgColor
if self.parent then self.parent:requestRedraw() end
end
function UIElement:containsPoint(x, y)
return x >= self.x and x <= self.x + self.width - 1 and
y >= self.y and y <= self.y + self.height - 1
end
function UIElement:draw()
-- To be overridden by subclasses
end
function UIElement:handleEvent(event, ...)
-- To be overridden by subclasses
return false
end
-- Label class
local Label = setmetatable({}, UIElement)
Label.__index = Label
function Label:new(x, y, text, width)
local obj = UIElement:new(x, y, width or #text, 1)
obj = setmetatable(obj, self)
obj.text = text
obj.align = "left"
return obj
end
function Label:setText(text)
self.text = text
if #text > self.width then
self.width = #text
end
if self.parent then self.parent:requestRedraw() end
end
function Label:setAlignment(align)
self.align = align
if self.parent then self.parent:requestRedraw() end
end
function Label:draw()
if not self.visible then return end
local oldFg = term.getTextColor()
local oldBg = term.getBackgroundColor()
term.setTextColor(self.fgColor)
term.setBackgroundColor(self.bgColor)
local displayText = self.text
if #displayText > self.width then
displayText = displayText:sub(1, self.width)
end
if self.align == "center" then
local padding = math.floor((self.width - #displayText) / 2)
term.setCursorPos(self.x + padding, self.y)
term.write(displayText)
elseif self.align == "right" then
local padding = self.width - #displayText
term.setCursorPos(self.x + padding, self.y)
term.write(displayText)
else -- left
term.setCursorPos(self.x, self.y)
term.write(displayText)
end
term.setTextColor(oldFg)
term.setBackgroundColor(oldBg)
end
-- Button class
local Button = setmetatable({}, UIElement)
Button.__index = Button
function Button:new(x, y, width, height, text)
local obj = UIElement:new(x, y, width, height)
obj = setmetatable(obj, self)
obj.text = text or "Button"
obj.onClick = nil
obj.active = false
obj.borderColor = lgui.colors.GRAY
return obj
end
function Button:setText(text)
self.text = text
if self.parent then self.parent:requestRedraw() end
end
function Button:setOnClick(callback)
self.onClick = callback
end
function Button:draw()
if not self.visible then return end
local oldFg = term.getTextColor()
local oldBg = term.getBackgroundColor()
-- Draw border
term.setTextColor(self.borderColor)
term.setBackgroundColor(self.bgColor)
-- Top border
term.setCursorPos(self.x, self.y)
term.write(" ")
for i = 1, self.width - 2 do
term.write("-")
end
term.write(" ")
-- Middle rows
for y = 1, self.height - 2 do
term.setCursorPos(self.x, self.y + y)
term.write("|")
term.setCursorPos(self.x + self.width - 1, self.y + y)
term.write("|")
end
-- Bottom border
term.setCursorPos(self.x, self.y + self.height - 1)
term.write(" ")
for i = 1, self.width - 2 do
term.write("-")
end
term.write(" ")
-- Draw text
if self.active and self.enabled then
term.setTextColor(self.bgColor)
term.setBackgroundColor(self.fgColor)
else
term.setTextColor(self.fgColor)
term.setBackgroundColor(self.bgColor)
end
local textX = self.x + math.floor((self.width - #self.text) / 2)
local textY = self.y + math.floor(self.height / 2)
term.setCursorPos(textX, textY)
term.write(self.text)
term.setTextColor(oldFg)
term.setBackgroundColor(oldBg)
end
function Button:handleEvent(event, ...)
if not self.enabled or not self.visible then return false end
if event == "mouse_click" then
local button, x, y = ...
if button == 1 and self:containsPoint(x, y) then
self.active = true
if self.parent then self.parent:requestRedraw() end
return true
end
elseif event == "mouse_up" then
local button, x, y = ...
if button == 1 and self.active then
self.active = false
if self.parent then self.parent:requestRedraw() end
if self:containsPoint(x, y) and self.onClick then
self.onClick()
end
return true
end
end
return false
end
-- TextField class
local TextField = setmetatable({}, UIElement)
TextField.__index = TextField
function TextField:new(x, y, width)
local obj = UIElement:new(x, y, width, 1)
obj = setmetatable(obj, self)
obj.text = ""
obj.cursorPos = 0
obj.focused = false
obj.onChange = nil
obj.borderColor = lgui.colors.GRAY
return obj
end
function TextField:setText(text)
self.text = text
self.cursorPos = #text
if self.parent then self.parent:requestRedraw() end
if self.onChange then self.onChange(self.text) end
end
function TextField:getText()
return self.text
end
function TextField:setOnChange(callback)
self.onChange = callback
end
function TextField:focus()
self.focused = true
if self.parent then self.parent:requestRedraw() end
end
function TextField:blur()
self.focused = false
if self.parent then self.parent:requestRedraw() end
end
function TextField:draw()
if not self.visible then return end
local oldFg = term.getTextColor()
local oldBg = term.getBackgroundColor()
-- Draw border
term.setTextColor(self.borderColor)
term.setBackgroundColor(self.bgColor)
term.setCursorPos(self.x, self.y)
term.write("[")
for i = 1, self.width - 2 do
term.write(" ")
end
term.write("]")
-- Draw text
term.setTextColor(self.fgColor)
term.setBackgroundColor(self.bgColor)
local displayText = self.text
if #displayText > self.width - 2 then
-- Scroll text if too long
local start = math.max(1, #displayText - (self.width - 2) + 1)
displayText = displayText:sub(start)
self.cursorPosDisplay = self.cursorPos - start + 1
else
self.cursorPosDisplay = self.cursorPos + 1
end
term.setCursorPos(self.x + 1, self.y)
term.write(displayText)
term.write(string.rep(" ", (self.width - 2) - #displayText))
-- Draw cursor
if self.focused and self.enabled then
term.setCursorPos(self.x + self.cursorPosDisplay, self.y)
-- CC Tweaked doesn't support term.blink, so we'll invert the colors instead
local currentFg = term.getTextColor()
local currentBg = term.getBackgroundColor()
term.setTextColor(currentBg)
term.setBackgroundColor(currentFg)
term.write(" ")
term.setTextColor(currentFg)
term.setBackgroundColor(currentBg)
term.setCursorPos(self.x + self.cursorPosDisplay, self.y)
end
term.setTextColor(oldFg)
term.setBackgroundColor(oldBg)
end
function TextField:handleEvent(event, ...)
if not self.enabled or not self.visible then return false end
if event == "mouse_click" then
local button, x, y = ...
if button == 1 and self:containsPoint(x, y) then
self:focus()
-- Set cursor position based on click
local relX = x - self.x - 1
if relX < 0 then relX = 0 end
if relX > #self.text then relX = #self.text end
self.cursorPos = relX
return true
elseif button == 1 then
self:blur()
end
elseif event == "key" and self.focused then
local key = ...
if key == keys.backspace then
if self.cursorPos > 0 then
self.text = self.text:sub(1, self.cursorPos - 1) .. self.text:sub(self.cursorPos + 1)
self.cursorPos = self.cursorPos - 1
if self.parent then self.parent:requestRedraw() end
if self.onChange then self.onChange(self.text) end
end
return true
elseif key == keys.delete then
if self.cursorPos < #self.text then
self.text = self.text:sub(1, self.cursorPos) .. self.text:sub(self.cursorPos + 2)
if self.parent then self.parent:requestRedraw() end
if self.onChange then self.onChange(self.text) end
end
return true
elseif key == keys.left then
if self.cursorPos > 0 then
self.cursorPos = self.cursorPos - 1
if self.parent then self.parent:requestRedraw() end
end
return true
elseif key == keys.right then
if self.cursorPos < #self.text then
self.cursorPos = self.cursorPos + 1
if self.parent then self.parent:requestRedraw() end
end
return true
elseif key == keys.home then
self.cursorPos = 0
if self.parent then self.parent:requestRedraw() end
return true
elseif key == keys["end"] then
self.cursorPos = #self.text
if self.parent then self.parent:requestRedraw() end
return true
end
elseif event == "char" and self.focused then
local char = ...
self.text = self.text:sub(1, self.cursorPos) .. char .. self.text:sub(self.cursorPos + 1)
self.cursorPos = self.cursorPos + 1
if self.parent then self.parent:requestRedraw() end
if self.onChange then self.onChange(self.text) end
return true
end
return false
end
-- Window class
local Window = setmetatable({}, UIElement)
Window.__index = Window
function Window:new(x, y, width, height, title)
local obj = UIElement:new(x, y, width, height)
obj = setmetatable(obj, self)
obj.title = title or "Window"
obj.children = {}
obj.dragging = false
obj.dragOffsetX = 0
obj.dragOffsetY = 0
obj.needsRedraw = true
obj.borderColor = lgui.colors.GRAY
obj.titleBarColor = lgui.colors.CYAN
obj.titleTextColor = lgui.colors.WHITE
return obj
end
function Window:addChild(child)
table.insert(self.children, child)
child.parent = self
self:requestRedraw()
end
function Window:removeChild(child)
for i, c in ipairs(self.children) do
if c == child then
table.remove(self.children, i)
child.parent = nil
self:requestRedraw()
return true
end
end
return false
end
function Window:requestRedraw()
self.needsRedraw = true
end
function Window:draw()
if not self.visible then return end
if not self.needsRedraw then return end
self.needsRedraw = false
local oldFg = term.getTextColor()
local oldBg = term.getBackgroundColor()
-- Draw border
term.setTextColor(self.borderColor)
term.setBackgroundColor(self.bgColor)
-- Top border with title
term.setCursorPos(self.x, self.y)
term.write(" ")
term.setTextColor(self.titleTextColor)
term.setBackgroundColor(self.titleBarColor)
local title = " " .. self.title .. " "
term.write(title)
term.setTextColor(self.borderColor)
term.setBackgroundColor(self.bgColor)
for i = #title + 1, self.width - 2 do
term.write("-")
end
term.write(" ")
-- Middle rows
for y = 1, self.height - 2 do
term.setCursorPos(self.x, self.y + y)
term.write("|")
term.setCursorPos(self.x + self.width - 1, self.y + y)
term.write("|")
end
-- Bottom border
term.setCursorPos(self.x, self.y + self.height - 1)
term.write(" ")
for i = 1, self.width - 2 do
term.write("-")
end
term.write(" ")
-- Clear content area
term.setBackgroundColor(self.bgColor)
for y = 1, self.height - 2 do
term.setCursorPos(self.x + 1, self.y + y)
term.write(string.rep(" ", self.width - 2))
end
-- Draw children
for _, child in ipairs(self.children) do
child:draw()
end
term.setTextColor(oldFg)
term.setBackgroundColor(oldBg)
end
function Window:handleEvent(event, ...)
if not self.visible then return false end
-- Handle window events first
if event == "mouse_click" then
local button, x, y = ...
if button == 1 and y == self.y and x >= self.x and x <= self.x + self.width - 1 then
-- Start dragging
self.dragging = true
self.dragOffsetX = x - self.x
self.dragOffsetY = y - self.y
return true
end
elseif event == "mouse_drag" then
local button, x, y = ...
if button == 1 and self.dragging then
-- Drag window
self:setPosition(x - self.dragOffsetX, y - self.dragOffsetY)
return true
end
elseif event == "mouse_up" then
local button, x, y = ...
if button == 1 and self.dragging then
-- Stop dragging
self.dragging = false
return true
end
end
-- Handle child events (reverse order so topmost gets events first)
for i = #self.children, 1, -1 do
local child = self.children[i]
if child:handleEvent(event, ...) then
return true
end
end
return false
end
function Window:show()
self:setVisible(true)
self:requestRedraw()
self:draw()
end
function Window:hide()
self:setVisible(false)
end
-- Main GUI manager
local GUIManager = {}
GUIManager.__index = GUIManager
function GUIManager:new()
local obj = setmetatable({}, self)
obj.windows = {}
obj.running = false
return obj
end
function GUIManager:addWindow(window)
table.insert(self.windows, window)
window.parent = self
end
function GUIManager:removeWindow(window)
for i, w in ipairs(self.windows) do
if w == window then
table.remove(self.windows, i)
window.parent = nil
return true
end
end
return false
end
function GUIManager:requestRedraw()
for _, window in ipairs(self.windows) do
window:requestRedraw()
end
end
function GUIManager:draw()
term.clear()
for _, window in ipairs(self.windows) do
window:draw()
end
end
function GUIManager:handleEvents()
while self.running do
-- Safely get the pullEvent function
local pullEventFunc = os.pullEvent
if type(pullEventFunc) ~= "function" then
error("os.pullEvent is not a function")
end
-- Get the next event
local event = {pullEventFunc()}
local eventName = event[1]
local handled = false
-- Handle events for all windows (reverse order so topmost gets events first)
for i = #self.windows, 1, -1 do
local window = self.windows[i]
if window:handleEvent(unpack(event)) then
handled = true
break
end
end
-- Redraw if needed
local needsRedraw = false
for _, window in ipairs(self.windows) do
if window.needsRedraw then
needsRedraw = true
break
end
end
if needsRedraw then
self:draw()
end
end
end
function GUIManager:start()
self.running = true
self:draw()
self:handleEvents()
end
function GUIManager:stop()
self.running = false
term.blink(false)
end
-- Export classes
lgui.UIElement = UIElement
lgui.Label = Label
lgui.Button = Button
lgui.TextField = TextField
lgui.Window = Window
lgui.GUIManager = GUIManager
-- Create a default manager
local defaultManager = GUIManager:new()
lgui.manager = defaultManager
-- Helper functions
function lgui.createWindow(x, y, width, height, title)
local win = Window:new(x, y, width, height, title)
defaultManager:addWindow(win)
return win
end
function lgui.start()
defaultManager:start()
end
function lgui.stop()
defaultManager:stop()
end
return lgui