45 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
cc818e4c6e Improve druid.color API with better organization and clearer function names
Co-authored-by: Insality <3294627+Insality@users.noreply.github.com>
2025-08-20 14:43:42 +00:00
copilot-swe-agent[bot]
92db5319a7 Initial plan 2025-08-20 14:33:44 +00:00
Insality
b6a7df5ff2 Update 2025-08-11 23:59:46 +03:00
Insality
a94121a1c6 Update properties panel 2025-08-10 00:56:08 +03:00
Insality
097963bdb3 Annotations update 2025-08-10 00:55:38 +03:00
Insality
574f764559 Add layout set_position_function 2025-08-08 18:22:03 +03:00
Insality
379c9acfc3 Add memory fps panel manual 2025-07-05 13:35:16 +03:00
Insality
4c8796e17d Merge branch 'master' into develop 2025-07-02 21:52:18 +03:00
Insality
ba7ee40510 Release 1.1.5 2025-07-02 21:51:43 +03:00
Insality
d019247ae4 Update for event v12 2025-07-02 21:50:48 +03:00
Insality
5dac7ed8e2 Merge branch 'properties_panel' into develop 2025-07-02 21:43:25 +03:00
Insality
633bb8ed5c Fix page 2025-06-30 01:48:37 +03:00
Insality
b35b584fad Merge branch 'master' into develop 2025-06-19 20:28:56 +03:00
Insality
616c513fbd Use tagged releases of dependencies 2025-06-19 20:28:45 +03:00
Insality
3606c9f49f Fix color check 2025-06-19 20:24:57 +03:00
Insality
a8aea0adee Merge branch 'master' into develop 2025-05-28 23:28:31 +03:00
Insality
8fde964e0e Revert "Merge pull request #311 from rigo128/fix_get_screen_aspect_koef"
This reverts commit 9a0f341a67, reversing
changes made to 8ddb6e4e60.

Revert until fully investigated
2025-05-28 23:28:03 +03:00
Insality
7b5264aca0 Update changelog 2025-05-28 23:27:45 +03:00
Insality
8fc5f4d144 Merge branch 'properties_panel' into develop 2025-05-28 23:22:29 +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
61111536a8 Release 1.1.4 2025-05-20 00:51:34 +03:00
Maksim Tuprikov
b5d2f313cc Merge pull request #312 from polivanni/master
Reset width and leading of the reusable table TEXT_METRICS_OPTIONS by default
2025-05-20 00:50:09 +03:00
Ivan Polovyi
d0067e5496 Reset width and leading of the reusable table TEXT_METRICS_OPTIONS by default 2025-05-18 23:33:40 +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
Maksim Tuprikov
9a0f341a67 Merge pull request #311 from rigo128/fix_get_screen_aspect_koef
Fix helper.get_screen_aspect_koef() for gui layouts.
2025-05-15 23:33:07 +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
Yury Grigoryev
ce49c3bb41 Fix helper.get_screen_aspect_koef() for gui layouts. 2025-05-15 22:58:55 +03:00
Insality
58f14d0a64 Return hack to keep cloned node_ids consistent 2025-05-13 21:43:05 +03:00
Insality
6572da3b14 Release 1.1.2 2025-05-07 23:54:30 +03:00
Insality
d7c26358a0 Fix for data list element amounts issue 2025-05-07 21:43:48 +03:00
Maksim Tuprikov
f007a28ab7 Merge pull request #310 from rigo128/example_data_list_matrix_basic
Add data_list_matrix_basic example
2025-05-07 18:50:53 +03:00
Yury Grigoryev
d98c3c2ef1 Add data_list_matrix_basic example 2025-05-07 17:07:58 +03:00
Insality
c87f1331ed Release 1.1.1 2025-05-05 21:33:32 +03:00
Maksim Tuprikov
efc38672c9 Merge pull request #309 from astrochili/container-max-size
Added `max_size_x` and `max_size_y` to `container`
2025-05-05 21:31:59 +03:00
Roman Silin
df516a08c6 Added max_size_x and max_size_y to container 2025-05-05 20:48:51 +03:00
50 changed files with 1617 additions and 135 deletions

4
.gitignore vendored
View File

@@ -17,3 +17,7 @@ manifest.private.der
manifest.public.der
/.editor_settings
/.deployer_cache
# Example and documentation files (not part of core library)
example_color_api_usage.lua
DRUID_COLOR_API_IMPROVEMENTS.md

View File

@@ -33,7 +33,8 @@
"Lua.runtime.version": "Lua 5.1",
"Lua.workspace.library": [
"~/Library/Application Support/Cursor/User/globalStorage/astronachos.defold",
"~/Library/Application Support/Cursor/User/workspaceStorage/1446075a23c89451a63f0e82b2291def/astronachos.defold"
"~/Library/Application Support/Cursor/User/workspaceStorage/1446075a23c89451a63f0e82b2291def/astronachos.defold",
"~/Library/Application Support/Cursor/User/workspaceStorage/7975bec62a9fa9724d190779fa01ec63/astronachos.defold"
],
"files.exclude": {
"**/*.gui": true

View File

@@ -39,13 +39,13 @@ Open your `game.project` file and add the following lines to the dependencies fi
**[Druid](https://github.com/Insality/druid/)**
```
https://github.com/Insality/druid/archive/refs/tags/1.1.0.zip
https://github.com/Insality/druid/archive/refs/tags/1.1.5.zip
```
**[Defold Event](https://github.com/Insality/defold-event)**
```
https://github.com/Insality/defold-event/archive/refs/tags/11.zip
https://github.com/Insality/defold-event/archive/refs/tags/12.zip
```
After that, select `Project ▸ Fetch Libraries` to update [library dependencies]((https://defold.com/manuals/libraries/#setting-up-library-dependencies)). This happens automatically whenever you open a project so you will only need to do this if the dependencies change without re-opening the project.

View File

@@ -38,6 +38,7 @@ Create container component with druid: `container = druid:new_container(node, mo
- [clear_draggable_corners](#clear_draggable_corners)
- [fit_into_node](#fit_into_node)
- [set_min_size](#set_min_size)
- [set_max_size](#set_max_size)
## Fields
@@ -54,6 +55,8 @@ Create container component with druid: `container = druid:new_container(node, mo
- [fit_size](#fit_size)
- [min_size_x](#min_size_x)
- [min_size_y](#min_size_y)
- [max_size_x](#max_size_x)
- [max_size_y](#max_size_y)
- [on_size_changed](#on_size_changed)
- [node_fill_x](#node_fill_x)
- [node_fill_y](#node_fill_y)
@@ -303,6 +306,21 @@ Set the minimum size of the container
- **Returns:**
- `self` *(druid.container)*: Current container instance
### set_max_size
---
```lua
container:set_max_size([max_size_x], [max_size_y])
```
Set the maximum size of the container
- **Parameters:**
- `[max_size_x]` *(number|nil)*: The maximum size x
- `[max_size_y]` *(number|nil)*: The maximum size y
- **Returns:**
- `self` *(druid.container)*: Current container instance
## Fields
<a name="node"></a>
@@ -344,6 +362,12 @@ Set the minimum size of the container
<a name="min_size_y"></a>
- **min_size_y** (_number_): The minimum size y
<a name="max_size_x"></a>
- **max_size_x** (_number_): The maximum size x
<a name="max_size_y"></a>
- **max_size_y** (_number_): The maximum size y
<a name="on_size_changed"></a>
- **on_size_changed** (_event_): fun(self: druid.container, size: vector3) The event triggered when the size changes

View File

@@ -177,6 +177,7 @@ container:refresh_origins()
container:refresh_scale()
container:remove_container_by_node([node])
container:set_min_size([min_size_x], [min_size_y])
container:set_max_size([max_size_x], [max_size_y])
container:set_parent_container([parent_container])
container:set_pivot(pivot)
container:set_position(pos_x, pos_y)

View File

@@ -20,7 +20,7 @@ local M = component.create("blocker")
---@param node node|string The node to use as a blocker
function M:init(node)
self.node = self:get_node(node)
self._is_enabled = gui.is_enabled(self.node, true)
self._is_enabled = true
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_pos = gui.get_position(self.anim_node)
self.params = custom_args
self.hover = self.druid:new_hover(node_or_node_id, self._on_button_hover)
self.hover.on_mouse_hover:subscribe(self._on_button_mouse_hover)
self.hover = self.druid:new_hover(node_or_node_id, self.button_hover)
self.hover.on_mouse_hover:subscribe(self.button_mouse_hover)
self.click_zone = nil
self.is_repeated_started = false
self.last_pressed_time = 0
@@ -184,7 +184,7 @@ function M:on_input(action_id, action)
if self._is_html5_mode then
self._is_html5_listener_set = true
html5.set_interaction_listener(function()
self:_on_button_click()
self:button_click()
end)
end
return is_consume
@@ -193,7 +193,7 @@ function M:on_input(action_id, action)
-- While hold button, repeat rate pick from input.repeat_interval
if action.repeated 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
end
end
@@ -211,7 +211,7 @@ function M:on_input(action_id, action)
end
if press_time >= self.style.LONGTAP_TIME then
self:_on_button_hold(press_time)
self:button_hold(press_time)
return is_consume
end
end
@@ -326,18 +326,18 @@ end
---@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)
end
---@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)
end
function M:_on_button_click()
function M:button_click()
if self._is_html5_mode then
self._is_html5_listener_set = false
html5.set_interaction_listener(nil)
@@ -348,7 +348,7 @@ function M:_on_button_click()
end
function M:_on_button_repeated_click()
function M:button_repeated_click()
if not self.is_repeated_started then
self.click_in_row = 0
self.is_repeated_started = true
@@ -360,7 +360,7 @@ function M:_on_button_repeated_click()
end
function M:_on_button_long_click()
function M:button_long_click()
self.click_in_row = 1
local time = socket.gettime() - self.last_pressed_time
self.on_long_click:trigger(self:get_context(), self.params, self, time)
@@ -368,7 +368,7 @@ function M:_on_button_long_click()
end
function M:_on_button_double_click()
function M:button_double_click()
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.style.on_click(self, self.anim_node)
@@ -376,7 +376,7 @@ end
---@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)
end
@@ -413,14 +413,14 @@ function M:_on_button_release()
if is_long_click then
local is_hold_complete = (time - self.last_pressed_time) >= self.style.AUTOHOLD_TRIGGER
if is_hold_complete then
self:_on_button_long_click()
self:button_long_click()
else
self.on_click_outside:trigger(self:get_context(), self.params, self)
end
elseif is_double_click then
self:_on_button_double_click()
self:button_double_click()
else
self:_on_button_click()
self:button_click()
end
self.last_released_time = time

View File

@@ -205,6 +205,59 @@ function M:scroll_to(point, is_instant)
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.
---@param index number Point index
---@param skip_cb boolean|nil If true, skip the point callback

View File

@@ -37,7 +37,7 @@ local utf8 = utf8 or utf8_lua --[[@as utf8]]
---@field on_update_text_scale event fun(self: druid.text, scale: vector3, metrics: table) The event triggered when the text scale is updated
---@field on_set_pivot event fun(self: druid.text, pivot: userdata) The event triggered when the text pivot is set
---@field style druid.text.style The style of the text
---@field start_pivot userdata The start pivot of the text
---@field start_pivot number The start pivot of the text
---@field start_scale vector3 The start scale of the text
---@field scale vector3 The current scale of the text
local M = component.create("text")
@@ -224,7 +224,7 @@ end
---Set text pivot. Text will re-anchor inside text area
---@param pivot userdata The gui.PIVOT_* constant
---@param pivot number The gui.PIVOT_* constant
---@return druid.text self Current text instance
function M:set_pivot(pivot)
local prev_pivot = gui.get_pivot(self.node)

View File

@@ -6,11 +6,23 @@ local COLOR_Z = hash("color.z")
local M = {}
-- =============================================================================
-- COLOR PARSING AND CREATION FUNCTIONS
-- =============================================================================
---Get color by string (hex or from palette)
---@param color_id string Color id from palette or hex color
---Get color by string (hex or from palette) or pass through vector4
---@param color_id string|vector4 Color id from palette, hex color, or existing vector4
---@return vector4
function M.get_color(color_id)
if type(color_id) ~= "string" 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
if type(color_id) == "string" then
if string.sub(color_id, 1, 1) == "#" or string.match(color_id, "^[0-9a-fA-F]+$") then
@@ -18,12 +30,86 @@ function M.get_color(color_id)
end
end
return PALETTE_DATA[color_id] or COLOR_WHITE
return COLOR_WHITE
end
---Parse color from various formats (hex, palette ID, or vector4)
---This is a clearer alias for get_color
---@param color_value string|vector4 Color value to parse
---@return vector4
function M.parse(color_value)
return M.get_color(color_value)
end
---Create a color from RGBA values
---@param r number Red component (0-1)
---@param g number Green component (0-1)
---@param b number Blue component (0-1)
---@param a number|nil Alpha component (0-1), defaults to 1
---@return vector4
function M.new(r, g, b, a)
return vmath.vector4(r, g, b, a or 1)
end
---Create a color from hex string
---@param hex string Hex color. #00BBAA or 00BBAA or #0BA or 0BA
---@param alpha number|nil Alpha value. Default is 1
---@return vector4
function M.from_hex(hex, alpha)
return M.hex2vector4(hex, alpha)
end
---Create a color from HSB values
---@param h number Hue (0-1)
---@param s number Saturation (0-1)
---@param b number Brightness/Value (0-1)
---@param a number|nil Alpha value. Default is 1
---@return vector4
function M.from_hsb(h, s, b, a)
local r, g, b_val, alpha = M.hsb2rgb(h, s, b, a)
return vmath.vector4(r, g, b_val, alpha or 1)
end
-- =============================================================================
-- COLOR FORMAT CONVERSION FUNCTIONS
-- =============================================================================
---Convert color to hex string
---@param color vector4 Color to convert
---@return string Hex color string (without #)
function M.to_hex(color)
return M.rgb2hex(color.x, color.y, color.z)
end
---Convert color to RGB values
---@param color vector4 Color to convert
---@return number, number, number, number r, g, b, a
function M.to_rgb(color)
return color.x, color.y, color.z, color.w
end
---Convert color to HSB values
---@param color vector4 Color to convert
---@return number, number, number, number h, s, b, a
function M.to_hsb(color)
return M.rgb2hsb(color.x, color.y, color.z, color.w)
end
-- =============================================================================
-- PALETTE MANAGEMENT FUNCTIONS
-- =============================================================================
---Add palette to palette data
---@param palette_data table<string, vector4>
---@param palette_data table<string, vector4|string>
function M.add_palette(palette_data)
for color_id, color in pairs(palette_data) do
if type(color) == "string" then
@@ -35,11 +121,59 @@ function M.add_palette(palette_data)
end
---Set a single color in the palette
---@param color_id string Color identifier
---@param color vector4|string Color value (vector4 or hex string)
function M.set_palette_color(color_id, color)
if type(color) == "string" then
PALETTE_DATA[color_id] = M.hex2vector4(color)
else
PALETTE_DATA[color_id] = color
end
end
---Remove a color from the palette
---@param color_id string Color identifier to remove
function M.remove_palette_color(color_id)
PALETTE_DATA[color_id] = nil
end
---Check if a color exists in the palette
---@param color_id string Color identifier to check
---@return boolean
function M.has_palette_color(color_id)
return PALETTE_DATA[color_id] ~= nil
end
---Get a color from the palette
---@param color_id string Color identifier
---@return vector4|nil Color or nil if not found
function M.get_palette_color(color_id)
return PALETTE_DATA[color_id]
end
---Get the entire palette
---@return table<string, vector4>
function M.get_palette()
return PALETTE_DATA
end
---Clear the entire palette
function M.clear_palette()
PALETTE_DATA = {}
end
-- =============================================================================
-- GUI NODE OPERATIONS
-- =============================================================================
---Set color of gui node without changing alpha
---@param gui_node node
---@param color vector4|vector3|string Color in vector4, vector3 or color id from palette
@@ -54,6 +188,34 @@ function M.set_color(gui_node, color)
end
---Apply color to gui node (clearer alias for set_color)
---@param gui_node node GUI node to apply color to
---@param color vector4|vector3|string Color in vector4, vector3 or color id from palette
function M.apply_to_node(gui_node, color)
M.set_color(gui_node, color)
end
---Set node color including alpha
---@param gui_node node GUI node to apply color to
---@param color vector4|string Color with alpha channel
function M.set_node_color_with_alpha(gui_node, color)
if type(color) == "string" then
color = M.get_color(color)
end
gui.set(gui_node, COLOR_X, color.x)
gui.set(gui_node, COLOR_Y, color.y)
gui.set(gui_node, COLOR_Z, color.z)
gui.set(gui_node, "color.w", color.w)
end
-- =============================================================================
-- COLOR MANIPULATION FUNCTIONS
-- =============================================================================
---Lerp colors via color HSB values
---@param t number Lerp value. 0 - color1, 1 - color2
---@param color1 vector4 Color 1
@@ -73,6 +235,55 @@ function M.lerp(t, color1, color2)
end
---Mix two colors using linear RGB interpolation (simpler than lerp)
---@param color1 vector4 First color
---@param color2 vector4 Second color
---@param ratio number Mixing ratio (0 = color1, 1 = color2)
---@return vector4 Mixed color
function M.mix(color1, color2, ratio)
local inv_ratio = 1 - ratio
return vmath.vector4(
color1.x * inv_ratio + color2.x * ratio,
color1.y * inv_ratio + color2.y * ratio,
color1.z * inv_ratio + color2.z * ratio,
color1.w * inv_ratio + color2.w * ratio
)
end
---Lighten a color by mixing with white
---@param color vector4 Color to lighten
---@param amount number Amount to lighten (0-1)
---@return vector4 Lightened color
function M.lighten(color, amount)
return M.mix(color, COLOR_WHITE, amount)
end
---Darken a color by mixing with black
---@param color vector4 Color to darken
---@param amount number Amount to darken (0-1)
---@return vector4 Darkened color
function M.darken(color, amount)
local black = vmath.vector4(0, 0, 0, color.w)
return M.mix(color, black, amount)
end
---Adjust color alpha
---@param color vector4 Color to adjust
---@param alpha number New alpha value (0-1)
---@return vector4 Color with adjusted alpha
function M.with_alpha(color, alpha)
return vmath.vector4(color.x, color.y, color.z, alpha)
end
-- =============================================================================
-- LOW-LEVEL CONVERSION FUNCTIONS
-- =============================================================================
---Convert hex color to rgb values.
---@param hex string Hex color. #00BBAA or 00BBAA or #0BA or 0BA
@@ -160,9 +371,10 @@ end
---Convert rgb color to hex color
---@param red number Red value
---@param green number Green value
---@param blue number Blue value
---@param red number Red value (0-1)
---@param green number Green value (0-1)
---@param blue number Blue value (0-1)
---@return string Hex color string (without #)
function M.rgb2hex(red, green, blue)
local r = string.format("%x", math.floor(red * 255))
local g = string.format("%x", math.floor(green * 255))
@@ -171,4 +383,21 @@ function M.rgb2hex(red, green, blue)
end
-- =============================================================================
-- INITIALIZATION
-- =============================================================================
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

View File

@@ -20,20 +20,20 @@ local helper = require("druid.helper")
---@field _uid number
---@class druid.component
---@field druid druid.instance Druid instance to create inner components
---@field init fun(self:druid.component, ...)|nil Called when component is created
---@field 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 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 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 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 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 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 druid druid.instance Druid instance to create inner components
---@field protected init fun(self:druid.component, ...)|nil Called when component is created
---@field protected update fun(self:druid.component, dt:number)|nil Called every frame
---@field protected on_remove fun(self:druid.component)|nil Called when component is removed
---@field protected on_input fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is triggered
---@field protected on_input_interrupt fun(self:druid.component, action_id:hash, action:table)|nil Called when input event is consumed before
---@field protected on_message fun(self:druid.component, message_id:hash, message:table, sender:url)|nil Called when message is received
---@field protected on_late_init fun(self:druid.component)|nil Called before update once time after GUI init
---@field protected on_focus_lost fun(self:druid.component)|nil Called when app lost focus
---@field protected on_focus_gained fun(self:druid.component)|nil Called when app gained focus
---@field protected on_style_change fun(self:druid.component, style: table)|nil Called when style is changed
---@field protected on_layout_change fun(self:druid.component)|nil Called when GUI layout is changed
---@field protected on_window_resized fun(self:druid.component)|nil Called when window is resized
---@field protected on_language_change fun(self:druid.component)|nil Called when language is changed
---@field private _component druid.component.component
---@field private _meta druid.component.meta
local M = {}
@@ -117,12 +117,23 @@ function M:set_nodes(nodes)
nodes = gui.clone_tree(nodes) --[[@as table<hash, node>]]
end
-- When we use gui.clone_tree in inner template (template inside other template)
-- this nodes have no id. We have table: hash(correct_id) : hash("") or hash("_nodeX"
-- It's wrong and we use this hack to fix this
if nodes then
for id, node in pairs(nodes) do
gui.set_id(node, id)
end
end
self._meta.nodes = nodes
return self
end
---Return current component context
---@protected
---@return any context Usually it's self of script but can be any other Druid component
function M:get_context()
return self._meta.context
@@ -138,6 +149,7 @@ end
---Get Druid instance for inner component creation.
---@protected
---@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
---@return druid.instance
@@ -166,6 +178,7 @@ end
---Get parent component name
---@protected
---@return string|nil parent_name The parent component name if exist or nil
function M:get_parent_name()
local parent = self:get_parent_component()
@@ -218,6 +231,7 @@ end
---Get component UID, unique identifier created in component creation order.
---@protected
---@return number uid The component uid
function M:get_uid()
return self._component._uid
@@ -300,6 +314,7 @@ end
---Get current component nodes
---@protected
---@return table<hash, node>|nil
function M:get_nodes()
local nodes = self._meta.nodes
@@ -342,6 +357,7 @@ end
---Return all children components, recursive
---@protected
---@return table Array of childrens if the Druid component instance
function M:get_childrens()
local childrens = {}

View File

@@ -73,10 +73,8 @@ M.REVERSE_PIVOTS = {
M.LAYOUT_MODE = {
STRETCH_X = "stretch_x",
STRETCH_Y = "stretch_y",
ZOOM_MIN = "zoom_min",
ZOOM_MAX = "zoom_max",
FIT = gui.ADJUST_FIT,
STRETCH = gui.ADJUST_STRETCH,
FIT = "fit",
STRETCH = "stretch",
}
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,
outline = settings.outline,
font = gui.get_font(settings.text_prefab),
split_to_characters = settings.split_to_characters,
-- Image params
---@type druid.rich_text.word.image
image = nil,

View File

@@ -29,6 +29,7 @@ local function add_word(text, settings, words)
end
words[#words + 1] = data
return data
end
@@ -44,8 +45,17 @@ local function split_line(line, settings, words)
else
local wi = #words
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)
end
end
local first = words[wi + 1]
first.text = ws_start .. first.text
first.source_text = first.text

View File

@@ -2,7 +2,8 @@
-- Author: Britzl
-- 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 tags = {}
@@ -43,20 +44,17 @@ end
-- Format: <color={COLOR_NAME}>{Text}</color>
-- Example: <color=FF0000>Rich Text</color>
M.register("color", function(params, settings, style)
params = style.COLORS[params] or params
settings.color = color.parse(params)
settings.color = color.get_color(params)
end)
M.register("shadow", function(params, settings, style)
params = style.COLORS[params] or params
settings.shadow = color.parse(params)
settings.shadow = color.get_color(params)
end)
M.register("outline", function(params, settings, style)
params = style.COLORS[params] or params
settings.outline = color.parse(params)
settings.outline = color.get_color(params)
end)

View File

@@ -13,6 +13,7 @@ local rich_text = require("druid.custom.rich_text.module.rt")
---@field image_pixel_grid_snap boolean
---@field combine_words boolean
---@field default_animation string
---@field split_by_character boolean
---@field text_prefab node
---@field adjust_scale number
---@field default_texture string
@@ -50,7 +51,6 @@ local rich_text = require("druid.custom.rich_text.module.rt")
---@field height number
---@class druid.rich_text.style
---@field COLORS table<string, vector4>
---@field ADJUST_STEPS number
---@field ADJUST_SCALE_DELTA number
@@ -104,7 +104,6 @@ end
---@param style druid.rich_text.style
function M:on_style_change(style)
self.style = {
COLORS = style.COLORS or {},
ADJUST_STEPS = style.ADJUST_STEPS or 20,
ADJUST_SCALE_DELTA = style.ADJUST_SCALE_DELTA or 0.02,
}
@@ -194,6 +193,15 @@ function M:tagged(tag)
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
---@return druid.rich_text.word[]
function M:get_words()
@@ -239,6 +247,7 @@ function M:_create_settings()
outline = gui.get_outline(self.root),
text_leading = gui.get_leading(self.root),
is_multiline = gui.get_line_break(self.root),
split_to_characters = false,
-- Image settings
image_pixel_grid_snap = false, -- disabled now

View File

@@ -1,6 +1,6 @@
local component = require("druid.component")
local helper = require("druid.helper")
local defer = require("event.defer")
local queues = require("event.queues")
---@class druid.tiling_node: druid.component
---@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())
end)
defer.push("druid.get_atlas_path", {
queues.push("druid.get_atlas_path", {
texture_name = gui.get_texture(self.node),
sender = msg.url(),
}, self.on_get_atlas_path, self)

View File

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

View File

@@ -114,6 +114,12 @@ local function wrap_widget(widget)
end
end
for key, value in pairs(widget) do
if event.is_event(value) then
wrapped_widget[key] = value
end
end
return wrapped_widget
end
@@ -126,23 +132,26 @@ end
--- msg.url(nil, object_url, "gui_widget") -- other game object
---@generic T: druid.widget
---@param widget_class T The class of the widget to return
---@param gui_url url GUI url
---@return T? widget The new created widget,
function M.get_widget(widget_class, gui_url)
---@param gui_url url|string GUI url or string of component name near current script
---@param params any|nil Additional parameters to pass to the widget's init function
---@return T widget The new created widget,
function M.get_widget(widget_class, gui_url, params)
if type(gui_url) == "string" then
gui_url = msg.url(nil, nil, gui_url)
end
gui_url = gui_url or msg.url()
local registered_druids = REGISTERED_GUI_WIDGETS[gui_url.socket]
if not registered_druids then
return nil
end
assert(registered_druids, "Druid widget not registered for this game object")
for index = 1, #registered_druids do
local druid = registered_druids[index]
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
return nil
error("Druid widget not found for this game object: " .. gui_url)
end
@@ -156,8 +165,8 @@ function M.register_druid_as_widget(druid)
table.insert(REGISTERED_GUI_WIDGETS[gui_url.socket], {
path = gui_url.path,
fragment = gui_url.fragment,
new_widget = event.create(function(widget_class)
return wrap_widget(druid:new_widget(widget_class))
new_widget = event.create(function(widget_class, template, nodes, params)
return wrap_widget(druid:new_widget(widget_class, template, nodes, params))
end),
})
end

View File

@@ -3,9 +3,9 @@
-- 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
local defer = require("event.defer")
local queues = require("event.queues")
---Usage: defer.push("druid.get_atlas_path", {
---Usage: queues.push("druid.get_atlas_path", {
--- texture_name = gui.get_texture(self.node),
--- sender = msg.url(),
---}, callback, [context])
@@ -30,15 +30,15 @@ local function get_atlas_path(self, request)
return nil
end
return go.get(request.sender, "textures", { key = request.texture_name })
return go.get(request.sender, "textures", { key = request.texture_name }) --[[@as string]]
end
function init(self)
defer.subscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self)
queues.subscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self)
end
function final(self)
defer.unsubscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self)
queues.unsubscribe(MESSAGE_GET_ATLAS_PATH, get_atlas_path, self)
end

35
druid/extended/container.lua Normal file → Executable file
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_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.
---
---### Setup
@@ -54,10 +56,12 @@ local CORNER_PIVOTS = {
---@field position vector3 The current position
---@field pivot_offset vector3 The pivot 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 min_size_x number|nil The minimum size x
---@field min_size_y number|nil The minimum size y
---@field max_size_x number|nil The maximum size x
---@field max_size_y number|nil The maximum size y
---@field on_size_changed event fun(self: druid.container, size: vector3) The event triggered when the size changes
---@field _parent_container druid.container The parent container
---@field _containers table The containers
@@ -75,6 +79,8 @@ function M:init(node, mode, callback)
self.min_size_x = 0
self.min_size_y = 0
self.max_size_x = nil
self.max_size_y = nil
self._containers = {}
self._draggable_corners = {}
self.node_offset = vmath.vector4(0)
@@ -166,6 +172,12 @@ function M:set_size(width, height, anchor_pivot)
if self.min_size_y then
height = max(height, self.min_size_y)
end
if self.max_size_x then
width = min(width, self.max_size_x)
end
if self.max_size_y then
height = min(height, self.max_size_y)
end
if (width and width ~= self.size.x) or (height and height ~= self.size.y) then
self.center_offset.x = -width * self.pivot_offset.x
@@ -261,7 +273,7 @@ end
---@param node_or_container node|string|druid.container|table The node or container to add
---@param mode string|nil stretch, fit, stretch_x, stretch_y. Default: Pick from node, "fit" or "stretch"
---@param mode druid.container.mode|nil stretch, fit, stretch_x, stretch_y. Default: Pick from node, "fit" or "stretch"
---@param on_resize_callback fun(self: userdata, size: vector3)|nil
---@return druid.container Container New created layout instance
function M:add_container(node_or_container, mode, on_resize_callback)
@@ -522,6 +534,12 @@ function M:_on_corner_drag(x, y, corner_offset)
if self.min_size_y and size.y + y < self.min_size_y then
y = self.min_size_y - size.y
end
if self.max_size_x and size.x + x > self.max_size_x then
x = self.max_size_x - size.x
end
if self.max_size_y and size.y + y > self.max_size_y then
y = self.max_size_y - size.y
end
if corner_offset.x < 0 then
self.node_offset.x = self.node_offset.x - x
@@ -570,4 +588,17 @@ function M:set_min_size(min_size_x, min_size_y)
end
---Set the maximum size of the container
---@param max_size_x number|nil The maximum size x
---@param max_size_y number|nil The maximum size y
---@return druid.container self Current container instance
function M:set_max_size(max_size_x, max_size_y)
self.max_size_x = max_size_x or self.max_size_x
self.max_size_y = max_size_y or self.max_size_y
self:refresh()
return self
end
return M

View File

@@ -267,10 +267,17 @@ function M:_refresh()
local start_index = self.grid:get_index(start_pos)
start_index = math.max(1, start_index)
local pivot = helper.get_pivot_offset(gui.get_pivot(self.scroll.view_node))
local offset_x = self.scroll.view_size.x * (0.5 - pivot.x)
local offset_y = self.scroll.view_size.y * (0.5 + pivot.y)
local offset_x = self.scroll.view_size.x
local offset_y = self.scroll.view_size.y
local end_pos = vmath.vector3(start_pos.x + offset_x, start_pos.y - offset_y, 0)
local max_offset_x = (self.grid.in_row - 1) * self.grid.node_size.x
end_pos.x = math.min(end_pos.x, start_pos.x + max_offset_x)
if #self._data <= self.grid.in_row then
end_pos.y = start_pos.y
end
local end_index = self.grid:get_index(end_pos)
end_index = math.min(#self._data, end_index)

View File

@@ -55,12 +55,13 @@ function M:init(node_or_node_id, layout_type)
self.entities = {}
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)
-- Margin X is a Slice9 R Value
-- Margin Y is a Slice9 B Value
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.w = self.padding.y
@@ -68,6 +69,7 @@ function M:init(node_or_node_id, layout_type)
self.is_resize_width = false
self.is_resize_height = false
self.is_justify = false
self._set_position_function = gui.set_position
self.on_size_changed = event.create() --[[@as event.on_size_changed]]
end
@@ -78,7 +80,7 @@ function M:update()
return
end
self:refresh_layout()
self:refresh_layout(false)
end
@@ -123,10 +125,10 @@ function M:set_margin(margin_x, margin_y)
end
---@param padding_x number|nil The padding x
---@param padding_y number|nil The padding y
---@param padding_z number|nil The padding z
---@param padding_w number|nil The padding w
---@param padding_x number|nil From Left
---@param padding_y number|nil From Top
---@param padding_z number|nil From Right
---@param padding_w number|nil From Bottom
---@return druid.layout self Current layout instance
function M:set_padding(padding_x, padding_y, padding_z, padding_w)
self.padding.x = padding_x or self.padding.x
@@ -234,8 +236,9 @@ function M:get_content_size()
end
---@param is_instant boolean|nil If true, node position update instantly, otherwise with set_position_function callback
---@return druid.layout self Current layout instance
function M:refresh_layout()
function M:refresh_layout(is_instant)
local layout_node = self.node
local entities = self.entities
@@ -353,7 +356,7 @@ function M:refresh_layout()
end
end
self:set_node_position(node, position_x, position_y)
self:set_node_position(node, position_x, position_y, is_instant)
end
end
@@ -498,13 +501,27 @@ local TEMP_VECTOR = vmath.vector3(0, 0, 0)
---@param x number
---@param y number
---@return node
function M:set_node_position(node, x, y)
function M:set_node_position(node, x, y, is_instant)
TEMP_VECTOR.x = x
TEMP_VECTOR.y = y
if is_instant then
gui.set_position(node, TEMP_VECTOR)
else
self._set_position_function(node, TEMP_VECTOR)
end
return node
end
---Set custom position function for layout nodes. It will call on update poses on layout elements. Default: gui.set_position
---@param callback function
---@return druid.layout self Current layout instance
function M:set_position_function(callback)
self._set_position_function = callback or gui.set_position
return self
end
return M

View File

@@ -473,6 +473,9 @@ function M.get_text_metrics_from_node(text_node)
options.tracking = gui.get_tracking(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
if options.line_break then
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

View File

@@ -1,5 +1,5 @@
---@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
---@field trace fun(message: string, context: any)

View File

@@ -1,3 +1,4 @@
---@diagnostic disable: invisible
-- Hello, Defolder! Wish you a good day!
local events = require("event.events")
@@ -681,7 +682,7 @@ end
local container = require("druid.extended.container")
---Create Container component
---@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
---@return druid.container container The new container component
function M:new_container(node, mode, callback)

View File

@@ -6,7 +6,6 @@ local color = require("druid.color")
---@field text_name druid.text
---@field button druid.button
---@field text_button druid.text
---@field druid druid.instance
local M = {}
@@ -51,6 +50,14 @@ function M:set_text_button(text)
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)
color.set_color(self:get_node("button"), color_value)
end

View File

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

View File

@@ -33,21 +33,34 @@ nodes {
}
}
nodes {
position {
x: 200.0
}
size {
x: 400.0
y: 40.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid/ui_circle_16"
id: "header"
pivot: PIVOT_N
pivot: PIVOT_NE
parent: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
slice9 {
x: 8.0
y: 8.0
z: 8.0
w: 8.0
}
}
nodes {
position {
x: -192.0
x: -392.0
y: -8.0
}
scale {
@@ -85,7 +98,7 @@ nodes {
}
nodes {
position {
x: 192.0
x: -8.0
y: -4.0
}
color {
@@ -101,6 +114,45 @@ nodes {
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
}
nodes {
position {
x: -48.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: -88.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 {
position {
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_slider = require("druid.widget.properties_panel.properties.property_slider")
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
local M = {}
local COLOR_BUTTON = "#4E4F50"
local COLOR_REFRESH_ACTIVE = "#8BD092"
function M:init()
self.root = self:get_node("root")
@@ -33,6 +39,10 @@ function M:init()
self.contaienr_scroll_content = self.container_scroll_view:add_container("scroll_content")
self.default_size = self.container:get_size()
self.header_size = gui.get_size(self:get_node("header"))
-- To have ability to go back to previous scene, collections of all properties to rebuild
self.scenes = {}
self.properties = {}
self.properties_constructors = {}
@@ -52,6 +62,15 @@ function M:init()
self:set_hidden(not self._is_hidden)
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
self.paginator = self.druid:new_widget(property_left_right_selector, "property_left_right_selector", "root")
self.paginator:set_text("Page")
@@ -80,6 +99,23 @@ function M:on_remove()
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)
local position = self.container:get_position()
self.container:set_position(position.x + dx, position.y + dy)
@@ -112,9 +148,41 @@ function M:clear_created_properties()
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()
self:clear_created_properties()
self.properties_constructors = {}
self.current_page = 1
end
@@ -139,7 +207,10 @@ end
function M:update(dt)
if self.is_dirty then
if not self.is_dirty then
return
end
self.is_dirty = false
self:clear_created_properties()
@@ -159,7 +230,6 @@ function M:update(dt)
for index = start_index, end_index do
self.properties_constructors[index]()
end
end
end
@@ -285,14 +355,32 @@ function M:remove(widget)
end
---Force to refresh properties next update
function M:set_dirty()
self.is_dirty = true
end
function M:set_hidden(is_hidden)
self._is_hidden = is_hidden
local hidden_size = gui.get_size(self:get_node("header"))
local node_header = self:get_node("header")
local new_size = self._is_hidden and hidden_size or self.default_size
local new_size = self._is_hidden and self.header_size or self.default_size
self.container:set_size(new_size.x, new_size.y, gui.PIVOT_N)
local hidden_width = self.header_size.y + 8
gui.set(node_header, "size.x", self._is_hidden and hidden_width or self.header_size.x)
gui.set_visible(node_header, self._is_hidden)
gui.set_visible(self.root, not self._is_hidden)
gui.set_enabled(self.text_header.node, 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
@@ -301,16 +389,183 @@ function M:is_hidden()
end
function M:load_previous_page()
self.current_page = self.current_page - 1
self.is_dirty = true
end
---@param properties_per_page number
function M:set_properties_per_page(properties_per_page)
self.properties_per_page = properties_per_page
end
---Set a page of current scene
---@param page number
function M:set_page(page)
self.current_page = page
self.is_dirty = true
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

View File

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

View File

@@ -2863,6 +2863,49 @@ nodes {
parent: "data_list_cache_with_component/button_component/root"
template_node_child: true
}
nodes {
type: TYPE_TEMPLATE
id: "data_list_matrix_basic"
parent: "data_list"
inherit_alpha: true
template: "/example/examples/data_list/basic/data_list_matrix_basic.gui"
}
nodes {
type: TYPE_BOX
id: "data_list_matrix_basic/root"
parent: "data_list_matrix_basic"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "data_list_matrix_basic/view"
parent: "data_list_matrix_basic/root"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "data_list_matrix_basic/content"
parent: "data_list_matrix_basic/view"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "data_list_matrix_basic/prefab"
parent: "data_list_matrix_basic/content"
template_node_child: true
}
nodes {
type: TYPE_BOX
id: "data_list_matrix_basic/panel"
parent: "data_list_matrix_basic/prefab"
template_node_child: true
}
nodes {
type: TYPE_TEXT
id: "data_list_matrix_basic/text"
parent: "data_list_matrix_basic/prefab"
template_node_child: true
}
nodes {
type: TYPE_BOX
texture: "druid_example/empty"

View File

@@ -11,6 +11,12 @@ function M:init()
self.drag = self.druid:new_drag("drag/root", self.on_drag)
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
self.start_position = gui.get_position(self.drag.node)
end

View File

@@ -1,4 +1,4 @@
---@class widget.container_anchors: druid.widget
---@class widget.container_resize: druid.widget
local M = {}

View File

@@ -0,0 +1,117 @@
fonts {
name: "text_bold"
font: "/example/assets/fonts/text_bold.font"
}
textures {
name: "druid_example"
texture: "/example/assets/druid_example.atlas"
}
nodes {
type: TYPE_BOX
texture: "druid_example/empty"
id: "root"
inherit_alpha: true
size_mode: SIZE_MODE_AUTO
visible: false
}
nodes {
position {
y: 350.0
}
size {
x: 400.0
y: 700.0
}
color {
x: 0.173
y: 0.184
z: 0.204
}
type: TYPE_BOX
texture: "druid_example/pixel"
id: "view"
pivot: PIVOT_N
parent: "root"
inherit_alpha: true
clipping_mode: CLIPPING_MODE_STENCIL
}
nodes {
size {
x: 400.0
y: 700.0
}
type: TYPE_BOX
texture: "druid_example/empty"
id: "content"
pivot: PIVOT_N
parent: "view"
inherit_alpha: true
visible: false
}
nodes {
position {
y: -350.0
}
size {
x: 100.0
y: 100.0
}
type: TYPE_BOX
id: "prefab"
parent: "content"
inherit_alpha: true
visible: false
}
nodes {
size {
x: 90.0
y: 90.0
}
color {
x: 0.631
y: 0.843
z: 0.961
}
type: TYPE_BOX
texture: "druid_example/ui_circle_32"
id: "panel"
parent: "prefab"
inherit_alpha: true
slice9 {
x: 16.0
y: 16.0
z: 16.0
w: 16.0
}
}
nodes {
size {
x: 50.0
y: 50.0
}
color {
x: 0.31
y: 0.318
z: 0.322
}
type: TYPE_TEXT
text: "1"
font: "text_bold"
id: "text"
outline {
x: 1.0
y: 1.0
z: 1.0
}
shadow {
x: 1.0
y: 1.0
z: 1.0
}
parent: "prefab"
inherit_alpha: true
outline_alpha: 0.0
shadow_alpha: 0.0
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT

View File

@@ -0,0 +1,95 @@
local event = require("event.event")
---@class examples.data_list_matrix_basic: druid.widget
---@field prefab node
---@field scroll druid.scroll
---@field grid druid.grid
---@field data_list druid.data_list
---@field on_item_click event
local M = {}
function M:init()
self.prefab = self:get_node("prefab")
gui.set_enabled(self.prefab, false)
self.scroll = self.druid:new_scroll("view", "content")
self.grid = self.druid:new_grid("content", self.prefab, 4)
self.data_list = self.druid:new_data_list(self.scroll, self.grid, self.create_item_callback) --[[@as druid.data_list]]
local data = {}
for index = 1, 1000 do
table.insert(data, {})
end
self.data_list:set_data(data)
self.on_item_click = event.create()
end
---@param item_data table
---@param index number
---@return node, druid.component
function M:create_item_callback(item_data, index)
local nodes = gui.clone_tree(self.prefab)
local root = nodes[self:get_template() .. "/prefab"]
local text = nodes[self:get_template() .. "/text"]
gui.set_enabled(root, true)
gui.set_text(text, tostring(index))
local button = self.druid:new_button(root, self.on_button_click, index)
return root, button
end
function M:on_button_click(index)
self.on_item_click:trigger(index)
end
---@param output_list output_list
function M:on_example_created(output_list)
self.on_item_click:subscribe(function(index)
output_list:add_log_text("Item clicked: " .. index)
end)
end
---@param properties_panel properties_panel
function M:properties_control(properties_panel)
local view_node = self.scroll.view_node
local is_stencil = gui.get_clipping_mode(view_node) == gui.CLIPPING_MODE_STENCIL
properties_panel:add_checkbox("ui_clipping", is_stencil, function(value)
gui.set_clipping_mode(view_node, value and gui.CLIPPING_MODE_STENCIL or gui.CLIPPING_MODE_NONE)
end)
properties_panel:add_slider("ui_scroll", 0, function(value)
self.scroll:scroll_to_percent(vmath.vector3(0, 1 - value, 0), true)
end)
end
---@return string
function M:get_debug_info()
local data_list = self.data_list
local data = data_list:get_data()
local info = ""
info = info .. "Data length: " .. #data .. "\n"
info = info .. "First Visual Index: " .. data_list.top_index .. "\n"
info = info .. "Last Visual Index: " .. data_list.last_index .. "\n"
local s = self.scroll
info = info .. "\n"
info = info .. "View Size Y: " .. gui.get(s.view_node, "size.y") .. "\n"
info = info .. "Content Size Y: " .. gui.get(s.content_node, "size.y") .. "\n"
info = info .. "Content position Y: " .. math.ceil(s.position.y) .. "\n"
info = info .. "Content Range Y: " .. s.available_pos.y .. " - " .. s.available_pos.w .. "\n"
return info
end
return M

View File

@@ -21,6 +21,15 @@ function M.get_examples()
widget_class = require("example.examples.data_list.basic.data_list_horizontal_basic"),
},
{
name_id = "ui_example_data_list_matrix_basic",
information_text_id = "ui_example_data_list_matrix_basic_description",
template = "data_list_matrix_basic",
root = "data_list_matrix_basic/root",
code_url = "example/examples/data_list/basic/data_list_matrix_basic.lua",
widget_class = require("example.examples.data_list.basic.data_list_matrix_basic"),
},
{
name_id = "ui_example_data_list_add_remove_clear",
information_text_id = "ui_example_data_list_add_remove_clear_description",

View File

@@ -147,6 +147,9 @@
"ui_example_data_list_horizontal_basic": "Data List Horizontal Basic",
"ui_example_data_list_horizontal_basic_description": "How to make a horizontal data list",
"ui_example_data_list_matrix_basic": "Data List Matrix Basic",
"ui_example_data_list_matrix_basic_description": "How to make a matrix data list",
"ui_example_data_list_add_remove_clear": "Data List Add Remove Clear",
"ui_example_data_list_add_remove_clear_description": "How the add, remove and clear functions work in the data list",

View File

@@ -14,16 +14,16 @@ update_frequency = 60
[project]
title = Druid
version = 1.1.0
version = 1.1.5
publisher = Insality
developer = Maksim Tuprikov
custom_resources = /example/locales
dependencies#0 = https://github.com/britzl/deftest/archive/refs/tags/2.8.0.zip
dependencies#1 = https://github.com/Insality/defold-saver/archive/refs/heads/develop.zip
dependencies#1 = https://github.com/Insality/defold-saver/archive/refs/tags/5.zip
dependencies#2 = https://github.com/Insality/defold-tweener/archive/refs/tags/3.zip
dependencies#3 = https://github.com/Insality/panthera/archive/refs/heads/develop.zip
dependencies#3 = https://github.com/Insality/panthera/archive/refs/tags/runtime.4.zip
dependencies#4 = https://github.com/Insality/defold-lang/archive/refs/tags/3.zip
dependencies#5 = https://github.com/Insality/defold-event/archive/refs/heads/develop.zip
dependencies#5 = https://github.com/Insality/defold-event/archive/refs/tags/12.zip
dependencies#6 = https://github.com/subsoap/defos/archive/refs/tags/v2.8.0.zip
[library]

View File

@@ -5,6 +5,7 @@ function init(self)
deftest.add(require("test.tests.test_back_handler"))
deftest.add(require("test.tests.test_blocker"))
deftest.add(require("test.tests.test_button"))
deftest.add(require("test.tests.test_color"))
deftest.add(require("test.tests.test_container"))
deftest.add(require("test.tests.test_drag"))
deftest.add(require("test.tests.test_grid"))

313
test/tests/test_color.lua Normal file
View File

@@ -0,0 +1,313 @@
return function()
describe("Color Module", function()
local color = nil
before(function()
color = require("druid.color")
end)
describe("Color Creation and Parsing", function()
it("Should create colors from RGBA values", function()
local test_color = color.new(1, 0.5, 0, 0.8)
assert(test_color.x == 1)
assert(test_color.y == 0.5)
assert(test_color.z == 0)
assert(test_color.w == 0.8)
end)
it("Should create colors from hex strings", function()
local red = color.from_hex("#FF0000")
assert(red.x == 1)
assert(red.y == 0)
assert(red.z == 0)
assert(red.w == 1)
local blue_with_alpha = color.from_hex("0000FF", 0.5)
assert(blue_with_alpha.x == 0)
assert(blue_with_alpha.y == 0)
assert(blue_with_alpha.z == 1)
assert(blue_with_alpha.w == 0.5)
end)
it("Should create colors from HSB values", function()
local red = color.from_hsb(0, 1, 1) -- HSB for red
assert(red.x == 1)
assert(red.y == 0)
assert(red.z == 0)
assert(red.w == 1)
end)
it("Should parse various color formats", function()
-- Test parsing alias for get_color
local hex_color = color.parse("#FF0000")
assert(hex_color.x == 1)
local vector_color = color.parse(vmath.vector4(0, 1, 0, 1))
assert(vector_color.y == 1)
end)
it("Should parse hex colors correctly with get_color", function()
-- Test with # prefix
local color1 = color.get_color("#FF0000")
assert(color1.x == 1)
assert(color1.y == 0)
assert(color1.z == 0)
assert(color1.w == 1)
-- Test without # prefix
local color2 = color.get_color("00FF00")
assert(color2.x == 0)
assert(color2.y == 1)
assert(color2.z == 0)
assert(color2.w == 1)
-- Test 3-digit hex
local color3 = color.get_color("F0F")
assert(color3.x == 1)
assert(color3.y == 0)
assert(color3.z == 1)
assert(color3.w == 1)
end)
it("Should handle vector4 input in get_color", function()
local input_color = vmath.vector4(1, 0.5, 0, 1)
local result = color.get_color(input_color)
assert(result == input_color)
end)
it("Should return white for unknown color IDs", function()
local result = color.get_color("unknown_color")
assert(result.x == 1)
assert(result.y == 1)
assert(result.z == 1)
assert(result.w == 1)
end)
end)
describe("Color Format Conversion", function()
it("Should convert colors to hex", function()
local red = vmath.vector4(1, 0, 0, 1)
local hex = color.to_hex(red)
assert(hex == "FF0000")
end)
it("Should convert colors to RGB values", function()
local test_color = vmath.vector4(1, 0.5, 0.25, 0.8)
local r, g, b, a = color.to_rgb(test_color)
assert(r == 1)
assert(g == 0.5)
assert(b == 0.25)
assert(a == 0.8)
end)
it("Should convert colors to HSB values", function()
local red = vmath.vector4(1, 0, 0, 1)
local h, s, b, a = color.to_hsb(red)
assert(h == 0) -- Red hue
assert(s == 1) -- Full saturation
assert(b == 1) -- Full brightness
assert(a == 1) -- Full alpha
end)
it("Should convert hex to rgb values", function()
local r, g, b = color.hex2rgb("#FF8000")
assert(r == 1)
assert(g == 0.5019607843137255) -- 128/255
assert(b == 0)
end)
it("Should convert hex to vector4", function()
local vec = color.hex2vector4("#FF8000", 0.5)
assert(vec.x == 1)
assert(vec.y == 0.5019607843137255)
assert(vec.z == 0)
assert(vec.w == 0.5)
end)
it("Should convert rgb to hex", function()
local hex = color.rgb2hex(1, 0.5019607843137255, 0)
assert(hex == "FF8000")
end)
end)
describe("Color Space Conversion", function()
it("Should convert RGB to HSB correctly", function()
local h, s, v, a = color.rgb2hsb(1, 0, 0, 1) -- Red
assert(h == 0)
assert(s == 1)
assert(v == 1)
assert(a == 1)
end)
it("Should convert HSB to RGB correctly", function()
local r, g, b, a = color.hsb2rgb(0, 1, 1, 1) -- Red
assert(r == 1)
assert(g == 0)
assert(b == 0)
assert(a == 1)
end)
it("Should handle round-trip HSB conversion", function()
local original_r, original_g, original_b = 0.5, 0.7, 0.3
local h, s, v = color.rgb2hsb(original_r, original_g, original_b)
local converted_r, converted_g, converted_b = color.hsb2rgb(h, s, v)
-- Allow for small floating point differences
assert(math.abs(converted_r - original_r) < 0.001)
assert(math.abs(converted_g - original_g) < 0.001)
assert(math.abs(converted_b - original_b) < 0.001)
end)
end)
describe("Palette Management", function()
it("Should add and retrieve palette colors", function()
local test_palette = {
primary = vmath.vector4(1, 0, 0, 1),
secondary = "#00FF00",
tertiary = vmath.vector4(0, 0, 1, 1)
}
color.add_palette(test_palette)
local primary = color.get_color("primary")
assert(primary.x == 1)
assert(primary.y == 0)
assert(primary.z == 0)
assert(primary.w == 1)
local secondary = color.get_color("secondary")
assert(secondary.x == 0)
assert(secondary.y == 1)
assert(secondary.z == 0)
assert(secondary.w == 1)
end)
it("Should manage individual palette colors", function()
-- Set a palette color
color.set_palette_color("test_red", vmath.vector4(1, 0, 0, 1))
assert(color.has_palette_color("test_red") == true)
local retrieved = color.get_palette_color("test_red")
assert(retrieved.x == 1)
assert(retrieved.y == 0)
assert(retrieved.z == 0)
-- Remove the color
color.remove_palette_color("test_red")
assert(color.has_palette_color("test_red") == false)
assert(color.get_palette_color("test_red") == nil)
end)
it("Should return the palette", function()
local palette = color.get_palette()
assert(type(palette) == "table")
end)
it("Should clear the palette", function()
color.set_palette_color("temp_color", vmath.vector4(1, 1, 1, 1))
color.clear_palette()
assert(color.has_palette_color("temp_color") == false)
end)
end)
describe("Color Manipulation", function()
it("Should lerp colors correctly", function()
local color1 = vmath.vector4(1, 0, 0, 1) -- Red
local color2 = vmath.vector4(0, 1, 0, 1) -- Green
local mid_color = color.lerp(0.5, color1, color2)
-- Note: lerp uses HSB interpolation, so the result might not be a simple average
assert(type(mid_color.x) == "number")
assert(type(mid_color.y) == "number")
assert(type(mid_color.z) == "number")
assert(mid_color.w == 1)
-- Test endpoints
local start_color = color.lerp(0, color1, color2)
local end_color = color.lerp(1, color1, color2)
assert(math.abs(start_color.x - color1.x) < 0.001)
assert(math.abs(end_color.x - color2.x) < 0.001)
end)
it("Should mix colors using linear RGB interpolation", function()
local red = vmath.vector4(1, 0, 0, 1)
local blue = vmath.vector4(0, 0, 1, 1)
local mixed = color.mix(red, blue, 0.5)
assert(mixed.x == 0.5) -- Half red
assert(mixed.y == 0) -- No green
assert(mixed.z == 0.5) -- Half blue
assert(mixed.w == 1) -- Full alpha
end)
it("Should lighten colors", function()
local dark_red = vmath.vector4(0.5, 0, 0, 1)
local lightened = color.lighten(dark_red, 0.5)
assert(lightened.x > dark_red.x) -- Should be lighter
assert(lightened.y > dark_red.y) -- Should have some green/white
assert(lightened.z > dark_red.z) -- Should have some blue/white
end)
it("Should darken colors", function()
local bright_red = vmath.vector4(1, 0, 0, 1)
local darkened = color.darken(bright_red, 0.5)
assert(darkened.x < bright_red.x) -- Should be darker
assert(darkened.y == 0) -- Should still have no green
assert(darkened.z == 0) -- Should still have no blue
end)
it("Should adjust alpha", function()
local opaque_red = vmath.vector4(1, 0, 0, 1)
local semi_transparent = color.with_alpha(opaque_red, 0.5)
assert(semi_transparent.x == 1) -- Color unchanged
assert(semi_transparent.y == 0)
assert(semi_transparent.z == 0)
assert(semi_transparent.w == 0.5) -- Alpha changed
end)
end)
describe("GUI Node Operations", function()
it("Should set color on GUI node", function()
-- Create a test node
local test_node = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(100, 100, 0))
-- Test with vector4
local test_color = vmath.vector4(1, 0.5, 0, 1)
color.set_color(test_node, test_color)
-- Verify color was set (we can't easily read it back, but we can verify no errors)
assert(true) -- If we get here, no error occurred
-- Test with string color
color.set_color(test_node, "#FF0000")
assert(true) -- If we get here, no error occurred
-- Clean up
gui.delete_node(test_node)
end)
it("Should apply color to node using alias", function()
local test_node = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(100, 100, 0))
-- Test the alias function
color.apply_to_node(test_node, vmath.vector4(0, 1, 0, 1))
assert(true) -- If we get here, no error occurred
gui.delete_node(test_node)
end)
it("Should set node color including alpha", function()
local test_node = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(100, 100, 0))
color.set_node_color_with_alpha(test_node, vmath.vector4(1, 0, 0, 0.5))
assert(true) -- If we get here, no error occurred
gui.delete_node(test_node)
end)
end)
end)
end

30
test/tests/test_container.lua Normal file → Executable file
View File

@@ -41,6 +41,8 @@ return function()
assert(container.mode == const.LAYOUT_MODE.FIT)
assert(container.min_size_x == 0)
assert(container.min_size_y == 0)
assert(container.max_size_x == nil)
assert(container.max_size_y == nil)
assert(container._containers ~= nil)
assert(#container._containers == 0)
@@ -137,6 +139,34 @@ return function()
gui.delete_node(container_node)
end)
it("Should set max size", function()
local container_node = gui.new_box_node(vmath.vector3(50, 50, 0), vmath.vector3(100, 100, 0))
local container = druid:new_container(container_node)
assert(container.max_size_x == nil)
assert(container.max_size_y == nil)
container:set_max_size(150, 200)
assert(container.max_size_x == 150)
assert(container.max_size_y == 200)
-- Should respect max size when setting larger size
container:set_size(300, 300)
local size = container:get_size()
assert(size.x == 150)
assert(size.y == 200)
-- Should allow smaller size
container:set_size(100, 100)
size = container:get_size()
assert(size.x == 100)
assert(size.y == 100)
druid:remove(container)
gui.delete_node(container_node)
end)
it("Should fire on_size_changed event", function()
local container_node = gui.new_box_node(vmath.vector3(50, 50, 0), vmath.vector3(100, 100, 0))
local container = druid:new_container(container_node)

View File

@@ -81,7 +81,7 @@ return function()
local rich_text = druid:new_rich_text(text_node)
-- 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)
-- Word should have a tags field with color tag
@@ -104,7 +104,7 @@ return function()
local rich_text = druid:new_rich_text(text_node)
-- 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[1].shadow ~= nil)
@@ -129,7 +129,7 @@ return function()
local rich_text = druid:new_rich_text(text_node)
-- 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[1].outline ~= nil)
@@ -228,7 +228,7 @@ return function()
local rich_text = druid:new_rich_text(text_node)
-- 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[1].tags.color)
@@ -236,7 +236,7 @@ return function()
assert(words[1].relative_scale == 2)
-- 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)
-- All words should have color tag

View File

@@ -703,6 +703,31 @@ Please support me if you like this project! It will help me keep engaged to upda
[![Github-sponsors](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/insality) [![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/insality) [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/insality)
### Druid 1.1.X
#### Druid 1.1.1
- [#309] Added max_size_x and max_size_y to container (by [astrochili](https://github.com/astrochili))
#### Druid 1.1.2
- [#310] Add data list matrix example (Grid 4 in row)
- [Data List] Fix for data list element amounts issue
#### Druid 1.1.3
- 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
#### Druid 1.1.5
- Update for using `defold-event` library v12
### Druid 1.2.0
- Fix for blocker internal enabled state depends from GUI node
- Move to druid colors for rich text
- Fix for container stretch mode (stretch and fit is not worked in init function)
- Add split_to_characters in rich text for making fancy text
- Druid GO Widgets now can wrap an events to (before only top level functions)
- Ability to pass params to Druid GO Widgets
- Update properties panel:
- Add "scenes" to manage a list of properties with back button support
- Add "refresh" button, which active a 1-sec refresh for current page
- Add "Render lua table" to easily render your lua tables with a various types support (any simple types and vector, functions, events etc)
- {Place for the community changelogs}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,98 @@
# Memory and FPS Panel Widgets
The `Druid 1.1` comes with widgets and two included widgets are `Memory Panel` and `FPS Panel` which allow you to monitor memory and FPS in your game.
Widgets in Druid usually consists from two files: GUI, which is used to placed as a template on your GUI scene and Lua script, which is used to be created with Druid.
<!-- Video -->
## Memory Panel
The `Memory Panel` is a widget which allows you to monitor memory of your game. It displays the last 3 seconds of memory allocations in graph, largest memory allocation step, total Lua memory and memory per second.
When you see an empty space in graphs - it means the garbage collector is working at this moment.
### How to add:
- Add `/druid/widget/memory_panel/memory_panel.gui` to your `*.gui` scene
![](/wiki/manuals/media/memory_fps_panel_add.png)
![](/wiki/manuals/media/memory_fps_panel_select.png)
- You can adjust a scale of the template if required
- Add Druid and widget setup to your `*.gui_script`
```lua
local druid = require("druid.druid")
local memory_panel = require("druid.widget.memory_panel.memory_panel")
function init(self)
self.druid = druid.new(self)
-- "memory_panel" is a name of the template in the GUI scene, often it matches the name of the template file
self.memory_panel = self.druid:new_widget(memory_panel, "memory_panel")
end
function final(self)
self.druid:final()
end
function update(self, dt)
self.druid:update(dt)
end
function on_message(self, message_id, message, sender)
self.druid:on_message(message_id, message, sender)
end
function on_input(self, action_id, action)
return self.druid:on_input(action_id, action)
end
```
And make sure of:
- The `*.gui_script` is attached to your `*.gui` scene
- The GUI component is added to your game scene
## FPS Panel
The `FPS Panel` is a widget which allows you to monitor FPS of your game. It displays the last 3 seconds of FPS graph, lowest and current FPS values
### How to add:
- Add `/druid/widget/fps_panel/fps_panel.gui` to your `*.gui` scene
- You can adjust a scale of the template if required
- Add Druid and widget setup to your `*.gui_script`
```lua
local druid = require("druid.druid")
local fps_panel = require("druid.widget.fps_panel.fps_panel")
function init(self)
self.druid = druid.new(self)
-- "fps_panel" is a name of the template in the GUI scene, often it matches the name of the template file
self.fps_panel = self.druid:new_widget(fps_panel, "fps_panel")
end
function final(self)
self.druid:final()
end
function update(self, dt)
self.druid:update(dt)
end
function on_message(self, message_id, message, sender)
self.druid:on_message(message_id, message, sender)
end
function on_input(self, action_id, action)
return self.druid:on_input(action_id, action)
end
```
And make sure of:
- The `*.gui_script` is attached to your `*.gui` scene
- The GUI component is added to your game scene
These widgets not only can be useful for development and profiling your game, but also as an example of how to create custom widgets with Druid and use them in your game.
Thanks for reading!