14 Commits

Author SHA1 Message Date
Insality
633bb8ed5c Fix page 2025-06-30 01:48:37 +03:00
Insality
16a5d9936a Update for queue events 2025-05-28 23:21:00 +03:00
Insality
350770ba9e Fix for Druid containers mode 2025-05-28 23:20:48 +03:00
Insality
97942965cd Remove colors from rich text 2025-05-28 22:13:44 +03:00
Insality
0cf5ba30db Update properties panel 2025-05-27 23:10:49 +03:00
Insality
2e1f280944 Blocker by default is enabled, update color palette, add rich text split by characters option, able to pass a data to GO widgets 2025-05-27 23:10:32 +03:00
Insality
fe955b6e64 Update properties for panel 2025-05-18 13:04:38 +03:00
Insality
2133492efe Update properties panel, add refresh button 2025-05-17 18:06:26 +03:00
Insality
22c49540df Add scenes to properties panel 2025-05-17 13:36:34 +03:00
Insality
8ddb6e4e60 Open button_* functions to call a button callbacks directly 2025-05-15 23:28:32 +03:00
Insality
b982bc8277 Add dirty function to make node visible in scroll 2025-05-15 23:28:14 +03:00
Insality
d939d017cb Add loading palette to color module 2025-05-15 23:27:12 +03:00
Insality
488e78c9d7 Update layout annotations 2025-05-15 23:26:46 +03:00
Insality
52659a96ee Add button to drag in drag_to_node example 2025-05-15 23:26:35 +03:00
31 changed files with 521 additions and 108 deletions

View File

@@ -39,7 +39,7 @@ Open your `game.project` file and add the following lines to the dependencies fi
**[Druid](https://github.com/Insality/druid/)** **[Druid](https://github.com/Insality/druid/)**
``` ```
https://github.com/Insality/druid/archive/refs/tags/1.1.4.zip https://github.com/Insality/druid/archive/refs/tags/1.1.3.zip
``` ```
**[Defold Event](https://github.com/Insality/defold-event)** **[Defold Event](https://github.com/Insality/defold-event)**

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

@@ -67,8 +67,8 @@ function M:init(node_or_node_id, callback, custom_args, anim_node)
self.start_scale = gui.get_scale(self.anim_node) self.start_scale = gui.get_scale(self.anim_node)
self.start_pos = gui.get_position(self.anim_node) self.start_pos = gui.get_position(self.anim_node)
self.params = custom_args self.params = custom_args
self.hover = self.druid:new_hover(node_or_node_id, self._on_button_hover) self.hover = self.druid:new_hover(node_or_node_id, self.button_hover)
self.hover.on_mouse_hover:subscribe(self._on_button_mouse_hover) self.hover.on_mouse_hover:subscribe(self.button_mouse_hover)
self.click_zone = nil self.click_zone = nil
self.is_repeated_started = false self.is_repeated_started = false
self.last_pressed_time = 0 self.last_pressed_time = 0
@@ -184,7 +184,7 @@ function M:on_input(action_id, action)
if self._is_html5_mode then if self._is_html5_mode then
self._is_html5_listener_set = true self._is_html5_listener_set = true
html5.set_interaction_listener(function() html5.set_interaction_listener(function()
self:_on_button_click() self:button_click()
end) end)
end end
return is_consume return is_consume
@@ -193,7 +193,7 @@ function M:on_input(action_id, action)
-- While hold button, repeat rate pick from input.repeat_interval -- While hold button, repeat rate pick from input.repeat_interval
if action.repeated then if action.repeated then
if not self.on_repeated_click:is_empty() and self.can_action then if not self.on_repeated_click:is_empty() and self.can_action then
self:_on_button_repeated_click() self:button_repeated_click()
return is_consume return is_consume
end end
end end
@@ -211,7 +211,7 @@ function M:on_input(action_id, action)
end end
if press_time >= self.style.LONGTAP_TIME then if press_time >= self.style.LONGTAP_TIME then
self:_on_button_hold(press_time) self:button_hold(press_time)
return is_consume return is_consume
end end
end end
@@ -326,18 +326,18 @@ end
---@param hover_state boolean True if the hover state is active ---@param hover_state boolean True if the hover state is active
function M:_on_button_hover(hover_state) function M:button_hover(hover_state)
self.style.on_hover(self, self.anim_node, hover_state) self.style.on_hover(self, self.anim_node, hover_state)
end end
---@param hover_state boolean True if the hover state is active ---@param hover_state boolean True if the hover state is active
function M:_on_button_mouse_hover(hover_state) function M:button_mouse_hover(hover_state)
self.style.on_mouse_hover(self, self.anim_node, hover_state) self.style.on_mouse_hover(self, self.anim_node, hover_state)
end end
function M:_on_button_click() function M:button_click()
if self._is_html5_mode then if self._is_html5_mode then
self._is_html5_listener_set = false self._is_html5_listener_set = false
html5.set_interaction_listener(nil) html5.set_interaction_listener(nil)
@@ -348,7 +348,7 @@ function M:_on_button_click()
end end
function M:_on_button_repeated_click() function M:button_repeated_click()
if not self.is_repeated_started then if not self.is_repeated_started then
self.click_in_row = 0 self.click_in_row = 0
self.is_repeated_started = true self.is_repeated_started = true
@@ -360,7 +360,7 @@ function M:_on_button_repeated_click()
end end
function M:_on_button_long_click() function M:button_long_click()
self.click_in_row = 1 self.click_in_row = 1
local time = socket.gettime() - self.last_pressed_time local time = socket.gettime() - self.last_pressed_time
self.on_long_click:trigger(self:get_context(), self.params, self, time) self.on_long_click:trigger(self:get_context(), self.params, self, time)
@@ -368,7 +368,7 @@ function M:_on_button_long_click()
end end
function M:_on_button_double_click() function M:button_double_click()
self.click_in_row = self.click_in_row + 1 self.click_in_row = self.click_in_row + 1
self.on_double_click:trigger(self:get_context(), self.params, self, self.click_in_row) self.on_double_click:trigger(self:get_context(), self.params, self, self.click_in_row)
self.style.on_click(self, self.anim_node) self.style.on_click(self, self.anim_node)
@@ -376,7 +376,7 @@ end
---@param press_time number Amount of time the button was held ---@param press_time number Amount of time the button was held
function M:_on_button_hold(press_time) function M:button_hold(press_time)
self.on_hold_callback:trigger(self:get_context(), self.params, self, press_time) self.on_hold_callback:trigger(self:get_context(), self.params, self, press_time)
end end
@@ -413,14 +413,14 @@ function M:_on_button_release()
if is_long_click then if is_long_click then
local is_hold_complete = (time - self.last_pressed_time) >= self.style.AUTOHOLD_TRIGGER local is_hold_complete = (time - self.last_pressed_time) >= self.style.AUTOHOLD_TRIGGER
if is_hold_complete then if is_hold_complete then
self:_on_button_long_click() self:button_long_click()
else else
self.on_click_outside:trigger(self:get_context(), self.params, self) self.on_click_outside:trigger(self:get_context(), self.params, self)
end end
elseif is_double_click then elseif is_double_click then
self:_on_button_double_click() self:button_double_click()
else else
self:_on_button_click() self:button_click()
end end
self.last_released_time = time self.last_released_time = time

View File

@@ -205,6 +205,59 @@ function M:scroll_to(point, is_instant)
end end
function M:scroll_to_make_node_visible(node, is_instant)
-- Can be any node not only directly at scroll content
local screen_position = gui.get_screen_position(node)
local local_position = gui.screen_to_local(self.content_node, screen_position)
-- Get the node borders in content node space
local node_border = helper.get_border(node, local_position)
-- Calculate how much we need to scroll to make the node visible
local scroll_position = vmath.vector3(self.position)
local view_border = self.view_border
-- Convert content position to view position
local node_in_view_x = node_border.x + scroll_position.x
local node_in_view_y = node_border.y + scroll_position.y
local node_in_view_z = node_border.z + scroll_position.x
local node_in_view_w = node_border.w + scroll_position.y
local target_position = vmath.vector3(scroll_position)
-- Check if node is outside view horizontally
if self._is_horizontal_scroll then
-- If the node is too far to the right (left side not visible)
if node_in_view_x < view_border.x then
target_position.x = scroll_position.x + (view_border.x - node_in_view_x)
end
-- If the node is too far to the left (right side not visible)
if node_in_view_z > view_border.z then
target_position.x = scroll_position.x - (node_in_view_z - view_border.z)
end
end
-- Check if node is outside view vertically
if self._is_vertical_scroll then
-- If the node is too far up (bottom side not visible)
if node_in_view_w < view_border.w then
target_position.y = scroll_position.y + (view_border.w - node_in_view_w)
end
-- If the node is too far down (top side not visible)
if node_in_view_y > view_border.y then
target_position.y = scroll_position.y - (node_in_view_y - view_border.y)
end
end
-- If we need to scroll, do it
if target_position.x ~= scroll_position.x or target_position.y ~= scroll_position.y then
-- Convert to scroll_to expected format (content position, not scroll position)
local scroll_to_position = vmath.vector3(-target_position.x, -target_position.y, 0)
self:scroll_to(scroll_to_position, is_instant)
end
end
---Scroll to item in scroll by point index. ---Scroll to item in scroll by point index.
---@param index number Point index ---@param index number Point index
---@param skip_cb boolean|nil If true, skip the point callback ---@param skip_cb boolean|nil If true, skip the point callback

View File

@@ -8,9 +8,17 @@ 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
return PALETTE_DATA[color_id]
end
-- Check is it hex: starts with "#" or contains only 3 or 6 hex symbols -- Check is it hex: starts with "#" or contains only 3 or 6 hex symbols
if type(color_id) == "string" then if type(color_id) == "string" then
if string.sub(color_id, 1, 1) == "#" or string.match(color_id, "^[0-9a-fA-F]+$") then if string.sub(color_id, 1, 1) == "#" or string.match(color_id, "^[0-9a-fA-F]+$") then
@@ -18,7 +26,7 @@ function M.get_color(color_id)
end end
end end
return PALETTE_DATA[color_id] or COLOR_WHITE return COLOR_WHITE
end end
@@ -171,4 +179,16 @@ function M.rgb2hex(red, green, blue)
end end
local DEFAULT_PALETTE_PATH = sys.get_config_string("druid.palette_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
return
end
M.add_palette(data)
end
return M return M

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,7 +45,16 @@ 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
add_word(word .. " ", settings, words) 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)
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

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

View File

@@ -55,12 +55,13 @@ function M:init(node_or_node_id, layout_type)
self.entities = {} self.entities = {}
self.size = gui.get_size(self.node) self.size = gui.get_size(self.node)
-- Padding X is a Slice9 L Value
-- Padding Y is a Slice9 T Value
self.padding = gui.get_slice9(self.node) self.padding = gui.get_slice9(self.node)
-- Margin X is a Slice9 R Value -- Margin X is a Slice9 R Value
-- Margin Y is a Slice9 B Value -- Margin Y is a Slice9 B Value
self.margin = { x = self.padding.z, y = self.padding.w } self.margin = { x = self.padding.z, y = self.padding.w }
-- Padding X is a Slice9 L Value
-- Padding Y is a Slice9 T Value
self.padding.z = self.padding.x self.padding.z = self.padding.x
self.padding.w = self.padding.y self.padding.w = self.padding.y
@@ -123,10 +124,10 @@ function M:set_margin(margin_x, margin_y)
end end
---@param padding_x number|nil The padding x ---@param padding_x number|nil From Left
---@param padding_y number|nil The padding y ---@param padding_y number|nil From Top
---@param padding_z number|nil The padding z ---@param padding_z number|nil From Right
---@param padding_w number|nil The padding w ---@param padding_w number|nil From Bottom
---@return druid.layout self Current layout instance ---@return druid.layout self Current layout instance
function M:set_padding(padding_x, padding_y, padding_z, padding_w) function M:set_padding(padding_x, padding_y, padding_z, padding_w)
self.padding.x = padding_x or self.padding.x self.padding.x = padding_x or self.padding.x

View File

@@ -473,9 +473,6 @@ function M.get_text_metrics_from_node(text_node)
options.tracking = gui.get_tracking(text_node) options.tracking = gui.get_tracking(text_node)
options.line_break = gui.get_line_break(text_node) options.line_break = gui.get_line_break(text_node)
options.width = 0
options.leading = 0
-- Gather other options only if it used in node -- Gather other options only if it used in node
if options.line_break then if options.line_break then
options.width = gui.get_size(text_node).x options.width = gui.get_size(text_node).x

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,9 +147,41 @@ 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 = {}
self.current_page = 1
end end
@@ -139,26 +206,28 @@ end
function M:update(dt) function M:update(dt)
if self.is_dirty then if not self.is_dirty then
self.is_dirty = false return
end
self:clear_created_properties() self.is_dirty = false
local properties_count = #self.properties_constructors self:clear_created_properties()
-- Render all current properties local properties_count = #self.properties_constructors
local start_index = (self.current_page - 1) * self.properties_per_page + 1
local end_index = start_index + self.properties_per_page - 1
end_index = math.min(end_index, properties_count)
local is_paginator_visible = properties_count > self.properties_per_page -- Render all current properties
gui.set_enabled(self.paginator.root, is_paginator_visible) local start_index = (self.current_page - 1) * self.properties_per_page + 1
self.paginator:set_number_type(1, math.ceil(properties_count / self.properties_per_page), true) local end_index = start_index + self.properties_per_page - 1
self.paginator.text_value:set_text(self.current_page .. " / " .. math.ceil(properties_count / self.properties_per_page)) end_index = math.min(end_index, properties_count)
for index = start_index, end_index do local is_paginator_visible = properties_count > self.properties_per_page
self.properties_constructors[index]() gui.set_enabled(self.paginator.root, is_paginator_visible)
end self.paginator:set_number_type(1, math.ceil(properties_count / self.properties_per_page), true)
self.paginator.text_value:set_text(self.current_page .. " / " .. math.ceil(properties_count / self.properties_per_page))
for index = start_index, end_index do
self.properties_constructors[index]()
end end
end end
@@ -285,6 +354,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 +368,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 +381,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

@@ -11,6 +11,12 @@ function M:init()
self.drag = self.druid:new_drag("drag/root", self.on_drag) self.drag = self.druid:new_drag("drag/root", self.on_drag)
self.drag.on_drag_end:subscribe(self.on_drag_end) self.drag.on_drag_end:subscribe(self.on_drag_end)
self.druid:new_button("drag/root", function()
self.counter = self.counter - 1
gui.set_text(self.text_counter, self.counter)
self:on_drop_to_zone()
end)
-- Save start position for animation -- Save start position for animation
self.start_position = gui.get_position(self.drag.node) self.start_position = gui.get_position(self.drag.node)
end end

View File

@@ -14,7 +14,7 @@ update_frequency = 60
[project] [project]
title = Druid title = Druid
version = 1.1.4 version = 1.1.3
publisher = Insality publisher = Insality
developer = Maksim Tuprikov developer = Maksim Tuprikov
custom_resources = /example/locales custom_resources = /example/locales

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

View File

@@ -712,6 +712,3 @@ Please support me if you like this project! It will help me keep engaged to upda
#### Druid 1.1.3 #### Druid 1.1.3
- Fix for node_id of cloned nodes with `gui.clone_tree` - Fix for node_id of cloned nodes with `gui.clone_tree`
#### Druid 1.1.4
- [#312] Fix for text metrics issue if returned height is 0 sometimes