---Helper library to create touchscreen-like screens. Feel free to copy it to your projects if needed, ---it’s CC0 and everything. ---Note: for it to work properly, use `touchscreen.tapped()` instead of regular `ui…` clicks. ---@diagnostic disable-next-line: lowercase-global touchscreen = {} local sleep = 0 local mouseDown = false local mouseDownTime = 0 local mouseClicked = false local mouseReleased = false local tappedBase = false local tapped = false local longTapped = false local prevMousePosY = -1 local mouseDownDelta = 0 local scrollingVelocity = 0 local scrollingWindowHovered = false local doubleTappedNow = false local doubleTappedNext = false local scrollBarVisible = 0 local vkPosition = 1e9 local vkOffset = 0 local vkActiveText = nil local vkHovered = false local vkActive = 0 local vkCooldown = 0 local vkHeldTimer = nil local stopNInput = 0 local sleptFor = 0 local vkStayActive = 0 local uis = ac.getUI() ---Call this function at the start of a frame. function touchscreen.update(forceAwake, dt) if vkActiveText then forceAwake = true end if scrollBarVisible <= 0 and not ui.anyItemActive() and not ui.isAnyMouseDown() and ui.mouseDelta().x == 0 and not forceAwake then sleep = sleep + dt if sleep > 3 then if sleptFor < 0.5 then sleptFor = sleptFor + dt ac.skipFrame() return true else sleptFor = 0 end end else if forceAwake then ac.boostFrameRate() end sleep = 0 end tappedBase = false doubleTappedNow = false mouseClicked = false mouseReleased = false if scrollBarVisible > 0 then scrollBarVisible = scrollBarVisible - dt end if ui.mouseDown() ~= mouseDown then mouseReleased = mouseDown mouseDown = not mouseDown mouseClicked = mouseDown prevMousePosY = ui.mousePos().y if mouseDown then scrollingVelocity = 0 mouseDownDelta = 0 mouseDownTime = ui.time() else tappedBase = mouseDownDelta < (uis.vrController and 100 or 20) doubleTappedNow = tappedBase and doubleTappedNext doubleTappedNext = false end elseif mouseDown then if scrollingWindowHovered then scrollingVelocity = vkHovered and 0 or prevMousePosY - ui.mousePos().y else scrollingVelocity = scrollingVelocity * 0.9 end prevMousePosY = ui.mousePos().y mouseDownDelta = mouseDownDelta + #ui.mouseDelta() if math.abs(scrollingVelocity) > 0.1 then scrollBarVisible = 2 ac.boostFrameRate() end else scrollingVelocity = scrollingVelocity * 0.9 end if ui.mouseDoubleClicked() then doubleTappedNext = true end if vkActive > 1 then vkActive = vkActive - 1 else if vkCooldown > 0 then vkCooldown = vkCooldown - dt end if vkActive > 0.999 and not vkActiveText then vkCooldown = 0.5 end vkActive = math.applyLag(vkActive, vkCooldown <= 0 and vkActiveText and 1 or 0, 0.8, dt) end if ui.frameCount() > vkStayActive then vkActiveText = nil end vkPosition = math.ceil(ui.windowHeight() * (1 - math.saturateN(vkActive) * 0.64)) vkOffset = ui.windowHeight() - vkPosition vkHovered = ui.mouseLocalPos().y > vkPosition tapped = tappedBase and not vkHovered and vkCooldown <= 0 and ui.time() - mouseDownTime < 0.5 longTapped = mouseDown and not vkHovered and vkCooldown <= 0 and ui.time() - dt - mouseDownTime < 0.5 and ui.time() - mouseDownTime > 0.5 if vkHeldTimer and not ui.mouseDown() then clearInterval(vkHeldTimer) vkHeldTimer = nil end scrollingWindowHovered = false end local reactivateText = nil local prevTextField = nil local charToAdd = nil ---@type string|number local vkBgColor = rgbm.new('#273238') local vkBtnColor = rgbm.new('#414A51') local vkBtnPressedColor = rgbm.new('#313A41') local vkBtnActiveColor = rgbm.new('#6EACA7') local vkBtnActivePressedColor = rgbm.new('#5E9C97') local vkBtnShadowColor = rgbm.new('#111A21') local vkBtnWidth = 70 local vkEnterIcon = ui.Icons.Enter local vkShift = false local vkButtonsReady local vkLayouts = { base = { { { 'q', '1', 'Q' }, { 'w', '2', 'W' }, { 'e', '3', 'E' }, { 'r', '4', 'R' }, { 't', '5', 'T' }, { 'y', '6', 'Y' }, { 'u', '7', 'U' }, { 'i', '8', 'I' }, { 'o', '9', 'O' }, { 'p', '0', 'P' } }, { { 'a', '@', 'A' }, { 's', '#', 'S' }, { 'd', '$', 'D' }, { 'f', '_', 'F' }, { 'g', '&', 'G' }, { 'h', '-', 'H' }, { 'j', '+', 'J' }, { 'k', '(', 'K' }, { 'l', ')', 'L' } }, { { i = ui.Icons.Shift, s = true, w = 1.5, p = vec2(37, 11) }, { 'z', '*', 'Z' }, { 'x', '"', 'X' }, { 'c', '\'', 'C' }, { 'v', ':', 'V' }, { 'b', ';', 'B' }, { 'n', '!', 'N' }, { 'm', '?', 'M' }, { c = ui.KeyIndex.Back, i = ui.Icons.Backspace, w = 1.5 } }, { { '?123', l = 'symbols', w = 1.5 }, { ',' }, { ' ', w = 4 }, { i = ui.Icons.Paste, p = vec2(20, 11) }, { '.' }, { i = ui.Icons.Enter, c = ui.KeyIndex.Return, p = vec2(33, 13), w = 1.5 } }, }, symbols = { { { '1' }, { '2' }, { '3' }, { '4' }, { '5' }, { '6' }, { '7' }, { '8' }, { '9' }, { '0' } }, { { '@' }, { '#' }, { '$' }, { '%' }, { '&' }, { '-' }, { '+' }, { '(' }, { ')' } }, { { '=\\<', l = 'symbolsAlt', w = 1.5, p = vec2(37, 11) }, { '*' }, { '"' }, { '\'' }, { ':' }, { ';' }, { '!' }, { '?' }, { c = ui.KeyIndex.Back, i = ui.Icons.Backspace, w = 1.5 } }, { { 'ABC', l = 'base', w = 1.5 }, { ',' }, { '_' }, { ' ', w = 3 }, { '/' }, { '.' }, { i = ui.Icons.Enter, c = ui.KeyIndex.Return, p = vec2(33, 13), w = 1.5 } }, }, symbolsAlt = { { { '~' }, { '`' }, { '|' }, { '•' }, { '√' }, { 'π' }, { '÷' }, { '×' }, { '¶' }, { '∆' } }, { { '£' }, { '¢' }, { '€' }, { '¥' }, { '∧' }, { '°' }, { '=' }, { '{' }, { '}' } }, { { '?123', l = 'symbols', w = 1.5, p = vec2(37, 11) }, { '\\' }, { '©' }, { '®' }, { '™' }, { '‰' }, { '[' }, { ']' }, { c = ui.KeyIndex.Back, i = ui.Icons.Backspace, w = 1.5 } }, { { 'ABC', l = 'base', w = 1.5 }, { ',' }, { '<' }, { ' ', w = 3 }, { '>' }, { '.' }, { i = ui.Icons.Enter, c = ui.KeyIndex.Return, p = vec2(33, 13), w = 1.5 } }, } } local vkLayout = vkLayouts.base local function keyboardButtonLayout(cur, char) local w = (char.w or 1) * vkBtnWidth char.p1 = cur:clone() char.p2 = cur + vec2(w - 10, 46) char.s1 = char.p1 + vec2(0, 2) char.s2 = char.p2 + vec2(0, 2) if char.i then char.ip = char.p1 + (char.p or vec2(33, 11)) end if char[1] then char.t1 = char.p1 + vec2((w - 10) / 2 - ui.measureDWriteText(char[1], 24).x / 2, 5) end if char[3] then char.t3 = char.p1 + vec2((w - 10) / 2 - ui.measureDWriteText(char[3], 24).x / 2, 5) end if char[2] then char.t2 = char.p1 + vec2((w - 10) / 2 - ui.measureDWriteText(char[2], 24).x / 2, -50) char.u1 = char.p1 + vec2(w - 20 - ui.measureDWriteText(char[2], 14).x / 2, 0) end return w end local function keyboardButton(char, layer) local cur = char.p1 local p2 = char.p2 if layer == 1 then local enterButton = char.c == ui.KeyIndex.Return ui.drawRectFilled(char.s1, char.s2, vkBtnShadowColor, 8) ui.drawRectFilled(cur, p2, (char.s and vkShift == 2 or enterButton) and (char.hovered and vkBtnActivePressedColor or vkBtnActiveColor) or char.hovered and vkBtnPressedColor or vkBtnColor, 8) if mouseClicked and ui.rectHovered(cur, p2) then char.hovered = true char.popup = false vkHeldTimer = char.c == ui.KeyIndex.Back and setInterval(function () charToAdd = ui.KeyIndex.Back end, 0.12) or char[2] and setTimeout(function () char.popup, vkHeldTimer = true, nil end, 0.3) elseif char.hovered and not mouseDown then if ui.rectHovered(cur, p2) then if char.s then vkShift = vkShift == true and 2 or not vkShift elseif char.l then vkLayout = vkLayouts[char.l] elseif char.i == ui.Icons.Paste then charToAdd = ui.getClipboardText() else charToAdd = char.c or (char.popup and char[2] or vkShift and char[3] or char[1]) if vkShift == true then vkShift = false end end end char.hovered, char.popup = false, false end if char.i then ui.setCursor(char.ip) ui.icon(enterButton and vkEnterIcon or char.s and vkShift and ui.Icons.ShiftActive or char.i, 24, rgbm.colors.white) end end if layer == 2 and char[1] then ui.dwriteDrawText(vkShift and char[3] or char[1], 24, vkShift and char.t3 or char.t1, rgbm.colors.white) if char[2] then ui.dwriteDrawText(char[2], 14, char.u1, rgbm(1, 1, 1, 0.5)) end end if layer == 3 then ui.pushClipRectFullScreen() ui.drawRectFilled(cur - vec2(0, 60), p2, vkBtnShadowColor, 8) ui.dwriteDrawText(char[2], 24, char.t2, rgbm.colors.white) ui.popClipRect() end end local function updateKeyboardButtons() local r = { layout = vkLayout } local p = (ui.availableSpace().x - vkBtnWidth * 9.6) / 2 for i = 1, #vkLayout do local cur = vec2(p + (i == 2 and vkBtnWidth/2 or 0), 55 * (i - 1) + 10) for j = 1, #vkLayout[i] do cur.x = cur.x + keyboardButtonLayout(cur, vkLayout[i][j]) table.insert(r, vkLayout[i][j]) end end return r end ---Call this function at the end of a frame, it would draw an on-screen keyboard if needed. function touchscreen.keyboard() if vkActiveText ~= nil then prevTextField = vkActiveText vkEnterIcon = vkActiveText == 'Search' and ui.Icons.Search or ui.Icons.Enter end if not vkButtonsReady or vkButtonsReady.layout ~= vkLayout then vkButtonsReady = updateKeyboardButtons() end if vkActive > 0.001 then ui.setCursor(vec2(0, vkPosition)) ui.childWindow('onscreenKeyboard', ui.availableSpace() + vec2(0, 10), true, bit.bor(ui.WindowFlags.NoFocusOnAppearing, ui.WindowFlags.NoScrollbar), function () ui.setCursor(0) ui.drawRectFilled(0, ui.availableSpace(), vkBgColor) local btnPopup for i = 1, #vkButtonsReady do local btn = vkButtonsReady[i] keyboardButton(btn, 1) if btn.popup then btnPopup = btn end end for i = 1, #vkButtonsReady do keyboardButton(vkButtonsReady[i], 2) end if btnPopup then keyboardButton(btnPopup, 3) end if ui.windowHovered(ui.HoveredFlags.AllowWhenBlockedByActiveItem) then if ui.mouseClicked() or ui.mouseReleased() then reactivateText = prevTextField vkActive = 3 end local captured = ui.captureKeyboard(true, true, true) if charToAdd == nil then if #captured > 0 then charToAdd = captured:queue() elseif captured.pressedCount > 0 then charToAdd = captured.pressed[0] end end end end) end end function touchscreen.keyboardHovered() return vkHovered end function touchscreen.keyboardOffset() return vkOffset end local lastActiveLabel ---Text input control with onscreen keyboard support. Returns updated string (which would be the input string unless it changed, so no) ---copying there. Second return value would change to `true` when text has changed. Example: ---``` ---myText = ui.inputText('Enter something:', myText) ---``` ---@param label string ---@param str string ---@param flags ui.InputTextFlags ---@return string ---@return boolean function touchscreen.inputText(label, str, flags) local done = false if label == reactivateText then reactivateText = nil ui.setKeyboardFocusHere() elseif charToAdd ~= nil and lastActiveLabel == label then if type(charToAdd) == 'number' then if charToAdd == ui.KeyIndex.Return then done = true end ui.setKeyboardButtonDown(charToAdd) charToAdd = nil elseif #charToAdd > 1 then ui.addInputCharacter(charToAdd:byte()) charToAdd = charToAdd:sub(2) else ui.addInputCharacter(charToAdd:byte()) charToAdd = nil end end local ret = ui.inputText(label, str, bit.bor(flags or 0, ui.InputTextFlags.NoUndoRedo)) if stopNInput == 0 and ui.itemActive() then vkActiveText = label lastActiveLabel = label end if str and #str > 0 then local c = ui.getCursor() ui.setItemAllowOverlap() ui.sameLine(0, 0) ui.offsetCursorX(-32) if touchscreen.iconButton(ui.Icons.Cancel, vec2(32, ui.textLineHeightWithSpacing() * 1.1), 0.3, nil, 0.4) then ret = '' end if ui.itemClicked() then vkActiveText = nil end ui.setCursor(c) end return ret, done end function touchscreen.inputBehaviour() -- if vkCooldown > 0.4 then -- vkCooldown = 0 -- -- vkActive = 1 -- end vkStayActive = ui.frameCount() + 1 vkActiveText = true local r = charToAdd charToAdd = nil return r end ---Adds touch scrolling to current list. Call it from a scrollable window. Pass `true` for `hidden` ---if you want to draw custom style scrollbar later. function touchscreen.scrolling(hidden) if stopNInput > 0 or vkOffset > 1 then return 0 end ui.setScrollY(scrollingVelocity, true) scrollingWindowHovered = ui.windowHovered(ui.HoveredFlags.AllowWhenBlockedByActiveItem) local scrollMax = ui.getScrollMaxY() if scrollMax > 1 and not hidden then local window = ui.windowSize() local scrollY = ui.getScrollY() local barMarginX = 4 local barMarginY = 4 local barArea = window.y - barMarginY * 2 local barX = window.x - math.lerp(-2, barMarginX, math.saturateN(scrollBarVisible * 4)) local barHeight = math.lerp(40, barArea, window.y / (scrollMax + window.y)) local barY = scrollY + barMarginY + scrollY / scrollMax * (barArea - barHeight) ui.setCursor(0) ui.drawLine(vec2(barX, barY), vec2(barX, barY + barHeight), rgbm.colors.white, 2) end return math.saturateN(scrollBarVisible * 4) end function touchscreen.scrollingVelocity() return (stopNInput > 0 or vkOffset > 1) and 0 or scrollingVelocity end ---Stops input until returned function is called function touchscreen.stopInput() stopNInput = stopNInput + 1 return function () stopNInput = stopNInput - 1 end end ---Pauses input for a bit function touchscreen.pauseInput(time) setTimeout(touchscreen.stopInput(), time or 0.3) end ---Returns true if screen is being touched (like, mouse is down) function touchscreen.touched() return mouseDown and stopNInput == 0 end ---Returns true if screen was touched in previous frame function touchscreen.touchReleased() return mouseReleased and stopNInput == 0 end ---Returns true if screen was just tapped function touchscreen.tapped() return tapped and stopNInput == 0 end ---Returns true if screen was long tapped function touchscreen.longTapped() return longTapped and stopNInput == 0 end ---Returns true if screen was just double tapped function touchscreen.doubleTapped() return doubleTappedNow and stopNInput == 0 end ---Returns true if previous item was just tapped function touchscreen.itemTapped() return tapped and ui.itemHovered() and stopNInput == 0 end ---Returns true if previous item was double tapped function touchscreen.itemLongTapped() return longTapped and ui.itemHovered() and stopNInput == 0 end local defBaseSize = vec2(24, 24) local defSize = vec2(24, 24) ---Touch button with an icon ---@param icon ui.Icons ---@param size vec2|nil ---@param iconAlphaOrColor number|rgbm|nil ---@param iconAngle number|nil ---@param iconScale number|nil ---@return boolean function touchscreen.iconButton(icon, size, iconAlphaOrColor, iconAngle, iconScale, iconSize) local p = ui.getCursor() if not vec2.isvec2(size) then size = defBaseSize:set(size or 40, size or 40) end if not vec2.isvec2(iconSize) then iconSize = defSize:set(iconSize or 24, iconSize or 24) end ui.offsetCursorX((size.x - defSize.x) / 2) ui.offsetCursorY((size.y - defSize.y) / 2) if iconAngle ~= nil then ui.beginRotation() end if iconScale ~= nil then ui.beginScale() end if type(iconAlphaOrColor) == 'number' then ui.pushStyleVarAlpha(iconAlphaOrColor) end ui.icon(icon, defSize, rgbm.isrgbm(iconAlphaOrColor) and iconAlphaOrColor or nil) if iconAngle ~= nil then ui.endRotation(iconAngle) end if iconScale ~= nil then ui.endScale(iconScale) end if type(iconAlphaOrColor) == 'number' then ui.popStyleVar() end ui.setCursor(p) ui.invisibleButton(icon, size) return tapped and stopNInput == 0 and ui.itemHovered() end local iconSizeValue = vec2(24, 24) ---Touch button with an icon ---@param label string ---@param buttonSize vec2 ---@param buttonColor rgbm ---@param fontSize number ---@param icon ui.Icons? ---@param iconSize vec2? ---@param disabled boolean? ---@return boolean function touchscreen.button(label, buttonSize, buttonColor, fontSize, icon, iconSize, disabled) local p = ui.getCursor() ui.invisibleButton(label, buttonSize) ui.drawRectFilled(p, p + buttonSize, not disabled and ui.itemActive() and (rawequal(buttonColor, rgbm.colors.black) and rgbm(0.12, 0.12, 0.12, 1) or rgbm(buttonColor.r * 0.8, buttonColor.g * 0.8, buttonColor.b * 0.8, buttonColor.mult)) or buttonColor) ui.setCursor(p) if fontSize ~= 0 then ui.dwriteTextAligned(label, fontSize, ui.Alignment.Center, ui.Alignment.Center, buttonSize) end if icon ~= nil then if iconSize then iconSizeValue:set(iconSize) end ui.offsetCursorX((buttonSize.x - iconSizeValue.x) / 2) ui.offsetCursorY((buttonSize.y - iconSizeValue.y) / 2) if disabled then ui.pushStyleVarAlpha(0.5) end ui.icon(icon, iconSizeValue) if disabled then ui.popStyleVar(1) end end return not disabled and tapped and stopNInput == 0 and ui.itemHovered() end local loadingSize = vec2() local function step(v) return (math.floor(v / 2) + math.smoothstep(math.min(v % 2, 1))) end function touchscreen.loading(size) local s = loadingSize:set(size) local t = ui.time() local r = math.min(s.x, s.y) / 2 ui.pathArcTo(ui.getCursor() + s * 0.5, r, step(t * 1.3 + 1) * 4.5 + t * 3, step(t * 1.3) * 4.5 + 5 + t * 3, 40) ui.pathStroke(rgbm.colors.white, false, r / 5) ui.dummy(s) sleep = 0 end function touchscreen.boostFrameRate() ac.boostFrameRate() sleep = 0 end function touchscreen.forceAwake() sleep = 0 end local storedValues = ac.storage{ muted = false, volume = 0.5 } local volumeFresh = -1 ---@param mediaPlayer ui.MediaPlayer function touchscreen.syncVolume(mediaPlayer) mediaPlayer:setVolume(storedValues.volume):setMuted(storedValues.muted) end function touchscreen.syncVolumeIfFresh(mediaPlayer) if ui.time() - volumeFresh < 0.5 then mediaPlayer:setVolume(storedValues.volume):setMuted(storedValues.muted) end end function touchscreen.getVolume() return storedValues.muted and 0 or storedValues.volume end function touchscreen.getVolumeIfFresh() if ui.time() - volumeFresh > 0.5 then return nil end return storedValues.muted and 0 or storedValues.volume end function touchscreen.setMuted(muted, forceStale) storedValues.muted = muted ~= false volumeFresh = forceStale and -1 or ui.time() end function touchscreen.setVolume(volume, forceStale) storedValues.volume = tonumber(volume) or 0 volumeFresh = forceStale and -1 or ui.time() end function touchscreen.volumeControl(pos, width, height) pos = pos or vec2() width = width or ui.windowWidth() height = height or ui.windowHeight() local ret = false local muted = storedValues.muted local volume = storedValues.volume local volumePivotX = width + 4 local volumePivotY = height / 2 - 12 ui.setCursor(pos + vec2(volumePivotX - 60, volumePivotY + 72)) if touchscreen.iconButton((muted or volume == 0) and ui.Icons.Mute or volume < 0.33 and ui.Icons.VolumeLow or volume < 0.67 and ui.Icons.VolumeMedium or ui.Icons.VolumeHigh, 40) then touchscreen.setMuted(not muted) storedValues.muted = not muted touchscreen.forceAwake() ret = true end local p1 = vec2(volumePivotX - 40, volumePivotY + 60) local p2 = vec2(volumePivotX - 40, volumePivotY - 60) ui.drawLine(pos + p1, pos + p2, rgbm(1, 1, 1, 0.2), 8) ui.drawLine(pos + p1, pos + math.lerp(p1, p2, volume), rgbm.colors.white, 8) if stopNInput == 0 and ui.mouseDown() and ui.rectHovered(pos + vec2(volumePivotX - 60, volumePivotY - 80), pos + vec2(volumePivotX - 20, volumePivotY + 80)) then local newVolume = math.lerpInvSat(ui.mouseLocalPos().y, p1.y, p2.y) touchscreen.setVolume(newVolume) storedValues.volume = newVolume if muted then touchscreen.setMuted(false) storedValues.muted = false end touchscreen.boostFrameRate() ret = true end return ret end local hidingControlsLastTime = 0 local hidingControlsPopup = 0 local hidingControlsTime = 0 function touchscreen.hidingControls(dt) local time = ui.time() if touchscreen.tapped() or time - hidingControlsLastTime > 1 then hidingControlsPopup = 1 end if hidingControlsPopup > 0 then hidingControlsTime = math.min(hidingControlsTime + dt * 10, 1) hidingControlsPopup = hidingControlsPopup - dt elseif hidingControlsTime > 0 then hidingControlsTime = math.max(hidingControlsTime - dt * 2, 0) end hidingControlsLastTime = time return math.smoothstep(hidingControlsTime) end local accentColor = rgbm.new('#5BD5F9') local accentPushedColor = accentColor * 0.8 local roundColor = rgbm.new('#DEE0FF') local roundPushedColor = roundColor * 0.8 function touchscreen.accentButton(icon, radius, iconSize) ui.drawCircleFilled(ui.getCursor() + radius, radius, mouseDown and ui.rectHovered(ui.getCursor(), ui.getCursor() + radius * 2) and accentPushedColor or accentColor, 30) return touchscreen.iconButton(icon, radius * 2, rgbm.colors.black, nil, nil, iconSize or 16) end function touchscreen.addButton(radius) return touchscreen.accentButton(ui.Icons.Plus, radius or 34, 12) end function touchscreen.roundButton(icon, radius, iconSize) ui.drawCircleFilled(ui.getCursor() + radius, radius, mouseDown and ui.rectHovered(ui.getCursor(), ui.getCursor() + radius * 2) and roundPushedColor or roundColor, 30) return touchscreen.iconButton(icon, radius * 2, rgbm.colors.black, nil, nil, iconSize or 16) end function touchscreen.listItem(itemSize) if not touchscreen.tapped() and not touchscreen.touched() then return end local c, r = ui.getCursor(), false if touchscreen.tapped() and ui.rectHovered(c, c + itemSize) then r = true elseif touchscreen.touched() and ui.rectHovered(c, c + itemSize) then ui.drawRectFilled(c, c + itemSize, rgbm(1, 1, 1, 0.03), 4) end return r end function touchscreen.textButton(label, size) local r = touchscreen.listItem(size) ui.pushFont(ui.Font.Title) ui.pushStyleColor(ui.StyleColor.Text, accentColor) ui.textAligned(label, 0.5, size) ui.popStyleColor() ui.popFont() return r end function touchscreen.smoothValue(id, target, lag) ui.pushID(id) local t = ui.loadStoredNumber(0, -1) local nt = math.applyLag(t, target, t == -1 and 0 or lag or 0.7, ui.deltaTime()) if math.abs(nt - t) > 0.001 then touchscreen.boostFrameRate() ui.storeNumber(0, nt) end ui.popID() return nt end local toggleColorBg = rgbm.new('#40484C') local toggleColorThumb = rgbm.new('#071E25') local toggleColorBgActive = rgbm.new('#CFE6F0') function touchscreen.toggle(id, active, size) local c, b = ui.getCursor(), active and toggleColorBgActive or toggleColorBg ui.drawRectFilled(vec2(c.x + size.y / 2, c.y), vec2(c.x + size.x - size.y / 2, c.y + size.y), b) local p, o = vec2(c.x + size.y / 2, c.y + size.y / 2), size.x - size.y ui.drawCircleFilled(p, size.y / 2, b, 24) p.x = p.x + o ui.drawCircleFilled(p, size.y / 2, b, 24) p.x = p.x - o * touchscreen.smoothValue(id, active and 0 or 1) ui.drawCircleFilled(p, size.y / 2.7, toggleColorThumb, 24) if touchscreen.tapped() and ui.rectHovered(c, c + size) then return true end end function touchscreen.tabs(tabs, dt) local sw = ui.windowWidth() local sh = ui.windowHeight() local tabBgColor = tabs.color or rgbm.colors.black ui.drawRectFilled(vec2(0, sh - 60), vec2(sw, sh), tabBgColor) local selected = tabs.selected or 1 local itemWidth = sw / #tabs local itemPos = vec2(0, sh - 60) local sizeIcon = vec2(itemWidth, 40) local sizeText = vec2(itemWidth, 50) for i = 1, #tabs do local tab = tabs[i] local s = i == selected ui.setCursor(itemPos) if s then ui.drawRectFilled( itemPos + vec2(itemWidth / 2 - 40, 8), itemPos + vec2(itemWidth / 2 + 40, 32), rgbm.colors.white, 12) touchscreen.iconButton(tab.icon, sizeIcon, tabBgColor) else touchscreen.iconButton(tab.icon, sizeIcon) end ui.setCursor(itemPos) ui.textAligned(tab.name, vec2(0.5, 1), sizeText) ui.setCursor(itemPos) ui.invisibleButton(tab.name, sizeText) if not s and touchscreen.itemTapped() then tabs.selectedPrevious = selected tabs.selectedTransition = 1 tabs.selectedTransitionInvert = selected > i tabs.selected = i end itemPos.x = itemPos.x + itemWidth end local transition = tabs.selectedTransition or 0 local invert = tabs.selectedTransitionInvert and -1 or 1 if transition > 0.001 then touchscreen.boostFrameRate() transition = math.applyLag(transition, 0, 0.85, dt) tabs.selectedTransition = transition local previous = tabs[tabs.selectedPrevious] if previous and previous.fn then ui.setCursor(vec2(math.floor((transition - 1) * sw) * invert, 0)) ui.childWindow('__tabPrevious', vec2(sw, sh - 60), function () previous.fn(dt) end) end end local current = tabs[selected] if current and current.fn then ui.setCursor(vec2(math.floor(transition * sw) * invert, 0)) ui.childWindow('__tab', vec2(sw, sh - 60), function () current.fn(dt) end) end end function touchscreen.createTransition(lag, initialValue) local value = initialValue or 0 return function (dt, condition) value = math.applyLag(value, condition and 1 or 0, lag, dt) if value > 0.001 and value < 0.999 then touchscreen.boostFrameRate() end return value end end ---@param defaultAccentColor rgbm function touchscreen.blurredBackgroundImage(defaultAccentColor) local progressBaseColor = defaultAccentColor local blurredBg = ui.ExtraCanvas(64, 4) local progressCurColor = progressBaseColor local bgColor = rgbm(0, 0, 0, 1) local coverColor1 = rgbm(0, 0, 0, 0.2) local coverColor2 = rgbm(0, 0, 0, 1) local active = -1 local hasImage = false return { ---@param image ui.ImageSource? update = function (image) if image then blurredBg:update(function (dt) ui.beginBlurring() ui.drawImage(image, 0, ui.windowSize()) ui.endBlurring(0.2) end) blurredBg:accessData(function (err, data) local c = rgbm() progressCurColor = rgbm() for y = 1, 3 do for x = 1, 3 do progressCurColor:add(data:colorTo(c, 16*x, 16*y)) end end progressCurColor:scale(1 / progressCurColor.mult) local h = progressCurColor.rgb:hsv() if h.s < 0.01 or h.v < 0.01 then progressCurColor.rgb:set(defaultAccentColor.rgb) else h.v = 1 h.s = math.min(h.s * 1.5, 1) progressCurColor.rgb:set(h:rgb()) end bgColor.rgb:set(progressCurColor.rgb):scale(0.07) coverColor2.rgb:set(progressCurColor.rgb):scale(0.07) hasImage = true end) else hasImage = false end end, draw = function (dt) if active == -1 then active = hasImage and 1 or 0 end active = math.applyLag(active, hasImage and 1 or 0, 0.9, dt) if active > 0.001 then ui.drawRectFilled(0, ui.windowSize(), bgColor) ui.beginBlurring() ui.drawImage(blurredBg, -80, ui.windowHeight() + 80) ui.endBlurring(0.03) ui.drawRectFilledMultiColor(-80, ui.windowHeight() + 80, coverColor1, coverColor2, coverColor2, coverColor1) end if active < 0.999 then ui.drawRectFilled(0, ui.windowSize(), rgbm(0, 0, 0, 1 - active)) end end, accent = function () if active < 0.999 then return active < 0.001 and defaultAccentColor or math.lerp(defaultAccentColor, progressCurColor, active) end return progressCurColor end, } end