local BASE = (...):gsub('layout', '') local Widget = require BASE..'widget' local core = require BASE..'core' local utils = require 'lib.gear' local isinstance = gear.meta.isinstance local rectunion = gear.rect.union local pointinrect = gear.rect.pointinside local Layout = setmetatable({}, Widget) Layout.__index = Layout -- Calculate initial widget size. local function calcsize(sizes, widget) local w, h = widget.w, widget.h if w == nil then assert(#sizes > 0, "Default width is undefined!") w = sizes[#sizes].w end if h == nil then assert(#sizes > 0, "Default height is undefined!") h = sizes[#sizes].h end if w == 'max' then w = 0 for _,v in ipairs(sizes) do if v.w > w then w = v.w end end elseif w == 'median' then w = 0 for _,v in ipairs(sizes) do w = w + v.w end w = math.ceil(w / #sizes) elseif w == 'min' then w = math.huge for _,v in ipairs(sizes) do if v.w < w then w = v.w end end else assert(type(w) == 'number') end if h == 'max' then h = 0 for _,v in ipairs(sizes) do if v.h > h then h = v.h end end elseif h == 'median' then h = 0 for _,v in ipairs(sizes) do h = h + v.h end h = math.ceil(h / #sizes) elseif h == 'min' then h = math.huge for _,v in ipairs(sizes) do if v.h < h then h = v.h end end else assert(type(h) == 'number') end sizes[#sizes+1] = { w = w, h = h } widget.w, widget.h = w, h return w, h end -- Lay down container widgets according to Layout type. function Layout:layoutWidgets() local nx,ny = self.x,self.y local sizes = {} local stack = self.stack local pad = self.padding -- Container bounds, empty local rx,ry,rw,rh = nx,ny,-1,-1 -- Layout container children for _,widget in ipairs(self) do widget.x, widget.y = nx, ny widget.ui = self.ui widget.parent = self if isinstance(widget, Layout) then widget:layoutWidgets() end local w,h = calcsize(sizes, widget) rx,ry,rw,rh = rectunion(rx,ry,rw,rh, nx,ny,w,h) nx,ny = self.advance(nx,ny, w,h, pad) stack[#stack+1] = widget end self.x = rx self.y = ry self.w = math.max(rw, 0) self.h = math.max(rh, 0) end function Layout:onPointerInput(px,py, clicked, down) local stack = self.stack -- Propagate pointer event from topmost widget to bottom for i = #stack,1,-1 do local widget = stack[i] local x,y,w,h = widget.x,widget.y,widget.w,widget.h if pointinrect(px,py, x,y,w,h) then widget:handlePointer(px,py, clicked, down) break end end end -- Find layout's child containing the provided widget. local function childof(layout, widget) local parent = widget.parent while parent ~= layout do widget = parent parent = widget.parent end return widget end local function findfirst(widget) while isinstance(widget, Layout) do -- Find first element accepting focus for i = 1,#widget do if not widget[i].nofocus then widget = widget[i] break end end end return widget end local function findnext(layout, widget) local child = childof(layout, widget) for i,w in ipairs(layout) do if w == child then -- Search to the right, wraparound to the left for j = i+1,#layout do if not layout[j].nofocus then return findfirst(layout[j]) end end for j = 1,i-1 do if not layout[j].nofocus then return findfirst(layout[j]) end end end end return widget end local function findprev(layout, widget) local child = childof(layout, widget) for i,w in ipairs(layout) do if w == child then -- Search to the left, wraparound to the right for j = i-1,1,-1 do if not layout[j].nofocus then return findfirst(layout[j]) end end for j = #layout,i+1,-1 do if not layout[j].nofocus then return findfirst(layout[j]) end end end end return widget end function Layout:firstFocusableWidget() return findfirst(self) end function Layout:nextFocusableWidget() return findnext(self, self.ui.focused) end function Layout:previousFocusableWidget() return findprev(self, self.ui.focused) end function Layout:onActionInput(action) local handled = false if action[self.next] then local n = self:nextFocusableWidget() n:grabFocus() handled = true end if action[self.prev] then local p = self:previousFocusableWidget() p:grabFocus() handled = true end return handled end function Layout:update(dt) for _,widget in ipairs(self.stack) do widget:update(dt) end end function Layout:draw() -- Draw all children according to their order (topmost last) for _,widget in ipairs(self.stack) do widget:draw() end end function Layout.new(args) local self = setmetatable(args, Layout) self.padding = self.padding or 0 self.stack = {} -- A Layout ignores focus if empty or containing only nofocus widgets self.nofocus = true for _,w in ipairs(self) do if not w.nofocus then self.nofocus = false break end end return self end return Layout