340 lines
8.7 KiB
Lua
340 lines
8.7 KiB
Lua
-- 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)
|
|
-- Live-apply to the current selection (a no-op if nothing is selected)
|
|
editor.set_color(state.rgb[1], state.rgb[2], state.rgb[3], state.rgb[4])
|
|
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)
|