From fdfa8d21bf52030dc8ae3c5e0c1e759a132caa6d Mon Sep 17 00:00:00 2001 From: Gabriel Tofvesson Date: Sat, 12 Oct 2024 17:56:16 +0200 Subject: [PATCH] Redesign main menu --- gfx/padding.lua | 4 + gfx/prop/visibility.lua | 29 +++ itemcontroller.lua | 445 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 477 insertions(+), 1 deletion(-) create mode 100644 gfx/prop/visibility.lua diff --git a/gfx/padding.lua b/gfx/padding.lua index 67b5bda..0f68ea1 100644 --- a/gfx/padding.lua +++ b/gfx/padding.lua @@ -93,6 +93,10 @@ function Padding:getInnerHeight() return self.element:getHeight() end +function Padding:getInnerElement() + return self.element +end + function Padding:getWidth() return self:getInnerWidth() + self:getPaddingLeft() + self:getPaddingRight() end diff --git a/gfx/prop/visibility.lua b/gfx/prop/visibility.lua new file mode 100644 index 0000000..bc543a4 --- /dev/null +++ b/gfx/prop/visibility.lua @@ -0,0 +1,29 @@ +local Prop = require("gfx.prop") +local Visibility = Prop:new{ defaultState = true, uid = "VISIBILITY" } + +Visibility.VISIBLE = true +Visibility.INVISIBLE = not Visibility.VISIBLE + +function Visibility:with(elementType) + local propSelf = self + local defaultDraw = elementType.draw + + function elementType:isVisible() + return propSelf:getState(self) + end + + function elementType:setVisible(visibility) + return propSelf:setState(self, visibility) + end + + function elementType:draw() + if self:isVisible() then + return defaultDraw(self) + end + return false + end + + return elementType +end + +return Visibility \ No newline at end of file diff --git a/itemcontroller.lua b/itemcontroller.lua index ce35b8a..773752c 100644 --- a/itemcontroller.lua +++ b/itemcontroller.lua @@ -8,7 +8,9 @@ local Padding = require("gfx.padding") local Container = require("gfx.container") local Element = require("gfx.element") local Progress = require("gfx.progress") +local Prop = require("gfx.prop") local Children = require("gfx.prop.children") +local Visibility = require("gfx.prop.visibility") local Orientation = require("gfx.prop.orientation") @@ -260,8 +262,448 @@ end local PAGES = { MAIN = function(state) - local pageState = state:currentPageState({}, state.changingPage) + local pageState = state:currentPageState({ + sortMode = 1, + filter = "", + displayKeyboard = false, + currentPage = 1 + }) + local function _genSort(func, invert, tiebreaker) + return function(a, b) + local aRes = func(a) + local bRes = func(b) + return (aRes == bRes and tiebreaker ~= nil and tiebreaker(a, b)) or ((aRes > bRes) ~= invert) + end + end + + local function sortByCount(invert, tiebreaker) + return _genSort(function(v) return v:getItemCount() end, invert, tiebreaker) + end + + local function sortByName(invert, tiebreaker) + return _genSort(function(v) return v:getSimpleName() end, invert, tiebreaker) + end + + local function sortByDamage(invert, tiebreaker) + return _genSort(function(v) + local damage = v:getDamage() + return damage == 0 and 1 or (damage / v:getMaxDamage()) + end, invert, tiebreaker) + end + + local function composeSortFuncs(...) + local funcs = {...} + + local compose = nil + for i=#funcs,1,-1 do + local current = funcs[i] + local invert = false + if type(current) == "boolean" then + invert = current + i = i - 1 + current = funcs[i] + end + compose = current(invert, compose) + end + return compose + end + + local SORT_MODE = { + composeSortFuncs(sortByCount, sortByName, sortByDamage), + composeSortFuncs(sortByName, sortByDamage, sortByCount) + } + + + local function matchFilter(filter, str) + if #filter == 0 then + return true + end + + local cur = 1 + filter = filter:lower() + str = str:lower() + for i=1,#str do + if str:sub(i,1) == filter:sub(cur,1) then + cur = cur + 1 + if cur > #filter then + return true + end + end + end + return false + end + + + local storageSatProgress = Progress:new { + width = state.width, + height = 1 + -- Percentage of storage slots occupied + } + + + ---- BOTTOM BAR + local keyboardButton = Text:new{ + id = "action_keyboard", + text = "KBD" + } + + local importButton = Text:new{ + id = "action_import", + text = "IMPORT" + } + + local sortButton = Text:new{ + id = "action_sort" + } + + local function tabActionButton(count) + local text = count < 0 and ("<"..tostring(-count)) or (tostring(count)..">") + return Padding:new{ + Text:new{ id = tostring(count), text = text } + } + end + + local function tabActionButtonID(index, interval, count, sign) + -- | s = 1: i - 1 + -- k(i, c, s) <| + -- | s = -1: c - i + + -- Simple solution (7 operations): + -- (c + 1)(1 - s) - 2 + -- k(i, c, s) = ------------------ + i*s + -- 2 + + -- (`tabActionButtonID` abbreviated as `tabID`) + -- tabID(i, t, c, s) = max(1, k(i, c, s) * t * s) + + -- E.g. + -- interval = 5, count = 3, sign = 1 + -- i=1: tabID(i, interval, count, sign) = max(1, k(1, 2, 1) * 5) * 1 = max(1, (-1 + 1) * 5) = max(1, 0) = 1 + -- i=2: tabID(i, interval, count, sign) = max(1, k(2, 2, 1) * 5) * 1 = max(1, (-1 + 2) * 5) = max(1, 5) = 5 + -- i=3: tabID(i, interval, count, sign) = max(1, k(3, 2, 1) * 5) * 1 = max(1, (-1 + 3) * 5) = max(1, 10) = 10 + + -- E.g. + -- interval = 5, count = 3, sign = -1 + -- i=1: tabID(i, interval, count, sign) = max(1, k(1, 2, -1) * 5) * (-1) = -max(1, (3 + -1) * 5) = -max(1, 10) = -10 + -- i=2: tabID(i, interval, count, sign) = max(1, k(2, 2, -1) * 5) * (-1) = -max(1, (3 + -2) * 5) = -max(1, 5) = -5 + -- i=3: tabID(i, interval, count, sign) = max(1, k(3, 2, -1) * 5) * (-1) = -max(1, (3 + -3) * 5) = -max(1, 0) = -1 + return math.max(1, (((((count + 1) * (1 - sign)) - 2) / 2) + (sign * index)) * interval) * sign + end + + local function tabActionList(interval, count, sign, spacing) + local buttons = {} + if count > 0 then + table.insert(buttons, tabActionButton(tabActionButtonID(1, interval, count, sign))) + for i=2,count do + table.insert(buttons, Element:new{ width = spacing }) + table.insert(buttons, tabActionButton(tabActionButtonID(i, interval, count, sign))) + end + end + + return List:new{ + [Orientation:getId()] = Orientation.HORIZONTAL, + [Children:getId()] = buttons + } + end + + local ACTION_COUNT = 3 + local ACTION_INTERVAL = 5 + local ACTION_SPACING = 1 + + local actions = { + tabActionList(ACTION_INTERVAL, ACTION_COUNT, 1, ACTION_SPACING), + List:new { + [Orientation:getId()] = Orientation.HORIZONTAL, + [Children:getId()] = { + keyboardButton, + Element:new{ width = 1 }, + importButton, + Element:new{ width = 1 }, + sortButton + } + }, + tabActionList(ACTION_INTERVAL, ACTION_COUNT, -1, ACTION_SPACING) + } + + for i=#actions+1,1,-1 do + table.insert(actions, i, Element:new{ width = 0, height = 0 }) + end + + local function calculateActionSpaces() + local freeSpace = 0 + for i=2,#actions,2 do + freeSpace = freeSpace + actions[i]:getWidth() + end + local asymmetry = freeSpace % ((#actions - 1) / 2) + local part = (freeSpace - asymmetry) / ((#actions + 1) / 2) + for i=1,#actions,2 do + actions[i]:setWidth((i <= asymmetry and 1 or 0) + part) + end + end + + + local bottomBarList = List:new { + [Orientation:getId()] = Orientation.HORIZONTAL, + [Children:getId()] = actions + } + + + local aboveEntries = { storageSatProgress } + local belowEntries = { bottomBarList } + local entries = {} + for _,v in ipairs(aboveEntries) do + table.insert(entries, v) + end + + local groupEntryListBudget = state.height + for _,v in ipairs(aboveEntries) do + groupEntryListBudget = groupEntryListBudget - v:getHeight() + end + for _,v in ipairs(belowEntries) do + groupEntryListBudget = groupEntryListBudget - v:getHeight() + end + + local GroupEntryID = { + PADDING = "padding", + NAME = "name", + COUNT = "count" + } + + for i=1,groupEntryListBudget do + table.insert( + entries, + Prop.attach( + List:new{ + id = tostring(i), + [Orientation:getId()] = Orientation.HORIZONTAL, + [Children:getId()] = { + Padding:new{ + id = GroupEntryID.PADDING, + element = Text:new{ id = GroupEntryID.NAME } + }, + Text:new{ + id = GroupEntryID.COUNT + } + } + }, + Visibility, + Visibility.INVISIBLE + ) + ) + end + + for _,v in ipairs(belowEntries) do + table.insert(entries, v) + end + + + + local mainList = List:new{ + [Orientation:getId()] = Orientation.VERTICAL, + [Children:getId()] = entries + } + + local KEY_BACKSPACE = "backspace" + local ID_FILTER_DISPLAY = "display_filter" + local keyboardLines = { + lines = { + { backspace = true, "1234567890" }, + { "qwertyuiop" }, + { "asdfghjkl" }, + { "zxcvbnm_:" } + }, + elements = { } + } + + local function charInputKeyList(chars, backspace) + local keys = {} + for i=1,#chars do + local key = chars:sub(i, 1) + table.insert(keys, Padding:new{ bgColor = colors.black, right = ((not backspace) and i == #keys and 0) or 1, element = Text:new{ + id = key, + text = key, + bgColor = colors.gray + }}) + end + if backspace then + table.insert(keys, Text:new{ + id = KEY_BACKSPACE, + text = "<--", + bgColor = colors.gray + }) + end + return List:new{ + [Orientation:getId()] = Orientation.HORIZONTAL, + [Children:getId()] = keys + } + end + + local keyboardWidth = 0 + for _,line in ipairs(keyboardLines.lines) do + local keyLineList = charInputKeyList(line[1], line.backspace) + keyboardWidth = math.max(keyboardWidth, keyLineList:getWidth()) + table.insert(keyboardLines, keyLineList) + end + + table.insert(keyboardLines, 1, Text:new{ id = ID_FILTER_DISPLAY, text = "" }) + + local keyboardList = List:new{ + [Orientation:getId()] = Orientation.Vertical, + [Children:getId()] = keyboardLines + } + + local screenContainer = Container:new{ + [Children:getId()] = { + mainList, + Prop.attach(keyboardList, Visibility, pageState.displayKeyboard) + } + } + + local function reloadState() + -- Enumerate inventory stats + local emptyCount = 0 + local totalCount = 0 + pageState.stacks = ItemGroup.collectStacks(state.controller:find(function(stack) + if stack:isEmpty() then + emptyCount = emptyCount + 1 + return false + else + totalCount = totalCount + 1 + local name = stack:getSimpleName() + return matchFilter(state.filter, name) or (name:find(":") ~= nil and matchFilter(state.filter, stack:getName())) + end + end)) + totalCount = emptyCount + totalCount + table.sort(pageState.stacks, SORT_MODE[pageState.sortMode]) + + pageState.pages = math.ceil(totalCount / groupEntryListBudget) + + + -- Set dynamic states for elements + sortButton:setText("<"..tostring(state.sortMode)..">") + storageSatProgress:setProgress(1 - (emptyCount / totalCount)) + + local basePageIndex = (pageState.currentPage - 1) * groupEntryListBudget + local pageEntryCount = math.min(groupEntryListBudget, totalCount - basePageIndex) + for i=1,groupEntryListBudget do + local isVisible = i <= pageEntryCount + local listEntry = mainList:findById(tostring(i)) + listEntry:setVisible(isVisible) + if isVisible then + local group = pageState.stacks[basePageIndex + i] + + local nameText = listEntry:findById(GroupEntryID.NAME) + local namePadding = listEntry:findById(GroupEntryID.PADDING) + local countText = listEntry:findById(GroupEntryID.COUNT) + + countText:setText(tostring(group:getItemCount())) + + local nameTextBudget = mainList:getWidth() - countText:getWidth() - 1 + local name = fitText(group:getSimpleName(), nameTextBudget) + + nameText:setText(name) + namePadding:setPadding{ right = nameTextBudget - #name } + listEntry:adjustPositions() + listEntry:setOnClick(function() + state:setPage("GROUP_DETAIL", group) + return true + end) + else + listEntry:setOnClick(nil) + end + end + +---@diagnostic disable-next-line: redefined-local + local keyboardWidth = keyboardList:getWidth() + keyboardList:setPos{ + x = math.floor((screenContainer:getWidth() - keyboardWidth) / 2), + y = screenContainer:getHeight() - keyboardList:getHeight() + } + + local filterDisplayedText = #pageState.filter < keyboardWidth and pageState.filter or pageState.filter:sub(#pageState.filter - keyboardWidth + 1, keyboardWidth) + local filterText = keyboardList:findById(ID_FILTER_DISPLAY) + filterText:setText(filterDisplayedText) + filterText:setX(math.floor((keyboardWidth - #filterDisplayedText) / 2)) + + calculateActionSpaces() + screenContainer:setDirty(true) + end + + for _,line in ipairs(keyboardLines.lines) do + for i=1,#line do + local key = line:sub(i, 1) + keyboardList:findById(key):setOnClick(function() + pageState.filter = pageState.filter..key + reloadState() + return true + end) + end + end + + keyboardList:findById(KEY_BACKSPACE):setOnClick(function() + pageState.filter = fitText(pageState.filter, math.max(0, #pageState.filter - 1)) + reloadState() + return true + end) + + local function bindTabActionButtons() + local function onClickHandler(change) + return function() + local newValue = math.max(1, math.min(pageState.currentPage + change, pageState.pages)) + if newValue ~= pageState.currentPage then + pageState.currentPage = newValue + reloadState() + end + return true + end + end + for i=1,ACTION_COUNT do + local id = tabActionButtonID(i, ACTION_INTERVAL, ACTION_COUNT, 1) + bottomBarList:findById(tostring(id)):setOnClick(onClickHandler(id)) + end + for i=1,ACTION_COUNT do + local id = tabActionButtonID(i, ACTION_INTERVAL, ACTION_COUNT, -1) + bottomBarList:findById(tostring(id)):setOnClick(onClickHandler(id)) + end + end + bindTabActionButtons() + + keyboardButton:setOnClick(function() + print("Toggling keyboard...") + state.showKeyboard = not state.showKeyboard + + reloadState() + return true + end) + + importButton:setOnClick(function() + print("Importing...") + state.node:rescan() + -- Safely handle transfers, priming computer for a full reset/rescan in case server stops mid-transaction + state:itemTransaction(function() + for _,nodeStack in ipairs(state.node:find(function(s) return not s:isEmpty() end)) do + if not state.controller:insertStack(nodeStack) then + print("Couldn't find a free slot for: "..nodeStack:getSimpleName()) + end + end + end) + + reloadState() + return true + end) + + sortButton:setOnClick(function() + print("Reorganizing...") + state.sortMode = (state.sortMode + 1 + #SORT_MODE) % #SORT_MODE + reloadState() + return true + end) + + reloadState() + + return renderDefault(state, screenContainer) +--[[ if pageState.stacks == nil then pageState.stacks = ItemGroup.collectStacks(state.controller:find(function(stack) return not stack:isEmpty() @@ -305,6 +747,7 @@ local PAGES = { pageState.listState = listState return renderDefault(state, listResult) +]]-- end, GROUP_DETAIL = function(state)