diff --git a/Elements.md b/Elements.md new file mode 100644 index 0000000..2a24763 --- /dev/null +++ b/Elements.md @@ -0,0 +1,54 @@ +# Elements + +All elements have the following standard methods: + +- `move(x, y)` - Moves from current position by `x`/`y`. +- `setPosition(x, y)` - Sets the `x`/`y` position based on current alignment. +- `getPosition()` - Returns `x` and `y` position based on current alignment. +- `setSize(x, y)` - Sets the witdh/height of the element. Will stretch based on + alignment. +- `getSize()` - Returns width and height of the element. +- `align(alignment)` - Sets alignment based on the parent's position and size. + `alignment` is a string specifying how to align: `top-left`, `top-center`, + `top-right`, `left-center`, `center`, `right-center`, `bottom-left`, + `bottom-center`, `bottom-right` +- `alignTo(element, alignment)` - Sets alignment based on an element's position + and size. Same `alignment`'s as `align()`. +- `setAlignment(alignment)` - Sets alignment *value* to this, but does not move + the element. +- `setSkin(skin)` - Sets the skin (see [Skins.md][1]) used for this element. + +**Note**! Calls to `align()`, `alignTo()`, and `setAlignment()` change what +positions will be returned, and how positioning and resizing will work. + +## Box Element + +Box is the simplest element, a rectangular area that can be styled or used for +alignment. + +`pop.box(parent, skin)` +If `parent` not specified, uses `pop.window` (the top level element). +If `skin` is not specified, uses `pop.currentSkin` (see [Skins.md][1]). + +TODO Make it possible to just specify skin? + +## Text Element + +Text is used to draw text. Its styling is based on its skin, see [Skins.md][1] +for information on how to set that up. + +`pop.text(parent, text, skin)` +If `parent` not specified, uses `pop.window` (the top level element). +If `skin` is not specified, uses `pop.currentSkin` (see [Skins.md][1]). + +TODO Make it possible to just specify text, or just text and skin? + +# Excluding Movement/Rendering + +If you set `excludeMovement` to `true` on any element, it and its children will +not be moved unless its own movement methods are used. + +If you set `excludeRendering` to `true` on any element, it and its children will +not be rendered. + +[1]: ./Skins.md diff --git a/README.md b/README.md index e67f5dc..98344a9 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,30 @@ extensible, allowing for complex interfaces to be built in it. I've never written a GUI library before..so we'll see how that goes. -## Features - ```lua local pop = require "pop" --- define love callbacks here +-- define love callbacks here (update, draw, textinput, mouse/key events) local box = pop.box() ``` -* `box` is a box (class) for containing things. -* `text` is a class for handling text. -* Nothing else! Is alpha, just started. +## Using + +Elements store position, size, and child elements. When moved, an element's +children also move. Elements have simple methods for adjusting their position +and size. + +`pop.window` - An element representing the game window. It will not auto-resize. + +Any element (and its children) with `excludeMovement == true` will not be moved +except when its `move()` or `setPosition()` are called. + +Children render on top of their parents. (Rendering starts at `pop.window` and +loops down.) Any element (and its children) with `excludeRendering == true` will +not be rendered. + +See [Elements.md][3] for the standard methods each element has, and what +elements are available. [1]: https://en.wikipedia.org/wiki/Cola_(programming_language) [2]: https://love2d.org/ +[3]: ./Elements.md diff --git a/Skins.md b/Skins.md new file mode 100644 index 0000000..b6dc819 --- /dev/null +++ b/Skins.md @@ -0,0 +1,17 @@ +# Skins + +Skins are simply tables containing information on how to draw elements that have +been assigned them. Skins are loaded from Pop's `skins` directory when you +require it. + +- `background` - A [Drawable][1] drawn before the `foreground`, or if a table, + assumed to be a [color][2] and that color is used (if `false`, is ignored). +- `foreground` - A [Drawable][1] drawn after the `background`, or if a table, + assumed to be a [color][2] and that color is used (if `false`, is ignored). +- `draw(element)` - If defined, will be used to render an element instead of the + standard drawing method. + +TODO various text style infos! + +[1]: https://love2d.org/wiki/Drawable +[2]: https://love2d.org/wiki/love.graphics.setColor diff --git a/main.lua b/main.lua index 8d1e34c..01bc655 100644 --- a/main.lua +++ b/main.lua @@ -1,14 +1,22 @@ -local pop = require "pop" --TODO tell user that pop must be required with SLASHES +local pop = require "pop" function love.load() - pop.box() -- returns the box element + --pop.box() -- returns the box element -- or pop.create("box") (this is what is actually called when you call pop.box()) end +function love.update(dt) + pop.update(dt) +end + function love.draw() pop.draw() end +function love.textinput(text) + pop.textinput(text) +end + function love.mousepressed(button, x, y) pop.mousepressed(button, x, y) end @@ -20,5 +28,11 @@ end function love.keypressed(key) if key == "escape" then love.event.quit() + else + pop.keypressed(key) end end + +function love.keyreleased(key) + pop.keyreleased(key) +end diff --git a/pop/elements/box.lua b/pop/elements/box.lua index a7e57cf..80a4a9a 100644 --- a/pop/elements/box.lua +++ b/pop/elements/box.lua @@ -1,24 +1,11 @@ -local path = string.sub(..., 1, string.len(...) - string.len(".elements.box")) -local class = require(path .. ".lib.middleclass") -local element = require(path .. ".elements.element") +local path = string.sub(..., 1, string.len(...) - string.len("/elements/box")) +local class = require(path .. "/lib/middleclass") +local element = require(path .. "/elements/element") -local box = class("pop.box", element) +local box = class("pop.box", element) --TODO follow middleclass standards!?@@R/ -function box:initialize(pop, parent) - element.initialize(self, pop, parent) - self.sizeControl = "specified" - self.outerWidth = 300 - self.outerHeight = 250 - self.innerWidth = self.outerWidth - self.skin.style.borderSize - self.innerHeight = self.outerHeight - self.skin.style.borderSize -end - -function box:update() - -- -end - -function box:draw() - --TODO find a way for relative x/y to be passed here, because else, we won't have proper coords for drawing +function box:initialize(pop, parent, skin) + element.initialize(self, pop, parent, skin) end return box diff --git a/pop/elements/element.lua b/pop/elements/element.lua index 1df6888..6350cd8 100644 --- a/pop/elements/element.lua +++ b/pop/elements/element.lua @@ -1,121 +1,182 @@ -local path = string.sub(..., 1, string.len(...) - string.len(".elements.element")) -local class = require(path .. ".lib.middleclass") ---TODO determine if these requires can break because of slashes / subdirectories +local path = string.sub(..., 1, string.len(...) - string.len("/elements/element")) +local class = require(path .. "/lib/middleclass") -local element = class("pop.element") - -function element:initialize(pop, parent) - self.ax = 0 -- absolute locations - self.ay = 0 - self.rx = 0 -- relative to parent locations - self.ry = 0 - - self.sizeControl = "fromInner" -- fromInner, fromOuter, specified - self.outerWidth = 0 - self.outerHeight = 0 - self.innerWidth = 0 - self.innerHeight = 0 - - self.skin = pop.skins[pop.currentSkin] - self.visible = true +local element = class("pop.element") --TODO follow middleclass standards!?@@R/ +function element:initialize(pop, parent, skin) self.parent = parent self.child = {} - parent.child[self] = self -- add ourselves to the parent's children + self.x = parent.x or 0 + self.y = parent.y or 0 + self.w = 10 + self.h = 10 + + self.skin = pop.skins[skin] or pop.skins[pop.currentSkin] + self.alignment = "top-left" end ---TODO completely redefine interface based on what we should expect users to do --- REMEMBER the goal is minimal effort on their part --- THEREFORE, we should reduce this interface, they should rely on skins for borderSize (and thus, differences in outer/inner sizes) --- all calls should be based on sizing the outside, and update() should update inners (including children!) based on outers +function element:move(x, y) + self.x = self.x + x + self.y = self.y + y +end + +function element:setPosition(x, y) + if self.alignment == "top-left" then + self.x = x + self.y = y + elseif self.alignment == "top-center" then + self.x = x - self.w/2 + self.y = y + elseif self.alignment == "top-right" then + self.x = x - self.w + self.y = y + elseif self.alignment == "left-center" then + self.x = x + self.y = y - self.h/2 + elseif self.alignment == "center" then + self.x = x - self.w/2 + self.y = y - self.h/2 + elseif self.alignment == "right-center" then + self.x = x - self.w + self.y = y - self.h/2 + elseif self.alignment == "bottom-left" then + self.x = x + self.y = y - self.h + elseif self.alignment == "bottom-center" then + self.x = x - self.w/2 + self.y = y + elseif self.alignment == "bottom-right" then + self.x = x - self.w + self.y = y - self.h + end +end + +function element:getPosition() + if self.alignment == "top-left" then + return self.x, self.y + elseif self.alignment == "top-center" then + return self.x + self.w/2, self.y + elseif self.alignment == "top-right" then + return self.x + self.w, self.y + elseif self.alignment == "left-center" then + return self.x, self.y + self.h/2 + elseif self.alignment == "center" then + return self.x + self.w/2, self.y + self.h/2 + elseif self.alignment == "right-center" then + return self.x + self.w, self.y + self.h/2 + elseif self.alignment == "bottom-left" then + return self.x, self.y + self.h + elseif self.alignment == "bottom-center" then + return self.x + self.w/2, self.y + elseif self.alignment == "bottom-right" then + return self.x + self.w, self.y + self.h + end +end + +function element:setSize(w, h) + if self.alignment == "top-left" then + self.w = w + self.h = h + elseif self.alignment == "top-center" then + -- x minus half difference to expand horizontally + self.x = self.x - (w - self.w)/2 + self.w = w + self.h = h + elseif self.alignment == "top-right" then + -- x minus difference to expand left + self.x = self.x - (w - self.w) + self.w = w + self.h = h + elseif self.alignment == "left-center" then + self.y = self.y - (h - self.h)/2 + self.w = w + self.h = h + elseif self.alignment == "center" then + self.x = self.x - (w - self.w)/2 + self.y = self.y - (h - self.h)/2 + self.w = w + self.h = h + elseif self.alignment == "right-center" then + self.x = self.x - (w - self.w) + self.y = self.y - (h - self.h)/2 + self.w = w + self.h = h + elseif self.alignment == "bottom-left" then + self.y = self.y - (h - self.h) + self.w = w + self.h = h + elseif self.alignment == "bottom-center" then + self.x = self.x - (w - self.w)/2 + self.y = self.y - (h - self.h) + self.w = w + self.h = h + elseif self.alignment == "bottom-right" then + self.x = self.x - (w - self.w) + self.y = self.y - (h - self.h) + self.w = w + self.h = h + end +end function element:getSize() - return self.outerWidth, self.outerHeight -end -function element:setSize(width, height) - assert(width > 0, "width must be above 0") - assert(height > 0, "height must be above 0") - self.outerWidth = width - self.outerHeight = height - self.sizeControl = "specified" + return self.w, self.h end -function element:getOuterWidth() - return self.outerWidth -end -function element:setOuterWidth(width) - assert(width > 0, "width must be above 0") - self.outerWidth = width - self.sizeControl = "specified" - --TODO needs to update() to update inner size based on borderSize ??? +function element:align(alignment) + self.alignment = alignment + + if self.alignment == "top-left" then + self.x = self.parent.x + self.y = self.parent.y + elseif self.alignment == "top-center" then + -- parent's x plus half of difference in width to center + self.x = self.parent.x + (self.parent.w - self.w)/2 + self.y = self.parent.y + elseif self.alignment == "top-right" then + -- parent's x plus difference in width to align right + self.x = self.parent.x + (self.parent.w - self.w) + self.y = self.parent.y + elseif self.alignment == "left-center" then + self.x = self.parent.x + self.y = self.parent.y + (self.parent.h - self.h)/2 + elseif self.alignment == "center" then + self.x = self.parent.x + (self.parent.w - self.w)/2 + self.y = self.parent.y + (self.parent.h - self.h)/2 + elseif self.alignment == "right-center" then + self.x = self.parent.x + (self.parent.w - self.w) + self.y = self.parent.y + (self.parent.h - self.h)/2 + elseif self.alignment == "bottom-left" then + self.x = self.parent.x + self.y = self.parent.y + (self.parent.h - self.h) + elseif self.alignment == "bottom-center" then + self.x = self.parent.x + (self.parent.w - self.w)/2 + self.y = self.parent.y + (self.parent.h - self.h) + elseif self.alignment == "bottom-right" then + self.x = self.parent.x + (self.parent.w - self.w) + self.y = self.parent.y + (self.parent.h - self.h) + end end -function element:getOuterHeight() - return self.outerHeight() -end -function element:setOuterHeight(height) - assert(height > 0, "height must be above 0") - self.outerHeight = height - self.sizeControl = "specified" - --TODO needs to update() to update inner size based on borderSize ??? +function element:alignTo(element, alignment) + local realParent = self.parent + self.parent = element + + self:align(alignment) + + self.parent = realParent end -function element:getInnerWidth() - return self.innerWidth -end -function element:setInnerWidth(width) - assert(width > 0, "width must be above 0") - self.innerWidth = width - self.sizeControl = "specified" - --TODO needs to update outerWidth ??? +function element:setAlignment(alignment) + self.alignment = alignment end -function element:getInnerHeight() - return self.innerHeight -end -function element:setInnerHeight(height) - assert(height > 0, "height must be above 0") - self.innerHeight = height - self.sizeControl = "specified" - --TODO needs to update outerHeight ?? -end - ---[[ TODO determine how to write these better (consistency motherfucker) -function element:getStyle() - return self.style.name -end -function element:setStyle(style) - self.style = style -end -]] - -function element:getVisible() - return self.visible -end -function element:setVisible(bool) - self.visible = bool -end - -function element:getParent() - return self.parent -end -function element:setParent(parent) - self.parent.child[self] = nil - self.parent = parent - self.parent.child[self] = self -end - ---TODO figure out how getting and setting children might work? or no?? - -function element:update() - --TODO a proper error message - print("update() not deifnenfei") -end - -function element:draw() - --TODO figure out how to get class name - print("Attempting to use element, or did not overwrite element's :draw() method.") +function element:setSkin(skin) + if type(skin) == "string" then + self.skin = pop.skins[skin] + else + self.skin = skin + end end return element diff --git a/pop/init.lua b/pop/init.lua index 351881d..3e707b3 100644 --- a/pop/init.lua +++ b/pop/init.lua @@ -1,99 +1,100 @@ local lf = love.filesystem local lg = love.graphics +local path = ... local pop = {} -local path = ... --NOTE Pop.Box must be required as its directory name (w SLASHES)! - --- elements are local -local box = require(path .. ".elements.box") -local text = require(path .. ".elements.text") ---TODO require these how skins are required -pop.elements = {} -local elements = lf.getDirectoryItems(path .. "/elements") --NOTE Pop.Box must be required with SLASHES! -for _, v in ipairs(elements) do - local name = v:sub(1,-5) - pop.elements[name] = require(path .. "/elements/" .. name) --TODO test this actually works right -end - --- skins define how elements are drawn +pop.elementClasses = {} +--pop.elements = {} +pop.window = false --top level element, defined in pop.load() pop.skins = {} -local skins = lf.getDirectoryItems(path .. "/skins") --NOTE Pop.Box must be required with SLASHES! -for _, v in ipairs(skins) do - local name = v:sub(1,-5) - pop.skins[name] = require(path .. "/skins/" .. name) - pop.skins[name].name = name +pop.currentSkin = "clear" + +function pop.load() + -- load element classes + local elementList = lf.getDirectoryItems(path .. "/elements") + + for i=0, #elementList do + local name = elementList[i]:sub(1, -5) + pop.elementClasses[name] = require(path .. "/elements/" .. name) + + -- wrapper to be able to call pop.element() to create elements + if not pop[name] then + pop[name] = function(...) return pop.create(name, ...) end + end + end + + -- load skins + local skinList = lf.getDirectoryItems(path .. "/skins") + + for i=0, #skinList do + local name = skinList[i]:sub(1, -5) + pop.skins[name] = require(path .. "/skins/" .. name) + pop.skins[name].name = name + end + + -- set top element + pop.window = pop.create("box"):setSize(lg.getWidth(), lg.getHeight()) end -pop.currentSkin = "clearspace" --default skin --- everything has one parent element (initialized at the end) -pop.parentElement = false - --- this function actually creates elements based on what type of element is requested function pop.create(elementType, parent, ...) if not parent then - parent = pop.parentElement + parent = pop.window end - local newElement - - --TODO replace these with calls to pop.elements.ELEMENTNAME - if elementType == "box" then - newElement = box(pop, parent, ...) - elseif elementType == "text" then - newElement = text(pop, parent, ...) - else - error("Invalid element type: " .. elementType) - end + local newElement = pop.elementClasses[elementType](pop, parent, ...) + table.insert(parent.child, newElement) --NOTE pop.window is its own parent! return newElement end --- pretty wrappers to call pop.element() instead of pop.create("element") -pop.box = function(...) return pop.create("box", ...) end -pop.text = function(...) return pop.create("text", ...) end - --- called every frame to draw elements -function pop.draw(element) +function pop.update(dt, element) if not element then - element = pop.parentElement + element = pop.window end - element:draw() + if element.update then + element:update(dt) + end + --TODO redo this loop + for _, childElement in pairs(element.child) do + pop.update(dt, childElement) + end +end + +function pop.draw(element) + if not element then + element = pop.window + end + + if element.skin.draw and element.skin.draw(element) then + -- do nothing... + elseif element.draw then + element:draw() + end + + --TODO redo this loop for _, childElement in pairs(element.child) do - --pop.draw(childElement, element.x, element.y) pop.draw(childElement) end end --- TODO decide if we should track mouse movement - --- these are functions to be overwritten by user if they want a global event handler -function pop.onMousePress(button, x, y) end -function pop.onMouseRelease(button, x, y) end - function pop.mousepressed(button, x, y) - --TODO find which element it belongs to and if that element has a callback set, - -- if it does, use that, else, use the global callback (defined above..as nil) + --TODO find element, if it has a callback, call with button and LOCAL x/y end function pop.mousereleased(button, x, y) - --TODO find which element it belongs to and if that element has a callback set, - -- if it does, use that, else, use the global callback (defined above..as nil) + --TODO find element, if it has a callback, call with button and LOCAL x/y end -function pop.keypressed(key, unicode) - --TODO handle this in some manner when needed +function pop.keypressed(key) + --TODO no idea what to do with this end function pop.keyreleased(key) - --TODO handle this when needed + --TODO no idea what to do with this end --- initialize the top element -pop.parentElement = box(pop, {child={}}) -- dummy object because it has no parent ---pop.parentElement:setSizeControl("specified") -- make it not try to update itself (TODO or based on outro, and custom code to check top parent against screen size) ---pop.parentElement:setSize(lg.getWidth(), lg.getHeight()) -- fill window size ---pop.parentElement:setVisible(false) -- uneeded since its clear... +pop.load() return pop diff --git a/pop/skins/blacknwhite.lua b/pop/skins/blacknwhite.lua deleted file mode 100644 index 007c3cb..0000000 --- a/pop/skins/blacknwhite.lua +++ /dev/null @@ -1,10 +0,0 @@ -local skin = {} - -skin.style = { - background = {255,255,255,1}, -- color table, image, or false - foreground = {0,0,0,1}, -- color table - borderSize = 2, -- integer (minimum 0) - borderStyle = {255,255,255,1} -- color table, image, or false -} - -return skin diff --git a/pop/skins/clear.lua b/pop/skins/clear.lua index 85691f1..8a00e9b 100644 --- a/pop/skins/clear.lua +++ b/pop/skins/clear.lua @@ -1,10 +1,6 @@ local skin = {} -skin.style = { - background = false, -- color table, image, or false - foreground = {0,0,0,1}, -- color table - borderSize = 0, -- integer (minimum 0) - borderStyle = false -- color table, image, or false -} +skin.background = false, -- Drawable, color table, or false +skin.foreground = {255,255,255,255} -- Drawable, color table, or false return skin diff --git a/pop/skins/clearspace.lua b/pop/skins/clearspace.lua deleted file mode 100644 index 1147ff5..0000000 --- a/pop/skins/clearspace.lua +++ /dev/null @@ -1,10 +0,0 @@ -local skin = {} - -skin.style = { - background = false, -- color table, image, or false - foreground = {0,0,0,1}, -- color table - borderSize = 2, -- integer (minimum 0) - borderStyle = false -- color table, image, or false -} - -return skin