diff --git a/README.md b/README.md index 98344a9..eb83a85 100644 --- a/README.md +++ b/README.md @@ -6,32 +6,27 @@ Pop.Box attempts to make a GUI system for use in the [LÖVE][2] engine that is easy to use, requiring as little code as possible to get working, but also 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 + +- Quickly set up and align GUI elements. +- Fully customizable alignment / styling. +- Supports moving/resizing things in ways that take the alignment headache away, + without trying to do too much and become bloated. +- Supports custom elements/skins, make your own and move them into the + appropriate directories for them to be automatically loaded. + +## Usage ```lua local pop = require "pop" --- define love callbacks here (update, draw, textinput, mouse/key events) +-- define LÖVE callbacks here (update, draw, textinput, mouse/key events) local box = pop.box() ``` -## 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. +Docs: [pop Module][3], [Elements][4], [Skins][5] [1]: https://en.wikipedia.org/wiki/Cola_(programming_language) [2]: https://love2d.org/ -[3]: ./Elements.md +[3]: ./docs/Pop.md +[4]: ./docs/Elements.md +[5]: ./docs/Skins.md diff --git a/Elements.md b/docs/Elements.md similarity index 50% rename from Elements.md rename to docs/Elements.md index 2a24763..2f0647a 100644 --- a/Elements.md +++ b/docs/Elements.md @@ -1,30 +1,41 @@ # Elements +Elements are the core of Pop.Box. + +- Elements are arranged hierarchically. +- When an element is moved, its child elements move with it. +- When an element is resized, it resizes in a way that makes sense based on its + alignment. +- Elements are drawn from the top down (meaning child elements will always draw + on top of their parents.) + +The alignment stuff is much easier explained by experimenting or running the +demo, please check it out! + All elements have the following standard methods: -- `move(x, y)` - Moves from current position by `x`/`y`. +- `move(x, y)` - Moves the element 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. + alignment (run the demo to see an example). - `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. +- `align(horizontal, vertical)` - Sets alignment based on the parent's position + and size. Valid `horizontal` strings: `left`, `center`, `right`. Valid + `vertical` strings: `top`, `center`, `bottom`. +- `alignTo(element, horizontal, vertical)` - Sets alignment based on an + element's position and size. Same `horizontal`/`vertical` strings as `align()` +- `setAlignment(horizontal, vertical)` - Sets alignment *values* to this, but + does not move the element. Same `horizontal`/`vertical` strings as `align()` - `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. +positions will be returned, and how positioning and resizing will work. Run the +demo to see how these affect things. ## Box Element -Box is the simplest element, a rectangular area that can be styled or used for -alignment. +Box is the simplest element, a rectangular area. `pop.box(parent, skin)` If `parent` not specified, uses `pop.window` (the top level element). @@ -32,16 +43,17 @@ If `skin` is not specified, uses `pop.currentSkin` (see [Skins.md][1]). TODO Make it possible to just specify skin? -## Text Element +## Text Element (NOT DEFINED YET!) -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. +Text is used to draw text. `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? +TODO Make it possible to use setting size on text to actually calculate what + font size will make that work? # Excluding Movement/Rendering diff --git a/docs/Pop.md b/docs/Pop.md new file mode 100644 index 0000000..95b3a50 --- /dev/null +++ b/docs/Pop.md @@ -0,0 +1,47 @@ +# `pop` Module + +`pop` is the name of the Pop.Box module. You are expected to require it and then +define the following callbacks in LÖVE's callbacks: `pop.update(dt)`, +`pop.draw()`, `pop.textinput(text)`, `pop.mousepressed(button, x, y)`, +`pop.mousereleased(button, x, y)`, `pop.keypressed(key)`, `pop.keyreleased(key)` + +Once that has been done (or at the very least, `pop` has been required and a +callback is set up for `pop.draw`), you can start creating [Elements][1] and +drawing them. + +Also look into [Skins][2], which control how elements are rendered. + +## `pop` Values / Methods + +- `pop.window` is the top level element. It essentially represents the game + window. +- `pop.currentSkin` holds a string specifying the currently in-use skin. + Basically, it's a shortcut so you don't have to specify a skin with every + call to construct an element. +- `pop.create(element, parent, ...)` is how elements are actually created, + `element` is a string naming the desired element. There are wrappers around + any element that doesn't conflict with a key in the `pop` module so that you + can call `pop.element(parent, ...)` instead. +- `pop.load()` loads elements and skins, and sets up `pop.window`. This is used + internally, and will probably lead to issues if you use it (namely, destroying + Pop.Box's access to any existing GUI). + +## `pop` Callbacks + +- `pop.update(dt)` is used so that any element can have a frame-by-frame update + attached to it. +- `pop.draw()` is used to draw everything. +- `pop.textinput(text)` is used to grab text input for any focused element that + can accept it. +- `pop.mousepressed(button, x, y)` is used to detect and handle when an element + is clicked on. (Not actually used yet.) + +- `pop.mousereleased(button, x, y)` is not used yet, but probably will be used + in the future. +- `pop.keypressed(key)` is not used yet, but probably will be used in the + future. +- `pop.keyreleased(key)` is also not used yet, but probably will be used in the + future. + +[1]: ./Elements.md +[2]: ./Skins.md diff --git a/Skins.md b/docs/Skins.md similarity index 93% rename from Skins.md rename to docs/Skins.md index b6dc819..8fcedeb 100644 --- a/Skins.md +++ b/docs/Skins.md @@ -11,6 +11,8 @@ require it. - `draw(element)` - If defined, will be used to render an element instead of the standard drawing method. +**Note**: Supported Drawables: Canvas, Image, Video + TODO various text style infos! [1]: https://love2d.org/wiki/Drawable diff --git a/pop/elements/box.lua b/pop/elements/box.lua index 80a4a9a..b89b7a8 100644 --- a/pop/elements/box.lua +++ b/pop/elements/box.lua @@ -1,11 +1,39 @@ +local lg = love.graphics + 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) --TODO follow middleclass standards!?@@R/ +local box = class("pop.box", element) function box:initialize(pop, parent, skin) element.initialize(self, pop, parent, skin) end +function box:draw() --TODO these ifs are probably wrong + if type(self.skin.background) == "table" then + lg.setColor(self.skin.background) + lg.rectangle("fill", self.x, self.y, self.w, self.h) + else + lg.setColor(255, 255, 255, 255) + local w, h = self.skin.background:getDimensions() + -- scale! + w = self.w/w + h = self.h/h + lg.draw(self.skin.background, self.x, self.y, 0, w, h) + end + + if type(self.skin.foreground) == "table" then + lg.setColor(self.skin.foreground) + lg.rectangle("fill", self.x, self.y, self.w, self.h) + else + lg.setColor(255, 255, 255, 255) + local w, h = self.skin.foreground:getDimensions() + -- scale! + w = self.w/w + h = self.h/h + lg.draw(self.skin.foreground, self.x, self.y, 0, w, h) + end +end + return box diff --git a/pop/elements/element.lua b/pop/elements/element.lua index 6350cd8..346984e 100644 --- a/pop/elements/element.lua +++ b/pop/elements/element.lua @@ -1,7 +1,7 @@ local path = string.sub(..., 1, string.len(...) - string.len("/elements/element")) local class = require(path .. "/lib/middleclass") -local element = class("pop.element") --TODO follow middleclass standards!?@@R/ +local element = class("pop.element") function element:initialize(pop, parent, skin) self.parent = parent @@ -13,152 +13,109 @@ function element:initialize(pop, parent, skin) self.h = 10 self.skin = pop.skins[skin] or pop.skins[pop.currentSkin] - self.alignment = "top-left" + + self.horizontal = "left" + self.vertical = "top" end function element:move(x, y) self.x = self.x + x self.y = self.y + y + + for i=1,#element.child do + if not element.child[i].excludeMovement then + element.child[i]:move(x - oldX, y - oldY) + end + end end function element:setPosition(x, y) - if self.alignment == "top-left" then + local oldX = self.x + local oldY = self.y + + if self.horizontal == "left" then self.x = x - self.y = y - elseif self.alignment == "top-center" then + elseif self.horizontal == "center" then self.x = x - self.w/2 - self.y = y - elseif self.alignment == "top-right" then + elseif self.horizontal == "right" then self.x = x - self.w + end + + if self.vertical == "top" then self.y = y - elseif self.alignment == "left-center" then - self.x = x + elseif self.vertical == "center" then 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 + elseif self.vertical == "bottom" then self.y = y - self.h end + + for i=1,#element.child do + if not element.child[i].excludeMovement then + element.child[i]:move(x - oldX, y - oldY) + end + 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 + local resultX = self.x + local resultY = self.y + + if self.horizontal == "center" then + resultX = resultX + self.w/2 + elseif self.horizontal == "right" then + resultX = resultX + self.w end + + if self.vertical == "center" then + resultY = resultY + self.h/2 + elseif self.vertical == "bottom" then + resultY = resultY + self.h + end + + return resultX, resultY 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 + if self.horizontal == "center" then 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 + elseif self.horizontal == "right" then 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 + + if self.vertical == "center" then + self.y = self.y - (h - self.h)/2 + elseif self.vertical == "bottom" then + self.y = self.y - (h - self.h) + end + + self.w = w + self.h = h end function element:getSize() return self.w, self.h end -function element:align(alignment) - self.alignment = alignment +function element:align(horizontal, vertical) + self:setAlignment(horizontal, vertical) - 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) + self.x = self.parent.x + self.y = self.parent.y + + if self.horizontal == "center" then + self.x = self.x + (self.parent.w - self.w)/2 + elseif self.horizontal == "right" then + self.x = self.x + (self.parent.w - self.w) + end + + if self.vertical == "center" then + self.y = self.y + (self.parent.h - self.h)/2 + elseif self.vertical == "bottom" then + self.y = self.y + (self.parent.h - self.h) end end -function element:alignTo(element, alignment) +function element:alignTo(element, horizontal, vertical) local realParent = self.parent self.parent = element @@ -167,8 +124,13 @@ function element:alignTo(element, alignment) self.parent = realParent end -function element:setAlignment(alignment) - self.alignment = alignment +function element:setAlignment(horizontal, vertical) + if horizontal then + self.horizontal = horizontal + end + if vertical then + self.vertical = vertical + end end function element:setSkin(skin) diff --git a/pop/init.lua b/pop/init.lua index 3e707b3..e558f5a 100644 --- a/pop/init.lua +++ b/pop/init.lua @@ -8,12 +8,13 @@ pop.elementClasses = {} pop.window = false --top level element, defined in pop.load() pop.skins = {} pop.currentSkin = "clear" +--TODO we need a "focused" element for textinput or whatever function pop.load() -- load element classes local elementList = lf.getDirectoryItems(path .. "/elements") - for i=0, #elementList do + for i=1, #elementList do local name = elementList[i]:sub(1, -5) pop.elementClasses[name] = require(path .. "/elements/" .. name) @@ -26,14 +27,14 @@ function pop.load() -- load skins local skinList = lf.getDirectoryItems(path .. "/skins") - for i=0, #skinList do + for i=1, #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()) + pop.window = pop.create("element"):setSize(lg.getWidth(), lg.getHeight()) end function pop.create(elementType, parent, ...) @@ -56,9 +57,8 @@ function pop.update(dt, element) element:update(dt) end - --TODO redo this loop - for _, childElement in pairs(element.child) do - pop.update(dt, childElement) + for i=1,#element.child do + pop.update(dt, element.child[i]) end end @@ -67,15 +67,16 @@ function pop.draw(element) element = pop.window end - if element.skin.draw and element.skin.draw(element) then - -- do nothing... - elseif element.draw then - element:draw() - end + if not element.excludeRendering then + 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) + for i=1,#element.child do + pop.draw(element.child[i]) + end end end