From ce6cb893bfdbdc72f49fd11c03abd97a2376165f Mon Sep 17 00:00:00 2001 From: Signal Date: Tue, 10 Feb 2026 21:46:25 -0500 Subject: [PATCH] Initial commit. --- imfs.lua | 1502 +++++++++++++++++++++++ init.lua | 282 +++++ package.lua | 152 +++ syntax.lua | 122 ++ textures/button_bg_shape.png | Bin 0 -> 224 bytes textures/button_border_shape.png | Bin 0 -> 249 bytes textures/cancel.png | Bin 0 -> 178 bytes textures/checkbox_bg_shape.png | Bin 0 -> 118 bytes textures/checkbox_border_shape.png | Bin 0 -> 128 bytes textures/checkbox_check_shape.png | Bin 0 -> 129 bytes textures/circle.png | Bin 0 -> 171 bytes textures/circle_active.png | Bin 0 -> 171 bytes textures/container_bg_shape.png | Bin 0 -> 242 bytes textures/container_border_shape.png | Bin 0 -> 261 bytes textures/field_bg_shape.png | Bin 0 -> 224 bytes textures/field_border_shape.png | Bin 0 -> 249 bytes textures/games.png | Bin 0 -> 168 bytes textures/loader.png | Bin 0 -> 371 bytes textures/menu_content.png | Bin 0 -> 285 bytes textures/menu_content_hovered.png | Bin 0 -> 377 bytes textures/menu_servers.png | Bin 0 -> 313 bytes textures/menu_servers_creative.png | Bin 0 -> 157 bytes textures/menu_servers_favorite.png | Bin 0 -> 216 bytes textures/menu_servers_hovered.png | Bin 0 -> 417 bytes textures/menu_servers_icon_ping_0.png | Bin 0 -> 117 bytes textures/menu_servers_icon_ping_1.png | Bin 0 -> 136 bytes textures/menu_servers_icon_ping_2.png | Bin 0 -> 156 bytes textures/menu_servers_icon_ping_3.png | Bin 0 -> 158 bytes textures/menu_servers_icon_ping_4.png | Bin 0 -> 145 bytes textures/menu_servers_mods.png | Bin 0 -> 246 bytes textures/menu_servers_peaceful.png | Bin 0 -> 188 bytes textures/menu_servers_players.png | Bin 0 -> 191 bytes textures/menu_servers_unfavorite.png | Bin 0 -> 265 bytes textures/refresh.png | Bin 0 -> 183 bytes textures/scrollbar_bg_shape.png | Bin 0 -> 224 bytes textures/scrollbar_border_shape.png | Bin 0 -> 249 bytes textures/scrollbar_thumb_shape.png | Bin 0 -> 191 bytes textures/search.png | Bin 0 -> 170 bytes textures/tab_bg_shape.png | Bin 0 -> 228 bytes textures/tab_border_shape.png | Bin 0 -> 245 bytes textures_rgt/button_bg_shape.png | Bin 0 -> 224 bytes textures_rgt/button_border_shape.png | Bin 0 -> 320 bytes textures_rgt/container_bg_shape.png | Bin 0 -> 214 bytes textures_rgt/container_border_shape.png | Bin 0 -> 477 bytes textures_rgt/field_bg_shape.png | Bin 0 -> 224 bytes textures_rgt/field_border_shape.png | Bin 0 -> 317 bytes textures_rgt/scrollbar_thumb.png | Bin 0 -> 191 bytes textures_rgt/scrollbar_track.png | Bin 0 -> 285 bytes theme.lua | 284 +++++ views/about.lua | 87 ++ views/content.lua | 236 ++++ views/docs.lua | 8 + views/game.lua | 0 views/meta_menu.lua | 129 ++ views/online_content.lua | 231 ++++ views/servers.lua | 562 +++++++++ views/settings.lua | 271 ++++ views/texturepacks.lua | 0 58 files changed, 3866 insertions(+) create mode 100644 imfs.lua create mode 100644 init.lua create mode 100644 package.lua create mode 100644 syntax.lua create mode 100644 textures/button_bg_shape.png create mode 100644 textures/button_border_shape.png create mode 100644 textures/cancel.png create mode 100644 textures/checkbox_bg_shape.png create mode 100644 textures/checkbox_border_shape.png create mode 100644 textures/checkbox_check_shape.png create mode 100644 textures/circle.png create mode 100644 textures/circle_active.png create mode 100644 textures/container_bg_shape.png create mode 100644 textures/container_border_shape.png create mode 100644 textures/field_bg_shape.png create mode 100644 textures/field_border_shape.png create mode 100644 textures/games.png create mode 100644 textures/loader.png create mode 100644 textures/menu_content.png create mode 100644 textures/menu_content_hovered.png create mode 100644 textures/menu_servers.png create mode 100644 textures/menu_servers_creative.png create mode 100644 textures/menu_servers_favorite.png create mode 100644 textures/menu_servers_hovered.png create mode 100644 textures/menu_servers_icon_ping_0.png create mode 100644 textures/menu_servers_icon_ping_1.png create mode 100644 textures/menu_servers_icon_ping_2.png create mode 100644 textures/menu_servers_icon_ping_3.png create mode 100644 textures/menu_servers_icon_ping_4.png create mode 100644 textures/menu_servers_mods.png create mode 100644 textures/menu_servers_peaceful.png create mode 100644 textures/menu_servers_players.png create mode 100644 textures/menu_servers_unfavorite.png create mode 100644 textures/refresh.png create mode 100644 textures/scrollbar_bg_shape.png create mode 100644 textures/scrollbar_border_shape.png create mode 100644 textures/scrollbar_thumb_shape.png create mode 100644 textures/search.png create mode 100644 textures/tab_bg_shape.png create mode 100644 textures/tab_border_shape.png create mode 100644 textures_rgt/button_bg_shape.png create mode 100644 textures_rgt/button_border_shape.png create mode 100644 textures_rgt/container_bg_shape.png create mode 100644 textures_rgt/container_border_shape.png create mode 100644 textures_rgt/field_bg_shape.png create mode 100644 textures_rgt/field_border_shape.png create mode 100644 textures_rgt/scrollbar_thumb.png create mode 100644 textures_rgt/scrollbar_track.png create mode 100644 theme.lua create mode 100644 views/about.lua create mode 100644 views/content.lua create mode 100644 views/docs.lua create mode 100644 views/game.lua create mode 100644 views/meta_menu.lua create mode 100644 views/online_content.lua create mode 100644 views/servers.lua create mode 100644 views/settings.lua create mode 100644 views/texturepacks.lua diff --git a/imfs.lua b/imfs.lua new file mode 100644 index 0000000..5585972 --- /dev/null +++ b/imfs.lua @@ -0,0 +1,1502 @@ + +local fe = minetest.formspec_escape +local hte = minetest.hypertext_escape + +local ctx + +local player_contexts = {} +local contexts = {} +local inventories = {} + +local theme = {} + +-- MARK: Helpers + +-- Detects percentage units andconverts them into an appropriate number (based on `ref`). +local function resolve_layout_units(value, ref) + if type(value) == "string" then + local num = tonumber(value) + if num then return num end + + local percent = value:match "(%d*.?%d+)%%" + if ref and percent then + local sign, offset = value:match "%%%s*([+-])%s*(%d*.?%d+)" + return tonumber(percent) /100 *ref +(tonumber(offset or 0) *(sign == "-" and -1 or 1)) + end + + print(string.match("100%", "(%d*.?%d+)%%"), percent, ref) + + error("Malformed layout units: "..value) + else + return value + end +end + +-- The same as resolve_layout_units, but allows (and notifies of) flex units. +local function resolve_flex_layout_units(value, ref) + if type(value) == "string" then + local num = tonumber(value) + if num then return num end + + local percent = value:match "(%d*.?%d+)%%" + if ref and percent then + local sign, offset = value:match "%%%s*([+-])%s*(%d*.?%d+)" + return tonumber(percent) /100 *ref +(tonumber(offset or 0) *(sign == "-" and -1 or 1)) + end + + local flex = value:match "(%d*.?%d+)x" + if flex then + return tonumber(flex), "flex" + end + + error("Malformed layout units: "..value) + else + return value + end +end + +-- This should be a method on any element classes that support inline styling. +local function add_elem_style(e, state, props) + -- Allow omitting the state while keeping the properties the last argument. + if not props then + props = state + state = "default" + end + + -- Permit targeting pseudo-elements as well. + if state:sub(1, 1) == "." then + e._styles[state] = imfs.style(e.__id..state, props, true) + else + e._styles[state] = imfs.style(e.__id..":"..state, props, true) + end + + return e +end + +-- Bulk version of `add_elem_style`. +local function add_elem_styles(e, states) + for state, props in pairs(states) do + if state:sub(1, 1) == "." then + e._styles[state] = imfs.style(e.__id..state, props, true) + else + e._styles[state] = imfs.style(e.__id..":"..state, props, true) + end + end + return e +end + +-- This should be a method on any element classes that support tooltips. +local function add_elem_tooltip(e, text, enabled, bgcolor, txtcolor) + -- Permit the user to pass a boolean to disable the tooltip (useful for modal dialogs that should occlude tooltips without more bothersome composition boilerplate than necessary). + if enabled == false then + return + elseif enabled ~= true then + txtcolor = bgcolor + bgcolor = enabled + end + -- We must overload `render()` both to ensure that tooltips don't clutter the element tree and mess with layout containers + -- and (for area tooltips) in order to ensure the tooltip's position and size match the parent's exactly in all cases. + + -- For named elements, prefer name-associated tooltips. + if e.__id then + local render = e.render + e.render = function(e, x, y, w, h) + local out = render(e, x, y, w, h) + return out..imfs.tooltip.named(e.__id, text, bgcolor, txtcolor):render() + end + -- For other elements, if they have a size, use an area tooltip instead. + elseif e.w then + local render = e.render + e.render = function(e, x, y, w, h) + local out = render(e, x, y, w, h) + return out..imfs.tooltip.init(x or e.x, y or e.y, w or e.w, h or e.h, text, bgcolor, txtcolor):render() + end + end + return e +end + +local name_scope = {"_"} +local nth_id = {0} + +local function scope(name) + table.insert(name_scope, name) + table.insert(nth_id, 0) +end + +local function scope_end() + table.remove(name_scope) + table.remove(nth_id) +end + +local function new_id() + local depth = #nth_id + nth_id[depth] = nth_id[depth] +1 + return table.concat(name_scope, ".").."_"..nth_id[depth] +end + +local function unique_id() + return "_"..minetest.get_us_time().."_"..math.random(1, 100000) +end + +local function string_or(a, b) + return (a ~= "" and a) or b +end + +-- MARK: Data structures + +local observers = {} +local state = { + observers = observers, + get = function(e) + local observer = observers[#observers] + if observer then + observer[e] = true + end + return e._val + end, + set = function(e, val) + if val == nil then error "!" end + e._old_val = e._val + e._val = val + for _, x in pairs(e._getters) do + x:update() + end + end, + __call = function(e, val) + if val == nil then -- Getter + return e:get() + else -- Setter + return e:set(val) + end + end +} +state.__index = state +setmetatable(state, { + __call = function(_, val) + local e = {_getters = {}, _val = val} + setmetatable(e, state) + + return e + end +}) + +local DerivedState = { + get = function(e) + if e._stale then + e:update() + end + return e._val + end, + update = function(e) + for dep in pairs(e._deps) do + dep._getters[e.__id] = nil + e._deps[dep] = nil + end + + local tracker = e._deps + table.insert(observers, tracker) + + local val = e._fn() + + table.remove(observers) + + e._val = val + + for dep in pairs(e._deps) do + dep._getters[e.__id] = e + end + + -- Don't propagate updates on initial computation to avoid stack overflow. + if not e._stale then + for _, x in pairs(e._getters) do + x:update() + end + end + e._stale = false + end, + __call = function(e) + return e:get() + end +} +DerivedState.__index = DerivedState + +function derive(fn) + return setmetatable({_fn = fn, _stale = true, _getters = {}, _deps = {}, __id = minetest.get_us_time()}, DerivedState) +end + +local function get(e, x) + local item = e[x] + local mt = getmetatable(item) + if mt == state or mt == DerivedState then + return fe(tostring(item() or "")) + end + return fe(tostring(item or "")) +end + +local function get_raw(e, x) + local item = e[x] + local mt = getmetatable(item) + if mt == state or mt == DerivedState then + return item() + end + return item +end + +-- MARK: Elements + +local fs_style = { + render = function(e) + local props = {} + for k, v in pairs(e.props) do + props[#props +1] = ";" + props[#props +1] = k + props[#props +1] = "=" + props[#props +1] = tostring(v) + end + return string.format("style[%s%s]", e.name, table.concat(props)) + end +} +fs_style.__index = fs_style +setmetatable(fs_style, { + __call = function(_, name, props, internal) + local e = {name = name, props = props} + setmetatable(e, fs_style) + if not internal then table.insert(ctx, e) end + return e + end +}) + +local fs_tooltip = { + _no_layout = true, + init = function(x, y, w, h, text, bgcolor, txtcolor) + local e = {x = x, y = y, w = w, h = h, text = text, bgcolor = bgcolor or imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"} + setmetatable(e, imfs.tooltip) + return e + end, + named = function(name, text, bgcolor, txtcolor) + local e = {_name = name, text = text, bgcolor = bgcolor or imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"} + setmetatable(e, imfs.tooltip) + return e + end, + render = function(e, x, y, w, h) + if e._name then + return string.format("tooltip[%s;%s;%s;%s]", e._name, get(e, "text"), get(e, "bgcolor"), get(e, "txtcolor")) + else + return string.format( + "tooltip[%f,%f;%f,%f;%s;%s;%s]", + x or get(e, "x"), y or get(e, "y"), + w or get(e, "w"), h or get(e, "h"), + get(e, "text"), + get(e, "bgcolor"), + get(e, "txtcolor") + ) + end + end +} +fs_tooltip.__index = fs_tooltip +setmetatable(fs_tooltip, { + __call = function(_, x, y, w, h, text, bgcolor, txtcolor) + local e = {x = x, y = y, w = w, h = h, text = text, bgcolor = bgcolor or imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"} + setmetatable(e, fs_tooltip) + table.insert(ctx, e) + return e + end +}) + +local fs_label = { + render = function(e, x, y) + return string.format("label[%f,%f;%s]", x or get(e, "x"), y or get(e, "y"), get(e, "txt")) + end +} +fs_label.__index = fs_label +setmetatable(fs_label, { + __call = function(_, x, y, txt) + local e = {x = x, y = y, txt = txt} + setmetatable(e, fs_label) + table.insert(ctx, e) + return e + end +}) + +local fs_arealabel = { + render = function(e, x, y, w, h) + x = x or get(e, "x") + y = y or get(e, "y") + w = w or get(e, "w") + h = h or get(e, "h") + + if e._scrollable then + return string.format("textarea[%f,%f;%f,%f;;;%s]", x, y, w, h, get(e, "txt")) + else + return string.format("label[%f,%f;%f,%f;%s]", x, y, w, h, get(e, "txt")) + end + end, + scrollable = function(e) + e._scrollable = true + return e + end, + tooltip = add_elem_tooltip, +} +fs_arealabel.__index = fs_arealabel +setmetatable(fs_arealabel, { + __call = function(_, x, y, w, h, txt) + local e = {x = x, y = y, w = w, h = h, txt = txt} + setmetatable(e, fs_arealabel) + table.insert(ctx, e) + return e + end +}) + +local fs_hypertext = { + render = function(e, x, y, w, h) + local out = {} + for _, x in pairs(e._styles) do + out[#out +1] = x:render() + end + + out[#out +1] = string.format("hypertext[%f,%f;%f,%f;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), e.__id, get(e, "txt")) + return table.concat(out) + end, + onaction = function(e, fn) + ctx._events.on_click[e.__id] = fn + return e + end, + named = function(e, name) + e.__id = name + return e + end, + styles = add_elem_styles, + style = add_elem_style, + tooltip = add_elem_tooltip, +} +fs_hypertext.__index = fs_hypertext +setmetatable(fs_hypertext, { + __call = function(_, x, y, w, h, txt) + local e = {x = x, y = y, w = w, h = h, txt = txt, _styles = {}} + e.__id = "_"..minetest.get_us_time().."_"..math.random(1, 100000) + setmetatable(e, fs_hypertext) + table.insert(ctx, e) + return e + end +}) + +local fs_box = { + render = function(e, x, y, w, h) + return string.format("box[%f,%f;%f,%f;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "bg")) + end, + tooltip = add_elem_tooltip, +} +fs_box.__index = fs_box +setmetatable(fs_box, { + __call = function(_, x, y, w, h, bg) + local e = {x = x, y = y, w = w, h = h, bg = bg} + setmetatable(e, fs_box) + table.insert(ctx, e) + return e + end +}) + +local fs_image = { + render = function(e, x, y, w, h) + if e._anim_frames then + return string.format( + "animated_image[%f,%f;%f,%f;%s;%s;%f;%f;%f;%s]", + x or get(e, "x"), y or get(e, "y"), + w or get(e, "w"), h or get(e, "h"), + e.__id, + get(e, "texture"), + get(e, "_anim_frames"), + get(e, "_anim_duration"), + get(e, "_anim_start"), + get(e, "middle") + ) + else + return string.format("image[%f,%f;%f,%f;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "texture"), get(e, "middle")) + end + end, + animated = function(e, frames, duration, start) + -- We only need an ID if the image is animated and must persist its state. + e.__id = e.__id or new_id() + e._anim_frames = frames + e._anim_duration = duration or 50 + e._anim_start = start or 1 + return e + end, + tooltip = add_elem_tooltip, +} +fs_image.__index = fs_image +setmetatable(fs_image, { + __call = function(_, x, y, w, h, texture, middle) + local e = {x = x, y = y, w = w, h = h, texture = texture, middle = middle or ""} + setmetatable(e, fs_image) + table.insert(ctx, e) + return e + end +}) + +local fs_item_image = { + render = function(e, x, y, w, h) + return string.format("item_image[%f,%f;%f,%f;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "item")) + end, + tooltip = add_elem_tooltip, +} +fs_item_image.__index = fs_item_image +setmetatable(fs_item_image, { + __call = function(_, x, y, w, h, item) + local e = {x = x, y = y, w = w, h = h, item = item} + setmetatable(e, fs_item_image) + table.insert(ctx, e) + return e + end +}) + +local fs_model = { + render = function(e, x, y, w, h) + local out = {} + for _, x in pairs(e._styles) do + out[#out +1] = x:render() + end + + local textures = get_raw(e, "textures") + if type(textures) ~= "string" then + textures = table.concat(textures, ",") + end + + local rx = get_raw(e, "_rotation_x") + local ry = get_raw(e, "_rotation_y") + local rotation = "" + if rx and ry then + rotation = string.format("%s,%s", fe(rx), fe(ry)) + end + + local as = get_raw(e, "_animation_start") + local ae = get_raw(e, "_animation_end") + local animation = "" + if as and ae then + animation = string.format("%s,%s", fe(as), fe(ae)) + end + + out[#out +1] = string.format( + "model[%f,%f;%f,%f;%s;%s;%s;%s;%s;%s;%s;%s]", + x or get(e, "x"), y or get(e, "y"), + w or get(e, "w"), h or get(e, "h"), + e.__id, + get(e, "mesh"), textures, + rotation, get(e, "_continuous"), + get(e, "_mouse_control"), + animation, get(e, "_animation_speed") + ) + return table.concat(out) + end, + rotation = function(e, x, y, continuous) + e._rotation_x = x + e._rotation_y = y or 0 + e._continuous = continuous + return e + end, + mouse_control = function(e, mouse_control) + e._mouse_control = mouse_control ~= false and true or false + return e + end, + animated = function(e, start, end_, speed) + e._animation_start = start + e._animation_end = end_ + e._animation_speed = speed or 1 + return e + end, + style = add_elem_style, + styles = add_elem_styles, + tooltip = add_elem_tooltip, +} +fs_model.__index = fs_model +setmetatable(fs_model, { + __call = function(_, x, y, w, h, mesh, textures) + local e = {x = x, y = y, w = w, h = h, mesh = mesh, textures = textures, _mouse_control = true, _styles = {}} + e.__id = new_id() + setmetatable(e, fs_model) + table.insert(ctx, e) + return e + end +}) + +local fs_button = { + render = function(e, x, y, w, h) + local out = {} + for _, x in pairs(e._styles) do + out[#out +1] = x:render() + end + if e._item then + out[#out +1] = string.format("item_image_button[%f,%f;%f,%f;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "_item"), e.__id, get(e, "label")) + elseif e._image then + if e._image_pressed then + -- We never specify noclip or border here. That's a job for styles. + out[#out +1] = string.format("image_button[%f,%f;%f,%f;%s;%s;%s;;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "_image"), e.__id, get(e, "label"), get(e, "_image_pressed")) + else + out[#out +1] = string.format("image_button%s[%f,%f;%f,%f;%s;%s;%s]", e._exit and "_exit" or "", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "_image"), e.__id, get(e, "label")) + end + else + out[#out +1] = string.format("button%s[%f,%f;%f,%f;%s;%s]", e._exit and "_exit" or "", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), e.__id, get(e, "label")) + end + return table.concat(out) + end, + image = function(e, img, pressed_img) + e._image = img + e._image_pressed = pressed_img + return e + end, + item_image = function(e, item) + e._item = item + return e + end, + exit = function(e) + e._exit = true + return e + end, + onclick = function(e, fn) + ctx._events.on_click[e.__id] = fn + return e + end, + style = add_elem_style, + styles = add_elem_styles, + tooltip = add_elem_tooltip, +} +fs_button.__index = fs_button +setmetatable(fs_button, { + __call = function(_, x, y, w, h, label) + local e = {x = x, y = y, w = w, h = h, label = label, _styles = {}} + e.__id = new_id() + setmetatable(e, fs_button) + table.insert(ctx, e) + return e + end +}) + +local fs_checkbox = { + render = function(e, x, y) + return string.format("checkbox[%f,%f;%s;%s;%s]", x or get(e, "x"), y or get(e, "y"), e.__id, get(e, "label"), get(e, "checked") and "true" or "false") + end, + onchange = function(fn) + ctx._events.on_change[e.__id] = fn + return e + end, + tooltip = add_elem_tooltip, +} +fs_checkbox.__index = fs_checkbox +setmetatable(fs_checkbox, { + __call = function(_, x, y, label, checked) + local e = {x = x, y = y, label = label or "", checked = checked or label or false} + e.__id = new_id() + setmetatable(e, fs_checkbox) + table.insert(ctx, e) + return e + end +}) + +local fs_list = { + render = function(e, x, y, w, h) + w = w or get(e, "w") + h = h or get(e, "h") + + return string.format("list[%s;%s;%f,%f;%d,%d;%s]", get(e, "location"), get(e, "list"), x or get(e, "x"), y or get(e, "y"), w, h, get(e, "start")) + end +} +fs_list.__index = fs_list +setmetatable(fs_list, { + __call = function(_, x, y, w, h, location, list, start) + local e = {x = x, y = y, w = type(w == "string") and w or w +((w -1) /4), h = type(h) == "string" and h or h +((h -1) /4), location = location or "current_player", list = list or "main", start = start or ""} + e.__id = new_id() + setmetatable(e, fs_list) + table.insert(ctx, e) + return e + end +}) + +local fs_inventory = fs_list + +local fs_listring = { + render = function(e) + if e.location then + return string.format("listring[%s;%s]", e.location, e.list) + else + return "listring[]" + end + end, +} +fs_listring.__index = fs_listring +setmetatable(fs_listring, { + __call = function(_, location, list) + local e = location and list and {location = location, list = list} or {} + setmetatable(e, fs_listring) + table.insert(ctx, e) + return e + end +}) + +local fs_field = { + render = function(e, x, y, w, h) + local out = {} + for _, x in pairs(e._styles) do + out[#out +1] = x:render() + end + + x = x or get(e, "x") + y = y or get(e, "y") + w = w or get(e, "w") + h = h or get(e, "h") + + if e._password then + out[#out +1] = string.format("pwdfield[%f,%f;%f,%f;%s;%s]", x, y, w, h, e.__id, get(e, "label")) + + if not e._close_on_enter then + out[#out +1] = string.format("field_close_on_enter[%s;false]", e.__id) + end + else + out[#out +1] = string.format("%s[%f,%f;%f,%f;%s;%s;%s]", e._textarea and "textarea" or "field", x, y, w, h, e.__id, get(e, "label"), get(e, "value")) + + if not e._textarea and not e._close_on_enter then + out[#out +1] = string.format("field_close_on_enter[%s;false]", e.__id) + end + end + + return table.concat(out) + end, + onenter = function(e, fn) + ctx._events.on_enter[e.__id] = fn + return e + end, + onchange = function(e, fn) + ctx._events.on_change[e.__id] = fn + return e + end, + close_on_enter = function(e) + e._close_on_enter = true + return e + end, + multiline = function(e) + e._textarea = true + return e + end, + password = function(e) + e._password = true + return e + end, + style = add_elem_style, + styles = add_elem_styles, + tooltip = add_elem_tooltip, +} +fs_field.__index = fs_field +setmetatable(fs_field, { + __call = function(_, x, y, w, h, label, value) + local e = {x = x, y = y, w = w, h = h, label = value and label or "", value = value or label or "", _styles = {}} + e.__id = new_id() + setmetatable(e, fs_field) + table.insert(ctx, e) + return e + end +}) + +local function fs_textarea(...) + return fs_field(...) + :multiline() +end + +local fs_scrollbar = { + render = function(e, x, y, w, h) + local out = {} + for _, x in pairs(e._styles) do + out[#out +1] = x:render() + end + + if e._options then + out[#out +1] = "scrollbaroptions[" + local first = true + for k, v in pairs(e._options) do + if not first then + out[#out +1] = ";" + end + out[#out +1] = k + out[#out +1] = "=" + out[#out +1] = v + first = nil + end + out[#out +1] = "]" + end + + out[#out +1] = string.format("scrollbar[%f,%f;%f,%f;%s;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "orientation"), e.__id, get(e, "value")) + + -- If we were given custom options, reset them all so they don't interfere with future scrollbars (particularly those automatically configured by scroll containers). + if e._options then + out[#out +1] = "scrollbaroptions[min=-1;max=-1;smallstep=-1;largestep=-1;thumbsize=-1;arrows=default]" + end + + return table.concat(out) + end, + options = function(e, props) + e._options = props + return e + end, + onchange = function(e, fn) + ctx._events.on_scrollbar_event[e.__id] = fn + return e + end, + style = add_elem_style, + styles = add_elem_styles, + tooltip = add_elem_tooltip, +} +fs_scrollbar.__index = fs_scrollbar +setmetatable(fs_scrollbar, { + __call = function(_, x, y, w, h, orientation, value) + local e = {x = x, y = y, w = w, h = h, orientation = orientation or "vertical", value = value or "", _styles = {}} + e.__id = "_"..minetest.get_us_time().."_"..math.random(1, 100000) + setmetatable(e, fs_scrollbar) + ctx[#ctx +1] = e + return e + end +}) + +local fs_scroll_container = { + render = function(e, x, y, w, h, orient, fac, pad) + x = x or get(e, "x") + y = y or get(e, "y") + w = w or get(e, "w") + h = h or get(e, "h") + + local out = { + string.format("scroll_container[%f,%f;%f,%f;%s;%s;%s;%s]", + x, y, + w, h, + e.__id, + orient or get(e, "orientation"), + fac or get(e, "factor"), + pad or get(e, "padding") + ) + } + for i = 1, #e do + local c = e[i] + local cx, cy + if c.x then + cx = resolve_layout_units(get_raw(c, "x"), w) + cy = resolve_layout_units(get_raw(c, "y"), h) + end + + local cw, ch + if c.w then + cw = resolve_layout_units(get(c, "w"), w) + ch = resolve_layout_units(get(c, "h"), h) + end + + out[#out +1] = c:render(cx, cy, cw, ch) + end + out[#out +1] = "scroll_container_end[]" + + if not e._scrollbar then + local v = e.__fs._ctx.fields[e.__id] + out[#out +1] = string.format("scrollbar[-800,-800;0,0;%s;%s;%s]", get(e, "orientation"), e.__id, v and v:sub(5) or "") + end + + return table.concat(out) + end, + scrollbar = function(e, fn, ...) + -- Make a fake ctx to put the scrollbar in. + ctx = setmetatable({ + __parent = ctx, + _events = setmetatable({}, { + __index = ctx._events, + __newindex = ctx._events + }), + }, { + -- Assignment trap to ensure that the scrollbar gets our ID immediately on construction. + __newindex = function(tbl, key, value) + value.__id = e.__id + return rawset(tbl, key, value) + end + }) + if type(fn) == "function" then + fn() + else + fs_scrollbar(fn, ...) + end + e._scrollbar = ctx[1] + ctx = ctx.__parent + e._scrollbar.__id = e.__id + return e + end, + onscroll = function(e, fn) + if e._scrollbar then + e._scrollbar:onchange(fn) + return e + end + e._scrollbar = fs_scrollbar(-800, -800, 0, 0, e._orientation, e.__id):onchange(fn) + return e + end, + -- A non-transient name is required in order to preserve scroll position upon a rebuild. + named = function(e, name) + e.__id = name + return e + end +} +fs_scroll_container.__index = fs_scroll_container +setmetatable(fs_scroll_container, { + __call = function(_, x, y, w, h, orientation, factor, padding) + local e = { + x = x, + y = y, + w = w, + h = h, + orientation = orientation or "vertical", + factor = factor or "", + padding = padding or "0", + _styles = {}, + __parent = ctx, + _events = setmetatable({}, { + __index = ctx._events, + __newindex = ctx._events + }), + __fs = ctx.__fs or ctx + } + e.__id = new_id() + setmetatable(e, fs_scroll_container) + table.insert(ctx, e) + ctx = e + return e + end +}) + +local function fs_scroll_container_end() + if getmetatable(ctx) ~= fs_scroll_container then + minetest.log("warn", "`scroll_container_end` has no scroll container to end; it will be ignored.") + return + end + -- If the container has a custom scrollbar, we add it _after_ the container's contents. + if ctx._scrollbar then + table.insert(ctx.__parent, ctx._scrollbar) + end + ctx = ctx.__parent +end + + +-- MARK: Builtin layouting helpers + +local fs_group = { + render = function(e, x, y, w, h) + x = x or get(e, "x") + y = y or get(e, "y") + w = w or get(e, "w") + h = h or get(e, "h") + + local out = {} + for i = 1, #e do + local c = e[i] + local cx, cy + if c.x then + cx = resolve_layout_units(get_raw(c, "x"), w) +x + cy = resolve_layout_units(get_raw(c, "y"), h) +y + end + + local cw, ch + if c.w then + cw = resolve_layout_units(get(c, "w"), w) + ch = resolve_layout_units(get(c, "h"), h) + end + + out[#out +1] = c:render(cx, cy, cw, ch) + end + + return table.concat(out) + end, +} +fs_group.__index = fs_group +setmetatable(fs_group, { + __call = function(_, x, y, w, h) + local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__fs or ctx} + setmetatable(e, fs_group) + table.insert(ctx, e) + ctx = e + return e + end +}) + +local function fs_group_end() + if getmetatable(ctx) ~= fs_group then + minetest.log("warn", "`group_end` has no group to end; it will be ignored.") + return + end + ctx = ctx.__parent +end + + +local fs_row = { + render = function(e, x, y, w, h) + x = x or get(e, "x") + y = y or get(e, "y") + w = w or get(e, "w") + h = h or get(e, "h") + + local axis = e._direction == "column" and y or x + local axis_size = e._direction == "column" and h or w + + local out = {} + + local total_grow = 0 + local used_space = 0 + local flex_found + + -- Pass 1: Collect total sizing information to allow layout computation. + for i = 1, #e do + local c = e[i] + if c.w and not c._no_layout then + local ca, ca_flex = resolve_flex_layout_units(get(c, e._direction == "column" and "h" or "w"), axis_size) + if ca_flex then + flex_found = true + c.__flex = ca + total_grow = total_grow +ca + else + used_space = used_space +(ca or 0) + end + end + end + + used_space = used_space +(e._gap *math.max(0, #e -1)) + local grow_basis = total_grow > 0 and (math.max(0, axis_size -used_space) /total_grow) or 0 + + -- If any flex element exists, it will take up all unused space, so the total width is guaranteed to be 100%. + -- Otherwise, we can simply use the size we already calculated for fixed elements. + local total_width = flex_found and axis_size or used_space + + -- Pass 2: Assign element positions based on flex ratios. + local current = 0 + for i = 1, #e do + local c = e[i] + if not c._no_layout then + c.__flex_offset = current + if c.__flex then + c.__flex = c.__flex *grow_basis + current = current +c.__flex +e._gap + elseif c.w then + current = current +resolve_layout_units(get(c, e._direction == "column" and "h" or "w"), axis_size) +e._gap + else + current = current +e._gap + end + end + end + + -- Pass 3: Justify and render. + local base = axis + if e._align == "center" then + base = axis +((axis_size -total_width) /2) + elseif e._align == "right" then + base = axis +axis_size -total_width + end + + for i = 1, #e do + local c = e[i] + if c._no_layout then + local cx, cy + if c.x then + cx = resolve_layout_units(get_raw(c, "x"), w) +x + cy = resolve_layout_units(get_raw(c, "y"), h) +y + end + + local cw, ch + if c.w then + cw = resolve_layout_units(get(c, "w"), w) + ch = resolve_layout_units(get(c, "h"), h) + end + + out[#out +1] = c:render(cx, cy, cw, ch) + elseif c.w then + if e._direction == "column" then + out[#out +1] = c:render(resolve_layout_units(get(c, "x"), w) +x, base +c.__flex_offset, resolve_layout_units(get(c, "w"), w), c.__flex) + else + out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y"), h) +y, c.__flex, resolve_layout_units(get(c, "h"), h)) + end + elseif c.x then + if e._direction == "column" then + out[#out +1] = c:render(resolve_layout_units(get(c, "x"), w) +x, base +c.__flex_offset) + else + out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y"), h) +y) + end + end + end + + return table.concat(out) + end, + direction = function(e, dir) + e._direction = dir + return e + end, + gap = function(e, gap) + e._gap = gap + return e + end, + align = function(e, align) + e._align = align + return e + end, +} +fs_row.__index = fs_row +setmetatable(fs_row, { + __call = function(_, x, y, w, h) + local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__fs or ctx} + setmetatable(e, fs_row) + table.insert(ctx, e) + ctx = e + return e + end +}) + + +local function fs_row_end() + if getmetatable(ctx) ~= fs_row then + minetest.log("warn", "`row_end` has no row to end; it will be ignored.") + return + end + local axis = ctx._direction == "column" and "w" or "h" + if ctx[axis] == "fit" then + local size = 0 + for i = 1, #ctx do + local child_size = tonumber(ctx[i][axis]) or 0 + if child_size > size then + size = child_size + end + end + ctx[axis] = size + end + ctx = ctx.__parent +end + +local function fs_column(...) + return fs_row(...) + :direction "column" +end + +local fs_column_end = fs_row_end + +-- MARK: Building + +local Window = { + render = function(e) + local out = { + "formspec_version[10]", + string.format("size[%f,%f]", e.width, e.height) + } + + if e._no_prepend then + out[#out +1] = "no_prepend[]" + end + + if e._position then + out[#out +1] = string.format("position[%f,%f]", e._position.x, e._position.y) + end + + if e._anchor then + out[#out +1] = string.format("anchor[%f,%f]", e._anchor.x, e._anchor.y) + end + + if e._padding then + out[#out +1] = string.format("padding[%f,%f]", e._padding.x, e._padding.y) + end + + if e._modal then + out[#out +1] = "allow_close[false]" + end + + if e._fullscreen then + out[#out +1] = string.format("bgcolor[%s;%s;%s]", get(e, "_fgcolor"), get(e, "_fullscreen"), get(e, "_bgcolor")) + end + + for i = 1, #e do + local c = e[i] + local cx, cy + if c.x then + cx = resolve_layout_units(get(c, "x"), e.width) + cy = resolve_layout_units(get(c, "y"), e.height) + end + + local cw, ch + if c.w then + cw = resolve_layout_units(get(c, "w"), e.width) + ch = resolve_layout_units(get(c, "h"), e.height) + end + + out[#out +1] = c:render(cx, cy, cw, ch) + end + return table.concat(out) + end, + no_prepend = function(e) + e._no_prepend = true + return e + end, + position = function(e, x, y) + e._position = {x = x, y = y} + return e + end, + anchor = function(e, x, y) + e._anchor = {x = x, y = y} + return e + end, + padding = function(e, x, y) + e._padding = {x = x, y = y} + return e + end, + modal = function(e, modal) + e._modal = modal ~= false and true or false + return e + end, + onclose = function(e, fn) + e._onclose = fn + return e + end, + bgcolor = function(e, foreground, fullscreen) + if fullscreen then + if fullscreen == true then + e._fullscreen = "true" + e._bgcolor = foreground + else + e._fullscreen = "both" + e._fgcolor = foreground + e._bgcolor = fullscreen + end + elseif not foreground then + e._fullscreen = "neither" + else + e._fullscreen = "false" + e._fgcolor = foreground + end + return e + end +} +Window.__index = Window + +local function fs_begin(w, h) + ctx = {_events = {on_click = {}, on_change = {}, on_scrollbar_event = {}, on_enter = {}}, width = w or 12, height = h or 10} + setmetatable(ctx, Window) + name_scope = {"_"} + nth_id = {0} + return ctx +end + +local function fs_end() + local _ctx = ctx + ctx = nil + name_scope = nil + nth_id = nil + return _ctx +end + +local Context = { + update = function(e) + -- This is used to prevent changes in state from inside the builder to trigger another rebuild. + if e._ignore then return end + -- `_inert` should be set when many state updates may take place at once, to avoid a rapid succession of rebuilds. + -- After these updates have taken place, the user should call `rebuild()` manually if `_dirty` is true. + if e._inert then + e._dirty = true + else + e:rebuild() + end + end, + rebuild = function(e) + e._dirty = nil + e._ignore = true + e:clear_state_bindings() + + local tracker = e._linked_states + table.insert(observers, tracker) + + local fs = type(e.formspec) == "function" and e.formspec(e.state, function() e:close() end) or e.formspec + + e._window = fs + + table.remove(observers) + + e._ignore = nil + + for x in pairs(e._linked_states) do + x._getters[e.id] = e + end + + fs._ctx = e + + e:collect_state_depends() + + e._events = fs._events + + local str = fs:render() + if e._mainmenu then + minetest.update_formspec(str) + elseif e._is_inventory then + e._player:set_inventory_formspec(str) + else + minetest.show_formspec(e.target, e.id, str) + end + end, + get_states_from_elem = function(e, el) + for k, x in pairs(el) do + local mt = getmetatable(x) + if mt == state or mt == DerivedState then + x._getters[e.id] = e + e._linked_states[x] = true + elseif tonumber(k) then + e:get_states_from_elem(x) + end + end + end, + collect_state_depends = function(e) + for _, el in ipairs(e._window) do + e:get_states_from_elem(el) + end + end, + clear_state_bindings = function(e) + for x in pairs(e._linked_states) do + x._getters[e.id] = nil + end + end, + deinit = function(e) + if contexts[e.id] then + contexts[e.id] = nil + end + if e._window._onclose then + e._window:_onclose() + end + e:clear_state_bindings() + -- Kill our rebuild capability in case any callbacks fire on the way out. + e.rebuild = function() end + end, + close = function(e) + -- Inventories cannot be 'closed', only replaced. + if e._is_inventory then return end + minetest.close_formspec(e.target, e.id) + e:deinit() + end +} +Context.__index = Context + +local function fs_show(target, fs, state) + local id = "form"..unique_id() + local ctx = setmetatable({ + formspec = fs, + fields = {}, + target = type(target) == "string" and target or target:get_player_name(), + id = id, + _linked_states = {}, + state = state or {} + }, Context) + + if player_contexts[ctx.target] then + player_contexts[ctx.target]:deinit() + end + + contexts[id] = ctx + player_contexts[ctx.target] = ctx + + ctx:rebuild() + + return ctx +end + +local function fs_set_inventory(p, fs, state) + local id = "form"..unique_id() + local name = type(p) == "string" and p or p:get_player_name() + local ctx = setmetatable({ + _is_inventory = true, + _player = type(p) == "string" and minetest.get_player_by_name(p) or p, + formspec = fs, + fields = {}, + target = name, + id = id, + _linked_states = {}, + state = state or {} + }, Context) + + inventories[name] = ctx + + ctx:rebuild() + + return ctx +end + +local function fs_remove_inventory(p) + local name = p:get_player_name() + local inv = inventories[name] + if inv then + inv:deinit() + inventories[name] = nil + end +end + +-- MARK: Callback handling + +local function handler(ctx, fields) + ctx._inert = true + + -- We split event handling into two passes here in order to guarantee that action events take precedence over state change events. + -- Otherwise, it would be possible for a field's state synchronization to accidentally overwrite state changed by an action, which is almost certainly not what the user intended to happen. + + -- First pass: Stateful element updates. + for k, v in pairs(fields) do + if ctx._events.on_scrollbar_event[k] then + local ev = minetest.explode_scrollbar_event(v) + ctx._events.on_scrollbar_event[k](ev.type, ev.value) + elseif (not ctx.fields[k] or ctx.fields[k] ~= v) and ctx._events.on_change[k] then + ctx._events.on_change[k](v) + end + end + + -- Handle pressing Enter in a field. (This is somewhere between a state event and an action, so we process between passes.) + if fields.key_enter_field and ctx._events.on_enter[fields.key_enter_field] then + ctx._events.on_enter[fields.key_enter_field](fields[fields.key_enter_field]) + end + + -- Second pass: Actions. + for k, v in pairs(fields) do + if ctx._events.on_click[k] then + ctx._events.on_click[k](v) + end + end + + ctx.fields = fields + + ctx._inert = nil + + if fields.quit then + ctx:deinit() + else + if ctx._dirty then + ctx:rebuild() + ctx._dirty = nil + end + end +end + +-- Compatibility with main menu usage. +if minetest.update_formspec then + local mainmenu_context + function fs_show(fs, state) + if mainmenu_context then + mainmenu_context:deinit() + end + + local id = "form"..unique_id() + local ctx = setmetatable({ + _mainmenu = true, + formspec = fs, + fields = {}, + target = "menu", + id = id, + _linked_states = {}, + state = state or {} + }, Context) + + mainmenu_context = ctx + + ctx:rebuild() + + return ctx + end + + fs_set_inventory = nil + + minetest.button_handler = function(fields) + handler(mainmenu_context, fields) + end +-- Normal in-game usage. +else + minetest.register_on_player_receive_fields(function(p, form, fields) + local name = p:get_player_name() + local ctx = contexts[form] + -- If this is an event from a player inventory, check if we have a context managing that inventory. + -- No other special handling is needed, because the context already knows that it's an inventory and will react appropriately. + if form == "" then + ctx = inventories[name] + end + + -- Ensure that a) players can only trigger effects of formspecs that were opened legitimately, and b) only the player who opened it may interact with a given formspec instance. + if ctx and ctx.target == name then + handler(ctx, fields) + end + end) + + -- Remove all contexts tied to a leaving player. + minetest.register_on_leaveplayer(function(p) + fs_remove_inventory(p) + end) +end + + +-- MARK: API exposure + +imfs = { + _contexts = contexts, + _inventories = inventories, + + _new_id = new_id, + _unique_id = unique_id, + + state = state, + derive = derive, + get_field = get, + begin = fs_begin, + end_ = fs_end, + scope = scope, + scope_end = scope_end, + show = fs_show, + set_inventory = fs_set_inventory, + remove_inventory = fs_remove_inventory, + resolve_layout_units = resolve_layout_units, + resolve_flex_layout_units = resolve_flex_layout_units, + container_start = function() + local container = {__parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__fs or ctx} + table.insert(ctx, container) + ctx = container + end, + container_end = function() + ctx = ctx.__parent + end, + add_to_context = function(container) + table.insert(ctx, container) + end, + + theme = theme, + + style = fs_style, + tooltip = fs_tooltip, + label = fs_label, + arealabel = fs_arealabel, + hypertext = fs_hypertext, + box = fs_box, + image = fs_image, + item_image = fs_item_image, + model = fs_model, + button = fs_button, + list = fs_list, + inventory = fs_inventory, + listring = fs_listring, + field = fs_field, + textarea = fs_textarea, + checkbox = fs_checkbox, + scrollbar = fs_scrollbar, + scroll_container = fs_scroll_container, + scroll_container_end = fs_scroll_container_end, + group = fs_group, + group_end = fs_group_end, + row = fs_row, + row_end = fs_row_end, + column = fs_column, + column_end = fs_column_end, + + -- Can be called in a game's base mod to globalize imfs for brevity. + export = function() + for k, v in pairs(imfs) do + local key + if k == "state" or k == "derive" then + key = k + elseif k == "end_" then + key = "fs_end" + else + key = "fs_"..k + end + _G[key] = v + end + end +} diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..f1ccfed --- /dev/null +++ b/init.lua @@ -0,0 +1,282 @@ + +menupath = core.settings:get("main_menu_script"):match("(.*/)[^/]+.lua$") or + core.get_builtin_path().."/" + +default_textures = minetest.get_texturepath_share().."/base/pack/" + +version = minetest.get_version() + +minetest.async_jobs = {} + +local function handle_job(jobid, serialized_retval) + local retval = minetest.deserialize(serialized_retval) + assert(type(minetest.async_jobs[jobid]) == "function") + minetest.async_jobs[jobid](retval) + minetest.async_jobs[jobid] = nil +end + +minetest.async_event_handler = handle_job + +function minetest.handle_async(func, parameter, callback) + -- Serialize parameters + local serialized_param = minetest.serialize(parameter) + + if serialized_param == nil then + return false + end + + local jobid = minetest.do_async_callback(func, serialized_param) + + minetest.async_jobs[jobid] = callback + + return true +end + +dofile(menupath.."imfs.lua") +dofile(menupath.."theme.lua") +dofile(menupath.."package.lua") +dofile(menupath.."syntax.lua") + + +window = minetest.get_window_info() +size = window.max_formspec_size + +function pixel_to_formspec(pixel) + return tonumber(pixel) / 80 * window.real_gui_scaling +end + +local style_no_background = { + bgimg = default_textures.."blank.png", + bgimg_middle = 0, + content_offset = "0,0", + textcolor = theme.styles.text_color, +} +styles_no_background = { + default = style_no_background, + hovered = style_no_background, + pressed = style_no_background, +} + +style_borderless = { + border = false, + bgimg = "[fill:1x1:0,0:#ffff", + bgimg_middle = 0, + bgcolor = theme.styles.container.bg_color, +} +style_borderless_alt = { + border = false, + bgimg = "[fill:1x1:0,0:#ffff", + bgimg_middle = 0, + bgcolor = theme.styles.container.bg_color_alt, +} +style_borderless_hovered = { + border = false, + bgimg = "[fill:1x1:0,0:#ffff", + bgimg_middle = 0, +} + +local return_to = "meta" + +function file_exists(path) + local f = io.open(path) + if f then + f:close() + return true + end + return false +end + +function read_file(path) + local f = io.open(path) + if f then + local data = f:read("a") + f:close() + if data == "" then return end + return data + end +end + +local function format_inline_markdown(str) + local out = {} + local i = 1 + while true do + local code_start, code_end, code = str:find("`(.-)`", i) + if not code_start then break end + out[#out + 1] = str:sub(i, code_start - 1) + :gsub("([_*])%1(.-)%1%1", "%2") + :gsub("([_*])(.-)%1", "%2") + :gsub("%[(.-)%]%((.-)%)", "%1") + out[#out + 1] = "" + i = code_end + 1 + end + out[#out + 1] = str:sub(i) + :gsub("([_*])%1(.-)%1%1", "%2") + :gsub("([_*])(.-)%1", "%2") + :gsub("%[(.-)%]%((.-)%)", "%1") + return table.concat(out) +end + +function markdown_to_hypertext(md) + md = core.hypertext_escape(md.."\n") + + local out = {} + local state = "doc" + for line in md:gmatch("(.-)\n") do + if state == "code" then + if line:match("^```") then + state = "doc" + local code = table.concat(out, "\n") + if out.__lang == "lua" then + code = syntax_highlight(code) + else + code = "" + end + out = out.__parent + out[#out + 1] = "" + out[#out + 1] = code + out[#out + 1] = "" + else + out[#out + 1] = line + out[#out + 1] = "\n" + end + else + local level, heading = line:match("^(#+) (.+)") + if heading then + out[#out + 1] = "" + out[#out + 1] = "\n" + else + local bullet, item = line:match("^%s-([*-]) (.+)") + if bullet then + out[#out + 1] = " • " + out[#out + 1] = format_inline_markdown(item) + out[#out + 1] = "\n" + else + local code, lang = line:match("^(```)(%a*)") + if code then + state = "code" + out = {__parent = out, __lang = lang} + else + out[#out + 1] = format_inline_markdown(line) + out[#out + 1] = "\n" + end + end + end + end + end + + return table.concat(out) +-- return md:gsub("\n%s*####%s*([^\n]-)\n", "\n%1\n") +-- :gsub("\n%s*###%s*([^\n]-)\n", "\n%1\n") +-- :gsub("([^\n]-)\n%-+\n", "\n%1\n") +-- :gsub("\n%s*##%s*([^\n]-)\n", "\n%1\n") +-- :gsub("\n%s*#%s*([^\n]-)\n", "\n\n") +-- :gsub("%*%*([^`\n]-)%*%*", "%1") +-- :gsub("%*([^`\n]-)%*", "%1") +-- :gsub("__([^`\n]-)__", "%1") +-- :gsub("\n_([^`\n]-)_", "%1") +-- :gsub("%*%s+(.-)\n", "• %1\n") +---- :gsub("```%a-\n(.-)```", "") +---- :gsub("`([^`]-)`", "") +-- -- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them. +-- :gsub("%[?!%[[^%]]-%]%([^\n]+%)", "") +end + +minetest.set_formspec_prepend("\ + style[*;textcolor=#aaa]\ + style_type[field,pwdfield;border=false]\ + style_type[image_button;border=false]\ + style_type[button;border=false;bgimg="..theme.get_background_image("button")..";bgimg_middle=8,8;textcolor="..theme.styles.button.text_color.."]\ + style_type[button:hovered;border=false;bgimg="..theme.get_background_image("button", "hovered")..";bgimg_middle=8,8]\ + style_type[button:pressed;border=false;bgimg="..theme.get_background_image("button", "active")..";bgimg_middle=8,8]\ + style_type[scrollbar;border=false;bgimg="..theme.get_background_image("scrollbar")..";bgimg_middle=8;padding=0,8;fgimg="..theme.get_image("scrollbar", "thumb")..";size=32]\ + style[nobg,nobg:hovered,nobg:focused,nobg:hovered+focused;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0;content_offset=0,0]\ +") + +local meta_menu = dofile(menupath.."views/meta_menu.lua") +local servers_menu = dofile(menupath.."views/servers.lua") +local content_menu = dofile(menupath.."views/content.lua") +local online_content_menu = dofile(menupath.."views/online_content.lua") +local settings_menu = dofile(menupath.."views/settings.lua") +local about_menu = dofile(menupath.."views/about.lua") + +function show_meta_menu() + return_to = "meta" + local last_game = minetest.settings:get("menu_last_game") + local last_game_index = 1 + local games = minetest.get_games() + table.sort(games, function(a, b) + return (a.title or a.name) < (b.title or b.name) + end) + for i, x in ipairs(games) do + if x.id == last_game then last_game_index = i end + end + imfs.show(meta_menu, { + games = games, + scroll_pos = imfs.state(last_game_index) + }) +end + +function show_servers_menu() + return_to = "servers" + imfs.show(servers_menu, { + scroll_pos = imfs.state(1), + serverlist = imfs.state(), + detail = imfs.state(), + detail_view = imfs.state(), + joining = imfs.state(), + join_error = imfs.state(), + address = imfs.state(), + port = imfs.state(), + username = imfs.state(core.settings:get("name")), + password = imfs.state(""), + search = "" + }) +end + +function show_content_menu() + imfs.show(content_menu, { + search = "", + show_type = imfs.state("all"), + content_shown = imfs.state(), + page = imfs.state(1), + current_package = imfs.state(), + }) +end + +function show_online_content_menu(content) + imfs.show(online_content_menu, { + search = "", + show_type = imfs.state("all"), + content_shown = imfs.state(), + page = imfs.state(1), + current_package = imfs.state(), + }) +end + +function show_settings_menu() + imfs.show(settings_menu, { + settings_shown = imfs.state(), + categories_shown = imfs.state(), + category = imfs.state("Theme"), + search = "" + }) +end + +function show_about_menu() + imfs.show(about_menu, { + search = "", + }) +end + +show_meta_menu() + \ No newline at end of file diff --git a/package.lua b/package.lua new file mode 100644 index 0000000..9296568 --- /dev/null +++ b/package.lua @@ -0,0 +1,152 @@ + +package = {} + +local function filename(url) + local path = url:split("/") + return path[#path] +end + +function package.install_from_git(url) + +end + +function package.install_from_contentdb(url) + +end + +function package.can_update() + +end + +function package.update() + +end + +function package.info() + +end + +function package.is_installed(kind, name) + local path = core.get_user_path() + if kind == "mod" or kind == "modpack" then + path = path.."/mods/"..name + elseif kind == "game" then + path = path.."/games/"..name + elseif kind == "txp" then + path = path.."/textures/"..name + end + return core.is_dir(path) +end + +local loading_screenshots = {} +local screenshot_path = core.get_cache_path().."/cdb" +function package.screenshot(pkg, url, level) + if not url then + url = pkg.thumbnail + end + + if not level then + level = 2 + end + + if not url then + return default_textures.."no_screenshot.png" + end + + url = url:gsub("/thumbnails/[0-9]+/", "/thumbnails/"..level.."/") + + if loading_screenshots[url] then + return loading_screenshots[url] + end + + local path = screenshot_path.."/" + ..string.format( + "%s-%s-%s-l%d-%s", + pkg.type, + pkg.author, + pkg.name, + level, + filename(url) + ) + + if file_exists(path) then + return path + end + + local state = imfs.state(default_textures.."loading_screenshot.png") + loading_screenshots[url] = state + + core.handle_async(function(args) + return core.download_file(args.url, args.path) + end, {path = path, url = url}, + function(success) + state(path) + loading_screenshots[url] = nil + end) + + return state +end + +local function alphabetic_sort(a, b) + return (a.title ~= "" and a.title or a.name) < (b.title ~= "" and b.title or b.name) +end + +function package.games(dir, sort) + if not dir then + dir = core.get_gamepath() + end + local out = {} + for _, x in ipairs(core.get_dir_list(dir, true)) do + local info = core.get_content_info(dir.."/"..x) + if info.type == "game" then + out[#out +1] = info + end + end + if sort ~= false then + table.sort(out, alphabetic_sort) + end + return out +end + +function package.mods(dir, sort) + if not dir then + dir = core.get_modpath() + end + local out = {} + for _, x in ipairs(core.get_dir_list(dir, true)) do + local info = core.get_content_info(dir.."/"..x) + if info.type == "mod" or info.type == "modpack" then + out[#out +1] = info + end + end + if sort ~= false then + table.sort(out, alphabetic_sort) + end + return out +end + +function package.texturepacks(dir, sort) + if not dir then + dir = core.get_texturepath() + end + local out = {} + for _, x in ipairs(core.get_dir_list(dir, true)) do + local info = core.get_content_info(dir.."/"..x) + if info.type == "txp" then + out[#out +1] = info + end + end + if sort ~= false then + table.sort(out, alphabetic_sort) + end + return out +end + +function package.all() + local out = {} + table.insert_all(out, package.mods(nil, false)) + table.insert_all(out, package.games(nil, false)) + table.insert_all(out, package.texturepacks(nil, false)) + table.sort(out, alphabetic_sort) + return out +end diff --git a/syntax.lua b/syntax.lua new file mode 100644 index 0000000..4e5cbdf --- /dev/null +++ b/syntax.lua @@ -0,0 +1,122 @@ +local keywords = { + "and", "break", "do", "else", "elseif", "end", --[["false",]] "for", "function", + "goto", "if", "in", --[["local", "nil",]] "not", "or", "repeat", "return", + "then", --[["true",]] "until", "while" +} +local keyword_set = {} +for _, kw in ipairs(keywords) do + keyword_set[kw] = true +end + +local keyword_value_set = { + ["true"] = true, + ["false"] = true, + ["nil"] = true, + ["local"] = true, + ["math.huge"] = true +} + +local function colorize(str, color) + return "" +end + +function syntax_highlight(code) + local out = "" + local pos = 1 + while pos <= #code do + local matched = false + + -- Whitespace + local ws_start, ws_end = code:find("^%s+", pos) + if ws_start then + out = out..colorize(code:sub(ws_start, ws_end), "whitespace") + pos = ws_end + 1 + matched = true + end + + -- Single-line comment + if not matched then + local com_start, com_end = code:find("^%-%-[^\n]*", pos) + if com_start then + out = out..colorize(code:sub(com_start, com_end), "comment") + pos = com_end + 1 + matched = true + end + end + + -- String (double-quoted, simple escape handling) + if not matched then + local str_start, str_end = code:find('^"[^"]*"', pos) + if str_start then + out = out..colorize(code:sub(str_start, str_end), "string") + pos = str_end + 1 + matched = true + end + end + + -- String (single-quoted, similar) + if not matched then + local str_start, str_end = code:find("^'[^']*", pos) + if str_start then + out = out..colorize(code:sub(str_start, str_end), "string") + pos = str_end + 1 + matched = true + end + end + + -- Documentation placeholders. These aren't in Lua, but that means they + -- can be highlighted without worrying about confusing alternative meanings. + if not matched then + local str_start, str_end = code:find('^\\?<[^>]*>', pos) + if str_start then + out = out..colorize(code:sub(str_start, str_end), "placeholder") + pos = str_end + 1 + matched = true + end + end + + -- Number (basic integer/float) + if not matched then + local num_start, num_end = code:find("^%d+%.?%d*[eE]?[+-]?%d*", pos) + if num_start then + out = out..colorize(code:sub(num_start, num_end), "number") + pos = num_end + 1 + matched = true + end + end + + -- Identifier/Keyword + if not matched then + local id_start, id_end, id_text = code:find("^([%a_][%w_]*)", pos) + if id_start then + if keyword_set[id_text] then + out = out..colorize(id_text, "keyword") + elseif keyword_value_set[id_text] then + out = out..colorize(id_text, "keyword_value") + else + out = out..colorize(id_text, "identifier") + end + pos = id_end + 1 + matched = true + end + end + + -- Operators/Punctuation (catch-all for non-alphanumeric) + if not matched then + local op_start, op_end = code:find("^[^%s%a%d_\"'{}(),%][]", pos) + if op_start then + local op = code:sub(op_start, op_end) + out = out..colorize(op, op == "-" and code:sub(op_end +1, op_end +2):find "^%d" and "number" or "operator") + pos = op_end + 1 + matched = true + end + end + + -- If nothing matched, advance to avoid infinite loop (e.g., invalid char) + if not matched then + out = out..colorize(code:sub(pos, pos), "generic") + pos = pos + 1 + end + end + return out +end diff --git a/textures/button_bg_shape.png b/textures/button_bg_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..0069ed3b8b1242120c5d310138c47c18f827b163 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|W_h|ehE&XX zdtoDQ0|O6>;h}%l?NV$9Jm#DZnQq^wQg3$t@Vj-MmB-KAsk4hcWp2{IcR|X5J%e`v z^A@%Xj8V)l7}heDG+bk_a>!{d54F1D|qd#XNwL3hDS+t1Z* z^N!WMe_ijuxTRqkLzF`>P*}4;?1G2`cLvu2Ruk3>OtY9W7=5WG{qv?^PWg{ikokHS Xas{eV-)!#yI-kMQ)z4*}Q$iB}#+*~4 literal 0 HcmV?d00001 diff --git a/textures/cancel.png b/textures/cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..58e95ff4e5fdd27580467998997d1a7dffcc642e GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|ay(reLo9le z6BdXCT)psT{i=mO=ZBQ`7&xZ5ZqJWNKeALZR#w=MtFZH6Y=bxB8Py+80_J%6NEj@r z6g<4Xg~yR!ei0M*^fWdgSkvgiwP2mN2A50MBEk7gPGSkXE;$Q(wJR6jaPUi8B*DNC Xd)<9UtbnHj&|U^lS3j3^P6i|)HXl-4A~{>p$7QE5zl3RmfrLY$lN8XDvk!TMM2aUz0QE9> My85}Sb4q9e05Y^9vH$=8 literal 0 HcmV?d00001 diff --git a/textures/checkbox_border_shape.png b/textures/checkbox_border_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..8c6a909365ea47ba39875e41987e97bd83ac2d04 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|tUO&DLo9le z6C^SYbe#G5|NsB`MJ9$uMoAq1+L@eY6|PdgR#vjr=SbEeRySUW5;lR^6DIt;;(dyl Zjo}rC`sajf@j{>}44$rjF6*2Ung9yLDN_Id literal 0 HcmV?d00001 diff --git a/textures/checkbox_check_shape.png b/textures/checkbox_check_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..1d0aa14d73506ea95a29e8a5c12015a6649b5310 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|tUX;ELo9le z6C_x_F#li35Id(c;_Qwm5+5@YQc_Y}uAMn?qGO@ldPWVdjTy%>8Fne|FcD_?@yCv9 b1qZ_`OZjh`j_gTe~DWM4f9R4b} literal 0 HcmV?d00001 diff --git a/textures/circle.png b/textures/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..86373f5da1348cc4efb03efe21b1f4dd7b69ff8e GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}sh%#5ArY;~ z2@nOlGBHHHSCQENf+-JZ OA%mx@pUXO@geCx_UN!Ro literal 0 HcmV?d00001 diff --git a/textures/circle_active.png b/textures/circle_active.png new file mode 100644 index 0000000000000000000000000000000000000000..bae8abb7c263df55d6e1804e63d552ec7664888c GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}sh%#5ArY;~ z2@!- QEzm*+Pgg&ebxsLQ0D6x&ssI20 literal 0 HcmV?d00001 diff --git a/textures/container_bg_shape.png b/textures/container_bg_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..9f34ac3741b777f9cece6c2ceba8c4f346787d9e GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|)_A%&hE&XX zd)tt&*?@t?@y!4KZ@xGl3bk>0b4BDd=kCWAllI(HwA$vgJ1pBw0I22WQ%mvv4FO#t9bSv3Fv literal 0 HcmV?d00001 diff --git a/textures/container_border_shape.png b/textures/container_border_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..6b0e384762555019b81cc9645ebdccad7bdbf54c GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|_ItWGhE&XX zdwV0_Ap?=40f+woFTT-zE+S;vkwe|Ox7pbvcV4AhQ zHpbjO_eIIF&o=+qL>#y?uIUq8x%5bQfqgh+P0F;99_H v0@6vo^j^tbcV8HQ%z3=8+WOCPkg0Z4A1hQCz0j-xdWFH$)z4*}Q$iB}#>ik1 literal 0 HcmV?d00001 diff --git a/textures/field_bg_shape.png b/textures/field_bg_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..0069ed3b8b1242120c5d310138c47c18f827b163 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|W_h|ehE&XX zdtoDQ0|O6>;h}%l?NV$9Jm#DZnQq^wQg3$t@Vj-MmB-KAsk4hcWp2{IcR|X5J%e`v z^A@%Xj8V)l7}heDG+bk_a>!{d54F1D|qd#XNwL3hDS+t1Z* z^N!WMe_ijuxTRqkLzF`>P*}4;?1G2`cLvu2Ruk3>OtY9W7=5WG{qv?^PWg{ikokHS Xas{eV-)!#yI-kMQ)z4*}Q$iB}#+*~4 literal 0 HcmV?d00001 diff --git a/textures/games.png b/textures/games.png new file mode 100644 index 0000000000000000000000000000000000000000..a68fb1f146a4aa12db3c4a53868081df05bc70a8 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|l001;Lo9le z6C^%02*x^uI)ApemHiWs z!?lj{0&Ca~n0Rz^8XP#pGF{Okwn1jb4<@nIj5kak7_~f#SYV;Z%gn&=^P7Af<1$%8 PpluAEu6{1-oD!MPx$ElET{RA_)|I+khjG6>UvwR%LP zAdF}@jcGDp&}6=#;WVZojOck#`C&mARW6d3X zCO_{XKe>5v!D9w4hb^euzs7qKdS8ZuAPCmQhO2N{RYLaCZL+^}`-hflz$b9|hvo+w zEgb&AdIq@sgZCu#z6=FH5S$ec_LC7;>9#r>n&=B{yE}kRe*|#fyspO`EYrk(H#9}( zAHMPfrl|abvl-C&hk7KT3&cMNf>2wWl;XE~&j0_WdZ*j53JIVdNhkD R-jM(R002ovPDHLkV1m?nq)q?; literal 0 HcmV?d00001 diff --git a/textures/menu_content.png b/textures/menu_content.png new file mode 100644 index 0000000000000000000000000000000000000000..3c077dc43798fd808b3ae645ef095f4b5a5426de GIT binary patch literal 285 zcmV+&0pk9NP)Px#*GWV{R9J=WmOT!_FbsuV!~r^W>0GJY|1cmivoLW3I#iZ|u@nEKiV%5If*S~8hEm+peJ7RFZU9%pi$mTV4mVV&L=V=UD?E6TWXr6&Ds(L5?)Z_UAU@{?= zz_KX>fL#EoF^yS4%mC7VD>1C-K@u@poDp}GY$XLaQlojAQPl(UG-d}4HMR7Fx(T^KBdxK>dPx$Gf6~2R9J=Wm%R?cFbsqp6b80VUAiN+@Bb_yv9mC-F;K}YHA)@l$4P^P;3i0` z+~=xJlX?LWZIyvBbL@F^^2NLg01=}Q!O6+|I8KimfXn%Gh(YXP<~7DlHnfyp z5Ae!|zN>P+-tGWP5&$ssBvuJDsGQ47VwC_Q+T%DqVS~!KoFw*c3U8W{>SA-H98S_0IRQHc@RBDSIRq*iT462iBg3sCyDz&gCsPx#^GQTOR9J=WRyz*EAPl8d50EKK=1S@P57SDWS(vy%Iz&bmHTbz8F_b4oLKtlG z{E3~L_;*~i1NZ%Lv>155o>d#y7Uex~D+%c3+qyKGIQ5j67|y_#SIEnKh~hqSCuhRM zfDbV#;>Kvr}H&L<*0*_%EuvyR-ceOKLv zSo<4yd_LxT%=EtQ`+EMjuXE(=nfFwNvoXwVoN|dnU>0LpDj$afN0-%vXZk17mww@w zxtW#D5t^_3+DQ3&ht-D(UXrhJZ7u~rQ*lsxPx$TS-JgR9J=WS3Pe6F${I8>(Gsfg&hh0|8D__orQ^wp(nY^Ir2G9Y$u#Lv`<1p zB=N`di*W${I+AA-B2Hb#Q<|+kO~cG_R{)4;22wS=IG>KE^Q*@>;dZ@zA!0R%kCyWh z#{yHWjD-ed=7;@m$n*X21X!2=fLSuJVqk;JB?=~13?SlkJe|M6$egm0@SM3_FQ1%+ z%z!8R`{S7nacA_MuQ#w_K^0-mIXTiY0GF$<0=N?3Ya0`wbl*U_Y7EA_Y%@6xs z{t)DEToc0qKxZ%To^h4HqPSHO3+`L66*4`38w7o_;XNUn$8KP}FYapdG(+t-N0+fHfNX_L?Qp^Js)J79g5x}Yh z%Yr#d51<&y)OUf#L@#QY6I2myz=Y}?te4T~!-SG(JxqT6Y4(pF16SZfHof?c00000 LNkvXXu0mjfg<-R1 literal 0 HcmV?d00001 diff --git a/textures/menu_servers_icon_ping_0.png b/textures/menu_servers_icon_ping_0.png new file mode 100644 index 0000000000000000000000000000000000000000..1c690617ff0e29cb682ac9ec3354ea0224a7dfd4 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|^gUf1Lo_BP zCoGWMQ2P1r`2#@E;@#BP*ytGQ{MlYuk5ghc=X91BMm(^b literal 0 HcmV?d00001 diff --git a/textures/menu_servers_icon_ping_1.png b/textures/menu_servers_icon_ping_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3b87b07cfdc7ccdd03753ceb64d21f9fcff09b60 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|oIG6|Lo_BP zCoGWMQ2P1r`2#@E;@#BP*ytGQ{MmkTSjbdHGfs)woiiI>+V}`=Vu@j7W@bJ-|Ml;4 gkBhk5q%SZshzbg%Yrhu$4m6Fy)78&qol`;+08cb4;Q#;t literal 0 HcmV?d00001 diff --git a/textures/menu_servers_icon_ping_2.png b/textures/menu_servers_icon_ping_2.png new file mode 100644 index 0000000000000000000000000000000000000000..409d13d1bc0f6dd1a82333ee4af8982ee8cdc273 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|!aQ9ZLo_BP zCoGWMQ2P1r`2#?}B7C81m65}=1Rw~wdg0G`j>!pW30D87Kb*ht!|c<~<85{wR#04i z=)mdKI;Vst0C0de AGXMYp literal 0 HcmV?d00001 diff --git a/textures/menu_servers_icon_ping_3.png b/textures/menu_servers_icon_ping_3.png new file mode 100644 index 0000000000000000000000000000000000000000..b728e2a9942d4c2093fdc9491274ee4656e1d7fb GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|B0OCjLo_BP zCoHhK(6!1a0SGpfe*Sy@01$NO9XxZOja>lvCK0k|V?^l*+&n)mDa~I63P3v;JYD@<);T3K0RSED BJ3;^e literal 0 HcmV?d00001 diff --git a/textures/menu_servers_icon_ping_4.png b/textures/menu_servers_icon_ping_4.png new file mode 100644 index 0000000000000000000000000000000000000000..a7344c415d11550f6be50b95cb843ed4c2570194 GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|yggkULo_BP zCn$(#xQ0p^fWV_k326yd|EE8kugI;($;QUkW`F(r-1LL02iLqjbN~d7oH)+=VN1tr*W^!e;V_-P+TBb*KS9w0rSO!m5KbLh*2~7ZzoH0xQ literal 0 HcmV?d00001 diff --git a/textures/menu_servers_mods.png b/textures/menu_servers_mods.png new file mode 100644 index 0000000000000000000000000000000000000000..ad04c7f4d5eb8cadd74df2723d9f0dc34a062ba3 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Hh8)?hFJ8T zon*_`Y{1hRAHeR`#^>Xsa4@8Eb%EcyrXyFp&Q3YW?3_P=@6U;dvRi`vAAhgelg>PS z-Lkhym&**7?u=Amm=e0z-j$(Z|2)5}xvX{X`X_IQNDKHdq4|z|L6*lc*3{7ECijhJ z)XwC*lzj4+VU}A%r)I^4Yt0ht+Gcz|%;op^qw(pmW&4c08GIsS7yR`9%EukO>a`kA uK=flx=4%t$S-AdvS3LPBea*q^EA(eo1exrgBP9WJJAN_fCBEA;1A)>1Z@sU3EO;9-|0ygwe*g#+7)9M5 zFP3O}$dtlxxr5(8FvO8z!48I3TmB10{I=WX>lVLAJh(K#$&a=5|D%htYhKOxkoiwx(fJRKF7m4zD|j0+ zj~!FH#gN2!l36M`KN-ne3*(9XCnPx#!%0LzR5*>@kueK`U=W5MC2=)Shooo;-TVcvZvNV~)_y^o(h@}t733O1UrWlT zo4~6NnH(>thR3o*d`*egyGETDkhoVW!Bj)`75m+h;}b4 z-M;wh+OxucA0E*D_UNVg;FVdQ&MBb@0IpU>KmY&$ literal 0 HcmV?d00001 diff --git a/textures/scrollbar_bg_shape.png b/textures/scrollbar_bg_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..0069ed3b8b1242120c5d310138c47c18f827b163 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|W_h|ehE&XX zdtoDQ0|O6>;h}%l?NV$9Jm#DZnQq^wQg3$t@Vj-MmB-KAsk4hcWp2{IcR|X5J%e`v z^A@%Xj8V)l7}heDG+bk_a>!{d54F1D|qd#XNwL3hDS+t1Z* z^N!WMe_ijuxTRqkLzF`>P*}4;?1G2`cLvu2Ruk3>OtY9W7=5WG{qv?^PWg{ikokHS Xas{eV-)!#yI-kMQ)z4*}Q$iB}#+*~4 literal 0 HcmV?d00001 diff --git a/textures/scrollbar_thumb_shape.png b/textures/scrollbar_thumb_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..aa9a7ab829e1fda0f5ad14619176f2b3f6fbf6e5 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|Dm`5sLo_BP zr!+LRw6y%^@klt>#M;AiV$vU>EDecejfRSf5C8v?JuVSp`BUOXg^Q#F5Y*LHp8fFq zw|biczc!oL49y&yMLc(sUh&k|)ukl>!H>Uxg_%J>c+*~x*pJVTg*OQtYKf1JGfqer fE>ls=XJS~SXnth*tO%eR7#KWV{an^LB{Ts5Imkg} literal 0 HcmV?d00001 diff --git a/textures/search.png b/textures/search.png new file mode 100644 index 0000000000000000000000000000000000000000..cbbd7f3ffd23ec4544416e8ca948bde34b2d9a3c GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|QaoK8Lo9le z6BY; z;iv&qXTX<(4A(MLLXEo^Z6fB@NdYtQ@i#WEYq=@Liy`w8JD1{(o~=DmGqBT>^9|gQu&X%Q~loCIBKtOIZK_ literal 0 HcmV?d00001 diff --git a/textures/tab_border_shape.png b/textures/tab_border_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..75b28d4c9f8cc8570306b8152188c98c42619f66 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|)_b}*hE&XX zd;1`7g9FbI2dDqG^6@1i1{`m=WVG+k-FnR`@5~t)`LzAtOSIlEV2E-EX3$-r*&ud7 z#DP15YXPeX>jkD+Oc{*6j9VI(QC+%!?uUJA|4RKf`}JKu0BG?}hM1o`)jy>tx&WQd N;OXk;vd$@?2>^?OPVoQ$ literal 0 HcmV?d00001 diff --git a/textures_rgt/button_bg_shape.png b/textures_rgt/button_bg_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..0069ed3b8b1242120c5d310138c47c18f827b163 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|W_h|ehE&XX zdtoDQ0|O6>;h}%l?NV$9Jm#DZnQq^wQg3$t@Vj-MmB-KAsk4hcWp2{IcR|X5J%e`v z^A@%Xj8V)l7}heDG+bk_a>!tkzvlJ>7Ja9yc5f9czj?%f zXZODUoonkp+f4uZ?eA@;@WTvS7<@bb|ID0q>i6CMP17Iy+B32(V9n6dEin1GN98Yz zM1$A@Lk3-kWQM4QV?f2dj2Voxc730B|7yqa*>y}j4%`>8DUo8nz+_@4eZ%{ZTz4cUar?_?Kx{NgkK+IKlxc}Iy|^``It4qnWjXubdW et%s%{6XafsDwPz6B?bdSfx*+&&t;ucLK6VcRC?h6 literal 0 HcmV?d00001 diff --git a/textures_rgt/container_bg_shape.png b/textures_rgt/container_bg_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..f155ff819079a34a8f819a0325d17d4bc6cb9830 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|CV09yhE&XX zd+Q))g8>7JWAcC7<3cXN5l3b+rP%t!$+0sqd~i?x%2<4Er~1>mhQ}^h&-op+?<7O1 z!`qKPSDybfcX|1t_xl-`uP~aDi~n=H_5P^_)D3h_D>th)j~|sla(!PC{xWt~$( F6994wMBxAc literal 0 HcmV?d00001 diff --git a/textures_rgt/container_border_shape.png b/textures_rgt/container_border_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..8a422d3ace605fb2e18af860de1a1e10b2ef3a95 GIT binary patch literal 477 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D%zr+d0MhE&XX zJA0!Sv!g(pt))^ROEmY|EvL03B7YtU3)|N;H!O-n%uzttXYtZIE@gQ)KJSzHT%TH2 z{O9M)xo6&og*F~ZU$@?g zzvlmsHCL}ZeDY((tKZwpzkgeNF5gd{(WpVp;j+v1KZhS*Uci*Lx%tAMv(Hvr?Ax(N zqwn^d33f~=47v@=zN%GZ|9kp#Yee>c^YrJ-=gOpgt+G_CWdYgHJDXYO%dG7Ywr1)T zOZpik9k>_VJjLj||9pS%_q@dK2AAC%cowi;XnEbR^5N4hUj!AHGZ;dkZT{mV|V!a5(;7t$x%-b*bh-_ZgR{KHUKAGG4o_4;$b_+s#M^>bP0l+XkK(E8jR literal 0 HcmV?d00001 diff --git a/textures_rgt/field_bg_shape.png b/textures_rgt/field_bg_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..0069ed3b8b1242120c5d310138c47c18f827b163 GIT binary patch literal 224 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|W_h|ehE&XX zdtoDQ0|O6>;h}%l?NV$9Jm#DZnQq^wQg3$t@Vj-MmB-KAsk4hcWp2{IcR|X5J%e`v z^A@%Xj8V)l7}heDG+bk_a>!S5481^2Qp^{aOiXX@tylTW0@ISr5Y=!D zS)OeHYsRI$(qHVI7(vQGTx2UA?{#M;AiV$vU>EDecejfRSf5C8v?JuVSp`BUOXg^Q#F5Y*LHp8fFq zw|biczc!oL49y&yMLc(sUh&k|)ukl>!H>Uxg_%J>c+*~x*pJVTg*OQtYKf1JGfqer fE>ls=XJS~SXnth*tO%eR7#KWV{an^LB{Ts5Imkg} literal 0 HcmV?d00001 diff --git a/textures_rgt/scrollbar_track.png b/textures_rgt/scrollbar_track.png new file mode 100644 index 0000000000000000000000000000000000000000..23e01c880c292da0418b8e42bd42794a1b71f238 GIT binary patch literal 285 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|u6w#ThE&XX zdwZjxqoV{vqQAbEK*r%7ITtH|mkuhy4QwR~IAk-LzEraQ;Msli@5OWXVt?>&=KTHk zmF3*OLhYAxCFVc(dupxGAa;T0vVHwNyE$jsBlPdT|MNcHfpJU2GKMIJUD+6d@H22WQ% Jmvv4FO#p0LW>5eC literal 0 HcmV?d00001 diff --git a/theme.lua b/theme.lua new file mode 100644 index 0000000..6dbe89d --- /dev/null +++ b/theme.lua @@ -0,0 +1,284 @@ + +theme = { + styles = { + bg_color = "#151618", + text_color = "#aaa", + text_color_muted = "#777", + heading_color = "#ddb969", + link_color = "#68b", + link_color_hovered = "#9be", + code_color = "#da8", + + syntax_keyword = "#d8d", + syntax_keyword_value = "#79e", + syntax_identifier = "#adf", + syntax_operator = "#f66", + syntax_string = "#da8", + syntax_number = "#cdb", + syntax_comment = "#777", + syntax_placeholder = "#ccc", + + container = { + border_color = "#292d2fff", + border_shape = menupath.."textures/container_border_shape.png", + bg_color = "#403e39ff", + bg_color_alt = "#373530ff", + bg_shape = menupath.."textures/container_bg_shape.png", + border_width = "8", + }, + field = { + border_color = "#263038ff", + border_shape = menupath.."textures/field_border_shape.png", + bg_color = "#393f40", + bg_shape = menupath.."textures/field_bg_shape.png", + border_width = "8", + }, + button = { + text_color = "#ccc", + border_color = "#292d2fff", + border_color_hovered = "#252829ff", + border_color_active = "#252829ff", + border_shape = menupath.."textures/button_border_shape.png", + bg_color = "#403e39ff", + bg_color_hovered = "#373633ff", + bg_color_active = "#373633ff", + bg_shape = menupath.."textures/button_bg_shape.png", + border_width = "8", + }, + tab = { + text_color = "#ccc", + border_color = "#292d2fff", + border_color_hovered = "#252829ff", + border_color_active = "#252829ff", + border_shape = menupath.."textures/tab_border_shape.png", + bg_color = "#403e39ff", + bg_color_hovered = "#373633ff", + bg_color_active = "#373633ff", + bg_shape = menupath.."textures/tab_bg_shape.png", + border_width = "8", + }, + checkbox = { + border_color = "#252829ff", + border_shape = menupath.."textures/checkbox_border_shape.png", + bg_color = "#3d464dff", + bg_shape = menupath.."textures/checkbox_bg_shape.png", + check_color = "#252829ff", + check_shape = menupath.."textures/checkbox_check_shape.png", + }, + scrollbar = { + border_color = "#292d2fff", + border_shape = menupath.."textures/scrollbar_border_shape.png", + bg_color = "#1c2023ff", + bg_shape = menupath.."textures/scrollbar_bg_shape.png", + thumb_color = "#fff", + thumb_shape = menupath.."textures/scrollbar_thumb_shape.png", + border_width = "8", + }, + + icons = { + menu_content = { + {shape = menupath.."textures/menu_content.png", color = "#fff"} + } + }, + } +} + +--[[ RGT theme. +theme = { + styles = { + bg_color = "#151618", + text_color = "#aaa", + text_color_muted = "#777", + heading_color = "#ddb969", + link_color = "#68b", + link_color_hovered = "#9be", + code_color = "#da8", + + syntax_keyword = "#d8d", + syntax_keyword_value = "#79e", + syntax_identifier = "#adf", + syntax_operator = "#f66", + syntax_string = "#da8", + syntax_number = "#cdb", + syntax_comment = "#777", + syntax_placeholder = "#ccc", + + container = { + border_color = "#684f1dff",--"#292d2fff", + border_shape = menupath.."textures/container_border_shape.png", + bg_color = "#1c2023ff", --"#403e39ff", + bg_color_alt = "#22272aff",--"#373530ff", + bg_shape = menupath.."textures/container_bg_shape.png", + border_width = "16", + }, + field = { + border_color = "#4d4d4dff", + border_shape = menupath.."textures/field_border_shape.png", + bg_color = "#5c5c5c", + bg_shape = menupath.."textures/field_bg_shape.png", + border_width = "8", + }, + button = { + text_color = "#ccc", + border_color = "#4d4d4dff", + border_color_hovered = "#3c3c3cff", + border_color_active = "#2b2b2bff", + border_shape = menupath.."textures/button_border_shape.png", + bg_color = "#5c5c5c", + bg_color_hovered = "#4b4b4bff", + bg_color_active = "#3a3a3aff", + bg_shape = menupath.."textures/button_bg_shape.png", + border_width = "8", + }, + icons = { + + }, + } +} +]] + +function theme.get_background_image(bg, state) + local bg_color + local border_color + + if state == "hovered" then + bg_color = theme.styles[bg].bg_color_hovered + border_color = theme.styles[bg].border_color_hovered + elseif state == "active" then + bg_color = theme.styles[bg].bg_color_active + border_color = theme.styles[bg].border_color_active + else + bg_color = theme.styles[bg].bg_color + border_color = theme.styles[bg].border_color + end + + return "("..theme.styles[bg].bg_shape.."^[multiply:"..bg_color..")^("..theme.styles[bg].border_shape.."^[multiply:"..border_color..")", theme.styles[bg].border_width +end + +function theme.get_image(element, prop) + return "("..theme.styles[element][prop.."_shape"].."^[multiply:"..theme.styles[element][prop.."_color"]..")" +end + +local assets = os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/" +function theme.icon(name) + local icon = theme.styles.icons[name] + if icon then + if type(icon) == "table" then + local out = {} + for i = 1, #icon do + out[#out + 1] = "("..icon[i].shape.."^[multiply:"..icon[i].color..")" + end + return table.concat(out, "^") + else + return icon + end + end + + local path = os.getenv("HOME").."/eclipse-workspace/mtmenu/textures/"..name..".png" + return file_exists(path) and path or assets..name..".png" +end + +function theme.field(x, y, w, h, label, value) + if value then + h = h + 0.3 + end + imfs.group(x, y, w, h) + if value then + imfs.label(0.2, 0.125, label) + end + imfs.image(0, value and 0.3 or 0, "100%", value and "100% - 0.3" or "100%", theme.get_background_image("field"), theme.styles.field.border_width) + local field = imfs.field(0.1, value and 0.3 or 0, "100% - 0.2", value and "100% - 0.3" or "100%", value or label) + imfs.group_end() + return field +end + +function theme.number_field(x, y, w, h, value, step) + step = step or 1 + imfs.group(x, y, w, h) + imfs.button(0, "50% - 0.25", 0.5, 0.5, "-") + :onclick(function() + value(tonumber(value()) - step) + end) + imfs.image(0.6, 0, "100% - 1.2", "100%", theme.get_background_image("field"), theme.styles.field.border_width) + local field = imfs.field(0.7, 0, "100% - 1.4", "100%", value()) + imfs.button("100% - 0.5", "50% - 0.25", 0.5, 0.5, "+") + :onclick(function() + value(tonumber(value()) + step) + end) + imfs.group_end() + return field +end + +function theme.checkbox(x, y, label, value, tooltip) + imfs.group(x, y, 5, 0.75) + local box + box = imfs.button(0, 0, 0.75, "100%") + :image(theme.get_background_image("checkbox")..(value() and "^"..theme.get_image("checkbox", "check") or "")) + :onclick(function() + value(not value()) + if box._onchange then + box._onchange(value._val) + end + end) + :tooltip(tooltip, not not tooltip) + box.onchange = function(e, fn) + e._onchange = fn + end + imfs.label(0.85, 0.375, label) + imfs.group_end() + return box +end + +local open_dropdown = imfs.state() +function theme.dropdown(x, y, w, h, options, value) + local id = imfs._new_id() + local el + local open = open_dropdown() == id + local border_width = pixel_to_formspec(theme.styles.container.border_width) + local box_height = math.min(#options, 5) * 0.5 + imfs.group(x, y, w, open and h + box_height or h) + imfs.group(0, 0, "100%", h) + el = imfs.button(0, 0, "100%", "100%", value()) + :onclick(function() + if open_dropdown() == id then + open_dropdown(false) + else + open_dropdown(id) + end + end) + imfs.label("100% - 0.6", "50%", open and "^" or "v") + imfs.group_end() + + if open then + imfs.image(0, h, "100%", box_height + border_width * 2, theme.get_background_image("container")) + imfs.scroll_container(border_width, h + border_width, "100% - "..(border_width * 2), box_height) + for i = 0, #options - 1 do + local option = options[i + 1] + imfs.button(0, i * 0.5, "100%", 0.5, option) + :style({ + border = false, + bgimg = assets.."white.png", + bgimg_middle = 0, + bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt, + }) + :style("hovered", { + border = false, + bgimg = assets.."white.png", + bgimg_middle = 0, + }) + :style("pressed", { + border = false, + bgimg = assets.."white.png", + bgimg_middle = 0, + }) + :onclick(function() + value(option) + open_dropdown(false) + end) + end + imfs.scroll_container_end() + end + imfs.group_end() + return el +end + diff --git a/views/about.lua b/views/about.lua new file mode 100644 index 0000000..c7505dc --- /dev/null +++ b/views/about.lua @@ -0,0 +1,87 @@ + +local function get_credits() + local f = assert(io.open(core.get_mainmenu_path() .. "/credits.json")) + local json = core.parse_json(f:read("*all")) + f:close() + return json +end + +local function get_renderer_info() + local out = {} + + local renderer = core.get_active_renderer() + if renderer:sub(1, 7) == "OpenGL " then + renderer = renderer:sub(8) + end + local m = renderer:match("^[%d.]+") + if not m then + m = renderer:match("^ES [%d.]+") + end + out[#out + 1] = m or renderer + out[#out + 1] = core.get_active_driver():lower() + out[#out + 1] = core.get_active_irrlicht_device():upper() + + return table.concat(out, " / ") +end + +return function(state) + imfs.begin(size.x, size.y) + :padding(0, 0) + :bgcolor(theme.styles.bg_color, true) + + if not state.credits then + local credits = get_credits() + state.credits = table.concat({ + "", + "", + "", + "", + "", fgettext_ne("Core Developers"), "\n", + core.hypertext_escape(table.concat(credits.core_developers, "\n")), + "\n\n", fgettext_ne("Core Team"), "\n", + core.hypertext_escape(table.concat(credits.core_team, "\n")), + "\n\n", fgettext_ne("Active Contributors"), "\n", + core.hypertext_escape(table.concat(credits.contributors, "\n")), + "\n\n", fgettext_ne("Previous Core Developers"), "\n", + core.hypertext_escape(table.concat(credits.previous_core_developers, "\n")), + "\n\n", fgettext_ne("Previous Contributors"), "\n", + core.hypertext_escape(table.concat(credits.previous_contributors, "\n")), + "\n\n", + }):gsub("%[.-%]", "%1"):gsub("\\<.-\\>", "%1") + end + + local header_height = size.x * (72/672) + imfs.image(0, 0.2, size.x, header_height, default_textures.."menu_header.png") + + imfs.hypertext(0, header_height + 0.3, "100%", 0.5, "Version "..version.string.."") + + imfs.hypertext(0, header_height + 0.9, "100%", "100% - "..(header_height + 0.9), state.credits) + + imfs.button("100% - 2.2", 0.2, 2, 0.75, "Back") + :onclick(function() + show_meta_menu() + end) + + imfs.button(0.2, 0.2, 3, 0.75, "API reference") + :onclick(function() + + end) + + imfs.button(0.2, 1.05, 3, 0.75, "Website") + :onclick(function() + core.open_url("https://luanti.org") + end) + + imfs.button(0.2, 1.9, 3, 0.75, "Open data directory") + :onclick(function() + core.open_dir(core.get_user_path()) + end) + + imfs.box(0, "100% - 0.45", 6, 0.45, theme.styles.bg_color) + imfs.label(0.2, "100% - 0.225", fgettext_ne("Active renderer:").." "..get_renderer_info()) + + local offset = math.abs(size.x - size.y) + imfs.image(size.x > size.y and offset / 2 or 0, size.x < size.y and offset / 2 or 0, math.min(size.x, size.y), math.min(size.x, size.y), default_textures.."logo.png^[opacity:32") + + return imfs.end_() +end diff --git a/views/content.lua b/views/content.lua new file mode 100644 index 0000000..9ba6361 --- /dev/null +++ b/views/content.lua @@ -0,0 +1,236 @@ + +local function search_content(state, search) + local out = {} + + for i = 1, #state.content do + local pkg = state.content[i] + if pkg.name:find(search, 1, true) then + out[#out + 1] = pkg + end + end + + return out +end + +return function(state) + imfs.begin(size.x, size.y) + :padding(0, 0) + :bgcolor(theme.styles.bg_color, true) + + if not state.content then + state.content = package.all() + state.content_shown._val = state.content + end + + local current_package = state.current_package() + + imfs.box(0, 0, "100%", current_package and 1 or 2, theme.styles.container.bg_color) + imfs.row(0.2, 0.125, "100% - 0.4", 0.75) + :gap(0.1) + theme.field(0, 0, "1x", "100%", state.search) + :onchange(function(value) + state.search = value + end) + :onenter(function() + state.page(1) + state.content_shown(search_content(state, state.search)) + end) + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("search")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Search content") + :onclick(function() + state.page(1) + state.content_shown(search_content(state, state.search)) + end) + + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("cancel")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Clear search") + :onclick(function() + state.search = "" + state.page(1) + state.content_shown(state.content) + end) + imfs.button(0, 0, 3.5, 0.75, "Check for updates") + :onclick(function() + + end) + imfs.button(0, 0, 2, 0.75, "Back") + :onclick(function() + if current_package then + state.current_package(false) + else + show_meta_menu() + end + end) + imfs.row_end() + imfs.box(0, 0.95, "100%", 0.1, theme.styles.container.border_color) + + if current_package then + imfs.hypertext(0.25, 1.05, "100% - 0.5", "100% - 1.05", "\ + \ + \ + "..markdown_to_hypertext(read_file(current_package.path.."/README.md") or "")) + else + imfs.row(0.2, 1.125, "100% - 0.4", 0.75) + :gap(0.1) + + imfs.group(0, 0, "1x", 0.75) + imfs.image(0, 0, "100%", "100%", theme.get_background_image("button"), "8,8") + imfs.row(0.15, 0.05, "100% - 0.3", "100% - 0.13") + local show_type = state.show_type() + imfs.button(0, 0, "1x", "100%", "All") + :style(show_type == "all" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "all" then + state.show_type("all") + state.page(1) + state.content = package.all() + if state.search ~= "" then + state.content_shown(search_content(state, state.search)) + else + state.content_shown(state.content) + end + end + end) + imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color) + imfs.button(0, 0, "1x", "100%", "Games") + :style(show_type == "games" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "games" then + state.show_type("games") + state.page(1) + state.content = package.games() + if state.search ~= "" then + state.content_shown(search_content(state, state.search)) + else + state.content_shown(state.content) + end + end + end) + imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color) + imfs.button(0, 0, "1x", "100%", "Mods") + :style(show_type == "mods" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "mods" then + state.show_type("mods") + state.page(1) + state.content = package.mods() + if state.search ~= "" then + state.content_shown(search_content(state, state.search)) + else + state.content_shown(state.content) + end + end + end) + imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color) + imfs.button(0, 0, "1x", "100%", "Texture Packs") + :style(show_type == "texturepacks" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "texturepacks" then + state.show_type("texturepacks") + state.page(1) + state.content = package.texturepacks() + if state.search ~= "" then + state.content_shown(search_content(state, state.search)) + else + state.content_shown(state.content) + end + end + end) + imfs.row_end() + imfs.group_end() + + imfs.button(0, 0, 3.5, 0.75, "Browse online content...") + :onclick(function() + show_online_content_menu() + end) + imfs.button(0, 0, 2.5, 0.75, "New package...") + :onclick(function() + + end) + imfs.row_end() + + imfs.box(0, 1.95, "100%", 0.1, theme.styles.container.border_color) + + local content = state.content_shown() + if #content == 0 then + imfs.hypertext(0, 2.05, "100%", 2, [[ + + + ]]) + else + local num_per_page = math.floor(size.x / 3) * math.floor((size.y - 2) / 3) + local pages = math.ceil(#content / num_per_page) + imfs.scroll_container(0, 2.05, "100%", "100% - 2.55", "horizontal", 0, "") + :scrollbar(function() + imfs.scrollbar(-800, -800, 0, 0, "horizontal", state.page) + :options({ + min = 1, + max = pages, + smallstep = 1 + }) + :onchange(function(action, value) + if action == "CHG" then + state.page(value) + end + end) + end) + local x = 0 + local y = 0 + local start = (state.page() - 1) * num_per_page + for i = start + 1, start + num_per_page do + local pkg = content[i] + if not pkg then break end + if not pkg.icon then + pkg.icon = pkg.type == "game" and pkg.path.."/menu/icon.png" or pkg.path.."/icon.png" + if not file_exists(pkg.icon) then + pkg.icon = theme.icon("menu_content") + end + end + + imfs.button(x + 0.125, y, 2.5, 2.5) + :image(pkg.icon) + :tooltip(pkg.title ~= "" and pkg.title or pkg.name) + :onclick(function() + state.current_package(pkg) + end) + x = x + 3 + if x + 2.5 > size.x then + x = 0 + y = y + 3 + end + end + imfs.scroll_container_end() + + imfs.row(0, "100% - 0.375", "100%", 0.25) + :align("center") + for i = 1, pages do + imfs.button(0, 0, 0.25, 0.25) + :image(theme.icon(i == state.page() and "circle_active" or "circle")) + :onclick(function() + state.page(i) + end) + end + imfs.row_end() + end + end + + return imfs.end_() +end diff --git a/views/docs.lua b/views/docs.lua new file mode 100644 index 0000000..c51f12f --- /dev/null +++ b/views/docs.lua @@ -0,0 +1,8 @@ + +return function(state) + imfs.begin(size.x, size.y) + :padding(0, 0) + :bgcolor(theme.styles.bg_color, true) + + return imfs.end_() +end diff --git a/views/game.lua b/views/game.lua new file mode 100644 index 0000000..e69de29 diff --git a/views/meta_menu.lua b/views/meta_menu.lua new file mode 100644 index 0000000..a5c78f4 --- /dev/null +++ b/views/meta_menu.lua @@ -0,0 +1,129 @@ + +return function(state) + imfs.begin(size.x, size.y) + :padding(0, 0) + :bgcolor("#151618", true) + + imfs.image(0, size.y * 0.08, size.x, size.x * (72/672), default_textures.."menu_header.png") + + local games = state.games + + if #games < 1 then + imfs.hypertext(0, 0, size.x, size.y, [[ + + + + + ]]) + return imfs.end_() + end + + imfs.scroll_container(0, 0, size.x, size.y, "horizontal", 0, "") + :scrollbar(function() + imfs.scrollbar(-800, -800, 0, 0, "horizontal", state.scroll_pos) + :options({ + min = 1, + max = #games, + smallstep = 1, + }) + :onchange(function(action, value) + if action == "CHG" then + state.scroll_pos(value) + end + end) + end) + + local center = size.x / 2 + + for i, x in ipairs(games) do + imfs.button(center + (i - #games / 2) * 0.25, size.y - 0.5, 0.25, 0.25) + :image(theme.icon(i == state.scroll_pos() and "circle_active" or "circle")) + :onclick(function() + state.scroll_pos(i) + end) + + local dist = i - state.scroll_pos() + local scale = 4 / (math.abs(dist) + 1) + if scale > 0.1 then + --This looks neat, but is useless for UI purposes: center +10^(1 /math.abs(dist)) *math.sign(dist) + local lc = center + math.abs(dist * 10) ^ 0.55 * math.sign(dist) + if x.menuicon_path == "" then + x.menuicon_path = theme.icon("games") + end + --TODO: Do these need some kind of background? + imfs.button(lc - scale /2, size.y / 2 - scale / 2 + (4 - scale) / 2 + 2, scale, scale) + :image(x.menuicon_path) + :tooltip(x.title, "#444", "#aaa") + if dist == 0 then + imfs.button(lc - scale / 2, size.y / 2 + 4, 4, 0.5, x.title) + :styles(styles_no_background) + end + end + end + + imfs.scroll_container_end() + + imfs.button("5%", "30%", 4, 4) + :style({ + border = false, + bgimg = theme.icon("menu_servers"), + bgimg_middle = 0, + }) + :style("hovered", { + bgimg = theme.icon("menu_servers_hovered"), + bgimg_middle = 0, + }) + :style("pressed", { + bgimg = theme.icon("menu_servers_hovered"), + bgimg_middle = 0, + }) + :onclick(show_servers_menu) + imfs.button("5%", size.y * 0.3 + 4, 4, 0.5, "Servers") + :styles(styles_no_background) + + imfs.button(size.x * 0.95 - 4, "30%", 4, 4) + :style({ + border = false, + bgimg = theme.icon("menu_content"), + bgimg_middle = 0, + }) + :style("hovered", { + bgimg = theme.icon("menu_content_hovered"), + bgimg_middle = 0, + }) + :style("pressed", { + bgimg = theme.icon("menu_content_hovered"), + bgimg_middle = 0, + }) + :onclick(show_content_menu) + imfs.button(size.x * 0.95 - 4, size.y * 0.3 + 4, 4, 0.5, "Content") + :styles(styles_no_background) + + imfs.button(1, 0, 2, 0.5, "About") + :style({ + bgimg = theme.get_background_image("tab") + }) + :style("hovered", { + bgimg = theme.get_background_image("tab", "hovered") + }) + :style("pressed", { + bgimg = theme.get_background_image("tab", "active"), + }) + :onclick(show_about_menu) + + imfs.button(size.x - 3, 0, 2, 0.5, "Settings") + :style({ + bgimg = theme.get_background_image("tab"), + }) + :style("hovered", { + bgimg = theme.get_background_image("tab", "hovered"), + }) + :style("pressed", { + bgimg = theme.get_background_image("tab", "active"), + }) + :onclick(show_settings_menu) + + return imfs.end_() +end \ No newline at end of file diff --git a/views/online_content.lua b/views/online_content.lua new file mode 100644 index 0000000..0580f55 --- /dev/null +++ b/views/online_content.lua @@ -0,0 +1,231 @@ + +local function fetch_content() + local url = core.settings:get("contentdb_url") + .."/api/packages/?type=mod&type=game&type=txp&protocol_version=" + ..core.get_max_supp_proto().."&engine_version=" + ..core.urlencode(core.get_version().string) + for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do + item = item:trim() + if item ~= "" then + url = url .. "&hide=" .. core.urlencode(item) + end + end + + local http = core.get_http_api() + local response = http.fetch_sync({ + url = url, + extra_headers = { + core.get_http_accept_languages() + }, + }) + return core.parse_json(response.data) +end + +return function(state) + imfs.begin(size.x, size.y) + :padding(0, 0) + :bgcolor(theme.styles.bg_color, true) + + imfs.box(0, 0, "100%", 1, theme.styles.container.bg_color) + imfs.row(0.2, 0.125, "100% - 0.4", 0.75) + :gap(0.1) + + imfs.group(0, 0, "1.25x", 0.75) + imfs.image(0, 0, "100%", "100%", theme.get_background_image("button")) + imfs.row(0.15, 0.05, "100% - 0.3", "100% - 0.13") + local show_type = state.show_type() + imfs.button(0, 0, "1x", "100%", "All") + :style(show_type == "all" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "all" then + state.show_type("all") + state.page(1) +-- state.content = package.all() +-- if state.search ~= "" then +-- state.content_shown(search_content(state, state.search)) +-- else +-- state.content_shown(state.content) +-- end + end + end) + imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color) + imfs.button(0, 0, "1x", "100%", "Games") + :style(show_type == "games" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "games" then + state.show_type("games") + state.page(1) +-- state.content = package.games() +-- if state.search ~= "" then +-- state.content_shown(search_content(state, state.search)) +-- else +-- state.content_shown(state.content) +-- end + end + end) + imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color) + imfs.button(0, 0, "1x", "100%", "Mods") + :style(show_type == "mods" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "mods" then + state.show_type("mods") + state.page(1) +-- state.content = package.mods() +-- if state.search ~= "" then +-- state.content_shown(search_content(state, state.search)) +-- else +-- state.content_shown(state.content) +-- end + end + end) + imfs.box(0, 0, 0.1, "100%", theme.styles.container.border_color) + imfs.button(0, 0, "1x", "100%", "Texture Packs") + :style(show_type == "texturepacks" and style_borderless_alt or style_borderless) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + :onclick(function() + if show_type ~= "texturepacks" then + state.show_type("texturepacks") + state.page(1) +-- state.content = package.texturepacks() +-- if state.search ~= "" then +-- state.content_shown(search_content(state, state.search)) +-- else +-- state.content_shown(state.content) +-- end + end + end) + imfs.row_end() + imfs.group_end() + + theme.field(0, 0, "1x", "100%") + :onchange(function(value) + state.search = value + end) + + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("search")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Search content") + :onclick(function() + + end) + + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("cancel")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Clear search") + :onclick(function() + + end) + + imfs.button(0, 0, 2, "100%", "Back") + :onclick(function() + show_content_menu() + end) + imfs.row_end() + imfs.box(0, 0.95, "100%", 0.1, theme.styles.container.border_color) + + if not state.content_shown() then + imfs.row(0, "50% - 0.25", "100%", 0.5) + :align("center") + + imfs.image(0, 0, 1, 0.5, theme.icon("loader")) + :animated(4, 200) + imfs.row_end() + if not state.loading then + core.handle_async(fetch_content, nil, function(content) + state.content = content + state.num_content = #content + state.content_shown(state.content) + state.loading = false + end) + state.loading = true + end + return imfs.end_() + end + + local content = state.content_shown() + local num_per_page = math.floor((size.x + 0.5) / 5.5) * math.floor((size.y - 1.05) / 3.5) + local excess_x = (size.x + 0.5) % 5.5 + local excess_y = (size.y - 1.05) % 3.5 + imfs.scroll_container(0, 1.05, "100%", "100% - 1.55", "horizontal", 0, "") + :scrollbar(function() + imfs.scrollbar(0, size.y - 0.5, size.x, 0.5, "horizontal", state.page()) + :options({ + min = 1, + max = math.ceil(state.num_content / num_per_page), + smallstep = 1 + }) + :onchange(function(action, value) + if action == "CHG" then + state.page(value) + end + end) + end) + + local x = 0 + local y = 0 + local start = (state.page() - 1) * num_per_page + local i = start + local idx = start + while idx < start + num_per_page do + i = i + 1 + local pkg = content[i] + if not pkg then break end + + if not package.is_installed(pkg.type, pkg.name) then + idx = idx + 1 + + imfs.group(x + excess_x / 2, y + excess_y / 2, 5, 3) + imfs.image(0, 0, "100%", "100%", package.screenshot(pkg)) + imfs.box(0, 0, "100%", "100%", "#000a") + imfs.hypertext(0.1, 0.1, "100% - 0.2", "100% - 0.2", string.format( + [[ + by %s + %s]], + core.hypertext_escape(pkg.title or pkg.name), + pkg.author, + pkg.short_description + )) + :style(".scrollbar", { + size = "0" + }) + imfs.button(0, 0, "100%", "100%") + :styles(styles_no_background) + :style("hovered", { + bgimg = "[fill:1x1:0,0:#fff", + bgcolor = "#0004" + }) + :style("pressed", { + bgimg = "[fill:1x1:0,0:#fff", + bgcolor = "#0004" + }) + :onclick(function() + + end) + imfs.group_end() + x = x + 5.5 + if x + 5 > size.x then + x = 0 + y = y + 3.5 + end + end + end + + imfs.scroll_container_end() + + return imfs.end_() +end diff --git a/views/servers.lua b/views/servers.lua new file mode 100644 index 0000000..9d3f984 --- /dev/null +++ b/views/servers.lua @@ -0,0 +1,562 @@ + +local function add_favorite_server(address, port) + state.favorite_servers[#state.favorite_servers +1] = {address = address, port = port} + local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w") + f:write(minetest.write_json(state.favorite_servers)) + f:flush() + f:close() +end + +local function remove_favorite_server(address, port) + local idx + for i, x in pairs(state.favorite_servers) do + if x.address == address and x.port == port then + idx = i + break + end + end + if idx then + table.remove(state.favorite_servers, idx) + local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w") + f:write(minetest.write_json(state.favorite_servers)) + f:flush() + f:close() + end +end + +local function search_server_list(state, input) + if input:trim() == "" then + return nil + end + + local search_str = "" + local words = {} + local mods = {} + local game + local players = {} + + input = input:lower() + + for x in input:gmatch("%S+") do + if x:sub(1, 4) == "mod:" then + mods[#mods + 1] = x:sub(5) + elseif x:sub(1, 7) == "player:" then + players[#players + 1] = x:sub(8) + elseif x:sub(1, 5) == "game:" then + game = x:sub(6) + else + words[#words + 1] = x + end + end + + local out = {} + for _, x in ipairs(state.servers) do + local passed = true + + for _, a in ipairs(words) do + if not (x.description and x.description:lower():find(a, 1, true) or x.name and x.name:lower():find(a, 1, true)) then + passed = false + end + end + + -- PUC Lua doesn't have `continue`, hence the indentation. + if passed then + if game and x.gameid and game ~= x.gameid:lower():trim() then + passed = false + end + + if passed then + if x.mods and #mods > 0 then + local found = 0 + for _, a in ipairs(x.mods) do + for _, b in ipairs(mods) do + if a == b then + found = found + 1 + end + end + end + passed = found == #mods + else + passed = not not x.mods + end + + if passed then + if x.clients_list and #players > 0 then + local found = 0 + for _, a in ipairs(x.clients_list) do + for _, b in ipairs(players) do + if a:lower():trim() == b then + found = found + 1 + end + end + end + + passed = found == #players + else + passed = not not x.clients_list + end + + if passed then + out[#out +1] = x + end + end + end + end + end + return out +end + +local function refresh_server_list(state) + core.handle_async( + function(param) + local http = core.get_http_api() + local url = string.format("%s/list?proto_version_min=%d&proto_version_max=%d", + "https://servers.luanti.org" or core.settings:get("serverlist_url"), + core.get_min_supp_proto(), + core.get_max_supp_proto() + ) + + local response = http.fetch_sync({url = url}) + if not response.succeeded then + return {} + end + + local retval = core.parse_json(response.data) + return retval and retval.list or {} + end, + nil, + function(result) + local list = table.copy(state.favorite_servers) + table.insert_all(list, result) + state.servers = list + if state.search ~= "" then + state.servers_filtered = search_server_list(state, state.search) + list = state.servers_filtered + end + state.serverlist(list) + end + ) +end + +local function join_server(state) + core.settings:set("name", state.username()) + local address + local port + if state.joining() == true then + if not tonumber(state.port()) then + state.join_error("Invalid port.") + return + end + address = state.address():lower() + port = state.port():lower() + else + address = state.joining().address:lower() + port = state.joining().port + end + local is_favorite = false + for _, x in ipairs(state.favorite_servers) do + if x.address == address and x.port == port then + is_favorite = true + end + end + if not is_favorite then + add_favorite_server(address, port) + end + core.settings:set("address", address) + core.settings:set("remote_port", port) + gamedata = { + playername = state.username(), + password = state.password(), + address = address, + port = port, + selected_world = 0, + singleplayer = false + } + + core.start() +end + +return function(state) + imfs.begin(size.x, size.y) + :padding(0, 0) + :bgcolor(theme.styles.bg_color, true) + + local loading + if not state.serverlist() then + loading = true + local favorite_servers = core.parse_json(read_file(core.get_user_path().."/client/serverlist/favoriteservers.json") or "{}") + for _, x in ipairs(favorite_servers) do + x.favorite = true + end + state.favorite_servers = favorite_servers + state.serverlist._val = favorite_servers + refresh_server_list(state) + end + + local enable_tooltips = not (state.joining() or state.detail_view()) + + local list = state.serverlist() + + imfs.row("10%", size.y * 0.1 - 0.85, "80%", 0.75) + :gap(0.1) + + theme.field(0, 0, "1x", "100%", state.search) + :onchange(function(value) + state.search = value + end) + :onenter(function(value) + state.search = value + state.servers_filtered = search_server_list(state, state.search) + state.serverlist(state.servers_filtered) + state.scroll_pos(1) + end) + + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("search")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Search server list") + :onclick(function() + state.servers_filtered = search_server_list(state, state.search) + state.serverlist(state.servers_filtered) + state.scroll_pos(1) + end) + + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("cancel")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Clear search") + :onclick(function() + state.search = "" + state.servers_filtered = nil + state.serverlist(state.servers) + state.scroll_pos(1) + end) + + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("refresh")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Refresh server list") + :onclick(function() + refresh_server_list(state) + end) + + imfs.button(0, 0, 3, 0.75, "Direct Connection...") + :onclick(function() + state.joining(true) + end) + + imfs.row_end() + + local border_width = pixel_to_formspec(theme.styles.container.border_width) + + imfs.image("10%", "10%", "80%", "80%", theme.get_background_image("container")) + imfs.group("10% + "..border_width, "10% + "..border_width, "80% - "..(border_width * 2), "80% - "..(border_width * 2)) + + imfs.scroll_container(0, 0, state.detail() and "65% - 0.05" or "100%", "100%", "vertical", 0, "") + :scrollbar(function() + imfs.scrollbar(state.detail() and "65% - 0.55" or "100% - 0.5", 0, 0.5, "100%", "vertical", state.scroll_pos) + :options({ + min = 1, + max = #list, + smallstep = 1, + thumbsize = 0, + }) + :onchange(function(action, value) + if action == "CHG" then + state.scroll_pos(value) + end + end) + end) + + imfs.scope("servers") + local height = size.y * 0.8 + local i = 0 + local idx = state.scroll_pos() + while true do + -- We must use a while loop here because we skip incompatible servers, + -- and we have no way of knowing how many there will be until we iterate. + if i > math.ceil(height / 0.5) then break end + + local x = list[idx] + -- End early if we've run out of list. + if not x then break end + + -- Skip incompatible servers. You can't join them, + -- so showing them is rather pointless. + if (x.proto_max or version.proto_min) >= version.proto_min then + local ping_lvl = 0 + local lag = (x.lag or 0) * 1000 + (x.ping or 0) * 250 + if lag <= 125 then + ping_lvl = 4 + elseif lag <= 175 then + ping_lvl = 3 + elseif lag <= 250 then + ping_lvl = 2 + elseif lag <= 400 then + ping_lvl = 1 + end + + local name = x.name and x.name:trim() or "" + if name == "" then + name = core.colorize("#888", x.address..":"..x.port) + end + + imfs.button(0, i * 0.5, "100%", 0.5) + :style({ + border = false, + bgimg = "[fill:1x1:0,0:#fff", + bgimg_middle = 0, + bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt, + }) + :style("hovered", { + border = false, + bgimg = "[fill:1x1:0,0:#fff", + bgimg_middle = 0, + }) + :style("pressed", { + border = false, + bgimg = "[fill:1x1:0,0:#fff", + bgimg_middle = 0, + }) + :onclick(function() + state.detail(x) + end) + imfs.row(0.1, i * 0.5, "100% - 0.65", 0.5) + if x.favorite then + imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_favorite")) + :tooltip("Favorite server", enable_tooltips) + end + + local name = x.name and x.name:trim() or "" + if name == "" then + name = core.colorize("#888", x.address..":"..x.port) + end + -- We use an arealabel to ensure that the name will be clipped + -- if it is inordinately long, rather than overflowing onto + -- other thigs. + imfs.arealabel(0, 0.125, "1x", 0.375, name) + + if x.pvp == false then + imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_peaceful")) + :tooltip("Peaceful", enable_tooltips) + end + + if x.creative then + imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_creative")) + :tooltip("Creative", enable_tooltips) + end + + if x.clients then + local clients = x.clients..( + x.clients_max and "/"..x.clients_max or "" + ) + local color = "#aaa" + if x.clients > 0 and x.clients_max then + local percent = x.clients /x.clients_max + if percent < 0.75 then + color = "#638b67" + elseif percent < 1 then + color = "#a69174" + else + color = "#9d5b5b" + end + end + imfs.hypertext(0, 0, 1, 0.5, ""..core.hypertext_escape(clients)) + end + + if x.ping or x.lag then + imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_icon_ping_"..ping_lvl)) + :tooltip("Ping: "..math.floor(lag), enable_tooltips) + end + + imfs.row_end() + + i = i + 1 + end + idx = idx + 1 + end + + imfs.scope_end() + + if loading then + imfs.hypertext(0, i * 0.5, "100%", 0.5, "Loading...") + end + + imfs.scroll_container_end() + + local detail = state.detail() + if detail then + imfs.box("65% - 0.05", 0, 0.1, "100%", theme.styles.container.border_color) + imfs.column("65% + 0.1", border_width, "35% - "..(border_width + 0.1), "100% - "..(border_width * 2)) + :gap(0.1) + imfs.hypertext(0, 0, "100%", 0.75, ""..core.hypertext_escape(detail.name or detail.address..":"..detail.port).."") + if detail.mods or detail.clients_list or detail.favorites then + imfs.row(0.1, 0, "100% - 0.2", 0.5) + if detail.mods then + imfs.button(0, 0, 0.5, 0.5) + :image(theme.icon("menu_servers_mods")) + :tooltip("Show mod list") + :onclick(function() + state.detail_view("mod_list") + end) + end + + if detail.clients_list then + imfs.button(0, 0, 0.5, 0.5) + :image(theme.icon("menu_servers_players")) + :tooltip("Show player list") + :onclick(function() + state.detail_view("player_list") + end) + end + + if detail.favorite then + imfs.button(0, 0, 0.5, 0.5) + :image(theme.icon("menu_servers_unfavorite")) + :tooltip("Remove from favorites") + :onclick(function() + detail.favorite = nil + remove_favorite_server(detail.address, detail.port) + -- Bump the serverlist to notify that we've changed it. + state.serverlist(state.serverlist) + end) + end + imfs.row_end() + end + imfs.hypertext(0.1, 0, "100% - 0.2", "1x", ""..core.hypertext_escape(detail.description or "")) + imfs.button(0, 0, "100%", 0.75, "Join server") + :onclick(function() + state.joining(detail) + end) + imfs.column_end() + end + + imfs.group_end() + + imfs.button("100% - 1", 0.25, 0.75, 0.75, "<") + :onclick(function() + show_meta_menu() + end) + + local joining = state.joining() + if joining then + imfs.box(0, 0, "100%", "100%", "#0008") + imfs.button(0, 0, "100%", "100%") + :styles(styles_no_background) + :onclick(function() + state.joining(false) + end) + + imfs.group("20%", "20%", "60%", "60%") + imfs.image(0, 0, "100%", "100%", theme.get_background_image("container")) + imfs.scroll_container(border_width + 0.1, border_width + 0.1, "100% - "..(border_width * 2 + 0.2), "100% - "..(border_width * 2 + 0.2)) + imfs.column(0, 0, "100%", 100) + :gap(0.1) + + if state.join_error() then + imfs.hypertext(0, 0, "100%", 0.75, ""..core.hypertext_escape(state.server_connection_error.msg)) + end + + if joining == true then + imfs.hypertext(0, 0, "100%", 0.75, "Connecting to server...") + imfs.group(0, 0, "100%", 1.05) + imfs.image(0, 0.3, "100%", "100% - 0.3", theme.get_background_image("field")) + imfs.label(0.2, 0.125, "Address") + imfs.field(0.1, 0.3, "70% - 0.2", "100% - 0.3") + :onchange(function(value) + state.address(value) + end) + :onenter(function() + join_server(state) + end) + imfs.box("70% - 0.05", 0.3, 0.1, "100% - 0.3", theme.styles.field.border_color) + imfs.label("70% + 0.2", 0.125, "Port") + imfs.field("70% + 0.1", 0.3, "30% - 0.2", "100% - 0.3") + :onchange(function(value) + state.port(value) + end) + :onenter(function() + join_server(state) + end) + imfs.group_end() + else + imfs.hypertext(0, 0, "100%", 0.75, "Connecting to "..core.hypertext_escape(joining.name or joining.address..":"..joining.port).."...") + end + + theme.field(0, 0, "100%", 0.75, "Username", state.username) + :onchange(function(value) + state.username(value) + end) + :onenter(function() + join_server(state) + end) + + theme.field(0, 0, "100%", 0.75, "Password", "") + :password() + :onchange(function(value) + state.password(value) + end) + :onenter(function() + join_server(state) + end) + + imfs.row(0, 0, "100%", 0.75) + :gap(0.1) + + imfs.button(0, 0, "1x", "100%", "Cancel") + :onclick(function() + state.joining(false) + end) + imfs.button(0, 0, "1x", "100%", "Join") + :onclick(function() + join_server(state) + end) + imfs.row_end() + imfs.column_end() + imfs.scroll_container_end() + imfs.group_end() + elseif state.detail_view() then + imfs.box(0, 0, "100%", "100%", "#0008") + imfs.button(0, 0, "100%", "100%") + :styles(styles_no_background) + :onclick(function() + state.detail_view(false) + end) + + imfs.image("20%", "20%", "60%", "60%", theme.get_background_image("container")) + imfs.group("20% + "..border_width, "20% + "..border_width, "60% - "..(border_width * 2), "60% - "..(border_width * 2)) + imfs.scroll_container(0, 0, "100%", "100% - 0.8") + :scrollbar("100% - 0.5", 0, 0.5, "100% - 0.8", "vertical") + local list = state.detail_view() == "mod_list" and detail.mods or detail.clients_list + for i = 0, #list - 1 do + imfs.button(0, i * 0.5, "100%", 0.5) + :style({ + border = false, + bgimg = "[fill:1x1:0,0:#fff", + bgimg_middle = 0, + bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt, + }) + :style("hovered", style_borderless_hovered) + :style("pressed", style_borderless_hovered) + -- This isn't just the button label because we want left-alignment. + imfs.arealabel(0.1, i * 0.5 + 0.125, "100%", 0.375, list[i + 1]) + end + imfs.scroll_container_end() + imfs.button(0, "100% - 0.75", "100%", 0.75, "Close") + :onclick(function() + state.detail_view(false) + end) + imfs.group_end() + end + + return imfs.end_() +end diff --git a/views/settings.lua b/views/settings.lua new file mode 100644 index 0000000..8e9ac9b --- /dev/null +++ b/views/settings.lua @@ -0,0 +1,271 @@ + +dofile(core.get_builtin_path().."common/settings/settingtypes.lua") + +-- Some categories, like Accessibility and Theme, do not appear +-- in the builtin settingtypes.txt, so they are added to the state table here. +local function inject_settings(state) + -- Theme + table.insert(state.categories, 1, "Theme") + state.settings.Theme = { + { + comment = "The background color of containers.", + context = "client", + default = "#fff", + name = "theme.container_bg_color", + readable_name = "Container background color", + requires = {}, + type = "string", + value = imfs.state("#fff") + } + } + + -- Accessibility + table.insert(state.categories, 2, "Accessibility") + state.settings.Accessibility = { + { + comment = "Smooths rotation of camera, also called look or mouse smoothing. 0 to disable.", + context = "client", + default = "0.0", + max = 0.99, + min = 0, + name = "camera_smoothing", + readable_name = "Camera smoothing", + requires = {}, + type = "float", + value = imfs.state("0.0") + } + } +end + +local function search_settings(state, search) + if state.search == "" then + return state.settings, state.categories + end + + local out = {} + for category, settings in pairs(state.settings) do + for i = 1, #settings do + if settings[i].name:find(search, 1, true) then + if not out[category] then + out[category] = {settings[i]} + else + table.insert(out[category], settings[i]) + end + end + end + end + + local categories = {} + for i = 1, #state.categories do + local category = state.categories[i] + if category.name then + for j = 1, #state.categories do + if out[category[j]] then + if not categories[category.name] then + table.insert(categories, {name = category.name}) + end + table.insert(categories[#categories], category[j]) + break + end + end + else + if out[category] then + table.insert(categories, category) + break + end + end + end + + return out, categories +end + +return function(state) + imfs.begin(size.x, size.y) + :padding(0, 0) + :bgcolor(theme.styles.bg_color, true) + + if not state.settings then + state.settings = {} + state.categories = {} + local category + local settings = settingtypes.parse_config_file(false, false) + for _, x in ipairs(settings) do + if x.type == "category" then + if x.level == 0 then + state.categories[#state.categories + 1] = {name = x.name} + elseif x.level == 1 then + local group = state.categories[#state.categories] + group[#group + 1] = x.name + state.settings[x.name] = {} + category = x.name + end + else + local s = state.settings[category] + x.value = imfs.state() + if x.type == "string" or x.type == "float" or x.type == "int" or x.type == "enum" then + x.value._val = minetest.settings:get(x.name) or x.default + elseif x.type == "bool" then + x.value._val = minetest.settings:get_bool(x.name, x.default) + end + s[#s +1] = x + end + end + + inject_settings(state) + + state.settings_shown._val = state.settings + state.categories_shown._val = state.categories + end + + imfs.box(0, 0, "100%", 1, theme.styles.container.bg_color) + + imfs.row(0.2, 0.125, "100% - 0.4", 0.75) + :gap(0.1) + theme.field(0, 0, "1x", "100%", state.search) + :onchange(function(value) + state.search = value + end) + :onenter(function() + local settings, categories = search_settings(state, state.search) + if categories[1] then + local first = categories[1].name and categories[1][1] or categories[1] + state.category(first) + end + state.categories_shown(categories) + state.settings_shown(settings) + end) + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("search")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Search settings") + :onclick(function() + local settings, categories = search_settings(state, state.search) + if categories[1] then + local first = categories[1].name and categories[1][1] or categories[1] + state.category(first) + end + state.categories_shown(categories) + state.settings_shown(settings) + end) + + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("cancel")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Clear search") + :onclick(function() + state.search = "" + state.settings_shown(state.settings) + state.categories_shown(state.categories) + end) + imfs.button(0, 0, 2, "100%", "Back") + :onclick(function() + show_meta_menu() + end) + imfs.row_end() + + imfs.box(0, 1, "20%", "100% - 1", theme.styles.container.bg_color) + imfs.box(0, 0.95, "100%", 0.1, theme.styles.container.border_color) + + imfs.scroll_container(0.2, 1.05, "20% - 0.45", "100% - 1.05") + local categories = state.categories_shown() + if categories then + local y = 0.1 + for i = 1, #categories do + local group = categories[i] + if not group then break end + -- Top-level categories. + if type(group) == "string" then + imfs.button(0, y, "100%", 0.5, group) + :onclick(function() + if state.category() ~= group then + state.category(group) + end + end) + y = y + 0.55 + -- Grouped categories. + else + imfs.arealabel(0, y + 0.125, "100%", 0.375, group.name) + y = y + 0.425 + local h = 0 + for j = 1, #group do + local category = group[j] + imfs.box(0.12, y + 0.245, 0.25, 0.01, theme.styles.text_color) + imfs.button(0.25, y, "100% - 0.25", 0.5, category) + :onclick(function() + if state.category() ~= category then + state.category(category) + end + end) + h = h + 0.55 + y = y + 0.55 + end + imfs.box(0.12, y - h, 0.01, h - 0.3, theme.styles.text_color) + end + end + end + imfs.scroll_container_end() + + imfs.box("20% - 0.05", 1, 0.1, "100% - 1", theme.styles.container.border_color) + + imfs.scroll_container("20% + 0.25", 1.05, "80% - 0.45", "100% - 1.05") + imfs.column(0, 0, "100%", 1000) + :gap(0.1) + local settings = state.settings_shown()[state.category()] + if settings then + for i = 1, #settings do + local setting = settings[i] + if not setting then break end + if setting.type == "bool" then + local active = minetest.settings:get_bool(setting.name, setting.default) + theme.checkbox(0, 0, setting.readable_name, setting.value, setting.comment) + elseif setting.type == "float" or setting.type == "int" then + imfs.arealabel(0, 0, "100%", 0.325, setting.readable_name) + imfs.row(0, 0, "100%", 0.75) + :gap(0.1) + theme.number_field(0, 0, "1x", 0.75, setting.value) + :tooltip(setting.comment) + imfs.button(0, 0, 2, 0.75, "Set") + imfs.row_end() + elseif setting.type == "string" then + imfs.arealabel(0, 0, "100%", 0.325, setting.readable_name) + imfs.row(0, 0, "100%", 0.75) + :gap(0.1) + theme.field(0, 0, "1x", 0.75, setting.value) + :tooltip(setting.comment) + if setting.value._val ~= setting.default then + imfs.button(0, 0, 0.75, 0.75) + :image(theme.icon("refresh")) + :style({ + bgimg = theme.get_background_image("button"), + bgimg_middle = theme.styles.button.border_width, + }) + :tooltip("Reset to default") + end + imfs.button(0, 0, 2, 0.75, "Set") + imfs.row_end() + elseif setting.type == "enum" then + imfs.scope(setting.name) + imfs.arealabel(0, 0, "100%", 0.325, setting.readable_name) + imfs.row(0, 0, "100%", "fit") + :gap(0.1) + theme.dropdown(0, 0, "1x", 0.75, setting.values, setting.value) + :tooltip(setting.comment) + imfs.button(0, 0, 2, 0.75, "Set") + imfs.row_end() + imfs.scope_end() + else + print(dump(setting)) + end + end + end + imfs.column_end() + imfs.scroll_container_end() + + return imfs.end_() +end \ No newline at end of file diff --git a/views/texturepacks.lua b/views/texturepacks.lua new file mode 100644 index 0000000..e69de29