aboutsummaryrefslogblamecommitdiff
path: root/src/syslua/lx/tk.lua
blob: 628ab9100f388546df20bba6660f1b6d2cdad8f6 (plain) (tree)
1
2
3
4
5




                              























































                                                                                                                    












































                                                                                                                          








                                                                 












                                                                                                                    



                                       
                   

                      

                                
                                              
         




                                          
 




                                                                                   
                                                          
                                               









                                                                                      

           




                                                                                    
           





                                                                                 

                   
 





                                                                                

           







                                                                   


                                                                                                               

                                                                                                                                           


                   




                                                                                         
                                                  
                                               
                          

                                                    

           
                                                
                                             


                                                    



                                                              


                                             















                                          





                                                    
                    


                                                                                   
         




                                               
 
                                                                        




                                                                                                  

                                                                                                                    
 






















                                                                                                                                                             

                   
 

                                                                       

   





                                       


                                      
                                                                

                       
                                        

                                   

                                                                            



                                                                                                                              

                           


                                                                                                 




                    






                                             
                                                                  





                                                               






                                                                                                                                           

                       
                                      


                                                                              


                                                                                                                                                                   




















                                                                                                                                           

                                              
                                                          

                                                                              
                                                   


                                                                  
                                

                         
 
                                               
                                           
                                                                      
                    








                                                                                              







                                                             


                                                               










                                                                                                                            

                           





                                           






                                                                                   







                                                                                                             
                                                                        










                                                                    
                                      

                                             
                                                                                           

                                                                                                                                                  
                         
                                                                          
                                                                                           




                                                                                                                                                 
                                                                                         


                                                    

                                                                                                                                     


                                                                                                            
                                                                                                                         


                                                                                                              
                                                                                                                        







































































                                                                                                                                                       









                                                 

                                                                                         

































                                                                                                               




































                                                                               

                                                                                
























































                                                                                          
                                                        




                   


                                            

                                                





                           
 





                                                                                   
                                 

                                               
                                                                                            





                                     

                                                  
 

                                                              
                                                                
 





                                                                                                               
 



                                                                       


                           

                                                                

                                                                                         
                                                                                           






















                                                                                                          
                                                                

                                                         

                                                                                                               
                                                                       
                                                            
                                                                     
                                   
                            
                                                                                    



                                    

                          












                                                                       
                                     

                           
                                          
 
                                    

           
                                   
                                                                

           
                                     
                                                                                         
 
                                               

                                             




                                                                                                     

                                                                                                                                                             





                                                                                   
                                   

                                                      

                           
















                                                                                                                                      












                                                                                                                              



                                                   
                                                  





                                               

















                                                                                               
                                                                    


                                                                                  





                                                                             


                                                               
                                                                              







                                                                                                                      

         
local draw = require 'lx.draw'
local sys = require 'lx.sys'

local tk = {}

--[[

	Widgets:

	window manager widget
	Window manager
	Widows are any widget which determine their own size

	box widget
	A box that can be:
	- scrollable (H/V)
	- resizable (H/V)
	Has a single child. Can either:
	a. constrain the size of the child to the size of the box
	b. let the child widget determine its size
	Can have a margin (outer margin) with associated outer background color,
	         a padding (inner margin) with associated background color,
			 a border with its color
	Can have onclick events to implement buttons.

	grid widget
	A grid widget, organizes its children in a grid, which can make a table or any kind of layout. It:
	- resizes its childs and repositions them to make a grid
	- accepts when its childs are resized and changes the width of columns
	  or the height of lines accordingly
	- draws borders/spacing between elements

	text widget

	image widget

	border widget

	centered widget

	text input

	text box



	text_layout_widget
	Positions text widgets and images widgets in a subtle and intelligent manner (lol)


How to make a table:

	widget = tk.grid({},
		{ { tk.box({h_resizable = true, width = 200}, tk.text("name")),
		    tk.box({h_resizable = true, width = 100}, tk.text("actions")) },
		  { tk.box({padding = 4}, tk.text("file A")),
		    tk.grid({}, {{ tk.box({padding = 4, border = 1, border_color = red,
			 			   on_click = function() do something end}, tk.text("open")) }}) } }

--]]

-- UTIL

function region_operation(reg1, reg2, sel_fun)
	local xs = {reg1.x, reg1.x + reg1.w, reg2.x, reg2.x + reg2.w}
	local ys = {reg1.y, reg1.y + reg1.h, reg2.y, reg2.y + reg2.h}
	table.sort(xs)
	table.sort(ys)
	local pieces = {}
	local function add_region(x, y, w, h)
		for i, r in pairs(pieces) do
			if r.y == y and r.h == h and r.x + r.w == x then
				table.remove(pieces, i)
				add_region(r.x, r.y, r.w + w, r.h)
				return
			elseif r.x == x and r.w == w and r.y + r.h == y then
				table.remove(pieces, i)
				add_region(r.x, r.y, r.w, r.h + h)
				return
			end
		end
		table.insert(pieces, {x = x, y = y, w = w, h = h})
	end
	for i = 1, 3 do
		for j = 1, 3 do
			local x0, x1, y0, y1 = xs[i], xs[i+1], ys[j], ys[j+1]
			if x1 > x0 and y1 > y0 then
				if sel_fun(x0, x1, y0, y1) then
					add_region(x0, y0, x1 - x0, y1 - y0)
				end
			end
		end
	end
	return pieces
end

function region_diff(reg1, reg2)
	return region_operation(reg1, reg2,
		function(x0, x1, y0, y1)
			return (x0 >= reg1.x and x0 < reg1.x + reg1.w and y0 >= reg1.y and y0 < reg1.y + reg1.h)
				and not (x0 >= reg2.x and x0 < reg2.x + reg2.w and y0 >= reg2.y and y0 < reg2.y + reg2.h) 
		end
	)
end

function region_inter(reg1, reg2)
	local x0 = math.max(reg1.x, reg2.x)
	local x1 = math.min(reg1.x + reg1.w, reg2.x + reg2.w)
	local y0 = math.max(reg1.y, reg2.y)
	local y1 = math.min(reg1.y + reg1.h, reg2.y + reg2.h)
	if x1 > x0 and y1 > y0 then
		return {x = x0, y = y0, w = x1 - x0, h = y1 - y0}
	else
		return nil
	end
end

function region_union(reg1, reg2)
	return region_operation(reg1, reg2,
		function(x0, x1, y0, y1)
			return (x0 >= reg1.x and x0 < reg1.x + reg1.w and y0 >= reg1.y and y0 < reg1.y + reg1.h) 
				or (x0 >= reg2.x and x0 < reg2.x + reg2.w and y0 >= reg2.y and y0 < reg2.y + reg2.h)
		end
	)
end



-- ACTUAL TK STUFF


function tk.widget(width, height, opts)
	local w = {
		x = 0,
		y = 0,
		width = width,
		height = height,
		on_click = function(self) end,
	}
	if opts then
		for k, v in pairs(opts) do
			w[k] = v
		end
	end

	-- Funcions that must be implemented by a parent widget :
	-- - resize_child : try to resize a child widget, checks if can be resized,
	-- 					does all the required redrawing
	-- - redraw : redraws a portion of the widget

	-- best resize : find best dimensions for resizing
	-- widget can replace this if necessary
	function w:best_resize(width, height)
		width = width or self.width
		width = math.max(self.min_width or 1, width)
		if self.max_width then width = math.min(width, self.max_width) end

		height = height or self.height
		height = math.max(self.min_height or 1, height)
		if self.max_height then height = math.min(height, self.max_height) end

		return width, height
	end

	-- do resize : updates position and size values but does not redraw anything
	-- widget must replace this when necessary !
	function w:do_resize(width, height)
		if width then self.width = width end
		if height then self.height = height end
	end

	-- resize : calls all the necessary procedures for resizing and redrawing
	-- widget must not replace this !
	function w:resize(width, height)
		if self.parent then
			self.parent:resize_child(self, width, height)
		end
	end

	-- redraw : calls parent to redraw a portion of screen
	-- can be replaced by widget to avoid redrawing where hidden things are
	function w:redraw(x0, y0, w, h, who)
		if self.parent then
			self.parent:redraw(x0 + self.x, y0 + self.y, w, h, self)
		end
	end

	-- draw : does the actual drawing on a buffer
	-- must be replaced by widget by code that does the drawing
	function w:draw(x0, y0, buf)
	end

	-- helper function to redraw subwidget
	-- must not be replaced by child
	function w:draw_sub(x0, y0, buf, subwidget)
		local region = {x = x0, y = y0, w = buf:width(), h = buf:height()}
		local subregion = {x = subwidget.x, y = subwidget.y, w = subwidget.width, h = subwidget.height}
		local inter = region_inter(region, subregion)
		if inter then
			subwidget:draw(inter.x - subwidget.x, inter.y - subwidget.y, buf:sub(inter.x - x0, inter.y - y0, inter.w, inter.h))
		end
	end

	function w:get_child(x, y)
		-- Replaced by widget by code that finds a child widget at given position
		return nil
	end

	function w:on_mouse_down(x, y, lb, rb, mb)
		-- Handler for mouse down event
		if lb then
			self.left_click_valid = true
		end
	end

	function w:on_mouse_up(x, y, lb, rb, mb)
		-- Handler for mouse up event
		if lb and self.left_click_valid then
			self:on_click()
		end
	end

	function w:on_mouse_move(prev_x, prev_y, new_x, new_y)
		-- Handler for mouse move event
		self.left_click_valid = false
	end

	function w:on_text_input(char)
		-- Handler for text input
	end

	function w:on_key_down(key)
		-- Handler for key press
	end

	function w:on_key_up(key)
		-- Handler for key release
	end

	return w
end

function tk.init(gui, root_widget)
	if tk.root_widget ~= nil then return end
	tk.root_widget = root_widget

	if root_widget.parent ~= nil then return end
	root_widget.parent = gui
	
	tk.fonts = {
		["vera.ttf"] = draw.load_ttf_font("sys:/fonts/vera.ttf"),
		["veraserif.ttf"] = draw.load_ttf_font("sys:/fonts/veraserif.ttf"),
		["veramono.ttf"] = draw.load_ttf_font("sys:/fonts/veramono.ttf"),
	}
	tk.fonts.default = tk.fonts["vera.ttf"]

	function tk.rgb(r, g, b)
		return gui.surface:rgb(r, g, b)
	end

	root_widget:do_resize(gui.surface:width(), gui.surface:height())

	gui.on_key_down = function(key) root_widget:on_key_down(key) end
	gui.on_key_up = function(key) root_widget:on_key_up(key) end
	gui.on_text_input = function(key) root_widget:on_text_input(char) end
	gui.on_mouse_move = function(ox, oy, nx, ny) root_widget:on_mouse_move(ox, oy, nx, ny) end
	gui.on_mouse_down = function(lb, rb, mb) root_widget:on_mouse_down(gui.mouse_x, gui.mouse_y, lb, rb, mb) end
	gui.on_mouse_up = function(lb, rb, mb) root_widget:on_mouse_up(gui.mouse_x, gui.mouse_y, lb, rb, mb) end

	local root_parent = {}
	function root_parent:resize_child(c)
		-- NO ! Do nothing.
	end
	function root_parent:redraw(x0, y0, w, h)
		local ss = region_inter({x = x0, y = y0, w = w, h = h},
								{x = 0, y = 0, w = gui.surface:width(), h = gui.surface:height()})

		if ss then
			local must_reshow_cursor = false
			if gui.cursor_visible and region_inter(ss, {x=gui.mouse_x, y=gui.mouse_y,
														w=gui.cursor:width(), h=gui.cursor:height()})
			then
				must_reshow_cursor = true
				gui.hide_cursor()
			end

			local buf = gui.surface:sub(ss.x, ss.y, ss.w, ss.h)
			root_widget:draw(ss.x, ss.y, buf)

			if must_reshow_cursor then
				gui.show_cursor()
			end
		end
	end

	root_widget.parent = root_parent
	root_parent:redraw(0, 0, root_widget.width, root_widget.height)
end

-- #### #### STANDARD WIDGETS #### ####

function tk.image(a, b)
	local img = b or a
	local opts = b and a or {}

	opts.min_width = img:width()
	opts.min_height = img:height()

	local image = tk.widget(img:width(), img:height(), opts)
	image.img = img

	function image:draw(x0, y0, buf)
		local step = 20
		local halfstep = 10
		for x = x0 - (x0 % step), x0 + buf:width(), step do
			for y = y0 - (y0 % step), y0 + buf:height(), step do
				buf:fillrect(x - x0, y - y0, halfstep, halfstep, buf:rgb(150, 150, 150))
				buf:fillrect(x - x0 + halfstep, y - y0 + halfstep, halfstep, halfstep, buf:rgb(150, 150, 150))
				buf:fillrect(x - x0 + halfstep, y - y0, halfstep, halfstep, buf:rgb(170, 170, 170))
				buf:fillrect(x - x0, y - y0 + halfstep, halfstep, halfstep, buf:rgb(170, 170, 170))
			end
		end
		if x0 < self.img:width() and y0 < self.img:height() then
			buf:blit(0, 0, self.img:sub(x0, y0, self.img:width(), self.img:height()))
		end
	end

	return image
end

function tk.text(a, b)
	local text = b or a
	local opts = b and a or {}

	-- Some defaults
	opts.text_size = opts.text_size or 16
	opts.padding = opts.padding or 2
	opts.background = opts.background or tk.rgb(220, 220, 220)
	opts.color = opts.color or tk.rgb(0, 0, 0)
	opts.line_spacing = opts.line_spacing or 1
	if opts.word_wrap == nil then opts.word_wrap = true end
	opts.font = opts.font or tk.fonts.default
	opts.dy = opts.dy or -(opts.text_size//8)

	local lines = string.split(text, "\n")
	local width = 0
	for i = 1, #lines do
		width = math.max(width, opts.font:text_width(lines[i], opts.text_size))
	end

	local txt = tk.widget(width+2*opts.padding, #lines*(opts.text_size + opts.line_spacing) - opts.line_spacing + 2*opts.padding, opts)
	txt.text = text

	function txt:draw(x0, y0, buf)
		local lines = string.split(self.text, "\n")

		buf:fillrect(0, 0, buf:width(), buf:height(), self.background)
		local s = region_inter({x = x0, y = y0, w = buf:width(), h = buf:height()},
							   {x = self.padding, y = self.padding, w = self.width - 2*self.padding, h = self.height - 2*self.padding})
		if s then
			local buf2 = buf:sub(s.x - x0, s.y - y0, s.w, s.h)
			for i = 1, #lines do
				-- TODO word wrap
				buf2:write(self.padding - s.x, (i-1) * (self.text_size + self.line_spacing) + self.padding - s.y + self.dy,
						   lines[i], self.font, self.text_size, self.color)
			end
		end
	end
	
	return txt
end

function tk.box(a, b)
	local content = b or a
	local opts = b and a or {}

	-- Some defaults
	opts.hresize = opts.hresize or false
	opts.vresize = opts.vresize or false
	opts.hscroll = opts.hscroll or false
	opts.vscroll = opts.vscroll or false
	opts.min_width = opts.min_width or 8
	opts.min_height = opts.min_height or 8
	opts.center_content = opts.center_content or false
	opts.constrain_size = opts.constrain_size or false
	opts.background_color = opts.background_color or tk.rgb(220, 220, 220)
	opts.control_size = opts.control_size or 12

	local box = tk.widget(content.width, content.height, opts)
	box.content = content
	box.content.parent = box
	box.content.x = 0
	box.content.y = 0

	function box:best_resize(width, height)
		if self.constrain_size then
			return self.content:best_resize(width, height)
		else
			width = width or self.width
			width = math.max(self.min_width or 1, width)
			if self.max_width then width = math.min(width, self.max_width) end

			height = height or self.height
			height = math.max(self.min_height or 1, height)
			if self.max_height then height = math.min(height, self.max_height) end

			return width, height
		end
	end

	function box:do_resize(width, height)
		if self.constrain_size then
			self.content:do_resize(width, height)
			self.width = self.content.width
			self.height = self.content.height
		else
			if width then self.width = width end
			if height then self.height = height end
		end
		if self.center_content then
			if self.width > self.content.width then
				self.content.x = (self.width - self.content.width) // 2
			else
				self.content.x = math.min(0, math.max(-(self.content.width - self.width), self.content.x))
			end
			if self.height > self.content.height then
				self.content.y = (self.height - self.content.height) // 2
			else
				self.content.y = math.min(0, math.max(-(self.content.height - self.height), self.content.y))
			end
		end
	end

	function box:resize_child(c, w, h)
		assert(c == self.content)
		if self.constrain_size then
			self:resize(w, h)
		else
			local w, h = self.content:best_resize(w, h)
			if w ~= self.content.width or h ~= self.content.height then
				self.content:do_resize(w, h)
				self:do_resize(self.width, self.height)
				self:redraw(0, 0, self.width, self.height, self)
			end
		end
	end

	function box:move_content(x, y)
		local ox, oy = self.content.x, self.content.y
		if x then self.content.x = math.min(0, math.max(-(self.content.width - self.width), x)) end
		if y then self.content.y = math.min(0, math.max(-(self.content.height - self.height), y)) end
		if self.parent and (self.content.x ~= ox or self.content.y ~= oy) then
			self:redraw(0, 0, self.width, self.height, self)
		end
	end

	function box:barcalc(pos, insize, outsize)
		local csz = self.control_size
		local barsize = (outsize - csz) * outsize // insize
		local baravail = outsize - csz - barsize
		local barpos = -pos * baravail // (insize - outsize)
		return barsize, barpos, baravail
	end

	function box:draw(x0, y0, buf)
		local csz = self.control_size

		local s = region_inter({x = x0, y = y0, w = buf:width(), h = buf:height()},
												{x = self.content.x, y = self.content.y,
												 w = self.content.width, h = self.content.height})
		if s then
			local buf2 = buf:sub(s.x - x0, s.y - y0, s.w, s.h)
			self.content:draw(s.x - self.content.x, s.y - self.content.y, buf2)
		end
		local background_surface = region_diff({x = x0, y = y0, w = buf:width(), h = buf:height()},
											   {x = self.content.x, y = self.content.y,
												w = self.content.width, h = self.content.height})
		for _, s in pairs(background_surface) do
			buf:fillrect(s.x - x0, s.y - y0, s.w, s.h, self.background_color)
		end

		if self.vresize or self.hresize then
			buf:rect(self.width - csz - x0, self.height - csz - y0, csz, csz, buf:rgb(0, 0, 0))
			buf:fillrect(self.width - csz + 1 - x0, self.height - csz + 1 - y0, csz - 2, csz - 2, buf:rgb(255, 255, 255))
		end
		if self.hscroll and self.width < self.content.width then
			local barsize, barpos = self:barcalc(self.content.x, self.content.width, self.width)
			buf:fillrect(barpos + 4 - x0, self.height - csz + 2 - y0, barsize - 8, csz - 4, buf:rgb(0, 0, 0))
		end
		if self.vscroll and self.height < self.content.height then
			local barsize, barpos = self:barcalc(self.content.y, self.content.height, self.height)
			buf:fillrect(self.width - csz + 2 - x0, barpos + 4 - y0, csz - 4, barsize - 8, buf:rgb(0, 0, 0))
		end
	end

	function box:on_mouse_down(x, y, lb, mb, rb)
		local csz = self.control_size

		if lb and (self.vresize or self.hresize) and x >= self.width - csz and y >= self.height - csz then
			self.resizing = true
			self.resize_initw = self.width
			self.resize_inith = self.height
			self.resize_initmx = x
			self.resize_initmy = y
		elseif lb and self.vscroll and x >= self.width - csz and self.height < self.content.height then
			local barsize, barpos = self:barcalc(self.content.y, self.content.height, self.height)
			if y < barpos then
				self:move_content(nil, self.content.y + self.height * 2 // 3)
			elseif y >= barpos + barsize then
				self:move_content(nil, self.content.y - self.height * 2 // 3)
			else
				self.vscrolling = true
				self.vscrolling_inity = self.content.y
				self.vscrolling_initmy = y
			end
		elseif lb and self.hscroll and y >= self.height - csz and self.width < self.content.width then
			local barsize, barpos = self:barcalc(self.content.x, self.content.width, self.width)
			if x < barpos then
				self:move_content(self.content.x + self.width * 2 // 3, nil)
			elseif x >= barpos + barsize then
				self:move_content(self.content.x - self.width * 2 // 3, nil)
			else
				self.hscrolling = true
				self.hscrolling_initx = self.content.x
				self.hscrolling_initmx = x
			end
		else
			self.content:on_mouse_down(x - self.content.x, y - self.content.y, lb, mb, rb)
		end
	end

	function box:on_mouse_up(x, y, lb, mb, rb)
		if lb and self.resizing then
			self.resizing = false
		elseif lb and self.vscrolling then
			self.vscrolling = false
		elseif lb and self.hscrolling then
			self.hscrolling = false
		else
			self.content:on_mouse_up(x - self.content.x, y - self.content.y, lb, mb, rb)
		end
	end

	function box:on_mouse_move(px, py, nx, ny)
		if self.resizing then
			local w = self.hresize and self.resize_initw + nx - self.resize_initmx
			local h = self.vresize and self.resize_inith + ny - self.resize_initmy
			self:resize(w, h)
		elseif self.vscrolling then
			local barsize, barpos, baravail = self:barcalc(self.content.y, self.content.height, self.height)
			self:move_content(nil, self.vscrolling_inity - (ny - self.vscrolling_initmy) * (self.content.height - self.height) // baravail)
		elseif self.hscrolling then
			local barsize, barpos, baravail = self:barcalc(self.content.x, self.content.width, self.width)
			self:move_content(self.hscrolling_initx - (nx - self.hscrolling_initmx) * (self.content.width - self.width) // baravail)
		else
			self.content:on_mouse_move(px - self.content.x, py - self.content.y, nx - self.content.x, ny - self.content.y)
		end
	end


	return box
end


function tk.grid(a, b)
	local contents = b or a
	local opts = b and a or {}

	opts.border_size = 0
	opts.border_color = tk.rgb(200, 200, 200)

	local grid = tk.widget(nil, nil, opts)
	grid.contents = contents

	function grid:best_resize(w, h)
		return self.col_pos[#self.contents[1]+1], self.line_pos[#self.contents+1]
	end

	function grid:reposition_elements()
		self.line_height = {}
		self.col_width = {}
		self.line_pos = {0}
		self.col_pos = {0}
		-- Recalculate line height
		for l = 1, #self.contents do
			self.line_height[l] = 0
			for c = 1, #self.contents[l] do
				self.line_height[l] = math.max(self.contents[l][c].height, self.line_height[l])
			end
			self.line_pos[l + 1] = self.line_pos[l] + self.line_height[l] + self.border_size
		end
		-- Recalculate column height
		for c = 1, #self.contents[1] do
			self.col_width[c] = 0
			for l = 1, #self.contents do
				self.col_width[c] = math.max(self.contents[l][c].width, self.col_width[c])
			end
			self.col_pos[c + 1] = self.col_pos[c] + self.col_width[c] + self.border_size
		end
		-- Reposition elements
		for l = 1, #self.contents do
			for c = 1, #self.contents[l] do
				self.contents[l][c].parent = self
				self.contents[l][c].grid_l = l
				self.contents[l][c].grid_c = c
				self.contents[l][c].x = self.col_pos[c]
				self.contents[l][c].y = self.line_pos[l]
				self.contents[l][c]:do_resize(self.col_width[c], self.line_height[l])
			end
		end
	end

	function grid:get_lc(x, y)
		function dichotomy(tab, val, i0, i1)
			if i0 == i1 then
				return i0
			elseif i1 == i0 + 1 then
				if tab[i1] <= val then
					return i1
				else
					return i0
				end
			else
				local mid = (i0 + i1) // 2
				if tab[mid] <= val then
					return dichotomy(tab, val, mid, i1)
				else
					return dichotomy(tab, val, i0, mid - 1)
				end
			end
		end
		return dichotomy(self.line_pos, y, 1, #self.contents),
			   dichotomy(self.col_pos, x, 1, #self.contents[1])
	end

	function grid:draw(x0, y0, buf)
		local l0, c0 = self:get_lc(x0, y0)
		local l1, c1 = self:get_lc(x0 + buf:width(), y0 + buf:height())

		for l = l0, l1 do
			for c = c0, c1 do
				self:draw_sub(x0, y0, buf, self.contents[l][c])
			end
		end
	end

	function grid:resize_child(item, width, height)
		local width, height = item:best_resize(width, height)
		if width == item.width and height == item.height then return end

		local l, c = item.grid_l, item.grid_c
		local recalc = false
		if width and width ~= self.col_width[c] then
			for l1 = 1, #self.contents do
				self.contents[l1][c]:do_resize(width, nil)
			end
			recalc = true
		end
		if height and height ~= self.line_height[l] then
			for c1 = 1, #self.contents[l] do
				self.contents[l][c1]:do_resize(nil, height)
			end
			recalc = true
		end
		if recalc then
			self:reposition_elements()
		end
		self:resize(self.width, self.height)
	end

	function grid:on_mouse_down(x, y, lb, rb, mb)
		local l, c = self:get_lc(x, y)
		local it = self.contents[l][c]

		self.mouse_lb = self.mouse_lb or lb
		self.mouse_rb = self.mouse_rb or rb
		self.mouse_mb = self.mouse_mb or mb

		if self.mouse_on then
			it = self.mouse_on
		else
			self.mouse_on = it
		end
		it:on_mouse_down(x - it.x, y - it.y, lb, mb, rb)
	end

	function grid:on_mouse_up(x, y, lb, rb, mb)
		local l, c = self:get_lc(x, y)
		local it = self.mouse_on or self.contents[l][c]
		it:on_mouse_up(x - it.x, y - it.y, lb, mb, rb)

		self.mouse_lb = self.mouse_lb and not lb
		self.mouse_rb = self.mouse_rb and not rb
		self.mouse_mb = self.mouse_mb and not mb
		if not (self.mouse_lb or self.mouse_mb or self.mouse_rb) then
			self.mouse_on = nil
		end
	end

	function grid:on_mouse_move(prev_x, prev_y, new_x, new_y)
		local l, c = self:get_lc(prev_x, prev_y)
		local it = self.mouse_on or self.contents[l][c]
		it:on_mouse_move(prev_x - it.x, prev_y - it.y, new_x - it.x, new_y - it.y)
	end

	grid:reposition_elements()
	grid.width, grid.height = grid:best_resize(0, 0)

	return grid
end


-- #### #### WINDOW MANAGER WIDGET #### ####

function tk.window_manager()
	local wm = tk.widget(100, 100)
	wm.windows = {}		-- Last = on top
	wm.window_pos = {}

	wm.mouse_lb = false
	wm.mouse_rb = false
	wm.mouse_mb = false
	wm.mouse_win = nil

	function wm:add(opts, content)
		opts.x = opts.x or 24
		opts.y = opts.y or 24
		opts.title = opts.title or "--"
		
		local win = tk.widget(content.width + 2, content.height + 22, opts)
		win.parent = self
		win.visible = true
		table.insert(self.windows, win)
		self.window_pos[win] = {x = win.x, y = win.y, h = win.height, w = win.width}

		win.content = content
		content.parent = win
		content.x = 1
		content.y = 21

		function win:resize_child(c, w, h)
			assert(c == self.content)

			local w, h = c:best_resize(w, h)
			if w ~= c.width or h ~= c.height then 
				local reg1 = wm.window_pos[self]

				c:do_resize(w, h)
				self.width = c.width + 2
				self.height = c.height + 22

				wm.window_pos[self] = {x = self.x, y = self.y, w = self.width, h = self.height}
				local reg2 = wm.window_pos[self]

				local pieces = region_union(reg1, reg2)
				for _, p in pairs(pieces) do
					wm:redraw(p.x, p.y, p.w, p.h)
				end
			end
		end

		function win:draw(x0, y0, buf)
			self:draw_sub(x0, y0, buf, self.content)

			buf:rect(-x0, -y0, self.width, self.height, buf:rgb(255, 128, 0))
			buf:fillrect(-x0+1, -y0+1, win.width-2, 20, buf:rgb(255, 255, 255))
			if win.title then
				buf:write(-x0+2, -y0+2, win.title, tk.fonts.default, 16, buf:rgb(0, 0, 0))
			end
		end

		function win:on_mouse_down(x, y, lb, rb, mb)
			if y < 22 and lb then
				self.moving = true
			else
				self.content:on_mouse_down(x-1, y-21, lb, rb, mb)
			end
		end

		function win:on_mouse_up(x, y, lb, rb, mb)
			if self.moving then
				self.moving = false
			else
				self.content:on_mouse_up(x-1, y-21, lb, rb, mb)
			end
		end

		function win:on_mouse_move(px, py, nx, ny)
			if self.moving then
				local reg1 = wm.window_pos[self]
				self.x = self.x + nx - px
				self.y = self.y + ny - py
				wm.window_pos[self] = {x = self.x, y = self.y, w = self.width, h = self.height}
				local reg2 = wm.window_pos[self]
				local pieces = region_union(reg1, reg2)
				for _, p in pairs(pieces) do
					wm:redraw(p.x, p.y, p.w, p.h)
				end
			else
				self.content:on_mouse_move(px-1, py-21, nx-1, ny-21)
			end
		end

		self:redraw_win(win)

		return win
	end

	function wm:remove(win)
		if win.parent ~= self then return end
		win.parent = nil
		win.get_draw_buffer = function() return nil end

		win.content.parent = nil
		win.content.get_draw_buffer = function() return nil end

		for i, w2 in pairs(self.windows) do
			if w2 == win then
				table.remove(self.windows, i)
				break
			end
		end
		self.window_pos[win] = nil

		self:redraw_win(win)
	end

	function wm:redraw_win(win)
		self:redraw(win.x, win.y, win.width, win.height)
	end

	function wm:draw(x0, y0, buf)
		local remaining = { {x = x0, y = y0, w = buf:width(), h = buf:height()} }

		for i = #self.windows, 1, -1 do
			win = self.windows[i]
			if win.visible then
				local remaining2 = {}
				local win_reg = {x = win.x, y = win.y, w = win.width, h = win.height}

				for _, reg in pairs(remaining) do
					local draw_to = region_inter(reg, win_reg)
					if draw_to then
						win:draw(draw_to.x - win.x, draw_to.y - win.y, buf:sub(draw_to.x - x0, draw_to.y - y0, draw_to.w, draw_to.h))
					end

					local remain_to = region_diff(reg, win_reg)
					for _, reg2 in pairs(remain_to) do
						table.insert(remaining2, reg2)
					end
				end

				remaining = remaining2
			end
		end

		-- Draw background
		function draw_background(x0, y0, buf)
			local step = 32
			local halfstep = 16
			for x = x0 - (x0 % step), x0 + buf:width(), step do
				for y = y0 - (y0 % step), y0 + buf:height(), step do
					buf:fillrect(x - x0, y - y0, halfstep, halfstep, buf:rgb(110, 110, 140))
					buf:fillrect(x - x0 + halfstep, y - y0 + halfstep, halfstep, halfstep, buf:rgb(110, 110, 140))
					buf:fillrect(x - x0 + halfstep, y - y0, halfstep, halfstep, buf:rgb(110, 140, 110))
					buf:fillrect(x - x0, y - y0 + halfstep, halfstep, halfstep, buf:rgb(110, 140, 110))
				end
			end
		end
		for _, reg in pairs(remaining) do
			draw_background(reg.x, reg.y, buf:sub(reg.x - x0, reg.y - y0, reg.w, reg.h))
		end
	end

	function wm:find_win(x, y)
		for i = #self.windows, 1, -1 do
			local win = self.windows[i]
			if win.visible and x >= win.x and y >= win.y and x < win.x + win.width and y < win.y + win.height then
				return win
			end
		end
		return nil
	end

	function wm:on_mouse_down(x, y, lb, rb, mb)
		self.mouse_lb = self.mouse_lb or lb
		self.mouse_rb = self.mouse_rb or rb
		self.mouse_mb = self.mouse_mb or mb

		local on_win = self:find_win(x, y)
		if self.mouse_win then
			on_win = self.mouse_win
		else
			self.mouse_win = on_win
		end

		if on_win then
			sys.dbg_print(string.format("Mouse down on window %s\n", on_win.title))

			if self.windows[#self.windows] ~= on_win then
				for i, w in pairs(self.windows) do
					if w == on_win then
						table.remove(self.windows, i)
						break
					end
				end
				table.insert(self.windows, on_win)
				self:redraw_win(on_win)
			end
			on_win:on_mouse_down(x - on_win.x, y - on_win.y, lb, rb, mb)
		end
	end

	function wm:on_mouse_up(x, y, lb, rb, mb)
		local on_win = self.mouse_win or self:find_win(x, y)
		if on_win then
			on_win:on_mouse_up(x - on_win.x, y - on_win.y, lb, rb, mb)
		end
		self.mouse_lb = self.mouse_lb and not lb
		self.mouse_rb = self.mouse_rb and not rb
		self.mouse_mb = self.mouse_mb and not mb
		if not (self.mouse_lb or self.mouse_mb or self.mouse_rb) then
			self.mouse_win = nil
		end
	end

	function wm:on_mouse_move(prev_x, prev_y, new_x, new_y)
		local on_win = self.mouse_win or self:find_win(prev_x, prev_y)
		if on_win then
			on_win:on_mouse_move(prev_x - on_win.x, prev_y - on_win.y, new_x - on_win.x, new_y - on_win.y)
		end
	end

	return wm
end


return tk