migrate dwarfmonitor widgets to overlay v2

develop
myk002 2022-11-02 12:40:18 -07:00
parent 63410d63c7
commit 2cf6767589
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
7 changed files with 201 additions and 467 deletions

@ -1,21 +1,3 @@
{ {
"widgets": [ "date_format": "Y-M-D"
{
"type": "weather",
"x": 22,
"y": -1
},
{
"type": "date",
"x": -30,
"y": 0,
"format": "Y-M-D"
},
{
"type": "misery",
"x": -2,
"y": -1,
"anchor": "right"
}
]
} }

@ -0,0 +1,12 @@
{
"pos": {
"x": 2,
"y": 2
},
"provider": "dwarfmonitor",
"class": "Widget_cursor",
"viewscreens": [
"dungeonmode",
"dwarfmode"
]
}

@ -0,0 +1,11 @@
{
"pos": {
"x": -16,
"y": 1
},
"provider": "dwarfmonitor",
"class": "Widget_date",
"viewscreens": [
"dwarfmode"
]
}

@ -0,0 +1,11 @@
{
"pos": {
"x": -2,
"y": -1
},
"provider": "dwarfmonitor",
"class": "Widget_misery",
"viewscreens": [
"dwarfmode"
]
}

@ -0,0 +1,11 @@
{
"pos": {
"x": 15,
"y": -1
},
"provider": "dwarfmonitor",
"class": "Widget_weather",
"viewscreens": [
"dwarfmode"
]
}

@ -52,7 +52,6 @@ using std::deque;
DFHACK_PLUGIN("dwarfmonitor"); DFHACK_PLUGIN("dwarfmonitor");
DFHACK_PLUGIN_IS_ENABLED(is_enabled); DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(current_weather);
REQUIRE_GLOBAL(world); REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(ui); REQUIRE_GLOBAL(ui);
@ -74,20 +73,8 @@ struct less_second {
} }
}; };
struct dwarfmonitor_configst {
std::string date_format;
};
static dwarfmonitor_configst dwarfmonitor_config;
static bool monitor_jobs = false;
static bool monitor_misery = true;
static bool monitor_date = true;
static bool monitor_weather = true;
static map<df::unit *, deque<activity_type>> work_history; static map<df::unit *, deque<activity_type>> work_history;
static int misery[] = { 0, 0, 0, 0, 0, 0, 0 };
static bool misery_upto_date = false;
static color_value monitor_colors[] = static color_value monitor_colors[] =
{ {
COLOR_LIGHTRED, COLOR_LIGHTRED,
@ -151,102 +138,18 @@ static void move_cursor(df::coord &pos)
static void open_stats_screen(); static void open_stats_screen();
namespace dm_lua { static int getStressCategoryColors(lua_State *L) {
static color_ostream_proxy *out; const size_t n = sizeof(monitor_colors)/sizeof(color_value);
static lua_State *state; lua_createtable(L, n, 0);
typedef int(*initializer)(lua_State*); for (size_t i = 0; i < n; ++i) {
int no_args (lua_State *L) { return 0; } Lua::Push(L, monitor_colors[i]);
void cleanup() lua_rawseti(L, -2, i+1);
{
if (out)
{
delete out;
out = NULL;
}
}
bool init_call (const char *func)
{
if (!out)
out = new color_ostream_proxy(Core::getInstance().getConsole());
return Lua::PushModulePublic(*out, state, "plugins.dwarfmonitor", func);
}
bool safe_call (int nargs)
{
return Lua::SafeCall(*out, state, nargs, 0);
}
bool call (const char *func, initializer init = no_args)
{
Lua::StackUnwinder top(state);
if (!init_call(func))
return false;
int nargs = init(state);
return safe_call(nargs);
} }
namespace api {
int monitor_state (lua_State *L)
{
std::string type = luaL_checkstring(L, 1);
if (type == "weather")
lua_pushboolean(L, monitor_weather);
else if (type == "misery")
lua_pushboolean(L, monitor_misery);
else if (type == "date")
lua_pushboolean(L, monitor_date);
else
lua_pushnil(L);
return 1; return 1;
}
int get_weather_counts (lua_State *L)
{
#define WEATHER_TYPES WTYPE(clear, None); WTYPE(rain, Rain); WTYPE(snow, Snow);
#define WTYPE(type, name) int type = 0;
WEATHER_TYPES
#undef WTYPE
int i, j;
for (i = 0; i < 5; ++i)
{
for (j = 0; j < 5; ++j)
{
switch ((*current_weather)[i][j])
{
#define WTYPE(type, name) case weather_type::name: type++; break;
WEATHER_TYPES
#undef WTYPE
}
}
}
lua_newtable(L);
#define WTYPE(type, name) Lua::TableInsert(L, #type, type);
WEATHER_TYPES
#undef WTYPE
#undef WEATHER_TYPES
return 1;
}
int get_misery_data (lua_State *L)
{
lua_newtable(L);
for (int i = 0; i < 7; i++)
{
Lua::Push(L, i);
lua_newtable(L);
Lua::TableInsert(L, "value", misery[i]);
Lua::TableInsert(L, "color", monitor_colors[i]);
Lua::TableInsert(L, "last", (i == 6));
lua_settable(L, -3);
}
return 1;
}
}
} }
#define DM_LUA_FUNC(name) { #name, df::wrap_function(dm_lua::api::name, true) }
#define DM_LUA_CMD(name) { #name, dm_lua::api::name }
DFHACK_PLUGIN_LUA_COMMANDS { DFHACK_PLUGIN_LUA_COMMANDS {
DM_LUA_CMD(monitor_state), DFHACK_LUA_COMMAND(getStressCategoryColors),
DM_LUA_CMD(get_weather_counts),
DM_LUA_CMD(get_misery_data),
DFHACK_LUA_END DFHACK_LUA_END
}; };
@ -1648,8 +1551,7 @@ public:
return (selected_column == 1) ? dwarf_column.getFirstSelectedElem() : nullptr; return (selected_column == 1) ? dwarf_column.getFirstSelectedElem() : nullptr;
} }
void feed(set<df::interface_key> *input) void feed(set<df::interface_key> *input) override {
{
bool key_processed = false; bool key_processed = false;
switch (selected_column) switch (selected_column)
{ {
@ -1723,8 +1625,7 @@ public:
} }
} }
void render() void render() override {
{
using namespace df::enums::interface_key; using namespace df::enums::interface_key;
if (Screen::isDismissed(this)) if (Screen::isDismissed(this))
@ -1751,7 +1652,7 @@ public:
getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY); getSelectedUnit() ? COLOR_WHITE : COLOR_DARKGREY);
} }
std::string getFocusString() { return "dwarfmonitor_preferences"; } std::string getFocusString() override { return "dwarfmonitor_preferences"; }
private: private:
ListColumn<size_t> preferences_column; ListColumn<size_t> preferences_column;
@ -1762,13 +1663,11 @@ private:
vector<preference_map> preferences_store; vector<preference_map> preferences_store;
void validateColumn() void validateColumn() {
{
set_to_limit(selected_column, 1); set_to_limit(selected_column, 1);
} }
void resize(int32_t x, int32_t y) void resize(int32_t x, int32_t y) override {
{
dfhack_viewscreen::resize(x, y); dfhack_viewscreen::resize(x, y);
preferences_column.resize(); preferences_column.resize();
dwarf_column.resize(); dwarf_column.resize();
@ -1776,15 +1675,12 @@ private:
}; };
static void open_stats_screen() static void open_stats_screen() {
{
Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self); Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
} }
static void add_work_history(df::unit *unit, activity_type type) static void add_work_history(df::unit *unit, activity_type type) {
{ if (work_history.find(unit) == work_history.end()) {
if (work_history.find(unit) == work_history.end())
{
auto max_history = get_max_history(); auto max_history = get_max_history();
for (int i = 0; i < max_history; i++) for (int i = 0; i < max_history; i++)
work_history[unit].push_back(JOB_UNKNOWN); work_history[unit].push_back(JOB_UNKNOWN);
@ -1794,8 +1690,7 @@ static void add_work_history(df::unit *unit, activity_type type)
work_history[unit].pop_front(); work_history[unit].pop_front();
} }
static bool is_at_leisure(df::unit *unit) static bool is_at_leisure(df::unit *unit) {
{
if (Units::getMiscTrait(unit, misc_trait_type::Migrant)) if (Units::getMiscTrait(unit, misc_trait_type::Migrant))
return true; return true;
@ -1805,32 +1700,17 @@ static bool is_at_leisure(df::unit *unit)
return false; return false;
} }
static void reset() static void reset() {
{
work_history.clear(); work_history.clear();
for (int i = 0; i < 7; i++)
misery[i] = 0;
misery_upto_date = false;
} }
static void update_dwarf_stats(bool is_paused) static void update_dwarf_stats(bool is_paused)
{ {
if (monitor_misery) for (auto unit : world->units.active) {
{
for (int i = 0; i < 7; i++)
misery[i] = 0;
}
for (auto iter = world->units.active.begin(); iter != world->units.active.end(); iter++)
{
df::unit* unit = *iter;
if (!Units::isCitizen(unit)) if (!Units::isCitizen(unit))
continue; continue;
if (!DFHack::Units::isActive(unit)) if (!DFHack::Units::isActive(unit)) {
{
auto it = work_history.find(unit); auto it = work_history.find(unit);
if (it != work_history.end()) if (it != work_history.end())
work_history.erase(it); work_history.erase(it);
@ -1838,35 +1718,25 @@ static void update_dwarf_stats(bool is_paused)
continue; continue;
} }
if (monitor_misery) if (is_paused)
{
misery[get_happiness_cat(unit)]++;
}
if (!monitor_jobs || is_paused)
continue; continue;
if (Units::isBaby(unit) || if (Units::isBaby(unit) ||
Units::isChild(unit) || Units::isChild(unit) ||
unit->profession == profession::DRUNK) unit->profession == profession::DRUNK)
{
continue; continue;
}
if (ENUM_ATTR(profession, military, unit->profession)) if (ENUM_ATTR(profession, military, unit->profession)) {
{
add_work_history(unit, JOB_MILITARY); add_work_history(unit, JOB_MILITARY);
continue; continue;
} }
if (!unit->job.current_job) if (!unit->job.current_job) {
{
add_work_history(unit, JOB_IDLE); add_work_history(unit, JOB_IDLE);
continue; continue;
} }
if (is_at_leisure(unit)) if (is_at_leisure(unit)) {
{
add_work_history(unit, JOB_LEISURE); add_work_history(unit, JOB_LEISURE);
continue; continue;
} }
@ -1876,107 +1746,21 @@ static void update_dwarf_stats(bool is_paused)
} }
DFhackCExport command_result plugin_onupdate (color_ostream &out) DFhackCExport command_result plugin_onupdate (color_ostream &out) {
{ if (!is_enabled | !Maps::IsValid())
if (!monitor_jobs && !monitor_misery)
return CR_OK;
if(!Maps::IsValid())
return CR_OK; return CR_OK;
bool is_paused = DFHack::World::ReadPauseState(); bool is_paused = DFHack::World::ReadPauseState();
if (is_paused) if (!is_paused && world->frame_counter % DELTA_TICKS != 0)
{
if (monitor_misery && !misery_upto_date)
misery_upto_date = true;
else
return CR_OK; return CR_OK;
}
else
{
if (world->frame_counter % DELTA_TICKS != 0)
return CR_OK;
}
update_dwarf_stats(is_paused); update_dwarf_stats(is_paused);
return CR_OK; return CR_OK;
} }
struct dwarf_monitor_hook : public df::viewscreen_dwarfmodest DFhackCExport command_result plugin_enable(color_ostream &, bool enable) {
{ if (is_enabled != enable) {
typedef df::viewscreen_dwarfmodest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, render, ())
{
INTERPOSE_NEXT(render)();
CoreSuspendClaimer suspend;
if (Maps::IsValid())
{
dm_lua::call("render_all");
}
}
};
IMPLEMENT_VMETHOD_INTERPOSE(dwarf_monitor_hook, render);
static bool set_monitoring_mode(const string &mode, const bool &state)
{
bool mode_recognized = false;
if (!is_enabled)
return false;
/*
NOTE: although we are not touching DF directly but there might be
code running that uses these values. So this could use another mutex
or just suspend the core while we edit our values.
*/
CoreSuspender guard;
if (mode == "work" || mode == "all")
{
mode_recognized = true;
monitor_jobs = state;
if (!monitor_jobs)
reset();
}
if (mode == "misery" || mode == "all")
{
mode_recognized = true;
monitor_misery = state;
}
if (mode == "date" || mode == "all")
{
mode_recognized = true;
monitor_date = state;
}
if (mode == "weather" || mode == "all")
{
mode_recognized = true;
monitor_weather = state;
}
return mode_recognized;
}
static bool load_config()
{
return dm_lua::call("load_config");
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
{
if (enable)
{
CoreSuspender guard;
load_config();
}
if (is_enabled != enable)
{
if (!INTERPOSE_HOOK(dwarf_monitor_hook, render).apply(enable))
return CR_FAILURE;
reset(); reset();
is_enabled = enable; is_enabled = enable;
} }
@ -1984,76 +1768,28 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable)
return CR_OK; return CR_OK;
} }
static command_result dwarfmonitor_cmd(color_ostream &out, vector <string> & parameters) static command_result dwarfmonitor_cmd(color_ostream &, vector <string> & parameters) {
{
bool show_help = false;
if (parameters.empty()) if (parameters.empty())
{ return CR_WRONG_USAGE;
show_help = true;
}
else
{
auto cmd = parameters[0][0];
string mode;
if (parameters.size() > 1)
mode = toLower(parameters[1]);
if (cmd == 'v' || cmd == 'V')
{
out << "DwarfMonitor" << endl << "Version: " << PLUGIN_VERSION << endl;
}
else if ((cmd == 'e' || cmd == 'E') && !mode.empty())
{
if (!is_enabled)
plugin_enable(out, true);
if (set_monitoring_mode(mode, true)) auto cmd = parameters[0][0];
{ if (cmd == 's' || cmd == 'S') {
out << "Monitoring enabled: " << mode << endl;
}
else
{
show_help = true;
}
}
else if ((cmd == 'd' || cmd == 'D') && !mode.empty())
{
if (set_monitoring_mode(mode, false))
out << "Monitoring disabled: " << mode << endl;
else
show_help = true;
}
else if (cmd == 's' || cmd == 'S')
{
CoreSuspender guard; CoreSuspender guard;
if(Maps::IsValid()) if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self); Screen::show(dts::make_unique<ViewscreenFortStats>(), plugin_self);
} }
else if (cmd == 'p' || cmd == 'P') else if (cmd == 'p' || cmd == 'P') {
{
CoreSuspender guard; CoreSuspender guard;
if(Maps::IsValid()) if(Maps::IsValid())
Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self); Screen::show(dts::make_unique<ViewscreenPreferences>(), plugin_self);
} }
else if (cmd == 'r' || cmd == 'R')
{
CoreSuspender guard;
load_config();
}
else else
{
show_help = true;
}
}
if (show_help)
return CR_WRONG_USAGE; return CR_WRONG_USAGE;
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) DFhackCExport command_result plugin_init(color_ostream &, std::vector <PluginCommand> &commands)
{ {
activity_labels[JOB_IDLE] = "Idle"; activity_labels[JOB_IDLE] = "Idle";
activity_labels[JOB_MILITARY] = "Military Duty"; activity_labels[JOB_MILITARY] = "Military Duty";
@ -2079,27 +1815,13 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
"Measure fort happiness and efficiency.", "Measure fort happiness and efficiency.",
dwarfmonitor_cmd)); dwarfmonitor_cmd));
dm_lua::state=Lua::Core::State;
if (dm_lua::state == NULL)
return CR_FAILURE;
return CR_OK; return CR_OK;
} }
DFhackCExport command_result plugin_shutdown(color_ostream &out) DFhackCExport command_result plugin_onstatechange(color_ostream &, state_change_event event)
{ {
dm_lua::cleanup(); if (event == SC_MAP_LOADED)
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event)
{
switch (event) {
case SC_MAP_LOADED:
reset(); reset();
break;
default:
break;
}
return CR_OK; return CR_OK;
} }

@ -1,175 +1,160 @@
local _ENV = mkmodule('plugins.dwarfmonitor') local _ENV = mkmodule('plugins.dwarfmonitor')
local gps = df.global.gps local json = require('json')
local gui = require 'gui' local guidm = require('gui.dwarfmode')
local overlay = require('plugins.overlay')
config = {} local DWARFMONITOR_CONFIG_FILE = 'dfhack-config/dwarfmonitor.json'
widgets = {}
function dmerror(desc) -- --------------
qerror('dwarfmonitor: ' .. tostring(desc)) -- Widget_weather
end -- --------------
Widget_weather = defclass(Widget_weather, overlay.OverlayWidget)
Widget = defclass(Widget) function Widget_weather:init()
function Widget:init(opts) self.rain = false
self.opts = opts self.snow = false
end end
function Widget:get_pos()
local x = self.opts.x >= 0 and self.opts.x or gps.dimx + self.opts.x function Widget_weather:overlay_onupdate()
local y = self.opts.y >= 0 and self.opts.y or gps.dimy + self.opts.y local rain, snow = false, false
if self.opts.anchor == 'right' then local cw = df.global.current_weather
x = x - (self:get_width() or 0) + 1 for i=0,4 do
for j=0,4 do
weather = cw[i][j]
if weather == df.weather_type.Rain then self.rain = true end
if weather == df.weather_type.Snow then self.snow = true end
end end
return x, y
end
function Widget:render()
if monitor_state(self.opts.type) == false then
return
end end
self:update() self.frame.w = (rain and 4 or 0) + (snow and 4 or 0) +
local x, y = self:get_pos() ((snow and rain) and 1 or 0)
local p = gui.Painter.new_xy(x, y, gps.dimx - 1, y) self.rain, self.snow = rain, snow
self:render_body(p)
end end
function Widget:update() end
function Widget:get_width() end
function Widget:render_body() end
Widget_weather = defclass(Widget_weather, Widget)
function Widget_weather:update() function Widget_weather:onRenderBody(dc)
self.counts = get_weather_counts() if self.rain then dc:string('Rain', COLOR_LIGHTBLUE):advance(1) end
if self.snow then dc:string('Snow', COLOR_WHITE) end
end end
function Widget_weather:get_width() -- -----------
if self.counts.rain > 0 then -- Widget_date
if self.counts.snow > 0 then -- -----------
return 9
end
return 4
elseif self.counts.snow > 0 then
return 4
end
return 0
end
function Widget_weather:render_body(p) local function get_date_format()
if self.counts.rain > 0 then local ok, config = pcall(json.decode_file, DWARFMONITOR_CONFIG_FILE)
p:string('Rain', COLOR_LIGHTBLUE):advance(1) if not ok or not config.date_format then
end return 'Y-M-D'
if self.counts.snow > 0 then
p:string('Snow', COLOR_WHITE)
end end
return config.date_format
end end
Widget_date = defclass(Widget_date, Widget) Widget_date = defclass(Widget_date, overlay.OverlayWidget)
Widget_date.ATTRS = {
output = ''
}
function Widget_date:update() function Widget_date:init()
if not self.opts.format then self.datestr = ''
self.opts.format = 'Y-M-D' self.fmt = get_date_format()
end end
function Widget_date:overlay_onupdate()
local year = dfhack.world.ReadCurrentYear() local year = dfhack.world.ReadCurrentYear()
local month = dfhack.world.ReadCurrentMonth() + 1 local month = dfhack.world.ReadCurrentMonth() + 1
local day = dfhack.world.ReadCurrentDay() local day = dfhack.world.ReadCurrentDay()
self.output = 'Date:'
for i = 1, #self.opts.format do local fmt = self.fmt
local c = self.opts.format:sub(i, i) local datestr = 'Date:'
for i=1,#fmt do
local c = fmt:sub(i, i)
if c == 'y' or c == 'Y' then if c == 'y' or c == 'Y' then
self.output = self.output .. year datestr = datestr .. year
elseif c == 'm' or c == 'M' then elseif c == 'm' or c == 'M' then
if c == 'M' and month < 10 then if c == 'M' and month < 10 then
self.output = self.output .. '0' datestr = datestr .. '0'
end end
self.output = self.output .. month datestr = datestr .. month
elseif c == 'd' or c == 'D' then elseif c == 'd' or c == 'D' then
if c == 'D' and day < 10 then if c == 'D' and day < 10 then
self.output = self.output .. '0' datestr = datestr .. '0'
end end
self.output = self.output .. day datestr = datestr .. day
else else
self.output = self.output .. c datestr = datestr .. c
end end
end end
end
function Widget_date:get_width() self.frame.w = #datestr
return #self.output self.datestr = datestr
end end
function Widget_date:render_body(p) function Widget_date:onRenderBody(dc)
p:string(self.output, COLOR_GREY) dc:string(self.datestr, COLOR_GREY)
end end
Widget_misery = defclass(Widget_misery, Widget) -- -------------
-- Widget_misery
-- -------------
function Widget_misery:update() Widget_misery = defclass(Widget_misery, overlay.OverlayWidget)
self.data = get_misery_data()
end
function Widget_misery:get_width() function Widget_misery:init()
local w = 2 + 6 self.colors = getStressCategoryColors()
for k, v in pairs(self.data) do self.stress_category_counts = {}
w = w + #tostring(v.value)
end
return w
end end
function Widget_misery:render_body(p) function Widget_misery:overlay_onupdate()
p:string("H:", COLOR_WHITE) local counts, num_colors = {}, #self.colors
for i = 0, 6 do for _,unit in ipairs(df.global.world.units.active) do
local v = self.data[i] local stress_category = math.min(num_colors,
p:string(tostring(v.value), v.color) dfhack.units.getStressCategory(unit))
if not v.last then counts[stress_category] = (counts[stress_category] or 0) + 1
p:string("/", COLOR_WHITE)
end end
local width = 2 + num_colors - 1 -- 'H:' plus the slashes
for i=1,num_colors do
width = width + #tostring(counts[i] or 0)
end end
end
Widget_cursor = defclass(Widget_cursor, Widget) self.stress_category_counts = counts
self.frame.w = width
end
function Widget_cursor:update() function Widget_misery:onRenderBody(dc)
if gps.mouse_x == -1 and not self.opts.show_invalid then dc:string('H:', COLOR_WHITE)
self.output = '' local counts = self.stress_category_counts
return for i,color in ipairs(self.colors) do
dc:string(tostring(counts[i] or 0), color)
if i < #self.colors then dc:string('/', COLOR_WHITE) end
end end
self.output = (self.opts.format or '(x,y)'):gsub('[xX]', gps.mouse_x):gsub('[yY]', gps.mouse_y)
end end
function Widget_cursor:get_width() -- -------------
return #self.output -- Widget_cursor
end -- -------------
function Widget_cursor:render_body(p) Widget_cursor = defclass(Widget_cursor, overlay.OverlayWidget)
p:string(self.output)
end
function render_all() function Widget_cursor:onRenderBody(dc)
for _, w in pairs(widgets) do local screenx, screeny = dfhack.screen.getMousePos()
w:render() local mouse_map = dfhack.gui.getMousePos()
end local keyboard_map = guidm.getCursorPos()
end
function load_config() local text = {}
config = require('json').decode_file('dfhack-config/dwarfmonitor.json') table.insert(text, ('mouse UI grid (%d,%d)'):format(screenx, screeny))
if not config.widgets then if mouse_map then
dmerror('No widgets enabled') table.insert(text, ('mouse map coord (%d,%d,%d)')
end :format(mouse_map.x, mouse_map.y, mouse_map.z))
if type(config.widgets) ~= 'table' then
dmerror('"widgets" is not a table')
end end
widgets = {} if keyboard_map then
for _, opts in pairs(config.widgets) do table.insert(text, ('kbd cursor coord (%d,%d,%d)')
if type(opts) ~= 'table' then dmerror('"widgets" is not an array') end :format(keyboard_map.x, keyboard_map.y, keyboard_map.z))
if not opts.type then dmerror('Widget missing type field') end
local cls = _ENV['Widget_' .. opts.type]
if not cls then
dmerror('Invalid widget type: ' .. opts.type)
end end
table.insert(widgets, cls(opts)) local width = 0
for i,line in ipairs(text) do
dc:seek(0, i-1):string(line)
width = math.max(width, #line)
end end
self.frame.w = width
self.frame.h = #text
end end
return _ENV return _ENV