Files
2026-06-07 22:53:58 +08:00

1301 lines
38 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---@type RuntimeUi
local runtime_ui = runtime.import("runtime_ui")
---@type RuntimeWidgets
local widgets = runtime.import("runtime_widgets")
local theme = runtime.import("theme")
local styles = runtime.import("styles")
local state = runtime.import("state")
local examples = runtime.import("examples")
local i18n = runtime.import("i18n")
---@class ShowcaseUi
local ui = {}
local base_preview_ids = {
"sample_rect",
"sample_circle",
"sample_line",
"sample_image_node",
"sample_sprite_node",
"sample_progress",
"widget_progress_label",
"widget_progress"
}
local text_preview_ids = {
"text_plain_title",
"text_plain_body",
"text_style_badge",
"text_rich_note"
}
local button_preview_ids = {
"button_hint",
"button_primary",
"button_secondary",
"button_disabled",
"button_state_text"
}
local button_image_preview_ids = {
"image_button_hint",
"image_button_normal",
"image_button_toggle",
"image_button_disabled",
"image_button_state_text"
}
local sprite_preview_ids = {
"sprite_image_demo",
"sprite_sprite_demo",
"sprite_frame_demo",
"sprite_label_demo"
}
local layout_preview_ids = {
"layout_canvas",
"layout_chip_1",
"layout_chip_2",
"layout_chip_3",
"layout_chip_4",
"layout_label"
}
local radio_preview_ids = {
"radio_title",
"radio_audio_dot",
"radio_audio_label",
"radio_spine_dot",
"radio_spine_label",
"radio_lua_dot",
"radio_lua_label",
"radio_value_text"
}
local list_preview_ids = {
"list_panel",
"list_row_1",
"list_row_2",
"list_row_3",
"list_row_4",
"list_row_5",
"list_row_1_text",
"list_row_2_text",
"list_row_3_text",
"list_row_4_text",
"list_row_5_text",
"list_value_text"
}
local particle_preview_ids = {
"particle_burst",
"particle_trail",
"particle_snow",
"particle_label"
}
local responsive_preview_ids = {
"responsive_info",
"responsive_device",
"responsive_sidebar",
"responsive_content"
}
---@return table
local function metrics()
local screen_w = theme.screen_width
local screen_h = theme.screen_height
local margin = 20
local gap = 16
local header_h = 82
local status_h = 30
local content_h = screen_h - header_h - status_h - margin
local compact = screen_w < 640
local menu_w = compact and 188 or 220
local detail_w = screen_w - margin * 2 - gap - menu_w
local content_y = header_h
local preview_h = 148
local action_y = content_y + content_h - preview_h - 54
local preview_y = content_y + content_h - preview_h - 10
local code_h = action_y - content_y - 150
if code_h < 128 then
code_h = 128
end
return {
screen_w = screen_w,
screen_h = screen_h,
margin = margin,
gap = gap,
compact = compact,
menu_x = margin,
menu_y = content_y,
menu_w = menu_w,
detail_x = margin + menu_w + gap,
detail_y = content_y,
detail_w = detail_w,
content_h = content_h,
action_y = action_y,
preview_y = preview_y,
preview_h = preview_h,
code_h = code_h,
status_y = screen_h - status_h - 8
}
end
---@param example ShowcaseExample
---@return boolean
local function preview_mode(example)
if example.id == "text_demo" then
return "text"
end
if example.id == "buttons" then
return "buttons"
end
if example.id == "button_images" then
return "button_images"
end
if example.id == "sprites" then
return "sprites"
end
if example.id == "layout_demo" then
return "layout"
end
if example.id == "radio_group" then
return "radio"
end
if example.id == "list_view" then
return "list"
end
if example.id == "particles" then
return "particle"
end
if example.id == "responsive" then
return "responsive"
end
return "base"
end
---@param nodes RuntimeNode[]
---@param text string
---@param y number
---@param m table
local function append_menu_group(nodes, text, y, m)
table.insert(nodes, widgets.label("group_" .. tostring(y), {
text = text,
x = 16,
y = y,
w = m.menu_w - 32,
h = 18,
parent = "example_list_panel",
color = theme.muted,
fontSize = 11,
layer = 30
}))
end
---@param nodes RuntimeNode[]
---@param example ShowcaseExample
---@param y number
---@param m table
local function append_menu_item(nodes, example, y, m)
local selected = example.id == state.selected_example
table.insert(nodes, widgets.list_item("example_" .. example.id, {
text = i18n.example_label(example),
x = 14,
y = y,
w = m.menu_w - 28,
h = 27,
handler = "select_example",
parent = "example_list_panel",
selected = selected,
activeColor = theme.primary,
inactiveColor = "#ff1e293b",
radius = 10,
fontSize = 11,
layer = 31
}))
end
---@param selected ShowcaseExample
---@param m table
---@return RuntimeNode[]
local function detail_action_nodes(selected, m)
local actions = {}
for index = 1, 3 do
local action = selected.actions[index]
actions[index] = {
id = "detail_action_" .. index,
text = action ~= nil and i18n.action_text(action) or "",
handler = action ~= nil and action.handler or "noop",
visible = action ~= nil,
color = index == 1 and "#ff2563eb" or "#ff334155"
}
end
return widgets.action_row("detail_action", actions, {
x = m.detail_x + 20,
y = m.action_y,
width = m.detail_w - 40,
itemHeight = 34,
gap = 12,
radius = 10,
fontSize = 12,
layer = 35
})
end
---@param example ShowcaseExample
---@return string
local function detail_body(example)
if state.detail_tab == "params" then
return i18n.example_params(example)
end
return example.code
end
---@return string
local function detail_copy_text()
if state.detail_tab == "params" then
return i18n.t("copy_params")
end
return i18n.t("copy_code")
end
---@param text string
---@return integer
local function line_count(text)
local value = text or ""
local count = 1
local start = 1
while true do
local index = string.find(value, "\n", start, true)
if index == nil then
return count
end
count = count + 1
start = index + 1
end
end
local detail_text_y
local detail_padding_left = 14
local detail_padding_top = 12
local detail_padding_right = 14
local detail_padding_bottom = 12
---@param m table
---@return number
local function detail_viewport_width(m)
return m.detail_w - 40 - detail_padding_left - detail_padding_right
end
---@param m table
---@return number
local function detail_viewport_height(m)
return m.code_h + 18 - detail_padding_top - detail_padding_bottom
end
---@param example ShowcaseExample
---@return number
local function detail_text_height(example)
return line_count(detail_body(example)) * 16 + 8
end
---@param example ShowcaseExample
---@param m table
---@return number
local function detail_content_height(example, m)
local estimated = detail_text_y(example, m) + detail_text_height(example) + 18
local viewport_h = detail_viewport_height(m)
if estimated < viewport_h then
return viewport_h
end
return estimated
end
---@param m table
---@return number
local function detail_content_width(m)
local viewport_w = detail_viewport_width(m)
if state.detail_tab == "params" then
return viewport_w
end
if line_count(detail_body(examples.find(state.selected_example))) <= 7 then
return viewport_w
end
return viewport_w * 1.45
end
---@param example ShowcaseExample
---@param m table
---@return number
local function detail_text_width(example, m)
local viewport_w = detail_viewport_width(m)
if state.detail_tab == "params" then
return viewport_w * 0.78
end
local content_w = detail_content_width(m) - 28
local centered_w = viewport_w * 0.78
if line_count(detail_body(example)) <= 7 then
return centered_w
end
return content_w
end
---@param example ShowcaseExample
---@param m table
---@return number
local function detail_text_x(example, m)
local viewport_w = detail_viewport_width(m)
local width = detail_text_width(example, m)
if state.detail_tab == "code" and width > viewport_w then
return 14
end
local x = (viewport_w - width) / 2
if x < 0 then
return 0
end
return x
end
---@param example ShowcaseExample
---@param m table
---@return number
detail_text_y = function(example, m)
return 0
end
---@return string
local function responsive_text()
local scale = state.viewport_scale or 1
local width = state.screen_width or theme.screen_width
local height = state.screen_height or theme.screen_height
local viewport_w = state.viewport_width or theme.screen_width
local viewport_h = state.viewport_height or theme.screen_height
return i18n.t("responsive_design") .. ": " .. tostring(theme.screen_width) .. "x" .. tostring(theme.screen_height)
.. " | " .. i18n.t("responsive_screen") .. ": " .. tostring(math.floor(width)) .. "x" .. tostring(math.floor(height))
.. " | " .. i18n.t("responsive_viewport") .. ": " .. tostring(math.floor(viewport_w)) .. "x" .. tostring(math.floor(viewport_h))
.. " @" .. string.format("%.2f", scale)
end
---@return number
local function responsive_device_width()
if state.responsive_mode == "phone" then
return 92
end
if state.responsive_mode == "tablet" then
return 124
end
return 148
end
---@param nodes RuntimeNode[]
---@param selected ShowcaseExample
local function append_preview_nodes(nodes, selected)
local mode = preview_mode(selected)
local base_visible = mode == "base"
local text_visible = mode == "text"
local button_visible = mode == "buttons"
local button_image_visible = mode == "button_images"
local sprite_visible = mode == "sprites"
local layout_visible = mode == "layout"
local radio_visible = mode == "radio"
local list_visible = mode == "list"
local particle_visible = mode == "particle"
local responsive_visible = mode == "responsive"
table.insert(nodes, runtime_ui.rect("sample_rect", 16, 48, 70, 44, {
parent = "preview_panel",
color = theme.primary,
radius = 8,
layer = 25,
visible = base_visible
}))
table.insert(nodes, runtime_ui.circle("sample_circle", 104, 42, 54, {
parent = "preview_panel",
color = theme.success,
interactive = true,
onTap = "demo_anim",
layer = 25,
visible = base_visible
}))
table.insert(nodes, runtime_ui.line("sample_line", 176, 70, 82, 0, {
parent = "preview_panel",
color = theme.warning,
strokeWidth = 4,
layer = 25,
visible = base_visible
}))
table.insert(nodes, runtime_ui.image("sample_image_node", "sample_image", 278, 42, 48, 48, {
parent = "preview_panel",
layer = 25,
visible = base_visible
}))
table.insert(nodes, runtime_ui.sprite("sample_sprite_node", "sample_image", 340, 42, 48, 48, {
parent = "preview_panel",
layer = 25,
visible = base_visible
}))
table.insert(nodes, runtime_ui.progress("sample_progress", 16, 112, 180, 16, state.progress, {
parent = "preview_panel",
color = theme.success,
radius = 8,
layer = 25,
visible = base_visible
}))
local progress_nodes = widgets.labeled_progress("widget_progress", "Widget Progress", 220, 98, 170, 12, state.progress, {
parent = "preview_panel",
labelStyle = { parent = "preview_panel", color = theme.muted, fontSize = 12, layer = 25, visible = base_visible },
color = theme.primary,
layer = 25,
visible = base_visible
})
runtime_ui.append_all(nodes, progress_nodes)
table.insert(nodes, runtime_ui.text("text_plain_title", state.text_variant == "plain" and "Text: 纯文本组件" or "Text: 样式已切换", 16, 46, 260, 24, {
parent = "preview_panel",
color = state.text_variant == "plain" and theme.text or theme.warning,
fontSize = state.text_variant == "plain" and 18 or 20,
layer = 25,
visible = text_visible
}))
table.insert(nodes, runtime_ui.text("text_plain_body", "字段text / color / fontSize / alpha", 16, 78, 310, 20, {
parent = "preview_panel",
color = theme.muted,
fontSize = 12,
layer = 25,
visible = text_visible
}))
table.insert(nodes, runtime_ui.rect("text_style_badge", 16, 108, 92, 24, {
parent = "preview_panel",
color = state.text_variant == "plain" and theme.primary or theme.purple,
radius = 8,
layer = 25,
visible = text_visible
}))
table.insert(nodes, runtime_ui.text("text_rich_note", "富文本:当前未支持 richText/spans", 124, 111, 240, 18, {
parent = "preview_panel",
color = theme.warning,
fontSize = 12,
layer = 26,
visible = text_visible
}))
table.insert(nodes, runtime_ui.text("button_hint", "button: text + background + onTap", 16, 44, 240, 20, {
parent = "preview_panel",
color = theme.muted,
fontSize = 12,
layer = 25,
visible = button_visible
}))
table.insert(nodes, runtime_ui.button("button_primary", "主按钮", 16, 76, 108, 34, "demo_button_primary", {
parent = "preview_panel",
color = state.button_active and theme.primary or "#ff475569",
radius = 10,
fontSize = 13,
layer = 26,
visible = button_visible
}))
table.insert(nodes, runtime_ui.button("button_secondary", "次按钮", 138, 76, 108, 34, "demo_button_primary", {
parent = "preview_panel",
color = "#ff334155",
radius = 10,
fontSize = 13,
layer = 26,
visible = button_visible
}))
table.insert(nodes, runtime_ui.button("button_disabled", "禁用态", 260, 76, 108, 34, "noop", {
parent = "preview_panel",
color = "#ff1f2937",
radius = 10,
fontSize = 13,
alpha = 0.55,
interactive = false,
layer = 26,
visible = button_visible
}))
table.insert(nodes, runtime_ui.text("button_state_text", state.button_active and "状态:可点击" or "状态:已置灰", 16, 118, 220, 18, {
parent = "preview_panel",
color = theme.warning,
fontSize = 12,
layer = 26,
visible = button_visible
}))
table.insert(nodes, runtime_ui.text("image_button_hint", "button image: normal / pressed / disabled", 16, 44, 300, 20, {
parent = "preview_panel",
color = theme.muted,
fontSize = 12,
layer = 25,
visible = button_image_visible
}))
table.insert(nodes, runtime_ui.button("image_button_normal", {
text = "按住 Pressed",
x = 16,
y = 76,
w = 116,
h = 40,
handler = "demo_button_image_tap",
parent = "preview_panel",
asset = "button_normal",
pressedAsset = "button_pressed",
disabledAsset = "button_disabled",
radius = 10,
fontSize = 12,
layer = 26,
visible = button_image_visible
}))
table.insert(nodes, runtime_ui.button("image_button_toggle", {
text = state.button_image_enabled and "可切换" or "已禁用",
x = 144,
y = 76,
w = 116,
h = 40,
handler = "demo_button_image_tap",
parent = "preview_panel",
asset = "button_normal",
pressedAsset = "button_pressed",
disabledAsset = "button_disabled",
radius = 10,
fontSize = 12,
interactive = state.button_image_enabled,
layer = 26,
visible = button_image_visible
}))
table.insert(nodes, runtime_ui.button("image_button_disabled", {
text = "Disabled",
x = 272,
y = 76,
w = 116,
h = 40,
handler = "noop",
parent = "preview_panel",
asset = "button_normal",
pressedAsset = "button_pressed",
disabledAsset = "button_disabled",
radius = 10,
fontSize = 12,
interactive = false,
layer = 26,
visible = button_image_visible
}))
table.insert(nodes, runtime_ui.text("image_button_state_text", state.button_image_enabled and "中间按钮interactive=true" or "中间按钮interactive=false显示 disabledAsset", 16, 122, 360, 18, {
parent = "preview_panel",
color = theme.warning,
fontSize = 12,
layer = 26,
visible = button_image_visible
}))
table.insert(nodes, runtime_ui.image("sprite_image_demo", "sample_image", 24, 54, 64, 64, {
parent = "preview_panel",
layer = 25,
visible = sprite_visible
}))
table.insert(nodes, runtime_ui.sprite("sprite_sprite_demo", "sample_image", 118, 54, 64, 64, {
parent = "preview_panel",
layer = 26,
visible = sprite_visible
}))
table.insert(nodes, runtime_ui.rect("sprite_frame_demo", 212, 54, 64, 64, {
parent = "preview_panel",
color = state.sprite_variant == "image" and theme.primary or theme.purple,
radius = 8,
layer = 25,
visible = sprite_visible
}))
table.insert(nodes, runtime_ui.text("sprite_label_demo", "image / sprite 都通过 manifest key 加载", 20, 120, 300, 18, {
parent = "preview_panel",
color = theme.muted,
fontSize = 12,
layer = 26,
visible = sprite_visible
}))
table.insert(nodes, runtime_ui.panel("layout_canvas", 16, 42, 360, 86, {
parent = "preview_panel",
color = "#ff111827",
radius = 10,
layer = 24,
visible = layout_visible
}))
table.insert(nodes, runtime_ui.rect("layout_chip_1", 34, 70, 58, 28, {
parent = "preview_panel",
color = theme.primary,
radius = 8,
layer = 26,
visible = layout_visible
}))
table.insert(nodes, runtime_ui.rect("layout_chip_2", 108, 70, 58, 28, {
parent = "preview_panel",
color = theme.success,
radius = 8,
layer = 26,
visible = layout_visible
}))
table.insert(nodes, runtime_ui.rect("layout_chip_3", 182, 70, 58, 28, {
parent = "preview_panel",
color = theme.warning,
radius = 8,
layer = 26,
visible = layout_visible
}))
table.insert(nodes, runtime_ui.rect("layout_chip_4", 256, 70, 58, 28, {
parent = "preview_panel",
color = theme.purple,
radius = 8,
layer = 26,
visible = layout_visible
}))
table.insert(nodes, runtime_ui.text("layout_label", "layout.rowgap + align + margin", 28, 112, 280, 18, {
parent = "preview_panel",
color = theme.muted,
fontSize = 12,
layer = 26,
visible = layout_visible
}))
table.insert(nodes, runtime_ui.text("radio_title", "RadioGroup: Lua 组合单选项", 16, 44, 260, 20, {
parent = "preview_panel",
color = theme.muted,
fontSize = 12,
layer = 25,
visible = radio_visible
}))
local radio_options = {
{ key = "audio", label = "Audio", y = 72 },
{ key = "spine", label = "Spine", y = 96 },
{ key = "Lua", label = "Lua", y = 120 }
}
for _, option in ipairs(radio_options) do
local selected_radio = state.radio_selected == option.key
table.insert(nodes, runtime_ui.circle("radio_" .. string.lower(option.key) .. "_dot", 18, option.y, 14, {
parent = "preview_panel",
color = selected_radio and theme.primary or "#ff475569",
layer = 25,
visible = radio_visible
}))
table.insert(nodes, runtime_ui.text("radio_" .. string.lower(option.key) .. "_label", option.label, 42, option.y - 2, 120, 18, {
parent = "preview_panel",
color = selected_radio and theme.text or theme.muted,
fontSize = 12,
layer = 25,
visible = radio_visible
}))
end
table.insert(nodes, runtime_ui.text("radio_value_text", "当前选择:" .. state.radio_selected, 190, 96, 160, 18, {
parent = "preview_panel",
color = theme.warning,
fontSize = 12,
layer = 25,
visible = radio_visible
}))
local list_horizontal = state.list_axis == "horizontal"
table.insert(nodes, runtime_ui.list_view("list_panel", 16, 42, 320, 72, {
parent = "preview_panel",
color = "#ff111827",
radius = 10,
contentWidth = list_horizontal and 650 or 430,
contentHeight = list_horizontal and 90 or 150,
scrollX = state.list_scroll_x,
scrollY = state.list_scroll_y,
virtualized = true,
cacheExtent = 24,
inertia = true,
onScroll = "demo_list_scrolled",
scrollbarThumbColor = theme.warning,
scrollbarTrackColor = "#33475569",
scrollbarThickness = 6,
layer = 24,
visible = list_visible
}))
local list_items = {
{ key = "Lua", label = "Lua 脚本层" },
{ key = "Runtime", label = "Runtime 协议层" },
{ key = "Flame", label = "Flame 渲染层" },
{ key = "Diff", label = "Diff 更新流" },
{ key = "Command", label = "Command 动作流" }
}
for index, item in ipairs(list_items) do
local selected_item = state.list_selected == item.key
local row_x = list_horizontal and (8 + (index - 1) * 126) or 8
local row_y = list_horizontal and 18 or (8 + (index - 1) * 28)
local row_w = list_horizontal and 116 or 292
table.insert(nodes, runtime_ui.button("list_row_" .. index, "", row_x, row_y, row_w, 24, "demo_list_pick_" .. index, {
parent = "list_panel",
color = selected_item and theme.primary or "#ff1e293b",
radius = 7,
layer = 25,
visible = list_visible
}))
table.insert(nodes, runtime_ui.text("list_row_" .. index .. "_text", item.label, 12, 4, 180, 16, {
parent = "list_row_" .. index,
color = theme.text,
fontSize = 11,
textAlign = "left",
layer = 26,
visible = list_visible
}))
end
table.insert(nodes, runtime_ui.text("list_value_text", state.list_axis .. " scrollX=" .. tostring(state.list_scroll_x) .. " scrollY=" .. tostring(state.list_scroll_y), 16, 120, 300, 16, {
parent = "preview_panel",
color = theme.warning,
fontSize = 11,
layer = 26,
visible = list_visible
}))
table.insert(nodes, runtime_ui.particle("particle_burst", 32, 42, 120, 92, {
parent = "preview_panel",
preset = "burst",
count = 42,
duration = 0.85,
color = "#ffffcc33",
colorTo = "#00ffcc33",
radius = 2.8,
speedMin = 60,
speedMax = 180,
gravityY = 90,
spread = 360,
autoRemove = false,
layer = 26,
visible = particle_visible
}))
table.insert(nodes, runtime_ui.particle("particle_trail", 168, 46, 120, 86, {
parent = "preview_panel",
preset = "trail",
count = 24,
duration = 0.7,
color = "#ff38bdf8",
radius = 2.2,
speedMin = 20,
speedMax = 80,
autoRemove = false,
layer = 26,
visible = particle_visible
}))
table.insert(nodes, runtime_ui.particle("particle_snow", 304, 34, 120, 104, {
parent = "preview_panel",
preset = "snow",
count = 56,
duration = 8,
color = "#ccffffff",
radius = 1.5,
autoRemove = false,
fadeOut = false,
layer = 26,
visible = particle_visible
}))
table.insert(nodes, runtime_ui.text("particle_label", "particle: burst / trail / snow", 16, 120, 280, 18, {
parent = "preview_panel",
color = theme.warning,
fontSize = 11,
layer = 27,
visible = particle_visible
}))
local width = responsive_device_width()
table.insert(nodes, runtime_ui.text("responsive_info", responsive_text(), 16, 112, 390, 18, {
parent = "preview_panel",
color = theme.muted,
fontSize = 10,
layer = 26,
visible = responsive_visible
}))
table.insert(nodes, runtime_ui.rect("responsive_device", 172, 44, width, 62, {
parent = "preview_panel",
color = "#ff1d4ed8",
radius = 10,
layer = 26,
visible = responsive_visible
}))
table.insert(nodes, runtime_ui.rect("responsive_sidebar", 184, 56, width * 0.26, 38, {
parent = "preview_panel",
color = theme.primary,
radius = 6,
layer = 27,
visible = responsive_visible
}))
table.insert(nodes, runtime_ui.rect("responsive_content", 192 + width * 0.26, 56, width * 0.56, 38, {
parent = "preview_panel",
color = theme.success,
radius = 6,
layer = 27,
visible = responsive_visible
}))
end
---@return RuntimeNode[]
function ui.create_nodes()
local nodes = {}
local selected = examples.find(state.selected_example)
local m = metrics()
table.insert(nodes, runtime_ui.rect("app_bg", 0, 0, theme.screen_width, theme.screen_height, {
color = theme.background,
layer = 0
}))
table.insert(nodes, runtime_ui.text("app_title", i18n.t("app_title"), 24, 18, 360, 34, styles.title))
table.insert(nodes, runtime_ui.text("app_subtitle", i18n.t("app_subtitle"), 24, 52, 660, 24, styles.label))
table.insert(nodes, runtime_ui.panel("example_list_panel", m.menu_x, m.menu_y, m.menu_w, m.content_h, styles.card))
table.insert(nodes, widgets.section_title("example_list_title", {
text = i18n.t("examples_title"),
x = 16,
y = 14,
w = m.menu_w - 32,
h = 24,
parent = "example_list_panel",
color = theme.text,
fontSize = 20,
layer = 25
}))
local menu_y = 44
local last_group = nil
for _, example in ipairs(examples.all()) do
local group = i18n.example_group(example)
if group ~= last_group then
append_menu_group(nodes, group, menu_y, m)
menu_y = menu_y + 15
last_group = group
end
append_menu_item(nodes, example, menu_y, m)
menu_y = menu_y + 32
end
table.insert(nodes, runtime_ui.panel("detail_panel", m.detail_x, m.detail_y, m.detail_w, m.content_h, styles.card))
runtime_ui.append_all(nodes, widgets.panel_header("detail", {
eyebrow = i18n.example_group(selected) .. " / " .. i18n.example_field(selected, "category"),
title = i18n.example_field(selected, "title"),
summary = i18n.example_field(selected, "summary"),
x = 20,
y = 16,
w = m.detail_w - 40,
parent = "detail_panel",
layer = 25,
gap = 4,
eyebrowHeight = 20,
titleHeight = 30,
summaryHeight = 24,
eyebrowId = "detail_category",
titleId = "detail_title",
summaryId = "detail_summary",
eyebrowStyle = { color = theme.primary, fontSize = 13 },
titleStyle = { color = theme.text, fontSize = 21 },
summaryStyle = { color = theme.muted, fontSize = 12 }
}))
runtime_ui.append_all(nodes, widgets.tabs("detail_tab", {
{ id = "detail_tab_code", key = "code", text = i18n.t("tab_code"), handler = "detail_tab_code" },
{ id = "detail_tab_params", key = "params", text = i18n.t("tab_params"), handler = "detail_tab_params" }
}, {
x = m.detail_x + 20,
y = m.detail_y + 104,
itemWidth = 72,
itemHeight = 24,
gap = 6,
selected = state.detail_tab,
activeColor = theme.primary,
inactiveColor = "#ff334155",
radius = 8,
fontSize = 11,
interactive = true,
layer = 90
}))
table.insert(nodes, widgets.text_button("detail_copy", {
text = detail_copy_text(),
x = m.detail_x + m.detail_w - 116,
y = m.detail_y + 104,
w = 96,
h = 24,
handler = "copy_detail",
color = "#ff475569",
radius = 8,
fontSize = 11,
interactive = true,
layer = 90
}))
table.insert(nodes, runtime_ui.list_view("code_panel", m.detail_x + 20, m.detail_y + 132, m.detail_w - 40, m.code_h + 18, {
color = "#ff020617",
radius = 12,
contentWidth = detail_content_width(m),
contentHeight = detail_content_height(selected, m),
scrollX = 0,
scrollY = 0,
paddingLeft = detail_padding_left,
paddingTop = detail_padding_top,
paddingRight = detail_padding_right,
paddingBottom = detail_padding_bottom,
virtualized = false,
inertia = true,
scrollbarThumbColor = theme.primary,
scrollbarTrackColor = "#33475569",
scrollbarThickness = 5,
layer = 20
}))
table.insert(nodes, runtime_ui.text("detail_code", detail_body(selected), detail_text_x(selected, m), detail_text_y(selected, m), detail_text_width(selected, m), detail_text_height(selected), {
parent = "code_panel",
color = "#ffe2e8f0",
fontSize = 11,
textAlign = "left",
layer = 25
}))
runtime_ui.append_all(nodes, detail_action_nodes(selected, m))
table.insert(nodes, runtime_ui.panel("preview_panel", m.detail_x + 20, m.preview_y, m.detail_w - 40, m.preview_h, {
color = "#ff0b1120",
radius = 12,
layer = 20
}))
table.insert(nodes, widgets.section_title("preview_title", {
text = i18n.t("preview_title"),
x = 14,
y = 10,
w = 220,
h = 22,
parent = "preview_panel",
color = theme.text,
fontSize = 15,
layer = 25
}))
append_preview_nodes(nodes, selected)
table.insert(nodes, runtime_ui.text("status_text", state.status, 24, m.status_y, theme.screen_width - 48, 26, {
color = theme.warning,
fontSize = 14,
layer = 50
}))
return nodes
end
---@param text string
---@return RuntimeNodeUpdate[]
function ui.status_updates(text)
state.status = text
return { runtime_ui.text_update("status_text", text) }
end
---@param selected ShowcaseExample
---@return RuntimeNodeUpdate[]
local function preview_visibility_updates(selected)
local updates = {}
local mode = preview_mode(selected)
local groups = {
{ ids = base_preview_ids, visible = mode == "base" },
{ ids = text_preview_ids, visible = mode == "text" },
{ ids = button_preview_ids, visible = mode == "buttons" },
{ ids = button_image_preview_ids, visible = mode == "button_images" },
{ ids = sprite_preview_ids, visible = mode == "sprites" },
{ ids = layout_preview_ids, visible = mode == "layout" },
{ ids = radio_preview_ids, visible = mode == "radio" },
{ ids = list_preview_ids, visible = mode == "list" },
{ ids = particle_preview_ids, visible = mode == "particle" },
{ ids = responsive_preview_ids, visible = mode == "responsive" }
}
for _, group in ipairs(groups) do
for _, id in ipairs(group.ids) do
table.insert(updates, runtime_ui.visible_update(id, group.visible))
end
end
return updates
end
---@param selected ShowcaseExample
---@return RuntimeNodeUpdate[]
function ui.example_updates(selected)
local m = metrics()
local updates = {
runtime_ui.text_update("detail_category", i18n.example_group(selected) .. " / " .. i18n.example_field(selected, "category")),
runtime_ui.text_update("detail_title", i18n.example_field(selected, "title")),
runtime_ui.text_update("detail_summary", i18n.example_field(selected, "summary")),
runtime_ui.text_update("detail_code", detail_body(selected)),
runtime_ui.update("detail_code", {
x = detail_text_x(selected, m),
y = detail_text_y(selected, m),
width = detail_text_width(selected, m),
height = detail_text_height(selected),
textAlign = "left"
}),
runtime_ui.update("code_panel", {
contentWidth = detail_content_width(m),
contentHeight = detail_content_height(selected, m),
scrollX = 0,
scrollY = 0
}),
runtime_ui.update("detail_tab_code", {
text = i18n.t("tab_code"),
onTap = "detail_tab_code",
interactive = true,
layer = 90,
color = state.detail_tab == "code" and theme.primary or "#ff334155"
}),
runtime_ui.update("detail_tab_params", {
text = i18n.t("tab_params"),
onTap = "detail_tab_params",
interactive = true,
layer = 90,
color = state.detail_tab == "params" and theme.primary or "#ff334155"
}),
runtime_ui.update("detail_copy", {
text = detail_copy_text(),
onTap = "copy_detail",
interactive = true,
layer = 90
}),
runtime_ui.text_update("status_text", i18n.t("selected_prefix") .. i18n.example_field(selected, "title"))
}
for _, example in ipairs(examples.all()) do
table.insert(updates, runtime_ui.update("example_" .. example.id, {
text = i18n.example_label(example),
color = example.id == selected.id and theme.primary or "#ff1e293b"
}))
end
for index = 1, 3 do
local action = selected.actions[index]
if action ~= nil then
table.insert(updates, runtime_ui.update("detail_action_" .. index, {
text = i18n.action_text(action),
onTap = action.handler,
visible = true,
color = index == 1 and "#ff2563eb" or "#ff334155"
}))
else
table.insert(updates, runtime_ui.update("detail_action_" .. index, {
text = "",
onTap = "noop",
visible = false
}))
end
end
local preview_updates = preview_visibility_updates(selected)
for _, update in ipairs(preview_updates) do
table.insert(updates, update)
end
table.insert(updates, runtime_ui.update("status_text", { width = m.screen_w - 48 }))
return updates
end
---@return RuntimeNodeUpdate[]
function ui.locale_updates()
local selected = examples.find(state.selected_example)
local updates = {
runtime_ui.text_update("app_title", i18n.t("app_title")),
runtime_ui.text_update("app_subtitle", i18n.t("app_subtitle")),
runtime_ui.text_update("example_list_title", i18n.t("examples_title")),
runtime_ui.text_update("preview_title", i18n.t("preview_title")),
runtime_ui.text_update("responsive_info", responsive_text())
}
local detail_updates = ui.example_updates(selected)
for _, update in ipairs(detail_updates) do
table.insert(updates, update)
end
return updates
end
---@return RuntimeNodeUpdate[]
function ui.responsive_updates()
local width = responsive_device_width()
return {
runtime_ui.text_update("responsive_info", responsive_text()),
runtime_ui.update("responsive_device", { width = width }),
runtime_ui.update("responsive_sidebar", { width = width * 0.26 }),
runtime_ui.update("responsive_content", {
x = 192 + width * 0.26,
width = width * 0.56
})
}
end
---@return RuntimeNodeUpdate[]
function ui.text_updates()
return {
runtime_ui.update("text_plain_title", {
text = state.text_variant == "plain" and "Text: 纯文本组件" or "Text: 样式已切换",
color = state.text_variant == "plain" and theme.text or theme.warning,
fontSize = state.text_variant == "plain" and 18 or 20
}),
runtime_ui.update("text_style_badge", {
color = state.text_variant == "plain" and theme.primary or theme.purple
})
}
end
---@return RuntimeNodeUpdate[]
function ui.radio_updates()
local updates = {
runtime_ui.text_update("radio_value_text", "当前选择:" .. state.radio_selected)
}
local options = { "audio", "spine", "lua" }
for _, key in ipairs(options) do
local value = key == "lua" and "Lua" or key
local selected = state.radio_selected == value
table.insert(updates, runtime_ui.update("radio_" .. key .. "_dot", {
color = selected and theme.primary or "#ff475569"
}))
table.insert(updates, runtime_ui.update("radio_" .. key .. "_label", {
color = selected and theme.text or theme.muted
}))
end
return updates
end
---@return RuntimeNodeUpdate[]
function ui.list_updates()
local horizontal = state.list_axis == "horizontal"
local updates = {
runtime_ui.update("list_panel", {
contentWidth = horizontal and 650 or 430,
contentHeight = horizontal and 90 or 150,
scrollX = state.list_scroll_x,
scrollY = state.list_scroll_y
}),
runtime_ui.text_update("list_value_text", state.list_axis .. " scrollX=" .. tostring(state.list_scroll_x) .. " scrollY=" .. tostring(state.list_scroll_y))
}
local items = { "Lua", "Runtime", "Flame", "Diff", "Command" }
for index, key in ipairs(items) do
local row_x = horizontal and (8 + (index - 1) * 126) or 8
local row_y = horizontal and 18 or (8 + (index - 1) * 28)
local row_w = horizontal and 116 or 292
table.insert(updates, runtime_ui.update("list_row_" .. index, {
x = row_x,
y = row_y,
width = row_w,
color = state.list_selected == key and theme.primary or "#ff1e293b"
}))
table.insert(updates, runtime_ui.update("list_row_" .. index .. "_text", {
textAlign = "left"
}))
end
return updates
end
---@return RuntimeNodeUpdate[]
function ui.button_updates()
return {
runtime_ui.update("button_primary", {
color = state.button_active and theme.primary or "#ff475569",
alpha = state.button_active and 1 or 0.65
}),
runtime_ui.text_update("button_state_text", state.button_active and "状态:可点击" or "状态:已置灰")
}
end
---@return RuntimeNodeUpdate[]
function ui.button_image_updates()
return {
runtime_ui.update("image_button_toggle", {
text = state.button_image_enabled and "可切换" or "已禁用",
interactive = state.button_image_enabled
}),
runtime_ui.text_update("image_button_state_text", state.button_image_enabled and "中间按钮interactive=true" or "中间按钮interactive=false显示 disabledAsset")
}
end
---@return RuntimeNodeUpdate[]
function ui.sprite_updates()
return {
runtime_ui.update("sprite_frame_demo", {
color = state.sprite_variant == "image" and theme.primary or theme.purple,
rotation = state.sprite_variant == "image" and 0 or 0.12
}),
runtime_ui.text_update("sprite_label_demo", state.sprite_variant == "image" and "当前image 原图节点" or "当前sprite 可动画节点")
}
end
---@param mode string
---@return RuntimeNodeUpdate[]
function ui.layout_updates(mode)
if mode == "column" then
return {
runtime_ui.update("layout_chip_1", { x = 44, y = 46 }),
runtime_ui.update("layout_chip_2", { x = 44, y = 74 }),
runtime_ui.update("layout_chip_3", { x = 44, y = 102 }),
runtime_ui.update("layout_chip_4", { x = 44, y = 130 }),
runtime_ui.text_update("layout_label", "layout.column纵向排列 + gap")
}
end
if mode == "box" then
return {
runtime_ui.update("layout_chip_1", { x = 44, y = 54 }),
runtime_ui.update("layout_chip_2", { x = 116, y = 54 }),
runtime_ui.update("layout_chip_3", { x = 44, y = 90 }),
runtime_ui.update("layout_chip_4", { x = 116, y = 90 }),
runtime_ui.text_update("layout_label", "layout.box2 行 × 2 列网格")
}
end
return {
runtime_ui.update("layout_chip_1", { x = 34, y = 70 }),
runtime_ui.update("layout_chip_2", { x = 108, y = 70 }),
runtime_ui.update("layout_chip_3", { x = 182, y = 70 }),
runtime_ui.update("layout_chip_4", { x = 256, y = 70 }),
runtime_ui.text_update("layout_label", "layout.row横向排列 + gap")
}
end
---@return RuntimeNodeUpdate[]
function ui.progress_updates()
return {
runtime_ui.update("sample_progress", { value = state.progress }),
runtime_ui.update("widget_progress", { value = state.progress })
}
end
---@return RuntimeNodeUpdate[]
function ui.visibility_updates()
return {
runtime_ui.visible_update("sample_rect", state.visible),
runtime_ui.alpha_update("sample_circle", state.visible and 1 or 0.35)
}
end
---@return RuntimeNode[]
function ui.dialog_nodes()
return widgets.dialog("sample_dialog", "Widget Dialog", "这是 Lua 侧组合组件overlay + card + text + button。输出仍然只是普通 RuntimeNode。", 150, 190, 420, 230, {
screenWidth = theme.screen_width,
screenHeight = theme.screen_height,
buttons = {
{ text = "播放音效", handler = "demo_sound" },
{ text = "关闭", handler = "close_dialog", color = theme.danger }
}
})
end
---@return { id: string }[]
function ui.dialog_removes()
return {
{ id = "sample_dialog_overlay" },
{ id = "sample_dialog" },
{ id = "sample_dialog_title" },
{ id = "sample_dialog_message" },
{ id = "sample_dialog_button_1" },
{ id = "sample_dialog_button_2" }
}
end
---@return RuntimeNode[]
function ui.temp_nodes()
local m = metrics()
return {
runtime_ui.rect("temp_node", m.detail_x + 270, m.preview_y + 98, 90, 28, {
color = theme.warning,
radius = 8,
layer = 45
}),
runtime_ui.text("temp_node_text", "临时节点", m.detail_x + 284, m.preview_y + 103, 72, 18, {
color = "#ff111827",
fontSize = 13,
layer = 46
})
}
end
return ui