Created
June 21, 2015 13:18
-
-
Save anonymous/f5b396dc34f312a27b1a to your computer and use it in GitHub Desktop.
Gists Codea Upload
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
--# Main | |
-- components | |
--[[ the idea: define an Object class that | |
uses components | |
can be used as a component | |
so complex objects can be built out of simpler ones | |
the functions draw, touched, update (, delete, tostring) should work | |
the update function seems the most complex, because of 2 way interaction: top-down, bottom-up | |
problems are: | |
• Order of objects matters | |
• Conflict between objects, trying to set self.x to different values | |
• some object wants to reject an update request (itself or or its consequencies) | |
--]] | |
function setup() | |
comment = [[ | |
Composite1 : full top-down messages, and 1 bottom-up message | |
(update rejection). | |
The menu below is a toy project including 'bad' cases (multiple branches). | |
Drag boxes towards bottom or right and watch the result: | |
moving a box around will move its children only. | |
The updated parent data are correctly passed down through the tree. | |
Boxes are bounded to the screen and feedback the change rejection | |
to their parent if needed, so their relative positions remain correct. | |
This composite class is more complex but still limited: you cannot | |
pass complex information to the parent. | |
]] | |
local a_menu = cMenu( {name="composite", x=100, y=HEIGHT-400, w=150, h=50 } ) | |
world = Composite() | |
world:insert( a_menu ) | |
end | |
function draw() | |
background(40, 40, 50) | |
strokeWidth(5) | |
textMode(CENTER) | |
fontSize(20) | |
fill(255, 184, 0, 255) | |
text(comment, WIDTH/2, HEIGHT-150) | |
world:update() | |
world:draw() | |
end | |
function touched(touch) | |
world:touched(touch) | |
end | |
--# Composite0 | |
-- Comoposite0: | |
-- the simplest one: it passes top-down messages only, so: | |
-- end-leaf components cannot be components, if a bottom-up feedback is needed (like a touch) | |
-- so one must define a self.leaf field and manage it explicitely | |
-- the sub classes can re-implement: | |
-- drawBeforeComponents() | |
-- drawAfterComponents() | |
-- touchedAfterComponents(touch) (check the details to have touch interception working) | |
-- updateBeforeComponents(data,cData) | |
-- updateAfterComponents(data) | |
-- Top-down communication is managed via the ** interface ** table you define | |
-- when inserting a new child | |
Composite = class() | |
function Composite:init(data) | |
data = data or {} | |
self:updateDefaultFields(self.default) | |
self:updateDefaultFields(data) | |
self.components = {} | |
self.interface = {} | |
self.name = data.name or "?" | |
end | |
-- insert/remove components | |
function Composite:bindComponent(component) | |
-- set other references pointing to this component | |
end | |
function Composite:unbindComponent(component) | |
-- remove other references pointing to this component | |
end | |
function Composite:insert(component,interface) | |
-- make sure component isnt already here | |
for _,c in ipairs(self.components) do | |
if c == component then | |
error("Trying to insert same component twice") | |
end | |
end | |
-- then insert the component | |
table.insert(self.components, component) | |
-- interface is a table of key/key pairs: {componentKey="selfKey", etc.. } | |
self.interface[component] = interface | |
self:bindComponent(component) | |
end | |
function Composite:remove(component) | |
-- find and remove | |
local found = false | |
for i,c in ipairs(self.components) do | |
if c == component then | |
self.interface[component] = nil | |
table.remove(self.components,i) | |
self:unbindComponent(component) | |
found = true | |
break | |
end | |
end | |
return found | |
end | |
-- draw self and components | |
function Composite:drawComponents() | |
-- order matters: first ones drawn first | |
for _,c in ipairs(self.components) do c:draw() end | |
end | |
function Composite:draw() | |
self:drawBeforeComponents() | |
self:drawComponents() | |
self:drawAfterComponents() | |
end | |
-- your code: | |
function Composite:drawBeforeComponents() end | |
function Composite:drawAfterComponents() end | |
function Composite:touchedComponents(touch) | |
local touchIntercepted = false | |
-- order matters: LAST ones touched first | |
local c = self.components | |
local n = #self.components | |
if n>0 then | |
for i=0,n-1 do | |
touchIntercepted = c[n-i]:touched(touch) | |
if touchIntercepted then return touchIntercepted end | |
end | |
end | |
return touchIntercepted | |
end | |
function Composite:touched(touch) | |
local touchIntercepted = false | |
touchIntercepted = self:touchedComponents(touch) | |
if touchIntercepted then return touchIntercepted end | |
touchIntercepted = self:touchedAfterComponents(touch) | |
return touchIntercepted | |
end | |
-- your code: | |
function Composite:touchedAfterComponents(touch) | |
local touchIntercepted = false | |
-- you code here | |
return touchIntercepted | |
end | |
-- this table contains all the data fields of the composite | |
Composite.default = {} | |
-- this function makes sure only acceptable fields are copied into self | |
function Composite:updateDefaultFields(data) | |
if data then | |
for key,val in pairs(data) do | |
local default = self.default[key] | |
if default~=nil then | |
if val ~= nil then self[key] = val | |
elseif self[key] == nil then self[key] = default | |
end | |
end | |
end | |
end | |
end | |
-- this returns the internal fields needed to update the component | |
function Composite:convert(data,interface) | |
local cData,val = {} | |
for key,keyTop in pairs(interface) do | |
val = data[keyTop] | |
if val == nil then val = self[keyTop] end | |
if val == nil then error("key: "..keyTop.." not found") end | |
cData[key] = val | |
end | |
return cData | |
end | |
-- update each component | |
function Composite:updateComponents(cData) | |
-- order matters: first ones updated first | |
for _,c in ipairs(self.components) do | |
local data | |
if cData then -- only the fields you have defined in the interface are passed | |
data = self:convert(cData, self.interface[c] or {}) | |
end | |
c:update(data) | |
end | |
end | |
function Composite:update(data) | |
self:updateDefaultFields(data) -- relevant fields are copied automatically | |
local cData = {} -- use this table to pass data to components | |
self:updateBeforeComponents(data,cData) | |
self:updateComponents(cData) | |
self:updateAfterComponents(data) | |
end | |
-- your code: | |
function Composite:updateBeforeComponents(data,cData) | |
-- use cData table to pass data to components | |
end | |
function Composite:updateAfterComponents(data) | |
end | |
--# Composite1 | |
-- Comoposite1: | |
-- more complex: it passes top-down messages only, but chidren can refuse the update so: | |
-- end-leaf components cannot be components, if a bottom-up feedback is needed (like a touch) | |
-- so one must define a self.leaf field and manage it explicitely | |
-- the sub classes can re-implement: | |
-- drawBeforeComponents() | |
-- drawAfterComponents() | |
-- touchedAfterComponents(touch) (check the details to have touch interception working) | |
-- updateBeforeComponents(data,cData,feedback) | |
-- updateAfterComponents(data,feedback) | |
-- Top-down communication is managed via the ** interface ** table you define | |
-- when inserting a new child | |
-- update rejection is managed via: | |
-- - a feedback table passed around | |
-- - saving the component state before updating | |
-- - and restoring this state if the update is rejected by a sub component | |
-- modified functions: | |
-- update each component | |
function Composite:updateComponents(data,feedback) | |
-- order matters: first ones updated first | |
for _,c in ipairs(self.components) do | |
local cData | |
if data then | |
cData = self:convert(data,self.interface[c] or {}) | |
end | |
c:update(cData,feedback) | |
end | |
end | |
-- update fonctions, to be overwritten by each sub class | |
function Composite:updateBeforeComponents(data,cData,feedback) end | |
function Composite:updateAfterComponents(data,feedback) end | |
function Composite:update(data, feedback) | |
local feedback = feedback or {} | |
self:saveState() | |
self:updateDefaultFields(data) -- relevant fields are copied automatically | |
local cData = {} | |
self:updateBeforeComponents( data, cData,feedback) | |
self:updateComponents(cData,feedback) | |
self:updateAfterComponents( data, feedback ) | |
if self:updateRefused(feedback) then | |
self:restoreState() | |
end | |
end | |
-- new functions: | |
function Composite:refuseUpdate(data) | |
data.updateRefused = true | |
end | |
function Composite:updateRefused(data) | |
return data.updateRefused | |
end | |
function Composite:saveState() | |
self.clone = self.clone or {} | |
for key,val in pairs(self.default) do | |
self.clone[key] = self[key] | |
end | |
end | |
function Composite:restoreState() | |
for key,val in pairs(self.default) do | |
self[key] = self.clone[key] | |
end | |
end | |
--# Rect | |
Rect = class() | |
function Rect:init(x,y,w,h,txt) | |
self.x = x or 0 | |
self.y = y or 0 | |
self.w = w or 100 | |
self.h = h or 100 | |
self.txt = txt or "-" | |
self.xr = self.x + self.w/2 | |
self.yr = self.y + self.h/2 | |
self.wr = self.w/2 | |
self.hr = self.h/2 | |
end | |
function Rect:draw() | |
fill(200) | |
rectMode(CORNER) | |
rect(self.x,self.y,self.w,self.h) | |
fill(0) | |
textMode(CENTER) | |
text(self.txt,self.xr, self.yr) | |
end | |
local abs = math.abs | |
function Rect:touched(touch) | |
if abs(self.xr-touch.x)<=self.wr and abs(self.yr-touch.y)<=self.hr then | |
return true | |
end | |
end | |
--# cItem | |
cItem = class(Composite) | |
-- a menu item | |
function cItem:init(data) | |
self.leaf = Rect() | |
Composite.init(self,data) | |
self:update(data) | |
end | |
cItem.default = { | |
name = "?", | |
xref=100, | |
yref=150, | |
dx=0, | |
dy=-50, | |
w=100, | |
h=50, | |
txt="?", | |
} | |
function cItem:boundX() | |
local ok = true | |
local val,size,mini,maxi = self.x, self.w, 0, WIDTH | |
if val < mini then self.x = mini ; ok = false end | |
if val + size > maxi then self.x = maxi-size ; ok = false end | |
return ok | |
end | |
function cItem:boundY() | |
local ok = true | |
local val,size,mini,maxi = self.y, self.h, 0, HEIGHT | |
if val < mini then self.y = mini ; ok = false end | |
if val + size > maxi then self.y = maxi-size ; ok = false end | |
return ok | |
end | |
function cItem:setTxt(txt) | |
self.txt = txt | |
self.leaf.txt = txt | |
end | |
function cItem:updateBeforeComponents(data,cData,feedback) | |
self.x = self.xref + self.dx | |
self.y = self.yref + self.dy | |
local ok = true | |
ok = ok and self:boundX() | |
ok = ok and self:boundY() | |
if not ok then | |
self:refuseUpdate(feedback) | |
end | |
self.leaf:init(self.x,self.y,self.w,self.h,self.txt) | |
-- component data | |
cData.x=self.x | |
cData.y=self.y | |
end | |
function cItem:draw() | |
self.leaf:draw() | |
self:drawComponents() | |
end | |
function cItem:touchedAfterComponents(touch) | |
local touchIntercepted = false | |
if self.leaf:touched(touch) then | |
if touch.state == BEGAN then | |
elseif touch.state == MOVING then | |
local data = {} | |
data.dx = self.dx + touch.deltaX | |
data.dy = self.dy + touch.deltaY | |
self:update(data) | |
touchIntercepted = true | |
end | |
end | |
return touchIntercepted | |
end | |
--# cMenu | |
cMenu = class(Composite) | |
-- a toy project with all the 'bad' cases (multiple branches) | |
cMenu.default = { | |
name = "top", | |
x=100, | |
y=500, | |
w=100, | |
h=70, | |
txt="top", | |
} | |
function cMenu:init(data) | |
Composite.init(self,data) | |
local menu1 = self:createBox(self,0,0,"menu1") | |
local dy = -self.h-10 | |
local child = self:createBox(menu1,0,dy,"item 1.1") | |
local child = self:createBox(child,0,dy,"item 1.2") | |
local menu2 = child | |
local child = self:createBox(child,0,dy,"item 1.3") | |
local child = self:createBox(child,0,dy,"item 1.4") | |
local child = self:createBox(menu2,160,0,"item 1.2.1") | |
local child = self:createBox(child,50,dy,"item 1.2.2") | |
local child = self:createBox(child,0,dy,"item 1.2.3") | |
end | |
function cMenu:createBox(parent,dx,dy,txt) | |
local x,y,w,h | |
x = parent.x | |
y = parent.y | |
w = parent.w | |
h = parent.h | |
local child = cItem( { dx=dx, dy=dy, xref=x, yref=y, w=w, h=h, txt=txt} ) | |
local topDownInterface = {xref="x", yref="y",} | |
parent:insert(child, topDownInterface) | |
return child | |
end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment