-
-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
390 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,390 @@ | ||
local helper = require("druid.helper") | ||
local component = require("druid.component") | ||
|
||
---@class druid.figma_layout.row_data | ||
---@field width number | ||
---@field height number | ||
---@field count number | ||
|
||
---@class druid.figma_layout.rows_data | ||
---@field total_width number | ||
---@field total_height number | ||
---@field nodes_width table<node, number> | ||
---@field nodes_height table<node, number> | ||
---@field rows druid.figma_layout.row_data[]> | ||
|
||
---@class druid.figma_layout: druid.base_component | ||
local M = component.create("layout") | ||
|
||
|
||
--- The @{Layout} constructor | ||
-- @tparam Layout self @{Layout} | ||
-- @tparam node node Gui node | ||
-- @tparam string layout_type The layout mode (from const.LAYOUT_MODE) | ||
-- @tparam function|nil on_size_changed_callback The callback on window resize | ||
function M:init(node, layout_type) | ||
self.node = self:get_node(node) | ||
|
||
print(self.node) | ||
self.is_dirty = true | ||
self.entities = {} | ||
self.margin = { x = 0, y = 0 } | ||
self.padding = gui.get_slice9(self.node) | ||
self.type = layout_type or "horizontal" | ||
self.is_resize_width = false | ||
self.is_resize_height = false | ||
self.is_justify = false | ||
end | ||
|
||
|
||
function M:update() | ||
if not self.is_dirty then | ||
return | ||
end | ||
|
||
self:refresh_layout() | ||
end | ||
|
||
|
||
---@param margin_x number|nil | ||
---@param margin_y number|nil | ||
---@return druid.figma_layout | ||
function M:set_margin(margin_x, margin_y) | ||
self.margin.x = margin_x or self.margin.x | ||
self.margin.y = margin_y or self.margin.y | ||
self.is_dirty = true | ||
|
||
return self | ||
end | ||
|
||
|
||
---@param padding vector4 The vector4 with padding values, where x - left, y - top, z - right, w - bottom | ||
function M:set_padding(padding) | ||
self.padding = padding | ||
self.is_dirty = true | ||
|
||
return self | ||
end | ||
|
||
|
||
function M:set_dirty() | ||
self.is_dirty = true | ||
|
||
return self | ||
end | ||
|
||
|
||
---@param is_justify boolean | ||
---@return druid.figma_layout | ||
function M:set_justify(is_justify) | ||
self.is_justify = is_justify | ||
self.is_dirty = true | ||
|
||
return self | ||
end | ||
|
||
|
||
---@param type string The layout type: "horizontal", "vertical", "horizontal_wrap" | ||
function M:set_type(type) | ||
self.type = type | ||
self.is_dirty = true | ||
|
||
return self | ||
end | ||
|
||
|
||
---@param is_hug boolean | ||
---@return druid.figma_layout | ||
function M:set_hug_content(entity, is_hug) | ||
self.is_resize_width = is_hug | ||
self.is_resize_height = is_hug | ||
self.is_dirty = true | ||
|
||
return entity | ||
end | ||
|
||
---@param node_or_node_id string|node | ||
---@return druid.figma_layout | ||
function M:add(node_or_node_id) | ||
-- Acquire node from entity or by id | ||
local node = node_or_node_id | ||
if type(node_or_node_id) == "table" then | ||
assert(node_or_node_id.node, "The entity should have a node") | ||
node = node_or_node_id.node | ||
else | ||
---@cast node_or_node_id string|node | ||
node = self:get_node(node_or_node_id) | ||
end | ||
|
||
---@cast node node | ||
table.insert(self.entities, node) | ||
gui.set_parent(node, self.node) | ||
|
||
self.is_dirty = true | ||
|
||
return self | ||
end | ||
|
||
|
||
---@return druid.figma_layout | ||
function M:refresh_layout() | ||
local layout_node = self.node | ||
|
||
local entities = self.entities | ||
local type = self.type -- vertical, horizontal, horizontal_wrap | ||
local margin = self.margin -- {x: horizontal, y: vertical} in pixels, between elements | ||
local padding = self.padding -- {x: left, y: top, z: right, w: bottom} in pixels | ||
local is_justify = self.is_justify | ||
local size = gui.get_size(layout_node) | ||
local max_width = size.x - padding.x - padding.z | ||
local max_height = size.y - padding.y - padding.w | ||
local layout_pivot_offset = helper.get_pivot_offset(gui.get_pivot(layout_node)) -- {x: -0.5, y: -0.5} - is left bot, {x: 0.5, y: 0.5} - is right top | ||
|
||
local rows_data = self:calculate_rows_data() | ||
local rows = rows_data.rows | ||
local row_index = 1 | ||
local row = rows[row_index] | ||
|
||
-- Current x and Current y is a top left corner of the node | ||
local current_x = -row.width * (0.5 + layout_pivot_offset.x) | ||
local current_y = rows_data.total_height * (0.5 - layout_pivot_offset.y) | ||
|
||
if is_justify then | ||
if (type == "horizontal" or type == "horizontal_wrap") and row.count > 1 then | ||
current_x = -max_width * (0.5 + layout_pivot_offset.x) | ||
end | ||
if type == "vertical" then | ||
current_y = max_height * (0.5 - layout_pivot_offset.y) | ||
end | ||
end | ||
|
||
for index = 1, #entities do | ||
local node = entities[index] | ||
local node_width = rows_data.nodes_width[node] | ||
local node_height = rows_data.nodes_height[node] | ||
local pivot_offset = helper.get_pivot_offset(gui.get_pivot(node)) | ||
|
||
if node_width > 0 and node_height > 0 then | ||
-- Calculate position for current node | ||
local position_x, position_y | ||
|
||
if type == "horizontal" then | ||
position_x = current_x + node_width * (0.5 + pivot_offset.x) | ||
position_y = current_y - row.height * (0.5 - pivot_offset.y) | ||
|
||
local node_margin = margin.x | ||
if is_justify and row.count > 1 then | ||
node_margin = (max_width - row.width) / (row.count - 1) + margin.x | ||
end | ||
current_x = current_x + node_width + node_margin | ||
end | ||
|
||
if type == "vertical" then | ||
position_x = current_x + row.width * (0.5 - pivot_offset.x) | ||
position_y = current_y - node_height * (0.5 + pivot_offset.y) | ||
|
||
local node_margin = margin.y | ||
if is_justify then | ||
node_margin = (max_height - rows_data.total_height) / (#rows - 1) + margin.y | ||
end | ||
|
||
current_y = current_y - node_height - node_margin | ||
end | ||
|
||
if type == "horizontal_wrap" then | ||
local width = row.width | ||
if is_justify and row.count > 0 then | ||
width = math.max(row.width, max_width) | ||
end | ||
local new_row_width = width * (0.5 - layout_pivot_offset.x) | ||
|
||
-- Compare with eps due the float loss and element flickering | ||
if current_x + node_width - new_row_width > 0.0001 then | ||
if row_index < #rows then | ||
row_index = row_index + 1 | ||
row = rows[row_index] | ||
end | ||
|
||
current_x = -row.width * (0.5 + layout_pivot_offset.x) | ||
current_y = current_y - row.height - margin.y | ||
if is_justify and row.count > 1 then | ||
current_x = -max_width * (0.5 + layout_pivot_offset.x) | ||
end | ||
end | ||
|
||
position_x = current_x + node_width * (0.5 + pivot_offset.x) | ||
position_y = current_y - row.height * (0.5 - pivot_offset.y) | ||
|
||
local node_margin = margin.x | ||
if is_justify and row.count > 1 then | ||
node_margin = (max_width - row.width) / (row.count - 1) + margin.x | ||
end | ||
current_x = current_x + node_width + node_margin | ||
end | ||
|
||
do -- Padding offset | ||
if layout_pivot_offset.x == -0.5 then | ||
position_x = position_x + padding.x | ||
end | ||
if layout_pivot_offset.y == 0.5 then | ||
position_y = position_y - padding.y | ||
end | ||
if layout_pivot_offset.x == 0.5 then | ||
position_x = position_x - padding.z | ||
end | ||
if layout_pivot_offset.y == -0.5 then | ||
position_y = position_y + padding.w | ||
end | ||
end | ||
|
||
self:set_node_position(node, position_x, position_y) | ||
end | ||
end | ||
|
||
if self.is_resize_width or self.is_resize_height then | ||
if self.is_resize_width then | ||
size.x = rows_data.total_width + padding.x + padding.z | ||
end | ||
if self.is_resize_height then | ||
size.y = rows_data.total_height + padding.y + padding.w | ||
end | ||
gui.set_size(layout_node, size) | ||
end | ||
|
||
self.is_dirty = false | ||
|
||
return self | ||
end | ||
|
||
|
||
---@return druid.figma_layout | ||
function M:clear_layout() | ||
for index = #self.entities, 1, -1 do | ||
self.entities[index] = nil | ||
end | ||
|
||
self.is_dirty = true | ||
|
||
return self | ||
end | ||
|
||
|
||
---@param node node | ||
---@return number, number | ||
function M.get_node_size(node) | ||
if not gui.is_enabled(node, false) then | ||
return 0, 0 | ||
end | ||
|
||
local scale = gui.get_scale(node) | ||
|
||
-- If node has text - get text size instead of node size | ||
if gui.get_text(node) then | ||
local text_metrics = helper.get_text_metrics_from_node(node) | ||
return text_metrics.width * scale.x, text_metrics.height * scale.y | ||
end | ||
|
||
local size = gui.get_size(node) | ||
return size.x * scale.x, size.y * scale.y | ||
end | ||
|
||
|
||
---Calculate rows data for layout. Contains total width, height and rows info (width, height, count of elements in row) | ||
---@private | ||
---@return druid.figma_layout.rows_data | ||
function M:calculate_rows_data() | ||
local entities = self.entities | ||
local margin = self.margin | ||
local type = self.type | ||
local padding = self.padding | ||
|
||
local size = gui.get_size(self.node) | ||
local max_width = size.x - padding.x - padding.z | ||
|
||
-- Collect rows info about width, height and count of elements in row | ||
local current_row = { width = 0, height = 0, count = 0 } | ||
local rows_data = { | ||
total_width = 0, | ||
total_height = 0, | ||
nodes_width = {}, | ||
nodes_height = {}, | ||
rows = { current_row } | ||
} | ||
|
||
for index = 1, #entities do | ||
local node = entities[index] | ||
local node_width = rows_data.nodes_width[node] | ||
local node_height = rows_data.nodes_height[node] | ||
|
||
-- Get node size if it's not calculated yet | ||
if not node_width or not node_height then | ||
node_width, node_height = M.get_node_size(node) | ||
rows_data.nodes_width[node] = node_width | ||
rows_data.nodes_height[node] = node_height | ||
end | ||
|
||
if node_width > 0 and node_height > 0 then | ||
if type == "horizontal" then | ||
current_row.width = current_row.width + node_width + margin.x | ||
current_row.height = math.max(current_row.height, node_height) | ||
current_row.count = current_row.count + 1 | ||
end | ||
|
||
if type == "vertical" then | ||
if current_row.count > 0 then | ||
current_row = { width = 0, height = 0, count = 0 } | ||
table.insert(rows_data.rows, current_row) | ||
end | ||
|
||
current_row.width = math.max(current_row.width, node_width + margin.x) | ||
current_row.height = node_height | ||
current_row.count = current_row.count + 1 | ||
end | ||
|
||
if type == "horizontal_wrap" then | ||
if current_row.width + node_width > max_width and current_row.count > 0 then | ||
current_row = { width = 0, height = 0, count = 0 } | ||
table.insert(rows_data.rows, current_row) | ||
end | ||
|
||
current_row.width = current_row.width + node_width + margin.x | ||
current_row.height = math.max(current_row.height, node_height) | ||
current_row.count = current_row.count + 1 | ||
end | ||
end | ||
end | ||
|
||
-- Remove last margin of each row | ||
-- Calculate total width and height | ||
local rows_count = #rows_data.rows | ||
for index = 1, rows_count do | ||
local row = rows_data.rows[index] | ||
if row.width > 0 then | ||
row.width = row.width - margin.x | ||
end | ||
|
||
rows_data.total_width = math.max(rows_data.total_width, row.width) | ||
rows_data.total_height = rows_data.total_height + row.height | ||
end | ||
|
||
rows_data.total_height = rows_data.total_height + margin.y * (rows_count - 1) | ||
return rows_data | ||
end | ||
|
||
|
||
---@private | ||
---@param node node | ||
---@param x number | ||
---@param y number | ||
---@return node | ||
function M:set_node_position(node, x, y) | ||
local position = gui.get_position(node) | ||
position.x = x | ||
position.y = y | ||
gui.set_position(node, position) | ||
|
||
return node | ||
end | ||
|
||
|
||
return M |