-- Editor UI controller. Loaded once at startup; owns the mode label -- (always present) and the color picker (created/destroyed with the mode), -- both driven by the editor.mode property. z controls stacking within an -- overlay; equal z means don't overlap. -- ============================== mode label ============================== local label_overlay = ui.create_overlay{anchor = ANCHOR_TOP_LEFT, size = {160, 40}} local label = ui.text{ pos = {0, 32}, size = 32, color = {1, 1, 1, 1}, font = 0, max_length = 8, z = 0, } label:set_text(app.get("editor.mode") or "None") -- ============================== color picker ============================= -- Elements/state are outer locals, assigned (not re-declared) each time -- open_picker() runs, so the shared on_button etc. below always see -- whichever picker is currently open. local picker_overlay = nil local bg, sv_square, hue_bar, sv_outline, sv_select local hex_area, hue_select, hex_text, slots, slot_index, drag local state local function clamp01(x) return math.max(0, math.min(1, x)) end local function update_hex_string() state.hex_string = string.format("#%02X%02X%02X%02X", math.floor(state.rgb[1]*255 + 0.5), math.floor(state.rgb[2]*255 + 0.5), math.floor(state.rgb[3]*255 + 0.5), math.floor(state.rgb[4]*255 + 0.5)) hex_text:set_text(state.hex_string) -- Publish for C and other overlays; only ever called with a complete -- color, so editor.color never holds a half-typed hex string app.set("editor.color", state.hex_string) end local function sync_rgb_from_hsv() state.rgb[1], state.rgb[2], state.rgb[3] = ui.hsv_to_rgb(state.hsv[1], state.hsv[2], state.hsv[3]) update_hex_string() end local function sv_pick(s, v) s = clamp01(s) v = clamp01(v) state.hsv[2] = s state.hsv[3] = v sv_select:set_pos(s*130 - 2, 130 - v*130 - 2) for corner = 0, 3 do sv_select:set_corner_color(corner, state.hsv[1], s, v, 1) end sv_outline:set_pos(s*130 - 3, 130 - v*130 - 3) sync_rgb_from_hsv() end local function hue_set(h) h = clamp01(h) state.hsv[1] = h for _, element in ipairs({sv_square, sv_select}) do for corner = 0, 3 do local _, s, v, a = element:corner_color(corner) element:set_corner_color(corner, h, s, v, a) end end hue_select:set_pos(134, 2 + h*130) sync_rgb_from_hsv() end local function hex_string_highlight(value) for corner = 0, 3 do local r, g, _, _ = hex_area:corner_color(corner) hex_area:set_corner_color(corner, r, g, value, value) end end local function set_saved(index, color) state.saved[index] = {color[1], color[2], color[3], color[4]} local slot = slots[index] for corner = 0, 3 do local _, _, _, a = slot:corner_color(corner) slot:set_corner_color(corner, color[1], color[2], color[3], a) end end -- Sync HSV+visuals from state.rgb; used after setting state.rgb directly. local function apply_state_rgb() state.hsv[1], state.hsv[2], state.hsv[3] = ui.rgb_to_hsv(state.rgb[1], state.rgb[2], state.rgb[3]) hue_set(state.hsv[1]) sv_pick(state.hsv[2], state.hsv[3]) end local function open_picker() if picker_overlay then return end picker_overlay = ui.create_overlay{anchor = ANCHOR_BOTTOM_LEFT, size = {190, 150}} bg = ui.rect{ pos = {0, 0}, size = {190, 150}, color = {0.4, 0.4, 0.4, 0.8}, z = 0, } sv_square = ui.rect{ type = RECT_HSV, pos = {2, 2}, size = {130, 130}, colors = {{0, 0, 1, 1}, {0, 1, 1, 1}, {0, 0, 0, 1}, {0, 1, 0, 1}}, events = EVENT_BUTTON, z = 1, } hue_bar = ui.rect{ type = RECT_HSV, pos = {134, 2}, size = {10, 130}, colors = {{0, 1, 1, 1}, {0, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}, events = EVENT_BUTTON + EVENT_SCROLL, z = 1, } sv_outline = ui.rect{ pos = {130-4, 130-4}, size = {7, 7}, color = {0, 0, 0, 1}, z = 2, } sv_select = ui.rect{ type = RECT_HSV, pos = {130-3, 130-3}, size = {5, 5}, color = {1, 0, 0, 1}, z = 3, } hex_area = ui.rect{ pos = {20, 134}, size = {95, 15}, events = EVENT_BUTTON + EVENT_CURSOR, z = 1, } hue_select = ui.rect{ pos = {134, 2}, size = {10, 1}, color = {0, 0, 0, 1}, z = 2, } hex_text = ui.text{ pos = {2, 150}, size = 16, color = {1, 1, 1, 1}, font = 0, max_length = 16, z = 2, } -- 12 saved color slots in a 2-wide grid slots = {} slot_index = {} for i = 1, 12 do local slot = ui.rect{ pos = {146 + ((i-1) % 2)*22, 2 + math.floor((i-1)/2)*22}, size = {20, 20}, color = {0, 0, 0, 1}, events = EVENT_BUTTON, z = 1, } slots[i] = slot slot_index[slot] = i end state = { hsv = {0, 0, 0}, rgb = {0, 0, 0, 1}, saved = {}, hex_string = "#000000FF", } for i = 1, 12 do state.saved[i] = {0, 0, 0, 0} end drag = { [sv_square] = function(x, y) sv_pick(x, 1 - y) end, [hue_bar] = function(x, y) hue_set(y) end, } -- adopt the current published color so reopening the picker doesn't -- reset the selection local published = app.get("editor.color") if published then for i = 1, 4 do state.rgb[i] = (tonumber(published:sub(2*i, 2*i + 1), 16) or 0) / 255 end end apply_state_rgb() end local function close_picker() if not picker_overlay then return end picker_overlay:destroy() picker_overlay = nil bg, sv_square, hue_bar, sv_outline, sv_select = nil, nil, nil, nil, nil hex_area, hue_select, hex_text, slots, slot_index, drag = nil, nil, nil, nil, nil, nil state = nil end -- ============================== dispatch ================================ -- label_overlay has no handlers; every handler below only ever fires for -- picker_overlay, but still checks since a shared script_env would call the -- same on_button etc. for any future overlay this script owns. function on_button(overlay, element, x, y, button, action, mods) if overlay ~= picker_overlay then return false end if drag[element] then if action == PRESS and button == MOUSE_LEFT then element:focus() drag[element](x, y) elseif action == RELEASE and button == MOUSE_LEFT then ui.blur() end return true elseif element == hex_area then if action == PRESS and button == MOUSE_LEFT then element:focus() hex_string_highlight(1) end return true elseif slot_index[element] then local index = slot_index[element] if action == PRESS then if button == MOUSE_LEFT then local saved = state.saved[index] state.rgb = {saved[1], saved[2], saved[3], saved[4]} apply_state_rgb() elseif button == MOUSE_RIGHT then set_saved(index, state.rgb) elseif button == MOUSE_MIDDLE then set_saved(index, {0, 0, 0, 0}) end end return true end return false end function on_cursor(overlay, element, x, y) if overlay ~= picker_overlay then return false end for el, fn in pairs(drag) do if el:is_focused() then fn(x, y) return true end end return false end function on_scroll(overlay, element, x, y) if overlay ~= picker_overlay then return false end if element == hue_bar then hue_set(state.hsv[1] + y*0.01) return true end return false end function on_key(overlay, element, key, action, mods) if overlay ~= picker_overlay or element ~= hex_area then return false end if action == PRESS then if key == KEY_ESCAPE then sync_rgb_from_hsv() ui.blur() elseif key == KEY_ENTER then local s = state.hex_string for i = 1, 4 do state.rgb[i] = (tonumber(s:sub(2*i, 2*i + 1), 16) or 0) / 255 end apply_state_rgb() ui.blur() elseif key == KEY_BACKSPACE then if #state.hex_string > 1 then state.hex_string = state.hex_string:sub(1, -2) hex_text:set_text(state.hex_string) end end end return true end function on_text(overlay, element, codepoint) if overlay ~= picker_overlay or element ~= hex_area then return false end if codepoint < 128 then local ch = string.char(codepoint):upper() if ch:match("[0-9A-F]") and #state.hex_string < 9 then state.hex_string = state.hex_string .. ch hex_text:set_text(state.hex_string) end end return true end function on_deselect(overlay, element) if overlay ~= picker_overlay then return end if element == hex_area then hex_string_highlight(0) end end -- ============================== mode tracking ============================ app.subscribe("engine.changed.editor.mode", function(source, mode) label:set_text(mode) if mode == "Vertex" or mode == "Neighbor" or mode == "Hex" then open_picker() else close_picker() end end)