Vyzor and Geyser are both fairly powerful window management systems, but they can be overkill for simple GUI layouts, and require learning a large amount of new syntax to interact with them. Also, my experience with Geyser left me frustrated with how complicated it was to position objects using a mix of percent and pixel values. So I created this script to address those issues. It handles window positioning and sizing, and adjusts position and size as the main window is resized, but stays out of the way the rest of the time. You create the GUI objects you want (labels, miniconsoles, gauges, mapper, and autowrap [a miniconsole window for which SWM automatically adjusts wordwrap as the window size changes]), using the windowManager.create function, or create the GUI objects yourself and pass them to SWM manually using the windowManager.add function. You can set size and position using any mix of pixel and percent values, and you can designate a corner as the origin corner for that object. There is no need to remember to add your object's height or width to its x or y values so that it isn't displayed off screen. Also, so long as you are managing a relatively small number of GUI objects, this runs dramatically faster than Geyser when the window is resized (hopefully also when using a large number of objects, but I haven't tested a complex setup in Geyser recently).
I tried to include sufficient documentation as comments at the top. If there are any comments or questions, please feel free to let me know.
-- Window Manager Script --
-- Zachary Hiland
-- 3/26/2017
-- v3.00a
--
-- Functions Details
--
-- Generic Functions
--
-- windowManager.create(name, type, x, y, width, height, origin, [font_size]) or windowManager.add(info_table)
-- creates an object and adds it to the windowManager
--
-- info_table must have a valid entry for each argument, by the same name, as in the normal function call
-- name - name for the object to be created (label, miniConsole, gauge, mapper, menu, or autowrap)
-- Note: for a mapper window, use any name you like, so long as it isn't the name of some other object
-- type - identify the type of window, must be label, miniConsole, gauge, mapper, autowrap, or menu
-- x - distance horizontally from the origin corner
-- y - distance vertically from the origin corner
-- width - width of window
-- height - height of window
-- origin - corner to act as origin for this window, must be topleft, topright, bottomleft, or bottomright
-- font_size - only used for miniconsoles and autowrap windows, will be automatically set as the font size for the created window
--
-- note: all measurements should be numbers, or strings with values measured in percent or pixels
-- if a measurement contains both percent and pixel values (or multiple values of the same type),
-- they should be separated by a "+" or "-" symbol. Example: "25% + 50"
--
-- windowManager.add(name, type, x, y, width, height, origin, font_size) or windowManager.create(info_table)
-- adds an already created window to the windowManager, uses the exact same arguments as those in the "create" function
--
-- windowManager.remove(name)
-- removes a window from the windowManager
--
-- windowManager.move(name, x, y)
-- moves a window with the windowManager
--
-- windowManager.resize(name, width, height)
-- resizes a window with the windowManager
--
-- windowManager.relocate(name, origin)
-- changes the corner acting as the origin for a window in the windowManager
--
-- windowManager.getValue(name, value)
-- returns the calculated size or position values, or the origin, for a window in the windowManager
--
-- note: to get size and position information on windows as they are stored (rather than the calculated
-- actual number of pixels), use windowManager.list[name]
-- x, y, h, and w values will always be shown as a string in the following format "## +/- ##%"
--
-- Functions Used for Compound Number Type (With Raw Number and Percentage Value)
--
-- windowManager.newCompoundNumber(number, percent, optional reference)
-- makes an object of the compound number type, taking up to three arguments of the number type
-- the reference number is used when evaluating the percentage value, resulting in that percent
-- of the reference number added to the base number.
--
-- windowManager.makeCompound(number, optional reference)
-- makes an object of the compound number type, taking a number or string for the first argument
-- and a number for the second argument, if a string is given, it must contain a simple expression
-- of the desired raw number and percentages to be used, separated by a space, with or without a +/- sign
-- Example: "25% - 50", "50 25%", or even "50 + -25%"
-- no more than one number and one percent value can be used
--
-- note: compound number objects created by either of these two functions can be used for any measurement
-- argument in the above functions, and can be manipulated with regular math operators (+, -, *, /)
-- two such numbers can be added or subtracted, one can be multiplied or divided by a regular number
-- or one can have a regular number, a percentage (as a string), or a string containing both a number
-- and a percentage added or subtracted to it, without the use of special functions
--
-- Functions Important for Mapper Window
--
-- windowManager.show(name)
-- shows a window that is managed by windowManager (you can also use showWindow or showGauge functions)
--
-- windowManager.hide(name)
-- hides a window that is managed by windowManager (you can also use hideWindow or hideGauge functions)
--
-- Functions Important for Autowrap Windows
--
-- windowManager.createBuffer(name)
-- creates a buffer to be used with an autowrap window, only needed if miniconsole added manually to windowManager
--
-- windowManager.append(name)
-- appends text to a miniconsole or autowrap window, works just like appendBuffer()
--
-- windowManager.echo(name, text)
-- echoes text to a miniconsole or autowrap window. cecho, decho, and hecho are also available.
--
-- windowManager.clear(name)
-- clears a miniconsole or autowrap window.
--
-- windowManager.setFontSize(name, font_size)
-- sets the font size for a miniconsole or autowrap window.
--
-- Functions Not Called by Users
--
-- windowManager.refresh(name)
-- redraws a window in the windowManager using previously set values
-- note: generally this function does not need to be called
--
-- windowManager.refreshAll()
-- note: this function is called automatically when a sysWindowResizeEvent event occurs,
-- and generally does not need to be called manually
windowManager = windowManager or {}
windowManager.list = windowManager.list or {}
-- localizing global variables
local getMainWindowSize, calcFontSize, setWindowWrap, clearWindow = getMainWindowSize, calcFontSize, setWindowWrap, clearWindow
local setMiniConsoleFontSize, copy, appendBuffer, createBuffer = setMiniConsoleFontSize, copy, appendBuffer, createBuffer
local selectCurrentLine, moveCursor, getMainWindowSize = selectCurrentLine, moveCursor, getMainWindowSize
local find, gmatch, gsub, sub, lower = string.find, string.gmatch, string.gsub, string.sub, string.lower
local floor, contains, update = math.floor, table.contains, table.update
local tonumber, error, type, pcall, getmetatable, setmetatable, pairs = tonumber, error, type, pcall, getmetatable, setmetatable, pairs
local make_compound, list, refresh, getValue
local signs = {["-"] = -1, ["+"] = 1}
local window_origins = {"topleft","topright","bottomleft","bottomright"}
local window_values = {"x","y","width","height","w","h","origin"}
local function round(num, idp)
if idp then
assert(idp > 0 and idp == floor(idp), "Invalid decimal place!")
local mult = 10^idp
return floor(num * mult + 0.5) / mult
else
return floor(num + 0.5)
end
end
local function is_compound(test) return type(test) == "table" and test.type == "compound number" end
local function pick_compound(a,b)
if is_compound(a) then
return a, b
else
return b, a
end
end
local compound_number_class
compound_number_class = {
__add = function(a, b)
local tmp,self,value,status
self,value = pick_compound(a,b)
status, value = pcall(make_compound,value)
if value then
tmp = compound_number_class:new(self.number + value.number, self.percent + value.percent, self.reference)
else
error("Compound Number Class: Add: value must be a number or a percentage",1)
end
return tmp
end,
__sub = function(a, b)
local tmp,self,value,status
self,value = pick_compound(a,b)
status, value = pcall(make_compound,value)
if value then
tmp = compound_number_class:new(self.number - value.number, self.percent - value.percent, self.reference)
else
error("Compound Number Class: Subtract: value must be a number or a percentage",1)
end
return tmp
end,
__mul = function(a, b)
local tmp
a,b = pick_compound(a,b)
if tonumber(b) then
tmp = compound_number_class:new(a.number * b, a.percent * b, a.reference)
else
error("Compound Number Class: Multiply: value must be a number or a percentage",1)
end
return tmp
end,
__div = function(a, b)
local tmp
if tonumber(b) then
tmp = compound_number_class:new(a.number / b, a.percent / b, a.reference)
else
error("Compound Number Class: Divide: value must be a number or a percentage",1)
end
return tmp
end,
__unm = function(self)
local tmp = compound_number_class:new(0 - self.number, 0 - self.percent, self.reference)
return tmp
end,
__tostring = function(self)
local str = self.number
if self.percent >= 0 then
str = str .. " + "
else
str = str .. " - "
end
str = str .. self.percent .. "%"
return str
end,
__index = function(self, key)
if key == "value" then
return self:getValue()
else
return getmetatable(self)[key] or self._values[key]
end
end,
__newindex = function(self,key,value)
if key == "number" or key == "percent" or key == "reference" then
if tonumber(value) then
self._values[key] = tonumber(value)
else
error("Compound Number Class: property values must be numbers",1)
end
else
error("Compound Number Class: cannot add index/key to class object",1)
end
end,
getValue = function(self,rnd)
local val = self.number + self.reference * (self.percent / 100)
if rnd then val = round(val, (rnd ~= 0 and rnd)) end
return val
end,
new = function(self,number,percent,reference)
local o = {_values = {number = number or 0, percent = percent or 0, reference = reference or 1, type = "compound number"}}
setmetatable(o,self)
return o
end,
}
local function rewrap_window(name)
local info = list[name]
local buffer = name .. "_windowManager_buffer"
local wrap = floor(getValue(name,"width") / calcFontSize(info.font))
local line, moved
setWindowWrap(name,wrap)
clearWindow(name)
line = 0
moved = moveCursor(buffer,1,line)
while moved do
selectCurrentLine(buffer)
copy(buffer)
line = line + 1
moved = moveCursor(buffer,1,line)
appendBuffer(name)
end
end
local window_creation_functions = {
label = createLabel,
miniconsole = createMiniConsole,
gauge = createGauge,
menu = createMenu,
autowrap = function(name)
createMiniConsole(name,0,0,0,0)
windowManager.makeBuffer(name)
end}
local window_refresh_functions = {
label = function(name,x,y,w,h) moveWindow(name,x,y) resizeWindow(name,w,h) end,
miniconsole = function(name,x,y,w,h) moveWindow(name,x,y) resizeWindow(name,w,h) end,
gauge = function(name,x,y,w,h) moveGauge(name,x,y) resizeGauge(name,w,h) end,
menu = function(name,x,y,w,h) moveMenu(name,x,y) resizeMenu(name,w,h) end,
autowrap = function(name,x,y,w,h) moveWindow(name,x,y) resizeWindow(name,w,h) rewrap_window(name) end,
mapper = function(name,x,y,w,h,hide) if not hide then createMapper(x,y,w,h) end end,
}
local window_hide_functions = {
label = hideWindow,
miniconsole = hideWindow,
autowrap = hideWindow,
gauge = hideGauge,
menu = hideMenu,
mapper = function(name) list[name].hide = true createMapper(0,0,0,0) end,
}
local window_show_functions = {
label = showWindow,
miniconsole = showWindow,
autowrap = showWindow,
gauge = showGauge,
menu = showMenu,
mapper = function(name) list[name].hide = false refresh(name) end
}
local function run_console(func,name,text)
local info = list[name]
if not info then error("windowManager." .. func .. ": no such window.",2) end
if info.type == "miniconsole" or info.type == "autowrap" then
func(name, text)
func(name .. "_windowManager_buffer", text)
end
end
function windowManager.newCompoundNumber(number,percent,reference) return compound_number_class:new(number,percent,reference) end
function windowManager.makeCompound(value,reference)
local sign, number, percent
if type(value) == "number" or tonumber(value) then
number = tonumber(value)
elseif type(value) == "string" then
for w in gmatch(value,"[^%s]+") do
if find(w,"%%") then
percent = gsub(w,"%%","")
percent = tonumber(percent) * (sign or 1)
elseif signs[w] then
sign = signs[w]
else
number = tonumber(w) * (sign or 1)
end
end
elseif is_compound(value) then
return value
else
error("makeCompound: value must be a number or a string")
end
return compound_number_class:new(number or 0, percent or 0, reference or 1)
end
function windowManager.create(name, window_type, ...)
local tbl = {}
local is_table
if type(name) == "table" then
update(tbl,name)
name = tbl.name
window_type = tbl.type
is_table = true
end
if type(window_type) ~= "string" then
error("windowManager.create: bad argument #2 \"type\", must be string.",2)
end
window_type = lower(window_type)
if window_type ~= "mapper" and not window_creation_functions[window_type] then
error("windowManager.create: invalid type",2)
end
if window_creation_functions[window_type] then window_creation_functions[window_type](name,0,0,0,0,1) end
if is_table then
return windowManager.add(tbl)
else
return windowManager.add(name, window_type, ...)
end
end
function windowManager.makeBuffer(name)
createBuffer(name .. "_windowManager_buffer")
setWindowWrap(name .. "_windowManager_buffer",1000)
end
function windowManager.add(name, window_type, x, y, w, h, origin, font)
local tbl = {}
if type(name) == "table" then
tbl = update(tbl,name)
name = tbl.name
x = tbl.x
y = tbl.y
w = tbl.width
h = tbl.height
origin = tbl.origin
window_type = tbl.type
font = tbl.font_size
tbl = {}
end
font = tonumber(font) or 10
if not name then
error("windowManager.add: bad argument #1 \"name\".",2)
end
list[name] = nil
if type(window_type) ~= "string" then
error("windowManager.add: bad argument #2 \"type\", must be string.",2)
end
window_type = lower(window_type)
if window_type ~= "mapper" and not window_creation_functions[window_type] then
error("windowManager.add: invalid type",2)
end
if not (x and y and w and h) then
error("windowManager.add: must have x, y, width, and height.",2)
end
origin = lower(origin or "topleft")
if not contains(window_origins,origin) then
error("windowManager.add: bad argument #7 \"origin\".",2)
end
tbl = {
type = window_type,
x = tostring(make_compound(x)), y = tostring(make_compound(y)),
h = tostring(make_compound(h)), w = tostring(make_compound(w)),
origin = origin}
if window_type == "autowrap" or window_type == "miniconsole" then
tbl.font = font
setMiniConsoleFontSize(name, font)
end
list[name] = tbl
refresh(name)
end
function windowManager.remove(name) list[name] = nil end
function windowManager.resize(name, w, h)
local info = list[name]
if not info then error("windowManager.resize: no such window.",2) end
if not (w and h) then error("windowManager.resize: must have both width and height.",2) end
list[name].w = tostring(make_compound(w))
list[name].h = tostring(make_compound(h))
refresh(name)
end
function windowManager.move(name, x, y)
local info = list[name]
if not info then error("windowManager.move: no such window.",2) end
if not (x and y) then error("windowManager.move: must have both x and y.",2) end
list[name].x = tostring(make_compound(x))
list[name].y = tostring(make_compound(y))
refresh(name)
end
function windowManager.relocate(name, origin)
local info = list[name]
if not info then error("windowManager.relocate: no such window.",2) end
origin = lower(origin)
if not contains(window_origins,origin) then
error("windowManager.relocate: bad argument #2 \"origin\".",2)
end
list[name].origin = origin
refresh(name)
end
function windowManager.hide(name)
local info = list[name]
if not info then error("windowManager.hide: no such window.",2) end
window_hide_functions[info.type](name)
end
function windowManager.show(name)
local info = list[name]
if not info then error("windowManager.show: no such window.",2) end
window_show_functions[info.type](name)
end
function windowManager.setFontSize(name, font_size)
local info = list[name]
if not info then error("windowManager.setFontSize: no such window.",2) end
if info.type == "miniconsole" or info.type == "autowrap" then
list[name].font = font_size
setMiniConsoleFontSize(name, font_size)
refresh(name)
end
end
function windowManager.clear(name) run_console(clearWindow,name) end
function windowManager.append(name) run_console(appendBuffer,name) end
function windowManager.echo(name, text) run_console(echo,name,text) end
function windowManager.cecho(name, text) run_console(cecho,name,text) end
function windowManager.hecho(name, text) run_console(hecho,name,text) end
function windowManager.decho(name, text) run_console(decho,name,text) end
function windowManager.getValue(name, value)
local info = list[name]
if not info then error("windowManager.getValue: no such window.",2) end
if not contains(window_values,value) then
error("windowManager.getValue: no such value.",2)
end
local sys_w, sys_h = getMainWindowSize()
if value == "width" or value == "height" then value = sub(value,1,1) end
local tmp = list[name][value]
if value == "w" or value == "x" then
tmp = make_compound(tmp,sys_w):getValue(0)
elseif value == "h" or value == "y" then
tmp = make_compound(tmp,sys_h):getValue(0)
end
return tmp
end
function windowManager.refresh(name, main_w, main_h)
local info = list[name]
if not info then error("windowManager.refresh: no such window.",2) end
local x,y,w,h
local origin,win_type = info.origin, info.type
if not (main_w and main_h) then
main_w, main_h = getMainWindowSize()
end
w = make_compound(info.w,main_w):getValue(0)
x = make_compound(info.x,main_w):getValue(0)
h = make_compound(info.h,main_h):getValue(0)
y = make_compound(info.y,main_h):getValue(0)
if find(origin,"right") then
x = main_w - x - w
end
if find(origin,"bottom") then
y = main_h - y - h
end
window_refresh_functions[win_type](name,x,y,w,h,info.hide)
end
function windowManager.refreshAll()
local main_w, main_h = getMainWindowSize()
for k,v in pairs(windowManager.list) do
refresh(k, main_w, main_h)
end
end
make_compound, list, refresh, getValue = windowManager.makeCompound, windowManager.list, windowManager.refresh, windowManager.getValue
registerAnonymousEventHandler("sysWindowResizeEvent", "windowManager.refreshAll")
Like the previous version of this script, you can use strings storing the number of pixels, as well as the percentage of screen size to specify position and size of any object you use with it. For example: "50 + 25%"
In addition, a new type has been created, which I'm calling a "compound number", which handles these two values for you, and allows you to use simple math operators to interact with them. Two different functions can be used to create such an object. The first is windowManager.newCompoundNumber, which takes three arguments, the first is the raw number portion, the second is the percentage portion (as a number), and the optional third argument is a reference value that the percent is based off of when calculating an absolute value (note that the reference value of any argument you use with this script is not used, as it always references the current screen size). The second is windowManager.makeCompound , which takes two arguments, the first being either a raw number or a string with a percentage or a combination of a number and a percentage (exactly the same thing you could use on its own as a measurement argument to any of the functions in this script), and the second being an optional reference number.
Once you have created a compound number object, you can use regular math operators to manipulate it, including adding or subtracting any of a number, another compound number, or a string (the same as specified above). You can also multiply or divide by a number. You can also get the calculated value of the number by referencing its "value" property (i.e. var.value), and you can generate a string representation of it by using the tostring function (i.e. tostring(var) ), which will produce a string like the ones above, though it will always have both a raw number and a percentage value, even if one of those values is zero.
This script also works with the generalized menu GUI objects that I created and posted
.