layout.lua
  1  -- File: layout.lua
  2  
  3  -- RESOURCES
  4  --   https://www.w3.org/TR/2011/REC-CSS2-20110607/#minitoc
  5  --   https://github.com/tensor-programming/rust_browser_part_4/blob/master/src/layout.rs
  6  
  7  local luaWidgetDir = 'LuaUI/Widgets/'
  8  
  9  local Element = VFS.Include(luaWidgetDir..'chmod777_includes/html_css/element.lua')
 10  
 11  local Rect = {}
 12  function Rect:new(x, y, width, height)
 13  	if x == nil then x = 0 end
 14  	if y == nil then y = 0 end
 15  	if width == nil then width = 0 end
 16  	if height == nil then height = 0 end
 17  
 18  	local this = {
 19  		x = x,
 20  		y = y,
 21  		width = width,
 22  		height = height,
 23  	}
 24  	function this:expand(edge_size)
 25  		local new_x = this.x - edge_size.left
 26  		local new_y = this.y - edge_size.top
 27  		local new_width = this.width + edge_size.left + edge_size.right
 28  		local new_height = this.height + edge_size.top + edge_size.bottom
 29  		return Rect:new(new_x, new_y, new_width, new_height)
 30  	end
 31  
 32  	function this:print()
 33  		Spring.Echo("x: "..this.x.." y: "..this.y.." w: "..this.width.." h: "..this.height)
 34  	end
 35  
 36  	return this
 37  end
 38  
 39  local EdgeSizes = {}
 40  function EdgeSizes:new(top, right, bottom, left)
 41  	if top == nil then top = 0 end
 42  	if right == nil then right = 0 end
 43  	if bottom == nil then bottom = 0 end
 44  	if left == nil then left = 0 end
 45  
 46  	local this = {
 47  		top = top,
 48  		right = right,
 49  		bottom = bottom,
 50  		left = left,
 51  	}
 52  
 53  	function this:print()
 54  		Spring.Echo("t: "..this.top.." r: "..this.right.." b: "..this.bottom.." l: "..this.left)
 55  	end
 56  
 57  	return this
 58  end
 59  
 60  local BoxType = {
 61  	"block",
 62  	"inline",
 63  	"inline-block",
 64  	"anonymous",
 65  }
 66  
 67  local Dimensions = {}
 68  ---@param content Rect
 69  ---@param padding EdgeSizes
 70  ---@param border EdgeSizes
 71  ---@param margin EdgeSizes
 72  ---@param current Rect
 73  ---@return Dimensions
 74  function Dimensions:new(content, padding, border, margin, current)
 75  	if content == nil then content = Rect:new() end
 76  	if padding == nil then padding = EdgeSizes:new() end
 77  	if border == nil then border = EdgeSizes:new() end
 78  	if margin == nil then margin = EdgeSizes:new() end
 79  	if current == nil then current = Rect:new() end
 80  
 81  	local this = {
 82  		content = content,
 83  		padding = padding,
 84  		border = border,
 85  		margin = margin,
 86  		current = current,
 87  	}
 88  
 89  	function this:print()
 90  		this.content:print()
 91  		this.padding:print()
 92  		this.border:print()
 93  		this.margin:print()
 94  	end
 95  
 96  	---@return Rect
 97  	function this:padding_box()
 98  		return this.content:expand(this.padding)
 99  	end
100  	---@return Rect
101  	function this:border_box()
102  		return this:padding_box():expand(this.border)
103  	end
104  	---@return Rect
105  	function this:margin_box()
106  		return this:border_box():expand(this.margin)
107  	end
108  	return this
109  end
110  
111  local Layout = {}
112  function Layout:new(style_node, box_type, dimensions, children)
113  	if style_node == nil then end
114  	if box_type == nil then box_type = BoxType[1] end
115  	if dimensions == nil then dimensions = Dimensions:new() end
116  	if children == nil then children = {} end
117  
118  	local this = {
119  		dimensions = dimensions,
120  		box_type = box_type,
121  		style_node = style_node,
122  		children = children,
123  	}
124  
125  	function this:layout(parent_dim)
126  		local box_type = this.box_type
127  		if box_type == "block" then
128  			this:layout_block(parent_dim)
129  		elseif box_type == "inline" then
130  			this:layout_inline(parent_dim)
131  		elseif box_type == "inline-block" then
132  			this:layout_inline_block(parent_dim)
133  		elseif box_type == "anonymous" then
134  			this:layout_anonymous()
135  		end
136  	end
137  	function this:layout_block(parent_dim)
138  		this:calculate_width(parent_dim)
139  		this:calculate_position(parent_dim)
140  		this:layout_children()
141  		this:calculate_height(parent_dim)
142  	end
143  	function this:layout_inline(parent_dim)
144  	end
145  	function this:layout_inline_block(parent_dim)
146  		this:calculate_inline_width(parent_dim)
147  		this:calculate_inline_position(parent_dim)
148  		this:layout_children()
149  		this:calculate_height(parent_dim)
150  	end
151  	function this:layout_anonymous()
152  	end
153  
154  	function this:calculate_width(parent_dim)
155  		local style_node = this.style_node
156  		local dimensions = this.dimensions
157  
158  		local width = style_node:get_absolute_value_or("width", 0, parent_dim.content.width)
159  		dimensions.padding.left = style_node:get_absolute_value_or("padding-left", 0)
160  		dimensions.padding.right = style_node:get_absolute_value_or("padding-right", 0)
161  		dimensions.border.left = style_node:get_absolute_value_or("border-left-width", 0)
162  		dimensions.border.right = style_node:get_absolute_value_or("border-right-width", 0)
163  		local margin_left = style_node:get_declaration("margin-left", false)
164  		local margin_right = style_node:get_declaration("margin-right", false)
165  		local margin_left_value = style_node:get_absolute_value_or("margin-left", 0)
166  		local margin_right_value = style_node:get_absolute_value_or("margin-right", 0)
167  
168  		local total = width
169  			+ dimensions.padding.left
170  			+ dimensions.padding.right
171  			+ dimensions.border.left
172  			+ dimensions.border.right
173  			+ margin_left_value
174  			+ margin_right_value
175  
176  		local underflow = parent_dim.content.width - total
177  		
178  		if width == 0 then
179  			if underflow > 0 then
180  				dimensions.content.width = underflow
181  				dimensions.margin.right = margin_right_value
182  			else
183  				dimensions.content.width = width
184  				dimensions.margin.right = margin_right_value + underflow
185  			end
186  			dimensions.margin.left = margin_left_value
187  		elseif margin_left == nil and margin_right ~= nil then
188  			if width ~= 0 then
189  				dimensions.content.width = width
190  				dimensions.margin.left = underflow
191  				dimensions.margin.right = margin_right_value
192  			end
193  		elseif margin_left ~= nil and margin_right == nil then
194  			if width ~= 0 then
195  				dimensions.content.width = width
196  				dimensions.margin.left = margin_left_value
197  				dimensions.margin.right = underflow
198  			end
199  		elseif margin_left == nil and margin_right == nil then
200  			if width ~= 0 then
201  				dimensions.content.width = width
202  				dimensions.margin.left = underflow / 2
203  				dimensions.margin.right = underflow / 2
204  			end
205  		else
206  			dimensions.content.width = width
207  			dimensions.margin.left = margin_left_value
208  			dimensions.margin.right = margin_right_value + underflow
209  		end
210  	end
211  	function this:calculate_position(parent_dim)
212  		local style_node = this.style_node
213  		local dimensions = this.dimensions
214  
215  		dimensions.padding.top = style_node:get_absolute_value_or("padding-top", 0)
216  		dimensions.padding.bottom = style_node:get_absolute_value_or("padding-bottom", 0)
217  		dimensions.border.top = style_node:get_absolute_value_or("border-top-width", 0)
218  		dimensions.border.bottom = style_node:get_absolute_value_or("border-bottom-width", 0)
219  		dimensions.margin.top = style_node:get_absolute_value_or("margin-top", 0)
220  		dimensions.margin.bottom = style_node:get_absolute_value_or("margin-bottom", 0)
221  
222  		dimensions.content.x = parent_dim.content.x
223  			+ dimensions.padding.left
224  			+ dimensions.border.left
225  			+ dimensions.margin.left
226  		dimensions.content.y = parent_dim.content.height
227  			+ parent_dim.content.y
228  			+ dimensions.padding.top
229  			+ dimensions.border.top
230  			+ dimensions.margin.top
231  	end
232  
233  	function this:calculate_inline_width(parent_dim)
234  		local style_node = this.style_node
235  		local dimensions = this.dimensions
236  
237  		dimensions.content.width = style_node:get_absolute_value_or("width", 0, parent_dim.content.width)
238  
239  		dimensions.padding.left = style_node:get_absolute_value_or("padding-left", 0)
240  		dimensions.padding.right = style_node:get_absolute_value_or("padding-right", 0)
241  		
242  		dimensions.border.left = style_node:get_absolute_value_or("border-left-width", 0)
243  		dimensions.border.right = style_node:get_absolute_value_or("border-right-width", 0)
244  		
245  		dimensions.margin.left = style_node:get_absolute_value_or("margin-left", 0)
246  		dimensions.margin.right = style_node:get_absolute_value_or("margin-right", 0)
247  	end
248  	function this:calculate_inline_position(parent_dim)
249  		local style_node = this.style_node
250  		local dimensions = this.dimensions
251  
252  		dimensions.padding.top = style_node:get_absolute_value_or("padding-top", 0)
253  		dimensions.padding.bottom = style_node:get_absolute_value_or("padding-bottom", 0)
254  		
255  		dimensions.border.top = style_node:get_absolute_value_or("border-top-width", 0)
256  		dimensions.border.bottom = style_node:get_absolute_value_or("border-bottom-width", 0)
257  		
258  		dimensions.margin.top = style_node:get_absolute_value_or("margin-top", 0)
259  		dimensions.margin.bottom = style_node:get_absolute_value_or("margin-bottom", 0)
260  
261  		dimensions.content.x = parent_dim.content.x
262  			+ parent_dim.current.x
263  			+ dimensions.padding.left
264  			+ dimensions.border.left
265  			+ dimensions.margin.left
266  		dimensions.content.y = parent_dim.content.height
267  			+ parent_dim.content.y
268  			+ dimensions.padding.top
269  			+ dimensions.border.top
270  			+ dimensions.margin.top
271  	end
272  
273  	function this:layout_children()
274  		local dimensions = this.dimensions
275  		local max_child_height = 0
276  		local previous_box_type = "block"
277  		for c,child in ipairs(this.children) do
278  			if previous_box_type == "inline-block" then
279  				if child.box_type == "block" then
280  					dimensions.content.height = dimensions.content.height + max_child_height
281  					dimensions.current.x = 0
282  				end
283  			end
284  
285  			child:layout(dimensions)
286  
287  			local new_height = child.dimensions:margin_box().height
288  			if new_height > max_child_height then
289  				max_child_height = new_height
290  			end
291  			
292  			if child.box_type == "block" then
293  				dimensions.content.height = dimensions.content.height + child.dimensions:margin_box().height
294  			elseif child.box_type == "inline-block" then
295  				dimensions.current.x = dimensions.current.x + child.dimensions:margin_box().width
296  				if dimensions.current.x > dimensions.content.width then
297  					dimensions.content.height = dimensions.content.height + max_child_height
298  					dimensions.current.x = 0
299  					child:layout(dimensions)
300  					dimensions.current.x = dimensions.current.x + child.dimensions:margin_box().width
301  				end
302  			end
303  
304  			previous_box_type = child.box_type
305  		end
306  	end
307  	function this:calculate_height(parent_dim)
308  		local style_node = this.style_node
309  		local dimensions = this.dimensions
310  		local height = style_node:get_absolute_value_or("height", 0, parent_dim.content.height)
311  		if height ~= nil then
312  			dimensions.content.height = height
313  		end
314  	end
315  
316  	return this
317  end
318  
319  function layout_tree(root, parent_dim)
320  	parent_dim.content.height = 0
321  	local root_box = build_layout_tree(root)
322  	root_box:layout(parent_dim)
323  	return root_box
324  end
325  
326  function build_layout_tree(style_node)
327  	local display = style_node:get_value_or("display", "block")
328  	if display == "none" then
329  		display = "anonymous"
330  	end
331  	Spring.Echo("'"..display.."'")
332  	local layout_node = Layout:new(style_node, display)
333  	for i,child in ipairs(style_node.children) do
334  		local child_display = child:get_value_or("display", "block")
335  		if child_display ~= "none" then
336  			layout_node.children[#layout_node.children+1] = build_layout_tree(child)
337  		end
338  	end
339  	return layout_node
340  end
341  
342  return {
343  	Rect = Rect,
344  	EdgeSizes = EdgeSizes,
345  	Dimensions = Dimensions,
346  	Layout = Layout,
347  	layout_tree = layout_tree,
348  	build_layout_tree = build_layout_tree,
349  }