require("advancedLua") local computer = require("computer") local keyboard = require("keyboard") local fs = require("filesystem") local unicode = require("unicode") local event = require("event") local color = require("color") local image = require("image") local buffer = require("doubleBuffering") ----------------------------------------------------------------------------------------------------- local GUI = {} GUI.alignment = { horizontal = enum( "left", "center", "right" ), vertical = enum( "top", "center", "bottom" ) } GUI.directions = enum( "horizontal", "vertical" ) GUI.sizePolicies = enum( "percentage", "absolute" ) GUI.dropDownMenuElementTypes = enum( "default", "separator" ) GUI.filesystemModes = enum( "file", "directory", "both", "open", "save" ) GUI.colors = { disabled = { background = 0x888888, text = 0xAAAAAA }, contextMenu = { separator = 0x888888, default = { background = 0xFFFFFF, text = 0x2D2D2D }, disabled = 0x888888, pressed = { background = 0x3366CC, text = 0xFFFFFF }, transparency = { background = 0.24, shadow = 0.4 } }, windows = { title = { background = 0xE1E1E1, text = 0x2D2D2D }, backgroundPanel = 0xF0F0F0, tabBar = { default = { background = 0x2D2D2D, text = 0xF0F0F0 }, selected = { background = 0xF0F0F0, text = 0x2D2D2D } } } } ----------------------------------------- Interface objects ----------------------------------------- local function callMethod(method, ...) if method then method(...) end end local function objectIsClicked(object, x, y) return x >= object.x and y >= object.y and x <= object.x + object.width - 1 and y <= object.y + object.height - 1 and not object.disabled and not object.hidden end local function objectDraw(object) return object end function GUI.object(x, y, width, height) return { x = x, y = y, width = width, height = height, isClicked = objectIsClicked, draw = objectDraw } end ----------------------------------------- Object alignment ----------------------------------------- function GUI.setAlignment(object, horizontalAlignment, verticalAlignment) object.alignment = { horizontal = horizontalAlignment, vertical = verticalAlignment } return object end function GUI.getAlignmentCoordinates(object, subObject) local x, y if object.alignment.horizontal == GUI.alignment.horizontal.left then x = object.x elseif object.alignment.horizontal == GUI.alignment.horizontal.center then x = math.floor(object.x + object.width / 2 - subObject.width / 2) elseif object.alignment.horizontal == GUI.alignment.horizontal.right then x = object.x + object.width - subObject.width else error("Unknown horizontal alignment: " .. tostring(object.alignment.horizontal)) end if object.alignment.vertical == GUI.alignment.vertical.top then y = object.y elseif object.alignment.vertical == GUI.alignment.vertical.center then y = math.floor(object.y + object.height / 2 - subObject.height / 2) elseif object.alignment.vertical == GUI.alignment.vertical.bottom then y = object.y + object.height - subObject.height else error("Unknown vertical alignment: " .. tostring(object.alignment.vertical)) end return x, y end function GUI.getMarginCoordinates(object) local x, y = object.x, object.y if object.alignment.horizontal == GUI.alignment.horizontal.left then x = x + object.margin.horizontal elseif object.alignment.horizontal == GUI.alignment.horizontal.right then x = x - object.margin.horizontal end if object.alignment.vertical == GUI.alignment.vertical.top then y = y + object.margin.vertical elseif object.alignment.vertical == GUI.alignment.vertical.bottom then y = y - object.margin.vertical end return x, y end ----------------------------------------- Containers ----------------------------------------- local function containerObjectIndexOf(object) if not object.parent then error("Object doesn't have a parent container") end for objectIndex = 1, #object.parent.children do if object.parent.children[objectIndex] == object then return objectIndex end end end local function containerObjectMoveForward(object) local objectIndex = object:indexOf() if objectIndex < #object.parent.children then object.parent.children[index], object.parent.children[index + 1] = object.parent.children[index + 1], object.parent.children[index] end return object end local function containerObjectMoveBackward(object) local objectIndex = object:indexOf() if objectIndex > 1 then object.parent.children[objectIndex], object.parent.children[objectIndex - 1] = object.parent.children[objectIndex - 1], object.parent.children[objectIndex] end return object end local function containerObjectMoveToFront(object) local objectIndex = object:indexOf() table.insert(object.parent.children, object) table.remove(object.parent.children, objectIndex) return object end local function containerObjectMoveToBack(object) local objectIndex = object:indexOf() table.insert(object.parent.children, 1, object) table.remove(object.parent.children, objectIndex + 1) return object end local function containerObjectGetFirstParent(object) local currentParent = object.parent while currentParent.parent do currentParent = currentParent.parent end return currentParent end local function containerObjectSelfDelete(object) table.remove(object.parent.children, containerObjectIndexOf(object)) end ----------------------------------------------------------------------------------------------------- local function containerObjectAnimationStart(animation, duration) animation.position = 0 animation.duration = duration animation.started = true animation.startUptime = computer.uptime() computer.pushSignal("GUI", "animationStarted") end local function containerObjectAnimationStop(animation) animation.position = 0 animation.started = false end local function containerObjectAnimationDelete(animation) animation.deleteLater = true end local function containerObjectAddAnimation(object, frameHandler, onFinish) local firstParent = object:getFirstParent() firstParent.animations = firstParent.animations or {} table.insert(firstParent.animations, { object = object, position = 0, start = containerObjectAnimationStart, stop = containerObjectAnimationStop, delete = containerObjectAnimationDelete, frameHandler = frameHandler, onFinish = onFinish, }) return firstParent.animations[#firstParent.animations] end function GUI.addChildToContainer(container, object, atIndex) object.localX = object.x object.localY = object.y object.indexOf = containerObjectIndexOf object.moveToFront = containerObjectMoveToFront object.moveToBack = containerObjectMoveToBack object.moveForward = containerObjectMoveForward object.moveBackward = containerObjectMoveBackward object.getFirstParent = containerObjectGetFirstParent object.delete = containerObjectSelfDelete object.parent = container object.addAnimation = containerObjectAddAnimation if atIndex then table.insert(container.children, atIndex, object) else table.insert(container.children, object) end return object end local function deleteContainersContent(container, from, to) from = from or 1 for objectIndex = from, to or #container.children do table.remove(container.children, from) end end local function getRectangleIntersection(R1X1, R1Y1, R1X2, R1Y2, R2X1, R2Y1, R2X2, R2Y2) if R2X1 <= R1X2 and R2Y1 <= R2Y2 and R2X2 >= R1X1 and R2Y2 >= R1Y1 then return math.max(R2X1, R1X1), math.max(R2Y1, R1Y1), math.min(R2X2, R1X2), math.min(R2Y2, R1Y2) else return end end function GUI.drawContainerContent(container) local R1X1, R1Y1, R1X2, R1Y2 = buffer.getDrawLimit() local intersectionX1, intersectionY1, intersectionX2, intersectionY2 = getRectangleIntersection( R1X1, R1Y1, R1X2, R1Y2, container.x, container.y, container.x + container.width - 1, container.y + container.height - 1 ) if intersectionX1 then buffer.setDrawLimit(intersectionX1, intersectionY1, intersectionX2, intersectionY2) for objectIndex = 1, #container.children do if not container.children[objectIndex].hidden then container.children[objectIndex].x, container.children[objectIndex].y = container.x + container.children[objectIndex].localX - 1, container.y + container.children[objectIndex].localY - 1 container.children[objectIndex]:draw() end end buffer.setDrawLimit(R1X1, R1Y1, R1X2, R1Y2) end return container end local function containerHandler(isScreenEvent, mainContainer, currentContainer, eventData, intersectionX1, intersectionY1, intersectionX2, intersectionY2) local breakRecursion = false if not isScreenEvent or intersectionX1 and eventData[3] >= intersectionX1 and eventData[4] >= intersectionY1 and eventData[3] <= intersectionX2 and eventData[4] <= intersectionY2 then for i = #currentContainer.children, 1, -1 do if not currentContainer.children[i].hidden then if currentContainer.children[i].children then local newIntersectionX1, newIntersectionY1, newIntersectionX2, newIntersectionY2 = getRectangleIntersection( intersectionX1, intersectionY1, intersectionX2, intersectionY2, currentContainer.children[i].x, currentContainer.children[i].y, currentContainer.children[i].x + currentContainer.children[i].width - 1, currentContainer.children[i].y + currentContainer.children[i].height - 1 ) if newIntersectionX1 then if containerHandler(isScreenEvent, mainContainer, currentContainer.children[i], eventData, newIntersectionX1, newIntersectionY1, newIntersectionX2, newIntersectionY2) then breakRecursion = true break end end else if isScreenEvent then if currentContainer.children[i]:isClicked(eventData[3], eventData[4]) then callMethod(currentContainer.children[i].eventHandler, mainContainer, currentContainer.children[i], eventData) breakRecursion = true break end else callMethod(currentContainer.children[i].eventHandler, mainContainer, currentContainer.children[i], eventData) end end end end callMethod(currentContainer.eventHandler, mainContainer, currentContainer, eventData) end if breakRecursion then return true end end local function containerStartEventHandling(container, eventHandlingDelay) container.eventHandlingDelay = eventHandlingDelay local eventData, animationIndex, animationNeedDraw while true do eventData = {event.pull(container.animations and 0 or container.eventHandlingDelay)} containerHandler( ( eventData[1] == "touch" or eventData[1] == "drag" or eventData[1] == "drop" or eventData[1] == "scroll" or eventData[1] == "double_touch" ), container, container, eventData, container.x, container.y, container.x + container.width - 1, container.y + container.height - 1 ) if container.animations then animationIndex = 1 while animationIndex <= #container.animations do if container.animations[animationIndex].started then animationNeedDraw = true container.animations[animationIndex].position = (computer.uptime() - container.animations[animationIndex].startUptime) / container.animations[animationIndex].duration if container.animations[animationIndex].position <= 1 then container.animations[animationIndex].frameHandler(container, container.animations[animationIndex].object, container.animations[animationIndex]) else container.animations[animationIndex].position = 1 container.animations[animationIndex].started = false container.animations[animationIndex].frameHandler(container, container.animations[animationIndex].object, container.animations[animationIndex]) if container.animations[animationIndex].onFinish then animationNeedDraw = false container:draw() buffer.draw() container.animations[animationIndex].onFinish(container, container.animations[animationIndex].object, container.animations[animationIndex]) end if container.animations[animationIndex].deleteLater then table.remove(container.animations, animationIndex) animationIndex = animationIndex - 1 end end end animationIndex = animationIndex + 1 end if animationNeedDraw then container:draw() buffer.draw() end if #container.animations == 0 then container.animations = nil end end if container.dataToReturn then local dataToReturn = container.dataToReturn container.dataToReturn = nil return table.unpack(dataToReturn) end end end local function containerReturnData(container, ...) container.dataToReturn = {...} end local function containerStopEventHandling(container) containerReturnData(container, nil) end function GUI.container(x, y, width, height) local container = GUI.object(x, y, width, height) container.children = {} container.draw = GUI.drawContainerContent container.deleteChildren = deleteContainersContent container.addChild = GUI.addChildToContainer container.returnData = containerReturnData container.startEventHandling = containerStartEventHandling container.stopEventHandling = containerStopEventHandling return container end function GUI.fullScreenContainer() return GUI.container(1, 1, buffer.getResolution()) end ----------------------------------------- Buttons ----------------------------------------- local function buttonDraw(object) local xText, yText = GUI.getAlignmentCoordinates(object, {width = unicode.len(object.text), height = 1}) local buttonColor, textColor = object.colors.default.background, object.colors.default.text if object.disabled then buttonColor, textColor = object.colors.disabled.background, object.colors.disabled.text else if object.pressed then buttonColor, textColor = object.colors.pressed.background, object.colors.pressed.text end end if buttonColor then if object.buttonType == 1 then buffer.square(object.x, object.y, object.width, object.height, buttonColor, textColor, " ") elseif object.buttonType == 2 then local x2, y2 = object.x + object.width - 1, object.y + object.height - 1 if object.height > 1 then buffer.text(object.x + 1, object.y, buttonColor, string.rep("▄", object.width - 2)) buffer.text(object.x, object.y, buttonColor, "⣠") buffer.text(x2, object.y, buttonColor, "⣄") buffer.square(object.x, object.y + 1, object.width, object.height - 2, buttonColor, textColor, " ") buffer.text(object.x + 1, y2, buttonColor, string.rep("▀", object.width - 2)) buffer.text(object.x, y2, buttonColor, "⠙") buffer.text(x2, y2, buttonColor, "⠋") else buffer.square(object.x, object.y, object.width, object.height, buttonColor, textColor, " ") GUI.roundedCorners(object.x, object.y, object.width, object.height, buttonColor) end else buffer.frame(object.x, object.y, object.width, object.height, buttonColor) end end buffer.text(xText, yText, textColor, object.text) return object end local function buttonPress(object) object.pressed = true buttonDraw(object) end local function buttonRelease(object) object.pressed = nil buttonDraw(object) end local function buttonPressAndRelease(object, pressTime) buttonPress(object) buffer.draw() os.sleep(pressTime or 0.2) buttonRelease(object) buffer.draw() end local function buttonEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then if object.switchMode then object.pressed = not object.pressed mainContainer:draw() buffer.draw() callMethod(object.onTouch, mainContainer, object, eventData) else object.pressed = true mainContainer:draw() buffer.draw() os.sleep(0.2) object.pressed = false mainContainer:draw() buffer.draw() callMethod(object.onTouch, mainContainer, object, eventData) end end end local function buttonCreate(buttonType, x, y, width, height, buttonColor, textColor, buttonPressedColor, textPressedColor, text, disabledState) local object = GUI.object(x, y, width, height) object.colors = { default = { background = buttonColor, text = textColor }, pressed = { background = buttonPressedColor, text = textPressedColor }, disabled = { background = GUI.colors.disabled.background, text = GUI.colors.disabled.text, } } object.eventHandler = buttonEventHandler object.buttonType = buttonType object.disabled = disabledState object.text = text object.press = buttonPress object.release = buttonRelease object.pressAndRelease = buttonPressAndRelease object.draw = buttonDraw object.setAlignment = GUI.setAlignment object:setAlignment(GUI.alignment.horizontal.center, GUI.alignment.vertical.center) return object end function GUI.button(...) return buttonCreate(1, ...) end function GUI.adaptiveButton(x, y, xOffset, yOffset, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...) return buttonCreate(1, x, y, unicode.len(text) + xOffset * 2, yOffset * 2 + 1, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...) end function GUI.roundedButton(...) return buttonCreate(2, ...) end function GUI.adaptiveRoundedButton(x, y, xOffset, yOffset, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...) return buttonCreate(2, x, y, unicode.len(text) + xOffset * 2, yOffset * 2 + 1, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...) end function GUI.framedButton(...) return buttonCreate(3, ...) end function GUI.adaptiveFramedButton(x, y, xOffset, yOffset, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...) return buttonCreate(3, x, y, unicode.len(text) + xOffset * 2, yOffset * 2 + 1, buttonColor, textColor, buttonPressedColor, textPressedColor, text, ...) end ----------------------------------------- TabBar ----------------------------------------- local function tabBarTabEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then object.parent.selectedItem = object:indexOf() - 1 mainContainer:draw() buffer.draw() callMethod(object.onTouch, mainContainer, object, eventData) end end local function tabBarDraw(tabBar) tabBar.backgroundPanel.width, tabBar.backgroundPanel.height, tabBar.backgroundPanel.colors.background = tabBar.width, tabBar.height, tabBar.colors.default.background local totalWidth = 0 for i = 2, #tabBar.children do totalWidth = totalWidth + tabBar.children[i].width + tabBar.spaceBetweenTabs end totalWidth = totalWidth - tabBar.spaceBetweenTabs local x = math.floor(tabBar.width / 2 - totalWidth / 2) for i = 2, #tabBar.children do tabBar.children[i].localX = x x = x + tabBar.children[i].width + tabBar.spaceBetweenTabs tabBar.children[i].pressed = (i - 1) == tabBar.selectedItem end GUI.drawContainerContent(tabBar) return tabBar end local function tabBarAddItem(tabBar, text) local item = tabBar:addChild(GUI.button(1, 1, unicode.len(text) + tabBar.horizontalTabOffset * 2, tabBar.height, tabBar.colors.default.background, tabBar.colors.default.text, tabBar.colors.selected.background, tabBar.colors.selected.text, text)) item.switchMode = true item.eventHandler = tabBarTabEventHandler return item end function GUI.tabBar(x, y, width, height, horizontalTabOffset, spaceBetweenTabs, backgroundColor, textColor, backgroundSelectedColor, textSelectedColor, ...) local tabBar = GUI.container(x, y, width, height) tabBar.backgroundPanel = tabBar:addChild(GUI.panel(1, 1, 1, 1, backgroundColor)) tabBar.horizontalTabOffset = horizontalTabOffset tabBar.spaceBetweenTabs = spaceBetweenTabs tabBar.colors = { default = { background = backgroundColor, text = textColor }, selected = { background = backgroundSelectedColor, text = textSelectedColor } } tabBar.selectedItem = 1 tabBar.draw = tabBarDraw tabBar.addItem = tabBarAddItem local items = {...} for i = 1, #items do tabBar:addItem(items[i]) end return tabBar end ----------------------------------------- Panel ----------------------------------------- local function drawPanel(object) buffer.square(object.x, object.y, object.width, object.height, object.colors.background, 0x000000, " ", object.colors.transparency) return object end function GUI.panel(x, y, width, height, color, transparency) local object = GUI.object(x, y, width, height) object.colors = { background = color, transparency = transparency } object.draw = drawPanel return object end ----------------------------------------- Label ----------------------------------------- local function drawLabel(object) local xText, yText = GUI.getAlignmentCoordinates(object, {width = unicode.len(object.text), height = 1}) buffer.text(xText, yText, object.colors.text, object.text) return object end function GUI.label(x, y, width, height, textColor, text) local object = GUI.object(x, y, width, height) object.setAlignment = GUI.setAlignment object:setAlignment(GUI.alignment.horizontal.left, GUI.alignment.vertical.top) object.colors = {text = textColor} object.text = text object.draw = drawLabel return object end ----------------------------------------- Image ----------------------------------------- local function drawImage(object) buffer.image(object.x, object.y, object.image) return object end function GUI.image(x, y, image) local object = GUI.object(x, y, image[1], image[2]) object.image = image object.draw = drawImage return object end ----------------------------------------- Action buttons ----------------------------------------- function GUI.actionButtons(x, y, fatSymbol) local symbol = fatSymbol and "⬤" or "●" local container = GUI.container(x, y, 5, 1) container.close = container:addChild(GUI.button(1, 1, 1, 1, nil, 0xFF4940, nil, 0x992400, symbol)) container.minimize = container:addChild(GUI.button(3, 1, 1, 1, nil, 0xFFB640, nil, 0x996D00, symbol)) container.maximize = container:addChild(GUI.button(5, 1, 1, 1, nil, 0x00B640, nil, 0x006D40, symbol)) return container end ----------------------------------------- Menu ----------------------------------------- local function menuDraw(menu) buffer.square(menu.x, menu.y, menu.width, 1, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency) menu:reimplementedDraw() end local function menuItemEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then if object.onTouch then object.pressed = true mainContainer:draw() buffer.draw() object.onTouch(eventData) object.pressed = false mainContainer:draw() buffer.draw() end end end local function menuAddItem(menu, text, textColor) local x = 2; for i = 1, #menu.children do x = x + unicode.len(menu.children[i].text) + 2; end local item = menu:addChild(GUI.adaptiveButton(x, 1, 1, 0, nil, textColor or menu.colors.default.text, menu.colors.pressed.background, menu.colors.pressed.text, text)) item.eventHandler = menuItemEventHandler return item end function GUI.menu(x, y, width, backgroundColor, textColor, backgroundPressedColor, textPressedColor, backgroundTransparency) local menu = GUI.container(x, y, width, 1) menu.colors = { default = { background = backgroundColor, text = textColor, }, pressed = { background = backgroundPressedColor, text = textPressedColor, }, transparency = backgroundTransparency } menu.addItem = menuAddItem menu.reimplementedDraw = menu.draw menu.draw = menuDraw return menu end ----------------------------------------- ProgressBar Object ----------------------------------------- local function drawProgressBar(object) local activeWidth = math.floor(object.value * object.width / 100) if object.thin then buffer.text(object.x, object.y, object.colors.passive, string.rep("━", object.width)) buffer.text(object.x, object.y, object.colors.active, string.rep("━", activeWidth)) else buffer.square(object.x, object.y, object.width, object.height, object.colors.passive, 0x0, " ") buffer.square(object.x, object.y, activeWidth, object.height, object.colors.active, 0x0, " ") end if object.showValue then local stringValue = tostring((object.valuePrefix or "") .. object.value .. (object.valuePostfix or "")) buffer.text(math.floor(object.x + object.width / 2 - unicode.len(stringValue) / 2), object.y + 1, object.colors.value, stringValue) end return object end function GUI.progressBar(x, y, width, activeColor, passiveColor, valueColor, value, thin, showValue, valuePrefix, valuePostfix) local object = GUI.object(x, y, width, 1) object.value = value object.colors = {active = activeColor, passive = passiveColor, value = valueColor} object.thin = thin object.draw = drawProgressBar object.showValue = showValue object.valuePrefix = valuePrefix object.valuePostfix = valuePostfix return object end ----------------------------------------- Other GUI elements ----------------------------------------- function GUI.windowShadow(x, y, width, height, transparency, thin) transparency = transparency if thin then buffer.square(x + width, y + 1, 1, height - 1, 0x000000, 0x000000, " ", transparency) buffer.text(x + 1, y + height, 0x000000, string.rep("▀", width), transparency) buffer.text(x + width, y, 0x000000, "▄", transparency) else buffer.square(x + width, y + 1, 2, height, 0x000000, 0x000000, " ", transparency) buffer.square(x + 2, y + height, width - 2, 1, 0x000000, 0x000000, " ", transparency) end end function GUI.roundedCorners(x, y, width, height, color, transparency) buffer.text(x - 1, y, color, "⠰", transparency) buffer.text(x + width, y, color, "⠆", transparency) end ------------------------------------------------- Error window ------------------------------------------------------------------- function GUI.error(...) local args = {...} for i = 1, #args do if type(args[i]) == "table" then args[i] = table.toString(args[i], true) else args[i] = tostring(args[i]) end end if #args == 0 then args[1] = "nil" end local sign = image.fromString([[06030000FF 0000FF 00F7FF▟00F7FF▙0000FF 0000FF 0000FF 00F7FF▟F7FF00 F7FF00 00F7FF▙0000FF 00F7FF▟F7FF00CF7FF00yF7FF00kF7FF00a00F7FF▙]]) local offset = 2 local lines = #args > 1 and "\"" .. table.concat(args, "\", \"") .. "\"" or args[1] local bufferWidth, bufferHeight = buffer.getResolution() local width = math.floor(bufferWidth * 0.5) local textWidth = width - image.getWidth(sign) - 2 lines = string.wrap(lines, textWidth) local height = image.getHeight(sign) if #lines + 2 > height then height = #lines + 2 end local mainContainer = GUI.container(1, math.floor(bufferHeight / 2 - height / 2), bufferWidth, height + offset * 2) local oldPixels = buffer.copy(mainContainer.x, mainContainer.y, mainContainer.width, mainContainer.height) local x, y = math.floor(bufferWidth / 2 - width / 2), offset + 1 mainContainer:addChild(GUI.panel(1, 1, mainContainer.width, mainContainer.height, 0x1D1D1D)) mainContainer:addChild(GUI.image(x, y, sign)) mainContainer:addChild(GUI.textBox(x + image.getWidth(sign) + 2, y, textWidth, #lines, 0x1D1D1D, 0xE1E1E1, lines, 1, 0, 0)).eventHandler = nil local buttonWidth = 10 local button = mainContainer:addChild(GUI.roundedButton(x + image.getWidth(sign) + textWidth - buttonWidth + 2, mainContainer.height - offset, buttonWidth, 1, 0x3366CC, 0xE1E1E1, 0xE1E1E1, 0x3366CC, "OK")) button.onTouch = function() mainContainer:stopEventHandling() buffer.paste(mainContainer.x, mainContainer.y, oldPixels) buffer.draw() end mainContainer.eventHandler = function(mainContainer, object, eventData) if eventData[1] == "key_down" and eventData[4] == 28 then button:pressAndRelease() button.onTouch() end end mainContainer:draw() buffer.draw(true) mainContainer:startEventHandling() end ----------------------------------------- CodeView object ----------------------------------------- local function codeViewDraw(codeView) local syntax = require("syntax") local toLine = codeView.fromLine + codeView.height - 1 -- Line numbers bar and code area codeView.lineNumbersWidth = unicode.len(tostring(toLine)) + 2 codeView.codeAreaPosition = codeView.x + codeView.lineNumbersWidth codeView.codeAreaWidth = codeView.width - codeView.lineNumbersWidth buffer.square(codeView.x, codeView.y, codeView.lineNumbersWidth, codeView.height, syntax.colorScheme.lineNumbersBackground, syntax.colorScheme.lineNumbersText, " ") buffer.square(codeView.codeAreaPosition, codeView.y, codeView.codeAreaWidth, codeView.height, syntax.colorScheme.background, syntax.colorScheme.text, " ") -- Line numbers texts local y = codeView.y for line = codeView.fromLine, toLine do if codeView.lines[line] then local text = tostring(line) if codeView.highlights[line] then buffer.square(codeView.x, y, codeView.lineNumbersWidth, 1, codeView.highlights[line], syntax.colorScheme.text, " ", 0.3) buffer.square(codeView.codeAreaPosition, y, codeView.codeAreaWidth, 1, codeView.highlights[line], syntax.colorScheme.text, " ") end buffer.text(codeView.codeAreaPosition - unicode.len(text) - 1, y, syntax.colorScheme.lineNumbersText, text) y = y + 1 else break end end local oldDrawLimitX1, oldDrawLimitY1, oldDrawLimitX2, oldDrawLimitY2 = buffer.getDrawLimit() buffer.setDrawLimit(codeView.codeAreaPosition, codeView.y, codeView.codeAreaPosition + codeView.codeAreaWidth - 1, codeView.y + codeView.height - 1) local function drawUpperSelection(y, selectionIndex) buffer.square( codeView.codeAreaPosition + codeView.selections[selectionIndex].from.symbol - codeView.fromSymbol + 1, y + codeView.selections[selectionIndex].from.line - codeView.fromLine, codeView.codeAreaWidth - codeView.selections[selectionIndex].from.symbol + codeView.fromSymbol - 1, 1, codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " " ) end local function drawLowerSelection(y, selectionIndex) buffer.square( codeView.codeAreaPosition, y + codeView.selections[selectionIndex].from.line - codeView.fromLine, codeView.selections[selectionIndex].to.symbol - codeView.fromSymbol + 2, 1, codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " " ) end if #codeView.selections > 0 then for selectionIndex = 1, #codeView.selections do y = codeView.y local dy = codeView.selections[selectionIndex].to.line - codeView.selections[selectionIndex].from.line if dy == 0 then buffer.square( codeView.codeAreaPosition + codeView.selections[selectionIndex].from.symbol - codeView.fromSymbol + 1, y + codeView.selections[selectionIndex].from.line - codeView.fromLine, codeView.selections[selectionIndex].to.symbol - codeView.selections[selectionIndex].from.symbol + 1, 1, codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " " ) elseif dy == 1 then drawUpperSelection(y, selectionIndex); y = y + 1 drawLowerSelection(y, selectionIndex) else drawUpperSelection(y, selectionIndex); y = y + 1 for i = 1, dy - 1 do buffer.square(codeView.codeAreaPosition, y + codeView.selections[selectionIndex].from.line - codeView.fromLine, codeView.codeAreaWidth, 1, codeView.selections[selectionIndex].color or syntax.colorScheme.selection, syntax.colorScheme.text, " "); y = y + 1 end drawLowerSelection(y, selectionIndex) end end end -- Code strings y = codeView.y buffer.setDrawLimit(codeView.codeAreaPosition + 1, y, codeView.codeAreaPosition + codeView.codeAreaWidth - 2, y + codeView.height - 1) for i = codeView.fromLine, toLine do if codeView.lines[i] then if codeView.highlightLuaSyntax then syntax.highlightString(codeView.codeAreaPosition - codeView.fromSymbol + 2, y, codeView.lines[i], codeView.indentationWidth) else buffer.text(codeView.codeAreaPosition - codeView.fromSymbol + 2, y, syntax.colorScheme.text, codeView.lines[i]) end y = y + 1 else break end end buffer.setDrawLimit(oldDrawLimitX1, oldDrawLimitY1, oldDrawLimitX2, oldDrawLimitY2) if #codeView.lines > codeView.height then codeView.scrollBars.vertical.hidden = false codeView.scrollBars.vertical.colors.background, codeView.scrollBars.vertical.colors.foreground = syntax.colorScheme.scrollBarBackground, syntax.colorScheme.scrollBarForeground codeView.scrollBars.vertical.minimumValue, codeView.scrollBars.vertical.maximumValue, codeView.scrollBars.vertical.value, codeView.scrollBars.vertical.shownValueCount = 1, #codeView.lines, codeView.fromLine, codeView.height codeView.scrollBars.vertical.localX = codeView.width codeView.scrollBars.vertical.localY = 1 codeView.scrollBars.vertical.height = codeView.height - 1 else codeView.scrollBars.vertical.hidden = true end if codeView.maximumLineLength > codeView.codeAreaWidth - 2 then codeView.scrollBars.horizontal.hidden = false codeView.scrollBars.horizontal.colors.background, codeView.scrollBars.horizontal.colors.foreground = syntax.colorScheme.scrollBarBackground, syntax.colorScheme.scrollBarForeground codeView.scrollBars.horizontal.minimumValue, codeView.scrollBars.horizontal.maximumValue, codeView.scrollBars.horizontal.value, codeView.scrollBars.horizontal.shownValueCount = 1, codeView.maximumLineLength, codeView.fromSymbol, codeView.codeAreaWidth - 2 codeView.scrollBars.horizontal.localX = codeView.lineNumbersWidth + 1 codeView.scrollBars.horizontal.localY = codeView.height codeView.scrollBars.horizontal.width = codeView.codeAreaWidth - 1 else codeView.scrollBars.horizontal.hidden = true end codeView:reimplementedDraw() end function GUI.codeView(x, y, width, height, lines, fromSymbol, fromLine, maximumLineLength, selections, highlights, highlightLuaSyntax, indentationWidth) local codeView = GUI.container(x, y, width, height) codeView.lines = lines codeView.fromSymbol = fromSymbol codeView.fromLine = fromLine codeView.maximumLineLength = maximumLineLength codeView.selections = selections or {} codeView.highlights = highlights or {} codeView.highlightLuaSyntax = highlightLuaSyntax codeView.indentationWidth = indentationWidth codeView.scrollBars = { vertical = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true)), horizontal = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true)) } codeView.reimplementedDraw = codeView.draw codeView.draw = codeViewDraw return codeView end ----------------------------------------- Color Selector object ----------------------------------------- local function colorSelectorDraw(colorSelector) local overlayColor = colorSelector.color < 0x7FFFFF and 0xFFFFFF or 0x000000 buffer.square(colorSelector.x, colorSelector.y, colorSelector.width, colorSelector.height, colorSelector.color, overlayColor, " ") if colorSelector.pressed then buffer.square(colorSelector.x, colorSelector.y, colorSelector.width, colorSelector.height, overlayColor, overlayColor, " ", 0.8) end if colorSelector.height > 1 then buffer.text(colorSelector.x, colorSelector.y + colorSelector.height - 1, overlayColor, string.rep("▄", colorSelector.width), 0.8) end buffer.text(colorSelector.x + 1, colorSelector.y + math.floor(colorSelector.height / 2), overlayColor, string.limit(colorSelector.text, colorSelector.width - 2)) return colorSelector end local function colorSelectorEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then object.pressed = true mainContainer:draw() buffer.draw() object.color = require("palette").show(math.floor(mainContainer.width / 2 - 35), math.floor(mainContainer.height / 2 - 12), object.color) or object.color object.pressed = false mainContainer:draw() buffer.draw() callMethod(object.onTouch, eventData) end end function GUI.colorSelector(x, y, width, height, color, text) local colorSelector = GUI.object(x, y, width, height) colorSelector.eventHandler = colorSelectorEventHandler colorSelector.color = color colorSelector.text = text colorSelector.draw = colorSelectorDraw return colorSelector end ----------------------------------------- Chart object ----------------------------------------- local function getAxisValue(number, postfix, roundValues) if roundValues then return math.floor(number) .. postfix else local integer, fractional = math.modf(number) local firstPart, secondPart = "", "" if math.abs(integer) >= 1000 then return math.shortenNumber(integer, 2) .. postfix else if math.abs(fractional) > 0 then return string.format("%.2f", number) .. postfix else return number .. postfix end end end end local function drawChart(object) -- Sorting by x value local valuesCopy = {} for i = 1, #object.values do valuesCopy[i] = object.values[i] end table.sort(valuesCopy, function(a, b) return a[1] < b[1] end) if #valuesCopy == 0 then valuesCopy = {{0, 0}} end -- Max, min, deltas local xMin, xMax, yMin, yMax = valuesCopy[1][1], valuesCopy[#valuesCopy][1], valuesCopy[1][2], valuesCopy[1][2] for i = 1, #valuesCopy do yMin, yMax = math.min(yMin, valuesCopy[i][2]), math.max(yMax, valuesCopy[i][2]) end local dx, dy = xMax - xMin, yMax - yMin -- y axis values and helpers local value, chartHeight, yAxisValueMaxWidth, yAxisValues = yMin, object.height - 1 - (object.showXAxisValues and 1 or 0), 0, {} for y = object.y + object.height - 3, object.y + 1, -chartHeight * object.yAxisValueInterval do local stringValue = getAxisValue(value, object.yAxisPostfix, object.roundValues) yAxisValueMaxWidth = math.max(yAxisValueMaxWidth, unicode.len(stringValue)) table.insert(yAxisValues, {y = math.ceil(y), value = stringValue}) value = value + dy * object.yAxisValueInterval end local stringValue = getAxisValue(yMax, object.yAxisPostfix, object.roundValues) table.insert(yAxisValues, {y = object.y, value = stringValue}) yAxisValueMaxWidth = math.max(yAxisValueMaxWidth, unicode.len(stringValue)) local chartWidth = object.width - (object.showYAxisValues and yAxisValueMaxWidth + 2 or 0) local chartX = object.x + object.width - chartWidth for i = 1, #yAxisValues do if object.showYAxisValues then buffer.text(chartX - unicode.len(yAxisValues[i].value) - 2, yAxisValues[i].y, object.colors.axisValue, yAxisValues[i].value) end buffer.text(chartX, yAxisValues[i].y, object.colors.helpers, string.rep("─", chartWidth)) end -- x axis values if object.showXAxisValues then value = xMin for x = chartX, chartX + chartWidth - 2, chartWidth * object.xAxisValueInterval do local stringValue = getAxisValue(value, object.xAxisPostfix, object.roundValues) buffer.text(math.floor(x - unicode.len(stringValue) / 2), object.y + object.height - 1, object.colors.axisValue, stringValue) value = value + dx * object.xAxisValueInterval end local value = getAxisValue(xMax, object.xAxisPostfix, object.roundValues) buffer.text(object.x + object.width - unicode.len(value), object.y + object.height - 1, object.colors.axisValue, value) end -- Axis lines for y = object.y, object.y + chartHeight - 1 do buffer.text(chartX - 1, y, object.colors.axis, "┨") end buffer.text(chartX - 1, object.y + chartHeight, object.colors.axis, "┗" .. string.rep("┯━", math.floor(chartWidth / 2))) local function fillVerticalPart(x1, y1, x2, y2) local dx, dy = x2 - x1, y2 - y1 local absdx, absdy = math.abs(dx), math.abs(dy) if absdx >= absdy then local step, y = dy / absdx, y1 for x = x1, x2, (x1 < x2 and 1 or -1) do local yFloor = math.floor(y) buffer.semiPixelSquare(math.floor(x), yFloor, 1, math.floor(object.y + chartHeight) * 2 - yFloor - 1, object.colors.chart) y = y + step end else local step, x = dx / absdy, x1 for y = y1, y2, (y1 < y2 and 1 or -1) do local yFloor = math.floor(y) buffer.semiPixelSquare(math.floor(x), yFloor, 1, math.floor(object.y + chartHeight) * 2 - yFloor - 1, object.colors.chart) x = x + step end end end -- chart for i = 1, #valuesCopy - 1 do local x = math.floor(chartX + (valuesCopy[i][1] - xMin) / dx * (chartWidth - 1)) local y = math.floor(object.y + chartHeight - 1 - (valuesCopy[i][2] - yMin) / dy * (chartHeight - 1)) * 2 local xNext = math.floor(chartX + (valuesCopy[i + 1][1] - xMin) / dx * (chartWidth - 1)) local yNext = math.floor(object.y + chartHeight - 1 - (valuesCopy[i + 1][2] - yMin) / dy * (chartHeight - 1)) * 2 if object.fillChartArea then fillVerticalPart(x, y, xNext, yNext) else buffer.semiPixelLine(x, y, xNext, yNext, object.colors.chart) end end return object end function GUI.chart(x, y, width, height, axisColor, axisValueColor, axisHelpersColor, chartColor, xAxisValueInterval, yAxisValueInterval, xAxisPostfix, yAxisPostfix, fillChartArea, values) local object = GUI.object(x, y, width, height) object.colors = {axis = axisColor, chart = chartColor, axisValue = axisValueColor, helpers = axisHelpersColor} object.draw = drawChart object.values = values or {} object.xAxisPostfix = xAxisPostfix object.yAxisPostfix = yAxisPostfix object.xAxisValueInterval = xAxisValueInterval object.yAxisValueInterval = yAxisValueInterval object.fillChartArea = fillChartArea object.showYAxisValues = true object.showXAxisValues = true return object end ----------------------------------------- Window object ----------------------------------------- local function windowDraw(window) GUI.windowShadow(window.x, window.y, window.width, window.height, nil, true) GUI.drawContainerContent(window) return window end local function windowCheck(container, x, y) for i = #container.children, 1, -1 do if container.children[i].children then if windowCheck(container.children[i], x, y) then return true end elseif container.children[i].eventHandler and container.children[i]:isClicked(x, y) then return true end end end local function windowEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then object.lastTouchPosition = object.lastTouchPosition or {} object.lastTouchPosition.x, object.lastTouchPosition.y = eventData[3], eventData[4] if object ~= object.parent.children[#object.parent.children] then object:moveToFront() mainContainer:draw() buffer.draw() end elseif eventData[1] == "drag" and object.lastTouchPosition and not windowCheck(object, eventData[3], eventData[4]) then local xOffset, yOffset = eventData[3] - object.lastTouchPosition.x, eventData[4] - object.lastTouchPosition.y object.lastTouchPosition.x, object.lastTouchPosition.y = eventData[3], eventData[4] if xOffset ~= 0 or yOffset ~= 0 then object.localX, object.localY = object.localX + xOffset, object.localY + yOffset mainContainer:draw() buffer.draw() end elseif eventData[1] == "drop" then object.lastTouchPosition = nil elseif eventData[1] == "key_down" then -- Ctrl or CMD if keyboard.isKeyDown(29) or keyboard.isKeyDown(219) then -- W if eventData[4] == 17 then if object == object.parent.children[#object.parent.children] and not eventData.windowHandled then eventData.windowHandled = true object:close() mainContainer:draw() buffer.draw() end end end end end function GUI.window(x, y, width, height) local window = GUI.container(x, y, width, height) window.eventHandler = windowEventHandler window.draw = windowDraw return window end function GUI.filledWindow(x, y, width, height, backgroundColor) local window = GUI.window(x, y, width, height) window.backgroundPanel = window:addChild(GUI.panel(1, 1, width, height, backgroundColor)) window.actionButtons = window:addChild(GUI.actionButtons(2, 2, false)) return window end function GUI.titledWindow(x, y, width, height, title, addTitlePanel) local window = GUI.filledWindow(x, y, width, height, GUI.colors.windows.backgroundPanel) if addTitlePanel then window.titlePanel = window:addChild(GUI.panel(1, 1, width, 1, GUI.colors.windows.title.background)) window.backgroundPanel.localY, window.backgroundPanel.height = 2, window.height - 1 end window.titleLabel = window:addChild(GUI.label(1, 1, width, height, GUI.colors.windows.title.text, title)):setAlignment(GUI.alignment.horizontal.center, GUI.alignment.vertical.top) window.actionButtons.localY = 1 window.actionButtons:moveToFront() return window end function GUI.tabbedWindow(x, y, width, height, ...) local window = GUI.filledWindow(x, y, width, height, GUI.colors.windows.backgroundPanel) window.tabBar = window:addChild(GUI.tabBar(1, 1, window.width, 3, 2, 0, GUI.colors.windows.tabBar.default.background, GUI.colors.windows.tabBar.default.text, GUI.colors.windows.tabBar.selected.background, GUI.colors.windows.tabBar.selected.text, ...)) window.backgroundPanel.localY, window.backgroundPanel.height = 4, window.height - 3 window.actionButtons:moveToFront() window.actionButtons.localY = 2 return window end ----------------------------------------- Dropdown Menu ----------------------------------------- local function dropDownMenuItemDraw(item) local yText = item.y + math.floor(item.height / 2) if item.type == GUI.dropDownMenuElementTypes.default then local textColor = item.color or item.parent.parent.colors.default.text if item.pressed then textColor = item.parent.parent.colors.pressed.text buffer.square(item.x, item.y, item.width, item.height, item.parent.parent.colors.pressed.background, textColor, " ") elseif item.disabled then textColor = item.parent.parent.colors.disabled.text end buffer.text(item.x + 1, yText, textColor, item.text) if item.shortcut then buffer.text(item.x + item.width - unicode.len(item.shortcut) - 1, yText, textColor, item.shortcut) end else buffer.text(item.x, yText, item.parent.parent.colors.separator, string.rep("─", item.width)) end return item end local function dropDownMenuItemEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then if object.type == GUI.dropDownMenuElementTypes.default then object.pressed = true mainContainer:draw() buffer.draw() if object.subMenu then object.subMenu.y = object.parent.y + object.localY - 1 object.subMenu.x = object.parent.x + object.parent.width if buffer.getWidth() - object.parent.x - object.parent.width + 1 < object.subMenu.width then object.subMenu.x = object.parent.x - object.subMenu.width end object.subMenu:show() else os.sleep(0.2) end object.pressed = false mainContainer:draw() buffer.draw() mainContainer.selectedItem = object:indexOf() callMethod(object.onTouch) end mainContainer:stopEventHandling() end end local function dropDownMenuCalculateSizes(menu) local totalHeight = 0 for i = 1, #menu.itemsContainer.children do totalHeight = totalHeight + (menu.itemsContainer.children[i].type == GUI.dropDownMenuElementTypes.separator and 1 or menu.itemHeight) menu.itemsContainer.children[i].width = menu.width end menu.height = math.min(totalHeight, menu.maximumHeight, buffer.getHeight() - menu.y) menu.itemsContainer.width, menu.itemsContainer.height = menu.width, menu.height menu.nextButton.localY = menu.height menu.prevButton.width, menu.nextButton.width = menu.width, menu.width menu.prevButton.hidden = menu.itemsContainer.children[1].localY >= 1 menu.nextButton.hidden = menu.itemsContainer.children[#menu.itemsContainer.children].localY + menu.itemsContainer.children[#menu.itemsContainer.children].height - 1 <= menu.height end local function dropDownMenuAddItem(menu, text, disabled, shortcut, color) local item = menu.itemsContainer:addChild(GUI.object(1, #menu.itemsContainer.children == 0 and 1 or menu.itemsContainer.children[#menu.itemsContainer.children].localY + menu.itemsContainer.children[#menu.itemsContainer.children].height, menu.width, menu.itemHeight)) item.type = GUI.dropDownMenuElementTypes.default item.text = text item.disabled = disabled item.shortcut = shortcut item.color = color item.draw = dropDownMenuItemDraw item.eventHandler = dropDownMenuItemEventHandler dropDownMenuCalculateSizes(menu) return item end local function dropDownMenuAddSeparator(menu) local item = dropDownMenuAddItem(menu) item.type = GUI.dropDownMenuElementTypes.separator item.height = 1 return item end local function dropDownMenuScrollDown(menu) if menu.itemsContainer.children[1].localY < 1 then for i = 1, #menu.itemsContainer.children do menu.itemsContainer.children[i].localY = menu.itemsContainer.children[i].localY + 1 end end menu:draw() buffer.draw() end local function dropDownMenuScrollUp(menu) if menu.itemsContainer.children[#menu.itemsContainer.children].localY + menu.itemsContainer.children[#menu.itemsContainer.children].height - 1 > menu.height then for i = 1, #menu.itemsContainer.children do menu.itemsContainer.children[i].localY = menu.itemsContainer.children[i].localY - 1 end end menu:draw() buffer.draw() end local function dropDownMenuEventHandler(mainContainer, object, eventData) if eventData[1] == "scroll" then if eventData[5] == 1 then dropDownMenuScrollDown(object) else dropDownMenuScrollUp(object) end end end local function dropDownMenuDraw(menu) dropDownMenuCalculateSizes(menu) if menu.oldPixels then buffer.paste(menu.x, menu.y, menu.oldPixels) else menu.oldPixels = buffer.copy(menu.x, menu.y, menu.width + 1, menu.height + 1) end buffer.square(menu.x, menu.y, menu.width, menu.height, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency.background) GUI.drawContainerContent(menu) GUI.windowShadow(menu.x, menu.y, menu.width, menu.height, menu.colors.transparency.shadow, true) return menu end local function dropDownMenuShow(menu) local mainContainer = GUI.fullScreenContainer() mainContainer:addChild(GUI.object(1, 1, mainContainer.width, mainContainer.height)).eventHandler = function(mainContainer, object, eventData) if eventData[1] == "touch" then buffer.paste(menu.x, menu.y, menu.oldPixels) buffer.draw() mainContainer:stopEventHandling() end end mainContainer:addChild(menu) menu:draw() buffer.draw() mainContainer:startEventHandling() buffer.paste(menu.x, menu.y, menu.oldPixels) buffer.draw() if mainContainer.selectedItem then return menu.itemsContainer.children[mainContainer.selectedItem].text, mainContainer.selectedItem end end function GUI.dropDownMenu(x, y, width, maximumHeight, itemHeight, backgroundColor, textColor, backgroundPressedColor, textPressedColor, disabledColor, separatorColor, backgroundTransparency, shadowTransparency) local menu = GUI.container(x, y, width, 1) menu.colors = { default = { background = backgroundColor, text = textColor }, pressed = { background = backgroundPressedColor, text = textPressedColor }, disabled = { text = disabledColor }, separator = separatorColor, transparency = { background = backgroundTransparency, shadow = shadowTransparency } } menu.itemsContainer = menu:addChild(GUI.container(1, 1, menu.width, menu.height)) menu.prevButton = menu:addChild(GUI.button(1, 1, menu.width, 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, "▲")) menu.nextButton = menu:addChild(GUI.button(1, 1, menu.width, 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, "▼")) menu.prevButton.colors.default.transparency, menu.nextButton.colors.default.transparency = backgroundTransparency, backgroundTransparency menu.prevButton.onTouch = function() dropDownMenuScrollDown(menu) end menu.nextButton.onTouch = function() dropDownMenuScrollUp(menu) end menu.itemHeight = itemHeight menu.addSeparator = dropDownMenuAddSeparator menu.addItem = dropDownMenuAddItem menu.draw = dropDownMenuDraw menu.show = dropDownMenuShow menu.maximumHeight = maximumHeight menu.eventHandler = dropDownMenuEventHandler return menu end ----------------------------------------- Context Menu ----------------------------------------- local function contextMenuCalculate(menu) local widestItem, widestShortcut = 0, 0 for i = 1, #menu.itemsContainer.children do if menu.itemsContainer.children[i].type == GUI.dropDownMenuElementTypes.default then widestItem = math.max(widestItem, unicode.len(menu.itemsContainer.children[i].text)) if menu.itemsContainer.children[i].shortcut then widestShortcut = math.max(widestShortcut, unicode.len(menu.itemsContainer.children[i].shortcut)) end end end menu.width = 2 + widestItem + (widestShortcut > 0 and 3 + widestShortcut or 0) menu.height = #menu.itemsContainer.children end local function contextMenuShow(menu) contextMenuCalculate(menu) local bufferWidth, bufferHeight = buffer.getResolution() if menu.y + menu.height >= bufferHeight then menu.y = bufferHeight - menu.height end if menu.x + menu.width + 1 >= bufferWidth then menu.x = bufferWidth - menu.width - 1 end return dropDownMenuShow(menu) end local function contextMenuAddItem(menu, ...) contextMenuCalculate(menu) return dropDownMenuAddItem(menu, ...) end local function contextMenuAddSeparator(menu, ...) contextMenuCalculate(menu) return dropDownMenuAddSeparator(menu, ...) end local function contextMenuAddSubMenu(menu, text) local item = menu:addItem(text, false, "►") item.subMenu = GUI.contextMenu(1, 1) item.subMenu.colors = menu.colors return item.subMenu end function GUI.contextMenu(x, y, backgroundColor, textColor, backgroundPressedColor, textPressedColor, disabledColor, separatorColor, backgroundTransparency, shadowTransparency) local menu = GUI.dropDownMenu(x, y, 1, math.ceil(buffer.getHeight() * 0.5), 1, backgroundColor or GUI.colors.contextMenu.default.background, textColor or GUI.colors.contextMenu.default.text, backgroundPressedColor or GUI.colors.contextMenu.pressed.background, textPressedColor or GUI.colors.contextMenu.pressed.text, disabledColor or GUI.colors.contextMenu.disabled, separatorColor or GUI.colors.contextMenu.separator, backgroundTransparency or GUI.colors.contextMenu.transparency.background, shadowTransparency or GUI.colors.contextMenu.transparency.shadow ) menu.colors.transparency.background = menu.colors.transparency.background or GUI.colors.contextMenu.transparency.background menu.colors.transparency.shadow = menu.colors.transparency.shadow or GUI.colors.contextMenu.transparency.shadow menu.show = contextMenuShow menu.addSubMenu = contextMenuAddSubMenu menu.addItem = contextMenuAddItem menu.addSeparator = contextMenuAddSeparator return menu end ----------------------------------------- Combo Box Object ----------------------------------------- local function drawComboBox(object) buffer.square(object.x, object.y, object.width, object.height, object.colors.default.background, object.colors.default.text, " ") local x, y, limit, arrowSize = object.x + 1, math.floor(object.y + object.height / 2), object.width - 3, object.height if object.dropDownMenu.itemsContainer.children[object.selectedItem] then buffer.text(x, y, object.colors.default.text, string.limit(object.dropDownMenu.itemsContainer.children[object.selectedItem].text, limit, "right")) end GUI.button(object.x + object.width - arrowSize * 2 + 1, object.y, arrowSize * 2 - 1, arrowSize, object.colors.arrow.background, object.colors.arrow.text, 0x0, 0x0, object.pressed and "▲" or "▼"):draw() return object end local function comboBoxGetItem(object, index) return object.dropDownMenu.itemsContainer.children[index] end local function comboBoxClear(object) object.dropDownMenu.itemsContainer:deleteChildren() return object end local function comboBoxIndexOfItem(object, text) for i = 1, #object.dropDownMenu.itemsContainer.children do if object.dropDownMenu.itemsContainer.children[i].text == text then return i end end end local function selectComboBoxItem(object) object.pressed = true object:draw() object.dropDownMenu.x, object.dropDownMenu.y = object.x, object.y + object.height object.dropDownMenu.width = object.width local _, selectedItem = object.dropDownMenu:show() object.selectedItem = selectedItem or object.selectedItem object.pressed = false object:draw() buffer.draw() return object end local function comboBoxEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then object:selectItem() callMethod(object.onItemSelected, object.dropDownMenu.itemsContainer.children[object.selectedItem], eventData) end end local function comboBoxAddItem(object, ...) return object.dropDownMenu:addItem(...) end local function comboBoxAddSeparator(object) return object.dropDownMenu:addSeparator() end function GUI.comboBox(x, y, width, elementHeight, backgroundColor, textColor, arrowBackgroundColor, arrowTextColor) local object = GUI.object(x, y, width, elementHeight) object.eventHandler = comboBoxEventHandler object.colors = { default = { background = backgroundColor, text = textColor }, pressed = { background = GUI.colors.contextMenu.pressed.background, text = GUI.colors.contextMenu.pressed.text }, arrow = { background = arrowBackgroundColor, text = arrowTextColor } } object.dropDownMenu = GUI.dropDownMenu(1, 1, 1, math.ceil(buffer.getHeight() * 0.5), elementHeight, object.colors.default.background, object.colors.default.text, object.colors.pressed.background, object.colors.pressed.text, GUI.colors.contextMenu.disabled, GUI.colors.contextMenu.separator, GUI.colors.contextMenu.transparency.background, GUI.colors.contextMenu.transparency.shadow ) object.selectedItem = 1 object.addItem = comboBoxAddItem object.addSeparator = comboBoxAddSeparator object.draw = drawComboBox object.selectItem = selectComboBoxItem object.clear = comboBoxClear object.indexOfItem = comboBoxIndexOfItem object.getItem = comboBoxGetItem return object end ----------------------------------------- Switch and label object ----------------------------------------- local function switchAndLabelDraw(switchAndLabel) switchAndLabel.label.width = switchAndLabel.width switchAndLabel.switch.localX = switchAndLabel.width - switchAndLabel.switch.width switchAndLabel.label.x, switchAndLabel.label.y = switchAndLabel.x + switchAndLabel.label.localX - 1, switchAndLabel.y + switchAndLabel.label.localY - 1 switchAndLabel.switch.x, switchAndLabel.switch.y = switchAndLabel.x + switchAndLabel.switch.localX - 1, switchAndLabel.y + switchAndLabel.switch.localY - 1 switchAndLabel.label:draw() switchAndLabel.switch:draw() return switchAndLabel end function GUI.switchAndLabel(x, y, width, switchWidth, activeColor, passiveColor, pipeColor, textColor, text, switchState) local switchAndLabel = GUI.container(x, y, width, 1) switchAndLabel.label = switchAndLabel:addChild(GUI.label(1, 1, width, 1, textColor, text)) switchAndLabel.switch = switchAndLabel:addChild(GUI.switch(1, 1, switchWidth, activeColor, passiveColor, pipeColor, switchState)) switchAndLabel.draw = switchAndLabelDraw return switchAndLabel end ----------------------------------------- Horizontal Slider Object ----------------------------------------- local function sliderDraw(object) -- На всякий случай делаем значение не меньше минимального и не больше максимального object.value = math.min(math.max(object.value, object.minimumValue), object.maximumValue) if object.showMaximumAndMinimumValues then local stringMaximumValue, stringMinimumValue = tostring(object.roundValues and math.floor(object.maximumValue) or math.roundToDecimalPlaces(object.maximumValue, 2)), tostring(object.roundValues and math.floor(object.minimumValue) or math.roundToDecimalPlaces(object.minimumValue, 2)) buffer.text(object.x - unicode.len(stringMinimumValue) - 1, object.y, object.colors.value, stringMinimumValue) buffer.text(object.x + object.width + 1, object.y, object.colors.value, stringMaximumValue) end if object.currentValuePrefix or object.currentValuePostfix then local stringCurrentValue = (object.currentValuePrefix or "") .. (object.roundValues and math.floor(object.value) or math.roundToDecimalPlaces(object.value, 2)) .. (object.currentValuePostfix or "") buffer.text(math.floor(object.x + object.width / 2 - unicode.len(stringCurrentValue) / 2), object.y + 1, object.colors.value, stringCurrentValue) end local activeWidth = math.floor(object.width - ((object.maximumValue - object.value) * object.width / (object.maximumValue - object.minimumValue))) buffer.text(object.x, object.y, object.colors.passive, string.rep("━", object.width)) buffer.text(object.x, object.y, object.colors.active, string.rep("━", activeWidth)) buffer.text(object.x + activeWidth - 1, object.y, object.colors.pipe, "⬤") return object end local function sliderEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" or eventData[1] == "drag" then local clickPosition = eventData[3] - object.x + 1 object.value = object.minimumValue + (clickPosition * (object.maximumValue - object.minimumValue) / object.width) mainContainer:draw() buffer.draw() callMethod(object.onValueChanged, object.value, eventData) end end function GUI.slider(x, y, width, activeColor, passiveColor, pipeColor, valueColor, minimumValue, maximumValue, value, showMaximumAndMinimumValues, currentValuePrefix, currentValuePostfix) local object = GUI.object(x, y, width, 1) object.eventHandler = sliderEventHandler object.colors = {active = activeColor, passive = passiveColor, pipe = pipeColor, value = valueColor} object.draw = sliderDraw object.minimumValue = minimumValue object.maximumValue = maximumValue object.value = value object.showMaximumAndMinimumValues = showMaximumAndMinimumValues object.currentValuePrefix = currentValuePrefix object.currentValuePostfix = currentValuePostfix object.roundValues = false return object end ----------------------------------------- Switch object ----------------------------------------- local function switchDraw(switch) buffer.text(switch.x - 1, switch.y, switch.colors.passive, "⠰") buffer.square(switch.x, switch.y, switch.width, 1, switch.colors.passive, 0x000000, " ") buffer.text(switch.x + switch.width, switch.y, switch.colors.passive, "⠆") buffer.text(switch.x - 1, switch.y, switch.colors.active, "⠰") buffer.square(switch.x, switch.y, switch.pipePosition - 1, 1, switch.colors.active, 0x000000, " ") buffer.text(switch.x + switch.pipePosition - 2, switch.y, switch.colors.pipe, "⠰") buffer.square(switch.x + switch.pipePosition - 1, switch.y, 2, 1, switch.colors.pipe, 0x000000, " ") buffer.text(switch.x + switch.pipePosition + 1, switch.y, switch.colors.pipe, "⠆") return switch end local function switchUpdate(switch) switch.pipePosition = switch.state and switch.width - 1 or 1 end local function switchEventHandler(mainContainer, switch, eventData) if eventData[1] == "touch" then switch.state = not switch.state switch:addAnimation( function(mainContainer, switch, animation) if switch.state then switch.pipePosition = math.round(1 + animation.position * (switch.width - 2)) else switch.pipePosition = math.round(1 + (1 - animation.position) * (switch.width - 2)) end end, function(mainContainer, switch, animation) animation:delete() callMethod(switch.onStateChanged) end ):start(switch.animationDuration) end end function GUI.switch(x, y, width, activeColor, passiveColor, pipeColor, state) local switch = GUI.object(x, y, width, 1) switch.pipePosition = 1 switch.eventHandler = switchEventHandler switch.colors = { active = activeColor, passive = passiveColor, pipe = pipeColor, } switch.draw = switchDraw switch.state = state or false switch.update = switchUpdate switch.animated = true switch.animationDuration = 0.3 switch:update() return switch end ----------------------------------------------------------------------------------------------------- local function brailleCanvasDraw(brailleCanvas) local index, background, foreground, symbol for y = 1, brailleCanvas.height do for x = 1, brailleCanvas.width do index = buffer.getIndex(brailleCanvas.x + x - 1, brailleCanvas.y + y - 1) background, foreground, symbol = buffer.rawGet(index) buffer.rawSet(index, background, brailleCanvas.pixels[y][x][9], brailleCanvas.pixels[y][x][10]) end end return brailleCanvas end local function brailleCanvasSet(brailleCanvas, x, y, state, color) local xReal, yReal = math.ceil(x / 2), math.ceil(y / 4) if xReal <= brailleCanvas.width and yReal <= brailleCanvas.height then brailleCanvas.pixels[yReal][xReal][(y - (yReal - 1) * 4 - 1) * 2 + x - (xReal - 1) * 2] = state and 1 or 0 brailleCanvas.pixels[yReal][xReal][9] = color brailleCanvas.pixels[yReal][xReal][10] = unicode.char( 10240 + 128 * brailleCanvas.pixels[yReal][xReal][8] + 64 * brailleCanvas.pixels[yReal][xReal][7] + 32 * brailleCanvas.pixels[yReal][xReal][6] + 16 * brailleCanvas.pixels[yReal][xReal][4] + 8 * brailleCanvas.pixels[yReal][xReal][2] + 4 * brailleCanvas.pixels[yReal][xReal][5] + 2 * brailleCanvas.pixels[yReal][xReal][3] + brailleCanvas.pixels[yReal][xReal][1] ) end return brailleCanvas end function GUI.brailleCanvas(x, y, width, height) local brailleCanvas = GUI.object(x, y, width, height) brailleCanvas.pixels = {} brailleCanvas.set = brailleCanvasSet brailleCanvas.draw = brailleCanvasDraw for j = 1, height * 4 do brailleCanvas.pixels[j] = {} for i = 1, width * 2 do brailleCanvas.pixels[j][i] = { 0, 0, 0, 0, 0, 0, 0, 0, 0x0, " " } end end return brailleCanvas end ----------------------------------------- Layout object ----------------------------------------- local function layoutCheckCell(layout, column, row) if column < 1 or column > #layout.columnSizes or row < 1 or row > #layout.rowSizes then error("Specified grid position (" .. tostring(column) .. "x" .. tostring(row) .. ") is out of layout grid range") end end local function layoutGetAbsoluteTotalSize(array) local absoluteTotalSize = 0 for i = 1, #array do if array[i].sizePolicy == GUI.sizePolicies.absolute then absoluteTotalSize = absoluteTotalSize + array[i].size end end return absoluteTotalSize end local function layoutGetCalculatedSize(array, index, dependency) if array[index].sizePolicy == GUI.sizePolicies.percentage then array[index].calculatedSize = array[index].size * dependency else array[index].calculatedSize = array[index].size end end local function layoutUpdate(layout) local columnPercentageTotalSize, rowPercentageTotalSize = layout.width - layoutGetAbsoluteTotalSize(layout.columnSizes), layout.height - layoutGetAbsoluteTotalSize(layout.rowSizes) for row = 1, #layout.rowSizes do layoutGetCalculatedSize(layout.rowSizes, row, rowPercentageTotalSize) for column = 1, #layout.columnSizes do layoutGetCalculatedSize(layout.columnSizes, column, columnPercentageTotalSize) layout.cells[row][column].totalWidth, layout.cells[row][column].totalHeight = 0, 0 end end -- Подготавливаем объекты к расположению и подсчитываем тотальные размеры local child, layoutRow, layoutColumn, cell for i = 1, #layout.children do child = layout.children[i] if not child.hidden then layoutRow, layoutColumn = child.layoutRow, child.layoutColumn -- Проверка на позицию в сетке if layoutRow >= 1 and layoutRow <= #layout.rowSizes and layoutColumn >= 1 and layoutColumn <= #layout.columnSizes then cell = layout.cells[layoutRow][layoutColumn] -- Авто-фиттинг объектов if cell.fitting.horizontal then child.width = math.round(layout.columnSizes[layoutColumn].calculatedSize - cell.fitting.horizontalRemove) end if cell.fitting.vertical then child.height = math.round(layout.rowSizes[layoutRow].calculatedSize - cell.fitting.verticalRemove) end -- Направление и расчет размеров if cell.direction == GUI.directions.horizontal then cell.totalWidth = cell.totalWidth + child.width + cell.spacing cell.totalHeight = math.max(cell.totalHeight, child.height) else cell.totalWidth = math.max(cell.totalWidth, child.width) cell.totalHeight = cell.totalHeight + child.height + cell.spacing end else error("Layout child with index " .. i .. " has been assigned to cell (" .. layoutColumn .. "x" .. layoutRow .. ") out of layout grid range") end end end -- Высчитываем позицицию объектов local x, y = 1, 1 for row = 1, #layout.rowSizes do for column = 1, #layout.columnSizes do local cell = layout.cells[row][column] cell.x, cell.y = GUI.getAlignmentCoordinates( { x = x, y = y, width = layout.columnSizes[column].calculatedSize, height = layout.rowSizes[row].calculatedSize, alignment = cell.alignment, }, { width = cell.totalWidth - (cell.direction == GUI.directions.horizontal and cell.spacing or 0), height = cell.totalHeight - (cell.direction == GUI.directions.vertical and cell.spacing or 0), } ) if cell.margin then cell.x, cell.y = GUI.getMarginCoordinates(cell) end x = x + layout.columnSizes[column].calculatedSize end x, y = 1, y + layout.rowSizes[row].calculatedSize end -- Размещаем все объекты for i = 1, #layout.children do child = layout.children[i] if not child.hidden then if cell.direction == GUI.directions.horizontal then child.localX = math.round(cell.x) child.localY = math.round(cell.y + cell.totalHeight / 2 - child.height / 2) cell.x = cell.x + child.width + cell.spacing else child.localX = math.round(cell.x + cell.totalWidth / 2 - child.width / 2) child.localY = math.round(cell.y) cell.y = cell.y + child.height + cell.spacing end end end end local function layoutSetCellPosition(layout, column, row, object) layoutCheckCell(layout, column, row) object.layoutRow = row object.layoutColumn = column return object end local function layoutSetCellDirection(layout, column, row, direction) layoutCheckCell(layout, column, row) layout.cells[row][column].direction = direction return layout end local function layoutSetCellSpacing(layout, column, row, spacing) layoutCheckCell(layout, column, row) layout.cells[row][column].spacing = spacing return layout end local function layoutSetCellAlignment(layout, column, row, horizontalAlignment, verticalAlignment) layoutCheckCell(layout, column, row) layout.cells[row][column].alignment.horizontal, layout.cells[row][column].alignment.vertical = horizontalAlignment, verticalAlignment return layout end local function layoutSetCellMargin(layout, column, row, horizontalMargin, verticalMargin) layoutCheckCell(layout, column, row) layout.cells[row][column].margin = { horizontal = horizontalMargin, vertical = verticalMargin } return layout end local function layoutNewCell() return { alignment = { horizontal = GUI.alignment.horizontal.center, vertical = GUI.alignment.vertical.center }, direction = GUI.directions.vertical, fitting = { horizontal = false, vertical = false}, spacing = 1, } end local function layoutCalculatePercentageSize(changingExistent, array, index) if array[index].sizePolicy == GUI.sizePolicies.percentage then local allPercents, beforeFromIndexPercents = 0, 0 for i = 1, #array do if array[i].sizePolicy == GUI.sizePolicies.percentage then allPercents = allPercents + array[i].size if i <= index then beforeFromIndexPercents = beforeFromIndexPercents + array[i].size end end end local modifyer if changingExistent then if beforeFromIndexPercents > 1 then error("Layout summary percentage > 100% at index " .. index) end modifyer = (1 - beforeFromIndexPercents) / (allPercents - beforeFromIndexPercents) else modifyer = (1 - array[index].size) / (allPercents - array[index].size) end for i = changingExistent and index + 1 or 1, #array do if array[i].sizePolicy == GUI.sizePolicies.percentage and i ~= index then array[i].size = modifyer * array[i].size end end end end local function layoutSetColumnWidth(layout, column, sizePolicy, size) layout.columnSizes[column].sizePolicy, layout.columnSizes[column].size = sizePolicy, size layoutCalculatePercentageSize(true, layout.columnSizes, column) return layout end local function layoutSetRowHeight(layout, row, sizePolicy, size) layout.rowSizes[row].sizePolicy, layout.rowSizes[row].size = sizePolicy, size layoutCalculatePercentageSize(true, layout.rowSizes, row) return layout end local function layoutAddColumn(layout, sizePolicy, size) for i = 1, #layout.rowSizes do table.insert(layout.cells[i], layoutNewCell()) end table.insert(layout.columnSizes, { sizePolicy = sizePolicy, size = size }) layoutCalculatePercentageSize(false, layout.columnSizes, #layout.columnSizes) -- GUI.error(layout.columnSizes) return layout end local function layoutAddRow(layout, sizePolicy, size) local row = {} for i = 1, #layout.columnSizes do table.insert(row, layoutNewCell()) end table.insert(layout.cells, row) table.insert(layout.rowSizes, { sizePolicy = sizePolicy, size = size }) layoutCalculatePercentageSize(false, layout.rowSizes, #layout.rowSizes) -- GUI.error(layout.rowSizes) return layout end local function layoutRemoveRow(layout, row) table.remove(layout.cells, row) layout.rowSizes[row].size = 0 layoutCalculatePercentageSize(false, layout.rowSizes, row) table.remove(layout.rowSizes, row) return layout end local function layoutRemoveColumn(layout, column) for i = 1, #layout.rowSizes do table.remove(layout.cells[i], column) end layout.columnSizes[column].size = 0 layoutCalculatePercentageSize(false, layout.columnSizes, column) table.remove(layout.columnSizes, column) return layout end local function layoutSetGridSize(layout, columnCount, rowCount) layout.cells = {} layout.rowSizes = {} layout.columnSizes = {} local rowSize, columnSize = 1 / rowCount, 1 / columnCount for i = 1, rowCount do layoutAddRow(layout, GUI.sizePolicies.percentage, 1 / i) end for i = 1, columnCount do layoutAddColumn(layout, GUI.sizePolicies.percentage, 1 / i) end return layout end local function layoutDraw(layout) layoutUpdate(layout) GUI.drawContainerContent(layout) if layout.showGrid then local x, y = layout.x, layout.y for j = 1, #layout.columnSizes do for i = 1, #layout.rowSizes do buffer.frame( math.round(x), math.round(y), math.round(layout.columnSizes[j].calculatedSize), math.round(layout.rowSizes[i].calculatedSize), 0xFF0000 ) y = y + layout.rowSizes[i].calculatedSize end x, y = x + layout.columnSizes[j].calculatedSize, layout.y end end end local function layoutFitToChildrenSize(layout, column, row) layout.width, layout.height = 0, 0 for i = 1, #layout.children do if not layout.children[i].hidden then if layout.cells[row][column].direction == GUI.directions.horizontal then layout.width = layout.width + layout.children[i].width + layout.cells[row][column].spacing layout.height = math.max(layout.height, layout.children[i].height) else layout.width = math.max(layout.width, layout.children[i].width) layout.height = layout.height + layout.children[i].height + layout.cells[row][column].spacing end end end if layout.cells[row][column].direction == GUI.directions.horizontal then layout.width = layout.width - layout.cells[row][column].spacing else layout.height = layout.height - layout.cells[row][column].spacing end return layout end local function layoutSetCellFitting(layout, column, row, horizontal, vertical, horizontalRemove, verticalRemove ) layoutCheckCell(layout, column, row) layout.cells[row][column].fitting = { horizontal = horizontal, vertical = vertical, horizontalRemove = horizontalRemove or 0, verticalRemove = verticalRemove or 0, } return layout end local function layoutAddChild(layout, object, ...) object.layoutRow = layout.defaultRow object.layoutColumn = layout.defaultColumn GUI.addChildToContainer(layout, object, ...) return object end function GUI.layout(x, y, width, height, columnCount, rowCount) local layout = GUI.container(x, y, width, height) layout.defaultRow = 1 layout.defaultColumn = 1 layout.addRow = layoutAddRow layout.addColumn = layoutAddColumn layout.removeRow = layoutRemoveRow layout.removeColumn = layoutRemoveColumn layout.setRowHeight = layoutSetRowHeight layout.setColumnWidth = layoutSetColumnWidth layout.setCellPosition = layoutSetCellPosition layout.setCellDirection = layoutSetCellDirection layout.setGridSize = layoutSetGridSize layout.setCellSpacing = layoutSetCellSpacing layout.setCellAlignment = layoutSetCellAlignment layout.setCellMargin = layoutSetCellMargin layout.fitToChildrenSize = layoutFitToChildrenSize layout.setCellFitting = layoutSetCellFitting layout.addChild = layoutAddChild layout.draw = layoutDraw layoutSetGridSize(layout, columnCount, rowCount) return layout end ----------------------------------------------------------------------------------------------------- local function filesystemDialogDraw(filesystemDialog) if filesystemDialog.extensionComboBox.hidden then filesystemDialog.input.width = filesystemDialog.cancelButton.localX - 4 else filesystemDialog.input.width = filesystemDialog.extensionComboBox.localX - 3 end if filesystemDialog.IOMode == GUI.filesystemModes.save then filesystemDialog.submitButton.disabled = not filesystemDialog.input.text else filesystemDialog.input.text = filesystemDialog.filesystemTree.selectedItem or "" filesystemDialog.submitButton.disabled = not filesystemDialog.filesystemTree.selectedItem end GUI.drawContainerContent(filesystemDialog) GUI.windowShadow(filesystemDialog.x, filesystemDialog.y, filesystemDialog.width, filesystemDialog.height, GUI.colors.contextMenu.transparency.shadow, true) return filesystemDialog end local function filesystemDialogSetMode(filesystemDialog, IOMode, filesystemMode) filesystemDialog.IOMode = IOMode filesystemDialog.filesystemMode = filesystemMode if filesystemDialog.IOMode == GUI.filesystemModes.save then filesystemDialog.filesystemTree.showMode = GUI.filesystemModes.directory filesystemDialog.filesystemTree.selectionMode = GUI.filesystemModes.directory filesystemDialog.input.disabled = false filesystemDialog.extensionComboBox.hidden = filesystemDialog.filesystemMode ~= GUI.filesystemModes.file or not filesystemDialog.filesystemTree.extensionFilters else if filesystemDialog.filesystemMode == GUI.filesystemModes.file then filesystemDialog.filesystemTree.showMode = GUI.filesystemModes.both filesystemDialog.filesystemTree.selectionMode = GUI.filesystemModes.file else filesystemDialog.filesystemTree.showMode = GUI.filesystemModes.directory filesystemDialog.filesystemTree.selectionMode = GUI.filesystemModes.directory end filesystemDialog.input.disabled = true filesystemDialog.extensionComboBox.hidden = true end filesystemDialog.filesystemTree:updateFileList() end local function filesystemDialogAddExtensionFilter(filesystemDialog, extension) filesystemDialog.extensionComboBox:addItem(extension) filesystemDialog.extensionComboBox.width = math.max(filesystemDialog.extensionComboBox.width, unicode.len(extension) + 3) filesystemDialog.extensionComboBox.localX = filesystemDialog.cancelButton.localX - filesystemDialog.extensionComboBox.width - 2 filesystemDialog.filesystemTree:addExtensionFilter(extension) filesystemDialog:setMode(filesystemDialog.IOMode, filesystemDialog.filesystemMode) end function GUI.filesystemDialog(x, y, width, height, submitButtonText, cancelButtonText, placeholderText, path) local filesystemDialog = GUI.container(x, y, width, height) filesystemDialog:addChild(GUI.panel(1, height - 2, width, 3, 0xD2D2D2)) filesystemDialog.cancelButton = filesystemDialog:addChild(GUI.adaptiveRoundedButton(1, height - 1, 2, 0, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xE1E1E1, cancelButtonText)) filesystemDialog.submitButton = filesystemDialog:addChild(GUI.adaptiveRoundedButton(1, height - 1, 2, 0, 0x3C3C3C, 0xE1E1E1, 0xE1E1E1, 0x3C3C3C, submitButtonText)) filesystemDialog.submitButton.localX = filesystemDialog.width - filesystemDialog.submitButton.width - 1 filesystemDialog.cancelButton.localX = filesystemDialog.submitButton.localX - filesystemDialog.cancelButton.width - 2 filesystemDialog.extensionComboBox = filesystemDialog:addChild(GUI.comboBox(1, height - 1, 1, 1, 0xE1E1E1, 0x666666, 0xC3C3C3, 0x888888)) filesystemDialog.extensionComboBox.hidden = true filesystemDialog.input = filesystemDialog:addChild(GUI.input(2, height - 1, 1, 1, 0xE1E1E1, 0x666666, 0x999999, 0xE1E1E1, 0x3C3C3C, "", placeholderText)) filesystemDialog.filesystemTree = filesystemDialog:addChild(GUI.filesystemTree(1, 1, width, height - 3, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xAAAAAA, 0x3C3C3C, 0xE1E1E1, 0xBBBBBB, 0xAAAAAA, 0xC3C3C3, 0x444444)) filesystemDialog.filesystemTree.workPath = path filesystemDialog.draw = filesystemDialogDraw filesystemDialog.setMode = filesystemDialogSetMode filesystemDialog.addExtensionFilter = filesystemDialogAddExtensionFilter filesystemDialog:setMode(GUI.filesystemModes.open, GUI.filesystemModes.file) return filesystemDialog end local function filesystemDialogShow(filesystemDialog) filesystemDialog:addAnimation( function(mainContainer, object, animation) object.localY = math.floor(1 + (1.0 - animation.position) * (-object.height)) end, function(mainContainer, switch, animation) animation:delete() end ):start(0.5) return filesystemDialog end ----------------------------------------------------------------------------------------------------- function GUI.addFilesystemDialogToContainer(parentContainer, ...) local container = parentContainer:addChild(GUI.container(1, 1, parentContainer.width, parentContainer.height)) container:addChild(GUI.object(1, 1, container.width, container.height)) local filesystemDialog = container:addChild(GUI.filesystemDialog(1, 1, math.floor(container.width * 0.35), math.floor(container.height * 0.8), ...)) filesystemDialog.localX = math.floor(container.width / 2 - filesystemDialog.width / 2) filesystemDialog.localY = -filesystemDialog.height local function onAnyTouch() local firstParent = filesystemDialog:getFirstParent() container:delete() firstParent:draw() buffer.draw() end filesystemDialog.cancelButton.onTouch = function() onAnyTouch() callMethod(filesystemDialog.onCancel) end filesystemDialog.submitButton.onTouch = function() onAnyTouch() local path = filesystemDialog.filesystemTree.selectedItem or filesystemDialog.filesystemTree.workPath or "/" if filesystemDialog.IOMode == GUI.filesystemModes.save then path = path .. filesystemDialog.input.text if filesystemDialog.filesystemMode == GUI.filesystemModes.file then local selectedItem = filesystemDialog.extensionComboBox:getItem(filesystemDialog.extensionComboBox.selectedItem) path = path .. (selectedItem and selectedItem.text or "") else path = path .. "/" end end callMethod(filesystemDialog.onSubmit, path) end filesystemDialog.show = filesystemDialogShow return filesystemDialog end ----------------------------------------------------------------------------------------------------- local function filesystemChooserDraw(object) local tipWidth = object.height * 2 - 1 local y = math.floor(object.y + object.height / 2) buffer.square(object.x, object.y, object.width - tipWidth, object.height, object.colors.background, object.colors.text, " ") buffer.square(object.x + object.width - tipWidth, object.y, tipWidth, object.height, object.pressed and object.colors.tipText or object.colors.tipBackground, object.pressed and object.colors.tipBackground or object.colors.tipText, " ") buffer.text(object.x + object.width - math.floor(tipWidth / 2) - 1, y, object.pressed and object.colors.tipBackground or object.colors.tipText, "…") buffer.text(object.x + 1, y, object.colors.text, string.limit(object.path or object.placeholderText, object.width - tipWidth - 2, "left")) return filesystemChooser end local function filesystemChooserAddExtensionFilter(object, extension) object.extensionFilters[unicode.lower(extension)] = true end local function filesystemChooserSetMode(object, filesystemMode) object.filesystemMode = filesystemMode end local function filesystemChooserEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then object.pressed = true mainContainer:draw() buffer.draw() local filesystemDialog = GUI.addFilesystemDialogToContainer(mainContainer, object.submitButtonText, object.cancelButtonText, object.placeholderText, object.filesystemDialogPath) for key in pairs(object.extensionFilters) do filesystemDialog:addExtensionFilter(key) end filesystemDialog:setMode(GUI.filesystemModes.open, object.filesystemMode) filesystemDialog.onCancel = function() object.pressed = false mainContainer:draw() buffer.draw() end filesystemDialog.onSubmit = function(path) object.path = path object.pressed = false mainContainer:draw() buffer.draw() callMethod(object.onSubmit, object.path) end filesystemDialog:show() end end function GUI.filesystemChooser(x, y, width, height, backgroundColor, textColor, tipBackgroundColor, tipTextColor, path, submitButtonText, cancelButtonText, placeholderText, filesystemDialogPath) local object = GUI.object(x, y, width, height) object.eventHandler = comboBoxEventHandler object.colors = { tipBackground = tipBackgroundColor, tipText = tipTextColor, text = textColor, background = backgroundColor } object.submitButtonText = submitButtonText object.cancelButtonText = cancelButtonText object.placeholderText = placeholderText object.pressed = false object.path = path object.filesystemDialogPath = filesystemDialogPath object.filesystemMode = GUI.filesystemModes.file object.extensionFilters = {} object.draw = filesystemChooserDraw object.eventHandler = filesystemChooserEventHandler object.addExtensionFilter = filesystemChooserAddExtensionFilter object.setMode = filesystemChooserSetMode return object end ----------------------------------------------------------------------------------------------------- local function resizerDraw(object) local horizontalMode = object.width >= object.height local x, y, symbol if horizontalMode then buffer.text(object.x, math.floor(object.y + object.height / 2), object.colors.helper, string.rep("━", object.width)) else local x = math.floor(object.x + object.width / 2) for i = object.y, object.y + object.height - 1 do local index = buffer.getIndex(x, i) buffer.rawSet(index, buffer.rawGet(index), object.colors.helper, "┃") end end if object.touchPosition then buffer.text(object.touchPosition.x - 1, object.touchPosition.y, object.colors.arrow, "←→") end end local function resizerEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then object.touchPosition = {x = eventData[3], y = eventData[4]} mainContainer:draw() buffer.draw() elseif eventData[1] == "drag" and object.touchPosition then local x, y = object.touchPosition.x, object.touchPosition.y object.touchPosition.x, object.touchPosition.y = eventData[3], eventData[4] if object.onResize then object.onResize(eventData[3] - x, eventData[4] - y) end mainContainer:draw() buffer.draw() elseif eventData[1] == "drop" then object.touchPosition = nil if object.onResizeFinished then object.onResizeFinished() end mainContainer:draw() buffer.draw() end end function GUI.resizer(x, y, width, height, helperColor, arrowColor) local object = GUI.object(x, y, width, height) object.colors = { helper = helperColor, arrow = arrowColor } object.draw = resizerDraw object.eventHandler = resizerEventHandler return object end ----------------------------------------- Scrollbar object ----------------------------------------- local function scrollBarDraw(scrollBar) local isVertical = scrollBar.height > scrollBar.width local valuesDelta = scrollBar.maximumValue - scrollBar.minimumValue + 1 local part = scrollBar.value / valuesDelta if isVertical then local barSize = math.ceil(scrollBar.shownValueCount / valuesDelta * scrollBar.height) local halfBarSize = math.floor(barSize / 2) scrollBar.ghostPosition.y = scrollBar.y + halfBarSize scrollBar.ghostPosition.height = scrollBar.height - barSize if scrollBar.thin then local y1 = math.floor(scrollBar.ghostPosition.y + part * scrollBar.ghostPosition.height - halfBarSize) local y2 = y1 + barSize - 1 local background for y = scrollBar.y, scrollBar.y + scrollBar.height - 1 do background = buffer.get(scrollBar.x, y) buffer.set(scrollBar.x, y, background, y >= y1 and y <= y2 and scrollBar.colors.foreground or scrollBar.colors.background, "┃") end else buffer.square(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ") buffer.square( scrollBar.x, math.floor(scrollBar.ghostPosition.y + part * scrollBar.ghostPosition.height - halfBarSize), scrollBar.width, barSize, scrollBar.colors.foreground, 0x0, " " ) end else local barSize = math.ceil(scrollBar.shownValueCount / valuesDelta * scrollBar.width) local halfBarSize = math.floor(barSize / 2) scrollBar.ghostPosition.x = scrollBar.x + halfBarSize scrollBar.ghostPosition.width = scrollBar.width - barSize if scrollBar.thin then local x1 = math.floor(scrollBar.ghostPosition.x + part * scrollBar.ghostPosition.width - halfBarSize) local x2 = x1 + barSize - 1 local background for x = scrollBar.x, scrollBar.x + scrollBar.width - 1 do background = buffer.get(x, scrollBar.y) buffer.set(x, scrollBar.y, background, x >= x1 and x <= x2 and scrollBar.colors.foreground or scrollBar.colors.background, "⠤") end else buffer.square(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ") buffer.square( math.floor(scrollBar.ghostPosition.x + part * scrollBar.ghostPosition.width - halfBarSize), scrollBar.y, barSize, scrollBar.height, scrollBar.colors.foreground, 0x0, " " ) end end return scrollBar end local function scrollBarEventHandler(mainContainer, object, eventData) local newValue = object.value if eventData[1] == "touch" or eventData[1] == "drag" then local delta = object.maximumValue - object.minimumValue + 1 if object.height > object.width then newValue = math.floor((eventData[4] - object.y + 1) / object.height * delta) else newValue = math.floor((eventData[3] - object.x + 1) / object.width * delta) end elseif eventData[1] == "scroll" then if eventData[5] == 1 then if object.value >= object.minimumValue + object.onScrollValueIncrement then newValue = object.value - object.onScrollValueIncrement else newValue = object.minimumValue end else if object.value <= object.maximumValue - object.onScrollValueIncrement then newValue = object.value + object.onScrollValueIncrement else newValue = object.maximumValue end end end if eventData[1] == "touch" or eventData[1] == "drag" or eventData[1] == "scroll" then object.value = newValue callMethod(object.onTouch, eventData) mainContainer:draw() buffer.draw() end end function GUI.scrollBar(x, y, width, height, backgroundColor, foregroundColor, minimumValue, maximumValue, value, shownValueCount, onScrollValueIncrement, thin) local scrollBar = GUI.object(x, y, width, height) scrollBar.eventHandler = scrollBarEventHandler scrollBar.maximumValue = maximumValue scrollBar.minimumValue = minimumValue scrollBar.value = value scrollBar.onScrollValueIncrement = onScrollValueIncrement scrollBar.shownValueCount = shownValueCount scrollBar.thin = thin scrollBar.colors = { background = backgroundColor, foreground = foregroundColor, } scrollBar.ghostPosition = {} scrollBar.draw = scrollBarDraw return scrollBar end ----------------------------------------- Tree object ----------------------------------------- local function treeDraw(tree) local y, yEnd, showScrollBar = tree.y, tree.y + tree.height - 1, #tree.items > tree.height local textLimit = tree.width - (showScrollBar and 1 or 0) if tree.colors.default.background then buffer.square(tree.x, tree.y, tree.width, tree.height, tree.colors.default.background, tree.colors.default.expandable, " ") end for i = tree.fromItem, #tree.items do local textColor, arrowColor, text = tree.colors.default.notExpandable, tree.colors.default.arrow, tree.items[i].expandable and "■ " or "□ " if tree.selectedItem == tree.items[i].definition then textColor, arrowColor = tree.colors.selected.any, tree.colors.selected.arrow buffer.square(tree.x, y, tree.width, 1, tree.colors.selected.background, textColor, " ") else if tree.items[i].expandable then textColor = tree.colors.default.expandable elseif tree.items[i].disabled then textColor = tree.colors.disabled end end if tree.items[i].expandable then buffer.text(tree.x + tree.items[i].offset, y, arrowColor, tree.expandedItems[tree.items[i].definition] and "▽" or "▷") end buffer.text(tree.x + tree.items[i].offset + 2, y, textColor, unicode.sub(text .. tree.items[i].name, 1, textLimit - tree.items[i].offset - 2)) y = y + 1 if y > yEnd then break end end if showScrollBar then local scrollBar = tree.scrollBar scrollBar.x = tree.x + tree.width - 1 scrollBar.y = tree.y scrollBar.width = 1 scrollBar.height = tree.height scrollBar.colors.background = tree.colors.scrollBar.background scrollBar.colors.foreground = tree.colors.scrollBar.foreground scrollBar.minimumValue = 1 scrollBar.maximumValue = #tree.items scrollBar.value = tree.fromItem scrollBar.shownValueCount = tree.height scrollBar.onScrollValueIncrement = 1 scrollBar.thin = true scrollBar:draw() end return tree end local function treeEventHandler(mainContainer, tree, eventData) if eventData[1] == "touch" then local i = eventData[4] - tree.y + tree.fromItem if tree.items[i] then if tree.items[i].expandable and ( tree.selectionMode == GUI.filesystemModes.file or eventData[3] >= tree.x + tree.items[i].offset - 1 and eventData[3] <= tree.x + tree.items[i].offset + 1 ) then if tree.expandedItems[tree.items[i].definition] then tree.expandedItems[tree.items[i].definition] = nil else tree.expandedItems[tree.items[i].definition] = true end callMethod(tree.onItemExpanded, tree.selectedItem, eventData) else if ( ( tree.selectionMode == GUI.filesystemModes.both or tree.selectionMode == GUI.filesystemModes.directory and tree.items[i].expandable or tree.selectionMode == GUI.filesystemModes.file ) and not tree.items[i].disabled ) then tree.selectedItem = tree.items[i].definition callMethod(tree.onItemSelected, tree.selectedItem, eventData) end end mainContainer:draw() buffer.draw() end elseif eventData[1] == "scroll" then if eventData[5] == 1 then if tree.fromItem > 1 then tree.fromItem = tree.fromItem - 1 mainContainer:draw() buffer.draw() end else if tree.fromItem < #tree.items then tree.fromItem = tree.fromItem + 1 mainContainer:draw() buffer.draw() end end end end local function treeAddItem(tree, name, definition, offset, expandable, disabled) table.insert(tree.items, {name = name, expandable = expandable, offset = offset or 0, definition = definition, disabled = disabled}) return tree end function GUI.tree(x, y, width, height, backgroundColor, expandableColor, notExpandableColor, arrowColor, backgroundSelectedColor, anySelectionColor, arrowSelectionColor, disabledColor, scrollBarBackground, scrollBarForeground, showMode, selectionMode) local tree = GUI.container(x, y, width, height) tree.eventHandler = treeEventHandler tree.colors = { default = { background = backgroundColor, expandable = expandableColor, notExpandable = notExpandableColor, arrow = arrowColor, }, selected = { background = backgroundSelectedColor, any = anySelectionColor, arrow = arrowSelectionColor, }, scrollBar = { background = scrollBarBackground, foreground = scrollBarForeground }, disabled = disabledColor } tree.items = {} tree.fromItem = 1 tree.selectedItem = nil tree.expandedItems = {} tree.scrollBar = GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1) tree.showMode = showMode tree.selectionMode = selectionMode tree.eventHandler = treeEventHandler tree.addItem = treeAddItem tree.draw = treeDraw return tree end ----------------------------------------- FilesystemTree object ----------------------------------------- local function filesystemTreeUpdateFileListRecursively(tree, path, offset) local list = {} for file in fs.list(path) do table.insert(list, file) end local i, expandables = 1, {} while i <= #list do if fs.isDirectory(path .. list[i]) then table.insert(expandables, list[i]) table.remove(list, i) else i = i + 1 end end table.sort(expandables, function(a, b) return unicode.lower(a) < unicode.lower(b) end) table.sort(list, function(a, b) return unicode.lower(a) < unicode.lower(b) end) if tree.showMode == GUI.filesystemModes.both or tree.showMode == GUI.filesystemModes.directory then for i = 1, #expandables do tree:addItem(fs.name(expandables[i]), path .. expandables[i], offset, true) if tree.expandedItems[path .. expandables[i]] then filesystemTreeUpdateFileListRecursively(tree, path .. expandables[i], offset + 2) end end end if tree.showMode == GUI.filesystemModes.both or tree.showMode == GUI.filesystemModes.file then for i = 1, #list do tree:addItem(list[i], path .. list[i], offset, false,tree.extensionFilters and not tree.extensionFilters[fs.extension(path .. list[i], true)] or false) end end end local function filesystemTreeUpdateFileList(tree) tree.items = {} filesystemTreeUpdateFileListRecursively(tree, tree.workPath, 1) end local function filesystemTreeAddExtensionFilter(tree, extensionFilter) tree.extensionFilters = tree.extensionFilters or {} tree.extensionFilters[unicode.lower(extensionFilter)] = true end function GUI.filesystemTree(...) local tree = GUI.tree(...) tree.workPath = "/" tree.updateFileList = filesystemTreeUpdateFileList tree.addExtensionFilter = filesystemTreeAddExtensionFilter tree.onItemExpanded = function() tree:updateFileList() end return tree end ----------------------------------------- Text Box object ----------------------------------------- local function textBoxCalculate(object) local doubleVerticalOffset = object.offset.vertical * 2 object.textWidth = object.width - object.offset.horizontal * 2 - (object.scrollBarEnabled and 1 or 0) object.linesCopy = {} if object.autoWrap then for i = 1, #object.lines do local isTable = type(object.lines[i]) == "table" for subLine in (isTable and object.lines[i].text or object.lines[i]):gmatch("[^\n]+") do local wrappedLine = string.wrap(subLine, object.textWidth) for j = 1, #wrappedLine do table.insert(object.linesCopy, isTable and {text = wrappedLine[j], color = object.lines[i].color} or wrappedLine[j]) end end end else for i = 1, #object.lines do table.insert(object.linesCopy, object.lines[i]) end end if object.autoHeight then object.height = #object.linesCopy + doubleVerticalOffset end object.textHeight = object.height - doubleVerticalOffset end local function textBoxDraw(object) textBoxCalculate(object) if object.colors.background then buffer.square(object.x, object.y, object.width, object.height, object.colors.background, object.colors.text, " ", object.colors.transparency) end local x, y = nil, object.y + object.offset.vertical local lineType, text, textColor for i = object.currentLine, object.currentLine + object.textHeight - 1 do if object.linesCopy[i] then lineType = type(object.linesCopy[i]) if lineType == "string" then text, textColor = string.limit(object.linesCopy[i], object.textWidth), object.colors.text elseif lineType == "table" then text, textColor = string.limit(object.linesCopy[i].text, object.textWidth), object.linesCopy[i].color else error("Unknown TextBox line type: " .. tostring(lineType)) end x = GUI.getAlignmentCoordinates( { x = object.x + object.offset.horizontal, y = 1, width = object.textWidth, height = 1, alignment = object.alignment }, { width = unicode.len(text), height = 1 } ) buffer.text(x, y, textColor, text) y = y + 1 else break end end if object.scrollBarEnabled and object.textHeight < #object.lines then object.scrollBar.x = object.x + object.width - 1 object.scrollBar.y = object.y object.scrollBar.height = object.height object.scrollBar.maximumValue = #object.lines - object.textHeight + 1 object.scrollBar.value = object.currentLine object.scrollBar.shownValueCount = object.textHeight object.scrollBar:draw() end return object end local function scrollDownTextBox(object, count) count = math.min(count or 1, #object.lines - object.height - object.currentLine + object.offset.vertical * 2 + 1) if #object.lines >= object.height and object.currentLine < #object.lines - count then object.currentLine = object.currentLine + count end return object end local function scrollUpTextBox(object, count) count = count or 1 if object.currentLine > count and object.currentLine >= 1 then object.currentLine = object.currentLine - count end return object end local function scrollToStartTextBox(object) object.currentLine = 1 return object end local function scrollToEndTextBox(object) if #object.lines > object.textHeight then object.currentLine = #object.lines - object.textHeight + 1 end return object end local function textBoxScrollEventHandler(mainContainer, object, eventData) if eventData[1] == "scroll" then if eventData[5] == 1 then object:scrollUp() mainContainer:draw() buffer.draw() else object:scrollDown() mainContainer:draw() buffer.draw() end end end function GUI.textBox(x, y, width, height, backgroundColor, textColor, lines, currentLine, horizontalOffset, verticalOffset, autoWrap, autoHeight) local object = GUI.object(x, y, width, height) object.eventHandler = textBoxScrollEventHandler object.colors = { text = textColor, background = backgroundColor } object.setAlignment = GUI.setAlignment object:setAlignment(GUI.alignment.horizontal.left, GUI.alignment.vertical.top) object.lines = lines object.currentLine = currentLine or 1 object.draw = textBoxDraw object.scrollUp = scrollUpTextBox object.scrollDown = scrollDownTextBox object.scrollToStart = scrollToStartTextBox object.scrollToEnd = scrollToEndTextBox object.offset = {horizontal = horizontalOffset or 0, vertical = verticalOffset or 0} object.autoWrap = autoWrap object.autoHeight = autoHeight object.scrollBar = GUI.scrollBar(1, 1, 1, 1, 0xC3C3C3, 0x444444, 1, 1, 1, 1, 1, true) object.scrollBarEnabled = false textBoxCalculate(object) return object end ----------------------------------------- Input object ----------------------------------------- local function inputSetCursorPosition(input, newPosition) if newPosition < 1 then newPosition = 1 elseif newPosition > unicode.len(input.text) + 1 then newPosition = unicode.len(input.text) + 1 end if newPosition > input.textCutFrom + input.width - 1 - input.textOffset * 2 then input.textCutFrom = input.textCutFrom + newPosition - (input.textCutFrom + input.width - 1 - input.textOffset * 2) elseif newPosition < input.textCutFrom then input.textCutFrom = newPosition end input.cursorPosition = newPosition return input end local function inputTextDrawMethod(x, y, color, text) buffer.text(x, y, color, text) end local function inputDraw(input) local background, foreground, transparency, text = input.colors.default.background, input.colors.default.text, input.colors.default.transparency, input.text if input.focused then background, foreground, transparency = input.colors.focused.background, input.colors.focused.text, input.colors.focused.transparency elseif input.text == "" then input.textCutFrom = 1 text, foreground = input.placeholderText or "", input.colors.placeholderText end if background then buffer.square(input.x, input.y, input.width, input.height, background, foreground, " ", transparency) end local y = input.y + math.floor(input.height / 2) input.textDrawMethod( input.x + input.textOffset, y, foreground, unicode.sub( input.textMask and string.rep(input.textMask, unicode.len(text)) or text, input.textCutFrom, input.textCutFrom + input.width - 1 - input.textOffset * 2 ) ) if input.cursorBlinkState then local index = buffer.getIndex(input.x + input.cursorPosition - input.textCutFrom + input.textOffset, y) local background = buffer.rawGet(index) buffer.rawSet(index, background, input.colors.cursor, input.cursorSymbol) end if input.autoCompleteEnabled then input.autoComplete.x = input.x if input.autoCompleteVerticalAlignment == GUI.alignment.vertical.top then input.autoComplete.y = input.y - input.autoComplete.height else input.autoComplete.y = input.y + input.height end input.autoComplete.width = input.width input.autoComplete:draw() end end local function inputEventHandler(mainContainer, input, mainEventData) if mainEventData[1] == "touch" then local textOnStart = input.text input.focused = true if input.historyEnabled then input.historyIndex = input.historyIndex + 1 end if input.eraseTextOnFocus then input.text = "" end input.cursorBlinkState = true input:setCursorPosition(unicode.len(input.text) + 1) if input.autoCompleteEnabled then input.autoCompleteMatchMethod() end mainContainer:draw() buffer.draw() while true do local eventData = { event.pull(input.cursorBlinkDelay) } if eventData[1] == "touch" or eventData[1] == "drag" then if input:isClicked(eventData[3], eventData[4]) then input:setCursorPosition(input.textCutFrom + eventData[3] - input.x - input.textOffset) input.cursorBlinkState = true mainContainer:draw() buffer.draw() elseif input.autoComplete:isClicked(eventData[3], eventData[4]) then input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData) else input.cursorBlinkState = false break end elseif eventData[1] == "scroll" then input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData) elseif eventData[1] == "key_down" then -- Return if eventData[4] == 28 then if input.autoCompleteEnabled and input.autoComplete.itemCount > 0 then input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData) else if input.historyEnabled then -- Очистка истории for i = 1, (#input.history - input.historyLimit) do table.remove(input.history, 1) end -- Добавление введенных данных в историю if input.history[#input.history] ~= input.text and unicode.len(input.text) > 0 then table.insert(input.history, input.text) end input.historyIndex = #input.history end input.cursorBlinkState = false break end -- Arrows up/down/left/right elseif eventData[4] == 200 then if input.autoCompleteEnabled and input.autoComplete.selectedItem > 1 then input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData) else if input.historyEnabled and #input.history > 0 then -- Добавление уже введенного текста в историю при стрелке вверх if input.historyIndex == #input.history + 1 and unicode.len(input.text) > 0 then input.history[input.historyIndex] = input.text end input.historyIndex = input.historyIndex - 1 if input.historyIndex > #input.history then input.historyIndex = #input.history elseif input.historyIndex < 1 then input.historyIndex = 1 end input.text = input.history[input.historyIndex] input:setCursorPosition(unicode.len(input.text) + 1) if input.autoCompleteEnabled then input.autoCompleteMatchMethod() end end end elseif eventData[4] == 208 then if input.autoCompleteEnabled and input.historyIndex == #input.history + 1 then input.autoComplete.eventHandler(mainContainer, input.autoComplete, eventData) else if input.historyEnabled and #input.history > 0 then input.historyIndex = input.historyIndex + 1 if input.historyIndex > #input.history then input.historyIndex = #input.history elseif input.historyIndex < 1 then input.historyIndex = 1 end input.text = input.history[input.historyIndex] input:setCursorPosition(unicode.len(input.text) + 1) if input.autoCompleteEnabled then input.autoCompleteMatchMethod() end end end elseif eventData[4] == 203 then input:setCursorPosition(input.cursorPosition - 1) elseif eventData[4] == 205 then input:setCursorPosition(input.cursorPosition + 1) -- Backspace elseif eventData[4] == 14 then input.text = unicode.sub(unicode.sub(input.text, 1, input.cursorPosition - 1), 1, -2) .. unicode.sub(input.text, input.cursorPosition, -1) input:setCursorPosition(input.cursorPosition - 1) if input.autoCompleteEnabled then input.autoCompleteMatchMethod() end -- Delete elseif eventData[4] == 211 then input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. unicode.sub(input.text, input.cursorPosition + 1, -1) if input.autoCompleteEnabled then input.autoCompleteMatchMethod() end else local char = unicode.char(eventData[3]) if not keyboard.isControl(eventData[3]) then input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. char .. unicode.sub(input.text, input.cursorPosition, -1) input:setCursorPosition(input.cursorPosition + 1) if input.autoCompleteEnabled then input.autoCompleteMatchMethod() end end end input.cursorBlinkState = true mainContainer:draw() buffer.draw() elseif eventData[1] == "clipboard" then input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. eventData[3] .. unicode.sub(input.text, input.cursorPosition, -1) input:setCursorPosition(input.cursorPosition + unicode.len(eventData[3])) input.cursorBlinkState = true mainContainer:draw() buffer.draw() elseif not eventData[1] then input.cursorBlinkState = not input.cursorBlinkState mainContainer:draw() buffer.draw() end end input.focused = false if input.autoCompleteEnabled then input.autoComplete:clear() end if input.validator then if not input.validator(input.text) then input.text = textOnStart input:setCursorPosition(unicode.len(input.text) + 1) end end callMethod(input.onInputFinished, mainContainer, input, mainEventData, input.text) mainContainer:draw() buffer.draw() end end function GUI.input(x, y, width, height, backgroundColor, textColor, placeholderTextColor, backgroundFocusedColor, textFocusedColor, text, placeholderText, eraseTextOnFocus, textMask) local input = GUI.object(x, y, width, height) input.colors = { default = { background = backgroundColor, text = textColor }, focused = { background = backgroundFocusedColor, text = textFocusedColor }, placeholderText = placeholderTextColor, cursor = 0x00A8FF } input.text = text or "" input.placeholderText = placeholderText input.eraseTextOnFocus = eraseTextOnFocus input.textMask = textMask input.textOffset = 1 input.textCutFrom = 1 input.cursorPosition = 1 input.cursorSymbol = "┃" input.cursorBlinkDelay = 0.4 input.cursorBlinkState = false input.textMask = textMask input.setCursorPosition = inputSetCursorPosition input.history = {} input.historyLimit = 20 input.historyIndex = 0 input.historyEnabled = false input.textDrawMethod = inputTextDrawMethod input.draw = inputDraw input.eventHandler = inputEventHandler input.autoComplete = GUI.autoComplete(1, 1, 30, 7, 0xE1E1E1, 0x999999, 0x3C3C3C, 0x3C3C3C, 0x999999, 0xE1E1E1, 0xC3C3C3, 0x444444) input.autoCompleteEnabled = false input.autoCompleteVerticalAlignment = GUI.alignment.vertical.bottom return input end ----------------------------------------------------------------------------------------------------- local function autoCompleteDraw(object) local y, yEnd = object.y, object.y + object.height - 1 buffer.square(object.x, object.y, object.width, object.height, object.colors.default.background, object.colors.default.text, " ") for i = object.fromItem, object.itemCount do local textColor, textMatchColor = object.colors.default.text, object.colors.default.textMatch if i == object.selectedItem then buffer.square(object.x, y, object.width, 1, object.colors.selected.background, object.colors.selected.text, " ") textColor, textMatchColor = object.colors.selected.text, object.colors.selected.textMatch end buffer.text(object.x + 1, y, textMatchColor, object.matchText) buffer.text(object.x + 1 + object.matchTextLength, y, textColor, unicode.sub(object.items[i], object.matchTextLength + 1, object.width - 2 - object.matchTextLength)) y = y + 1 if y > yEnd then break end end if object.itemCount > object.height then object.scrollBar.x = object.x + object.width - 1 object.scrollBar.y = object.y object.scrollBar.height = object.height object.scrollBar.maximumValue = object.itemCount - object.height + 1 object.scrollBar.value = object.fromItem object.scrollBar.shownValueCount = object.height object.scrollBar:draw() end end local function autoCompleteScroll(mainContainer, object, direction) if object.itemCount >= object.height then object.fromItem = object.fromItem + direction if object.fromItem < 1 then object.fromItem = 1 elseif object.fromItem > object.itemCount - object.height + 1 then object.fromItem = object.itemCount - object.height + 1 end end end local function autoCompleteEventHandler(mainContainer, object, eventData) if eventData[1] == "touch" then object.selectedItem = eventData[4] - object.y + object.fromItem mainContainer:draw() buffer.draw() callMethod(object.onItemSelected, mainContainer, object, eventData, object.selectedItem) elseif eventData[1] == "scroll" then autoCompleteScroll(mainContainer, object, -eventData[5]) mainContainer:draw() buffer.draw() elseif eventData[1] == "key_down" then if eventData[4] == 28 then callMethod(object.onItemSelected, mainContainer, object, eventData, object.selectedItem) elseif eventData[4] == 200 then object.selectedItem = object.selectedItem - 1 if object.selectedItem < 1 then object.selectedItem = 1 end if object.selectedItem == object.fromItem - 1 then autoCompleteScroll(mainContainer, object, -1) end mainContainer:draw() buffer.draw() elseif eventData[4] == 208 then object.selectedItem = object.selectedItem + 1 if object.selectedItem > object.itemCount then object.selectedItem = object.itemCount end if object.selectedItem == object.fromItem + object.height then autoCompleteScroll(mainContainer, object, 1) end mainContainer:draw() buffer.draw() end end end local function autoCompleteClear(object) object.items = {} object.itemCount = 0 object.fromItem = 1 object.selectedItem = 1 object.height = 0 end local function autoCompleteMatch(object, variants, text) object:clear() if text then for i = 1, #variants do if variants[i] ~= text and variants[i]:match("^" .. text) then table.insert(object.items, variants[i]) end end else for i = 1, #variants do table.insert(object.items, variants[i]) end end object.matchText = text or "" object.matchTextLength = unicode.len(object.matchText) table.sort(object.items, function(a, b) return unicode.lower(a) < unicode.lower(b) end) object.itemCount = #object.items object.height = math.min(object.itemCount, object.maximumHeight) return object end function GUI.autoComplete(x, y, width, maximumHeight, backgroundColor, textColor, textMatchColor, backgroundSelectedColor, textSelectedColor, textMatchSelectedColor, scrollBarBackground, scrollBarForeground) local object = GUI.object(x, y, width, maximumHeight) object.colors = { default = { background = backgroundColor, text = textColor, textMatch = textMatchColor }, selected = { background = backgroundSelectedColor, text = textSelectedColor, textMatch = textMatchSelectedColor } } object.maximumHeight = maximumHeight object.fromItem = 1 object.selectedItem = 1 object.items = {} object.matchText = " " object.matchTextLength = 1 object.itemCount = 0 object.scrollBar = GUI.scrollBar(1, 1, 1, 1, scrollBarBackground, scrollBarForeground, 1, 1, 1, 1, 1, true) object.match = autoCompleteMatch object.draw = autoCompleteDraw object.eventHandler = autoCompleteEventHandler object.clear = autoCompleteClear object:clear() return object end ----------------------------------------------------------------------------------------------------- -- buffer.clear() -- buffer.draw(true) -- -- Создаем полноэкранный контейнер, добавляем на него загруженное изображение и полупрозрачную черную панель -- local mainContainer = GUI.fullScreenContainer() -- mainContainer:addChild(GUI.image(1, 1, image.load("/MineOS/Pictures/Raspberry.pic"))) -- local filesystemDialog = GUI.addFilesystemDialogToContainer(mainContainer, "Save", "Cancel", "File name", "/") -- filesystemDialog:setMode(GUI.filesystemModes.save, GUI.filesystemModes.file) -- filesystemDialog:addExtensionFilter(".pic") -- filesystemDialog:show() -- filesystemDialog.onSubmit = function(path) -- GUI.error(path) -- end -- mainContainer:draw() -- buffer.draw(true) -- mainContainer:startEventHandling() ----------------------------------------------------------------------------------------------------- return GUI