-- gui_yaml.lua -- Parses a YAML-like table (pre-parsed by a YAML lib) into GUI elements. -- Usage: local yaml = require("tinyyaml") (or lyaml, etc.) -- local def = yaml.parse(yaml_string) -- local root = gui_yaml.build(gui, def) local gui_yaml = {} -- ───────────────────────────────────────────── -- Helpers -- ───────────────────────────────────────────── local function parseColor(v) if type(v) == "string" then return require("gui.core.color").new(v) elseif type(v) == "table" then -- {r, g, b} or {r, g, b, a} — values 0-255 or 0-1 local r, g, b, a = v[1], v[2], v[3], v[4] or 255 -- normalise if in 0-255 range if r > 1 or g > 1 or b > 1 then r, g, b = r/255, g/255, b/255 if a > 1 then a = a/255 end end return {r, g, b, a} end return nil end local function parseDualDim(def) --[[ YAML formats accepted: pos: [x, y] offset size: [w, h] offset scale-pos: [sx, sy] scale (0-1) scale-size: [sw, sh] scale (0-1) Or shorthand flat: x, y, w, h, sx, sy, sw, sh ]] local px = def.x or (def.pos and def.pos[1]) or 0 local py = def.y or (def.pos and def.pos[2]) or 0 local pw = def.w or def.width or (def.size and def.size[1]) or 0 local ph = def.h or def.height or (def.size and def.size[2]) or 0 local sx = def.sx or (def["scale-pos"] and def["scale-pos"][1]) or 0 local sy = def.sy or (def["scale-pos"] and def["scale-pos"][2]) or 0 local sw = def.sw or (def["scale-size"] and def["scale-size"][1]) or 0 local sh = def.sh or (def["scale-size"] and def["scale-size"][2]) or 0 return px, py, pw, ph, sx, sy, sw, sh end local function applyShared(parent, obj, def) -- Color / border if def.color then obj.color = parseColor(def.color) end if def["border-color"] then obj.borderColor = parseColor(def["border-color"]) end if def["draw-border"] ~= nil then obj.drawBorder = def["draw-border"] end -- Visibility if def.visible ~= nil then obj.visible = def.visible end if def.active ~= nil then obj.active = def.active end if def.visibility ~= nil then obj.visibility = def.visibility end -- Rotation if def.rotation then obj.rotation = def.rotation end -- Tag if def.tag then obj:tag(def.tag) end -- Tags (multi) if def.tags then for _, t in ipairs(def.tags) do obj:setTag(t) end end -- Form factor if def.form then local f = def.form if f == "circle" then local x, y, w, h, sx, sy, sw = parseDualDim(def) local r = def.radius or (w / 2) obj:makeCircle(x, y, r, sx, sy, sw, def.segments) elseif f == "arc" then local x, y, w, h, sx, sy, sw = parseDualDim(def) local r = def.radius or (w / 2) obj:makeArc( def["arc-type"] or "open", x, y, r, sx, sy, sw, def["angle-start"] or 0, def["angle-end"] or math.pi * 2, def.segments ) end end -- Roundness if def.roundness then local r = def.roundness if type(r) == "table" then obj:setRoundness(r[1], r[2], r[3], r.side) elseif type(r) == "string" then -- "top" | "bottom" shorthand obj:setRoundness(5, 5, 30, r) else obj:setRoundness(r, r, 30) end end -- Centering if def["center-x"] then obj:centerX(def["center-x"]) end if def["center-y"] then obj:centerY(def["center-y"]) end -- Full frame shorthand if def["full-frame"] then obj:fullFrame() end -- Square lock if def.square then obj.square = def.square end -- Dragging if def.draggable then obj:enableDragging( type(def.draggable) == "number" and def.draggable or 1 ) end -- Hierarchy if def["respect-hierarchy"] ~= nil then obj:respectHierarchy(def["respect-hierarchy"]) end -- Clip descendants if def["clip-descendants"] ~= nil then obj.clipDescendants = def["clip-descendants"] end -- Effects (function reference by name — looked up via _G or a registry) if def.effect then local fn = type(def.effect) == "function" and def.effect or _G[def.effect] if fn then obj.effect = fn end end -- Shader (name → looked up in _G) if def.shader then obj.shader = type(def.shader) == "userdata" and def.shader or _G[def.shader] end -- Position on stack if def.stack then if def.stack == "top" then obj:topStack() end if def.stack == "bottom" then obj:bottomStack() end end end local function applyEvents(obj, def, env) --[[ Events in YAML can be: on-pressed: "myFunction" -- looks up _G or env on-pressed: | print("hello") -- raw Lua string, loaded as chunk ]] local function resolve(v) if type(v) == "function" then return v end if type(v) == "string" then -- Try global lookup first if _G[v] and type(_G[v]) == "function" then return _G[v] end -- Otherwise treat as Lua source if env then return env[v] end end end local map = { ["on-pressed"] = "OnPressed", ["on-released"] = "OnReleased", ["on-released-outer"] = "OnReleasedOuter", ["on-pressed-outer"] = "OnPressedOuter", ["on-enter"] = "OnEnter", ["on-exit"] = "OnExit", ["on-moved"] = "OnMoved", ["on-drag-start"] = "OnDragStart", ["on-dragging"] = "OnDragging", ["on-drag-end"] = "OnDragEnd", ["on-wheel"] = "OnWheelMoved", ["on-size-changed"] = "OnSizeChanged", ["on-position-changed"] = "OnPositionChanged", ["on-destroy"] = "OnDestroy", ["on-load"] = "OnLoad", ["on-return"] = "OnReturn", -- textbox only } for yaml_key, conn_key in pairs(map) do if def[yaml_key] and obj[conn_key] then local fn = resolve(def[yaml_key]) if fn then obj[conn_key](fn) end end end -- on-update is special (not a connection) if def["on-update"] then local fn = resolve(def["on-update"]) if fn then obj:OnUpdate(fn) end end -- Hotkeys if def.hotkeys then for _, hk in ipairs(def.hotkeys) do -- {keys: [lctrl, s], action: "mySaveFunction"} local fn = resolve(hk.action) if fn then obj:setHotKey(hk.keys)(fn) end end end end local function applyTextProps(obj, def) if def.text then obj.text = tostring(def.text) end if def["text-color"] then obj.textColor = parseColor(def["text-color"]) end if def["text-visibility"] then obj.textVisibility = def["text-visibility"] end if def["text-scale"] then obj.textScaleX = def["text-scale"][1] or 1 obj.textScaleY = def["text-scale"][2] or 1 end if def["text-offset"] then obj.textOffsetX = def["text-offset"][1] or 0 obj.textOffsetY = def["text-offset"][2] or 0 end if def["text-shear"] then obj.textShearingFactorX = def["text-shear"][1] or 0 obj.textShearingFactorY = def["text-shear"][2] or 0 end -- Alignment local alignMap = {left = 1, center = 0, right = 2} if def.align then obj.align = alignMap[def.align] or 1 end -- Font if def.font then local f = def.font if type(f) == "number" then obj:setFont(f) elseif type(f) == "string" then obj:setFont(f, def["font-size"]) elseif type(f) == "table" then -- {file: "fonts/roboto.ttf", size: 18} obj:setFont(f.file or f[1], f.size or f[2]) end end -- fit-font: true | {min: 8, max: 200, scale: 1} if def["fit-font"] then local ff = def["fit-font"] if ff == true then obj:fitFont() elseif type(ff) == "table" then obj:fitFont(ff.min, ff.max, ff.scale and {scale=ff.scale} or nil) end end -- center-font: true | offset if def["center-font"] then local cf = def["center-font"] obj:centerFont(type(cf) == "number" and cf or nil) end end local function applyImageProps(obj, def) -- source: "path/to/image.png" -- tile: [x, y, w, h] (optional sub-quad) if def.source then if def.tile then local t = def.tile obj:setImage(def.source, t[1], t[2], t[3], t[4]) else obj:setImage(def.source) end end if def["scale-x"] then obj.scaleX = def["scale-x"] end if def["scale-y"] then obj.scaleY = def["scale-y"] end if def["image-color"] then obj.imageColor = parseColor(def["image-color"]) end if def["image-visibility"] then obj.imageVisibility = def["image-visibility"] end -- flip: "horizontal" | "vertical" | "both" if def.flip then local fl = def.flip if fl == "horizontal" or fl == "both" then obj:flip(false) end if fl == "vertical" or fl == "both" then obj:flip(true) end end -- gradient shorthand if def.gradient then local g = def.gradient -- {direction: "vertical", colors: [[r,g,b,a], ...]} local colors = {} for _, c in ipairs(g.colors) do colors[#colors+1] = parseColor(c) end obj:applyGradient(g.direction or "vertical", table.unpack(colors)) end end local function applyVideoProps(obj, def) if def.source then obj:setVideo(def.source) end if def.volume then obj:setVolume(def.volume) end if def.autoplay and def.autoplay then obj:play() end if def["video-color"] then obj.videoColor = parseColor(def["video-color"]) end if def["video-visibility"] then obj.videoVisibility = def["video-visibility"] end end -- ───────────────────────────────────────────── -- Core builder -- ───────────────────────────────────────────── local builders -- forward ref for recursion builders = { ["frame"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newFrame(x,y,w,h,sx,sy,sw,sh) end, ["virtual-frame"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newVirtualFrame(x,y,w,h,sx,sy,sw,sh) end, ["visual-frame"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newVisualFrame(x,y,w,h,sx,sy,sw,sh) end, ["label"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newTextLabel(def.text or "", x,y,w,h,sx,sy,sw,sh) end, ["button"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newTextButton(def.text or "", x,y,w,h,sx,sy,sw,sh) end, ["textbox"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newTextBox(def.text or "", x,y,w,h,sx,sy,sw,sh) end, ["image"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newImageLabel(def.source, x,y,w,h,sx,sy,sw,sh) end, ["image-button"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newImageButton(def.source, x,y,w,h,sx,sy,sw,sh) end, ["video"] = function(parent, def) local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) return parent:newVideo(def.source, x,y,w,h,sx,sy,sw,sh) end, } local TEXT_TYPES = {label=true, button=true, textbox=true} local IMAGE_TYPES = {image=true, ["image-button"]=true} local VIDEO_TYPES = {video=true} function gui_yaml.build(parent, def, env) --[[ parent : gui element or gui root def : parsed YAML table (one element) env : optional Lua env table for event string resolution Returns the created object (or nil on unknown type). ]] local typ = def.type if not typ then error("gui_yaml: element missing 'type' field") end local builder = builders[typ] if not builder then error("gui_yaml: unknown element type '" .. tostring(typ) .. "'") end local obj = builder(parent, def) -- Apply shared properties applyShared(parent, obj, def) -- Apply type-specific properties if TEXT_TYPES[typ] then applyTextProps(obj, def) end if IMAGE_TYPES[typ] then applyImageProps(obj, def) end if VIDEO_TYPES[typ] then applyVideoProps(obj, def) end -- Events applyEvents(obj, def, env) -- Recurse into children if def.children then for _, child_def in ipairs(def.children) do gui_yaml.build(obj, child_def, env) end end return obj end function gui_yaml.buildMany(parent, defs, env) local results = {} for _, def in ipairs(defs) do results[#results+1] = gui_yaml.build(parent, def, env) end return results end -- Convenience: parse a YAML string and build in one call. -- Requires a YAML library. Tries tinyyaml, then lyaml. function gui_yaml.fromString(parent, yaml_str, env) local ok, yaml = pcall(require, "gui.yaml.tinyyaml") if not ok then ok, yaml = pcall(require, "lyaml") if not ok then error("gui_yaml.fromString: no YAML library found (tried tinyyaml, lyaml)") end end local def = yaml.parse(yaml_str) -- Support both single-element and list-of-elements at root if def.type then return gui_yaml.build(parent, def, env) else return gui_yaml.buildMany(parent, def, env) end end return gui_yaml