1
0
mirror of https://github.com/kepler155c/opus synced 2025-10-12 22:37:41 +00:00

ui overhaul

This commit is contained in:
kepler155c@gmail.com
2019-02-05 23:03:57 -05:00
parent 89400ac1bd
commit 915085ac5f
47 changed files with 2879 additions and 3199 deletions

View File

@@ -0,0 +1,30 @@
local class = require('class')
local UI = require('ui')
UI.ActiveLayer = class(UI.Window)
UI.ActiveLayer.defaults = {
UIElement = 'ActiveLayer',
}
function UI.ActiveLayer:setParent()
self:layout(self)
self.canvas = self:addLayer()
UI.Window.setParent(self)
end
function UI.ActiveLayer:enable(...)
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...)
if self.parent.transitionHint then
self:addTransition(self.parent.transitionHint)
end
self:focusFirst()
end
function UI.ActiveLayer:disable()
if self.canvas then
self.canvas:setVisible(false)
end
UI.Window.disable(self)
end

View File

@@ -0,0 +1,66 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
UI.Button = class(UI.Window)
UI.Button.defaults = {
UIElement = 'Button',
text = 'button',
backgroundColor = colors.lightGray,
backgroundFocusColor = colors.gray,
textFocusColor = colors.white,
textInactiveColor = colors.gray,
textColor = colors.black,
centered = true,
height = 1,
focusIndicator = ' ',
event = 'button_press',
accelerators = {
space = 'button_activate',
enter = 'button_activate',
mouse_click = 'button_activate',
}
}
function UI.Button:setParent()
if not self.width and not self.ex then
self.width = #self.text + 2
end
UI.Window.setParent(self)
end
function UI.Button:draw()
local fg = self.textColor
local bg = self.backgroundColor
local ind = ' '
if self.focused then
bg = self.backgroundFocusColor
fg = self.textFocusColor
ind = self.focusIndicator
elseif self.inactive then
fg = self.textInactiveColor
end
local text = ind .. self.text .. ' '
if self.centered then
self:clear(bg)
self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg)
else
self:write(1, 1, Util.widthify(text, self.width), bg, fg)
end
end
function UI.Button:focus()
if self.focused then
self:scrollIntoView()
end
self:draw()
end
function UI.Button:eventHandler(event)
if event.type == 'button_activate' then
self:emit({ type = self.event, button = self })
return true
end
return false
end

View File

@@ -0,0 +1,67 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
UI.Checkbox = class(UI.Window)
UI.Checkbox.defaults = {
UIElement = 'Checkbox',
nochoice = 'Select',
checkedIndicator = 'X',
leftMarker = '[',
rightMarker = ']',
value = false,
textColor = colors.white,
backgroundColor = colors.black,
backgroundFocusColor = colors.lightGray,
height = 1,
accelerators = {
space = 'checkbox_toggle',
mouse_click = 'checkbox_toggle',
}
}
function UI.Checkbox:setParent()
if not self.width and not self.ex then
self.width = (self.label and #self.label or 0) + 3
else
self.widthh = 3
end
UI.Window.setParent(self)
end
function UI.Checkbox:draw()
local bg = self.backgroundColor
if self.focused then
bg = self.backgroundFocusColor
end
if type(self.value) == 'string' then
self.value = nil -- TODO: fix form
end
local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator)
local x = 1
if self.label then
self:write(1, 1, self.label)
x = #self.label + 2
end
self:write(x, 1, text, bg)
self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor)
self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg)
self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor)
end
function UI.Checkbox:focus()
self:draw()
end
function UI.Checkbox:reset()
self.value = false
end
function UI.Checkbox:eventHandler(event)
if event.type == 'checkbox_toggle' then
self.value = not self.value
self:emit({ type = 'checkbox_change', checked = self.value, element = self })
self:draw()
return true
end
end

View File

@@ -0,0 +1,88 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
UI.Chooser = class(UI.Window)
UI.Chooser.defaults = {
UIElement = 'Chooser',
choices = { },
nochoice = 'Select',
backgroundFocusColor = colors.lightGray,
textInactiveColor = colors.gray,
leftIndicator = '<',
rightIndicator = '>',
height = 1,
}
function UI.Chooser:setParent()
if not self.width and not self.ex then
self.width = 1
for _,v in pairs(self.choices) do
if #v.name > self.width then
self.width = #v.name
end
end
self.width = self.width + 4
end
UI.Window.setParent(self)
end
function UI.Chooser:draw()
local bg = self.backgroundColor
if self.focused then
bg = self.backgroundFocusColor
end
local fg = self.inactive and self.textInactiveColor or self.textColor
local choice = Util.find(self.choices, 'value', self.value)
local value = self.nochoice
if choice then
value = choice.name
end
self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black)
self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg)
self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black)
end
function UI.Chooser:focus()
self:draw()
end
function UI.Chooser:eventHandler(event)
if event.type == 'key' then
if event.key == 'right' or event.key == 'space' then
local _,k = Util.find(self.choices, 'value', self.value)
local choice
if not k then k = 1 end
if k and k < #self.choices then
choice = self.choices[k+1]
else
choice = self.choices[1]
end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
elseif event.key == 'left' then
local _,k = Util.find(self.choices, 'value', self.value)
local choice
if k and k > 1 then
choice = self.choices[k-1]
else
choice = self.choices[#self.choices]
end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
end
elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
if event.x == 1 then
self:emit({ type = 'key', key = 'left' })
return true
elseif event.x == self.width then
self:emit({ type = 'key', key = 'right' })
return true
end
end
end

View File

@@ -0,0 +1,39 @@
local Canvas = require('ui.canvas')
local class = require('class')
local UI = require('ui')
local colors = _G.colors
UI.Dialog = class(UI.SlideOut)
UI.Dialog.defaults = {
UIElement = 'Dialog',
height = 7,
textColor = colors.black,
backgroundColor = colors.white,
okEvent ='dialog_ok',
cancelEvent = 'dialog_cancel',
}
function UI.Dialog:postInit()
self.y = -self.height
self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title })
end
function UI.Dialog:show(...)
local canvas = self.parent:getCanvas()
self.oldPalette = canvas.palette
canvas:applyPalette(Canvas.darkPalette)
UI.SlideOut.show(self, ...)
end
function UI.Dialog:hide(...)
self.parent:getCanvas().palette = self.oldPalette
UI.SlideOut.hide(self, ...)
self.parent:draw()
end
function UI.Dialog:eventHandler(event)
if event.type == 'dialog_cancel' then
self:hide()
end
return UI.SlideOut.eventHandler(self, event)
end

View File

@@ -0,0 +1,71 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
UI.DropMenu = class(UI.MenuBar)
UI.DropMenu.defaults = {
UIElement = 'DropMenu',
backgroundColor = colors.white,
buttonClass = 'DropMenuItem',
}
function UI.DropMenu:setParent()
UI.MenuBar.setParent(self)
local maxWidth = 1
for y,child in ipairs(self.children) do
child.x = 1
child.y = y
if #(child.text or '') > maxWidth then
maxWidth = #child.text
end
end
for _,child in ipairs(self.children) do
child.width = maxWidth + 2
if child.spacer then
child.text = string.rep('-', child.width - 2)
end
end
self.height = #self.children + 1
self.width = maxWidth + 2
self.ow = self.width
self.canvas = self:addLayer()
end
function UI.DropMenu:enable()
end
function UI.DropMenu:show(x, y)
self.x, self.y = x, y
self.canvas:move(x, y)
self.canvas:setVisible(true)
UI.Window.enable(self)
self:draw()
self:capture(self)
self:focusFirst()
end
function UI.DropMenu:hide()
self:disable()
self.canvas:setVisible(false)
self:release(self)
end
function UI.DropMenu:eventHandler(event)
if event.type == 'focus_lost' and self.enabled then
if not Util.contains(self.children, event.focused) then
self:hide()
end
elseif event.type == 'mouse_out' and self.enabled then
self:hide()
self:refocus()
else
return UI.MenuBar.eventHandler(self, event)
end
return true
end

View File

@@ -0,0 +1,21 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
--[[-- DropMenuItem --]]--
UI.DropMenuItem = class(UI.Button)
UI.DropMenuItem.defaults = {
UIElement = 'DropMenuItem',
textColor = colors.black,
backgroundColor = colors.white,
textFocusColor = colors.white,
textInactiveColor = colors.lightGray,
backgroundFocusColor = colors.lightGray,
}
function UI.DropMenuItem:eventHandler(event)
if event.type == 'button_activate' then
self.parent:hide()
end
return UI.Button.eventHandler(self, event)
end

View File

@@ -0,0 +1,69 @@
local class = require('class')
local Terminal = require('terminal')
local UI = require('ui')
local colors = _G.colors
UI.Embedded = class(UI.Window)
UI.Embedded.defaults = {
UIElement = 'Embedded',
backgroundColor = colors.black,
textColor = colors.white,
maxScroll = 100,
accelerators = {
up = 'scroll_up',
down = 'scroll_down',
}
}
function UI.Embedded:setParent()
UI.Window.setParent(self)
self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false)
self.win.setMaxScroll(self.maxScroll)
local canvas = self:getCanvas()
self.win.getCanvas().parent = canvas
table.insert(canvas.layers, self.win.getCanvas())
self.canvas = self.win.getCanvas()
self.win.setCursorPos(1, 1)
self.win.setBackgroundColor(self.backgroundColor)
self.win.setTextColor(self.textColor)
self.win.clear()
end
function UI.Embedded:draw()
self.canvas:dirty()
end
function UI.Embedded:enable()
self.canvas:setVisible(true)
self.canvas:raise()
if self.visible then
-- the window will automatically update on changes
-- the canvas does not need to be rendereed
self.win.setVisible(true)
end
UI.Window.enable(self)
self.canvas:dirty()
end
function UI.Embedded:disable()
self.canvas:setVisible(false)
self.win.setVisible(false)
UI.Window.disable(self)
end
function UI.Embedded:eventHandler(event)
if event.type == 'scroll_up' then
self.win.scrollUp()
return true
elseif event.type == 'scroll_down' then
self.win.scrollDown()
return true
end
end
function UI.Embedded:focus()
-- allow scrolling
end

View File

@@ -0,0 +1,149 @@
local class = require('class')
local Sound = require('sound')
local UI = require('ui')
local colors = _G.colors
UI.Form = class(UI.Window)
UI.Form.defaults = {
UIElement = 'Form',
values = { },
margin = 2,
event = 'form_complete',
cancelEvent = 'form_cancel',
}
function UI.Form:postInit()
self:createForm()
end
function UI.Form:reset()
for _,child in pairs(self.children) do
if child.reset then
child:reset()
end
end
end
function UI.Form:setValues(values)
self:reset()
self.values = values
for _,child in pairs(self.children) do
if child.formKey then
-- this should be child:setValue(self.values[child.formKey])
-- so chooser can set default choice if null
-- null should be valid as well
child.value = self.values[child.formKey] or ''
end
end
end
function UI.Form:createForm()
self.children = self.children or { }
if not self.labelWidth then
self.labelWidth = 1
for _, child in pairs(self) do
if type(child) == 'table' and child.UIElement then
if child.formLabel then
self.labelWidth = math.max(self.labelWidth, #child.formLabel + 2)
end
end
end
end
local y = self.margin
for _, child in pairs(self) do
if type(child) == 'table' and child.UIElement then
if child.formKey then
child.value = self.values[child.formKey] or ''
end
if child.formLabel then
child.x = self.labelWidth + self.margin - 1
child.y = y
if not child.width and not child.ex then
child.ex = -self.margin
end
table.insert(self.children, UI.Text {
x = self.margin,
y = y,
textColor = colors.black,
width = #child.formLabel,
value = child.formLabel,
})
end
if child.formKey or child.formLabel then
y = y + 1
end
end
end
if not self.manualControls then
table.insert(self.children, UI.Button {
y = -self.margin, x = -12 - self.margin,
text = 'Ok',
event = 'form_ok',
})
table.insert(self.children, UI.Button {
y = -self.margin, x = -7 - self.margin,
text = 'Cancel',
event = self.cancelEvent,
})
end
end
function UI.Form:validateField(field)
if field.required then
if not field.value or #tostring(field.value) == 0 then
return false, 'Field is required'
end
end
if field.validate == 'numeric' then
if #tostring(field.value) > 0 then
if not tonumber(field.value) then
return false, 'Invalid number'
end
end
end
return true
end
function UI.Form:save()
for _,child in pairs(self.children) do
if child.formKey then
local s, m = self:validateField(child)
if not s then
self:setFocus(child)
Sound.play('entity.villager.no', .5)
self:emit({ type = 'form_invalid', message = m, field = child })
return false
end
end
end
for _,child in pairs(self.children) do
if child.formKey then
if (child.pruneEmpty and type(child.value) == 'string' and #child.value == 0) or
(child.pruneEmpty and type(child.value) == 'boolean' and not child.value) then
self.values[child.formKey] = nil
elseif child.validate == 'numeric' then
self.values[child.formKey] = tonumber(child.value)
else
self.values[child.formKey] = child.value
end
end
end
return true
end
function UI.Form:eventHandler(event)
if event.type == 'form_ok' then
if not self:save() then
return false
end
self:emit({ type = self.event, UIElement = self, values = self.values })
else
return UI.Window.eventHandler(self, event)
end
return true
end

View File

@@ -0,0 +1,492 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
local os = _G.os
local _rep = string.rep
local _sub = string.sub
local function safeValue(v)
local t = type(v)
if t == 'string' or t == 'number' then
return v
end
return tostring(v)
end
local Writer = class()
function Writer:init(element, y)
self.element = element
self.y = y
self.x = 1
end
function Writer:write(s, width, justify, bg, fg)
local len = #tostring(s or '')
if len > width then
s = _sub(s, 1, width)
end
local padding = len < width and _rep(' ', width - len)
if padding then
if justify == 'right' then
s = padding .. s
else
s = s .. padding
end
end
self.element:write(self.x, self.y, s, bg, fg)
self.x = self.x + width
end
function Writer:finish(bg)
if self.x <= self.element.width then
self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg)
end
self.x = 1
self.y = self.y + 1
end
--[[-- Grid --]]--
UI.Grid = class(UI.Window)
UI.Grid.defaults = {
UIElement = 'Grid',
index = 1,
inverseSort = false,
disableHeader = false,
headerHeight = 1,
marginRight = 0,
textColor = colors.white,
textSelectedColor = colors.white,
backgroundColor = colors.black,
backgroundSelectedColor = colors.gray,
headerBackgroundColor = colors.cyan,
headerTextColor = colors.white,
headerSortColor = colors.yellow,
unfocusedTextSelectedColor = colors.white,
unfocusedBackgroundSelectedColor = colors.gray,
focusIndicator = '>',
sortIndicator = ' ',
inverseSortIndicator = '^',
values = { },
columns = { },
accelerators = {
enter = 'key_enter',
[ 'control-c' ] = 'copy',
down = 'scroll_down',
up = 'scroll_up',
home = 'scroll_top',
[ 'end' ] = 'scroll_bottom',
pageUp = 'scroll_pageUp',
[ 'control-b' ] = 'scroll_pageUp',
pageDown = 'scroll_pageDown',
[ 'control-f' ] = 'scroll_pageDown',
},
}
function UI.Grid:setParent()
UI.Window.setParent(self)
for _,c in pairs(self.columns) do
c.cw = c.width
if not c.heading then
c.heading = ''
end
end
self:update()
if not self.pageSize then
if self.disableHeader then
self.pageSize = self.height
else
self.pageSize = self.height - self.headerHeight
end
end
end
function UI.Grid:resize()
UI.Window.resize(self)
if self.disableHeader then
self.pageSize = self.height
else
self.pageSize = self.height - self.headerHeight
end
self:adjustWidth()
end
function UI.Grid:adjustWidth()
local t = { } -- cols without width
local w = self.width - #self.columns - 1 - self.marginRight -- width remaining
for _,c in pairs(self.columns) do
if c.width then
c.cw = c.width
w = w - c.cw
else
table.insert(t, c)
end
end
if #t == 0 then
return
end
if #t == 1 then
t[1].cw = #(t[1].heading or '')
t[1].cw = math.max(t[1].cw, w)
return
end
if not self.autospace then
for k,c in ipairs(t) do
c.cw = math.floor(w / (#t - k + 1))
w = w - c.cw
end
else
for _,c in ipairs(t) do
c.cw = #(c.heading or '')
w = w - c.cw
end
-- adjust the size to the length of the value
for key,row in pairs(self.values) do
if w <= 0 then
break
end
row = self:getDisplayValues(row, key)
for _,col in pairs(t) do
local value = row[col.key]
if value then
value = tostring(value)
if #value > col.cw then
w = w + col.cw
col.cw = math.min(#value, w)
w = w - col.cw
if w <= 0 then
break
end
end
end
end
end
-- last column does not get padding (right alignment)
if not self.columns[#self.columns].width then
Util.removeByValue(t, self.columns[#self.columns])
end
-- got some extra room - add some padding
if w > 0 then
for k,c in ipairs(t) do
local padding = math.floor(w / (#t - k + 1))
c.cw = c.cw + padding
w = w - padding
end
end
end
end
function UI.Grid:setPageSize(pageSize)
self.pageSize = pageSize
end
function UI.Grid:getValues()
return self.values
end
function UI.Grid:setValues(t)
self.values = t
self:update()
end
function UI.Grid:setInverseSort(inverseSort)
self.inverseSort = inverseSort
self:update()
self:setIndex(self.index)
end
function UI.Grid:setSortColumn(column)
self.sortColumn = column
end
function UI.Grid:getDisplayValues(row, key)
return row
end
function UI.Grid:getSelected()
if self.sorted then
return self.values[self.sorted[self.index]], self.sorted[self.index]
end
end
function UI.Grid:setSelected(name, value)
if self.sorted then
for k,v in pairs(self.sorted) do
if self.values[v][name] == value then
self:setIndex(k)
return
end
end
end
self:setIndex(1)
end
function UI.Grid:focus()
self:drawRows()
end
function UI.Grid:draw()
if not self.disableHeader then
self:drawHeadings()
end
if self.index <= 0 then
self:setIndex(1)
elseif self.index > #self.sorted then
self:setIndex(#self.sorted)
end
self:drawRows()
end
-- Something about the displayed table has changed
-- resort the table
function UI.Grid:update()
local function sort(a, b)
if not a[self.sortColumn] then
return false
elseif not b[self.sortColumn] then
return true
end
return self:sortCompare(a, b)
end
local function inverseSort(a, b)
return not sort(a, b)
end
local order
if self.sortColumn then
order = sort
if self.inverseSort then
order = inverseSort
end
end
self.sorted = Util.keys(self.values)
if order then
table.sort(self.sorted, function(a,b)
return order(self.values[a], self.values[b])
end)
end
self:adjustWidth()
end
function UI.Grid:drawHeadings()
if self.headerHeight > 1 then
self:clear(self.headerBackgroundColor)
end
local sb = Writer(self, math.ceil(self.headerHeight / 2))
for _,col in ipairs(self.columns) do
local ind = ' '
local color = self.headerTextColor
if col.key == self.sortColumn then
if self.inverseSort then
ind = self.inverseSortIndicator
else
ind = self.sortIndicator
end
color = self.headerSortColor
end
sb:write(ind .. col.heading,
col.cw + 1,
col.justify,
self.headerBackgroundColor,
color)
end
sb:finish(self.headerBackgroundColor)
end
function UI.Grid:sortCompare(a, b)
a = safeValue(a[self.sortColumn])
b = safeValue(b[self.sortColumn])
if type(a) == type(b) then
return a < b
end
return tostring(a) < tostring(b)
end
function UI.Grid:drawRows()
local startRow = math.max(1, self:getStartRow())
local sb = Writer(self, self.disableHeader and 1 or self.headerHeight + 1)
local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted)
for index = startRow, lastRow do
local key = self.sorted[index]
local rawRow = self.values[key]
local row = self:getDisplayValues(rawRow, key)
local ind = ' '
if self.focused and index == self.index and not self.inactive then
ind = self.focusIndicator
end
local selected = index == self.index and not self.inactive
local bg = self:getRowBackgroundColor(rawRow, selected)
local fg = self:getRowTextColor(rawRow, selected)
for _,col in pairs(self.columns) do
sb:write(ind .. safeValue(row[col.key] or ''),
col.cw + 1,
col.justify,
bg,
fg)
ind = ' '
end
sb:finish(bg)
end
if sb.y <= self.height then
self:clearArea(1, sb.y, self.width, self.height - sb.y + 1)
end
end
function UI.Grid:getRowTextColor(row, selected)
if selected then
if self.focused then
return self.textSelectedColor
end
return self.unfocusedTextSelectedColor
end
return self.textColor
end
function UI.Grid:getRowBackgroundColor(row, selected)
if selected then
if self.focused then
return self.backgroundSelectedColor
end
return self.unfocusedBackgroundSelectedColor
end
return self.backgroundColor
end
function UI.Grid:getIndex()
return self.index
end
function UI.Grid:setIndex(index)
index = math.max(1, index)
self.index = math.min(index, #self.sorted)
local selected = self:getSelected()
if selected ~= self.selected then
self:drawRows()
self.selected = selected
if selected then
self:emit({ type = 'grid_focus_row', selected = selected, element = self })
end
end
end
function UI.Grid:getStartRow()
return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1
end
function UI.Grid:getPage()
return math.floor(self.index / self.pageSize) + 1
end
function UI.Grid:getPageCount()
local tableSize = Util.size(self.values)
local pc = math.floor(tableSize / self.pageSize)
if tableSize % self.pageSize > 0 then
pc = pc + 1
end
return pc
end
function UI.Grid:nextPage()
self:setPage(self:getPage() + 1)
end
function UI.Grid:previousPage()
self:setPage(self:getPage() - 1)
end
function UI.Grid:setPage(pageNo)
-- 1 based paging
self:setIndex((pageNo-1) * self.pageSize + 1)
end
function UI.Grid:eventHandler(event)
if event.type == 'mouse_click' or
event.type == 'mouse_rightclick' or
event.type == 'mouse_doubleclick' then
if not self.disableHeader then
if event.y <= self.headerHeight then
local col = 2
for _,c in ipairs(self.columns) do
if event.x < col + c.cw then
self:emit({
type = 'grid_sort',
sortColumn = c.key,
inverseSort = self.sortColumn == c.key and not self.inverseSort,
element = self,
})
break
end
col = col + c.cw + 1
end
return true
end
end
local row = self:getStartRow() + event.y - 1
if not self.disableHeader then
row = row - self.headerHeight
end
if row > 0 and row <= Util.size(self.values) then
self:setIndex(row)
if event.type == 'mouse_doubleclick' then
self:emit({ type = 'key_enter' })
elseif event.type == 'mouse_rightclick' then
self:emit({ type = 'grid_select_right', selected = self.selected, element = self })
end
return true
end
return false
elseif event.type == 'grid_sort' then
self.sortColumn = event.sortColumn
self:setInverseSort(event.inverseSort)
self:draw()
elseif event.type == 'scroll_down' then
self:setIndex(self.index + 1)
elseif event.type == 'scroll_up' then
self:setIndex(self.index - 1)
elseif event.type == 'scroll_top' then
self:setIndex(1)
elseif event.type == 'scroll_bottom' then
self:setIndex(Util.size(self.values))
elseif event.type == 'scroll_pageUp' then
self:setIndex(self.index - self.pageSize)
elseif event.type == 'scroll_pageDown' then
self:setIndex(self.index + self.pageSize)
elseif event.type == 'scroll_to' then
self:setIndex(event.offset)
elseif event.type == 'key_enter' then
if self.selected then
self:emit({ type = 'grid_select', selected = self.selected, element = self })
end
elseif event.type == 'copy' then
if self.selected then
os.queueEvent('clipboard_copy', self.selected)
end
else
return false
end
return true
end

View File

@@ -0,0 +1,40 @@
local class = require('class')
local UI = require('ui')
UI.Image = class(UI.Window)
UI.Image.defaults = {
UIElement = 'Image',
event = 'button_press',
}
function UI.Image:setParent()
if self.image then
self.height = #self.image
end
if self.image and not self.width then
self.width = #self.image[1]
end
UI.Window.setParent(self)
end
function UI.Image:draw()
self:clear()
if self.image then
for y = 1, #self.image do
local line = self.image[y]
for x = 1, #line do
local ch = line[x]
if type(ch) == 'number' then
if ch > 0 then
self:write(x, y, ' ', ch)
end
else
self:write(x, y, ch)
end
end
end
end
end
function UI.Image:setImage(image)
self.image = image
end

View File

@@ -0,0 +1,60 @@
local class = require('class')
local UI = require('ui')
--[[-- Menu --]]--
UI.Menu = class(UI.Grid)
UI.Menu.defaults = {
UIElement = 'Menu',
disableHeader = true,
columns = { { heading = 'Prompt', key = 'prompt', width = 20 } },
}
function UI.Menu:postInit()
self.values = self.menuItems
self.pageSize = #self.menuItems
end
function UI.Menu:setParent()
UI.Grid.setParent(self)
self.itemWidth = 1
for _,v in pairs(self.values) do
if #v.prompt > self.itemWidth then
self.itemWidth = #v.prompt
end
end
self.columns[1].width = self.itemWidth
if self.centered then
self:center()
else
self.width = self.itemWidth + 2
end
end
function UI.Menu:center()
self.x = (self.width - self.itemWidth + 2) / 2
self.width = self.itemWidth + 2
end
function UI.Menu:eventHandler(event)
if event.type == 'key' then
if event.key == 'enter' then
local selected = self.menuItems[self.index]
self:emit({
type = selected.event or 'menu_select',
selected = selected
})
return true
end
elseif event.type == 'mouse_click' then
if event.y <= #self.menuItems then
UI.Grid.setIndex(self, event.y)
local selected = self.menuItems[self.index]
self:emit({
type = selected.event or 'menu_select',
selected = selected
})
return true
end
end
return UI.Grid.eventHandler(self, event)
end

View File

@@ -0,0 +1,92 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
local function getPosition(element)
local x, y = 1, 1
repeat
x = element.x + x - 1
y = element.y + y - 1
element = element.parent
until not element
return x, y
end
UI.MenuBar = class(UI.Window)
UI.MenuBar.defaults = {
UIElement = 'MenuBar',
buttons = { },
height = 1,
backgroundColor = colors.lightGray,
textColor = colors.black,
spacing = 2,
lastx = 1,
showBackButton = false,
buttonClass = 'MenuItem',
}
UI.MenuBar.spacer = { spacer = true, text = 'spacer', inactive = true }
function UI.MenuBar:postInit()
self:addButtons(self.buttons)
end
function UI.MenuBar:addButtons(buttons)
if not self.children then
self.children = { }
end
for _,button in pairs(buttons) do
if button.UIElement then
table.insert(self.children, button)
else
local buttonProperties = {
x = self.lastx,
width = #button.text + self.spacing,
centered = false,
}
self.lastx = self.lastx + buttonProperties.width
UI:mergeProperties(buttonProperties, button)
button = UI[self.buttonClass](buttonProperties)
if button.name then
self[button.name] = button
else
table.insert(self.children, button)
end
if button.dropdown then
button.dropmenu = UI.DropMenu { buttons = button.dropdown }
end
end
end
if self.parent then
self:initChildren()
end
end
function UI.MenuBar:getActive(menuItem)
return not menuItem.inactive
end
function UI.MenuBar:eventHandler(event)
if event.type == 'button_press' and event.button.dropmenu then
if event.button.dropmenu.enabled then
event.button.dropmenu:hide()
self:refocus()
return true
else
local x, y = getPosition(event.button)
if x + event.button.dropmenu.width > self.width then
x = self.width - event.button.dropmenu.width + 1
end
for _,c in pairs(event.button.dropmenu.children) do
if not c.spacer then
c.inactive = not self:getActive(c)
end
end
event.button.dropmenu:show(x, y + 1)
end
return true
end
end

View File

@@ -0,0 +1,14 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
--[[-- MenuItem --]]--
UI.MenuItem = class(UI.Button)
UI.MenuItem.defaults = {
UIElement = 'MenuItem',
textColor = colors.black,
backgroundColor = colors.lightGray,
textFocusColor = colors.white,
backgroundFocusColor = colors.lightGray,
}

View File

@@ -0,0 +1,33 @@
local class = require('class')
local UI = require('ui')
UI.NftImage = class(UI.Window)
UI.NftImage.defaults = {
UIElement = 'NftImage',
event = 'button_press',
}
function UI.NftImage:setParent()
if self.image then
self.height = self.image.height
end
if self.image and not self.width then
self.width = self.image.width
end
UI.Window.setParent(self)
end
function UI.NftImage:draw()
if self.image then
for y = 1, self.image.height do
for x = 1, #self.image.text[y] do
self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x])
end
end
else
self:clear()
end
end
function UI.NftImage:setImage(image)
self.image = image
end

View File

@@ -0,0 +1,67 @@
local class = require('class')
local Event = require('event')
local Sound = require('sound')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
UI.Notification = class(UI.Window)
UI.Notification.defaults = {
UIElement = 'Notification',
backgroundColor = colors.gray,
height = 3,
}
function UI.Notification:draw()
end
function UI.Notification:enable()
end
function UI.Notification:error(value, timeout)
self.backgroundColor = colors.red
Sound.play('entity.villager.no', .5)
self:display(value, timeout)
end
function UI.Notification:info(value, timeout)
self.backgroundColor = colors.gray
self:display(value, timeout)
end
function UI.Notification:success(value, timeout)
self.backgroundColor = colors.green
self:display(value, timeout)
end
function UI.Notification:cancel()
if self.canvas then
Event.cancelNamedTimer('notificationTimer')
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
end
end
function UI.Notification:display(value, timeout)
self.enabled = true
local lines = Util.wordWrap(value, self.width - 2)
self.height = #lines + 1
self.y = self.parent.height - self.height + 1
if self.canvas then
self.canvas:removeLayer()
end
self.canvas = self:addLayer(self.backgroundColor, self.textColor)
self:addTransition('expandUp', { ticks = self.height })
self.canvas:setVisible(true)
self:clear()
for k,v in pairs(lines) do
self:write(2, k, v)
end
Event.addNamedTimer('notificationTimer', timeout or 3, false, function()
self:cancel()
self:sync()
end)
end

View File

@@ -0,0 +1,18 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
UI.ProgressBar = class(UI.Window)
UI.ProgressBar.defaults = {
UIElement = 'ProgressBar',
progressColor = colors.lime,
backgroundColor = colors.gray,
height = 1,
value = 0,
}
function UI.ProgressBar:draw()
self:clear()
local width = math.ceil(self.value / 100 * self.width)
self:clearArea(1, 1, width, self.height, self.progressColor)
end

View File

@@ -0,0 +1,74 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
UI.ScrollBar = class(UI.Window)
UI.ScrollBar.defaults = {
UIElement = 'ScrollBar',
lineChar = '|',
sliderChar = '#',
upArrowChar = '^',
downArrowChar = 'v',
scrollbarColor = colors.lightGray,
width = 1,
x = -1,
ey = -1,
}
function UI.ScrollBar:draw()
local view = self.parent:getViewArea()
if view.totalHeight > view.height then
local maxScroll = view.totalHeight - view.height
local percent = view.offsetY / maxScroll
local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2)))
local x = self.width
local row = view.y
if not view.static then -- does the container scroll ?
self.height = view.totalHeight
end
for i = 1, view.height - 2 do
self:write(x, row + i, self.lineChar, nil, self.scrollbarColor)
end
local y = Util.round((view.height - 2 - sliderSize) * percent)
for i = 1, sliderSize do
self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor)
end
local color = self.scrollbarColor
if view.offsetY > 0 then
color = colors.white
end
self:write(x, row, self.upArrowChar, nil, color)
color = self.scrollbarColor
if view.offsetY + view.height < view.totalHeight then
color = colors.white
end
self:write(x, row + view.height - 1, self.downArrowChar, nil, color)
end
end
function UI.ScrollBar:eventHandler(event)
if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
if event.x == 1 then
local view = self.parent:getViewArea()
if view.totalHeight > view.height then
if event.y == view.y then
self:emit({ type = 'scroll_up'})
elseif event.y == view.y + view.height - 1 then
self:emit({ type = 'scroll_down'})
else
local percent = (event.y - view.y) / (view.height - 2)
local y = math.floor((view.totalHeight - view.height) * percent)
self:emit({ type = 'scroll_to', offset = y })
end
end
return true
end
end
end

View File

@@ -0,0 +1,60 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
--[[-- ScrollingGrid --]]--
UI.ScrollingGrid = class(UI.Grid)
UI.ScrollingGrid.defaults = {
UIElement = 'ScrollingGrid',
scrollOffset = 0,
marginRight = 1,
}
function UI.ScrollingGrid:postInit()
self.scrollBar = UI.ScrollBar()
end
function UI.ScrollingGrid:drawRows()
UI.Grid.drawRows(self)
self.scrollBar:draw()
end
function UI.ScrollingGrid:getViewArea()
local y = 1
if not self.disableHeader then
y = y + self.headerHeight
end
return {
static = true, -- the container doesn't scroll
y = y, -- scrollbar Y
height = self.pageSize, -- viewable height
totalHeight = Util.size(self.values), -- total height
offsetY = self.scrollOffset, -- scroll offset
}
end
function UI.ScrollingGrid:getStartRow()
local ts = Util.size(self.values)
if ts < self.pageSize then
self.scrollOffset = 0
end
return self.scrollOffset + 1
end
function UI.ScrollingGrid:setIndex(index)
if index < self.scrollOffset + 1 then
self.scrollOffset = index - 1
elseif index - self.scrollOffset > self.pageSize then
self.scrollOffset = index - self.pageSize
end
if self.scrollOffset < 0 then
self.scrollOffset = 0
else
local ts = Util.size(self.values)
if self.pageSize + self.scrollOffset + 1 > ts then
self.scrollOffset = math.max(0, ts - self.pageSize)
end
end
UI.Grid.setIndex(self, index)
end

View File

@@ -0,0 +1,51 @@
local class = require('class')
local UI = require('ui')
--[[-- SlideOut --]]--
UI.SlideOut = class(UI.Window)
UI.SlideOut.defaults = {
UIElement = 'SlideOut',
pageType = 'modal',
}
function UI.SlideOut:setParent()
-- TODO: size should be set at this point
self:layout()
self.canvas = self:addLayer()
UI.Window.setParent(self)
end
function UI.SlideOut:enable()
end
function UI.SlideOut:show(...)
self:addTransition('expandUp')
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...)
self:draw()
self:capture(self)
self:focusFirst()
end
function UI.SlideOut:disable()
self.canvas:setVisible(false)
UI.Window.disable(self)
end
function UI.SlideOut:hide()
self:disable()
self:release(self)
self:refocus()
end
function UI.SlideOut:eventHandler(event)
if event.type == 'slide_show' then
self:show()
return true
elseif event.type == 'slide_hide' then
self:hide()
return true
end
end

View File

@@ -0,0 +1,99 @@
local class = require('class')
local Event = require('event')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
UI.StatusBar = class(UI.Window)
UI.StatusBar.defaults = {
UIElement = 'StatusBar',
backgroundColor = colors.lightGray,
textColor = colors.gray,
height = 1,
ey = -1,
}
function UI.StatusBar:adjustWidth()
-- Can only have 1 adjustable width
if self.columns then
local w = self.width - #self.columns - 1
for _,c in pairs(self.columns) do
if c.width then
c.cw = c.width -- computed width
w = w - c.width
end
end
for _,c in pairs(self.columns) do
if not c.width then
c.cw = w
end
end
end
end
function UI.StatusBar:resize()
UI.Window.resize(self)
self:adjustWidth()
end
function UI.StatusBar:setParent()
UI.Window.setParent(self)
self:adjustWidth()
end
function UI.StatusBar:setStatus(status)
if self.values ~= status then
self.values = status
self:draw()
end
end
function UI.StatusBar:setValue(name, value)
if not self.values then
self.values = { }
end
self.values[name] = value
end
function UI.StatusBar:getValue(name)
if self.values then
return self.values[name]
end
end
function UI.StatusBar:timedStatus(status, timeout)
timeout = timeout or 3
self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor)
Event.addNamedTimer('statusTimer', timeout, false, function()
if self.parent.enabled then
self:draw()
self:sync()
end
end)
end
function UI.StatusBar:getColumnWidth(name)
local c = Util.find(self.columns, 'key', name)
return c and c.cw
end
function UI.StatusBar:setColumnWidth(name, width)
local c = Util.find(self.columns, 'key', name)
if c then
c.cw = width
end
end
function UI.StatusBar:draw()
if not self.values then
self:clear()
elseif type(self.values) == 'string' then
self:write(1, 1, Util.widthify(' ' .. self.values, self.width))
else
local s = ''
for _,c in ipairs(self.columns) do
s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw)
end
self:write(1, 1, Util.widthify(s, self.width))
end
end

View File

@@ -0,0 +1,12 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
UI.Tab = class(UI.ActiveLayer)
UI.Tab.defaults = {
UIElement = 'Tab',
tabTitle = 'tab',
backgroundColor = colors.cyan,
y = 2,
}

View File

@@ -0,0 +1,45 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
UI.TabBar = class(UI.MenuBar)
UI.TabBar.defaults = {
UIElement = 'TabBar',
buttonClass = 'TabBarMenuItem',
selectedBackgroundColor = colors.cyan,
}
function UI.TabBar:enable()
UI.MenuBar.enable(self)
if not Util.find(self.children, 'selected', true) then
local menuItem = self:getFocusables()[1]
if menuItem then
menuItem.selected = true
end
end
end
function UI.TabBar:eventHandler(event)
if event.type == 'tab_select' then
local selected, si = Util.find(self.children, 'uid', event.button.uid)
local previous, pi = Util.find(self.children, 'selected', true)
if si ~= pi then
selected.selected = true
if previous then
previous.selected = false
self:emit({ type = 'tab_change', current = si, last = pi, tab = selected })
end
end
UI.MenuBar.draw(self)
end
return UI.MenuBar.eventHandler(self, event)
end
function UI.TabBar:selectTab(text)
local menuItem = Util.find(self.children, 'text', text)
if menuItem then
menuItem.selected = true
end
end

View File

@@ -0,0 +1,25 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
--[[-- TabBarMenuItem --]]--
UI.TabBarMenuItem = class(UI.Button)
UI.TabBarMenuItem.defaults = {
UIElement = 'TabBarMenuItem',
event = 'tab_select',
textColor = colors.black,
selectedBackgroundColor = colors.cyan,
unselectedBackgroundColor = colors.lightGray,
backgroundColor = colors.lightGray,
}
function UI.TabBarMenuItem:draw()
if self.selected then
self.backgroundColor = self.selectedBackgroundColor
self.backgroundFocusColor = self.selectedBackgroundColor
else
self.backgroundColor = self.unselectedBackgroundColor
self.backgroundFocusColor = self.unselectedBackgroundColor
end
UI.Button.draw(self)
end

View File

@@ -0,0 +1,89 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
UI.Tabs = class(UI.Window)
UI.Tabs.defaults = {
UIElement = 'Tabs',
}
function UI.Tabs:postInit()
self:add(self)
end
function UI.Tabs:add(children)
local buttons = { }
for _,child in pairs(children) do
if type(child) == 'table' and child.UIElement and child.tabTitle then
child.y = 2
table.insert(buttons, {
text = child.tabTitle,
event = 'tab_select',
tabUid = child.uid,
})
end
end
if not self.tabBar then
self.tabBar = UI.TabBar({
buttons = buttons,
})
else
self.tabBar:addButtons(buttons)
end
if self.parent then
return UI.Window.add(self, children)
end
end
function UI.Tabs:selectTab(tab)
local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid)
if menuItem then
self.tabBar:emit({ type = 'tab_select', button = { uid = menuItem.uid } })
end
end
function UI.Tabs:setActive(tab, active)
local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid)
if menuItem then
menuItem.inactive = not active
end
end
function UI.Tabs:enable()
self.enabled = true
self.transitionHint = nil
self.tabBar:enable()
local menuItem = Util.find(self.tabBar.children, 'selected', true)
for _,child in pairs(self.children) do
if child.uid == menuItem.tabUid then
child:enable()
self:emit({ type = 'tab_activate', activated = child })
elseif child.tabTitle then
child:disable()
end
end
end
function UI.Tabs:eventHandler(event)
if event.type == 'tab_change' then
local tab = self:find(event.tab.tabUid)
if event.current > event.last then
self.transitionHint = 'slideLeft'
else
self.transitionHint = 'slideRight'
end
for _,child in pairs(self.children) do
if child.uid == event.tab.tabUid then
child:enable()
elseif child.tabTitle then
child:disable()
end
end
self:emit({ type = 'tab_activate', activated = tab })
tab:draw()
end
end

View File

@@ -0,0 +1,20 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
UI.Text = class(UI.Window)
UI.Text.defaults = {
UIElement = 'Text',
value = '',
height = 1,
}
function UI.Text:setParent()
if not self.width and not self.ex then
self.width = #tostring(self.value)
end
UI.Window.setParent(self)
end
function UI.Text:draw()
self:write(1, 1, Util.widthify(self.value or '', self.width), self.backgroundColor)
end

View File

@@ -0,0 +1,36 @@
local class = require('class')
local UI = require('ui')
--[[-- TextArea --]]--
UI.TextArea = class(UI.Viewport)
UI.TextArea.defaults = {
UIElement = 'TextArea',
marginRight = 2,
value = '',
}
function UI.TextArea:postInit()
self.scrollBar = UI.ScrollBar()
end
function UI.TextArea:setText(text)
self:reset()
self.value = text
self:draw()
end
function UI.TextArea:focus()
-- allow keyboard scrolling
end
function UI.TextArea:draw()
self:clear()
-- self:setCursorPos(1, 1)
self.cursorX, self.cursorY = 1, 1
self:print(self.value)
for _,child in pairs(self.children) do
if child.enabled then
child:draw()
end
end
end

View File

@@ -0,0 +1,189 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
local colors = _G.colors
local os = _G.os
local _rep = string.rep
UI.TextEntry = class(UI.Window)
UI.TextEntry.defaults = {
UIElement = 'TextEntry',
value = '',
shadowText = '',
focused = false,
textColor = colors.white,
shadowTextColor = colors.gray,
backgroundColor = colors.black, -- colors.lightGray,
backgroundFocusColor = colors.black, --lightGray,
height = 1,
limit = 6,
pos = 0,
accelerators = {
[ 'control-c' ] = 'copy',
}
}
function UI.TextEntry:postInit()
self.value = tostring(self.value)
end
function UI.TextEntry:setValue(value)
self.value = value
end
function UI.TextEntry:setPosition(pos)
self.pos = pos
end
function UI.TextEntry:updateScroll()
if not self.scroll then
self.scroll = 0
end
if not self.pos then
self.pos = #tostring(self.value)
self.scroll = 0
elseif self.pos > #tostring(self.value) then
self.pos = #tostring(self.value)
self.scroll = 0
end
if self.pos - self.scroll > self.width - 2 then
self.scroll = self.pos - (self.width - 2)
elseif self.pos < self.scroll then
self.scroll = self.pos
end
end
function UI.TextEntry:draw()
local bg = self.backgroundColor
local tc = self.textColor
if self.focused then
bg = self.backgroundFocusColor
end
self:updateScroll()
local text = tostring(self.value)
if #text > 0 then
if self.scroll and self.scroll > 0 then
text = text:sub(1 + self.scroll)
end
if self.mask then
text = _rep('*', #text)
end
else
tc = self.shadowTextColor
text = self.shadowText
end
self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc)
if self.focused then
self:setCursorPos(self.pos-self.scroll+2, 1)
end
end
function UI.TextEntry:reset()
self.pos = 0
self.value = ''
self:draw()
self:updateCursor()
end
function UI.TextEntry:updateCursor()
self:updateScroll()
self:setCursorPos(self.pos-self.scroll+2, 1)
end
function UI.TextEntry:focus()
self:draw()
if self.focused then
self:setCursorBlink(true)
else
self:setCursorBlink(false)
end
end
--[[
A few lines below from theoriginalbit
http://www.computercraft.info/forums2/index.php?/topic/16070-read-and-limit-length-of-the-input-field/
--]]
function UI.TextEntry:eventHandler(event)
if event.type == 'key' then
local ch = event.key
if ch == 'left' then
if self.pos > 0 then
self.pos = math.max(self.pos-1, 0)
self:draw()
end
elseif ch == 'right' then
local input = tostring(self.value)
if self.pos < #input then
self.pos = math.min(self.pos+1, #input)
self:draw()
end
elseif ch == 'home' then
self.pos = 0
self:draw()
elseif ch == 'end' then
self.pos = #tostring(self.value)
self:draw()
elseif ch == 'backspace' then
if self.pos > 0 then
local input = tostring(self.value)
self.value = input:sub(1, self.pos-1) .. input:sub(self.pos+1)
self.pos = self.pos - 1
self:draw()
self:emit({ type = 'text_change', text = self.value, element = self })
end
elseif ch == 'delete' then
local input = tostring(self.value)
if self.pos < #input then
self.value = input:sub(1, self.pos) .. input:sub(self.pos+2)
self:draw()
self:emit({ type = 'text_change', text = self.value, element = self })
end
elseif #ch == 1 then
local input = tostring(self.value)
if #input < self.limit then
self.value = input:sub(1, self.pos) .. ch .. input:sub(self.pos+1)
self.pos = self.pos + 1
self:draw()
self:emit({ type = 'text_change', text = self.value, element = self })
end
else
return false
end
return true
elseif event.type == 'copy' then
os.queueEvent('clipboard_copy', self.value)
elseif event.type == 'paste' then
local input = tostring(self.value)
local text = event.text
if #input + #text > self.limit then
text = text:sub(1, self.limit-#input)
end
self.value = input:sub(1, self.pos) .. text .. input:sub(self.pos+1)
self.pos = self.pos + #text
self:draw()
self:updateCursor()
self:emit({ type = 'text_change', text = self.value, element = self })
return true
elseif event.type == 'mouse_click' then
if self.focused and event.x > 1 then
self.pos = event.x + self.scroll - 2
self:updateCursor()
return true
end
elseif event.type == 'mouse_rightclick' then
local input = tostring(self.value)
if #input > 0 then
self:reset()
self:emit({ type = 'text_change', text = self.value, element = self })
end
end
return false
end

View File

@@ -0,0 +1,65 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
local os = _G.os
UI.Throttle = class(UI.Window)
UI.Throttle.defaults = {
UIElement = 'Throttle',
backgroundColor = colors.gray,
bordercolor = colors.cyan,
height = 4,
width = 10,
timeout = .075,
ctr = 0,
image = {
' //) (O )~@ &~&-( ?Q ',
' //) (O )- @ \\-( ?) && ',
' //) (O ), @ \\-(?) && ',
' //) (O ). @ \\-d ) (@ '
}
}
function UI.Throttle:setParent()
self.x = math.ceil((self.parent.width - self.width) / 2)
self.y = math.ceil((self.parent.height - self.height) / 2)
UI.Window.setParent(self)
end
function UI.Throttle:enable()
self.c = os.clock()
self.enabled = false
end
function UI.Throttle:disable()
if self.canvas then
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
self.ctr = 0
end
end
function UI.Throttle:update()
local cc = os.clock()
if cc > self.c + self.timeout then
os.sleep(0)
self.c = os.clock()
self.enabled = true
if not self.canvas then
self.canvas = self:addLayer(self.backgroundColor, self.borderColor)
self.canvas:setVisible(true)
self:clear(self.borderColor)
end
local image = self.image[self.ctr + 1]
local width = self.width - 2
for i = 0, #self.image do
self:write(2, i + 1, image:sub(width * i + 1, width * i + width),
self.backgroundColor, self.textColor)
end
self.ctr = (self.ctr + 1) % #self.image
self:sync()
end
end

View File

@@ -0,0 +1,73 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
local _rep = string.rep
local _sub = string.sub
-- For manipulating text in a fixed width string
local SB = class()
function SB:init(width)
self.width = width
self.buf = _rep(' ', width)
end
function SB:insert(x, str, width)
if x < 1 then
x = self.width + x + 1
end
width = width or #str
if x + width - 1 > self.width then
width = self.width - x
end
if width > 0 then
self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width)
end
end
function SB:fill(x, ch, width)
width = width or self.width - x + 1
self:insert(x, _rep(ch, width))
end
function SB:center(str)
self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str)
end
function SB:get()
return self.buf
end
UI.TitleBar = class(UI.Window)
UI.TitleBar.defaults = {
UIElement = 'TitleBar',
height = 1,
textColor = colors.white,
backgroundColor = colors.cyan,
title = '',
frameChar = '-',
closeInd = '*',
}
function UI.TitleBar:draw()
local sb = SB(self.width)
sb:fill(2, self.frameChar, sb.width - 3)
sb:center(string.format(' %s ', self.title))
if self.previousPage or self.event then
sb:insert(-1, self.closeInd)
else
sb:insert(-2, self.frameChar)
end
self:write(1, 1, sb:get())
end
function UI.TitleBar:eventHandler(event)
if event.type == 'mouse_click' then
if (self.previousPage or self.event) and event.x == self.width then
if self.event then
self:emit({ type = self.event, element = self })
elseif type(self.previousPage) == 'string' or
type(self.previousPage) == 'table' then
UI:setPage(self.previousPage)
else
UI:setPreviousPage()
end
return true
end
end
end

View File

@@ -0,0 +1,18 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
UI.VerticalMeter = class(UI.Window)
UI.VerticalMeter.defaults = {
UIElement = 'VerticalMeter',
backgroundColor = colors.gray,
meterColor = colors.lime,
width = 1,
value = 0,
}
function UI.VerticalMeter:draw()
local height = self.height - math.ceil(self.value / 100 * self.height)
self:clear()
self:clearArea(1, height + 1, self.width, self.height, self.meterColor)
end

View File

@@ -0,0 +1,96 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
--[[-- Viewport --]]--
UI.Viewport = class(UI.Window)
UI.Viewport.defaults = {
UIElement = 'Viewport',
backgroundColor = colors.cyan,
accelerators = {
down = 'scroll_down',
up = 'scroll_up',
home = 'scroll_top',
[ 'end' ] = 'scroll_bottom',
pageUp = 'scroll_pageUp',
[ 'control-b' ] = 'scroll_pageUp',
pageDown = 'scroll_pageDown',
[ 'control-f' ] = 'scroll_pageDown',
},
}
function UI.Viewport:setParent()
UI.Window.setParent(self)
self.canvas = self:addLayer()
end
function UI.Viewport:enable()
UI.Window.enable(self)
self.canvas:setVisible(true)
end
function UI.Viewport:disable()
UI.Window.disable(self)
self.canvas:setVisible(false)
end
function UI.Viewport:setScrollPosition(offset)
local oldOffset = self.offy
self.offy = math.max(offset, 0)
self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height)
if self.offy ~= oldOffset then
if self.scrollBar then
self.scrollBar:draw()
end
self.canvas.offy = offset
self.canvas:dirty()
end
end
function UI.Viewport:write(x, y, text, bg, tc)
if y > #self.canvas.lines then
for i = #self.canvas.lines, y do
self.canvas.lines[i + 1] = { }
self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor)
end
end
return UI.Window.write(self, x, y, text, bg, tc)
end
function UI.Viewport:reset()
self.offy = 0
self.canvas.offy = 0
for i = self.height + 1, #self.canvas.lines do
self.canvas.lines[i] = nil
end
end
function UI.Viewport:getViewArea()
return {
y = (self.offy or 0) + 1,
height = self.height,
totalHeight = #self.canvas.lines,
offsetY = self.offy or 0,
}
end
function UI.Viewport:eventHandler(event)
if event.type == 'scroll_down' then
self:setScrollPosition(self.offy + 1)
elseif event.type == 'scroll_up' then
self:setScrollPosition(self.offy - 1)
elseif event.type == 'scroll_top' then
self:setScrollPosition(0)
elseif event.type == 'scroll_bottom' then
self:setScrollPosition(10000000)
elseif event.type == 'scroll_pageUp' then
self:setScrollPosition(self.offy - self.height)
elseif event.type == 'scroll_pageDown' then
self:setScrollPosition(self.offy + self.height)
elseif event.type == 'scroll_to' then
self:setScrollPosition(event.offset)
else
return false
end
return true
end

View File

@@ -0,0 +1,124 @@
local class = require('class')
local UI = require('ui')
local Util = require('util')
UI.Wizard = class(UI.Window)
UI.Wizard.defaults = {
UIElement = 'Wizard',
pages = { },
}
function UI.Wizard:postInit()
self.cancelButton = UI.Button {
x = 2, y = -1,
text = 'Cancel',
event = 'cancel',
}
self.previousButton = UI.Button {
x = -18, y = -1,
text = '< Back',
event = 'previousView',
}
self.nextButton = UI.Button {
x = -9, y = -1,
text = 'Next >',
event = 'nextView',
}
Util.merge(self, self.pages)
--for _, child in pairs(self.pages) do
-- child.ey = -2
--end
end
function UI.Wizard:add(pages)
Util.merge(self.pages, pages)
Util.merge(self, pages)
for _, child in pairs(self.pages) do
child.ey = child.ey or -2
end
if self.parent then
self:initChildren()
end
end
function UI.Wizard:getPage(index)
return Util.find(self.pages, 'index', index)
end
function UI.Wizard:enable(...)
self.enabled = true
self.index = 1
self.transitionHint = nil
local initial = self:getPage(1)
for _,child in pairs(self.children) do
if child == initial or not child.index then
child:enable(...)
else
child:disable()
end
end
self:emit({ type = 'enable_view', next = initial })
end
function UI.Wizard:isViewValid()
local currentView = self:getPage(self.index)
return not currentView.validate and true or currentView:validate()
end
function UI.Wizard:eventHandler(event)
if event.type == 'nextView' then
local currentView = self:getPage(self.index)
if self:isViewValid() then
self.index = self.index + 1
local nextView = self:getPage(self.index)
currentView:emit({ type = 'enable_view', next = nextView, current = currentView })
end
elseif event.type == 'previousView' then
local currentView = self:getPage(self.index)
local nextView = self:getPage(self.index - 1)
if nextView then
self.index = self.index - 1
currentView:emit({ type = 'enable_view', prev = nextView, current = currentView })
end
return true
elseif event.type == 'wizard_complete' then
if self:isViewValid() then
self:emit({ type = 'accept' })
end
elseif event.type == 'enable_view' then
local current = event.next or event.prev
if not current then error('property "index" is required on wizard pages') end
if event.current then
if event.next then
self.transitionHint = 'slideLeft'
elseif event.prev then
self.transitionHint = 'slideRight'
end
event.current:disable()
end
if self:getPage(self.index - 1) then
self.previousButton:enable()
else
self.previousButton:disable()
end
if self:getPage(self.index + 1) then
self.nextButton.text = 'Next >'
self.nextButton.event = 'nextView'
else
self.nextButton.text = 'Accept'
self.nextButton.event = 'wizard_complete'
end
-- a new current view
current:enable()
current:emit({ type = 'view_enabled', view = current })
self:draw()
end
end

View File

@@ -0,0 +1,11 @@
local class = require('class')
local UI = require('ui')
local colors = _G.colors
UI.WizardPage = class(UI.ActiveLayer)
UI.WizardPage.defaults = {
UIElement = 'WizardPage',
backgroundColor = colors.cyan,
ey = -2,
}