Merge branch 'properties_panel' into develop

This commit is contained in:
Insality 2025-05-28 23:22:29 +03:00
commit 8fc5f4d144
23 changed files with 425 additions and 89 deletions

View File

@ -20,7 +20,7 @@ local M = component.create("blocker")
---@param node node|string The node to use as a blocker ---@param node node|string The node to use as a blocker
function M:init(node) function M:init(node)
self.node = self:get_node(node) self.node = self:get_node(node)
self._is_enabled = gui.is_enabled(self.node, true) self._is_enabled = true
end end

View File

@ -8,9 +8,13 @@ local M = {}
---Get color by string (hex or from palette) ---Get color by string (hex or from palette)
---@param color_id string Color id from palette or hex color ---@param color_id string|vector4 Color id from palette or hex color
---@return vector4 ---@return vector4
function M.get_color(color_id) function M.get_color(color_id)
if type(color_id) == "vector4" then
return color_id
end
if PALETTE_DATA[color_id] then if PALETTE_DATA[color_id] then
return PALETTE_DATA[color_id] return PALETTE_DATA[color_id]
end end
@ -175,21 +179,15 @@ function M.rgb2hex(red, green, blue)
end end
local load_palette_from_json = function(path) local DEFAULT_PALETTE_PATH = sys.get_config_string("druid.palette_path")
local data = sys.load_resource(path) if DEFAULT_PALETTE_PATH then
local loaded_palette = sys.load_resource(DEFAULT_PALETTE_PATH)
local data = loaded_palette and json.decode(loaded_palette)
if not data then if not data then
return return
end end
return json.decode(data) M.add_palette(data)
end
local DEFAULT_PALETTE_PATH = sys.get_config_string("druid.palette_path")
if DEFAULT_PALETTE_PATH then
local loaded_palette = load_palette_from_json(DEFAULT_PALETTE_PATH)
if loaded_palette and loaded_palette["default"] then
M.add_palette(loaded_palette["default"])
end
end end

View File

@ -20,20 +20,20 @@ local helper = require("druid.helper")
---@field _uid number ---@field _uid number
---@class druid.component ---@class druid.component
---@field druid druid.instance Druid instance to create inner components ---@field protected druid druid.instance Druid instance to create inner components
---@field init fun(self:druid.component, ...)|nil Called when component is created ---@field protected init fun(self:druid.component, ...)|nil Called when component is created
---@field update fun(self:druid.component, dt:number)|nil Called every frame ---@field protected update fun(self:druid.component, dt:number)|nil Called every frame
---@field on_remove fun(self:druid.component)|nil Called when component is removed ---@field protected on_remove fun(self:druid.component)|nil Called when component is removed
---@field on_input fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is triggered ---@field protected on_input fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is triggered
---@field on_input_interrupt fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is consumed before ---@field protected on_input_interrupt fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is consumed before
---@field on_message fun(self:druid.component, message_id:hash, message:table, sender:url)|nil Called when message is received ---@field protected on_message fun(self:druid.component, message_id:hash, message:table, sender:url)|nil Called when message is received
---@field on_late_init fun(self:druid.component)|nil Called before update once time after GUI init ---@field protected on_late_init fun(self:druid.component)|nil Called before update once time after GUI init
---@field on_focus_lost fun(self:druid.component)|nil Called when app lost focus ---@field protected on_focus_lost fun(self:druid.component)|nil Called when app lost focus
---@field on_focus_gained fun(self:druid.component)|nil Called when app gained focus ---@field protected on_focus_gained fun(self:druid.component)|nil Called when app gained focus
---@field on_style_change fun(self:druid.component, style: table)|nil Called when style is changed ---@field protected on_style_change fun(self:druid.component, style: table)|nil Called when style is changed
---@field on_layout_change fun(self:druid.component)|nil Called when GUI layout is changed ---@field protected on_layout_change fun(self:druid.component)|nil Called when GUI layout is changed
---@field on_window_resized fun(self:druid.component)|nil Called when window is resized ---@field protected on_window_resized fun(self:druid.component)|nil Called when window is resized
---@field on_language_change fun(self:druid.component)|nil Called when language is changed ---@field protected on_language_change fun(self:druid.component)|nil Called when language is changed
---@field private _component druid.component.component ---@field private _component druid.component.component
---@field private _meta druid.component.meta ---@field private _meta druid.component.meta
local M = {} local M = {}
@ -133,6 +133,7 @@ end
---Return current component context ---Return current component context
---@protected
---@return any context Usually it's self of script but can be any other Druid component ---@return any context Usually it's self of script but can be any other Druid component
function M:get_context() function M:get_context()
return self._meta.context return self._meta.context
@ -148,6 +149,7 @@ end
---Get Druid instance for inner component creation. ---Get Druid instance for inner component creation.
---@protected
---@param template string|nil ---@param template string|nil
---@param nodes table<hash, node>|node|string|nil The nodes table from gui.clone_tree or prefab node to use for clone or node id to clone ---@param nodes table<hash, node>|node|string|nil The nodes table from gui.clone_tree or prefab node to use for clone or node id to clone
---@return druid.instance ---@return druid.instance
@ -176,6 +178,7 @@ end
---Get parent component name ---Get parent component name
---@protected
---@return string|nil parent_name The parent component name if exist or nil ---@return string|nil parent_name The parent component name if exist or nil
function M:get_parent_name() function M:get_parent_name()
local parent = self:get_parent_component() local parent = self:get_parent_component()
@ -228,6 +231,7 @@ end
---Get component UID, unique identifier created in component creation order. ---Get component UID, unique identifier created in component creation order.
---@protected
---@return number uid The component uid ---@return number uid The component uid
function M:get_uid() function M:get_uid()
return self._component._uid return self._component._uid
@ -310,6 +314,7 @@ end
---Get current component nodes ---Get current component nodes
---@protected
---@return table<hash, node>|nil ---@return table<hash, node>|nil
function M:get_nodes() function M:get_nodes()
local nodes = self._meta.nodes local nodes = self._meta.nodes
@ -352,6 +357,7 @@ end
---Return all children components, recursive ---Return all children components, recursive
---@protected
---@return table Array of childrens if the Druid component instance ---@return table Array of childrens if the Druid component instance
function M:get_childrens() function M:get_childrens()
local childrens = {} local childrens = {}
@ -368,6 +374,7 @@ end
---Сreate a new component class, which will inherit from the base Druid component. ---Сreate a new component class, which will inherit from the base Druid component.
---@protected
---@param name string|nil The name of the component ---@param name string|nil The name of the component
---@param input_priority number|nil The input priority. The bigger number processed first. Default value: 10 ---@param input_priority number|nil The input priority. The bigger number processed first. Default value: 10
---@return druid.component ---@return druid.component

View File

@ -73,10 +73,8 @@ M.REVERSE_PIVOTS = {
M.LAYOUT_MODE = { M.LAYOUT_MODE = {
STRETCH_X = "stretch_x", STRETCH_X = "stretch_x",
STRETCH_Y = "stretch_y", STRETCH_Y = "stretch_y",
ZOOM_MIN = "zoom_min", FIT = "fit",
ZOOM_MAX = "zoom_max", STRETCH = "stretch",
FIT = gui.ADJUST_FIT,
STRETCH = gui.ADJUST_STRETCH,
} }
M.CURRENT_SYSTEM_NAME = sys.get_sys_info().system_name M.CURRENT_SYSTEM_NAME = sys.get_sys_info().system_name

View File

@ -181,6 +181,7 @@ function M.create(text, settings, style)
shadow = settings.shadow, shadow = settings.shadow,
outline = settings.outline, outline = settings.outline,
font = gui.get_font(settings.text_prefab), font = gui.get_font(settings.text_prefab),
split_to_characters = settings.split_to_characters,
-- Image params -- Image params
---@type druid.rich_text.word.image ---@type druid.rich_text.word.image
image = nil, image = nil,

View File

@ -29,6 +29,7 @@ local function add_word(text, settings, words)
end end
words[#words + 1] = data words[#words + 1] = data
return data
end end
@ -44,8 +45,17 @@ local function split_line(line, settings, words)
else else
local wi = #words local wi = #words
for word in trimmed_text:gmatch("%S+") do for word in trimmed_text:gmatch("%S+") do
if settings.split_to_characters then
for i = 1, #word do
local symbol = utf8.sub(word, i, i)
local w = add_word(symbol, settings, words)
w.nobr = true
end
add_word(" ", settings, words)
else
add_word(word .. " ", settings, words) add_word(word .. " ", settings, words)
end end
end
local first = words[wi + 1] local first = words[wi + 1]
first.text = ws_start .. first.text first.text = ws_start .. first.text
first.source_text = first.text first.source_text = first.text

View File

@ -2,7 +2,8 @@
-- Author: Britzl -- Author: Britzl
-- Modified by: Insality -- Modified by: Insality
local color = require("druid.custom.rich_text.module.rt_color") --local color = require("druid.custom.rich_text.module.rt_color")
local color = require("druid.color")
local M = {} local M = {}
local tags = {} local tags = {}
@ -43,20 +44,17 @@ end
-- Format: <color={COLOR_NAME}>{Text}</color> -- Format: <color={COLOR_NAME}>{Text}</color>
-- Example: <color=FF0000>Rich Text</color> -- Example: <color=FF0000>Rich Text</color>
M.register("color", function(params, settings, style) M.register("color", function(params, settings, style)
params = style.COLORS[params] or params settings.color = color.get_color(params)
settings.color = color.parse(params)
end) end)
M.register("shadow", function(params, settings, style) M.register("shadow", function(params, settings, style)
params = style.COLORS[params] or params settings.shadow = color.get_color(params)
settings.shadow = color.parse(params)
end) end)
M.register("outline", function(params, settings, style) M.register("outline", function(params, settings, style)
params = style.COLORS[params] or params settings.outline = color.get_color(params)
settings.outline = color.parse(params)
end) end)

View File

@ -13,6 +13,7 @@ local rich_text = require("druid.custom.rich_text.module.rt")
---@field image_pixel_grid_snap boolean ---@field image_pixel_grid_snap boolean
---@field combine_words boolean ---@field combine_words boolean
---@field default_animation string ---@field default_animation string
---@field split_by_character boolean
---@field text_prefab node ---@field text_prefab node
---@field adjust_scale number ---@field adjust_scale number
---@field default_texture string ---@field default_texture string
@ -50,7 +51,6 @@ local rich_text = require("druid.custom.rich_text.module.rt")
---@field height number ---@field height number
---@class druid.rich_text.style ---@class druid.rich_text.style
---@field COLORS table<string, vector4>
---@field ADJUST_STEPS number ---@field ADJUST_STEPS number
---@field ADJUST_SCALE_DELTA number ---@field ADJUST_SCALE_DELTA number
@ -104,7 +104,6 @@ end
---@param style druid.rich_text.style ---@param style druid.rich_text.style
function M:on_style_change(style) function M:on_style_change(style)
self.style = { self.style = {
COLORS = style.COLORS or {},
ADJUST_STEPS = style.ADJUST_STEPS or 20, ADJUST_STEPS = style.ADJUST_STEPS or 20,
ADJUST_SCALE_DELTA = style.ADJUST_SCALE_DELTA or 0.02, ADJUST_SCALE_DELTA = style.ADJUST_SCALE_DELTA or 0.02,
} }
@ -194,6 +193,15 @@ function M:tagged(tag)
end end
---Set if the rich text should split to characters, not words
---@param value boolean
---@return druid.rich_text self
function M:set_split_to_characters(value)
self._settings.split_to_characters = value
return self
end
---Get all current created words, each word is a table that contains the information about the word ---Get all current created words, each word is a table that contains the information about the word
---@return druid.rich_text.word[] ---@return druid.rich_text.word[]
function M:get_words() function M:get_words()
@ -239,6 +247,7 @@ function M:_create_settings()
outline = gui.get_outline(self.root), outline = gui.get_outline(self.root),
text_leading = gui.get_leading(self.root), text_leading = gui.get_leading(self.root),
is_multiline = gui.get_line_break(self.root), is_multiline = gui.get_line_break(self.root),
split_to_characters = false,
-- Image settings -- Image settings
image_pixel_grid_snap = false, -- disabled now image_pixel_grid_snap = false, -- disabled now

View File

@ -1,6 +1,6 @@
local component = require("druid.component") local component = require("druid.component")
local helper = require("druid.helper") local helper = require("druid.helper")
local defer = require("event.defer") local queues = require("event.queues")
---@class druid.tiling_node: druid.component ---@class druid.tiling_node: druid.component
---@field animation table ---@field animation table
@ -28,7 +28,7 @@ function M:init(node)
print("The druid.script is not found, please add it nearby to the GUI collection", msg.url()) print("The druid.script is not found, please add it nearby to the GUI collection", msg.url())
end) end)
defer.push("druid.get_atlas_path", { queues.push("druid.get_atlas_path", {
texture_name = gui.get_texture(self.node), texture_name = gui.get_texture(self.node),
sender = msg.url(), sender = msg.url(),
}, self.on_get_atlas_path, self) }, self.on_get_atlas_path, self)

View File

@ -28,4 +28,7 @@ images {
images { images {
image: "/druid/images/icons/icon_arrow.png" image: "/druid/images/icons/icon_arrow.png"
} }
images {
image: "/druid/images/icons/icon_refresh.png"
}
extrude_borders: 2 extrude_borders: 2

View File

@ -114,6 +114,12 @@ local function wrap_widget(widget)
end end
end end
for key, value in pairs(widget) do
if event.is_event(value) then
wrapped_widget[key] = value
end
end
return wrapped_widget return wrapped_widget
end end
@ -127,22 +133,21 @@ end
---@generic T: druid.widget ---@generic T: druid.widget
---@param widget_class T The class of the widget to return ---@param widget_class T The class of the widget to return
---@param gui_url url GUI url ---@param gui_url url GUI url
---@return T? widget The new created widget, ---@param params any|nil Additional parameters to pass to the widget's init function
function M.get_widget(widget_class, gui_url) ---@return T widget The new created widget,
function M.get_widget(widget_class, gui_url, params)
gui_url = gui_url or msg.url() gui_url = gui_url or msg.url()
local registered_druids = REGISTERED_GUI_WIDGETS[gui_url.socket] local registered_druids = REGISTERED_GUI_WIDGETS[gui_url.socket]
if not registered_druids then assert(registered_druids, "Druid widget not registered for this game object")
return nil
end
for index = 1, #registered_druids do for index = 1, #registered_druids do
local druid = registered_druids[index] local druid = registered_druids[index]
if druid.fragment == gui_url.fragment and druid.path == gui_url.path then if druid.fragment == gui_url.fragment and druid.path == gui_url.path then
return druid.new_widget:trigger(widget_class) return druid.new_widget:trigger(widget_class, nil, nil, params)
end end
end end
return nil error("Druid widget not found for this game object: " .. gui_url)
end end
@ -156,8 +161,8 @@ function M.register_druid_as_widget(druid)
table.insert(REGISTERED_GUI_WIDGETS[gui_url.socket], { table.insert(REGISTERED_GUI_WIDGETS[gui_url.socket], {
path = gui_url.path, path = gui_url.path,
fragment = gui_url.fragment, fragment = gui_url.fragment,
new_widget = event.create(function(widget_class) new_widget = event.create(function(widget_class, template, nodes, params)
return wrap_widget(druid:new_widget(widget_class)) return wrap_widget(druid:new_widget(widget_class, template, nodes, params))
end), end),
}) })
end end

View File

@ -3,7 +3,7 @@
-- This one is a required to make a unified "Shaders" pipeline in the GUI scripts -- This one is a required to make a unified "Shaders" pipeline in the GUI scripts
-- This required to grab a texture data with `go.get` function -- This required to grab a texture data with `go.get` function
local defer = require("event.defer") local queues = require("event.queues")
---Usage: defer.push("druid.get_atlas_path", { ---Usage: defer.push("druid.get_atlas_path", {
--- texture_name = gui.get_texture(self.node), --- texture_name = gui.get_texture(self.node),
@ -35,10 +35,10 @@ end
function init(self) function init(self)
defer.subscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self) queues.subscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self)
end end
function final(self) function final(self)
defer.unsubscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self) queues.unsubscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self)
end end

View File

@ -31,6 +31,8 @@ local CORNER_PIVOTS = {
---@field DRAGGABLE_CORNER_SIZE vector3 Size of box node for debug draggable corners ---@field DRAGGABLE_CORNER_SIZE vector3 Size of box node for debug draggable corners
---@field DRAGGABLE_CORNER_COLOR vector4 Color of debug draggable corners ---@field DRAGGABLE_CORNER_COLOR vector4 Color of debug draggable corners
---@alias druid.container.mode "stretch" | "fit" | "stretch_x" | "stretch_y"
---Druid component to manage the size and positions with other containers relations to create a adaptable layouts. ---Druid component to manage the size and positions with other containers relations to create a adaptable layouts.
--- ---
---### Setup ---### Setup
@ -54,7 +56,7 @@ local CORNER_PIVOTS = {
---@field position vector3 The current position ---@field position vector3 The current position
---@field pivot_offset vector3 The pivot offset ---@field pivot_offset vector3 The pivot offset
---@field center_offset vector3 The center offset ---@field center_offset vector3 The center offset
---@field mode string The layout mode ---@field mode druid.container.mode The layout mode
---@field fit_size vector3 The fit size ---@field fit_size vector3 The fit size
---@field min_size_x number|nil The minimum size x ---@field min_size_x number|nil The minimum size x
---@field min_size_y number|nil The minimum size y ---@field min_size_y number|nil The minimum size y

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

View File

@ -148,12 +148,4 @@ M["hotkey"] = {
} }
M["rich_text"] = {
COLORS = {
white = "#FFFFFF",
black = "#000000"
}
}
return M return M

View File

@ -1,5 +1,5 @@
---@class druid.widget: druid.component ---@class druid.widget: druid.component
---@field druid druid.instance Ready to use druid instance ---@field protected druid druid.instance Ready to use druid instance
---@class druid.logger ---@class druid.logger
---@field trace fun(message: string, context: any) ---@field trace fun(message: string, context: any)

View File

@ -681,7 +681,7 @@ end
local container = require("druid.extended.container") local container = require("druid.extended.container")
---Create Container component ---Create Container component
---@param node string|node The node_id or gui.get_node(node_id). ---@param node string|node The node_id or gui.get_node(node_id).
---@param mode string|nil Layout mode ---@param mode druid.container.mode|nil Layout mode. Default Fit or Stretch depends from node adjust mode from GUI scene
---@param callback fun(self: druid.container, size: vector3)|nil Callback on size changed ---@param callback fun(self: druid.container, size: vector3)|nil Callback on size changed
---@return druid.container container The new container component ---@return druid.container container The new container component
function M:new_container(node, mode, callback) function M:new_container(node, mode, callback)

View File

@ -6,7 +6,6 @@ local color = require("druid.color")
---@field text_name druid.text ---@field text_name druid.text
---@field button druid.button ---@field button druid.button
---@field text_button druid.text ---@field text_button druid.text
---@field druid druid.instance
local M = {} local M = {}
@ -51,6 +50,14 @@ function M:set_text_button(text)
end end
---@param enabled boolean
---@return druid.widget.property_button
function M:set_enabled(enabled)
self.button:set_enabled(enabled)
return self
end
function M:set_color(color_value) function M:set_color(color_value)
color.set_color(self:get_node("button"), color_value) color.set_color(self:get_node("button"), color_value)
end end

View File

@ -73,4 +73,11 @@ function M:on_change(callback)
end end
---Set the enabled state of the checkbox
---@param enabled boolean
function M:set_enabled(enabled)
self.button:set_enabled(enabled)
end
return M return M

View File

@ -101,6 +101,45 @@ nodes {
inherit_alpha: true inherit_alpha: true
size_mode: SIZE_MODE_AUTO size_mode: SIZE_MODE_AUTO
} }
nodes {
position {
x: 152.0
y: -4.0
}
color {
x: 0.306
y: 0.31
z: 0.314
}
type: TYPE_BOX
texture: "druid/icon_refresh"
id: "icon_refresh"
pivot: PIVOT_NE
parent: "header"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
x: 112.0
y: -4.0
}
scale {
x: -1.0
}
color {
x: 0.306
y: 0.31
z: 0.314
}
type: TYPE_BOX
texture: "druid/icon_arrow"
id: "icon_back"
pivot: PIVOT_NW
parent: "header"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes { nodes {
position { position {
y: -50.0 y: -50.0

View File

@ -1,3 +1,7 @@
local event = require("event.event")
local color = require("druid.color")
local helper = require("druid.helper")
local property_checkbox = require("druid.widget.properties_panel.properties.property_checkbox") local property_checkbox = require("druid.widget.properties_panel.properties.property_checkbox")
local property_slider = require("druid.widget.properties_panel.properties.property_slider") local property_slider = require("druid.widget.properties_panel.properties.property_slider")
local property_button = require("druid.widget.properties_panel.properties.property_button") local property_button = require("druid.widget.properties_panel.properties.property_button")
@ -21,6 +25,8 @@ local property_vector3 = require("druid.widget.properties_panel.properties.prope
---@field properties_constructors fun()[] List of properties functions to create a new widget. Used to not spawn non-visible widgets but keep the reference ---@field properties_constructors fun()[] List of properties functions to create a new widget. Used to not spawn non-visible widgets but keep the reference
local M = {} local M = {}
local COLOR_BUTTON = "#4E4F50"
local COLOR_REFRESH_ACTIVE = "#8BD092"
function M:init() function M:init()
self.root = self:get_node("root") self.root = self:get_node("root")
@ -34,6 +40,9 @@ function M:init()
self.default_size = self.container:get_size() self.default_size = self.container:get_size()
-- To have ability to go back to previous scene, collections of all properties to rebuild
self.scenes = {}
self.properties = {} self.properties = {}
self.properties_constructors = {} self.properties_constructors = {}
self.current_page = 1 self.current_page = 1
@ -52,6 +61,15 @@ function M:init()
self:set_hidden(not self._is_hidden) self:set_hidden(not self._is_hidden)
end):set_style(nil) end):set_style(nil)
self.button_back = self.druid:new_button("icon_back", function()
self:previous_scene()
end)
gui.set_enabled(self.button_back.node, false)
self.button_refresh = self.druid:new_button("icon_refresh", function()
self:toggle_auto_refresh()
end)
-- We not using as a part of properties, since it handled in a way to be paginable -- We not using as a part of properties, since it handled in a way to be paginable
self.paginator = self.druid:new_widget(property_left_right_selector, "property_left_right_selector", "root") self.paginator = self.druid:new_widget(property_left_right_selector, "property_left_right_selector", "root")
self.paginator:set_text("Page") self.paginator:set_text("Page")
@ -80,6 +98,23 @@ function M:on_remove()
end end
function M:toggle_auto_refresh()
self._is_auto_refresh = not self._is_auto_refresh
if self._is_auto_refresh then
self.is_dirty = true
color.set_color(self.button_refresh.node, COLOR_REFRESH_ACTIVE)
self._timer_refresh = timer.delay(1, true, function()
self.is_dirty = true
end)
else
color.set_color(self.button_refresh.node, COLOR_BUTTON)
timer.cancel(self._timer_refresh)
self._timer_refresh = nil
end
end
function M:on_drag_widget(dx, dy) function M:on_drag_widget(dx, dy)
local position = self.container:get_position() local position = self.container:get_position()
self.container:set_position(position.x + dx, position.y + dy) self.container:set_position(position.x + dx, position.y + dy)
@ -112,6 +147,37 @@ function M:clear_created_properties()
end end
function M:next_scene()
local scene = {
header = self.text_header:get_text(),
current_page = self.current_page,
}
helper.add_array(scene, self.properties_constructors)
table.insert(self.scenes, scene)
self:clear()
self.is_dirty = true
gui.set_enabled(self.button_back.node, #self.scenes > 0)
end
function M:previous_scene()
local scene = table.remove(self.scenes)
self:clear()
helper.add_array(self.properties_constructors, scene)
self.text_header:set_text(scene.header)
self.current_page = scene.current_page
self.is_dirty = true
gui.set_enabled(self.button_back.node, #self.scenes > 0)
end
function M:clear() function M:clear()
self:clear_created_properties() self:clear_created_properties()
self.properties_constructors = {} self.properties_constructors = {}
@ -139,7 +205,10 @@ end
function M:update(dt) function M:update(dt)
if self.is_dirty then if not self.is_dirty then
return
end
self.is_dirty = false self.is_dirty = false
self:clear_created_properties() self:clear_created_properties()
@ -160,7 +229,6 @@ function M:update(dt)
self.properties_constructors[index]() self.properties_constructors[index]()
end end
end end
end
---@param on_create fun(checkbox: druid.widget.property_checkbox)|nil ---@param on_create fun(checkbox: druid.widget.property_checkbox)|nil
@ -285,6 +353,12 @@ function M:remove(widget)
end end
---Force to refresh properties next update
function M:set_dirty()
self.is_dirty = true
end
function M:set_hidden(is_hidden) function M:set_hidden(is_hidden)
self._is_hidden = is_hidden self._is_hidden = is_hidden
local hidden_size = gui.get_size(self:get_node("header")) local hidden_size = gui.get_size(self:get_node("header"))
@ -293,6 +367,11 @@ function M:set_hidden(is_hidden)
self.container:set_size(new_size.x, new_size.y, gui.PIVOT_N) self.container:set_size(new_size.x, new_size.y, gui.PIVOT_N)
gui.set_enabled(self.content, not self._is_hidden) gui.set_enabled(self.content, not self._is_hidden)
gui.set_enabled(self.button_refresh.node, not self._is_hidden)
if not self._is_hidden then
self.is_dirty = true
end
end end
@ -301,16 +380,183 @@ function M:is_hidden()
end end
function M:load_previous_page()
self.current_page = self.current_page - 1
self.is_dirty = true
end
---@param properties_per_page number ---@param properties_per_page number
function M:set_properties_per_page(properties_per_page) function M:set_properties_per_page(properties_per_page)
self.properties_per_page = properties_per_page self.properties_per_page = properties_per_page
end end
---Set a page of current scene
---@param page number
function M:set_page(page) function M:set_page(page)
self.current_page = page self.current_page = page
self.is_dirty = true self.is_dirty = true
end end
---Set a text at left top corner of the properties panel
---@param header string
function M:set_header(header)
self.text_header:set_text(header)
end
---@param data table
function M:render_lua_table(data)
local component_order = {}
for component_id in pairs(data) do
table.insert(component_order, component_id)
end
table.sort(component_order, function(a, b)
local a_type = type(data[a])
local b_type = type(data[b])
if a_type ~= b_type then
return a_type < b_type
end
if type(a) == "number" and type(b) == "number" then
return a < b
end
return tostring(a) < tostring(b)
end)
for i = 1, #component_order do
local component_id = component_order[i]
self:add_property_component(component_id, data)
end
local metatable = getmetatable(data)
if metatable and metatable.__index and type(metatable.__index) == "table" then
local metatable_order = {}
for key in pairs(metatable.__index) do
table.insert(metatable_order, key)
end
table.sort(metatable_order)
for i = 1, #metatable_order do
local component_id = metatable_order[i]
local component = metatable.__index[component_id]
self:add_property_component("M:" .. component_id, data)
end
end
end
---@private
---@param component_id string
---@param data table
function M:add_property_component(component_id, data)
local component = data[component_id]
local component_type = type(component)
if component_type == "table" then
local is_event = event.is_event(component)
if is_event then
self:add_button(function(button)
button:set_text_property(tostring(component_id))
button:set_text_button("Call Event (" .. #component .. ")")
button.button.on_click:subscribe(function()
component:trigger()
end)
end)
else
self:add_button(function(button)
local is_empty = next(component) == nil
local is_array = component[1] ~= nil
local name = "Inspect"
if is_empty then
name = "Inspect (Empty)"
end
if is_array then
name = "Inspect (" .. #component .. ")"
end
local button_name = component_id
-- If it's a number or array, try to get the id/name/prefab_id from the component
if type(component) == "table" and type(component_id) == "number" then
local extracted_id = component.name or component.prefab_id or component.node_id or component.id
if extracted_id then
button_name = component_id .. ". " .. extracted_id
end
end
button:set_text_property(button_name)
button:set_text_button(name)
button.button.on_click:subscribe(function()
self:next_scene()
self:set_header(button_name)
self:render_lua_table(component)
end)
end)
end
end
if component_type == "string" then
self:add_input(function(input)
input:set_text_property(tostring(component_id))
input:set_text_value(tostring(data[component_id]))
input:on_change(function(_, value)
data[component_id] = value
end)
end)
end
if component_type == "number" then
self:add_input(function(input)
input:set_text_property(tostring(component_id))
input:set_text_value(tostring(helper.round(data[component_id], 3)))
input:on_change(function(_, value)
data[component_id] = tonumber(value)
end)
end)
end
if component_type == "boolean" then
self:add_checkbox(function(checkbox)
checkbox:set_text_property(tostring(component_id))
checkbox:set_value(data[component_id])
checkbox:on_change(function(value)
data[component_id] = value
end)
end)
end
if component_type == "userdata" then
if types.is_vector3(component) then
---@cast component vector3
self:add_vector3(function(vector3)
vector3:set_text_property(tostring(component_id))
vector3:set_value(data[component_id].x, data[component_id].y, data[component_id].z)
vector3.on_change:subscribe(function(value)
data[component_id].x = value.x
data[component_id].y = value.y
data[component_id].z = value.z
end)
end)
else
self:add_text(function(text)
text:set_text_property(tostring(component_id))
text:set_text_value(tostring(data[component_id]))
end)
end
end
if component_type == "function" then
self:add_button(function(button)
button:set_text_property(tostring(component_id))
button:set_text_button("Call")
button.button.on_click:subscribe(function()
component(data)
end)
end)
end
end
return M return M

View File

@ -0,0 +1,14 @@
profiles {
name: "Landscape"
qualifiers {
width: 1920
height: 1080
}
}
profiles {
name: "Portrait"
qualifiers {
width: 1080
height: 1920
}
}

View File

@ -81,7 +81,7 @@ return function()
local rich_text = druid:new_rich_text(text_node) local rich_text = druid:new_rich_text(text_node)
-- Test color tag with named color -- Test color tag with named color
local words = rich_text:set_text("<color=red>Colored Text</color>") local words = rich_text:set_text("<color=#FF0000>Colored Text</color>")
assert(#words > 0) assert(#words > 0)
-- Word should have a tags field with color tag -- Word should have a tags field with color tag
@ -104,7 +104,7 @@ return function()
local rich_text = druid:new_rich_text(text_node) local rich_text = druid:new_rich_text(text_node)
-- Test shadow tag with named color -- Test shadow tag with named color
local words = rich_text:set_text("<shadow=black>Shadowed Text</shadow>") local words = rich_text:set_text("<shadow=#000000>Shadowed Text</shadow>")
assert(#words > 0) assert(#words > 0)
assert(words[1].shadow ~= nil) assert(words[1].shadow ~= nil)
@ -129,7 +129,7 @@ return function()
local rich_text = druid:new_rich_text(text_node) local rich_text = druid:new_rich_text(text_node)
-- Test outline tag with named color -- Test outline tag with named color
local words = rich_text:set_text("<outline=black>Outlined Text</outline>") local words = rich_text:set_text("<outline=#000000>Outlined Text</outline>")
assert(#words > 0) assert(#words > 0)
assert(words[1].outline ~= nil) assert(words[1].outline ~= nil)
@ -228,7 +228,7 @@ return function()
local rich_text = druid:new_rich_text(text_node) local rich_text = druid:new_rich_text(text_node)
-- Test combined tags -- Test combined tags
local words = rich_text:set_text("<color=red><size=2>Big Red Text</size></color>") local words = rich_text:set_text("<color=#FF0000><size=2>Big Red Text</size></color>")
assert(#words > 0) assert(#words > 0)
assert(words[1].tags.color) assert(words[1].tags.color)
@ -236,7 +236,7 @@ return function()
assert(words[1].relative_scale == 2) assert(words[1].relative_scale == 2)
-- Test nested tags -- Test nested tags
words = rich_text:set_text("<color=red>Red <size=2>Big Red</size> Red</color>") words = rich_text:set_text("<color=#FF0000>Red <size=2>Big Red</size> Red</color>")
assert(#words >= 3) assert(#words >= 3)
-- All words should have color tag -- All words should have color tag