├── .gitignore ├── GUI.lua ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /GUI.lua: -------------------------------------------------------------------------------- 1 | 2 | require("advancedLua") 3 | local component = require("component") 4 | local computer = require("computer") 5 | local keyboard = require("keyboard") 6 | local filesystem = require("filesystem") 7 | local unicode = require("unicode") 8 | local event = require("event") 9 | local color = require("color") 10 | local image = require("image") 11 | local buffer = require("doubleBuffering") 12 | 13 | ----------------------------------------------------------------------------------------- 14 | 15 | local GUI = { 16 | ALIGNMENT_HORIZONTAL_LEFT = 1, 17 | ALIGNMENT_HORIZONTAL_CENTER = 2, 18 | ALIGNMENT_HORIZONTAL_RIGHT = 3, 19 | ALIGNMENT_VERTICAL_TOP = 4, 20 | ALIGNMENT_VERTICAL_CENTER = 5, 21 | ALIGNMENT_VERTICAL_BOTTOM = 6, 22 | 23 | DIRECTION_HORIZONTAL = 7, 24 | DIRECTION_VERTICAL = 8, 25 | 26 | SIZE_POLICY_ABSOLUTE = 9, 27 | SIZE_POLICY_RELATIVE = 10, 28 | 29 | IO_MODE_FILE = 11, 30 | IO_MODE_DIRECTORY = 12, 31 | IO_MODE_BOTH = 13, 32 | IO_MODE_OPEN = 14, 33 | IO_MODE_SAVE = 15, 34 | 35 | BUTTON_PRESS_DURATION = 0.2, 36 | BUTTON_ANIMATION_DURATION = 0.2, 37 | SWITCH_ANIMATION_DURATION = 0.3, 38 | FILESYSTEM_DIALOG_ANIMATION_DURATION = 0.5, 39 | 40 | CONTEXT_MENU_SEPARATOR_COLOR = 0x878787, 41 | CONTEXT_MENU_DEFAULT_TEXT_COLOR = 0x2D2D2D, 42 | CONTEXT_MENU_DEFAULT_BACKGROUND_COLOR = 0xFFFFFF, 43 | CONTEXT_MENU_PRESSED_BACKGROUND_COLOR = 0x3366CC, 44 | CONTEXT_MENU_PRESSED_TEXT_COLOR = 0xFFFFFF, 45 | CONTEXT_MENU_DISABLED_COLOR = 0x878787, 46 | CONTEXT_MENU_BACKGROUND_TRANSPARENCY = 0.18, 47 | CONTEXT_MENU_SHADOW_TRANSPARENCY = 0.4, 48 | 49 | BACKGROUND_CONTAINER_PANEL_COLOR = 0x0, 50 | BACKGROUND_CONTAINER_TITLE_COLOR = 0xE1E1E1, 51 | BACKGROUND_CONTAINER_PANEL_TRANSPARENCY = 0.3, 52 | 53 | WINDOW_BACKGROUND_PANEL_COLOR = 0xF0F0F0, 54 | WINDOW_SHADOW_TRANSPARENCY = 0.6, 55 | WINDOW_TITLE_BACKGROUND_COLOR = 0xE1E1E1, 56 | WINDOW_TITLE_TEXT_COLOR = 0x2D2D2D, 57 | WINDOW_TAB_BAR_DEFAULT_BACKGROUND_COLOR = 0x2D2D2D, 58 | WINDOW_TAB_BAR_DEFAULT_TEXT_COLOR = 0xF0F0F0, 59 | WINDOW_TAB_BAR_SELECTED_BACKGROUND_COLOR = 0xF0F0F0, 60 | WINDOW_TAB_BAR_SELECTED_TEXT_COLOR = 0x2D2D2D, 61 | 62 | PALETTE_CONFIG_PATH = filesystem.path(getCurrentScript()) .. ".palette.cfg", 63 | 64 | LUA_SYNTAX_COLOR_SCHEME = { 65 | background = 0x1E1E1E, 66 | text = 0xE1E1E1, 67 | strings = 0x99FF80, 68 | loops = 0xFFFF98, 69 | comments = 0x898989, 70 | boolean = 0xFFDB40, 71 | logic = 0xFFCC66, 72 | numbers = 0x66DBFF, 73 | functions = 0xFFCC66, 74 | compares = 0xFFCC66, 75 | lineNumbersBackground = 0x2D2D2D, 76 | lineNumbersText = 0xC3C3C3, 77 | scrollBarBackground = 0x2D2D2D, 78 | scrollBarForeground = 0x5A5A5A, 79 | selection = 0x4B4B4B, 80 | indentation = 0x2D2D2D 81 | }, 82 | 83 | LUA_SYNTAX_PATTERNS = { 84 | "[%.%,%>%<%=%~%+%-%*%/%^%#%%%&]", "compares", 0, 0, 85 | "[^%a%d][%.%d]+[^%a%d]", "numbers", 1, 1, 86 | "[^%a%d][%.%d]+$", "numbers", 1, 0, 87 | "0x%w+", "numbers", 0, 0, 88 | " not ", "logic", 0, 1, 89 | " or ", "logic", 0, 1, 90 | " and ", "logic", 0, 1, 91 | "function%(", "functions", 0, 1, 92 | "function%s[^%s%(%)%{%}%[%]]+%(", "functions", 9, 1, 93 | "nil", "boolean", 0, 0, 94 | "false", "boolean", 0, 0, 95 | "true", "boolean", 0, 0, 96 | " break$", "loops", 0, 0, 97 | "elseif ", "loops", 0, 1, 98 | "else[%s%;]", "loops", 0, 1, 99 | "else$", "loops", 0, 0, 100 | "function ", "loops", 0, 1, 101 | "local ", "loops", 0, 1, 102 | "return", "loops", 0, 0, 103 | "until ", "loops", 0, 1, 104 | "then", "loops", 0, 0, 105 | "if ", "loops", 0, 1, 106 | "repeat$", "loops", 0, 0, 107 | " in ", "loops", 0, 1, 108 | "for ", "loops", 0, 1, 109 | "end[%s%;]", "loops", 0, 1, 110 | "end$", "loops", 0, 0, 111 | "do ", "loops", 0, 1, 112 | "do$", "loops", 0, 0, 113 | "while ", "loops", 0, 1, 114 | "\'[^\']+\'", "strings", 0, 0, 115 | "\"[^\"]+\"", "strings", 0, 0, 116 | "%-%-.+", "comments", 0, 0, 117 | }, 118 | } 119 | 120 | -------------------------------------------------------------------------------- 121 | 122 | function GUI.setAlignment(object, horizontalAlignment, verticalAlignment) 123 | object.horizontalAlignment, object.verticalAlignment = horizontalAlignment, verticalAlignment 124 | 125 | return object 126 | end 127 | 128 | function GUI.getAlignmentCoordinates(x, y, width1, height1, horizontalAlignment, verticalAlignment, width2, height2) 129 | if horizontalAlignment == GUI.ALIGNMENT_HORIZONTAL_CENTER then 130 | x = x + width1 / 2 - width2 / 2 131 | elseif horizontalAlignment == GUI.ALIGNMENT_HORIZONTAL_RIGHT then 132 | x = x + width1 - width2 133 | elseif horizontalAlignment ~= GUI.ALIGNMENT_HORIZONTAL_LEFT then 134 | error("Unknown horizontal alignment: " .. tostring(horizontalAlignment)) 135 | end 136 | 137 | if verticalAlignment == GUI.ALIGNMENT_VERTICAL_CENTER then 138 | y = y + height1 / 2 - height2 / 2 139 | elseif verticalAlignment == GUI.ALIGNMENT_VERTICAL_BOTTOM then 140 | y = y + height1 - height2 141 | elseif verticalAlignment ~= GUI.ALIGNMENT_VERTICAL_TOP then 142 | error("Unknown vertical alignment: " .. tostring(verticalAlignment)) 143 | end 144 | 145 | return x, y 146 | end 147 | 148 | function GUI.getMarginCoordinates(x, y, horizontalAlignment, verticalAlignment, horizontalMargin, verticalMargin) 149 | if horizontalAlignment == GUI.ALIGNMENT_HORIZONTAL_RIGHT then 150 | x = x - horizontalMargin 151 | else 152 | x = x + horizontalMargin 153 | end 154 | 155 | if verticalAlignment == GUI.ALIGNMENT_VERTICAL_BOTTOM then 156 | y = y - verticalMargin 157 | else 158 | y = y + verticalMargin 159 | end 160 | 161 | return x, y 162 | end 163 | 164 | -------------------------------------------------------------------------------- 165 | 166 | local function objectDraw(object) 167 | return object 168 | end 169 | 170 | function GUI.object(x, y, width, height) 171 | return { 172 | x = x, 173 | y = y, 174 | width = width, 175 | height = height, 176 | draw = objectDraw 177 | } 178 | end 179 | 180 | -------------------------------------------------------------------------------- 181 | 182 | local function containerObjectIndexOf(object) 183 | if not object.parent then error("Object doesn't have a parent container") end 184 | 185 | for objectIndex = 1, #object.parent.children do 186 | if object.parent.children[objectIndex] == object then 187 | return objectIndex 188 | end 189 | end 190 | end 191 | 192 | local function containerObjectMoveForward(object) 193 | local objectIndex = containerObjectIndexOf(object) 194 | if objectIndex < #object.parent.children then 195 | object.parent.children[index], object.parent.children[index + 1] = object.parent.children[index + 1], object.parent.children[index] 196 | end 197 | 198 | return object 199 | end 200 | 201 | local function containerObjectMoveBackward(object) 202 | local objectIndex = containerObjectIndexOf(object) 203 | if objectIndex > 1 then 204 | object.parent.children[objectIndex], object.parent.children[objectIndex - 1] = object.parent.children[objectIndex - 1], object.parent.children[objectIndex] 205 | end 206 | 207 | return object 208 | end 209 | 210 | local function containerObjectMoveToFront(object) 211 | table.remove(object.parent.children, containerObjectIndexOf(object)) 212 | table.insert(object.parent.children, object) 213 | 214 | return object 215 | end 216 | 217 | local function containerObjectMoveToBack(object) 218 | table.remove(object.parent.children, containerObjectIndexOf(object)) 219 | table.insert(object.parent.children, 1, object) 220 | 221 | return object 222 | end 223 | 224 | local function containerObjectRemove(object) 225 | table.remove(object.parent.children, containerObjectIndexOf(object)) 226 | end 227 | 228 | local function containerObjectAnimationStart(animation, duration) 229 | animation.position = 0 230 | animation.duration = duration 231 | animation.started = true 232 | animation.startUptime = computer.uptime() 233 | 234 | computer.pushSignal("GUI", "animationStarted") 235 | end 236 | 237 | local function containerObjectAnimationStop(animation) 238 | animation.position = 0 239 | animation.started = false 240 | end 241 | 242 | local function containerObjectAnimationRemove(animation) 243 | animation.removeLater = true 244 | end 245 | 246 | local function containerObjectAddAnimation(object, frameHandler, onFinish) 247 | local animation = { 248 | object = object, 249 | position = 0, 250 | start = containerObjectAnimationStart, 251 | stop = containerObjectAnimationStop, 252 | remove = containerObjectAnimationRemove, 253 | frameHandler = frameHandler, 254 | onFinish = onFinish, 255 | } 256 | 257 | object.firstParent.animations = object.firstParent.animations or {} 258 | table.insert(object.firstParent.animations, animation) 259 | 260 | return animation 261 | end 262 | 263 | local function containerAddChild(container, object, atIndex) 264 | object.localX = object.x 265 | object.localY = object.y 266 | object.indexOf = containerObjectIndexOf 267 | object.moveToFront = containerObjectMoveToFront 268 | object.moveToBack = containerObjectMoveToBack 269 | object.moveForward = containerObjectMoveForward 270 | object.moveBackward = containerObjectMoveBackward 271 | object.remove = containerObjectRemove 272 | object.addAnimation = containerObjectAddAnimation 273 | 274 | local function updateFirstParent(object, firstParent) 275 | object.firstParent = firstParent 276 | if object.children then 277 | for i = 1, #object.children do 278 | updateFirstParent(object.children[i], firstParent) 279 | end 280 | end 281 | end 282 | 283 | object.parent = container 284 | updateFirstParent(object, container.firstParent or container) 285 | 286 | if atIndex then 287 | table.insert(container.children, atIndex, object) 288 | else 289 | table.insert(container.children, object) 290 | end 291 | 292 | return object 293 | end 294 | 295 | local function containerRemoveChildren(container, from, to) 296 | from = from or 1 297 | for objectIndex = from, to or #container.children do 298 | table.remove(container.children, from) 299 | end 300 | end 301 | 302 | local function getRectangleIntersection(R1X1, R1Y1, R1X2, R1Y2, R2X1, R2Y1, R2X2, R2Y2) 303 | if R2X1 <= R1X2 and R2Y1 <= R1Y2 and R2X2 >= R1X1 and R2Y2 >= R1Y1 then 304 | return 305 | math.max(R2X1, R1X1), 306 | math.max(R2Y1, R1Y1), 307 | math.min(R2X2, R1X2), 308 | math.min(R2Y2, R1Y2) 309 | else 310 | return 311 | end 312 | end 313 | 314 | function containerDraw(container) 315 | local R1X1, R1Y1, R1X2, R1Y2, child = buffer.getDrawLimit() 316 | local intersectionX1, intersectionY1, intersectionX2, intersectionY2 = getRectangleIntersection( 317 | R1X1, 318 | R1Y1, 319 | R1X2, 320 | R1Y2, 321 | container.x, 322 | container.y, 323 | container.x + container.width - 1, 324 | container.y + container.height - 1 325 | ) 326 | 327 | if intersectionX1 then 328 | buffer.setDrawLimit(intersectionX1, intersectionY1, intersectionX2, intersectionY2) 329 | 330 | for i = 1, #container.children do 331 | child = container.children[i] 332 | 333 | if not child.hidden then 334 | child.x, child.y = container.x + child.localX - 1, container.y + child.localY - 1 335 | child:draw() 336 | end 337 | end 338 | 339 | buffer.setDrawLimit(R1X1, R1Y1, R1X2, R1Y2) 340 | end 341 | 342 | return container 343 | end 344 | 345 | function GUI.container(x, y, width, height) 346 | local container = GUI.object(x, y, width, height) 347 | 348 | container.children = {} 349 | container.passScreenEvents = true 350 | 351 | container.draw = containerDraw 352 | container.removeChildren = containerRemoveChildren 353 | container.addChild = containerAddChild 354 | 355 | return container 356 | end 357 | 358 | function GUI.fullScreenContainer() 359 | return GUI.container(1, 1, buffer.getResolution()) 360 | end 361 | 362 | -------------------------------------------------------------------------------- 363 | 364 | local function applicationStart(application, eventPullTimeout) 365 | local animation, animationIndex, animationOnFinishMethodsIndex, animationOnFinishMethods, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32 366 | 367 | local function handle(isScreenEvent, currentContainer, intersectionX1, intersectionY1, intersectionX2, intersectionY2) 368 | if 369 | not isScreenEvent or 370 | intersectionX1 and 371 | e3 >= intersectionX1 and 372 | e3 <= intersectionX2 and 373 | e4 >= intersectionY1 and 374 | e4 <= intersectionY2 375 | then 376 | local currentContainerPassed, child, newIntersectionX1, newIntersectionY1, newIntersectionX2, newIntersectionY2 377 | 378 | if isScreenEvent then 379 | if currentContainer.eventHandler and not currentContainer.disabled then 380 | currentContainer.eventHandler(application, currentContainer, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32) 381 | end 382 | 383 | currentContainerPassed = not currentContainer.passScreenEvents 384 | elseif currentContainer.eventHandler then 385 | currentContainer.eventHandler(application, currentContainer, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32) 386 | end 387 | 388 | for i = #currentContainer.children, 1, -1 do 389 | child = currentContainer.children[i] 390 | 391 | if not child.hidden then 392 | if child.children then 393 | newIntersectionX1, newIntersectionY1, newIntersectionX2, newIntersectionY2 = getRectangleIntersection( 394 | intersectionX1, 395 | intersectionY1, 396 | intersectionX2, 397 | intersectionY2, 398 | child.x, 399 | child.y, 400 | child.x + child.width - 1, 401 | child.y + child.height - 1 402 | ) 403 | 404 | if 405 | newIntersectionX1 and 406 | handle( 407 | isScreenEvent, 408 | child, 409 | newIntersectionX1, 410 | newIntersectionY1, 411 | newIntersectionX2, 412 | newIntersectionY2, 413 | e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32 414 | ) 415 | then 416 | return true 417 | end 418 | else 419 | if application.needConsume then 420 | application.needConsume = nil 421 | return true 422 | end 423 | 424 | if isScreenEvent then 425 | if 426 | e3 >= child.x and 427 | e3 <= child.x + child.width - 1 and 428 | e4 >= child.y and 429 | e4 <= child.y + child.height - 1 430 | then 431 | if child.eventHandler and not child.disabled then 432 | child.eventHandler(application, child, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32) 433 | end 434 | 435 | if not child.passScreenEvents then 436 | return true 437 | end 438 | end 439 | elseif child.eventHandler then 440 | child.eventHandler(application, child, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32) 441 | end 442 | end 443 | end 444 | end 445 | 446 | if currentContainerPassed then 447 | return true 448 | end 449 | end 450 | end 451 | 452 | application.eventPullTimeout = eventPullTimeout 453 | 454 | repeat 455 | e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32 = event.pull(application.animations and 0 or application.eventPullTimeout) 456 | 457 | handle( 458 | e1 == "touch" or 459 | e1 == "drag" or 460 | e1 == "drop" or 461 | e1 == "scroll" or 462 | e1 == "double_touch", 463 | application, 464 | application.x, 465 | application.y, 466 | application.x + application.width - 1, 467 | application.y + application.height - 1 468 | ) 469 | 470 | if application.animations then 471 | animationIndex, animationOnFinishMethodsIndex, animationOnFinishMethods = 1, 1, {} 472 | -- Продрачиваем анимации и вызываем обработчики кадров 473 | while animationIndex <= #application.animations do 474 | animation = application.animations[animationIndex] 475 | 476 | if animation.removeLater then 477 | table.remove(application.animations, animationIndex) 478 | 479 | if #application.animations == 0 then 480 | application.animations = nil 481 | break 482 | end 483 | else 484 | if animation.started then 485 | animation.position = (computer.uptime() - animation.startUptime) / animation.duration 486 | 487 | if animation.position < 1 then 488 | animation.frameHandler(animation) 489 | else 490 | animation.position, animation.started = 1, false 491 | animation.frameHandler(animation) 492 | 493 | animationOnFinishMethods[animationOnFinishMethodsIndex] = animation 494 | animationOnFinishMethodsIndex = animationOnFinishMethodsIndex + 1 495 | end 496 | end 497 | 498 | animationIndex = animationIndex + 1 499 | end 500 | end 501 | 502 | -- По завершению продрочки отрисовываем изменения на экране 503 | application:draw() 504 | 505 | -- Вызываем поочередно все методы .onFinish 506 | for i = 1, #animationOnFinishMethods do 507 | animationOnFinishMethods[i].onFinish(animationOnFinishMethods[i]) 508 | end 509 | end 510 | until application.needClose 511 | 512 | application.needClose = nil 513 | end 514 | 515 | local function applicationStop(application) 516 | application.needClose = true 517 | end 518 | 519 | local function applicationConsumeEvent(application) 520 | application.needConsume = true 521 | end 522 | 523 | local function applicationDraw(object, ...) 524 | containerDraw(object) 525 | buffer.drawChanges(...) 526 | end 527 | 528 | function GUI.application(x, y, width, height) 529 | local application = GUI.container(x or 1, y or 1, width or buffer.getWidth(), height or buffer.getHeight()) 530 | 531 | application.draw = applicationDraw 532 | application.start = applicationStart 533 | application.stop = applicationStop 534 | application.consumeEvent = applicationConsumeEvent 535 | 536 | return application 537 | end 538 | 539 | -------------------------------------------------------------------------------- 540 | 541 | local function pressableDraw(pressable) 542 | local background = pressable.pressed and pressable.colors.pressed.background or pressable.disabled and pressable.colors.disabled.background or pressable.colors.default.background 543 | local text = pressable.pressed and pressable.colors.pressed.text or pressable.disabled and pressable.colors.disabled.text or pressable.colors.default.text 544 | 545 | if background then 546 | buffer.drawRectangle(pressable.x, pressable.y, pressable.width, pressable.height, background, text, " ") 547 | end 548 | buffer.drawText(math.floor(pressable.x + pressable.width / 2 - unicode.len(pressable.text) / 2), math.floor(pressable.y + pressable.height / 2), text, pressable.text) 549 | end 550 | 551 | local function pressableHandlePress(application, pressable, ...) 552 | pressable.pressed = not pressable.pressed 553 | application:draw() 554 | 555 | if not pressable.switchMode then 556 | pressable.pressed = not pressable.pressed 557 | os.sleep(GUI.BUTTON_PRESS_DURATION) 558 | 559 | application:draw() 560 | end 561 | 562 | if pressable.onTouch then 563 | pressable.onTouch(application, pressable, ...) 564 | end 565 | end 566 | 567 | local function pressableEventHandler(application, pressable, e1, ...) 568 | if e1 == "touch" then 569 | pressableHandlePress(application, pressable, e1, ...) 570 | end 571 | end 572 | 573 | local function pressable(x, y, width, height, backgroundColor, textColor, backgroundPressedColor, textPressedColor, backgroundDisabledColor, textDisabledColor, text) 574 | local pressable = GUI.object(x, y, width, height) 575 | 576 | pressable.colors = { 577 | default = { 578 | background = backgroundColor, 579 | text = textColor 580 | }, 581 | pressed = { 582 | background = backgroundPressedColor, 583 | text = textPressedColor 584 | }, 585 | disabled = { 586 | background = backgroundDisabledColor, 587 | text = textDisabledColor 588 | } 589 | } 590 | 591 | pressable.pressed = false 592 | pressable.text = text 593 | pressable.draw = pressableDraw 594 | pressable.eventHandler = pressableEventHandler 595 | 596 | return pressable 597 | end 598 | 599 | -------------------------------------------------------------------------------- 600 | 601 | local function buttonPlayAnimation(button, onFinish) 602 | button.animationStarted = true 603 | button:addAnimation( 604 | function(animation) 605 | if button.pressed then 606 | if button.colors.default.background and button.colors.pressed.background then 607 | button.animationCurrentBackground = color.transition(button.colors.pressed.background, button.colors.default.background, animation.position) 608 | end 609 | button.animationCurrentText = color.transition(button.colors.pressed.text, button.colors.default.text, animation.position) 610 | else 611 | if button.colors.default.background and button.colors.pressed.background then 612 | button.animationCurrentBackground = color.transition(button.colors.default.background, button.colors.pressed.background, animation.position) 613 | end 614 | button.animationCurrentText = color.transition(button.colors.default.text, button.colors.pressed.text, animation.position) 615 | end 616 | end, 617 | function(animation) 618 | button.animationStarted = false 619 | button.pressed = not button.pressed 620 | onFinish(animation) 621 | end 622 | ):start(button.animationDuration) 623 | end 624 | 625 | local function buttonPress(button, application, object, ...) 626 | if button.animated then 627 | local eventData = {...} 628 | 629 | buttonPlayAnimation(button, function(animation) 630 | if button.onTouch then 631 | button.onTouch(application, button, table.unpack(eventData)) 632 | end 633 | 634 | animation:remove() 635 | 636 | if not button.switchMode then 637 | buttonPlayAnimation(button, function(animation) 638 | animation:remove() 639 | end) 640 | end 641 | end) 642 | else 643 | pressableHandlePress(application, button, ...) 644 | end 645 | end 646 | 647 | local function buttonEventHandler(application, button, e1, ...) 648 | if e1 == "touch" and (not button.animated or not button.animationStarted) then 649 | button:press(application, button, e1, ...) 650 | end 651 | end 652 | 653 | local function buttonGetColors(button) 654 | if button.disabled then 655 | return button.colors.disabled.background, button.colors.disabled.text 656 | else 657 | if button.animated and button.animationStarted then 658 | return button.animationCurrentBackground, button.animationCurrentText 659 | else 660 | if button.pressed then 661 | return button.colors.pressed.background, button.colors.pressed.text 662 | else 663 | return button.colors.default.background, button.colors.default.text 664 | end 665 | end 666 | end 667 | end 668 | 669 | local function buttonDrawText(button, textColor) 670 | buffer.drawText(math.floor(button.x + button.width / 2 - unicode.len(button.text) / 2), math.floor(button.y + button.height / 2), textColor, button.text) 671 | end 672 | 673 | local function buttonDraw(button) 674 | local backgroundColor, textColor = buttonGetColors(button) 675 | 676 | if backgroundColor then 677 | buffer.drawRectangle(button.x, button.y, button.width, button.height, backgroundColor, textColor, " ", button.colors.transparency) 678 | end 679 | buttonDrawText(button, textColor) 680 | end 681 | 682 | local function framedButtonDraw(button) 683 | local backgroundColor, textColor = buttonGetColors(button) 684 | 685 | if backgroundColor then 686 | buffer.drawFrame(button.x, button.y, button.width, button.height, backgroundColor) 687 | end 688 | buttonDrawText(button, textColor) 689 | end 690 | 691 | local function roundedButtonDraw(button) 692 | local backgroundColor, textColor = buttonGetColors(button) 693 | 694 | if backgroundColor then 695 | local x2, y2 = button.x + button.width - 1, button.y + button.height - 1 696 | if button.height > 1 then 697 | buffer.drawText(button.x + 1, button.y, backgroundColor, string.rep("▄", button.width - 2)) 698 | buffer.drawText(button.x, button.y, backgroundColor, "⣠") 699 | buffer.drawText(x2, button.y, backgroundColor, "⣄") 700 | 701 | buffer.drawRectangle(button.x, button.y + 1, button.width, button.height - 2, backgroundColor, textColor, " ") 702 | 703 | buffer.drawText(button.x + 1, y2, backgroundColor, string.rep("▀", button.width - 2)) 704 | buffer.drawText(button.x, y2, backgroundColor, "⠙") 705 | buffer.drawText(x2, y2, backgroundColor, "⠋") 706 | else 707 | buffer.drawRectangle(button.x, button.y, button.width, button.height, backgroundColor, textColor, " ") 708 | GUI.roundedCorners(button.x, button.y, button.width, button.height, backgroundColor) 709 | end 710 | end 711 | 712 | buttonDrawText(button, textColor) 713 | end 714 | 715 | local function tagButtonDraw(button) 716 | local backgroundColor, textColor = buttonGetColors(button) 717 | 718 | buffer.drawRectangle(button.x, button.y, button.width, button.height, backgroundColor, textColor, " ") 719 | buffer.drawText(button.x - 1, button.y, backgroundColor, "◀") 720 | buttonDrawText(button, textColor) 721 | end 722 | 723 | local function buttonCreate(x, y, width, height, backgroundColor, textColor, backgroundPressedColor, textPressedColor, text) 724 | local button = pressable(x, y, width, height, backgroundColor, textColor, backgroundPressedColor, textPressedColor, 0x878787, 0xA5A5A5, text) 725 | 726 | button.animationDuration = GUI.BUTTON_ANIMATION_DURATION 727 | button.animated = true 728 | 729 | button.animationCurrentBackground = backgroundColor 730 | button.animationCurrentText = textColor 731 | 732 | button.press = buttonPress 733 | button.eventHandler = buttonEventHandler 734 | 735 | return button 736 | end 737 | 738 | local function adaptiveButtonCreate(x, y, xOffset, yOffset, backgroundColor, textColor, backgroundPressedColor, textPressedColor, text) 739 | return buttonCreate(x, y, unicode.len(text) + xOffset * 2, yOffset * 2 + 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, text) 740 | end 741 | 742 | function GUI.button(...) 743 | local button = buttonCreate(...) 744 | button.draw = buttonDraw 745 | 746 | return button 747 | end 748 | 749 | function GUI.adaptiveButton(...) 750 | local button = adaptiveButtonCreate(...) 751 | button.draw = buttonDraw 752 | 753 | return button 754 | end 755 | 756 | function GUI.framedButton(...) 757 | local button = buttonCreate(...) 758 | button.draw = framedButtonDraw 759 | 760 | return button 761 | end 762 | 763 | function GUI.adaptiveFramedButton(...) 764 | local button = adaptiveButtonCreate(...) 765 | button.draw = framedButtonDraw 766 | 767 | return button 768 | end 769 | 770 | function GUI.roundedButton(...) 771 | local button = buttonCreate(...) 772 | button.draw = roundedButtonDraw 773 | 774 | return button 775 | end 776 | 777 | function GUI.adaptiveRoundedButton(...) 778 | local button = adaptiveButtonCreate(...) 779 | button.draw = roundedButtonDraw 780 | 781 | return button 782 | end 783 | 784 | function GUI.tagButton(...) 785 | local button = buttonCreate(...) 786 | button.draw = tagButtonDraw 787 | 788 | return button 789 | end 790 | 791 | function GUI.adaptiveTagButton(...) 792 | local button = adaptiveButtonCreate(...) 793 | button.draw = tagButtonDraw 794 | 795 | return button 796 | end 797 | 798 | -------------------------------------------------------------------------------- 799 | 800 | local function drawPanel(object) 801 | buffer.drawRectangle(object.x, object.y, object.width, object.height, object.colors.background, 0x0, " ", object.colors.transparency) 802 | return object 803 | end 804 | 805 | function GUI.panel(x, y, width, height, color, transparency) 806 | local object = GUI.object(x, y, width, height) 807 | 808 | object.colors = { 809 | background = color, 810 | transparency = transparency 811 | } 812 | object.draw = drawPanel 813 | 814 | return object 815 | end 816 | 817 | -------------------------------------------------------------------------------- 818 | 819 | local function drawLabel(object) 820 | local xText, yText = GUI.getAlignmentCoordinates( 821 | object.x, 822 | object.y, 823 | object.width, 824 | object.height, 825 | object.horizontalAlignment, 826 | object.verticalAlignment, 827 | unicode.len(object.text), 828 | 1 829 | ) 830 | buffer.drawText(math.floor(xText), math.floor(yText), object.colors.text, object.text) 831 | return object 832 | end 833 | 834 | function GUI.label(x, y, width, height, textColor, text) 835 | local object = GUI.object(x, y, width, height) 836 | 837 | object.setAlignment = GUI.setAlignment 838 | object:setAlignment(GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP) 839 | object.colors = {text = textColor} 840 | object.text = text 841 | object.draw = drawLabel 842 | 843 | return object 844 | end 845 | 846 | -------------------------------------------------------------------------------- 847 | 848 | local function drawImage(object) 849 | buffer.drawImage(object.x, object.y, object.image) 850 | return object 851 | end 852 | 853 | function GUI.image(x, y, image) 854 | local object = GUI.object(x, y, image[1], image[2]) 855 | object.image = image 856 | object.draw = drawImage 857 | return object 858 | end 859 | 860 | -------------------------------------------------------------------------------- 861 | 862 | function GUI.actionButtons(x, y, fatSymbol) 863 | local symbol = fatSymbol and "⬤" or "●" 864 | 865 | local container = GUI.container(x, y, 5, 1) 866 | container.close = container:addChild(GUI.button(1, 1, 1, 1, nil, 0xFF4940, nil, 0x992400, symbol)) 867 | container.minimize = container:addChild(GUI.button(3, 1, 1, 1, nil, 0xFFB640, nil, 0x996D00, symbol)) 868 | container.maximize = container:addChild(GUI.button(5, 1, 1, 1, nil, 0x00B640, nil, 0x006D40, symbol)) 869 | 870 | return container 871 | end 872 | 873 | -------------------------------------------------------------------------------- 874 | 875 | local function drawProgressBar(object) 876 | local activeWidth = math.floor(math.min(object.value, 100) / 100 * object.width) 877 | if object.thin then 878 | buffer.drawText(object.x, object.y, object.colors.passive, string.rep("━", object.width)) 879 | buffer.drawText(object.x, object.y, object.colors.active, string.rep("━", activeWidth)) 880 | else 881 | buffer.drawRectangle(object.x, object.y, object.width, object.height, object.colors.passive, 0x0, " ") 882 | buffer.drawRectangle(object.x, object.y, activeWidth, object.height, object.colors.active, 0x0, " ") 883 | end 884 | 885 | if object.showValue then 886 | local stringValue = (object.valuePrefix or "") .. object.value .. (object.valuePostfix or "") 887 | buffer.drawText(math.floor(object.x + object.width / 2 - unicode.len(stringValue) / 2), object.y + 1, object.colors.value, stringValue) 888 | end 889 | 890 | return object 891 | end 892 | 893 | function GUI.progressBar(x, y, width, activeColor, passiveColor, valueColor, value, thin, showValue, valuePrefix, valuePostfix) 894 | local object = GUI.object(x, y, width, 1) 895 | 896 | object.value = value 897 | object.colors = {active = activeColor, passive = passiveColor, value = valueColor} 898 | object.thin = thin 899 | object.draw = drawProgressBar 900 | object.showValue = showValue 901 | object.valuePrefix = valuePrefix 902 | object.valuePostfix = valuePostfix 903 | 904 | return object 905 | end 906 | 907 | -------------------------------------------------------------------------------- 908 | 909 | function GUI.drawShadow(x, y, width, height, transparency, thin) 910 | if thin then 911 | buffer.drawRectangle(x + width, y + 1, 1, height - 1, 0x0, 0x0, " ", transparency) 912 | buffer.drawText(x + 1, y + height, 0x0, string.rep("▀", width), transparency) 913 | buffer.drawText(x + width, y, 0x0, "▄", transparency) 914 | else 915 | buffer.drawRectangle(x + width, y + 1, 2, height, 0x0, 0x0, " ", transparency) 916 | buffer.drawRectangle(x + 2, y + height, width - 2, 1, 0x0, 0x0, " ", transparency) 917 | end 918 | end 919 | 920 | function GUI.roundedCorners(x, y, width, height, color, transparency) 921 | buffer.drawText(x - 1, y, color, "⠰", transparency) 922 | buffer.drawText(x + width, y, color, "⠆", transparency) 923 | end 924 | 925 | -------------------------------------------------------------------------------- 926 | 927 | function GUI.alert(...) 928 | local args = {...} 929 | for i = 1, #args do 930 | if type(args[i]) == "table" then 931 | args[i] = table.toString(args[i], true) 932 | else 933 | args[i] = tostring(args[i]) 934 | end 935 | end 936 | if #args == 0 then args[1] = "nil" end 937 | 938 | local sign = image.fromString([[06030000FF 0000FF 00F7FF▟00F7FF▙0000FF 0000FF 0000FF 00F7FF▟F7FF00 F7FF00 00F7FF▙0000FF 00F7FF▟F7FF00CF7FF00yF7FF00kF7FF00a00F7FF▙]]) 939 | local offset = 2 940 | local lines = #args > 1 and "\"" .. table.concat(args, "\", \"") .. "\"" or args[1] 941 | local bufferWidth, bufferHeight = buffer.getResolution() 942 | local width = math.floor(bufferWidth * 0.5) 943 | local textWidth = width - image.getWidth(sign) - 2 944 | 945 | lines = string.wrap(lines, textWidth) 946 | local height = image.getHeight(sign) 947 | if #lines + 2 > height then 948 | height = #lines + 2 949 | end 950 | 951 | local application = GUI.application(1, math.floor(bufferHeight / 2 - height / 2), bufferWidth, height + offset * 2) 952 | local oldPixels = buffer.copy(application.x, application.y, application.width, application.height) 953 | 954 | local x, y = math.floor(bufferWidth / 2 - width / 2), offset + 1 955 | application:addChild(GUI.panel(1, 1, application.width, application.height, 0x1D1D1D)) 956 | application:addChild(GUI.image(x, y, sign)) 957 | application:addChild(GUI.textBox(x + image.getWidth(sign) + 2, y, textWidth, #lines, 0x1D1D1D, 0xE1E1E1, lines, 1, 0, 0)).eventHandler = nil 958 | local buttonWidth = 10 959 | local button = application:addChild(GUI.roundedButton(x + image.getWidth(sign) + textWidth - buttonWidth + 2, application.height - offset, buttonWidth, 1, 0x3366CC, 0xE1E1E1, 0xE1E1E1, 0x3366CC, "OK")) 960 | 961 | button.onTouch = function() 962 | application:stop() 963 | buffer.paste(application.x, application.y, oldPixels) 964 | buffer.drawChanges() 965 | end 966 | 967 | application.eventHandler = function(application, object, e1, e2, e3, e4, ...) 968 | if e1 == "key_down" and e4 == 28 then 969 | button.animated = false 970 | button:press(application, object, e1, e2, e3, e4, ...) 971 | end 972 | end 973 | 974 | application:draw(true) 975 | application:start() 976 | end 977 | 978 | -------------------------------------------------------------------------------- 979 | 980 | local function codeViewDraw(codeView) 981 | local toLine, colorScheme, patterns = codeView.fromLine + codeView.height - 1, codeView.syntaxColorScheme, codeView.syntaxPatterns 982 | -- Line numbers bar and code area 983 | codeView.lineNumbersWidth = unicode.len(tostring(toLine)) + 2 984 | codeView.codeAreaPosition = codeView.x + codeView.lineNumbersWidth 985 | codeView.codeAreaWidth = codeView.width - codeView.lineNumbersWidth 986 | -- Line numbers 987 | buffer.drawRectangle(codeView.x, codeView.y, codeView.lineNumbersWidth, codeView.height, colorScheme.lineNumbersBackground, colorScheme.lineNumbersText, " ") 988 | -- Background 989 | buffer.drawRectangle(codeView.codeAreaPosition, codeView.y, codeView.codeAreaWidth, codeView.height, colorScheme.background, colorScheme.text, " ") 990 | -- Line numbers texts 991 | local y = codeView.y 992 | for line = codeView.fromLine, toLine do 993 | if codeView.lines[line] then 994 | local text = tostring(line) 995 | if codeView.highlights[line] then 996 | buffer.drawRectangle(codeView.x, y, codeView.lineNumbersWidth, 1, codeView.highlights[line], colorScheme.text, " ", 0.3) 997 | buffer.drawRectangle(codeView.codeAreaPosition, y, codeView.codeAreaWidth, 1, codeView.highlights[line], colorScheme.text, " ") 998 | end 999 | buffer.drawText(codeView.codeAreaPosition - unicode.len(text) - 1, y, colorScheme.lineNumbersText, text) 1000 | y = y + 1 1001 | else 1002 | break 1003 | end 1004 | end 1005 | 1006 | local function drawUpperSelection(y, selectionIndex) 1007 | buffer.drawRectangle( 1008 | codeView.codeAreaPosition + codeView.selections[selectionIndex].from.symbol - codeView.fromSymbol + 1, 1009 | y + codeView.selections[selectionIndex].from.line - codeView.fromLine, 1010 | codeView.codeAreaWidth - codeView.selections[selectionIndex].from.symbol + codeView.fromSymbol - 1, 1011 | 1, 1012 | codeView.selections[selectionIndex].color or colorScheme.selection, colorScheme.text, " " 1013 | ) 1014 | end 1015 | 1016 | local function drawLowerSelection(y, selectionIndex) 1017 | buffer.drawRectangle( 1018 | codeView.codeAreaPosition, 1019 | y + codeView.selections[selectionIndex].from.line - codeView.fromLine, 1020 | codeView.selections[selectionIndex].to.symbol - codeView.fromSymbol + 2, 1021 | 1, 1022 | codeView.selections[selectionIndex].color or colorScheme.selection, colorScheme.text, " " 1023 | ) 1024 | end 1025 | 1026 | local oldDrawLimitX1, oldDrawLimitY1, oldDrawLimitX2, oldDrawLimitY2 = buffer.getDrawLimit() 1027 | buffer.setDrawLimit(codeView.codeAreaPosition, codeView.y, codeView.codeAreaPosition + codeView.codeAreaWidth - 1, codeView.y + codeView.height - 1) 1028 | 1029 | if #codeView.selections > 0 then 1030 | for selectionIndex = 1, #codeView.selections do 1031 | y = codeView.y 1032 | local dy = codeView.selections[selectionIndex].to.line - codeView.selections[selectionIndex].from.line 1033 | if dy == 0 then 1034 | buffer.drawRectangle( 1035 | codeView.codeAreaPosition + codeView.selections[selectionIndex].from.symbol - codeView.fromSymbol + 1, 1036 | y + codeView.selections[selectionIndex].from.line - codeView.fromLine, 1037 | codeView.selections[selectionIndex].to.symbol - codeView.selections[selectionIndex].from.symbol + 1, 1038 | 1, 1039 | codeView.selections[selectionIndex].color or colorScheme.selection, colorScheme.text, " " 1040 | ) 1041 | elseif dy == 1 then 1042 | drawUpperSelection(y, selectionIndex); y = y + 1 1043 | drawLowerSelection(y, selectionIndex) 1044 | else 1045 | drawUpperSelection(y, selectionIndex); y = y + 1 1046 | for i = 1, dy - 1 do 1047 | buffer.drawRectangle(codeView.codeAreaPosition, y + codeView.selections[selectionIndex].from.line - codeView.fromLine, codeView.codeAreaWidth, 1, codeView.selections[selectionIndex].color or colorScheme.selection, colorScheme.text, " "); y = y + 1 1048 | end 1049 | 1050 | drawLowerSelection(y, selectionIndex) 1051 | end 1052 | end 1053 | end 1054 | 1055 | -- Code strings 1056 | y = codeView.y 1057 | buffer.setDrawLimit(codeView.codeAreaPosition + 1, y, codeView.codeAreaPosition + codeView.codeAreaWidth - 2, y + codeView.height - 1) 1058 | 1059 | for i = codeView.fromLine, toLine do 1060 | if codeView.lines[i] then 1061 | if codeView.syntaxHighlight then 1062 | GUI.highlightString(codeView.codeAreaPosition + 1, 1063 | y, 1064 | codeView.codeAreaWidth - 2, 1065 | codeView.fromSymbol, 1066 | codeView.indentationWidth, 1067 | patterns, 1068 | colorScheme, 1069 | codeView.lines[i] 1070 | ) 1071 | else 1072 | buffer.drawText(codeView.codeAreaPosition - codeView.fromSymbol + 2, y, colorScheme.text, codeView.lines[i]) 1073 | end 1074 | 1075 | y = y + 1 1076 | else 1077 | break 1078 | end 1079 | end 1080 | 1081 | buffer.setDrawLimit(oldDrawLimitX1, oldDrawLimitY1, oldDrawLimitX2, oldDrawLimitY2) 1082 | 1083 | if #codeView.lines > codeView.height then 1084 | codeView.verticalScrollBar.colors.background, codeView.verticalScrollBar.colors.foreground = colorScheme.scrollBarBackground, colorScheme.scrollBarForeground 1085 | codeView.verticalScrollBar.minimumValue, codeView.verticalScrollBar.maximumValue, codeView.verticalScrollBar.value, codeView.verticalScrollBar.shownValueCount = 1, #codeView.lines, codeView.fromLine, codeView.height 1086 | codeView.verticalScrollBar.localX = codeView.width 1087 | codeView.verticalScrollBar.localY = 1 1088 | codeView.verticalScrollBar.height = codeView.height - 1 1089 | codeView.verticalScrollBar.hidden = false 1090 | else 1091 | codeView.verticalScrollBar.hidden = true 1092 | end 1093 | 1094 | if codeView.maximumLineLength > codeView.codeAreaWidth - 2 then 1095 | codeView.horizontalScrollBar.colors.background, codeView.horizontalScrollBar.colors.foreground = colorScheme.scrollBarBackground, colorScheme.scrollBarForeground 1096 | codeView.horizontalScrollBar.minimumValue, codeView.horizontalScrollBar.maximumValue, codeView.horizontalScrollBar.value, codeView.horizontalScrollBar.shownValueCount = 1, codeView.maximumLineLength, codeView.fromSymbol, codeView.codeAreaWidth - 2 1097 | codeView.horizontalScrollBar.localX = codeView.lineNumbersWidth + 1 1098 | codeView.horizontalScrollBar.localY = codeView.height 1099 | codeView.horizontalScrollBar.width = codeView.codeAreaWidth - 1 1100 | codeView.horizontalScrollBar.hidden = false 1101 | else 1102 | codeView.horizontalScrollBar.hidden = true 1103 | end 1104 | 1105 | codeView:overrideDraw() 1106 | end 1107 | 1108 | function GUI.codeView(x, y, width, height, fromSymbol, fromLine, maximumLineLength, selections, highlights, syntaxPatterns, syntaxColorScheme, syntaxHighlight, lines) 1109 | local codeView = GUI.container(x, y, width, height) 1110 | 1111 | codeView.passScreenEvents = false 1112 | codeView.lines = lines 1113 | codeView.fromSymbol = fromSymbol 1114 | codeView.fromLine = fromLine 1115 | codeView.maximumLineLength = maximumLineLength 1116 | codeView.selections = selections or {} 1117 | codeView.highlights = highlights or {} 1118 | codeView.syntaxHighlight = syntaxHighlight 1119 | codeView.syntaxPatterns = syntaxPatterns 1120 | codeView.syntaxColorScheme = syntaxColorScheme 1121 | codeView.indentationWidth = 2 1122 | 1123 | codeView.verticalScrollBar = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true)) 1124 | codeView.horizontalScrollBar = codeView:addChild(GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1, true)) 1125 | 1126 | codeView.overrideDraw = codeView.draw 1127 | codeView.draw = codeViewDraw 1128 | 1129 | return codeView 1130 | end 1131 | 1132 | -------------------------------------------------------------------------------- 1133 | 1134 | local function colorSelectorDraw(colorSelector) 1135 | local overlayColor = colorSelector.color < 0x7FFFFF and 0xFFFFFF or 0x0 1136 | 1137 | buffer.drawRectangle( 1138 | colorSelector.x, 1139 | colorSelector.y, 1140 | colorSelector.width, 1141 | colorSelector.height, 1142 | colorSelector.pressed and color.blend(colorSelector.color, overlayColor, 0.8) or colorSelector.color, 1143 | overlayColor, 1144 | " " 1145 | ) 1146 | 1147 | if colorSelector.height > 1 and colorSelector.drawLine then 1148 | buffer.drawText(colorSelector.x, colorSelector.y + colorSelector.height - 1, overlayColor, string.rep("▄", colorSelector.width), 0.8) 1149 | end 1150 | 1151 | buffer.drawText(colorSelector.x + 1, colorSelector.y + math.floor(colorSelector.height / 2), overlayColor, string.limit(colorSelector.text, colorSelector.width - 2)) 1152 | 1153 | return colorSelector 1154 | end 1155 | 1156 | local function colorSelectorEventHandler(application, object, e1, ...) 1157 | if e1 == "touch" then 1158 | local eventData = {...} 1159 | object.pressed = true 1160 | 1161 | local palette = application:addChild(GUI.palette(1, 1, object.color)) 1162 | palette.localX, palette.localY = math.floor(application.width / 2 - palette.width / 2), math.floor(application.height / 2 - palette.height / 2) 1163 | 1164 | palette.cancelButton.onTouch = function() 1165 | object.pressed = false 1166 | palette:remove() 1167 | application:draw() 1168 | 1169 | if object.onColorSelected then 1170 | object.onColorSelected(application, object, e1, table.unpack(eventData)) 1171 | end 1172 | end 1173 | 1174 | palette.submitButton.onTouch = function() 1175 | object.color = palette.color.integer 1176 | palette.cancelButton.onTouch() 1177 | end 1178 | 1179 | application:draw() 1180 | end 1181 | end 1182 | 1183 | function GUI.colorSelector(x, y, width, height, color, text) 1184 | local colorSelector = GUI.object(x, y, width, height) 1185 | 1186 | colorSelector.drawLine = true 1187 | colorSelector.eventHandler = colorSelectorEventHandler 1188 | colorSelector.color = color 1189 | colorSelector.text = text 1190 | colorSelector.draw = colorSelectorDraw 1191 | 1192 | return colorSelector 1193 | end 1194 | 1195 | -------------------------------------------------------------------------------- 1196 | 1197 | local function getAxisValue(number, postfix, roundValues) 1198 | if roundValues then 1199 | return math.floor(number) .. postfix 1200 | else 1201 | local integer, fractional = math.modf(number) 1202 | local firstPart, secondPart = "", "" 1203 | if math.abs(integer) >= 1000 then 1204 | return math.shorten(integer, 2) .. postfix 1205 | else 1206 | if math.abs(fractional) > 0 then 1207 | return string.format("%.2f", number) .. postfix 1208 | else 1209 | return number .. postfix 1210 | end 1211 | end 1212 | end 1213 | end 1214 | 1215 | local function drawChart(object) 1216 | -- Sorting by x value 1217 | local valuesCopy = {} 1218 | for i = 1, #object.values do valuesCopy[i] = object.values[i] end 1219 | table.sort(valuesCopy, function(a, b) return a[1] < b[1] end) 1220 | 1221 | if #valuesCopy == 0 then valuesCopy = {{0, 0}} end 1222 | 1223 | -- Max, min, deltas 1224 | local xMin, xMax, yMin, yMax = valuesCopy[1][1], valuesCopy[#valuesCopy][1], valuesCopy[1][2], valuesCopy[1][2] 1225 | for i = 1, #valuesCopy do yMin, yMax = math.min(yMin, valuesCopy[i][2]), math.max(yMax, valuesCopy[i][2]) end 1226 | local dx, dy = xMax - xMin, yMax - yMin 1227 | 1228 | -- y axis values and helpers 1229 | local value, chartHeight, yAxisValueMaxWidth, yAxisValues = yMin, object.height - 1 - (object.showXAxisValues and 1 or 0), 0, {} 1230 | for y = object.y + object.height - 3, object.y + 1, -chartHeight * object.yAxisValueInterval do 1231 | local stringValue = getAxisValue(value, object.yAxisPostfix, object.roundValues) 1232 | yAxisValueMaxWidth = math.max(yAxisValueMaxWidth, unicode.len(stringValue)) 1233 | table.insert(yAxisValues, {y = math.ceil(y), value = stringValue}) 1234 | value = value + dy * object.yAxisValueInterval 1235 | end 1236 | local stringValue = getAxisValue(yMax, object.yAxisPostfix, object.roundValues) 1237 | table.insert(yAxisValues, {y = object.y, value = stringValue}) 1238 | yAxisValueMaxWidth = math.max(yAxisValueMaxWidth, unicode.len(stringValue)) 1239 | 1240 | local chartWidth = object.width - (object.showYAxisValues and yAxisValueMaxWidth + 2 or 0) 1241 | local chartX = object.x + object.width - chartWidth 1242 | for i = 1, #yAxisValues do 1243 | if object.showYAxisValues then 1244 | buffer.drawText(chartX - unicode.len(yAxisValues[i].value) - 2, yAxisValues[i].y, object.colors.axisValue, yAxisValues[i].value) 1245 | end 1246 | buffer.drawText(chartX, yAxisValues[i].y, object.colors.helpers, string.rep("─", chartWidth)) 1247 | end 1248 | 1249 | -- x axis values 1250 | if object.showXAxisValues then 1251 | value = xMin 1252 | for x = chartX, chartX + chartWidth - 2, chartWidth * object.xAxisValueInterval do 1253 | local stringValue = getAxisValue(value, object.xAxisPostfix, object.roundValues) 1254 | buffer.drawText(math.floor(x - unicode.len(stringValue) / 2), object.y + object.height - 1, object.colors.axisValue, stringValue) 1255 | value = value + dx * object.xAxisValueInterval 1256 | end 1257 | local value = getAxisValue(xMax, object.xAxisPostfix, object.roundValues) 1258 | buffer.drawText(object.x + object.width - unicode.len(value), object.y + object.height - 1, object.colors.axisValue, value) 1259 | end 1260 | 1261 | -- Axis lines 1262 | for y = object.y, object.y + chartHeight - 1 do 1263 | buffer.drawText(chartX - 1, y, object.colors.axis, "┨") 1264 | end 1265 | buffer.drawText(chartX - 1, object.y + chartHeight, object.colors.axis, "┗" .. string.rep("┯━", math.floor(chartWidth / 2))) 1266 | 1267 | local function fillVerticalPart(x1, y1, x2, y2) 1268 | local dx, dy = x2 - x1, y2 - y1 1269 | local absdx, absdy = math.abs(dx), math.abs(dy) 1270 | if absdx >= absdy then 1271 | local step, y = dy / absdx, y1 1272 | for x = x1, x2, (x1 < x2 and 1 or -1) do 1273 | local yFloor = math.floor(y) 1274 | buffer.drawSemiPixelRectangle(math.floor(x), yFloor, 1, math.floor(object.y + chartHeight) * 2 - yFloor - 1, object.colors.chart) 1275 | y = y + step 1276 | end 1277 | else 1278 | local step, x = dx / absdy, x1 1279 | for y = y1, y2, (y1 < y2 and 1 or -1) do 1280 | local yFloor = math.floor(y) 1281 | buffer.drawSemiPixelRectangle(math.floor(x), yFloor, 1, math.floor(object.y + chartHeight) * 2 - yFloor - 1, object.colors.chart) 1282 | x = x + step 1283 | end 1284 | end 1285 | end 1286 | 1287 | -- chart 1288 | for i = 1, #valuesCopy - 1 do 1289 | local x = math.floor(chartX + (valuesCopy[i][1] - xMin) / dx * (chartWidth - 1)) 1290 | local y = math.floor(object.y + chartHeight - 1 - (valuesCopy[i][2] - yMin) / dy * (chartHeight - 1)) * 2 1291 | local xNext = math.floor(chartX + (valuesCopy[i + 1][1] - xMin) / dx * (chartWidth - 1)) 1292 | local yNext = math.floor(object.y + chartHeight - 1 - (valuesCopy[i + 1][2] - yMin) / dy * (chartHeight - 1)) * 2 1293 | if object.fillChartArea then 1294 | fillVerticalPart(x, y, xNext, yNext) 1295 | else 1296 | buffer.drawSemiPixelLine(x, y, xNext, yNext, object.colors.chart) 1297 | end 1298 | end 1299 | 1300 | return object 1301 | end 1302 | 1303 | function GUI.chart(x, y, width, height, axisColor, axisValueColor, axisHelpersColor, chartColor, xAxisValueInterval, yAxisValueInterval, xAxisPostfix, yAxisPostfix, fillChartArea, values) 1304 | local object = GUI.object(x, y, width, height) 1305 | 1306 | object.colors = {axis = axisColor, chart = chartColor, axisValue = axisValueColor, helpers = axisHelpersColor} 1307 | object.draw = drawChart 1308 | object.values = values or {} 1309 | object.xAxisPostfix = xAxisPostfix 1310 | object.yAxisPostfix = yAxisPostfix 1311 | object.xAxisValueInterval = xAxisValueInterval 1312 | object.yAxisValueInterval = yAxisValueInterval 1313 | object.fillChartArea = fillChartArea 1314 | object.showYAxisValues = true 1315 | object.showXAxisValues = true 1316 | 1317 | return object 1318 | end 1319 | 1320 | -------------------------------------------------------------------------------- 1321 | 1322 | local function switchAndLabelDraw(switchAndLabel) 1323 | switchAndLabel.label.width = switchAndLabel.width 1324 | switchAndLabel.switch.localX = switchAndLabel.width - switchAndLabel.switch.width 1325 | 1326 | switchAndLabel.label.x, switchAndLabel.label.y = switchAndLabel.x + switchAndLabel.label.localX - 1, switchAndLabel.y + switchAndLabel.label.localY - 1 1327 | switchAndLabel.switch.x, switchAndLabel.switch.y = switchAndLabel.x + switchAndLabel.switch.localX - 1, switchAndLabel.y + switchAndLabel.switch.localY - 1 1328 | 1329 | switchAndLabel.label:draw() 1330 | switchAndLabel.switch:draw() 1331 | 1332 | return switchAndLabel 1333 | end 1334 | 1335 | function GUI.switchAndLabel(x, y, width, switchWidth, activeColor, passiveColor, pipeColor, textColor, text, switchState) 1336 | local switchAndLabel = GUI.container(x, y, width, 1) 1337 | 1338 | switchAndLabel.label = switchAndLabel:addChild(GUI.label(1, 1, width, 1, textColor, text)) 1339 | switchAndLabel.switch = switchAndLabel:addChild(GUI.switch(1, 1, switchWidth, activeColor, passiveColor, pipeColor, switchState)) 1340 | switchAndLabel.draw = switchAndLabelDraw 1341 | 1342 | return switchAndLabel 1343 | end 1344 | 1345 | -------------------------------------------------------------------------------- 1346 | 1347 | local function sliderDraw(object) 1348 | -- На всякий случай делаем значение не меньше минимального и не больше максимального 1349 | object.value = math.min(math.max(object.value, object.minimumValue), object.maximumValue) 1350 | 1351 | if object.showMaximumAndMinimumValues then 1352 | 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)) 1353 | buffer.drawText(object.x - unicode.len(stringMinimumValue) - 1, object.y, object.colors.value, stringMinimumValue) 1354 | buffer.drawText(object.x + object.width + 1, object.y, object.colors.value, stringMaximumValue) 1355 | end 1356 | 1357 | if object.currentValuePrefix or object.currentValuePostfix then 1358 | local stringCurrentValue = (object.currentValuePrefix or "") .. (object.roundValues and math.floor(object.value) or math.roundToDecimalPlaces(object.value, 2)) .. (object.currentValuePostfix or "") 1359 | buffer.drawText(math.floor(object.x + object.width / 2 - unicode.len(stringCurrentValue) / 2), object.y + 1, object.colors.value, stringCurrentValue) 1360 | end 1361 | 1362 | local activeWidth = math.round((object.value - object.minimumValue) / (object.maximumValue - object.minimumValue) * object.width) 1363 | buffer.drawText(object.x, object.y, object.colors.passive, string.rep("━", object.width)) 1364 | buffer.drawText(object.x, object.y, object.colors.active, string.rep("━", activeWidth)) 1365 | buffer.drawText(activeWidth >= object.width and object.x + activeWidth - 1 or object.x + activeWidth, object.y, object.colors.pipe, "⬤") 1366 | 1367 | return object 1368 | end 1369 | 1370 | local function sliderEventHandler(application, object, e1, e2, e3, ...) 1371 | if e1 == "touch" or e1 == "drag" then 1372 | local clickPosition = e3 - object.x 1373 | 1374 | if clickPosition == 0 then 1375 | object.value = object.minimumValue 1376 | elseif clickPosition == object.width - 1 then 1377 | object.value = object.maximumValue 1378 | else 1379 | object.value = object.minimumValue + (clickPosition / object.width * (object.maximumValue - object.minimumValue)) 1380 | end 1381 | 1382 | application:draw() 1383 | 1384 | if object.onValueChanged then 1385 | object.onValueChanged(application, object, e1, e2, e3, ...) 1386 | end 1387 | end 1388 | end 1389 | 1390 | function GUI.slider(x, y, width, activeColor, passiveColor, pipeColor, valueColor, minimumValue, maximumValue, value, showMaximumAndMinimumValues, currentValuePrefix, currentValuePostfix) 1391 | local object = GUI.object(x, y, width, 1) 1392 | 1393 | object.eventHandler = sliderEventHandler 1394 | object.colors = {active = activeColor, passive = passiveColor, pipe = pipeColor, value = valueColor} 1395 | object.draw = sliderDraw 1396 | object.minimumValue = minimumValue 1397 | object.maximumValue = maximumValue 1398 | object.value = value 1399 | object.showMaximumAndMinimumValues = showMaximumAndMinimumValues 1400 | object.currentValuePrefix = currentValuePrefix 1401 | object.currentValuePostfix = currentValuePostfix 1402 | object.roundValues = false 1403 | 1404 | return object 1405 | end 1406 | 1407 | -------------------------------------------------------------------------------- 1408 | 1409 | local function switchDraw(switch) 1410 | buffer.drawText(switch.x - 1, switch.y, switch.colors.passive, "⠰") 1411 | buffer.drawRectangle(switch.x, switch.y, switch.width, 1, switch.colors.passive, 0x0, " ") 1412 | buffer.drawText(switch.x + switch.width, switch.y, switch.colors.passive, "⠆") 1413 | 1414 | buffer.drawText(switch.x - 1, switch.y, switch.colors.active, "⠰") 1415 | buffer.drawRectangle(switch.x, switch.y, switch.pipePosition - 1, 1, switch.colors.active, 0x0, " ") 1416 | 1417 | buffer.drawText(switch.x + switch.pipePosition - 2, switch.y, switch.colors.pipe, "⠰") 1418 | buffer.drawRectangle(switch.x + switch.pipePosition - 1, switch.y, 2, 1, switch.colors.pipe, 0x0, " ") 1419 | buffer.drawText(switch.x + switch.pipePosition + 1, switch.y, switch.colors.pipe, "⠆") 1420 | 1421 | return switch 1422 | end 1423 | 1424 | local function switchSetState(switch, state) 1425 | switch.state = state 1426 | switch.pipePosition = switch.state and switch.width - 1 or 1 1427 | 1428 | return switch 1429 | end 1430 | 1431 | local function switchEventHandler(application, switch, e1, ...) 1432 | if e1 == "touch" then 1433 | local eventData = {...} 1434 | 1435 | switch.state = not switch.state 1436 | switch:addAnimation( 1437 | function(animation) 1438 | if switch.state then 1439 | switch.pipePosition = math.round(1 + animation.position * (switch.width - 2)) 1440 | else 1441 | switch.pipePosition = math.round(1 + (1 - animation.position) * (switch.width - 2)) 1442 | end 1443 | end, 1444 | function(animation) 1445 | animation:remove() 1446 | if switch.onStateChanged then 1447 | switch.onStateChanged(switch, e1, table.unpack(eventData)) 1448 | end 1449 | end 1450 | ):start(switch.animationDuration) 1451 | end 1452 | end 1453 | 1454 | function GUI.switch(x, y, width, activeColor, passiveColor, pipeColor, state) 1455 | local switch = GUI.object(x, y, width, 1) 1456 | 1457 | switch.pipePosition = 1 1458 | switch.eventHandler = switchEventHandler 1459 | switch.colors = { 1460 | active = activeColor, 1461 | passive = passiveColor, 1462 | pipe = pipeColor, 1463 | } 1464 | switch.draw = switchDraw 1465 | switch.state = state or false 1466 | switch.update = switchUpdate 1467 | switch.animated = true 1468 | switch.animationDuration = GUI.SWITCH_ANIMATION_DURATION 1469 | switch.setState = switchSetState 1470 | 1471 | switch:setState(state) 1472 | 1473 | return switch 1474 | end 1475 | 1476 | -------------------------------------------------------------------------------- 1477 | 1478 | local function layoutCheckCell(layout, column, row) 1479 | if column < 1 or column > #layout.columnSizes or row < 1 or row > #layout.rowSizes then 1480 | error("Specified grid position (" .. tostring(column) .. "x" .. tostring(row) .. ") is out of layout grid range") 1481 | end 1482 | end 1483 | 1484 | local function layoutGetAbsoluteTotalSize(array) 1485 | local absoluteTotalSize = 0 1486 | for i = 1, #array do 1487 | if array[i].sizePolicy == GUI.SIZE_POLICY_ABSOLUTE then 1488 | absoluteTotalSize = absoluteTotalSize + array[i].size 1489 | end 1490 | end 1491 | return absoluteTotalSize 1492 | end 1493 | 1494 | local function layoutGetCalculatedSize(array, index, dependency) 1495 | if array[index].sizePolicy == GUI.SIZE_POLICY_RELATIVE then 1496 | array[index].calculatedSize = array[index].size * dependency 1497 | else 1498 | array[index].calculatedSize = array[index].size 1499 | end 1500 | end 1501 | 1502 | local function layoutUpdate(layout) 1503 | local columnPercentageTotalSize, rowPercentageTotalSize = layout.width - layoutGetAbsoluteTotalSize(layout.columnSizes), layout.height - layoutGetAbsoluteTotalSize(layout.rowSizes) 1504 | for row = 1, #layout.rowSizes do 1505 | layoutGetCalculatedSize(layout.rowSizes, row, rowPercentageTotalSize) 1506 | for column = 1, #layout.columnSizes do 1507 | layoutGetCalculatedSize(layout.columnSizes, column, columnPercentageTotalSize) 1508 | layout.cells[row][column].childrenWidth, layout.cells[row][column].childrenHeight = 0, 0 1509 | end 1510 | end 1511 | 1512 | -- Подготавливаем объекты к расположению и подсчитываем тотальные размеры 1513 | local child, layoutRow, layoutColumn, cell 1514 | for i = 1, #layout.children do 1515 | child = layout.children[i] 1516 | 1517 | if not child.hidden then 1518 | layoutRow, layoutColumn = child.layoutRow, child.layoutColumn 1519 | 1520 | -- Проверка на позицию в сетке 1521 | if layoutRow >= 1 and layoutRow <= #layout.rowSizes and layoutColumn >= 1 and layoutColumn <= #layout.columnSizes then 1522 | cell = layout.cells[layoutRow][layoutColumn] 1523 | -- Авто-фиттинг объектов 1524 | if cell.horizontalFitting then 1525 | child.width = math.round(layout.columnSizes[layoutColumn].calculatedSize - cell.horizontalFittingRemove) 1526 | end 1527 | 1528 | if cell.verticalFitting then 1529 | child.height = math.round(layout.rowSizes[layoutRow].calculatedSize - cell.verticalFittingRemove) 1530 | end 1531 | 1532 | -- Направление и расчет размеров 1533 | if cell.direction == GUI.DIRECTION_HORIZONTAL then 1534 | cell.childrenWidth = cell.childrenWidth + child.width + cell.spacing 1535 | cell.childrenHeight = math.max(cell.childrenHeight, child.height) 1536 | else 1537 | cell.childrenWidth = math.max(cell.childrenWidth, child.width) 1538 | cell.childrenHeight = cell.childrenHeight + child.height + cell.spacing 1539 | end 1540 | else 1541 | error("Layout child with index " .. i .. " has been assigned to cell (" .. layoutColumn .. "x" .. layoutRow .. ") out of layout grid range") 1542 | end 1543 | end 1544 | end 1545 | 1546 | -- Высчитываем стартовую позицию объектов ячейки 1547 | local x, y = 1, 1 1548 | for row = 1, #layout.rowSizes do 1549 | for column = 1, #layout.columnSizes do 1550 | cell = layout.cells[row][column] 1551 | cell.x, cell.y = GUI.getAlignmentCoordinates( 1552 | x, 1553 | y, 1554 | layout.columnSizes[column].calculatedSize, 1555 | layout.rowSizes[row].calculatedSize, 1556 | cell.horizontalAlignment, 1557 | cell.verticalAlignment, 1558 | cell.childrenWidth - (cell.direction == GUI.DIRECTION_HORIZONTAL and cell.spacing or 0), 1559 | cell.childrenHeight - (cell.direction == GUI.DIRECTION_VERTICAL and cell.spacing or 0) 1560 | ) 1561 | 1562 | -- Учитываем отступы от краев ячейки 1563 | if cell.horizontalMargin ~= 0 or cell.verticalMargin ~= 0 then 1564 | cell.x, cell.y = GUI.getMarginCoordinates( 1565 | cell.x, 1566 | cell.y, 1567 | cell.horizontalAlignment, 1568 | cell.verticalAlignment, 1569 | cell.horizontalMargin, 1570 | cell.verticalMargin 1571 | ) 1572 | end 1573 | 1574 | x = x + layout.columnSizes[column].calculatedSize 1575 | end 1576 | 1577 | x, y = 1, y + layout.rowSizes[row].calculatedSize 1578 | end 1579 | 1580 | -- Размещаем все объекты 1581 | for i = 1, #layout.children do 1582 | child = layout.children[i] 1583 | 1584 | if not child.hidden then 1585 | cell = layout.cells[child.layoutRow][child.layoutColumn] 1586 | 1587 | child.localX, cell.localY = GUI.getAlignmentCoordinates( 1588 | cell.x, 1589 | cell.y, 1590 | cell.childrenWidth, 1591 | cell.childrenHeight, 1592 | cell.horizontalAlignment, 1593 | cell.verticalAlignment, 1594 | child.width, 1595 | child.height 1596 | ) 1597 | 1598 | if cell.direction == GUI.DIRECTION_HORIZONTAL then 1599 | child.localX, child.localY = math.floor(cell.x), math.floor(cell.localY) 1600 | cell.x = cell.x + child.width + cell.spacing 1601 | else 1602 | child.localX, child.localY = math.floor(child.localX), math.floor(cell.y) 1603 | cell.y = cell.y + child.height + cell.spacing 1604 | end 1605 | end 1606 | end 1607 | end 1608 | 1609 | local function layoutSetPosition(layout, column, row, object) 1610 | layoutCheckCell(layout, column, row) 1611 | object.layoutRow = row 1612 | object.layoutColumn = column 1613 | 1614 | return object 1615 | end 1616 | 1617 | local function layoutSetDirection(layout, column, row, direction) 1618 | layoutCheckCell(layout, column, row) 1619 | layout.cells[row][column].direction = direction 1620 | 1621 | return layout 1622 | end 1623 | 1624 | local function layoutSetSpacing(layout, column, row, spacing) 1625 | layoutCheckCell(layout, column, row) 1626 | layout.cells[row][column].spacing = spacing 1627 | 1628 | return layout 1629 | end 1630 | 1631 | local function layoutSetAlignment(layout, column, row, horizontalAlignment, verticalAlignment) 1632 | layoutCheckCell(layout, column, row) 1633 | layout.cells[row][column].horizontalAlignment, layout.cells[row][column].verticalAlignment = horizontalAlignment, verticalAlignment 1634 | 1635 | return layout 1636 | end 1637 | 1638 | local function layoutGetMargin(layout, column, row) 1639 | layoutCheckCell(layout, column, row) 1640 | 1641 | return layout.cells[row][column].horizontalMargin, layout.cells[row][column].verticalMargin 1642 | end 1643 | 1644 | local function layoutSetMargin(layout, column, row, horizontalMargin, verticalMargin) 1645 | layoutCheckCell(layout, column, row) 1646 | layout.cells[row][column].horizontalMargin = horizontalMargin 1647 | layout.cells[row][column].verticalMargin = verticalMargin 1648 | 1649 | return layout 1650 | end 1651 | 1652 | local function layoutNewCell() 1653 | return { 1654 | horizontalAlignment = GUI.ALIGNMENT_HORIZONTAL_CENTER, 1655 | verticalAlignment = GUI.ALIGNMENT_VERTICAL_CENTER, 1656 | horizontalMargin = 0, 1657 | verticalMargin = 0, 1658 | direction = GUI.DIRECTION_VERTICAL, 1659 | spacing = 1 1660 | } 1661 | end 1662 | 1663 | local function layoutCalculatePercentageSize(changingExistent, array, index) 1664 | if array[index].sizePolicy == GUI.SIZE_POLICY_RELATIVE then 1665 | local allPercents, beforeFromIndexPercents = 0, 0 1666 | for i = 1, #array do 1667 | if array[i].sizePolicy == GUI.SIZE_POLICY_RELATIVE then 1668 | allPercents = allPercents + array[i].size 1669 | 1670 | if i <= index then 1671 | beforeFromIndexPercents = beforeFromIndexPercents + array[i].size 1672 | end 1673 | end 1674 | end 1675 | 1676 | local modifyer 1677 | if changingExistent then 1678 | if beforeFromIndexPercents > 1 then 1679 | error("Layout summary percentage > 100% at index " .. index) 1680 | end 1681 | modifyer = (1 - beforeFromIndexPercents) / (allPercents - beforeFromIndexPercents) 1682 | else 1683 | modifyer = (1 - array[index].size) / (allPercents - array[index].size) 1684 | end 1685 | 1686 | for i = changingExistent and index + 1 or 1, #array do 1687 | if array[i].sizePolicy == GUI.SIZE_POLICY_RELATIVE and i ~= index then 1688 | array[i].size = modifyer * array[i].size 1689 | end 1690 | end 1691 | end 1692 | end 1693 | 1694 | local function layoutSetColumnWidth(layout, column, sizePolicy, size) 1695 | layout.columnSizes[column].sizePolicy, layout.columnSizes[column].size = sizePolicy, size 1696 | layoutCalculatePercentageSize(true, layout.columnSizes, column) 1697 | 1698 | return layout 1699 | end 1700 | 1701 | local function layoutSetRowHeight(layout, row, sizePolicy, size) 1702 | layout.rowSizes[row].sizePolicy, layout.rowSizes[row].size = sizePolicy, size 1703 | layoutCalculatePercentageSize(true, layout.rowSizes, row) 1704 | 1705 | return layout 1706 | end 1707 | 1708 | local function layoutAddColumn(layout, sizePolicy, size) 1709 | for i = 1, #layout.rowSizes do 1710 | table.insert(layout.cells[i], layoutNewCell()) 1711 | end 1712 | 1713 | table.insert(layout.columnSizes, { 1714 | sizePolicy = sizePolicy, 1715 | size = size 1716 | }) 1717 | layoutCalculatePercentageSize(false, layout.columnSizes, #layout.columnSizes) 1718 | 1719 | return layout 1720 | end 1721 | 1722 | local function layoutAddRow(layout, sizePolicy, size) 1723 | local row = {} 1724 | for i = 1, #layout.columnSizes do 1725 | table.insert(row, layoutNewCell()) 1726 | end 1727 | 1728 | table.insert(layout.cells, row) 1729 | table.insert(layout.rowSizes, { 1730 | sizePolicy = sizePolicy, 1731 | size = size 1732 | }) 1733 | 1734 | layoutCalculatePercentageSize(false, layout.rowSizes, #layout.rowSizes) 1735 | 1736 | return layout 1737 | end 1738 | 1739 | local function layoutRemoveRow(layout, row) 1740 | table.remove(layout.cells, row) 1741 | 1742 | layout.rowSizes[row].size = 0 1743 | layoutCalculatePercentageSize(false, layout.rowSizes, row) 1744 | 1745 | table.remove(layout.rowSizes, row) 1746 | 1747 | return layout 1748 | end 1749 | 1750 | local function layoutRemoveColumn(layout, column) 1751 | for i = 1, #layout.rowSizes do 1752 | table.remove(layout.cells[i], column) 1753 | end 1754 | 1755 | layout.columnSizes[column].size = 0 1756 | layoutCalculatePercentageSize(false, layout.columnSizes, column) 1757 | 1758 | table.remove(layout.columnSizes, column) 1759 | 1760 | return layout 1761 | end 1762 | 1763 | local function layoutSetGridSize(layout, columnCount, rowCount) 1764 | layout.cells = {} 1765 | layout.rowSizes = {} 1766 | layout.columnSizes = {} 1767 | 1768 | local rowSize, columnSize = 1 / rowCount, 1 / columnCount 1769 | for i = 1, rowCount do 1770 | layoutAddRow(layout, GUI.SIZE_POLICY_RELATIVE, 1 / i) 1771 | end 1772 | 1773 | for i = 1, columnCount do 1774 | layoutAddColumn(layout, GUI.SIZE_POLICY_RELATIVE, 1 / i) 1775 | end 1776 | 1777 | return layout 1778 | end 1779 | 1780 | local function layoutDraw(layout) 1781 | layout:update() 1782 | containerDraw(layout) 1783 | 1784 | if layout.showGrid then 1785 | local x, y = layout.x, layout.y 1786 | for j = 1, #layout.columnSizes do 1787 | for i = 1, #layout.rowSizes do 1788 | buffer.drawFrame( 1789 | math.round(x), 1790 | math.round(y), 1791 | math.round(layout.columnSizes[j].calculatedSize), 1792 | math.round(layout.rowSizes[i].calculatedSize), 1793 | 0xFF0000 1794 | ) 1795 | y = y + layout.rowSizes[i].calculatedSize 1796 | end 1797 | x, y = x + layout.columnSizes[j].calculatedSize, layout.y 1798 | end 1799 | end 1800 | end 1801 | 1802 | local function layoutFitToChildrenSize(layout, column, row) 1803 | layout.width, layout.height = 0, 0 1804 | 1805 | for i = 1, #layout.children do 1806 | if not layout.children[i].hidden then 1807 | if layout.cells[row][column].direction == GUI.DIRECTION_HORIZONTAL then 1808 | layout.width = layout.width + layout.children[i].width + layout.cells[row][column].spacing 1809 | layout.height = math.max(layout.height, layout.children[i].height) 1810 | else 1811 | layout.width = math.max(layout.width, layout.children[i].width) 1812 | layout.height = layout.height + layout.children[i].height + layout.cells[row][column].spacing 1813 | end 1814 | end 1815 | end 1816 | 1817 | if layout.cells[row][column].direction == GUI.DIRECTION_HORIZONTAL then 1818 | layout.width = layout.width - layout.cells[row][column].spacing 1819 | else 1820 | layout.height = layout.height - layout.cells[row][column].spacing 1821 | end 1822 | 1823 | return layout 1824 | end 1825 | 1826 | local function layoutSetFitting(layout, column, row, horizontal, vertical, horizontalRemove, verticalRemove) 1827 | layoutCheckCell(layout, column, row) 1828 | layout.cells[row][column].horizontalFitting = horizontal 1829 | layout.cells[row][column].verticalFitting = vertical 1830 | layout.cells[row][column].horizontalFittingRemove = horizontalRemove or 0 1831 | layout.cells[row][column].verticalFittingRemove = verticalRemove or 0 1832 | 1833 | return layout 1834 | end 1835 | 1836 | local function layoutAddChild(layout, object, ...) 1837 | object.layoutRow = layout.defaultRow 1838 | object.layoutColumn = layout.defaultColumn 1839 | containerAddChild(layout, object, ...) 1840 | 1841 | return object 1842 | end 1843 | 1844 | function GUI.layout(x, y, width, height, columnCount, rowCount) 1845 | local layout = GUI.container(x, y, width, height) 1846 | 1847 | layout.defaultRow = 1 1848 | layout.defaultColumn = 1 1849 | 1850 | layout.addRow = layoutAddRow 1851 | layout.addColumn = layoutAddColumn 1852 | layout.removeRow = layoutRemoveRow 1853 | layout.removeColumn = layoutRemoveColumn 1854 | 1855 | layout.setRowHeight = layoutSetRowHeight 1856 | layout.setColumnWidth = layoutSetColumnWidth 1857 | 1858 | layout.setPosition = layoutSetPosition 1859 | layout.setDirection = layoutSetDirection 1860 | layout.setGridSize = layoutSetGridSize 1861 | layout.setSpacing = layoutSetSpacing 1862 | layout.setAlignment = layoutSetAlignment 1863 | layout.setMargin = layoutSetMargin 1864 | layout.getMargin = layoutGetMargin 1865 | 1866 | layout.fitToChildrenSize = layoutFitToChildrenSize 1867 | layout.setFitting = layoutSetFitting 1868 | 1869 | layout.update = layoutUpdate 1870 | layout.addChild = layoutAddChild 1871 | layout.draw = layoutDraw 1872 | 1873 | layout:setGridSize(columnCount, rowCount) 1874 | 1875 | return layout 1876 | end 1877 | 1878 | -------------------------------------------------------------------------------- 1879 | 1880 | local function filesystemDialogDraw(filesystemDialog) 1881 | if filesystemDialog.extensionComboBox.hidden then 1882 | filesystemDialog.input.width = filesystemDialog.cancelButton.localX - 4 1883 | else 1884 | filesystemDialog.input.width = filesystemDialog.extensionComboBox.localX - 3 1885 | end 1886 | 1887 | if filesystemDialog.IOMode == GUI.IO_MODE_SAVE then 1888 | filesystemDialog.submitButton.disabled = not filesystemDialog.input.text or filesystemDialog.input.text == "" 1889 | else 1890 | filesystemDialog.input.text = filesystemDialog.filesystemTree.selectedItem or "" 1891 | filesystemDialog.submitButton.disabled = not filesystemDialog.filesystemTree.selectedItem 1892 | end 1893 | 1894 | containerDraw(filesystemDialog) 1895 | GUI.drawShadow(filesystemDialog.x, filesystemDialog.y, filesystemDialog.width, filesystemDialog.height, GUI.CONTEXT_MENU_SHADOW_TRANSPARENCY, true) 1896 | 1897 | return filesystemDialog 1898 | end 1899 | 1900 | local function filesystemDialogSetMode(filesystemDialog, IOMode, filesystemMode) 1901 | filesystemDialog.IOMode = IOMode 1902 | filesystemDialog.filesystemMode = filesystemMode 1903 | 1904 | if filesystemDialog.IOMode == GUI.IO_MODE_SAVE then 1905 | filesystemDialog.filesystemTree.showMode = GUI.IO_MODE_DIRECTORY 1906 | filesystemDialog.filesystemTree.selectionMode = GUI.IO_MODE_DIRECTORY 1907 | filesystemDialog.input.disabled = false 1908 | filesystemDialog.extensionComboBox.hidden = filesystemDialog.filesystemMode ~= GUI.IO_MODE_FILE or not filesystemDialog.filesystemTree.extensionFilters 1909 | else 1910 | if filesystemDialog.filesystemMode == GUI.IO_MODE_FILE then 1911 | filesystemDialog.filesystemTree.showMode = GUI.IO_MODE_BOTH 1912 | filesystemDialog.filesystemTree.selectionMode = GUI.IO_MODE_FILE 1913 | else 1914 | filesystemDialog.filesystemTree.showMode = GUI.IO_MODE_DIRECTORY 1915 | filesystemDialog.filesystemTree.selectionMode = GUI.IO_MODE_DIRECTORY 1916 | end 1917 | 1918 | filesystemDialog.input.disabled = true 1919 | filesystemDialog.extensionComboBox.hidden = true 1920 | end 1921 | end 1922 | 1923 | local function filesystemDialogAddExtensionFilter(filesystemDialog, extension) 1924 | filesystemDialog.extensionComboBox:addItem(extension) 1925 | filesystemDialog.extensionComboBox.width = math.max(filesystemDialog.extensionComboBox.width, unicode.len(extension) + 3) 1926 | filesystemDialog.extensionComboBox.localX = filesystemDialog.cancelButton.localX - filesystemDialog.extensionComboBox.width - 2 1927 | filesystemDialog.filesystemTree:addExtensionFilter(extension) 1928 | 1929 | filesystemDialog:setMode(filesystemDialog.IOMode, filesystemDialog.filesystemMode) 1930 | end 1931 | 1932 | local function filesystemDialogExpandPath(filesystemDialog, ...) 1933 | filesystemDialog.filesystemTree:expandPath(...) 1934 | end 1935 | 1936 | function GUI.filesystemDialog(x, y, width, height, submitButtonText, cancelButtonText, placeholderText, path) 1937 | local filesystemDialog = GUI.container(x, y, width, height) 1938 | 1939 | filesystemDialog:addChild(GUI.panel(1, height - 2, width, 3, 0xD2D2D2)) 1940 | 1941 | filesystemDialog.cancelButton = filesystemDialog:addChild(GUI.adaptiveRoundedButton(1, height - 1, 1, 0, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xE1E1E1, cancelButtonText)) 1942 | filesystemDialog.submitButton = filesystemDialog:addChild(GUI.adaptiveRoundedButton(1, height - 1, 1, 0, 0x3C3C3C, 0xE1E1E1, 0xE1E1E1, 0x3C3C3C, submitButtonText)) 1943 | filesystemDialog.submitButton.localX = filesystemDialog.width - filesystemDialog.submitButton.width - 1 1944 | filesystemDialog.cancelButton.localX = filesystemDialog.submitButton.localX - filesystemDialog.cancelButton.width - 2 1945 | 1946 | filesystemDialog.extensionComboBox = filesystemDialog:addChild(GUI.comboBox(1, height - 1, 1, 1, 0xE1E1E1, 0x696969, 0xC3C3C3, 0x878787)) 1947 | filesystemDialog.extensionComboBox.hidden = true 1948 | 1949 | filesystemDialog.input = filesystemDialog:addChild(GUI.input(2, height - 1, 1, 1, 0xE1E1E1, 0x696969, 0x969696, 0xE1E1E1, 0x3C3C3C, "", placeholderText)) 1950 | 1951 | filesystemDialog.filesystemTree = filesystemDialog:addChild(GUI.filesystemTree(1, 1, width, height - 3, 0xE1E1E1, 0x3C3C3C, 0x3C3C3C, 0xA5A5A5, 0x3C3C3C, 0xE1E1E1, 0xB4B4B4, 0xA5A5A5, 0xC3C3C3, 0x4B4B4B)) 1952 | filesystemDialog.filesystemTree.workPath = path 1953 | filesystemDialog.animationDuration = GUI.FILESYSTEM_DIALOG_ANIMATION_DURATION 1954 | 1955 | filesystemDialog.draw = filesystemDialogDraw 1956 | filesystemDialog.setMode = filesystemDialogSetMode 1957 | filesystemDialog.addExtensionFilter = filesystemDialogAddExtensionFilter 1958 | 1959 | filesystemDialog.expandPath = filesystemDialogExpandPath 1960 | filesystemDialog:setMode(GUI.IO_MODE_OPEN, GUI.IO_MODE_FILE) 1961 | 1962 | return filesystemDialog 1963 | end 1964 | 1965 | local function filesystemDialogShow(filesystemDialog) 1966 | filesystemDialog.filesystemTree:updateFileList() 1967 | filesystemDialog:addAnimation( 1968 | function(animation) 1969 | filesystemDialog.localY = math.floor(1 + (1.0 - animation.position) * (-filesystemDialog.height)) 1970 | end, 1971 | function(animation) 1972 | animation:remove() 1973 | end 1974 | ):start(filesystemDialog.animationDuration) 1975 | 1976 | return filesystemDialog 1977 | end 1978 | 1979 | -------------------------------------------------------------------------------- 1980 | 1981 | function GUI.addFilesystemDialog(parentContainer, addPanel, ...) 1982 | local container = GUI.addBackgroundContainer(parentContainer, addPanel, false, nil) 1983 | 1984 | local filesystemDialog = container:addChild(GUI.filesystemDialog(1, 1, ...)) 1985 | filesystemDialog.localX = math.floor(container.width / 2 - filesystemDialog.width / 2) 1986 | filesystemDialog.localY = -filesystemDialog.height 1987 | 1988 | local function onAnyTouch() 1989 | container:remove() 1990 | filesystemDialog.firstParent:draw() 1991 | end 1992 | 1993 | filesystemDialog.cancelButton.onTouch = function() 1994 | onAnyTouch() 1995 | 1996 | if filesystemDialog.onCancel then 1997 | filesystemDialog.onCancel() 1998 | end 1999 | end 2000 | 2001 | filesystemDialog.submitButton.onTouch = function() 2002 | onAnyTouch() 2003 | 2004 | local path = filesystemDialog.filesystemTree.selectedItem or filesystemDialog.filesystemTree.workPath or "/" 2005 | if filesystemDialog.IOMode == GUI.IO_MODE_SAVE then 2006 | path = path .. filesystemDialog.input.text 2007 | 2008 | if filesystemDialog.filesystemMode == GUI.IO_MODE_FILE then 2009 | local selectedItem = filesystemDialog.extensionComboBox:getItem(filesystemDialog.extensionComboBox.selectedItem) 2010 | path = path .. (selectedItem and selectedItem.text or "") 2011 | else 2012 | path = path .. "/" 2013 | end 2014 | end 2015 | 2016 | if filesystemDialog.onSubmit then 2017 | filesystemDialog.onSubmit(path) 2018 | end 2019 | end 2020 | 2021 | filesystemDialog.show = filesystemDialogShow 2022 | 2023 | return filesystemDialog 2024 | end 2025 | 2026 | -------------------------------------------------------------------------------- 2027 | 2028 | local function filesystemChooserDraw(object) 2029 | local tipWidth = object.height * 2 - 1 2030 | local y = math.floor(object.y + object.height / 2) 2031 | 2032 | buffer.drawRectangle(object.x, object.y, object.width - tipWidth, object.height, object.colors.background, object.colors.text, " ") 2033 | buffer.drawRectangle(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, " ") 2034 | buffer.drawText(object.x + object.width - math.floor(tipWidth / 2) - 1, y, object.pressed and object.colors.tipBackground or object.colors.tipText, "…") 2035 | buffer.drawText(object.x + 1, y, object.colors.text, string.limit(object.path or object.placeholderText, object.width - tipWidth - 2, "left")) 2036 | 2037 | return filesystemChooser 2038 | end 2039 | 2040 | local function filesystemChooserAddExtensionFilter(object, extension) 2041 | object.extensionFilters[unicode.lower(extension)] = true 2042 | end 2043 | 2044 | local function filesystemChooserSetMode(object, IOMode, filesystemMode) 2045 | object.IOMode = IOMode 2046 | object.filesystemMode = filesystemMode 2047 | end 2048 | 2049 | local function filesystemChooserEventHandler(application, object, e1) 2050 | if e1 == "touch" then 2051 | object.pressed = true 2052 | application:draw() 2053 | 2054 | local filesystemDialog = GUI.addFilesystemDialog(application, false, 50, math.floor(application.height * 0.8), object.submitButtonText, object.cancelButtonText, object.placeholderText, object.filesystemDialogPath) 2055 | 2056 | for key in pairs(object.extensionFilters) do 2057 | filesystemDialog:addExtensionFilter(key) 2058 | end 2059 | 2060 | filesystemDialog:setMode(object.IOMode, object.filesystemMode) 2061 | 2062 | if object.path and #object.path > 0 then 2063 | -- local path = object.path:gsub("/+", "/") 2064 | filesystemDialog.filesystemTree.selectedItem = object.IOMode == GUI.IO_MODE_OPEN and object.path or filesystem.path(object.path) 2065 | filesystemDialog.input.text = filesystem.name(object.path) 2066 | filesystemDialog:expandPath(object.IOMode == GUI.IO_MODE_OPEN and filesystem.path(object.path) or filesystem.path(filesystem.path(object.path))) 2067 | end 2068 | 2069 | filesystemDialog.onCancel = function() 2070 | object.pressed = false 2071 | application:draw() 2072 | end 2073 | 2074 | filesystemDialog.onSubmit = function(path) 2075 | object.path = path 2076 | filesystemDialog.onCancel() 2077 | if object.onSubmit then 2078 | object.onSubmit(object.path) 2079 | end 2080 | end 2081 | 2082 | filesystemDialog:show() 2083 | end 2084 | end 2085 | 2086 | function GUI.filesystemChooser(x, y, width, height, backgroundColor, textColor, tipBackgroundColor, tipTextColor, path, submitButtonText, cancelButtonText, placeholderText, filesystemDialogPath) 2087 | local object = GUI.object(x, y, width, height) 2088 | 2089 | object.eventHandler = comboBoxEventHandler 2090 | object.colors = { 2091 | tipBackground = tipBackgroundColor, 2092 | tipText = tipTextColor, 2093 | text = textColor, 2094 | background = backgroundColor 2095 | } 2096 | 2097 | object.submitButtonText = submitButtonText 2098 | object.cancelButtonText = cancelButtonText 2099 | object.placeholderText = placeholderText 2100 | object.pressed = false 2101 | object.path = path 2102 | object.filesystemDialogPath = filesystemDialogPath 2103 | object.filesystemMode = GUI.IO_MODE_FILE 2104 | object.IOMode = GUI.IO_MODE_OPEN 2105 | object.extensionFilters = {} 2106 | 2107 | object.draw = filesystemChooserDraw 2108 | object.eventHandler = filesystemChooserEventHandler 2109 | object.addExtensionFilter = filesystemChooserAddExtensionFilter 2110 | object.setMode = filesystemChooserSetMode 2111 | 2112 | return object 2113 | end 2114 | 2115 | -------------------------------------------------------------------------------- 2116 | 2117 | local function resizerDraw(object) 2118 | local horizontalMode, x, y, symbol = object.width >= object.height 2119 | 2120 | if horizontalMode then 2121 | buffer.drawText(object.x, math.floor(object.y + object.height / 2), object.colors.helper, string.rep("━", object.width)) 2122 | 2123 | if object.lastTouchX then 2124 | buffer.drawText(object.lastTouchX, object.lastTouchY, object.colors.arrow, "↑") 2125 | end 2126 | else 2127 | local x = math.floor(object.x + object.width / 2) 2128 | local bufferWidth, bufferHeight, index = buffer.getResolution() 2129 | 2130 | for i = object.y, object.y + object.height - 1 do 2131 | if x >= 1 and x <= bufferWidth and i >= 1 and i <= bufferHeight then 2132 | index = buffer.getIndex(x, i) 2133 | buffer.rawSet(index, buffer.rawGet(index), object.colors.helper, "┃") 2134 | end 2135 | end 2136 | 2137 | if object.lastTouchX then 2138 | buffer.drawText(object.lastTouchX - 1, object.lastTouchY, object.colors.arrow, "←→") 2139 | end 2140 | end 2141 | end 2142 | 2143 | local function resizerEventHandler(application, object, e1, e2, e3, e4) 2144 | if e1 == "touch" then 2145 | object.lastTouchX, object.lastTouchY = e3, e4 2146 | application:draw() 2147 | elseif e1 == "drag" and object.lastTouchX then 2148 | if object.onResize then 2149 | object.onResize(e3 - object.lastTouchX, e4 - object.lastTouchY) 2150 | end 2151 | 2152 | object.lastTouchX, object.lastTouchY = e3, e4 2153 | application:draw() 2154 | elseif e1 == "drop" then 2155 | if object.onResizeFinished then 2156 | object.onResizeFinished() 2157 | end 2158 | 2159 | object.lastTouchX, object.lastTouchY = nil, nil 2160 | application:draw() 2161 | end 2162 | end 2163 | 2164 | function GUI.resizer(x, y, width, height, helperColor, arrowColor) 2165 | local object = GUI.object(x, y, width, height) 2166 | 2167 | object.colors = { 2168 | helper = helperColor, 2169 | arrow = arrowColor 2170 | } 2171 | 2172 | object.draw = resizerDraw 2173 | object.eventHandler = resizerEventHandler 2174 | 2175 | return object 2176 | end 2177 | 2178 | -------------------------------------------------------------------------------- 2179 | 2180 | local function scrollBarDraw(scrollBar) 2181 | local isVertical = scrollBar.height > scrollBar.width 2182 | local valuesDelta = scrollBar.maximumValue - scrollBar.minimumValue 2183 | local part = scrollBar.value / valuesDelta 2184 | 2185 | if isVertical then 2186 | local barSize = math.ceil(scrollBar.shownValueCount / valuesDelta * scrollBar.height) 2187 | local halfBarSize = math.floor(barSize / 2) 2188 | 2189 | scrollBar.ghostPosition.y = scrollBar.y + halfBarSize 2190 | scrollBar.ghostPosition.height = scrollBar.height - barSize 2191 | 2192 | if scrollBar.thin then 2193 | local y1 = math.floor(scrollBar.ghostPosition.y + part * scrollBar.ghostPosition.height - halfBarSize) 2194 | local y2 = y1 + barSize - 1 2195 | local background 2196 | 2197 | for y = scrollBar.y, scrollBar.y + scrollBar.height - 1 do 2198 | background = buffer.get(scrollBar.x, y) 2199 | buffer.set(scrollBar.x, y, background, y >= y1 and y <= y2 and scrollBar.colors.foreground or scrollBar.colors.background, "┃") 2200 | end 2201 | else 2202 | buffer.drawRectangle(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ") 2203 | buffer.drawRectangle( 2204 | scrollBar.x, 2205 | math.floor(scrollBar.ghostPosition.y + part * scrollBar.ghostPosition.height - halfBarSize), 2206 | scrollBar.width, 2207 | barSize, 2208 | scrollBar.colors.foreground, 0x0, " " 2209 | ) 2210 | end 2211 | else 2212 | local barSize = math.ceil(scrollBar.shownValueCount / valuesDelta * scrollBar.width) 2213 | local halfBarSize = math.floor(barSize / 2) 2214 | 2215 | scrollBar.ghostPosition.x = scrollBar.x + halfBarSize 2216 | scrollBar.ghostPosition.width = scrollBar.width - barSize 2217 | 2218 | if scrollBar.thin then 2219 | local x1 = math.floor(scrollBar.ghostPosition.x + part * scrollBar.ghostPosition.width - halfBarSize) 2220 | local x2 = x1 + barSize - 1 2221 | local background 2222 | 2223 | for x = scrollBar.x, scrollBar.x + scrollBar.width - 1 do 2224 | background = buffer.get(x, scrollBar.y) 2225 | buffer.set(x, scrollBar.y, background, x >= x1 and x <= x2 and scrollBar.colors.foreground or scrollBar.colors.background, "⠤") 2226 | end 2227 | else 2228 | buffer.drawRectangle(scrollBar.x, scrollBar.y, scrollBar.width, scrollBar.height, scrollBar.colors.background, scrollBar.colors.foreground, " ") 2229 | buffer.drawRectangle( 2230 | math.floor(scrollBar.ghostPosition.x + part * scrollBar.ghostPosition.width - halfBarSize), 2231 | scrollBar.y, 2232 | barSize, 2233 | scrollBar.height, 2234 | scrollBar.colors.foreground, 0x0, " " 2235 | ) 2236 | end 2237 | end 2238 | 2239 | return scrollBar 2240 | end 2241 | 2242 | local function scrollBarEventHandler(application, object, e1, e2, e3, e4, e5, ...) 2243 | local newValue = object.value 2244 | 2245 | if e1 == "touch" or e1 == "drag" then 2246 | if object.height > object.width then 2247 | if e4 == object.y + object.height - 1 then 2248 | newValue = object.maximumValue 2249 | else 2250 | newValue = object.minimumValue + (e4 - object.y) / object.height * (object.maximumValue - object.minimumValue) 2251 | end 2252 | else 2253 | if e3 == object.x + object.width - 1 then 2254 | newValue = object.maximumValue 2255 | else 2256 | newValue = object.minimumValue + (e3 - object.x) / object.width * (object.maximumValue - object.minimumValue) 2257 | end 2258 | end 2259 | elseif e1 == "scroll" then 2260 | if e5 == 1 then 2261 | if object.value >= object.minimumValue + object.onScrollValueIncrement then 2262 | newValue = object.value - object.onScrollValueIncrement 2263 | else 2264 | newValue = object.minimumValue 2265 | end 2266 | else 2267 | if object.value <= object.maximumValue - object.onScrollValueIncrement then 2268 | newValue = object.value + object.onScrollValueIncrement 2269 | else 2270 | newValue = object.maximumValue 2271 | end 2272 | end 2273 | end 2274 | 2275 | if e1 == "touch" or e1 == "drag" or e1 == "scroll" then 2276 | object.value = newValue 2277 | if object.onTouch then 2278 | object.onTouch(application, object, e1, e2, e3, e4, e5, ...) 2279 | end 2280 | 2281 | application:draw() 2282 | end 2283 | end 2284 | 2285 | function GUI.scrollBar(x, y, width, height, backgroundColor, foregroundColor, minimumValue, maximumValue, value, shownValueCount, onScrollValueIncrement, thin) 2286 | local scrollBar = GUI.object(x, y, width, height) 2287 | 2288 | scrollBar.eventHandler = scrollBarEventHandler 2289 | scrollBar.maximumValue = maximumValue 2290 | scrollBar.minimumValue = minimumValue 2291 | scrollBar.value = value 2292 | scrollBar.onScrollValueIncrement = onScrollValueIncrement 2293 | scrollBar.shownValueCount = shownValueCount 2294 | scrollBar.thin = thin 2295 | scrollBar.colors = { 2296 | background = backgroundColor, 2297 | foreground = foregroundColor, 2298 | } 2299 | scrollBar.ghostPosition = {} 2300 | scrollBar.draw = scrollBarDraw 2301 | 2302 | return scrollBar 2303 | end 2304 | 2305 | -------------------------------------------------------------------------------- 2306 | 2307 | local function treeDraw(tree) 2308 | local y, yEnd, showScrollBar = tree.y, tree.y + tree.height - 1, #tree.items > tree.height 2309 | local textLimit = tree.width - (showScrollBar and 1 or 0) 2310 | 2311 | if tree.colors.default.background then 2312 | buffer.drawRectangle(tree.x, tree.y, tree.width, tree.height, tree.colors.default.background, tree.colors.default.expandable, " ") 2313 | end 2314 | 2315 | for i = tree.fromItem, #tree.items do 2316 | local textColor, arrowColor, text = tree.colors.default.notExpandable, tree.colors.default.arrow, tree.items[i].expandable and "■ " or "□ " 2317 | 2318 | if tree.selectedItem == tree.items[i].definition then 2319 | textColor, arrowColor = tree.colors.selected.any, tree.colors.selected.arrow 2320 | buffer.drawRectangle(tree.x, y, tree.width, 1, tree.colors.selected.background, textColor, " ") 2321 | else 2322 | if tree.items[i].expandable then 2323 | textColor = tree.colors.default.expandable 2324 | elseif tree.items[i].disabled then 2325 | textColor = tree.colors.disabled 2326 | end 2327 | end 2328 | 2329 | if tree.items[i].expandable then 2330 | buffer.drawText(tree.x + tree.items[i].offset, y, arrowColor, tree.expandedItems[tree.items[i].definition] and "▽" or "▷") 2331 | end 2332 | 2333 | buffer.drawText(tree.x + tree.items[i].offset + 2, y, textColor, unicode.sub(text .. tree.items[i].name, 1, textLimit - tree.items[i].offset - 2)) 2334 | 2335 | y = y + 1 2336 | if y > yEnd then break end 2337 | end 2338 | 2339 | if showScrollBar then 2340 | local scrollBar = tree.scrollBar 2341 | scrollBar.x = tree.x + tree.width - 1 2342 | scrollBar.y = tree.y 2343 | scrollBar.width = 1 2344 | scrollBar.height = tree.height 2345 | scrollBar.colors.background = tree.colors.scrollBar.background 2346 | scrollBar.colors.foreground = tree.colors.scrollBar.foreground 2347 | scrollBar.minimumValue = 1 2348 | scrollBar.maximumValue = #tree.items 2349 | scrollBar.value = tree.fromItem 2350 | scrollBar.shownValueCount = tree.height 2351 | scrollBar.onScrollValueIncrement = 1 2352 | scrollBar.thin = true 2353 | 2354 | scrollBar:draw() 2355 | end 2356 | 2357 | return tree 2358 | end 2359 | 2360 | local function treeEventHandler(application, tree, e1, e2, e3, e4, e5, ...) 2361 | if e1 == "touch" then 2362 | local i = e4 - tree.y + tree.fromItem 2363 | if tree.items[i] then 2364 | if 2365 | tree.items[i].expandable and 2366 | ( 2367 | tree.selectionMode == GUI.IO_MODE_FILE or 2368 | e3 >= tree.x + tree.items[i].offset - 1 and e3 <= tree.x + tree.items[i].offset + 1 2369 | ) 2370 | then 2371 | if tree.expandedItems[tree.items[i].definition] then 2372 | tree.expandedItems[tree.items[i].definition] = nil 2373 | else 2374 | tree.expandedItems[tree.items[i].definition] = true 2375 | end 2376 | 2377 | if tree.onItemExpanded then 2378 | tree.onItemExpanded(tree.selectedItem, e1, e2, e3, e4, e5, ...) 2379 | end 2380 | else 2381 | if 2382 | ( 2383 | tree.selectionMode == GUI.IO_MODE_BOTH or 2384 | tree.selectionMode == GUI.IO_MODE_DIRECTORY and tree.items[i].expandable or 2385 | tree.selectionMode == GUI.IO_MODE_FILE 2386 | ) and not tree.items[i].disabled 2387 | then 2388 | tree.selectedItem = tree.items[i].definition 2389 | 2390 | if tree.onItemSelected then 2391 | tree.onItemSelected(tree.selectedItem, e1, e2, e3, e4, e5, ...) 2392 | end 2393 | end 2394 | end 2395 | 2396 | application:draw() 2397 | end 2398 | elseif e1 == "scroll" then 2399 | if e5 == 1 then 2400 | if tree.fromItem > 1 then 2401 | tree.fromItem = tree.fromItem - 1 2402 | application:draw() 2403 | end 2404 | else 2405 | if tree.fromItem < #tree.items then 2406 | tree.fromItem = tree.fromItem + 1 2407 | application:draw() 2408 | end 2409 | end 2410 | end 2411 | end 2412 | 2413 | local function treeAddItem(tree, name, definition, offset, expandable, disabled) 2414 | local item = { 2415 | name = name, 2416 | expandable = expandable, 2417 | offset = offset or 0, 2418 | definition = definition, 2419 | disabled = disabled 2420 | } 2421 | table.insert(tree.items, item) 2422 | 2423 | return item 2424 | end 2425 | 2426 | function GUI.tree(x, y, width, height, backgroundColor, expandableColor, notExpandableColor, arrowColor, backgroundSelectedColor, anySelectionColor, arrowSelectionColor, disabledColor, scrollBarBackground, scrollBarForeground, showMode, selectionMode) 2427 | local tree = GUI.object(x, y, width, height) 2428 | 2429 | tree.eventHandler = treeEventHandler 2430 | tree.colors = { 2431 | default = { 2432 | background = backgroundColor, 2433 | expandable = expandableColor, 2434 | notExpandable = notExpandableColor, 2435 | arrow = arrowColor, 2436 | }, 2437 | selected = { 2438 | background = backgroundSelectedColor, 2439 | any = anySelectionColor, 2440 | arrow = arrowSelectionColor, 2441 | }, 2442 | scrollBar = { 2443 | background = scrollBarBackground, 2444 | foreground = scrollBarForeground 2445 | }, 2446 | disabled = disabledColor 2447 | } 2448 | tree.items = {} 2449 | tree.fromItem = 1 2450 | tree.selectedItem = nil 2451 | tree.expandedItems = {} 2452 | 2453 | tree.scrollBar = GUI.scrollBar(1, 1, 1, 1, 0x0, 0x0, 1, 1, 1, 1, 1) 2454 | 2455 | tree.showMode = showMode 2456 | tree.selectionMode = selectionMode 2457 | tree.eventHandler = treeEventHandler 2458 | tree.addItem = treeAddItem 2459 | tree.draw = treeDraw 2460 | 2461 | return tree 2462 | end 2463 | 2464 | -------------------------------------------------------------------------------- 2465 | 2466 | local function filesystemTreeUpdateFileListRecursively(tree, path, offset) 2467 | local list = {} 2468 | for file in filesystem.list(path) do 2469 | table.insert(list, file) 2470 | end 2471 | 2472 | local i, expandables = 1, {} 2473 | while i <= #list do 2474 | if filesystem.isDirectory(path .. list[i]) then 2475 | table.insert(expandables, list[i]) 2476 | table.remove(list, i) 2477 | else 2478 | i = i + 1 2479 | end 2480 | end 2481 | 2482 | table.sort(expandables, function(a, b) return unicode.lower(a) < unicode.lower(b) end) 2483 | table.sort(list, function(a, b) return unicode.lower(a) < unicode.lower(b) end) 2484 | 2485 | if tree.showMode == GUI.IO_MODE_BOTH or tree.showMode == GUI.IO_MODE_DIRECTORY then 2486 | for i = 1, #expandables do 2487 | tree:addItem(filesystem.name(expandables[i]), path .. expandables[i], offset, true) 2488 | 2489 | if tree.expandedItems[path .. expandables[i]] then 2490 | filesystemTreeUpdateFileListRecursively(tree, path .. expandables[i], offset + 2) 2491 | end 2492 | end 2493 | end 2494 | 2495 | if tree.showMode == GUI.IO_MODE_BOTH or tree.showMode == GUI.IO_MODE_FILE then 2496 | for i = 1, #list do 2497 | tree:addItem(list[i], path .. list[i], offset, false, tree.extensionFilters and not tree.extensionFilters[filesystem.extension(path .. list[i], true)] or false) 2498 | end 2499 | end 2500 | end 2501 | 2502 | local function filesystemTreeUpdateFileList(tree) 2503 | tree.items = {} 2504 | filesystemTreeUpdateFileListRecursively(tree, tree.workPath, 1) 2505 | end 2506 | 2507 | local function filesystemTreeAddExtensionFilter(tree, extensionFilter) 2508 | tree.extensionFilters = tree.extensionFilters or {} 2509 | tree.extensionFilters[unicode.lower(extensionFilter)] = true 2510 | end 2511 | 2512 | local function filesystemTreeExpandPath(tree, path) 2513 | local blyadina = tree.workPath 2514 | for pizda in path:gmatch("[^/]+") do 2515 | blyadina = blyadina .. pizda .. "/" 2516 | tree.expandedItems[blyadina] = true 2517 | end 2518 | end 2519 | 2520 | function GUI.filesystemTree(...) 2521 | local tree = GUI.tree(...) 2522 | 2523 | tree.workPath = "/" 2524 | tree.updateFileList = filesystemTreeUpdateFileList 2525 | tree.addExtensionFilter = filesystemTreeAddExtensionFilter 2526 | tree.expandPath = filesystemTreeExpandPath 2527 | tree.onItemExpanded = function() 2528 | tree:updateFileList() 2529 | end 2530 | 2531 | return tree 2532 | end 2533 | 2534 | -------------------------------------------------------------------------------- 2535 | 2536 | local function textBoxUpdate(object) 2537 | local doubleVerticalOffset = object.offset.vertical * 2 2538 | object.textWidth = object.width - object.offset.horizontal * 2 - (object.scrollBarEnabled and 1 or 0) 2539 | 2540 | object.linesCopy = {} 2541 | 2542 | if object.autoWrap then 2543 | for i = 1, #object.lines do 2544 | local isTable = type(object.lines[i]) == "table" 2545 | for subLine in (isTable and object.lines[i].text or object.lines[i]):gmatch("[^\n]+") do 2546 | local wrappedLine = string.wrap(subLine, object.textWidth) 2547 | for j = 1, #wrappedLine do 2548 | table.insert(object.linesCopy, isTable and {text = wrappedLine[j], color = object.lines[i].color} or wrappedLine[j]) 2549 | end 2550 | end 2551 | end 2552 | else 2553 | for i = 1, #object.lines do 2554 | table.insert(object.linesCopy, object.lines[i]) 2555 | end 2556 | end 2557 | 2558 | if object.autoHeight then 2559 | object.height = #object.linesCopy + doubleVerticalOffset 2560 | end 2561 | 2562 | object.textHeight = object.height - doubleVerticalOffset 2563 | end 2564 | 2565 | local function textBoxDraw(object) 2566 | object:update() 2567 | 2568 | if object.colors.background then 2569 | buffer.drawRectangle(object.x, object.y, object.width, object.height, object.colors.background, object.colors.text, " ", object.colors.transparency) 2570 | end 2571 | 2572 | local x, y = nil, object.y + object.offset.vertical 2573 | local lineType, text, textColor 2574 | for i = object.currentLine, object.currentLine + object.textHeight - 1 do 2575 | if object.linesCopy[i] then 2576 | lineType = type(object.linesCopy[i]) 2577 | if lineType == "string" then 2578 | text, textColor = string.limit(object.linesCopy[i], object.textWidth), object.colors.text 2579 | elseif lineType == "table" then 2580 | text, textColor = string.limit(object.linesCopy[i].text, object.textWidth), object.linesCopy[i].color 2581 | else 2582 | error("Unknown TextBox line type: " .. tostring(lineType)) 2583 | end 2584 | 2585 | x = GUI.getAlignmentCoordinates( 2586 | object.x + object.offset.horizontal, 2587 | 1, 2588 | object.textWidth, 2589 | 1, 2590 | object.horizontalAlignment, 2591 | object.verticalAlignment, 2592 | unicode.len(text), 2593 | 1 2594 | ) 2595 | 2596 | buffer.drawText(math.floor(x), y, textColor, text) 2597 | y = y + 1 2598 | else 2599 | break 2600 | end 2601 | end 2602 | 2603 | if object.scrollBarEnabled and object.textHeight < #object.lines then 2604 | object.scrollBar.x = object.x + object.width - 1 2605 | object.scrollBar.y = object.y 2606 | object.scrollBar.height = object.height 2607 | object.scrollBar.maximumValue = #object.lines - object.textHeight + 1 2608 | object.scrollBar.value = object.currentLine 2609 | object.scrollBar.shownValueCount = object.textHeight 2610 | 2611 | object.scrollBar:draw() 2612 | end 2613 | 2614 | return object 2615 | end 2616 | 2617 | local function scrollDownTextBox(object, count) 2618 | count = math.min(count or 1, #object.lines - object.height - object.currentLine + object.offset.vertical * 2 + 1) 2619 | if #object.lines >= object.height and object.currentLine < #object.lines - count then 2620 | object.currentLine = object.currentLine + count 2621 | end 2622 | 2623 | return object 2624 | end 2625 | 2626 | local function scrollUpTextBox(object, count) 2627 | count = count or 1 2628 | if object.currentLine > count and object.currentLine >= 1 then object.currentLine = object.currentLine - count end 2629 | return object 2630 | end 2631 | 2632 | local function scrollToStartTextBox(object) 2633 | object.currentLine = 1 2634 | return object 2635 | end 2636 | 2637 | local function scrollToEndTextBox(object) 2638 | if #object.lines > object.textHeight then 2639 | object.currentLine = #object.lines - object.textHeight + 1 2640 | end 2641 | 2642 | return object 2643 | end 2644 | 2645 | local function textBoxScrollEventHandler(application, object, e1, e2, e3, e4, e5) 2646 | if e1 == "scroll" then 2647 | if e5 == 1 then 2648 | object:scrollUp() 2649 | else 2650 | object:scrollDown() 2651 | end 2652 | 2653 | application:draw() 2654 | end 2655 | end 2656 | 2657 | function GUI.textBox(x, y, width, height, backgroundColor, textColor, lines, currentLine, horizontalOffset, verticalOffset, autoWrap, autoHeight) 2658 | local object = GUI.object(x, y, width, height) 2659 | 2660 | object.colors = { 2661 | text = textColor, 2662 | background = backgroundColor 2663 | } 2664 | object.lines = lines 2665 | object.currentLine = currentLine or 1 2666 | object.scrollUp = scrollUpTextBox 2667 | object.scrollDown = scrollDownTextBox 2668 | object.scrollToStart = scrollToStartTextBox 2669 | object.scrollToEnd = scrollToEndTextBox 2670 | object.offset = {horizontal = horizontalOffset or 0, vertical = verticalOffset or 0} 2671 | object.autoWrap = autoWrap 2672 | object.autoHeight = autoHeight 2673 | object.scrollBar = GUI.scrollBar(1, 1, 1, 1, 0xC3C3C3, 0x4B4B4B, 1, 1, 1, 1, 1, true) 2674 | object.scrollBarEnabled = false 2675 | 2676 | object.eventHandler = textBoxScrollEventHandler 2677 | object.draw = textBoxDraw 2678 | object.update = textBoxUpdate 2679 | 2680 | object.setAlignment = GUI.setAlignment 2681 | object:setAlignment(GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP) 2682 | object:update() 2683 | 2684 | return object 2685 | end 2686 | 2687 | -------------------------------------------------------------------------------- 2688 | 2689 | local function inputSetCursorPosition(input, newPosition) 2690 | if newPosition < 1 then 2691 | newPosition = 1 2692 | elseif newPosition > unicode.len(input.text) + 1 then 2693 | newPosition = unicode.len(input.text) + 1 2694 | end 2695 | 2696 | if newPosition > input.textCutFrom + input.width - 1 - input.textOffset * 2 then 2697 | input.textCutFrom = input.textCutFrom + newPosition - (input.textCutFrom + input.width - 1 - input.textOffset * 2) 2698 | elseif newPosition < input.textCutFrom then 2699 | input.textCutFrom = newPosition 2700 | end 2701 | 2702 | input.cursorPosition = newPosition 2703 | 2704 | return input 2705 | end 2706 | 2707 | local function inputTextDrawMethod(x, y, color, text) 2708 | buffer.drawText(x, y, color, text) 2709 | end 2710 | 2711 | local function inputDraw(input) 2712 | local background, foreground, transparency, text 2713 | if input.focused then 2714 | background, transparency = input.colors.focused.background, input.colors.focused.transparency 2715 | if input.text == "" then 2716 | input.textCutFrom = 1 2717 | foreground, text = input.colors.placeholderText, input.text 2718 | else 2719 | foreground = input.colors.focused.text 2720 | if input.textMask then 2721 | text = string.rep(input.textMask, unicode.len(input.text)) 2722 | else 2723 | text = input.text 2724 | end 2725 | end 2726 | else 2727 | background, transparency = input.colors.default.background, input.colors.default.transparency 2728 | if input.text == "" then 2729 | input.textCutFrom = 1 2730 | foreground, text = input.colors.placeholderText, input.placeholderText 2731 | else 2732 | foreground = input.colors.default.text 2733 | if input.textMask then 2734 | text = string.rep(input.textMask, unicode.len(input.text)) 2735 | else 2736 | text = input.text 2737 | end 2738 | end 2739 | end 2740 | 2741 | if background then 2742 | buffer.drawRectangle(input.x, input.y, input.width, input.height, background, foreground, " ", transparency) 2743 | end 2744 | 2745 | local y = input.y + math.floor(input.height / 2) 2746 | 2747 | input.textDrawMethod( 2748 | input.x + input.textOffset, 2749 | y, 2750 | foreground, 2751 | unicode.sub( 2752 | text or "", 2753 | input.textCutFrom, 2754 | input.textCutFrom + input.width - 1 - input.textOffset * 2 2755 | ) 2756 | ) 2757 | 2758 | if input.cursorBlinkState then 2759 | local index = buffer.getIndex(input.x + input.cursorPosition - input.textCutFrom + input.textOffset, y) 2760 | local background = buffer.rawGet(index) 2761 | buffer.rawSet(index, background, input.colors.cursor, input.cursorSymbol) 2762 | end 2763 | end 2764 | 2765 | local function inputCursorBlink(application, input, state) 2766 | input.cursorBlinkState = state 2767 | input.cursorBlinkUptime = computer.uptime() 2768 | application:draw() 2769 | end 2770 | 2771 | local function inputStopInput(application, input) 2772 | input.stopInputObject:remove() 2773 | input.focused = false 2774 | 2775 | if input.validator then 2776 | if not input.validator(input.text) then 2777 | input.text = input.startText 2778 | input.startText = nil 2779 | 2780 | input:setCursorPosition(unicode.len(input.text) + 1) 2781 | end 2782 | end 2783 | 2784 | if input.onInputFinished then 2785 | input.onInputFinished(application, input) 2786 | end 2787 | 2788 | inputCursorBlink(application, input, false) 2789 | end 2790 | 2791 | local function inputStartInput(input) 2792 | input.startText = input.text 2793 | input.focused = true 2794 | 2795 | if input.historyEnabled then 2796 | input.historyIndex = input.historyIndex + 1 2797 | end 2798 | 2799 | if input.eraseTextOnFocus then 2800 | input.text = "" 2801 | end 2802 | 2803 | input:setCursorPosition(unicode.len(input.text) + 1) 2804 | 2805 | input.stopInputObject.width, input.stopInputObject.height = input.firstParent.width, input.firstParent.height 2806 | input.firstParent:addChild(input.stopInputObject) 2807 | 2808 | inputCursorBlink(input.firstParent, input, true) 2809 | end 2810 | 2811 | local function inputEventHandler(application, input, e1, e2, e3, e4, e5, e6) 2812 | if e1 == "touch" or e1 == "drag" then 2813 | if input.focused then 2814 | input:setCursorPosition(input.textCutFrom + e3 - input.x - input.textOffset) 2815 | inputCursorBlink(application, input, true) 2816 | else 2817 | input:startInput() 2818 | end 2819 | elseif e1 == "key_down" and input.focused then 2820 | application:consumeEvent() 2821 | 2822 | -- Return 2823 | if e4 == 28 then 2824 | if input.historyEnabled then 2825 | -- Очистка истории 2826 | for i = 1, (#input.history - input.historyLimit) do 2827 | table.remove(input.history, 1) 2828 | end 2829 | 2830 | -- Добавление введенных данных в историю 2831 | if input.history[#input.history] ~= input.text and unicode.len(input.text) > 0 then 2832 | table.insert(input.history, input.text) 2833 | end 2834 | input.historyIndex = #input.history 2835 | end 2836 | 2837 | inputStopInput(application, input) 2838 | return 2839 | -- Arrows up/down/left/right 2840 | elseif e4 == 200 then 2841 | if input.historyEnabled and #input.history > 0 then 2842 | -- Добавление уже введенного текста в историю при стрелке вверх 2843 | if input.historyIndex == #input.history + 1 and unicode.len(input.text) > 0 then 2844 | input.history[input.historyIndex] = input.text 2845 | end 2846 | 2847 | input.historyIndex = input.historyIndex - 1 2848 | if input.historyIndex > #input.history then 2849 | input.historyIndex = #input.history 2850 | elseif input.historyIndex < 1 then 2851 | input.historyIndex = 1 2852 | end 2853 | 2854 | input.text = input.history[input.historyIndex] 2855 | input:setCursorPosition(unicode.len(input.text) + 1) 2856 | end 2857 | elseif e4 == 208 then 2858 | if input.historyEnabled and #input.history > 0 then 2859 | input.historyIndex = input.historyIndex + 1 2860 | if input.historyIndex > #input.history then 2861 | input.historyIndex = #input.history 2862 | elseif input.historyIndex < 1 then 2863 | input.historyIndex = 1 2864 | end 2865 | 2866 | input.text = input.history[input.historyIndex] 2867 | input:setCursorPosition(unicode.len(input.text) + 1) 2868 | end 2869 | elseif e4 == 203 then 2870 | input:setCursorPosition(input.cursorPosition - 1) 2871 | elseif e4 == 205 then 2872 | input:setCursorPosition(input.cursorPosition + 1) 2873 | -- Backspace 2874 | elseif e4 == 14 then 2875 | input.text = unicode.sub(unicode.sub(input.text, 1, input.cursorPosition - 1), 1, -2) .. unicode.sub(input.text, input.cursorPosition, -1) 2876 | input:setCursorPosition(input.cursorPosition - 1) 2877 | -- Delete 2878 | elseif e4 == 211 then 2879 | input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. unicode.sub(input.text, input.cursorPosition + 1, -1) 2880 | else 2881 | local char = unicode.char(e3) 2882 | if not keyboard.isControl(e3) then 2883 | input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. char .. unicode.sub(input.text, input.cursorPosition, -1) 2884 | input:setCursorPosition(input.cursorPosition + 1) 2885 | end 2886 | end 2887 | 2888 | inputCursorBlink(application, input, true) 2889 | elseif e1 == "clipboard" and input.focused then 2890 | input.text = unicode.sub(input.text, 1, input.cursorPosition - 1) .. e3 .. unicode.sub(input.text, input.cursorPosition, -1) 2891 | input:setCursorPosition(input.cursorPosition + unicode.len(e3)) 2892 | 2893 | inputCursorBlink(application, input, true) 2894 | application:consumeEvent() 2895 | elseif not e1 and input.focused and computer.uptime() - input.cursorBlinkUptime > input.cursorBlinkDelay then 2896 | inputCursorBlink(application, input, not input.cursorBlinkState) 2897 | end 2898 | end 2899 | 2900 | function GUI.input(x, y, width, height, backgroundColor, textColor, placeholderTextColor, backgroundFocusedColor, textFocusedColor, text, placeholderText, eraseTextOnFocus, textMask) 2901 | local input = GUI.object(x, y, width, height) 2902 | 2903 | input.colors = { 2904 | default = { 2905 | background = backgroundColor, 2906 | text = textColor 2907 | }, 2908 | focused = { 2909 | background = backgroundFocusedColor, 2910 | text = textFocusedColor 2911 | }, 2912 | placeholderText = placeholderTextColor, 2913 | cursor = 0x00A8FF 2914 | } 2915 | 2916 | input.text = text or "" 2917 | input.placeholderText = placeholderText 2918 | input.eraseTextOnFocus = eraseTextOnFocus 2919 | input.textMask = textMask 2920 | 2921 | input.textOffset = 1 2922 | input.textCutFrom = 1 2923 | input.cursorPosition = 1 2924 | input.cursorSymbol = "┃" 2925 | input.cursorBlinkDelay = 0.4 2926 | input.cursorBlinkState = false 2927 | input.textMask = textMask 2928 | input.setCursorPosition = inputSetCursorPosition 2929 | 2930 | input.history = {} 2931 | input.historyLimit = 20 2932 | input.historyIndex = 0 2933 | input.historyEnabled = false 2934 | 2935 | input.stopInputObject = GUI.object(1, 1, 1, 1) 2936 | input.stopInputObject.eventHandler = function(application, object, e1, e2, e3, e4, ...) 2937 | if e1 == "touch" or e1 == "drop" then 2938 | if 2939 | e3 >= input.x and 2940 | e3 < input.x + input.width and 2941 | e4 >= input.y and 2942 | e4 < input.y + input.height 2943 | then 2944 | input.eventHandler(application, input, e1, e2, e3, e4, ...) 2945 | else 2946 | inputStopInput(application, input) 2947 | end 2948 | end 2949 | end 2950 | 2951 | input.textDrawMethod = inputTextDrawMethod 2952 | input.draw = inputDraw 2953 | input.eventHandler = inputEventHandler 2954 | input.startInput = inputStartInput 2955 | 2956 | return input 2957 | end 2958 | 2959 | -------------------------------------------------------------------------------- 2960 | 2961 | local function autoCompleteDraw(object) 2962 | local y, yEnd = object.y, object.y + object.height - 1 2963 | 2964 | buffer.drawRectangle(object.x, object.y, object.width, object.height, object.colors.default.background, object.colors.default.text, " ") 2965 | 2966 | for i = object.fromItem, object.itemCount do 2967 | local textColor, textMatchColor = object.colors.default.text, object.colors.default.textMatch 2968 | if i == object.selectedItem then 2969 | buffer.drawRectangle(object.x, y, object.width, 1, object.colors.selected.background, object.colors.selected.text, " ") 2970 | textColor, textMatchColor = object.colors.selected.text, object.colors.selected.textMatch 2971 | end 2972 | 2973 | buffer.drawText(object.x + 1, y, textMatchColor, unicode.sub(object.matchText, 1, object.width - 2)) 2974 | buffer.drawText(object.x + object.matchTextLength + 1, y, textColor, unicode.sub(object.items[i], object.matchTextLength + 1, object.matchTextLength + object.width - object.matchTextLength - 2)) 2975 | 2976 | y = y + 1 2977 | if y > yEnd then 2978 | break 2979 | end 2980 | end 2981 | 2982 | if object.itemCount > object.height then 2983 | object.scrollBar.x = object.x + object.width - 1 2984 | object.scrollBar.y = object.y 2985 | object.scrollBar.height = object.height 2986 | object.scrollBar.maximumValue = object.itemCount - object.height + 2 2987 | object.scrollBar.value = object.fromItem 2988 | object.scrollBar.shownValueCount = object.height 2989 | 2990 | object.scrollBar:draw() 2991 | end 2992 | end 2993 | 2994 | local function autoCompleteScroll(application, object, direction) 2995 | if object.itemCount >= object.height then 2996 | object.fromItem = object.fromItem + direction 2997 | if object.fromItem < 1 then 2998 | object.fromItem = 1 2999 | elseif object.fromItem > object.itemCount - object.height + 1 then 3000 | object.fromItem = object.itemCount - object.height + 1 3001 | end 3002 | end 3003 | end 3004 | 3005 | local function autoCompleteEventHandler(application, object, e1, e2, e3, e4, e5, ...) 3006 | if e1 == "touch" then 3007 | object.selectedItem = e4 - object.y + object.fromItem 3008 | application:draw() 3009 | 3010 | if object.onItemSelected then 3011 | os.sleep(0.2) 3012 | object.onItemSelected(application, object, e1, e2, e3, e4, e5, ...) 3013 | end 3014 | elseif e1 == "scroll" then 3015 | autoCompleteScroll(application, object, -e5) 3016 | application:draw() 3017 | elseif e1 == "key_down" then 3018 | if e4 == 28 then 3019 | if object.onItemSelected then 3020 | object.onItemSelected(application, object, e1, e2, e3, e4, e5, ...) 3021 | end 3022 | elseif e4 == 200 then 3023 | object.selectedItem = object.selectedItem - 1 3024 | if object.selectedItem < 1 then 3025 | object.selectedItem = 1 3026 | end 3027 | 3028 | if object.selectedItem == object.fromItem - 1 then 3029 | autoCompleteScroll(application, object, -1) 3030 | end 3031 | 3032 | application:draw() 3033 | elseif e4 == 208 then 3034 | object.selectedItem = object.selectedItem + 1 3035 | if object.selectedItem > object.itemCount then 3036 | object.selectedItem = object.itemCount 3037 | end 3038 | 3039 | if object.selectedItem == object.fromItem + object.height then 3040 | autoCompleteScroll(application, object, 1) 3041 | end 3042 | 3043 | application:draw() 3044 | end 3045 | end 3046 | end 3047 | 3048 | local function autoCompleteClear(object) 3049 | object.items = {} 3050 | object.itemCount = 0 3051 | object.fromItem = 1 3052 | object.selectedItem = 1 3053 | object.height = 0 3054 | end 3055 | 3056 | local function autoCompleteMatch(object, variants, text, asKey) 3057 | object:clear() 3058 | 3059 | if asKey then 3060 | if text then 3061 | for key in pairs(variants) do 3062 | if key ~= text and key:match("^" .. text) then 3063 | table.insert(object.items,key) 3064 | end 3065 | end 3066 | else 3067 | for key in pairs(variants) do 3068 | table.insert(object.items, key) 3069 | end 3070 | end 3071 | else 3072 | if text then 3073 | for i = 1, #variants do 3074 | if variants[i] ~= text and variants[i]:match("^" .. text) then 3075 | table.insert(object.items, variants[i]) 3076 | end 3077 | end 3078 | else 3079 | for i = 1, #variants do 3080 | table.insert(object.items, variants[i]) 3081 | end 3082 | end 3083 | end 3084 | 3085 | object.matchText = text or "" 3086 | object.matchTextLength = unicode.len(object.matchText) 3087 | 3088 | table.sort(object.items, function(a, b) return unicode.lower(a) < unicode.lower(b) end) 3089 | 3090 | object.itemCount = #object.items 3091 | object.height = math.min(object.itemCount, object.maximumHeight) 3092 | 3093 | return object 3094 | end 3095 | 3096 | function GUI.autoComplete(x, y, width, maximumHeight, backgroundColor, textColor, textMatchColor, backgroundSelectedColor, textSelectedColor, textMatchSelectedColor, scrollBarBackground, scrollBarForeground) 3097 | local object = GUI.object(x, y, width, maximumHeight) 3098 | 3099 | object.colors = { 3100 | default = { 3101 | background = backgroundColor, 3102 | text = textColor, 3103 | textMatch = textMatchColor 3104 | }, 3105 | selected = { 3106 | background = backgroundSelectedColor, 3107 | text = textSelectedColor, 3108 | textMatch = textMatchSelectedColor 3109 | } 3110 | } 3111 | 3112 | object.maximumHeight = maximumHeight 3113 | object.fromItem = 1 3114 | object.selectedItem = 1 3115 | object.items = {} 3116 | object.matchText = " " 3117 | object.matchTextLength = 1 3118 | object.itemCount = 0 3119 | 3120 | object.scrollBar = GUI.scrollBar(1, 1, 1, 1, scrollBarBackground, scrollBarForeground, 1, 1, 1, 1, 1, true) 3121 | 3122 | object.match = autoCompleteMatch 3123 | object.draw = autoCompleteDraw 3124 | object.eventHandler = autoCompleteEventHandler 3125 | object.clear = autoCompleteClear 3126 | 3127 | object:clear() 3128 | 3129 | return object 3130 | end 3131 | 3132 | -------------------------------------------------------------------------------- 3133 | 3134 | local function brailleCanvasDraw(brailleCanvas) 3135 | local index, background, foreground, symbol 3136 | for y = 1, brailleCanvas.height do 3137 | for x = 1, brailleCanvas.width do 3138 | index = buffer.getIndex(brailleCanvas.x + x - 1, brailleCanvas.y + y - 1) 3139 | background, foreground, symbol = buffer.rawGet(index) 3140 | buffer.rawSet(index, background, brailleCanvas.pixels[y][x][9], brailleCanvas.pixels[y][x][10]) 3141 | end 3142 | end 3143 | 3144 | return brailleCanvas 3145 | end 3146 | 3147 | local function brailleCanvasSet(brailleCanvas, x, y, state, color) 3148 | local xReal, yReal = math.ceil(x / 2), math.ceil(y / 4) 3149 | 3150 | brailleCanvas.pixels[yReal][xReal][(y - (yReal - 1) * 4 - 1) * 2 + x - (xReal - 1) * 2] = state and 1 or 0 3151 | brailleCanvas.pixels[yReal][xReal][9] = color or brailleCanvas.pixels[yReal][xReal][9] 3152 | brailleCanvas.pixels[yReal][xReal][10] = unicode.char( 3153 | 10240 + 3154 | 128 * brailleCanvas.pixels[yReal][xReal][8] + 3155 | 64 * brailleCanvas.pixels[yReal][xReal][7] + 3156 | 32 * brailleCanvas.pixels[yReal][xReal][6] + 3157 | 16 * brailleCanvas.pixels[yReal][xReal][4] + 3158 | 8 * brailleCanvas.pixels[yReal][xReal][2] + 3159 | 4 * brailleCanvas.pixels[yReal][xReal][5] + 3160 | 2 * brailleCanvas.pixels[yReal][xReal][3] + 3161 | brailleCanvas.pixels[yReal][xReal][1] 3162 | ) 3163 | 3164 | return brailleCanvas 3165 | end 3166 | 3167 | local function brailleCanvasGet(brailleCanvas, x, y) 3168 | local xReal, yReal = math.ceil(x / 2), math.ceil(y / 4) 3169 | return brailleCanvas.pixels[yReal][xReal][(y - (yReal - 1) * 4 - 1) * 2 + x - (xReal - 1) * 2], brailleCanvas.pixels[yReal][xReal][9], brailleCanvas.pixels[yReal][xReal][10] 3170 | end 3171 | 3172 | local function brailleCanvasFill(brailleCanvas, x, y, width, height, state, color) 3173 | for j = y, y + height - 1 do 3174 | for i = x, x + width - 1 do 3175 | brailleCanvas:set(i, j, state, color) 3176 | end 3177 | end 3178 | end 3179 | 3180 | local function brailleCanvasClear(brailleCanvas) 3181 | for j = 1, brailleCanvas.height * 4 do 3182 | brailleCanvas.pixels[j] = {} 3183 | for i = 1, brailleCanvas.width * 2 do 3184 | brailleCanvas.pixels[j][i] = { 0, 0, 0, 0, 0, 0, 0, 0, 0x0, " " } 3185 | end 3186 | end 3187 | end 3188 | 3189 | function GUI.brailleCanvas(x, y, width, height) 3190 | local brailleCanvas = GUI.object(x, y, width, height) 3191 | 3192 | brailleCanvas.pixels = {} 3193 | 3194 | brailleCanvas.get = brailleCanvasGet 3195 | brailleCanvas.set = brailleCanvasSet 3196 | brailleCanvas.fill = brailleCanvasFill 3197 | brailleCanvas.clear = brailleCanvasClear 3198 | 3199 | brailleCanvas.draw = brailleCanvasDraw 3200 | 3201 | brailleCanvas:clear() 3202 | 3203 | return brailleCanvas 3204 | end 3205 | 3206 | -------------------------------------------------------------------------------- 3207 | 3208 | local function paletteShow(palette) 3209 | local application = GUI.application() 3210 | 3211 | application:addChild(palette) 3212 | 3213 | palette.submitButton.onTouch = function() 3214 | application:stop() 3215 | end 3216 | palette.cancelButton.onTouch = palette.submitButton.onTouch 3217 | 3218 | application:draw() 3219 | application:start() 3220 | 3221 | return palette.color.integer 3222 | end 3223 | 3224 | function GUI.palette(x, y, startColor) 3225 | local palette = GUI.window(x, y, 71, 25) 3226 | 3227 | palette.color = {hsb = {}, rgb = {}} 3228 | palette:addChild(GUI.panel(1, 1, palette.width, palette.height, 0xF0F0F0)) 3229 | 3230 | local bigImage = palette:addChild(GUI.image(1, 1, image.create(50, 25))) 3231 | local bigCrest = palette:addChild(GUI.object(1, 1, 5, 3)) 3232 | 3233 | local function paletteDrawBigCrestPixel(x, y, symbol) 3234 | local background, foreground = buffer.get(x, y) 3235 | local r, g, b = color.integerToRGB(background) 3236 | buffer.set(x, y, background, (r + g + b) / 3 >= 127 and 0x0 or 0xFFFFFF, symbol) 3237 | end 3238 | 3239 | bigCrest.draw = function(object) 3240 | paletteDrawBigCrestPixel(object.x, object.y + 1, "─") 3241 | paletteDrawBigCrestPixel(object.x + 1, object.y + 1, "─") 3242 | paletteDrawBigCrestPixel(object.x + 3, object.y + 1, "─") 3243 | paletteDrawBigCrestPixel(object.x + 4, object.y + 1, "─") 3244 | paletteDrawBigCrestPixel(object.x + 2, object.y, "│") 3245 | paletteDrawBigCrestPixel(object.x + 2, object.y + 2, "│") 3246 | end 3247 | bigCrest.passScreenEvents = true 3248 | 3249 | local miniImage = palette:addChild(GUI.image(53, 1, image.create(3, 25))) 3250 | 3251 | local miniCrest = palette:addChild(GUI.object(52, 1, 5, 1)) 3252 | miniCrest.draw = function(object) 3253 | buffer.drawText(object.x, object.y, 0x0, ">") 3254 | buffer.drawText(object.x + 4, object.y, 0x0, "<") 3255 | end 3256 | 3257 | local colorPanel = palette:addChild(GUI.panel(58, 2, 12, 3, 0x0)) 3258 | palette.submitButton = palette:addChild(GUI.roundedButton(58, 6, 12, 1, 0x4B4B4B, 0xFFFFFF, 0x2D2D2D, 0xFFFFFF, "OK")) 3259 | palette.cancelButton = palette:addChild(GUI.roundedButton(58, 8, 12, 1, 0xFFFFFF, 0x696969, 0x2D2D2D, 0xFFFFFF, "Cancel")) 3260 | 3261 | local function paletteRefreshBigImage() 3262 | local saturationStep, brightnessStep, saturation, brightness = 1 / bigImage.width, 1 / bigImage.height, 0, 1 3263 | for j = 1, bigImage.height do 3264 | for i = 1, bigImage.width do 3265 | image.set(bigImage.image, i, j, color.optimize(color.HSBToInteger(palette.color.hsb.hue, saturation, brightness)), 0x0, 0x0, " ") 3266 | saturation = saturation + saturationStep 3267 | end 3268 | saturation, brightness = 0, brightness - brightnessStep 3269 | end 3270 | end 3271 | 3272 | local function paletteRefreshMiniImage() 3273 | local hueStep, hue = 360 / miniImage.height, 0 3274 | for j = 1, miniImage.height do 3275 | for i = 1, miniImage.width do 3276 | image.set(miniImage.image, i, j, color.optimize(color.HSBToInteger(hue, 1, 1)), 0x0, 0, " ") 3277 | end 3278 | hue = hue + hueStep 3279 | end 3280 | end 3281 | 3282 | local function paletteUpdateCrestsCoordinates() 3283 | bigCrest.localX = math.floor((bigImage.width - 1) * palette.color.hsb.saturation) - 1 3284 | bigCrest.localY = math.floor((bigImage.height - 1) - (bigImage.height - 1) * palette.color.hsb.brightness) 3285 | miniCrest.localY = math.ceil(palette.color.hsb.hue / 360 * miniImage.height + 0.5) 3286 | end 3287 | 3288 | local inputs 3289 | 3290 | local function paletteUpdateInputs() 3291 | inputs[1].text = tostring(palette.color.rgb.red) 3292 | inputs[2].text = tostring(palette.color.rgb.green) 3293 | inputs[3].text = tostring(palette.color.rgb.blue) 3294 | inputs[4].text = tostring(math.floor(palette.color.hsb.hue)) 3295 | inputs[5].text = tostring(math.floor(palette.color.hsb.saturation * 100)) 3296 | inputs[6].text = tostring(math.floor(palette.color.hsb.brightness * 100)) 3297 | inputs[7].text = string.format("%06X", palette.color.integer) 3298 | colorPanel.colors.background = palette.color.integer 3299 | end 3300 | 3301 | local function paletteSwitchColorFromHex(hex) 3302 | palette.color.integer = hex 3303 | palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue = color.integerToRGB(hex) 3304 | palette.color.hsb.hue, palette.color.hsb.saturation, palette.color.hsb.brightness = color.RGBToHSB(palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue) 3305 | paletteUpdateInputs() 3306 | end 3307 | 3308 | local function paletteSwitchColorFromHsb(hue, saturation, brightness) 3309 | palette.color.hsb.hue, palette.color.hsb.saturation, palette.color.hsb.brightness = hue, saturation, brightness 3310 | palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue = color.HSBToRGB(hue, saturation, brightness) 3311 | palette.color.integer = color.RGBToInteger(palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue) 3312 | paletteUpdateInputs() 3313 | end 3314 | 3315 | local function paletteSwitchColorFromRgb(red, green, blue) 3316 | palette.color.rgb.red, palette.color.rgb.green, palette.color.rgb.blue = red, green, blue 3317 | palette.color.hsb.hue, palette.color.hsb.saturation, palette.color.hsb.brightness = color.RGBToHSB(red, green, blue) 3318 | palette.color.integer = color.RGBToInteger(red, green, blue) 3319 | paletteUpdateInputs() 3320 | end 3321 | 3322 | local function onAnyInputFinished() 3323 | paletteRefreshBigImage() 3324 | paletteUpdateCrestsCoordinates() 3325 | palette.firstParent:draw() 3326 | end 3327 | 3328 | local function onHexInputFinished() 3329 | paletteSwitchColorFromHex(tonumber("0x" .. inputs[7].text)) 3330 | onAnyInputFinished() 3331 | end 3332 | 3333 | local function onRgbInputFinished() 3334 | paletteSwitchColorFromRgb(tonumber(inputs[1].text), tonumber(inputs[2].text), tonumber(inputs[3].text)) 3335 | onAnyInputFinished() 3336 | end 3337 | 3338 | local function onHsbInputFinished() 3339 | paletteSwitchColorFromHsb(tonumber(inputs[4].text), tonumber(inputs[5].text) / 100, tonumber(inputs[6].text) / 100) 3340 | onAnyInputFinished() 3341 | end 3342 | 3343 | local function rgbValidaror(text) 3344 | local number = tonumber(text) if number and number >= 0 and number <= 255 then return true end 3345 | end 3346 | 3347 | local function hValidator(text) 3348 | local number = tonumber(text) if number and number >= 0 and number <= 359 then return true end 3349 | end 3350 | 3351 | local function sbValidator(text) 3352 | local number = tonumber(text) if number and number >= 0 and number <= 100 then return true end 3353 | end 3354 | 3355 | local function hexValidator(text) 3356 | if string.match(text, "^[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]$") then 3357 | return true 3358 | end 3359 | end 3360 | 3361 | inputs = { 3362 | { shortcut = "R:", validator = rgbValidaror, onInputFinished = onRgbInputFinished }, 3363 | { shortcut = "G:", validator = rgbValidaror, onInputFinished = onRgbInputFinished }, 3364 | { shortcut = "B:", validator = rgbValidaror, onInputFinished = onRgbInputFinished }, 3365 | { shortcut = "H:", validator = hValidator, onInputFinished = onHsbInputFinished }, 3366 | { shortcut = "S:", validator = sbValidator, onInputFinished = onHsbInputFinished }, 3367 | { shortcut = "L:", validator = sbValidator, onInputFinished = onHsbInputFinished }, 3368 | { shortcut = "0x", validator = hexValidator, onInputFinished = onHexInputFinished } 3369 | } 3370 | 3371 | local y = 10 3372 | for i = 1, #inputs do 3373 | palette:addChild(GUI.label(58, y, 2, 1, 0x0, inputs[i].shortcut)) 3374 | 3375 | local validator, onInputFinished = inputs[i].validator, inputs[i].onInputFinished 3376 | inputs[i] = palette:addChild(GUI.input(61, y, 9, 1, 0xFFFFFF, 0x696969, 0x696969, 0xFFFFFF, 0x0, "", "", true)) 3377 | inputs[i].validator = validator 3378 | inputs[i].onInputFinished = onInputFinished 3379 | 3380 | y = y + 2 3381 | end 3382 | 3383 | local favourites 3384 | if filesystem.exists(GUI.PALETTE_CONFIG_PATH) then 3385 | favourites = table.fromFile(GUI.PALETTE_CONFIG_PATH) 3386 | else 3387 | favourites = {} 3388 | for i = 1, 6 do favourites[i] = color.HSBToInteger(math.random(0, 360), 1, 1) end 3389 | table.toFile(GUI.PALETTE_CONFIG_PATH, favourites) 3390 | end 3391 | 3392 | local favouritesContainer = palette:addChild(GUI.container(58, 24, 12, 1)) 3393 | for i = 1, #favourites do 3394 | favouritesContainer:addChild(GUI.button(i * 2 - 1, 1, 2, 1, favourites[i], 0x0, 0x0, 0x0, " ")).onTouch = function(application) 3395 | paletteSwitchColorFromHex(favourites[i]) 3396 | paletteRefreshBigImage() 3397 | paletteUpdateCrestsCoordinates() 3398 | application:draw() 3399 | end 3400 | end 3401 | 3402 | palette:addChild(GUI.button(58, 25, 12, 1, 0xFFFFFF, 0x4B4B4B, 0x2D2D2D, 0xFFFFFF, "+")).onTouch = function(application) 3403 | local favouriteExists = false 3404 | for i = 1, #favourites do 3405 | if favourites[i] == palette.color.integer then 3406 | favouriteExists = true 3407 | break 3408 | end 3409 | end 3410 | 3411 | if not favouriteExists then 3412 | table.insert(favourites, 1, palette.color.integer) 3413 | table.remove(favourites, #favourites) 3414 | for i = 1, #favourites do 3415 | favouritesContainer.children[i].colors.default.background = favourites[i] 3416 | favouritesContainer.children[i].colors.pressed.background = 0x0 3417 | end 3418 | 3419 | table.toFile(GUI.PALETTE_CONFIG_PATH, favourites) 3420 | 3421 | application:draw() 3422 | end 3423 | end 3424 | 3425 | bigImage.eventHandler = function(application, object, e1, e2, e3, e4) 3426 | if e1 == "touch" or e1 == "drag" then 3427 | bigCrest.localX, bigCrest.localY = e3 - palette.x - 1, e4 - palette.y 3428 | paletteSwitchColorFromHex(select(3, component.gpu.get(e3, e4))) 3429 | application:draw() 3430 | end 3431 | end 3432 | 3433 | miniImage.eventHandler = function(application, object, e1, e2, e3, e4) 3434 | if e1 == "touch" or e1 == "drag" then 3435 | miniCrest.localY = e4 - palette.y + 1 3436 | paletteSwitchColorFromHsb((e4 - miniImage.y) * 360 / miniImage.height, palette.color.hsb.saturation, palette.color.hsb.brightness) 3437 | paletteRefreshBigImage() 3438 | application:draw() 3439 | end 3440 | end 3441 | 3442 | palette.show = paletteShow 3443 | 3444 | paletteSwitchColorFromHex(startColor) 3445 | paletteUpdateCrestsCoordinates() 3446 | paletteRefreshBigImage() 3447 | paletteRefreshMiniImage() 3448 | 3449 | return palette 3450 | end 3451 | 3452 | -------------------------------------------------------------------------------- 3453 | 3454 | local function textUpdate(object) 3455 | object.width = unicode.len(object.text) 3456 | return object 3457 | end 3458 | 3459 | local function textDraw(object) 3460 | object:update() 3461 | buffer.drawText(object.x, object.y, object.color, object.text) 3462 | return object 3463 | end 3464 | 3465 | function GUI.text(x, y, color, text) 3466 | local object = GUI.object(x, y, 1, 1) 3467 | 3468 | object.text = text 3469 | object.color = color 3470 | object.update = textUpdate 3471 | object.draw = textDraw 3472 | object:update() 3473 | 3474 | return object 3475 | end 3476 | 3477 | -------------------------------------------------------------------------------- 3478 | 3479 | function GUI.addBackgroundContainer(parentContainer, addPanel, addLayout, title) 3480 | local container = parentContainer:addChild(GUI.container(1, 1, parentContainer.width, parentContainer.height)) 3481 | 3482 | if addPanel then 3483 | container.panel = container:addChild(GUI.panel(1, 1, container.width, container.height, GUI.BACKGROUND_CONTAINER_PANEL_COLOR, GUI.BACKGROUND_CONTAINER_PANEL_TRANSPARENCY)) 3484 | container.panel.eventHandler = function(parentContainer, object, e1) 3485 | if e1 == "touch" then 3486 | container:remove() 3487 | parentContainer:draw() 3488 | end 3489 | end 3490 | end 3491 | 3492 | if addLayout then 3493 | container.layout = container:addChild(GUI.layout(1, 1, container.width, container.height, 1, 1)) 3494 | 3495 | if title then 3496 | container.label = container.layout:addChild(GUI.label(1, 1, 1, 1, GUI.BACKGROUND_CONTAINER_TITLE_COLOR, title)):setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP) 3497 | end 3498 | end 3499 | 3500 | return container 3501 | end 3502 | 3503 | -------------------------------------------------------------------------------- 3504 | 3505 | local function listUpdate(list) 3506 | local step, child = false 3507 | for i = 1, #list.children do 3508 | child = list.children[i] 3509 | -- Жмяканье пизды 3510 | child.pressed = i == list.selectedItem 3511 | 3512 | -- Цвет залупы 3513 | if step then 3514 | child.colors.default = list.colors.alternating 3515 | else 3516 | child.colors.default = list.colors.default 3517 | end 3518 | child.colors.pressed, step = list.colors.selected, not step 3519 | 3520 | -- Размеры хуйни 3521 | if list.cells[1][1].direction == GUI.DIRECTION_HORIZONTAL then 3522 | if list.offsetMode then 3523 | child.width, child.height = list.itemSize * 2 + unicode.len(child.text), list.height 3524 | else 3525 | child.width, child.height = list.itemSize, list.height 3526 | end 3527 | else 3528 | if list.offsetMode then 3529 | child.width, child.height = list.width, list.itemSize * 2 + 1 3530 | else 3531 | child.width, child.height = list.width, list.itemSize 3532 | end 3533 | end 3534 | end 3535 | 3536 | layoutUpdate(list) 3537 | end 3538 | 3539 | local function listItemEventHandler(application, item, e1, ...) 3540 | if e1 == "touch" or e1 == "drag" then 3541 | item.parent.selectedItem = item:indexOf() 3542 | item.parent:update() 3543 | application:draw() 3544 | 3545 | if item.onTouch then 3546 | item.onTouch(application, item, e1, ...) 3547 | end 3548 | end 3549 | end 3550 | 3551 | local function listAddItem(list, text) 3552 | local item = list:addChild(pressable(1, 1, 1, 1, 0, 0, 0, 0, 0, 0, text)) 3553 | 3554 | item.switchMode = true 3555 | item.eventHandler = listItemEventHandler 3556 | 3557 | return item 3558 | end 3559 | 3560 | local function listSetAlignment(list, ...) 3561 | layoutSetAlignment(list, 1, 1, ...) 3562 | return list 3563 | end 3564 | 3565 | local function listSetSpacing(list, ...) 3566 | layoutSetSpacing(list, 1, 1, ...) 3567 | return list 3568 | end 3569 | 3570 | local function listSetDirection(list, ...) 3571 | layoutSetDirection(list, 1, 1, ...) 3572 | return list 3573 | end 3574 | 3575 | local function listSetFitting(list, ...) 3576 | layoutSetFitting(list, 1, 1, ...) 3577 | return list 3578 | end 3579 | 3580 | local function listSetMargin(list, ...) 3581 | layoutSetMargin(list, 1, 1, ...) 3582 | return list 3583 | end 3584 | 3585 | local function listGetMargin(list, ...) 3586 | return layoutGetMargin(list, 1, 1, ...) 3587 | end 3588 | 3589 | local function listGetItem(list, what) 3590 | if type(what) == "number" then 3591 | return list.children[what] 3592 | else 3593 | for i = 1, #list.children do 3594 | if list.children[i].text == what then 3595 | return list.children[i], i 3596 | end 3597 | end 3598 | end 3599 | end 3600 | 3601 | local function listCount(list) 3602 | return #list.children 3603 | end 3604 | 3605 | local function listDraw(list) 3606 | buffer.drawRectangle(list.x, list.y, list.width, list.height, list.colors.default.background, list.colors.default.text, " ") 3607 | layoutDraw(list) 3608 | end 3609 | 3610 | function GUI.list(x, y, width, height, itemSize, spacing, backgroundColor, textColor, backgroundAlternatingColor, textAlternatingColor, backgroundSelectedColor, textSelectedColor, offsetMode) 3611 | local list = GUI.layout(x, y, width, height, 1, 1) 3612 | 3613 | list.colors = { 3614 | default = { 3615 | background = backgroundColor, 3616 | text = textColor 3617 | }, 3618 | alternating = { 3619 | background = backgroundAlternatingColor, 3620 | text = textAlternatingColor 3621 | }, 3622 | selected = { 3623 | background = backgroundSelectedColor, 3624 | text = textSelectedColor 3625 | }, 3626 | } 3627 | 3628 | list.passScreenEvents = false 3629 | list.selectedItem = 1 3630 | list.offsetMode = offsetMode 3631 | list.itemSize = itemSize 3632 | 3633 | list.addItem = listAddItem 3634 | list.getItem = listGetItem 3635 | list.count = listCount 3636 | list.setAlignment = listSetAlignment 3637 | list.setSpacing = listSetSpacing 3638 | list.setDirection = listSetDirection 3639 | list.setFitting = listSetFitting 3640 | list.setMargin = listSetMargin 3641 | list.getMargin = listGetMargin 3642 | list.update = listUpdate 3643 | list.draw = listDraw 3644 | 3645 | list:setAlignment(GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP) 3646 | list:setSpacing(spacing) 3647 | list:setDirection(GUI.DIRECTION_VERTICAL) 3648 | 3649 | return list 3650 | end 3651 | 3652 | --------------------------------------------------------------------------------------------------- 3653 | 3654 | local function keyAndValueUpdate(object) 3655 | object.keyLength, object.valueLength = unicode.len(object.key), unicode.len(object.value) 3656 | object.width = object.keyLength + object.valueLength 3657 | end 3658 | 3659 | local function keyAndValueDraw(object) 3660 | keyAndValueUpdate(object) 3661 | buffer.drawText(object.x, object.y, object.colors.key, object.key) 3662 | buffer.drawText(object.x + object.keyLength, object.y, object.colors.value, object.value) 3663 | end 3664 | 3665 | function GUI.keyAndValue(x, y, keyColor, valueColor, key, value) 3666 | local object = GUI.object(x, y, 1, 1) 3667 | 3668 | object.colors = { 3669 | key = keyColor, 3670 | value = valueColor 3671 | } 3672 | object.key = key 3673 | object.value = value 3674 | 3675 | object.update = keyAndValueUpdate 3676 | object.draw = keyAndValueDraw 3677 | 3678 | object:update() 3679 | 3680 | return object 3681 | end 3682 | 3683 | --------------------------------------------------------------------------------------------------- 3684 | 3685 | function GUI.highlightString(x, y, width, fromChar, indentationWidth, patterns, colorScheme, s) 3686 | fromChar = fromChar or 1 3687 | 3688 | local counter, symbols, colors, stringLength, bufferIndex, newFrameBackgrounds, newFrameForegrounds, newFrameSymbols, searchFrom, starting, ending = indentationWidth, {}, {}, unicode.len(s), buffer.getIndex(x, y), buffer.getNewFrameTables() 3689 | local toChar = math.min(stringLength, fromChar + width - 1) 3690 | 3691 | -- Пидорасим на символы 3692 | for i = fromChar, toChar do 3693 | symbols[i] = unicode.sub(s, i, i) 3694 | end 3695 | 3696 | -- Вгоняем в цветовую карту синтаксическую подсветку 3697 | for j = 1, #patterns, 4 do 3698 | searchFrom = 1 3699 | 3700 | while true do 3701 | starting, ending = string.unicodeFind(s, patterns[j], searchFrom) 3702 | 3703 | if starting then 3704 | for i = starting + patterns[j + 2], ending - patterns[j + 3] do 3705 | colors[i] = colorScheme[patterns[j + 1]] 3706 | end 3707 | else 3708 | break 3709 | end 3710 | 3711 | searchFrom = ending + 1 - patterns[j + 3] 3712 | end 3713 | end 3714 | 3715 | -- Ебошим индентейшны 3716 | for i = fromChar, toChar do 3717 | if symbols[i] == " " then 3718 | colors[i] = colorScheme.indentation 3719 | 3720 | if counter == indentationWidth then 3721 | symbols[i], counter = "│", 0 3722 | end 3723 | 3724 | counter = counter + 1 3725 | else 3726 | break 3727 | end 3728 | end 3729 | 3730 | -- Рисуем текст 3731 | for i = fromChar, toChar do 3732 | newFrameForegrounds[bufferIndex], newFrameSymbols[bufferIndex] = colors[i] or colorScheme.text, symbols[i] or " " 3733 | bufferIndex = bufferIndex + 1 3734 | end 3735 | end 3736 | 3737 | -------------------------------------------------------------------------------- 3738 | 3739 | local function dropDownMenuItemDraw(item) 3740 | local yText = item.y + math.floor(item.height / 2) 3741 | 3742 | if item.type == 1 then 3743 | local textColor = item.color or item.parent.parent.colors.default.text 3744 | 3745 | if item.pressed then 3746 | textColor = item.parent.parent.colors.selected.text 3747 | buffer.drawRectangle(item.x, item.y, item.width, item.height, item.parent.parent.colors.selected.background, textColor, " ") 3748 | elseif item.disabled then 3749 | textColor = item.parent.parent.colors.disabled.text 3750 | end 3751 | 3752 | buffer.drawText(item.x + 1, yText, textColor, item.text) 3753 | if item.shortcut then 3754 | buffer.drawText(item.x + item.width - unicode.len(item.shortcut) - 1, yText, textColor, item.shortcut) 3755 | end 3756 | else 3757 | buffer.drawText(item.x, yText, item.parent.parent.colors.separator, string.rep("─", item.width)) 3758 | end 3759 | 3760 | return item 3761 | end 3762 | 3763 | local function dropDownMenuReleaseItems(menu) 3764 | for i = 1, #menu.itemsContainer.children do 3765 | menu.itemsContainer.children[i].pressed = false 3766 | end 3767 | 3768 | return menu 3769 | end 3770 | 3771 | local function dropDownMenuItemEventHandler(application, object, e1, ...) 3772 | if e1 == "touch" then 3773 | if object.type == 1 and not object.pressed then 3774 | object.pressed = true 3775 | application:draw() 3776 | 3777 | if object.subMenu then 3778 | object.parent.parent.parent:addChild(object.subMenu:releaseItems()) 3779 | object.subMenu.localX = object.parent.parent.localX + object.parent.parent.width 3780 | object.subMenu.localY = object.parent.parent.localY + object.localY - 1 3781 | if buffer.getWidth() - object.parent.parent.localX - object.parent.parent.width + 1 < object.subMenu.width then 3782 | object.subMenu.localX = object.parent.parent.localX - object.subMenu.width 3783 | object.parent.parent:moveToFront() 3784 | end 3785 | 3786 | application:draw() 3787 | else 3788 | os.sleep(0.2) 3789 | 3790 | object.parent.parent.parent:remove() 3791 | 3792 | local objectIndex = object:indexOf() 3793 | for i = 2, #object.parent.parent.parent.children do 3794 | if object.parent.parent.parent.children[i].onMenuClosed then 3795 | object.parent.parent.parent.children[i].onMenuClosed(objectIndex) 3796 | end 3797 | end 3798 | 3799 | if object.onTouch then 3800 | object.onTouch() 3801 | end 3802 | 3803 | application:draw() 3804 | end 3805 | end 3806 | end 3807 | end 3808 | 3809 | local function dropDownMenuGetHeight(menu) 3810 | local height = 0 3811 | for i = 1, #menu.itemsContainer.children do 3812 | height = height + (menu.itemsContainer.children[i].type == 2 and 1 or menu.itemHeight) 3813 | end 3814 | 3815 | return height 3816 | end 3817 | 3818 | local function dropDownMenuReposition(menu) 3819 | menu.itemsContainer.width, menu.itemsContainer.height = menu.width, menu.height 3820 | menu.prevButton.width, menu.nextButton.width = menu.width, menu.width 3821 | menu.nextButton.localY = menu.height 3822 | 3823 | local y = menu.itemsContainer.children[1].localY 3824 | for i = 1, #menu.itemsContainer.children do 3825 | menu.itemsContainer.children[i].localY = y 3826 | menu.itemsContainer.children[i].width = menu.itemsContainer.width 3827 | y = y + menu.itemsContainer.children[i].height 3828 | end 3829 | 3830 | menu.prevButton.hidden = menu.itemsContainer.children[1].localY >= 1 3831 | menu.nextButton.hidden = menu.itemsContainer.children[#menu.itemsContainer.children].localY + menu.itemsContainer.children[#menu.itemsContainer.children].height - 1 <= menu.height 3832 | end 3833 | 3834 | local function dropDownMenuUpdate(menu) 3835 | if #menu.itemsContainer.children > 0 then 3836 | menu.height = math.min(dropDownMenuGetHeight(menu), menu.maximumHeight, buffer.getHeight() - menu.y) 3837 | dropDownMenuReposition(menu) 3838 | end 3839 | end 3840 | 3841 | local function dropDownMenuRemoveItem(menu, index) 3842 | table.remove(menu.itemsContainer.children, index) 3843 | 3844 | menu:update() 3845 | 3846 | return menu 3847 | end 3848 | 3849 | local function dropDownMenuAddItem(menu, text, disabled, shortcut, color) 3850 | local item = menu.itemsContainer:addChild(GUI.object(1, 1, 1, menu.itemHeight)) 3851 | item.type = 1 3852 | item.text = text 3853 | item.disabled = disabled 3854 | item.shortcut = shortcut 3855 | item.color = color 3856 | item.draw = dropDownMenuItemDraw 3857 | item.eventHandler = dropDownMenuItemEventHandler 3858 | 3859 | menu:update() 3860 | 3861 | return item 3862 | end 3863 | 3864 | local function dropDownMenuAddSeparator(menu) 3865 | local item = menu.itemsContainer:addChild(GUI.object(1, 1, 1, 1)) 3866 | item.type = 2 3867 | item.draw = dropDownMenuItemDraw 3868 | item.eventHandler = dropDownMenuItemEventHandler 3869 | 3870 | menu:update() 3871 | 3872 | return item 3873 | end 3874 | 3875 | local function dropDownMenuScrollDown(application, menu) 3876 | if menu.itemsContainer.children[1].localY < 1 then 3877 | for i = 1, #menu.itemsContainer.children do 3878 | menu.itemsContainer.children[i].localY = menu.itemsContainer.children[i].localY + 1 3879 | end 3880 | 3881 | dropDownMenuReposition(menu) 3882 | application:draw() 3883 | end 3884 | end 3885 | 3886 | local function dropDownMenuScrollUp(application, menu) 3887 | if menu.itemsContainer.children[#menu.itemsContainer.children].localY + menu.itemsContainer.children[#menu.itemsContainer.children].height - 1 > menu.height then 3888 | for i = 1, #menu.itemsContainer.children do 3889 | menu.itemsContainer.children[i].localY = menu.itemsContainer.children[i].localY - 1 3890 | end 3891 | 3892 | dropDownMenuReposition(menu) 3893 | application:draw() 3894 | end 3895 | end 3896 | 3897 | local function dropDownMenuEventHandler(application, menu, e1, e2, e3, e4, e5) 3898 | if e1 == "scroll" then 3899 | if e5 == 1 then 3900 | dropDownMenuScrollDown(application, menu) 3901 | else 3902 | dropDownMenuScrollUp(application, menu) 3903 | end 3904 | end 3905 | end 3906 | 3907 | local function dropDownMenuPrevButtonOnTouch(application, button) 3908 | dropDownMenuScrollDown(application, button.parent) 3909 | end 3910 | 3911 | local function dropDownMenuNextButtonOnTouch(application, button) 3912 | dropDownMenuScrollUp(application, button.parent) 3913 | end 3914 | 3915 | local function dropDownMenuDraw(menu) 3916 | buffer.drawRectangle(menu.x, menu.y, menu.width, menu.height, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency.background) 3917 | GUI.drawShadow(menu.x, menu.y, menu.width, menu.height, menu.colors.transparency.shadow, true) 3918 | containerDraw(menu) 3919 | end 3920 | 3921 | local function dropDownMenuBackgroundObjectEventHandler(application, object, e1) 3922 | if e1 == "touch" then 3923 | for i = 2, #object.parent.children do 3924 | if object.parent.children[i].onMenuClosed then 3925 | object.parent.children[i].onMenuClosed() 3926 | end 3927 | end 3928 | 3929 | object.parent:remove() 3930 | application:draw() 3931 | end 3932 | end 3933 | 3934 | local function dropDownMenuAdd(parentContainer, menu) 3935 | local container = parentContainer:addChild(GUI.container(1, 1, parentContainer.width, parentContainer.height)) 3936 | container:addChild(GUI.object(1, 1, container.width, container.height)).eventHandler = dropDownMenuBackgroundObjectEventHandler 3937 | 3938 | return container:addChild(menu:releaseItems()) 3939 | end 3940 | 3941 | function GUI.dropDownMenu(x, y, width, maximumHeight, itemHeight, backgroundColor, textColor, backgroundPressedColor, textPressedColor, disabledColor, separatorColor, backgroundTransparency, shadowTransparency) 3942 | local menu = GUI.container(x, y, width, 1) 3943 | 3944 | menu.colors = { 3945 | default = { 3946 | background = backgroundColor, 3947 | text = textColor 3948 | }, 3949 | selected = { 3950 | background = backgroundPressedColor, 3951 | text = textPressedColor 3952 | }, 3953 | disabled = { 3954 | text = disabledColor 3955 | }, 3956 | separator = separatorColor, 3957 | transparency = { 3958 | background = backgroundTransparency, 3959 | shadow = shadowTransparency 3960 | } 3961 | } 3962 | 3963 | menu.itemsContainer = menu:addChild(GUI.container(1, 1, menu.width, menu.height)) 3964 | menu.prevButton = menu:addChild(GUI.button(1, 1, menu.width, 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, "▲")) 3965 | menu.nextButton = menu:addChild(GUI.button(1, 1, menu.width, 1, backgroundColor, textColor, backgroundPressedColor, textPressedColor, "▼")) 3966 | menu.prevButton.colors.transparency, menu.nextButton.colors.transparency = backgroundTransparency, backgroundTransparency 3967 | menu.prevButton.onTouch = dropDownMenuPrevButtonOnTouch 3968 | menu.nextButton.onTouch = dropDownMenuNextButtonOnTouch 3969 | 3970 | menu.releaseItems = dropDownMenuReleaseItems 3971 | menu.itemHeight = itemHeight 3972 | menu.addSeparator = dropDownMenuAddSeparator 3973 | menu.addItem = dropDownMenuAddItem 3974 | menu.removeItem = dropDownMenuRemoveItem 3975 | menu.draw = dropDownMenuDraw 3976 | menu.maximumHeight = maximumHeight 3977 | menu.eventHandler = dropDownMenuEventHandler 3978 | menu.update = dropDownMenuUpdate 3979 | 3980 | return menu 3981 | end 3982 | 3983 | -------------------------------------------------------------------------------- 3984 | 3985 | local function contextMenuUpdate(menu) 3986 | if #menu.itemsContainer.children > 0 then 3987 | local widestItem, widestShortcut = 0, 0 3988 | for i = 1, #menu.itemsContainer.children do 3989 | if menu.itemsContainer.children[i].type == 1 then 3990 | widestItem = math.max(widestItem, unicode.len(menu.itemsContainer.children[i].text)) 3991 | if menu.itemsContainer.children[i].shortcut then 3992 | widestShortcut = math.max(widestShortcut, unicode.len(menu.itemsContainer.children[i].shortcut)) 3993 | end 3994 | end 3995 | end 3996 | 3997 | menu.width, menu.height = 2 + widestItem + (widestShortcut > 0 and 3 + widestShortcut or 0), math.min(dropDownMenuGetHeight(menu), menu.maximumHeight) 3998 | dropDownMenuReposition(menu) 3999 | 4000 | local bufferWidth, bufferHeight = buffer.getResolution() 4001 | if menu.x + menu.width + 1 >= bufferWidth then 4002 | menu.localX = bufferWidth - menu.width - 1 4003 | end 4004 | if menu.y + menu.height >= bufferHeight then 4005 | menu.localY = bufferHeight - menu.height 4006 | end 4007 | end 4008 | end 4009 | 4010 | local contextMenuCreate, contextMenuAddSubMenu 4011 | 4012 | contextMenuAddSubMenu = function(menu, text, disabled) 4013 | local item = menu:addItem(text, disabled, "►") 4014 | item.subMenu = contextMenuCreate(1, 1) 4015 | item.subMenu.colors = menu.colors 4016 | 4017 | return item.subMenu 4018 | end 4019 | 4020 | contextMenuCreate = function(x, y, backgroundColor, textColor, backgroundPressedColor, textPressedColor, disabledColor, separatorColor, backgroundTransparency, shadowTransparency) 4021 | local menu = GUI.dropDownMenu( 4022 | x, 4023 | y, 4024 | 1, 4025 | math.ceil(buffer.getHeight() * 0.5), 4026 | 1, 4027 | backgroundColor or GUI.CONTEXT_MENU_DEFAULT_BACKGROUND_COLOR, 4028 | textColor or GUI.CONTEXT_MENU_DEFAULT_TEXT_COLOR, 4029 | backgroundPressedColor or GUI.CONTEXT_MENU_PRESSED_BACKGROUND_COLOR, 4030 | textPressedColor or GUI.CONTEXT_MENU_PRESSED_TEXT_COLOR, 4031 | disabledColor or GUI.CONTEXT_MENU_DISABLED_COLOR, 4032 | separatorColor or GUI.CONTEXT_MENU_SEPARATOR_COLOR, 4033 | backgroundTransparency or GUI.CONTEXT_MENU_BACKGROUND_TRANSPARENCY, 4034 | shadowTransparency or GUI.CONTEXT_MENU_SHADOW_TRANSPARENCY 4035 | ) 4036 | 4037 | menu.update = contextMenuUpdate 4038 | menu.addSubMenu = contextMenuAddSubMenu 4039 | 4040 | return menu 4041 | end 4042 | 4043 | function GUI.addContextMenu(parentContainer, arg1, ...) 4044 | if type(arg1) == "table" then 4045 | return dropDownMenuAdd(parentContainer, arg1, ...) 4046 | else 4047 | return dropDownMenuAdd(parentContainer, contextMenuCreate(arg1, ...)) 4048 | end 4049 | end 4050 | 4051 | -------------------------------------------------------------------------------- 4052 | 4053 | local function comboBoxDraw(object) 4054 | buffer.drawRectangle(object.x, object.y, object.width, object.height, object.colors.default.background, object.colors.default.text, " ") 4055 | if object.dropDownMenu.itemsContainer.children[object.selectedItem] then 4056 | buffer.drawText(object.x + 1, math.floor(object.y + object.height / 2), object.colors.default.text, string.limit(object.dropDownMenu.itemsContainer.children[object.selectedItem].text, object.width - object.height - 2, "right")) 4057 | end 4058 | 4059 | local width = object.height * 2 - 1 4060 | buffer.drawRectangle(object.x + object.width - object.height * 2 + 1, object.y, width, object.height, object.colors.arrow.background, object.colors.arrow.text, " ") 4061 | buffer.drawText(math.floor(object.x + object.width - width / 2), math.floor(object.y + object.height / 2), object.colors.arrow.text, object.pressed and "▲" or "▼") 4062 | 4063 | return object 4064 | end 4065 | 4066 | local function comboBoxGetItem(object, what) 4067 | if type(what) == "number" then 4068 | return object.dropDownMenu.itemsContainer.children[what] 4069 | else 4070 | local children = object.dropDownMenu.itemsContainer.children 4071 | for i = 1, #children do 4072 | if children[i].text == what then 4073 | return children[i], i 4074 | end 4075 | end 4076 | end 4077 | end 4078 | 4079 | local function comboBoxRemoveItem(object, index) 4080 | object.dropDownMenu:removeItem(index) 4081 | if object.selectedItem > #object.dropDownMenu.itemsContainer.children then 4082 | object.selectedItem = #object.dropDownMenu.itemsContainer.children 4083 | end 4084 | end 4085 | 4086 | local function comboBoxCount(object) 4087 | return #object.dropDownMenu.itemsContainer.children 4088 | end 4089 | 4090 | local function comboBoxClear(object) 4091 | object.dropDownMenu.itemsContainer:removeChildren() 4092 | object.selectedItem = 1 4093 | 4094 | return object 4095 | end 4096 | 4097 | local function comboBoxEventHandler(application, object, e1, ...) 4098 | if e1 == "touch" and #object.dropDownMenu.itemsContainer.children > 0 then 4099 | object.pressed = true 4100 | object.dropDownMenu.x, object.dropDownMenu.y, object.dropDownMenu.width = object.x, object.y + object.height, object.width 4101 | object.dropDownMenu:update() 4102 | dropDownMenuAdd(application, object.dropDownMenu) 4103 | application:draw() 4104 | end 4105 | end 4106 | 4107 | local function comboBoxAddItem(object, ...) 4108 | return object.dropDownMenu:addItem(...) 4109 | end 4110 | 4111 | local function comboBoxAddSeparator(object) 4112 | return object.dropDownMenu:addSeparator() 4113 | end 4114 | 4115 | function GUI.comboBox(x, y, width, itemSize, backgroundColor, textColor, arrowBackgroundColor, arrowTextColor) 4116 | local comboBox = GUI.object(x, y, width, itemSize) 4117 | 4118 | comboBox.colors = { 4119 | default = { 4120 | background = backgroundColor, 4121 | text = textColor 4122 | }, 4123 | selected = { 4124 | background = GUI.CONTEXT_MENU_PRESSED_BACKGROUND_COLOR, 4125 | text = GUI.CONTEXT_MENU_PRESSED_TEXT_COLOR 4126 | }, 4127 | arrow = { 4128 | background = arrowBackgroundColor, 4129 | text = arrowTextColor 4130 | } 4131 | } 4132 | 4133 | comboBox.dropDownMenu = GUI.dropDownMenu( 4134 | 1, 4135 | 1, 4136 | 1, 4137 | math.ceil(buffer.getHeight() * 0.5), 4138 | itemSize, 4139 | comboBox.colors.default.background, 4140 | comboBox.colors.default.text, 4141 | comboBox.colors.selected.background, 4142 | comboBox.colors.selected.text, 4143 | GUI.CONTEXT_MENU_DISABLED_COLOR, 4144 | GUI.CONTEXT_MENU_SEPARATOR_COLOR, 4145 | GUI.CONTEXT_MENU_BACKGROUND_TRANSPARENCY, 4146 | GUI.CONTEXT_MENU_SHADOW_TRANSPARENCY 4147 | ) 4148 | 4149 | comboBox.dropDownMenu.onMenuClosed = function(index) 4150 | comboBox.pressed = false 4151 | comboBox.selectedItem = index or comboBox.selectedItem 4152 | comboBox.firstParent:draw() 4153 | 4154 | if index and comboBox.onItemSelected then 4155 | comboBox.onItemSelected(index) 4156 | end 4157 | end 4158 | 4159 | comboBox.selectedItem = 1 4160 | comboBox.addItem = comboBoxAddItem 4161 | comboBox.removeItem = comboBoxRemoveItem 4162 | comboBox.addSeparator = comboBoxAddSeparator 4163 | comboBox.draw = comboBoxDraw 4164 | comboBox.clear = comboBoxClear 4165 | comboBox.getItem = comboBoxGetItem 4166 | comboBox.count = comboBoxCount 4167 | comboBox.eventHandler = comboBoxEventHandler 4168 | 4169 | return comboBox 4170 | end 4171 | 4172 | --------------------------------------------------------------------------------------------------- 4173 | 4174 | function windowDraw(window) 4175 | containerDraw(window) 4176 | GUI.drawShadow(window.x, window.y, window.width, window.height, GUI.WINDOW_SHADOW_TRANSPARENCY, true) 4177 | 4178 | return window 4179 | end 4180 | 4181 | local function windowCheck(window, x, y) 4182 | local child 4183 | for i = #window.children, 1, -1 do 4184 | child = window.children[i] 4185 | 4186 | if 4187 | not child.hidden and 4188 | not child.disabled and 4189 | x >= child.x and 4190 | x < child.x + child.width and 4191 | y >= child.y and 4192 | y < child.y + child.height 4193 | then 4194 | if not child.passScreenEvents and child.eventHandler then 4195 | return true 4196 | elseif child.children then 4197 | local result = windowCheck(child, x, y) 4198 | if result == true then 4199 | return true 4200 | elseif result == false then 4201 | return false 4202 | end 4203 | end 4204 | end 4205 | end 4206 | end 4207 | 4208 | local function windowEventHandler(application, window, e1, e2, e3, e4, ...) 4209 | if e1 == "touch" then 4210 | if not windowCheck(window, e3, e4) then 4211 | window.lastTouchX, window.lastTouchY = e3, e4 4212 | end 4213 | 4214 | if window ~= window.parent.children[#window.parent.children] then 4215 | window:moveToFront() 4216 | 4217 | if window.onFocus then 4218 | window.onFocus(application, window, e1, e2, e3, e4, ...) 4219 | end 4220 | 4221 | application:draw() 4222 | end 4223 | elseif e1 == "drag" and window.lastTouchX and not windowCheck(window, e3, e4) then 4224 | local xOffset, yOffset = e3 - window.lastTouchX, e4 - window.lastTouchY 4225 | if xOffset ~= 0 or yOffset ~= 0 then 4226 | window.localX, window.localY = window.localX + xOffset, window.localY + yOffset 4227 | window.lastTouchX, window.lastTouchY = e3, e4 4228 | 4229 | application:draw() 4230 | end 4231 | elseif e1 == "drop" then 4232 | window.lastTouchX, window.lastTouchY = nil, nil 4233 | end 4234 | end 4235 | 4236 | local function windowResize(window, width, height) 4237 | window.width, window.height = width, height 4238 | if window.onResize then 4239 | window.onResize(width, height) 4240 | end 4241 | 4242 | return window 4243 | end 4244 | 4245 | local function windowMaximize(window) 4246 | if window.maximized then 4247 | window.localX, window.localY = window.oldGeometryX, window.oldGeometryY 4248 | window:resize(window.oldGeometryWidth, window.oldGeometryHeight) 4249 | else 4250 | window.oldGeometryX, window.oldGeometryY, window.oldGeometryWidth, window.oldGeometryHeight = window.localX, window.localY, window.width, window.height 4251 | window.localX, window.localY = 1, 1 4252 | window:resize(window.parent.width, window.parent.height) 4253 | end 4254 | 4255 | window.maximized = not window.maximized 4256 | window.firstParent:draw() 4257 | end 4258 | 4259 | local function windowMinimize(window) 4260 | window.hidden = not window.hidden 4261 | window.firstParent:draw() 4262 | end 4263 | 4264 | local function windowClose(window) 4265 | window:remove() 4266 | window.firstParent:draw() 4267 | end 4268 | 4269 | function GUI.window(x, y, width, height) 4270 | local window = GUI.container(x, y, width, height) 4271 | 4272 | window.passScreenEvents = false 4273 | 4274 | window.resize = windowResize 4275 | window.maximize = windowMaximize 4276 | window.minimize = windowMinimize 4277 | window.close = windowClose 4278 | window.eventHandler = windowEventHandler 4279 | window.draw = windowDraw 4280 | 4281 | return window 4282 | end 4283 | 4284 | function GUI.filledWindow(x, y, width, height, backgroundColor) 4285 | local window = GUI.window(x, y, width, height) 4286 | 4287 | window.backgroundPanel = window:addChild(GUI.panel(1, 1, width, height, backgroundColor)) 4288 | window.actionButtons = window:addChild(GUI.actionButtons(2, 2, true)) 4289 | 4290 | return window 4291 | end 4292 | 4293 | function GUI.titledWindow(x, y, width, height, title, addTitlePanel) 4294 | local window = GUI.filledWindow(x, y, width, height, GUI.WINDOW_BACKGROUND_PANEL_COLOR) 4295 | 4296 | if addTitlePanel then 4297 | window.titlePanel = window:addChild(GUI.panel(1, 1, width, 1, GUI.WINDOW_TITLE_BACKGROUND_COLOR)) 4298 | window.backgroundPanel.localY, window.backgroundPanel.height = 2, window.height - 1 4299 | end 4300 | 4301 | window.titleLabel = window:addChild(GUI.label(1, 1, width, height, GUI.WINDOW_TITLE_TEXT_COLOR, title)):setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP) 4302 | window.actionButtons.localY = 1 4303 | window.actionButtons:moveToFront() 4304 | 4305 | return window 4306 | end 4307 | 4308 | function GUI.tabbedWindow(x, y, width, height, ...) 4309 | local window = GUI.filledWindow(x, y, width, height, GUI.WINDOW_BACKGROUND_PANEL_COLOR) 4310 | 4311 | window.tabBar = window:addChild(GUI.tabBar(1, 1, window.width, 3, 2, 0, GUI.WINDOW_TAB_BAR_DEFAULT_BACKGROUND_COLOR, GUI.WINDOW_TAB_BAR_DEFAULT_TEXT_COLOR, GUI.WINDOW_TAB_BAR_DEFAULT_BACKGROUND_COLOR, GUI.WINDOW_TAB_BAR_DEFAULT_TEXT_COLOR, GUI.WINDOW_TAB_BAR_SELECTED_BACKGROUND_COLOR, GUI.WINDOW_TAB_BAR_SELECTED_TEXT_COLOR, true)) 4312 | 4313 | window.backgroundPanel.localY, window.backgroundPanel.height = 4, window.height - 3 4314 | window.actionButtons:moveToFront() 4315 | window.actionButtons.localY = 2 4316 | 4317 | return window 4318 | end 4319 | 4320 | --------------------------------------------------------------------------------------------------- 4321 | 4322 | function GUI.tabBar(...) 4323 | local tabBar = GUI.list(...) 4324 | 4325 | tabBar:setDirection(GUI.DIRECTION_HORIZONTAL) 4326 | tabBar:setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP) 4327 | 4328 | return tabBar 4329 | end 4330 | 4331 | -------------------------------------------------------------------------------- 4332 | 4333 | local function menuDraw(menu) 4334 | buffer.drawRectangle(menu.x, menu.y, menu.width, 1, menu.colors.default.background, menu.colors.default.text, " ", menu.colors.transparency) 4335 | layoutDraw(menu) 4336 | end 4337 | 4338 | local function menuAddItem(menu, text, textColor) 4339 | local item = menu:addChild(pressable(1, 1, unicode.len(text) + 2, 1, nil, textColor or menu.colors.default.text, menu.colors.selected.background, menu.colors.selected.text, 0x0, 0x0, text)) 4340 | item.eventHandler = pressableEventHandler 4341 | 4342 | return item 4343 | end 4344 | 4345 | local function menuGetItem(menu, what) 4346 | if type(what) == "number" then 4347 | return menu.children[what] 4348 | else 4349 | for i = 1, #menu.children do 4350 | if menu.children[i].text == what then 4351 | return menu.children[i], i 4352 | end 4353 | end 4354 | end 4355 | end 4356 | 4357 | local function menuContextMenuItemOnTouch(application, item) 4358 | item.contextMenu.x, item.contextMenu.y = item.x, item.y + 1 4359 | dropDownMenuAdd(application, item.contextMenu) 4360 | 4361 | application:draw() 4362 | end 4363 | 4364 | local function menuAddContextMenu(menu, ...) 4365 | local item = menu:addItem(...) 4366 | 4367 | item.switchMode = true 4368 | item.onTouch = menuContextMenuItemOnTouch 4369 | item.contextMenu = contextMenuCreate(1, 1) 4370 | item.contextMenu.onMenuClosed = function() 4371 | item.pressed = false 4372 | item.firstParent:draw() 4373 | end 4374 | 4375 | return item.contextMenu 4376 | end 4377 | 4378 | function GUI.menu(x, y, width, backgroundColor, textColor, backgroundPressedColor, textPressedColor, backgroundTransparency) 4379 | local menu = GUI.layout(x, y, width, 1, 1, 1) 4380 | 4381 | menu.colors = { 4382 | default = { 4383 | background = backgroundColor, 4384 | text = textColor, 4385 | }, 4386 | selected = { 4387 | background = backgroundPressedColor, 4388 | text = textPressedColor, 4389 | }, 4390 | transparency = backgroundTransparency 4391 | } 4392 | 4393 | menu.passScreenEvents = false 4394 | menu.addContextMenu = menuAddContextMenu 4395 | menu.addItem = menuAddItem 4396 | menu.getItem = menuGetItem 4397 | menu.draw = menuDraw 4398 | 4399 | menu:setDirection(1, 1, GUI.DIRECTION_HORIZONTAL) 4400 | menu:setAlignment(1, 1, GUI.ALIGNMENT_HORIZONTAL_LEFT, GUI.ALIGNMENT_VERTICAL_TOP) 4401 | menu:setSpacing(1, 1, 0) 4402 | menu:setMargin(1, 1, 1, 0) 4403 | 4404 | return menu 4405 | end 4406 | 4407 | --------------------------------------------------------------------------------------------------- 4408 | 4409 | local function progressIndicatorDraw(self) 4410 | local color = self.active and (self.position == 1 and self.colors.secondary or self.colors.primary) or self.colors.passive 4411 | buffer.drawText(self.x + 1, self.y, color, "⢀") 4412 | buffer.drawText(self.x + 2, self.y, color, "⡀") 4413 | 4414 | color = self.active and (self.position == 2 and self.colors.secondary or self.colors.primary) or self.colors.passive 4415 | buffer.drawText(self.x + 3, self.y + 1, color, "⠆") 4416 | buffer.drawText(self.x + 2, self.y + 1, color, "⢈") 4417 | 4418 | color = self.active and (self.position == 3 and self.colors.secondary or self.colors.primary) or self.colors.passive 4419 | buffer.drawText(self.x + 1, self.y + 2, color, "⠈") 4420 | buffer.drawText(self.x + 2, self.y + 2, color, "⠁") 4421 | 4422 | color = self.active and (self.position == 4 and self.colors.secondary or self.colors.primary) or self.colors.passive 4423 | buffer.drawText(self.x, self.y + 1, color, "⠰") 4424 | buffer.drawText(self.x + 1, self.y + 1, color, "⡁") 4425 | end 4426 | 4427 | local function progressIndicatorRoll(self) 4428 | self.position = self.position + 1 4429 | if self.position > 4 then 4430 | self.position = 1 4431 | end 4432 | end 4433 | 4434 | local function progressIndicatorReset(self, state) 4435 | self.active = state 4436 | self.position = 1 4437 | end 4438 | 4439 | function GUI.progressIndicator(x, y, passiveColor, primaryColor, secondaryColor) 4440 | local object = GUI.object(x, y, 4, 3) 4441 | 4442 | object.colors = { 4443 | passive = passiveColor, 4444 | primary = primaryColor, 4445 | secondary = secondaryColor 4446 | } 4447 | 4448 | object.active = false 4449 | object.reset = progressIndicatorReset 4450 | object.draw = progressIndicatorDraw 4451 | object.roll = progressIndicatorRoll 4452 | 4453 | object:reset() 4454 | 4455 | return object 4456 | end 4457 | 4458 | --------------------------------------------------------------------------------------------------- 4459 | 4460 | return GUI 4461 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Igor Timofeev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Important 2 | ====== 3 | This library is no longer supported cause it became part of MineOS. This repository exists only for compatibility with old software installers. For any new features and bugfixes go here: 4 | https://github.com/IgorTimofeev/MineOS/wiki/GUI-API 5 | 6 | If you need documentation for legacy version for OpenOS, go here: 7 | https://github.com/IgorTimofeev/GUI/blob/0fadb161469d404d477dd9babfdc9a5aa42ff203/README.md 8 | --------------------------------------------------------------------------------