├── demo ├── .gitignore ├── suit │ ├── .gitignore │ ├── docs │ │ ├── themes.rst │ │ ├── _static │ │ │ ├── demo.gif │ │ │ ├── keyboard.gif │ │ │ ├── layout.gif │ │ │ ├── options.gif │ │ │ ├── hello-world.gif │ │ │ └── mutable-state.gif │ │ ├── license.rst │ │ ├── widgets.rst │ │ ├── core.rst │ │ ├── layout.rst │ │ ├── Makefile │ │ ├── index.rst │ │ ├── conf.py │ │ └── gettingstarted.rst │ ├── label.lua │ ├── button.lua │ ├── checkbox.lua │ ├── license.txt │ ├── imagebutton.lua │ ├── slider.lua │ ├── README.md │ ├── init.lua │ ├── input.lua │ ├── theme.lua │ ├── core.lua │ └── layout.lua ├── mushroom.png ├── mushroom walk.png ├── mushroom burrow.png ├── main.lua └── sodapop.lua ├── .gitattributes ├── .gitignore ├── sodapop.lua └── readme.md /demo/.gitignore: -------------------------------------------------------------------------------- 1 | .atom-build.json 2 | -------------------------------------------------------------------------------- /demo/suit/.gitignore: -------------------------------------------------------------------------------- 1 | main.lua 2 | *.love 3 | -------------------------------------------------------------------------------- /demo/mushroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/mushroom.png -------------------------------------------------------------------------------- /demo/mushroom walk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/mushroom walk.png -------------------------------------------------------------------------------- /demo/suit/docs/themes.rst: -------------------------------------------------------------------------------- 1 | Themeing 2 | ======== 3 | 4 | .. note:: 5 | Under construction. 6 | -------------------------------------------------------------------------------- /demo/mushroom burrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/mushroom burrow.png -------------------------------------------------------------------------------- /demo/suit/docs/_static/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/suit/docs/_static/demo.gif -------------------------------------------------------------------------------- /demo/suit/docs/_static/keyboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/suit/docs/_static/keyboard.gif -------------------------------------------------------------------------------- /demo/suit/docs/_static/layout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/suit/docs/_static/layout.gif -------------------------------------------------------------------------------- /demo/suit/docs/_static/options.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/suit/docs/_static/options.gif -------------------------------------------------------------------------------- /demo/suit/docs/_static/hello-world.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/suit/docs/_static/hello-world.gif -------------------------------------------------------------------------------- /demo/suit/docs/_static/mutable-state.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tesselode/sodapop/HEAD/demo/suit/docs/_static/mutable-state.gif -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /demo/suit/label.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...):match('(.-)[^%.]+$') 4 | 5 | return function(core, text, ...) 6 | local opt, x,y,w,h = core.getOptionsAndSize(...) 7 | opt.id = opt.id or text 8 | opt.font = opt.font or love.graphics.getFont() 9 | 10 | w = w or opt.font:getWidth(text) + 4 11 | h = h or opt.font:getHeight() + 4 12 | 13 | opt.state = core:registerHitbox(opt.id, x,y,w,h) 14 | core:registerDraw(opt.draw or core.theme.Label, text, opt, x,y,w,h) 15 | 16 | return { 17 | id = opt.id, 18 | hit = core:mouseReleasedOn(opt.id), 19 | hovered = core:isHovered(opt.id), 20 | entered = core:isHovered(opt.id) and not core:wasHovered(opt.id), 21 | left = not core:isHovered(opt.id) and core:wasHovered(opt.id) 22 | } 23 | end 24 | -------------------------------------------------------------------------------- /demo/suit/button.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...):match('(.-)[^%.]+$') 4 | 5 | return function(core, text, ...) 6 | local opt, x,y,w,h = core.getOptionsAndSize(...) 7 | opt.id = opt.id or text 8 | opt.font = opt.font or love.graphics.getFont() 9 | 10 | w = w or opt.font:getWidth(text) + 4 11 | h = h or opt.font:getHeight() + 4 12 | 13 | opt.state = core:registerHitbox(opt.id, x,y,w,h) 14 | core:registerDraw(opt.draw or core.theme.Button, text, opt, x,y,w,h) 15 | 16 | return { 17 | id = opt.id, 18 | hit = core:mouseReleasedOn(opt.id), 19 | hovered = core:isHovered(opt.id), 20 | entered = core:isHovered(opt.id) and not core:wasHovered(opt.id), 21 | left = not core:isHovered(opt.id) and core:wasHovered(opt.id) 22 | } 23 | end 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | -------------------------------------------------------------------------------- /demo/suit/checkbox.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...):match('(.-)[^%.]+$') 4 | 5 | return function(core, checkbox, ...) 6 | local opt, x,y,w,h = core.getOptionsAndSize(...) 7 | opt.id = opt.id or checkbox 8 | opt.font = opt.font or love.graphics.getFont() 9 | 10 | w = w or (opt.font:getWidth(checkbox.text) + opt.font:getHeight() + 4) 11 | h = h or opt.font:getHeight() + 4 12 | 13 | opt.state = core:registerHitbox(opt.id, x,y,w,h) 14 | local hit = core:mouseReleasedOn(opt.id) 15 | if hit then 16 | checkbox.checked = not checkbox.checked 17 | end 18 | core:registerDraw(opt.draw or core.theme.Checkbox, checkbox, opt, x,y,w,h) 19 | 20 | return { 21 | id = opt.id, 22 | hit = hit, 23 | hovered = core:isHovered(opt.id), 24 | entered = core:isHovered(opt.id) and not core:wasHovered(opt.id), 25 | left = not core:isHovered(opt.id) and core:wasHovered(opt.id) 26 | } 27 | end 28 | -------------------------------------------------------------------------------- /demo/suit/license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Matthias Richter 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | Except as contained in this notice, the name(s) of the above copyright holders 14 | shall not be used in advertising or otherwise to promote the sale, use or 15 | other dealings in this Software without prior written authorization. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /demo/suit/docs/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | Copyright (c) 2016 Matthias Richter 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | Except as contained in this notice, the name(s) of the above copyright holders 17 | shall not be used in advertising or otherwise to promote the sale, use or 18 | other dealings in this Software without prior written authorization. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /demo/suit/imagebutton.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...):match('(.-)[^%.]+$') 4 | 5 | return function(core, normal, ...) 6 | local opt, x,y = core.getOptionsAndSize(...) 7 | opt.normal = normal or opt.normal or opt[1] 8 | opt.hovered = opt.hovered or opt[2] or opt.normal 9 | opt.active = opt.active or opt[3] or opt.hovered 10 | assert(opt.normal, "Need at least `normal' state image") 11 | opt.id = opt.id or opt.normal 12 | 13 | opt.state = core:registerMouseHit(opt.id, x,y, function(u,v) 14 | local id = opt.normal:getData() 15 | assert(id:typeOf("ImageData"), "Can only use uncompressed images") 16 | u, v = math.floor(u+.5), math.floor(v+.5) 17 | if u < 0 or u >= opt.normal:getWidth() or v < 0 or v >= opt.normal:getHeight() then 18 | return false 19 | end 20 | local _,_,_,a = id:getPixel(u,v) 21 | return a > 0 22 | end) 23 | 24 | local img = opt.normal 25 | if core:isActive(opt.id) then 26 | img = opt.active 27 | elseif core:isHovered(opt.id) then 28 | img = opt.hovered 29 | end 30 | 31 | core:registerDraw(opt.draw or function(img,x,y, r,g,b,a) 32 | love.graphics.setColor(r,g,b,a) 33 | love.graphics.draw(img,x,y) 34 | end, img, x,y, love.graphics.getColor()) 35 | 36 | return { 37 | id = opt.id, 38 | hit = core:mouseReleasedOn(opt.id), 39 | hovered = core:isHovered(opt.id), 40 | entered = core:isHovered(opt.id) and not core:wasHovered(opt.id), 41 | left = not core:isHovered(opt.id) and core:wasHovered(opt.id) 42 | } 43 | end 44 | -------------------------------------------------------------------------------- /demo/suit/slider.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...):match('(.-)[^%.]+$') 4 | 5 | return function(core, info, ...) 6 | local opt, x,y,w,h = core.getOptionsAndSize(...) 7 | 8 | opt.id = opt.id or info 9 | 10 | info.min = info.min or math.min(info.value, 0) 11 | info.max = info.max or math.max(info.value, 1) 12 | info.step = info.step or (info.max - info.min) / 10 13 | local fraction = (info.value - info.min) / (info.max - info.min) 14 | local value_changed = false 15 | 16 | opt.state = core:registerHitbox(opt.id, x,y,w,h) 17 | 18 | if core:isActive(opt.id) then 19 | -- mouse update 20 | local mx,my = core:getMousePosition() 21 | if opt.vertical then 22 | fraction = math.min(1, math.max(0, (y+h - my) / h)) 23 | else 24 | fraction = math.min(1, math.max(0, (mx - x) / w)) 25 | end 26 | local v = fraction * (info.max - info.min) + info.min 27 | if v ~= info.value then 28 | info.value = v 29 | value_changed = true 30 | end 31 | 32 | -- keyboard update 33 | local key_up = opt.vertical and 'up' or 'right' 34 | local key_down = opt.vertical and 'down' or 'left' 35 | if core:getPressedKey() == key_up then 36 | info.value = math.min(info.max, info.value + info.step) 37 | value_changed = true 38 | elseif core:getPressedKey() == key_down then 39 | info.value = math.max(info.min, info.value - info.step) 40 | value_changed = true 41 | end 42 | end 43 | 44 | core:registerDraw(opt.draw or core.theme.Slider, fraction, opt, x,y,w,h) 45 | 46 | return { 47 | id = opt.id, 48 | hit = core:mouseReleasedOn(opt.id), 49 | changed = value_changed, 50 | hovered = core:isHovered(opt.id), 51 | entered = core:isHovered(opt.id) and not core:wasHovered(opt.id), 52 | left = not core:isHovered(opt.id) and core:wasHovered(opt.id) 53 | } 54 | end 55 | -------------------------------------------------------------------------------- /demo/suit/README.md: -------------------------------------------------------------------------------- 1 | # SUIT 2 | 3 | Simple User Interface Toolkit for LÖVE. 4 | 5 | SUIT is an immediate mode GUI library. 6 | 7 | ## Documentation? 8 | 9 | Over at [readthedocs](http://suit.readthedocs.org/en/latest/). 10 | 11 | ## Looks? 12 | 13 | Here is how SUIT looks like with the default theme: 14 | 15 | ![Demo of all widgets](docs/_static/demo.gif) 16 | 17 | More info and code is over at [readthedocs](http://suit.readthedocs.org/en/latest/). 18 | 19 | ## Hello, World! 20 | 21 | ```lua 22 | -- suit up 23 | local suit = require 'suit' 24 | 25 | -- storage for text input 26 | local input = {text = ""} 27 | 28 | -- all the UI is defined in love.update or functions that are called from here 29 | function love.update(dt) 30 | -- put the layout origin at position (100,100) 31 | -- the layout will grow down and to the right from this point 32 | suit.layout:reset(100,100) 33 | 34 | -- put an input widget at the layout origin, with a cell size of 200 by 30 pixels 35 | suit.Input(input, suit.layout:row(200,30)) 36 | 37 | -- put a label that displays the text below the first cell 38 | -- the cell size is the same as the last one (200x30 px) 39 | -- the label text will be aligned to the left 40 | suit.Label("Hello, "..input.text, {align = "left"}, suit.layout:row()) 41 | 42 | -- put an empty cell that has the same size as the last cell (200x30 px) 43 | suit.layout:row() 44 | 45 | -- put a button of size 200x30 px in the cell below 46 | -- if the button is pressed, quit the game 47 | if suit.Button("Close", suit.layout:row()).hit then 48 | love.event.quit() 49 | end 50 | end 51 | 52 | function love.draw() 53 | -- draw the gui 54 | suit.draw() 55 | end 56 | 57 | function love.textinput(t) 58 | -- forward text input to SUIT 59 | suit.textinput(t) 60 | end 61 | 62 | function love.keypressed(key) 63 | -- forward keypresses to SUIT 64 | suit.keypressed(key) 65 | end 66 | ``` 67 | -------------------------------------------------------------------------------- /demo/suit/init.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...) .. "." 4 | local suit = require(BASE .. "core") 5 | 6 | local instance = suit.new() 7 | return setmetatable({ 8 | new = suit.new, 9 | getOptionsAndSize = suit.getOptionsAndSize, 10 | 11 | -- core functions 12 | anyHovered = function(...) return instance:anyHovered(...) end, 13 | isHovered = function(...) return instance:isHovered(...) end, 14 | wasHovered = function(...) return instance:wasHovered(...) end, 15 | isActive = function(...) return instance:isActive(...) end, 16 | 17 | mouseInRect = function(...) return instance:mouseInRect(...) end, 18 | registerHitbox = function(...) return instance:registerHitbox(...) end, 19 | registerMouseHit = function(...) return instance:registerMouseHit(...) end, 20 | mouseReleasedOn = function(...) return instance:mouseReleasedOn(...) end, 21 | updateMouse = function(...) return instance:updateMouse(...) end, 22 | getMousePosition = function(...) return instance:getMousePosition(...) end, 23 | 24 | getPressedKey = function(...) return instance:getPressedKey(...) end, 25 | keypressed = function(...) return instance:keypressed(...) end, 26 | textinput = function(...) return instance:textinput(...) end, 27 | grabKeyboardFocus = function(...) return instance:grabKeyboardFocus(...) end, 28 | hasKeyboardFocus = function(...) return instance:hasKeyboardFocus(...) end, 29 | keyPressedOn = function(...) return instance:keyPressedOn(...) end, 30 | 31 | enterFrame = function(...) return instance:enterFrame(...) end, 32 | exitFrame = function(...) return instance:exitFrame(...) end, 33 | registerDraw = function(...) return instance:registerDraw(...) end, 34 | draw = function(...) return instance:draw(...) end, 35 | 36 | -- widgets 37 | Button = function(...) return instance:Button(...) end, 38 | ImageButton = function(...) return instance:ImageButton(...) end, 39 | Label = function(...) return instance:Label(...) end, 40 | Checkbox = function(...) return instance:Checkbox(...) end, 41 | Input = function(...) return instance:Input(...) end, 42 | Slider = function(...) return instance:Slider(...) end, 43 | 44 | -- layout 45 | layout = instance.layout 46 | }, { 47 | -- theme 48 | __newindex = function(t, k, v) 49 | if k == "theme" then 50 | instance.theme = v 51 | else 52 | rawset(t, k, v) 53 | end 54 | end, 55 | __index = function(t, k) 56 | return k == "theme" and instance.theme or rawget(t, k) 57 | end, 58 | }) 59 | -------------------------------------------------------------------------------- /demo/main.lua: -------------------------------------------------------------------------------- 1 | local sodapop = require 'sodapop' 2 | local suit = require 'suit' 3 | 4 | local mushroom = sodapop.newAnimatedSprite(600, 200) 5 | mushroom:addAnimation('walk', { 6 | image = love.graphics.newImage 'mushroom walk.png', 7 | frameWidth = 64, 8 | frameHeight = 64, 9 | frames = { 10 | {1, 1, 4, 1, .15} 11 | } 12 | }) 13 | mushroom:addAnimation('burrow', { 14 | image = love.graphics.newImage 'mushroom burrow.png', 15 | frameWidth = 64, 16 | frameHeight = 64, 17 | stopAtEnd = true, 18 | frames = { 19 | {1, 1, 12, 1, .05} 20 | } 21 | }) 22 | mushroom:addAnimation('unburrow', { 23 | image = love.graphics.newImage 'mushroom burrow.png', 24 | frameWidth = 64, 25 | frameHeight = 64, 26 | reverse = true, 27 | onReachedEnd = function() mushroom:switch 'walk' end, 28 | frames = { 29 | {1, 1, 12, 1, .05} 30 | } 31 | }) 32 | 33 | local sliders = { 34 | rotation = {name = 'Rotation', value = 0, min = 0, max = 2 * math.pi}, 35 | sx = {name = 'Horizontal scale', value = 1, min = 0, max = 5}, 36 | sy = {name = 'Vertical scale', value = 1, min = 0, max = 5}, 37 | } 38 | 39 | local anchorToMouse = false 40 | 41 | function love.update(dt) 42 | suit.layout:reset(100, 100) 43 | suit.layout:padding(10, 10) 44 | 45 | -- sliders 46 | for _, slider in pairs(sliders) do 47 | suit.Label(slider.name, {align = 'left'}, suit.layout:row(300, 0)) 48 | suit.Slider(slider, suit.layout:row(300, 30)) 49 | end 50 | 51 | -- reset button 52 | local button = suit.Button('Reset', suit.layout:row(300, 30)) 53 | if button.hit then 54 | sliders.rotation.value = 0 55 | sliders.sx.value = 1 56 | sliders.sy.value = 1 57 | end 58 | 59 | -- anchor to mouse toggle 60 | if anchorToMouse then 61 | local button = suit.Button('Remove anchor', suit.layout:row(300, 30)) 62 | if button.hit then 63 | anchorToMouse = false 64 | mushroom:setAnchor() 65 | mushroom.x, mushroom.y = 600, 200 66 | end 67 | else 68 | local button = suit.Button('Anchor to mouse', suit.layout:row(300, 30)) 69 | if button.hit then 70 | anchorToMouse = true 71 | mushroom:setAnchor(function() 72 | return love.mouse.getX(), love.mouse.getY() 73 | end) 74 | end 75 | end 76 | 77 | -- switch animation buttons 78 | local button = suit.Button('Burrow', suit.layout:row(145, 30)) 79 | if button.hit then 80 | mushroom:switch 'burrow' 81 | end 82 | local button = suit.Button('Unburrow', suit.layout:col(145, 30)) 83 | if button.hit then 84 | mushroom:switch 'unburrow' 85 | end 86 | 87 | -- link sprite properties to sliders 88 | mushroom.r = sliders.rotation.value 89 | mushroom.sx = sliders.sx.value 90 | mushroom.sy = sliders.sy.value 91 | 92 | mushroom:update(dt) 93 | end 94 | 95 | function love.draw() 96 | mushroom:draw() 97 | suit.draw() 98 | end 99 | -------------------------------------------------------------------------------- /demo/suit/input.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...):match('(.-)[^%.]+$') 4 | local utf8 = require 'utf8' 5 | 6 | local function split(str, pos) 7 | local offset = utf8.offset(str, pos) or 0 8 | return str:sub(1, offset-1), str:sub(offset) 9 | end 10 | 11 | return function(core, input, ...) 12 | local font = love.graphics.getFont() 13 | local opt, x,y,w,h = core.getOptionsAndSize(...) 14 | opt.id = opt.id or input 15 | opt.font = opt.font or love.graphics.getFont() 16 | 17 | local text_width = opt.font:getWidth(input.text) 18 | w = w or text_width + 4 19 | h = h or opt.font:getHeight() + 4 20 | 21 | input.text = input.text or "" 22 | input.cursor = math.max(1, math.min(utf8.len(input.text)+1, input.cursor or utf8.len(input.text)+1)) 23 | -- cursor is position *before* the character (including EOS) i.e. in "hello": 24 | -- position 1: |hello 25 | -- position 2: h|ello 26 | -- ... 27 | -- position 6: hello| 28 | 29 | -- get size of text and cursor position 30 | opt.cursor_pos = 0 31 | if input.cursor > 1 then 32 | local s = input.text:sub(0, utf8.offset(input.text, input.cursor)-1) 33 | opt.cursor_pos = opt.font:getWidth(s) 34 | end 35 | 36 | -- compute drawing offset 37 | input.text_draw_offset = input.text_draw_offset or 0 38 | if opt.cursor_pos - input.text_draw_offset < 0 then 39 | -- cursor left of input box 40 | input.text_draw_offset = opt.cursor_pos 41 | end 42 | if opt.cursor_pos - input.text_draw_offset > w then 43 | -- cursor right of input box 44 | input.text_draw_offset = opt.cursor_pos - w 45 | end 46 | if text_width - input.text_draw_offset < w and text_width > w then 47 | -- text bigger than input box, but does not fill it 48 | input.text_draw_offset = text_width - w 49 | end 50 | 51 | -- user interaction 52 | opt.state = core:registerHitbox(opt.id, x,y,w,h) 53 | opt.hasKeyboardFocus = core:grabKeyboardFocus(opt.id) 54 | 55 | if opt.hasKeyboardFocus then 56 | local keycode,char = core:getPressedKey() 57 | -- text input 58 | if char ~= "" then 59 | local a,b = split(input.text, input.cursor) 60 | input.text = table.concat{a, char, b} 61 | input.cursor = input.cursor + 1 62 | end 63 | 64 | -- text editing 65 | if keycode == 'backspace' then 66 | local a,b = split(input.text, input.cursor) 67 | input.text = table.concat{split(a,utf8.len(a)), b} 68 | input.cursor = math.max(1, input.cursor-1) 69 | elseif keycode == 'delete' then 70 | local a,b = split(input.text, input.cursor) 71 | local _,b = split(b, 2) 72 | input.text = table.concat{a, b} 73 | end 74 | 75 | -- cursor movement 76 | if keycode =='left' then 77 | input.cursor = math.max(0, input.cursor-1) 78 | elseif keycode =='right' then -- cursor movement 79 | input.cursor = math.min(utf8.len(input.text)+1, input.cursor+1) 80 | elseif keycode =='home' then -- cursor movement 81 | input.cursor = 1 82 | elseif keycode =='end' then -- cursor movement 83 | input.cursor = utf8.len(input.text)+1 84 | end 85 | 86 | -- move cursor position with mouse when clicked on 87 | if core:mouseReleasedOn(opt.id) then 88 | local mx = core:getMousePosition() - x + input.text_draw_offset 89 | input.cursor = utf8.len(input.text) + 1 90 | for c = 1,input.cursor do 91 | local s = input.text:sub(0, utf8.offset(input.text, c)-1) 92 | if opt.font:getWidth(s) >= mx then 93 | input.cursor = c-1 94 | break 95 | end 96 | end 97 | end 98 | end 99 | 100 | core:registerDraw(opt.draw or core.theme.Input, input, opt, x,y,w,h) 101 | 102 | return { 103 | id = opt.id, 104 | hit = core:mouseReleasedOn(opt.id), 105 | submitted = core:keyPressedOn(opt.id, "return"), 106 | hovered = core:isHovered(opt.id), 107 | entered = core:isHovered(opt.id) and not core:wasHovered(opt.id), 108 | left = not core:isHovered(opt.id) and core:wasHovered(opt.id) 109 | } 110 | end 111 | -------------------------------------------------------------------------------- /demo/suit/theme.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local BASE = (...):match('(.-)[^%.]+$') 4 | 5 | local theme = {} 6 | theme.cornerRadius = 4 7 | 8 | theme.color = { 9 | normal = {bg = { 66, 66, 66}, fg = {188,188,188}}, 10 | hovered = {bg = { 50,153,187}, fg = {255,255,255}}, 11 | active = {bg = {255,153, 0}, fg = {225,225,225}} 12 | } 13 | 14 | 15 | -- HELPER 16 | function theme.getColorForState(opt) 17 | local s = opt.state or "normal" 18 | return (opt.color and opt.color[opt.state]) or theme.color[s] 19 | end 20 | 21 | function theme.drawBox(x,y,w,h, colors) 22 | love.graphics.setColor(colors.bg) 23 | love.graphics.rectangle('fill', x,y, w,h, theme.cornerRadius) 24 | end 25 | 26 | function theme.getVerticalOffsetForAlign(valign, font, h) 27 | if valign == "top" then 28 | return 0 29 | elseif valign == "bottom" then 30 | return h - font:getHeight() 31 | end 32 | -- else: "middle" 33 | return (h - font:getHeight()) / 2 34 | end 35 | 36 | -- WIDGET VIEWS 37 | function theme.Label(text, opt, x,y,w,h) 38 | y = y + theme.getVerticalOffsetForAlign(opt.valign, opt.font, h) 39 | 40 | love.graphics.setColor((opt.color and opt.color.normal or {}).fg or theme.color.normal.fg) 41 | love.graphics.setFont(opt.font) 42 | love.graphics.printf(text, x+2, y, w-4, opt.align or "center") 43 | end 44 | 45 | function theme.Button(text, opt, x,y,w,h) 46 | local c = theme.getColorForState(opt) 47 | 48 | theme.drawBox(x,y,w,h, c) 49 | love.graphics.setColor(c.fg) 50 | love.graphics.setFont(opt.font) 51 | 52 | y = y + theme.getVerticalOffsetForAlign(opt.valign, opt.font, h) 53 | love.graphics.printf(text, x+2, y, w-4, opt.align or "center") 54 | end 55 | 56 | function theme.Checkbox(chk, opt, x,y,w,h) 57 | local c = theme.getColorForState(opt) 58 | local th = opt.font:getHeight() 59 | 60 | theme.drawBox(x+h/10,y+h/10,h*.8,h*.8, c) 61 | love.graphics.setColor(c.fg) 62 | if chk.checked then 63 | love.graphics.setLineStyle('smooth') 64 | love.graphics.setLineWidth(5) 65 | love.graphics.setLineJoin("bevel") 66 | love.graphics.line(x+h*.2,y+h*.55, x+h*.45,y+h*.75, x+h*.8,y+h*.2) 67 | end 68 | 69 | if chk.text then 70 | love.graphics.setFont(opt.font) 71 | y = y + theme.getVerticalOffsetForAlign(opt.valign, opt.font, h) 72 | love.graphics.printf(chk.text, x + h, y, w - h, opt.align or "left") 73 | end 74 | end 75 | 76 | function theme.Slider(fraction, opt, x,y,w,h) 77 | local xb, yb, wb, hb -- size of the progress bar 78 | local r = math.min(w,h) / 2.1 79 | if opt.vertical then 80 | x, w = x + w*.25, w*.5 81 | xb, yb, wb, hb = x, y+h*(1-fraction), w, h*fraction 82 | else 83 | y, h = y + h*.25, h*.5 84 | xb, yb, wb, hb = x,y, w*fraction, h 85 | end 86 | 87 | local c = theme.getColorForState(opt) 88 | theme.drawBox(x,y,w,h, c) 89 | theme.drawBox(x,yb,wb,hb, {bg=c.fg}) 90 | 91 | if opt.state ~= nil and opt.state ~= "normal" then 92 | love.graphics.setColor((opt.color and opt.color.active or {}).fg or theme.color.active.fg) 93 | if opt.vertical then 94 | love.graphics.circle('fill', x+wb/2, yb, r) 95 | else 96 | love.graphics.circle('fill', x+wb, yb+hb/2, r) 97 | end 98 | end 99 | end 100 | 101 | function theme.Input(input, opt, x,y,w,h) 102 | local utf8 = require 'utf8' 103 | theme.drawBox(x,y,w,h, (opt.color and opt.color.normal) or theme.color.normal) 104 | x = x + 3 105 | w = w - 6 106 | 107 | local th = opt.font:getHeight() 108 | 109 | -- set scissors 110 | local sx, sy, sw, sh = love.graphics.getScissor() 111 | love.graphics.setScissor(x-1,y,w+2,h) 112 | x = x - input.text_draw_offset 113 | 114 | -- text 115 | love.graphics.setColor(opt.color and opt.color.normal or theme.color.normal.fg) 116 | love.graphics.setFont(opt.font) 117 | love.graphics.print(input.text, x, y+(h-th)/2) 118 | 119 | -- cursor 120 | if opt.hasKeyboardFocus and (love.timer.getTime() % 1) > .5 then 121 | love.graphics.setLineWidth(1) 122 | love.graphics.setLineStyle('rough') 123 | love.graphics.line(x + opt.cursor_pos, y + (h-th)/2, 124 | x + opt.cursor_pos, y + (h+th)/2) 125 | end 126 | 127 | -- reset scissor 128 | love.graphics.setScissor(sx,sy,sw,sh) 129 | end 130 | 131 | return theme 132 | -------------------------------------------------------------------------------- /demo/suit/core.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local NONE = {} 4 | local BASE = (...):match('(.-)[^%.]+$') 5 | local default_theme = require(BASE..'theme') 6 | 7 | local suit = {} 8 | suit.__index = suit 9 | 10 | function suit.new(theme) 11 | return setmetatable({ 12 | -- TODO: deep copy/copy on write? better to let user handle => documentation? 13 | theme = theme or default_theme, 14 | mouse_x = 0, mouse_y = 0, 15 | mouse_button_down = false, 16 | 17 | draw_queue = {n = 0}, 18 | 19 | Button = require(BASE.."button"), 20 | ImageButton = require(BASE.."imagebutton"), 21 | Label = require(BASE.."label"), 22 | Checkbox = require(BASE.."checkbox"), 23 | Input = require(BASE.."input"), 24 | Slider = require(BASE.."slider"), 25 | 26 | layout = require(BASE.."layout").new(), 27 | }, suit) 28 | end 29 | 30 | -- helper 31 | function suit.getOptionsAndSize(opt, ...) 32 | if type(opt) == "table" then 33 | return opt, ... 34 | end 35 | return {}, opt, ... 36 | end 37 | 38 | -- gui state 39 | function suit:anyHovered() 40 | return self.hovered ~= nil 41 | end 42 | 43 | function suit:isHovered(id) 44 | return id == self.hovered 45 | end 46 | 47 | function suit:wasHovered(id) 48 | return id == self.hovered_last 49 | end 50 | 51 | function suit:isActive(id) 52 | return id == self.active 53 | end 54 | 55 | function suit:getStateName(id) 56 | if self:isActive(id) then 57 | return "active" 58 | elseif self:isHovered(id) then 59 | return "hovered" 60 | end 61 | return "normal" 62 | end 63 | 64 | -- mouse handling 65 | function suit:mouseInRect(x,y,w,h) 66 | return self.mouse_x >= x and self.mouse_y >= y and 67 | self.mouse_x <= x+w and self.mouse_y <= y+h 68 | end 69 | 70 | function suit:registerMouseHit(id, ul_x, ul_y, hit) 71 | if hit(self.mouse_x - ul_x, self.mouse_y - ul_y) then 72 | self.hovered = id 73 | if self.active == nil and self.mouse_button_down then 74 | self.active = id 75 | end 76 | end 77 | return self:getStateName(id) 78 | end 79 | 80 | function suit:registerHitbox(id, x,y,w,h) 81 | return self:registerMouseHit(id, x,y, function(x,y) 82 | return x >= 0 and x <= w and y >= 0 and y <= h 83 | end) 84 | end 85 | 86 | function suit:mouseReleasedOn(id) 87 | return not self.mouse_button_down and self:isActive(id) and self:isHovered(id) 88 | end 89 | 90 | function suit:updateMouse(x, y, button_down) 91 | self.mouse_x, self.mouse_y = x,y 92 | if button_down ~= nil then 93 | self.mouse_button_down = button_down 94 | end 95 | end 96 | 97 | function suit:getMousePosition() 98 | return self.mouse_x, self.mouse_y 99 | end 100 | 101 | -- keyboard handling 102 | function suit:getPressedKey() 103 | return self.key_down, self.textchar 104 | end 105 | 106 | function suit:keypressed(key) 107 | self.key_down = key 108 | end 109 | 110 | function suit:textinput(char) 111 | self.textchar = char 112 | end 113 | 114 | function suit:grabKeyboardFocus(id) 115 | if self:isActive(id) then 116 | if love.system.getOS() == "Android" or love.system.getOS() == "iOS" then 117 | if id == NONE then 118 | love.keyboard.setTextInput( false ) 119 | else 120 | love.keyboard.setTextInput( true ) 121 | end 122 | end 123 | self.keyboardFocus = id 124 | end 125 | return self:hasKeyboardFocus(id) 126 | end 127 | 128 | function suit:hasKeyboardFocus(id) 129 | return self.keyboardFocus == id 130 | end 131 | 132 | function suit:keyPressedOn(id, key) 133 | return self:hasKeyboardFocus(id) and self.key_down == key 134 | end 135 | 136 | -- state update 137 | function suit:enterFrame() 138 | self.hovered_last, self.hovered = self.hovered, nil 139 | self:updateMouse(love.mouse.getX(), love.mouse.getY(), love.mouse.isDown(1)) 140 | self.key_down, self.textchar = nil, "" 141 | self:grabKeyboardFocus(NONE) 142 | end 143 | 144 | function suit:exitFrame() 145 | if not self.mouse_button_down then 146 | self.active = nil 147 | elseif self.active == nil then 148 | self.active = NONE 149 | end 150 | end 151 | 152 | -- draw 153 | function suit:registerDraw(f, ...) 154 | local args = {...} 155 | local nargs = select('#', ...) 156 | self.draw_queue.n = self.draw_queue.n + 1 157 | self.draw_queue[self.draw_queue.n] = function() 158 | f(unpack(args, 1, nargs)) 159 | end 160 | end 161 | 162 | function suit:draw() 163 | self:exitFrame() 164 | for i = 1,self.draw_queue.n do 165 | self.draw_queue[i]() 166 | end 167 | self.draw_queue.n = 0 168 | self:enterFrame() 169 | end 170 | 171 | return suit 172 | -------------------------------------------------------------------------------- /sodapop.lua: -------------------------------------------------------------------------------- 1 | local sodapop = { 2 | _VERSION = 'Sodapop', 3 | _DESCRIPTION = 'A sprite and animation library for LÖVE', 4 | _URL = 'https://github.com/tesselode/sodapop', 5 | _LICENSE = [[ 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2016 Andrew Minnich 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | ]] 28 | } 29 | 30 | local Animation = {} 31 | 32 | function Animation:addFrames(x1, y1, x2, y2, duration) 33 | for x = x1, x2 do 34 | for y = y1, y2 do 35 | table.insert(self.frames, { 36 | quad = love.graphics.newQuad( 37 | self.frameWidth * (x - 1), 38 | self.frameHeight * (y - 1), 39 | self.frameWidth, 40 | self.frameHeight, 41 | self.image:getWidth(), 42 | self.image:getHeight() 43 | ), 44 | duration = duration, 45 | }) 46 | end 47 | end 48 | end 49 | 50 | function Animation:reverse() 51 | local newFrames = {} 52 | for i = #self.frames, 1, -1 do 53 | table.insert(newFrames, self.frames[i]) 54 | end 55 | self.frames = newFrames 56 | end 57 | 58 | function Animation:advance() 59 | if self.current == #self.frames then 60 | self:onReachedEnd() 61 | if self.stopAtEnd then 62 | self.playing = false 63 | else 64 | self.current = 1 65 | end 66 | else 67 | self.current = self.current + 1 68 | end 69 | if self.playing then 70 | self.timer = self.timer + self.frames[self.current].duration 71 | end 72 | end 73 | 74 | function Animation:goToFrame(frame) 75 | assert(frame <= #self.frames, 'Frame number out of range') 76 | self.current = frame 77 | self.timer = self.frames[self.current].duration 78 | self.playing = true 79 | end 80 | 81 | function Animation:update(dt) 82 | if self.playing then 83 | self.timer = self.timer - dt 84 | while self.timer < 0 do 85 | self:advance() 86 | if not self.playing then 87 | break 88 | end 89 | end 90 | end 91 | end 92 | 93 | function Animation:draw(x, y, r, sx, sy, flipX, flipY) 94 | if flipX then sx = -sx end 95 | if flipY then sy = -sy end 96 | 97 | love.graphics.draw(self.image, self.frames[self.current].quad, x, y, 98 | r, sx, sy, self.frameWidth / 2, self.frameHeight / 2) 99 | end 100 | 101 | local function newAnimation(parameters) 102 | local animation = { 103 | image = parameters.image, 104 | frameWidth = parameters.frameWidth, 105 | frameHeight = parameters.frameHeight, 106 | stopAtEnd = parameters.stopAtEnd, 107 | onReachedEnd = parameters.onReachedEnd or function() end, 108 | frames = {}, 109 | 110 | playing = true, 111 | current = 1, 112 | } 113 | setmetatable(animation, {__index = Animation}) 114 | 115 | for i = 1, #parameters.frames do 116 | animation:addFrames(unpack(parameters.frames[i])) 117 | end 118 | if parameters.reverse then animation:reverse() end 119 | animation.timer = animation.frames[1].duration 120 | 121 | return animation 122 | end 123 | 124 | local Sprite = {} 125 | 126 | function Sprite:addAnimation(name, parameters) 127 | self.animations[name] = newAnimation(parameters) 128 | if not self.current then self:switch(name) end 129 | end 130 | 131 | function Sprite:switch(name, resume) 132 | assert(self.animations[name], 'No animation named '..name) 133 | self.current = self.animations[name] 134 | if resume then else self.current:goToFrame(1) end 135 | end 136 | 137 | function Sprite:goToFrame(frame) self.current:goToFrame(frame) end 138 | 139 | function Sprite:setAnchor(f) 140 | self.anchor = f 141 | end 142 | 143 | function Sprite:update(dt) 144 | if self.playing then self.current:update(dt) end 145 | if self.anchor then 146 | self.x, self.y = self.anchor() 147 | end 148 | end 149 | 150 | function Sprite:draw(ox, oy) 151 | ox, oy = ox or 0, oy or 0 152 | love.graphics.setColor(self.color) 153 | self.current:draw(self.x + ox, self.y + oy, self.r, self.sx, self.sy, 154 | self.flipX, self.flipY) 155 | end 156 | 157 | function sodapop.newAnimatedSprite(x, y) 158 | local sprite = { 159 | animations = {}, 160 | x = x, 161 | y = y, 162 | r = 0, 163 | sx = 1, 164 | sy = 1, 165 | flipX = false, 166 | flipY = false, 167 | color = {255, 255, 255, 255}, 168 | playing = true, 169 | } 170 | setmetatable(sprite, {__index = Sprite}) 171 | return sprite 172 | end 173 | 174 | function sodapop.newSprite(image, x, y) 175 | local sprite = sodapop.newAnimatedSprite(x, y) 176 | sprite:addAnimation('main', { 177 | image = image, 178 | frameWidth = image:getWidth(), 179 | frameHeight = image:getHeight(), 180 | frames = { 181 | {1, 1, 1, 1, 1}, 182 | }, 183 | }) 184 | return sprite 185 | end 186 | 187 | return sodapop 188 | -------------------------------------------------------------------------------- /demo/sodapop.lua: -------------------------------------------------------------------------------- 1 | local sodapop = { 2 | _VERSION = 'Sodapop', 3 | _DESCRIPTION = 'A sprite and animation library for LÖVE', 4 | _URL = 'https://github.com/tesselode/sodapop', 5 | _LICENSE = [[ 6 | The MIT License (MIT) 7 | 8 | Copyright (c) 2016 Andrew Minnich 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | ]] 28 | } 29 | 30 | local Animation = {} 31 | 32 | function Animation:addFrames(x1, y1, x2, y2, duration) 33 | for x = x1, x2 do 34 | for y = y1, y2 do 35 | table.insert(self.frames, { 36 | quad = love.graphics.newQuad( 37 | self.frameWidth * (x - 1), 38 | self.frameHeight * (y - 1), 39 | self.frameWidth, 40 | self.frameHeight, 41 | self.image:getWidth(), 42 | self.image:getHeight() 43 | ), 44 | duration = duration, 45 | }) 46 | end 47 | end 48 | end 49 | 50 | function Animation:reverse() 51 | local newFrames = {} 52 | for i = #self.frames, 1, -1 do 53 | table.insert(newFrames, self.frames[i]) 54 | end 55 | self.frames = newFrames 56 | end 57 | 58 | function Animation:advance() 59 | if self.current == #self.frames then 60 | self:onReachedEnd() 61 | if self.stopAtEnd then 62 | self.playing = false 63 | else 64 | self.current = 1 65 | end 66 | else 67 | self.current = self.current + 1 68 | end 69 | if self.playing then 70 | self.timer = self.timer + self.frames[self.current].duration 71 | end 72 | end 73 | 74 | function Animation:goToFrame(frame) 75 | assert(frame <= #self.frames, 'Frame number out of range') 76 | self.current = frame 77 | self.timer = self.frames[self.current].duration 78 | self.playing = true 79 | end 80 | 81 | function Animation:update(dt) 82 | if self.playing then 83 | self.timer = self.timer - dt 84 | while self.timer < 0 do 85 | self:advance() 86 | if not self.playing then 87 | break 88 | end 89 | end 90 | end 91 | end 92 | 93 | function Animation:draw(x, y, r, sx, sy, flipX, flipY) 94 | if flipX then sx = -sx end 95 | if flipY then sy = -sy end 96 | 97 | love.graphics.draw(self.image, self.frames[self.current].quad, x, y, 98 | r, sx, sy, self.frameWidth / 2, self.frameHeight / 2) 99 | end 100 | 101 | local function newAnimation(parameters) 102 | local animation = { 103 | image = parameters.image, 104 | frameWidth = parameters.frameWidth, 105 | frameHeight = parameters.frameHeight, 106 | stopAtEnd = parameters.stopAtEnd, 107 | onReachedEnd = parameters.onReachedEnd or function() end, 108 | frames = {}, 109 | 110 | playing = true, 111 | current = 1, 112 | } 113 | setmetatable(animation, {__index = Animation}) 114 | 115 | for i = 1, #parameters.frames do 116 | animation:addFrames(unpack(parameters.frames[i])) 117 | end 118 | if parameters.reverse then animation:reverse() end 119 | animation.timer = animation.frames[1].duration 120 | 121 | return animation 122 | end 123 | 124 | local Sprite = {} 125 | 126 | function Sprite:addAnimation(name, parameters) 127 | self.animations[name] = newAnimation(parameters) 128 | if not self.current then self:switch(name) end 129 | end 130 | 131 | function Sprite:switch(name, resume) 132 | assert(self.animations[name], 'No animation named '..name) 133 | self.current = self.animations[name] 134 | if resume then else self.current:goToFrame(1) end 135 | end 136 | 137 | function Sprite:goToFrame(frame) self.current:goToFrame(frame) end 138 | 139 | function Sprite:setAnchor(f) 140 | self.anchor = f 141 | end 142 | 143 | function Sprite:update(dt) 144 | if self.playing then self.current:update(dt) end 145 | if self.anchor then 146 | self.x, self.y = self.anchor() 147 | end 148 | end 149 | 150 | function Sprite:draw(ox, oy) 151 | ox, oy = ox or 0, oy or 0 152 | love.graphics.setColor(self.color) 153 | self.current:draw(self.x + ox, self.y + oy, self.r, self.sx, self.sy, 154 | self.flipX, self.flipY) 155 | end 156 | 157 | function sodapop.newAnimatedSprite(x, y) 158 | local sprite = { 159 | animations = {}, 160 | x = x, 161 | y = y, 162 | r = 0, 163 | sx = 1, 164 | sy = 1, 165 | flipX = false, 166 | flipY = false, 167 | color = {255, 255, 255, 255}, 168 | playing = true, 169 | } 170 | setmetatable(sprite, {__index = Sprite}) 171 | return sprite 172 | end 173 | 174 | function sodapop.newSprite(image, x, y) 175 | local sprite = sodapop.newAnimatedSprite(x, y) 176 | sprite:addAnimation('main', { 177 | image = image, 178 | frameWidth = image:getWidth(), 179 | frameHeight = image:getHeight(), 180 | frames = { 181 | {1, 1, 1, 1, 1}, 182 | }, 183 | }) 184 | return sprite 185 | end 186 | 187 | return sodapop 188 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Sodapop 2 | ======= 3 | Sodapop is a sprite/animation library for LÖVE. It allows you to create sprites with a variety of animations, and it lets you easily do common transformations when drawing the sprites. 4 | 5 | Example 6 | ------- 7 | ```lua 8 | function love.load() 9 | sodapop = require 'sodapop' 10 | 11 | playerSprite = sodapop.newAnimatedSprite(100, 200) 12 | playerSprite:addAnimation('walk', { 13 | image = love.graphics.newImage 'walk.png', 14 | frameWidth = 64, 15 | frameHeight = 64, 16 | frames = { 17 | {1, 1, 4, 1, .2}, 18 | }, 19 | }) 20 | playerSprite:addAnimation('burrow', { 21 | image = love.graphics.newImage 'burrow.png', 22 | frameWidth = 64, 23 | frameHeight = 64, 24 | stopAtEnd = true, 25 | frames = { 26 | {1, 1, 12, 1, .05}, 27 | }, 28 | }) 29 | playerSprite:addAnimation('unburrow', { 30 | image = love.graphics.newImage 'burrow.png', 31 | frameWidth = 64, 32 | frameHeight = 64, 33 | stopAtEnd = true, 34 | reverse = true, 35 | onReachedEnd = function() playerSprite:switch 'walk' end, 36 | frames = { 37 | {1, 1, 12, 1, .05}, 38 | }, 39 | }) 40 | end 41 | 42 | function love.update(dt) 43 | playerSprite:update(dt) 44 | end 45 | 46 | function love.keypressed(key) 47 | if key == 'left' then playerSprite.flipX = true end 48 | if key == 'right' then playerSprite.flipX = false end 49 | if key == 'down' then playerSprite:switch 'burrow' end 50 | if key == 'up' then playerSprite:switch 'unburrow' end 51 | end 52 | 53 | function love.draw() 54 | playerSprite:draw() 55 | end 56 | ``` 57 | 58 | API 59 | --- 60 | ### `sprite = sodapop.newAnimatedSprite(x, y)` 61 | 62 | Creates a new sprite. 63 | - `x` (number) - the x coordinate of the center of the sprite. 64 | - `y` (number) - the y coordinate of the center of the sprite. 65 | 66 | **Note:** you do not have to include `x` and `y` if you use anchor functions (see below). 67 | 68 | ### `sprite:addAnimation(name, parameters)` 69 | 70 | Adds an animation to the sprite. 71 | - `name` (string) - the name of the animation. 72 | - `parameters` (table) - a table containing parameters. 73 | 74 | #### Parameters: 75 | - `image` (image) - the image the animation should use. 76 | - `frameWidth` (number) - the width of each frame of animation. 77 | - `frameHeight` (number) - the height of each frame of animation. 78 | - `stopAtEnd` (boolean, default=`false`) - whether the animation should stop when it reaches the last frame. 79 | - `onReachedEnd` (function, optional) - a function to call when the animation reaches the last frame. 80 | - `reverse` (boolean, default=`false`) - whether the list of frames should be reversed after they are added. 81 | - `frames` (table) - a list of frames. Frames are tables with the form `{x1, y1, x2, y2, duration}`. 82 | - `x1` (number) - the leftmost frame to load (in frames, not pixels). 83 | - `y1` (number) - the topmost frame to load (in frames, not pixels). 84 | - `x2` (number) - the rightmost frame to load (in frames, not pixels). 85 | - `y2` (number) - the bottommost frame to load (in frames, not pixels). 86 | - `duration` (number) - how long the frame should last (in seconds). 87 | - **Note:** frames are added left to right, top to bottom. 88 | 89 | #### Example: 90 | ```lua 91 | sprite:addAnimation('unburrow', { 92 | image = love.graphics.newImage 'burrow.png', 93 | frameWidth = 64, 94 | frameHeight = 64, 95 | stopAtEnd = true, 96 | reverse = true, 97 | onReachedEnd = function() sprite:switch 'walk' end, 98 | frames = { 99 | {1, 1, 3, 4, .05}, 100 | {4, 4, 4, 4, .1}, 101 | }, 102 | }) 103 | ``` 104 | 105 | ### `sprite:switch(name, resume)` 106 | 107 | Switches the sprite to a different animation. 108 | - `name` (string) - the name of the animation to switch to. 109 | - `resume` (boolean, default=`false`) - if `true`, the animation will continue from the frame that was playing last; if `false`, it will start over. 110 | 111 | ### `sprite:goToFrame(frame)` 112 | 113 | Jumps to a frame in the current animation. 114 | - `frame` (number) - the frame to jump to. 115 | 116 | ### `sprite:setAnchor(f)` 117 | 118 | Sets an anchor function for the sprite. When the sprite has an anchor function, it will automatically move to the coordinates returned by the function when you call `sprite:update()`. To remove the anchor function, call `sprite:setAnchor()` without any arguments. 119 | 120 | #### Example: 121 | ```lua 122 | --the sprite will automatically move to the mouse position 123 | sprite:setAnchor(function() 124 | return love.mouse.getX(), love.mouse.getY() 125 | end) 126 | ``` 127 | 128 | ### `sprite:update(dt)` 129 | 130 | Updates the sprite. 131 | - `dt` (number) - the time between the current and previous updates. 132 | 133 | ### ` sprite:draw(ox, oy) ` 134 | 135 | Draws the sprite. 136 | - `ox` (number, optional) - the horizontal offset to draw the sprite with. 137 | - `oy` (number, optional) - the vertical offset to draw the sprite with. 138 | 139 | __Note__: `ox` and `oy` do not take the scale of the sprite into account. They are merely added on to the sprite's position. 140 | 141 | ### `sprite = sodapop.newSprite(image, x, y)` 142 | 143 | A shortcut function for creating a sprite with no animation. This function creates a sprite with one animation named "main" with one frame. 144 | - `image` (image) - the image the sprite should have. 145 | - `x` (number) - the x coordinate of the center of the sprite. 146 | - `y` (number) - the y coordinate of the center of the sprite. 147 | 148 | ### Properties 149 | Sprites have a number of properties you can set. 150 | - `x` (number) - the x coordinate of the center of the sprite. 151 | - `y` (number) - the y coordinate of the center of the sprite. 152 | - `r` (number, default=`0`) - the rotation of the sprite (around the center, in radians). 153 | - `sx` (number, default=`1`) - the horizontal scale of the sprite. 154 | - `sy` (number, default=`1`) - the vertical scale of the sprite. 155 | - `flipX` (boolean, default=`false`) - whether the sprite should be flipped horizontally. 156 | - `flipY` (boolean, default=`false`) - whether the sprite should be flipped vertically. 157 | - `color` (table, default=`{255, 255, 255, 255}`) - the color the sprite should be drawn with (has the form `{r, g, b, a}`). 158 | - `playing` (boolean, default=`true`) - whether the current animation should play or not. 159 | -------------------------------------------------------------------------------- /demo/suit/docs/widgets.rst: -------------------------------------------------------------------------------- 1 | Widgets 2 | ======= 3 | 4 | .. note:: 5 | Still under construction... 6 | 7 | Immutable Widgets 8 | ----------------- 9 | 10 | .. function:: Button(text, [options], x,y,w,h) 11 | 12 | :param string text: Button label. 13 | :param table options: Optional settings (see below). 14 | :param numbers x,y: Upper left corner of the widget. 15 | :param numbers w,h: Width and height of the widget.o 16 | :returns: Return state (see below). 17 | 18 | Creates a button widget at position ``(x,y)`` with width ``w`` and height 19 | ``h``. 20 | 21 | .. function:: Label(text, [options], x,y,w,h) 22 | 23 | :param string text: Label text. 24 | :param table options: Optional settings (see below). 25 | :param numbers x,y: Upper left corner of the widget. 26 | :param numbers w,h: Width and height of the widget.o 27 | :returns: Return state (see below). 28 | 29 | Creates a label at position ``(x,y)`` with width ``w`` and height ``h``. 30 | 31 | .. function:: ImageButton(normal, options, x,y) 32 | 33 | :param Image notmal: Image of the button in normal state. 34 | :param table options: Widget options. 35 | :param numbers x,y: Upper left corner of the widget. 36 | :returns: Return state (see below). 37 | 38 | Creates an image button widget at position ``(x,y)``. 39 | Unlike all other widgets, an ``ImageButton`` is not affected by the current 40 | theme. 41 | The argument ``normal`` defines the image of the normal state as well as the 42 | area of the widget: The button activates when the mouse is over a pixel with 43 | non-zero alpha value. 44 | You can provide additional ``hovered`` and ``active`` images, but the widget area 45 | is always computed from the ``normal`` image. 46 | 47 | Note that ``ImageButton`` does not recieve width and height parameters. As 48 | such, it does not necessarily honor the cell size of a :doc:`layout`. 49 | 50 | **Additional Options:** 51 | 52 | ``normal`` 53 | Image for the normal state of the widget. Defaults to widget payload. 54 | 55 | ``hovered`` 56 | Image for the hovered state of the widget. Defaults to ``normal`` if omitted. 57 | 58 | ``active`` 59 | Image for the active state of the widget. Defaults to ``hovered`` if omitted. 60 | 61 | Mutable Widgets 62 | --------------- 63 | 64 | .. function:: Checkbox(checkbox, [options], x,y,w,h) 65 | 66 | :param table checkbox: Checkbox state. 67 | :param table options: Optional settings (see below). 68 | :param numbers x,y: Upper left corner of the widget. 69 | :param numbers w,h: Width and height of the widget.o 70 | :returns: Return state (see below). 71 | 72 | Creates a checkbox at position ``(x,y)`` with width ``w`` and height ``h``. 73 | 74 | **State:** 75 | 76 | ``checkbox`` is a table with the following components: 77 | 78 | ``checked`` 79 | ``true`` if the checkbox is checked, ``false`` otherwise. 80 | 81 | ``text`` 82 | Optional label to show besides the checkbox. 83 | 84 | .. function:: Slider(slider, [options], x,y,w,h) 85 | 86 | :param table slider: Slider state. 87 | :param table options: Optional settings (see below). 88 | :param numbers x,y: Upper left corner of the widget. 89 | :param numbers w,h: Width and height of the widget.o 90 | :returns: Return state (see below). 91 | 92 | Creates a slider at position ``(x,y)`` with width ``w`` and height ``h``. 93 | Sliders can be horizontal (default) or vertical. 94 | 95 | **State:** 96 | 97 | ``value`` 98 | Current value of the slider. Mandatory argument. 99 | 100 | ``min`` 101 | Minimum value of the slider. Defaults to ``min(value, 0)`` if omitted. 102 | 103 | ``max`` 104 | Maximum value of the slider. Defaults to ``min(value, 1)`` if omitted. 105 | 106 | ``step`` 107 | Value stepping for keyboard input. Defaults to ``(max - min)/10`` if omitted. 108 | 109 | **Additional Options:** 110 | 111 | ``vertical`` 112 | Whether the slider is vertical or horizontal. 113 | 114 | **Additional Return State:** 115 | 116 | ``changed`` 117 | ``true`` when the slider value was changed, ``false`` otherwise. 118 | 119 | 120 | .. function:: Input(input, [options], x,y,w,h) 121 | 122 | :param table input: Checkbox state 123 | :param table options: Optional settings (see below). 124 | :param numbers x,y: Upper left corner of the widget. 125 | :param numbers w,h: Width and height of the widget.o 126 | :returns: Return state (see below). 127 | 128 | Creates an input box at position ``(x,y)`` with width ``w`` and height ``h``. 129 | Implements typical movement (arrow keys, home and end key) and editing 130 | (deletion with backspace and delete) facilities. 131 | 132 | **State:** 133 | 134 | ``text`` 135 | Current text inside the input box. Defaults to the empty string if omitted. 136 | 137 | ``cursor`` 138 | Cursor position. Defined as the position before the character (including 139 | EOS), so ``1`` is the position before the first character, etc. Defaults to 140 | the end of ``text`` if omitted. 141 | 142 | **Additional Return State:** 143 | 144 | ``submitted`` 145 | ``true`` when enter was pressed while the widget has keyboard focus. 146 | 147 | 148 | Common Options 149 | -------------- 150 | 151 | ``id`` 152 | Identifier of the widget regarding user interaction. Defaults to the first 153 | argument (e.g., ``text`` for buttons) if omitted. 154 | 155 | ``font`` 156 | Font of the label. Defaults to the current font (``love.graphics.getFont()``). 157 | 158 | ``align`` 159 | Horizontal alignment of the label. One of ``"left"``, ``"center"``, or 160 | ``"right"``. Defaults to ``"center"``. 161 | 162 | ``valign`` 163 | Vertical alignment of the label. On of ``"top"``, ``"middle"``, or 164 | ``"bottom"``. Defaults to ``"middle"``. 165 | 166 | ``color`` 167 | A table to overwrite the color. Undefined colors default to the theme colors. 168 | 169 | ``draw`` 170 | A function to replace the drawing function. Refer to :doc:`themes` for more information about the function signatures. 171 | 172 | 173 | Common Return States 174 | -------------------- 175 | 176 | ``id`` 177 | Identifier of the widget. 178 | 179 | ``hit`` 180 | ``true`` if the mouse was pressed and released on the button, ``false`` 181 | otherwise. 182 | 183 | ``hovered`` 184 | ``true`` if the mouse is above the widget, ``false`` otherwise. 185 | 186 | ``entered`` 187 | ``true`` if the mouse entered the widget area, ``false`` otherwise. 188 | 189 | ``left`` 190 | ``true`` if the mouse left the widget area, ``false`` otherwise. 191 | -------------------------------------------------------------------------------- /demo/suit/docs/core.rst: -------------------------------------------------------------------------------- 1 | Core Functions 2 | ============== 3 | 4 | The core functions can be divided into two parts: Functions of interest to the 5 | user and functions of interest to the (widget) developer. 6 | 7 | External Interface 8 | ------------------ 9 | 10 | Drawing 11 | ^^^^^^^ 12 | 13 | .. function:: draw() 14 | 15 | Draw the GUI - call in ``love.draw``. 16 | 17 | .. data:: theme 18 | 19 | The current theme. See :doc:`themes`. 20 | 21 | 22 | Mouse Input 23 | ^^^^^^^^^^^ 24 | 25 | .. function:: updateMouse(x,y, buttonDown) 26 | 27 | :param number x,y: Position of the mouse. 28 | :param boolean buttonDown: Whether the mouse button is down. 29 | 30 | Update mouse position and button status. You do not need to call this function, 31 | unless you use some screen transformation (e.g., scaling, camera systems, ...). 32 | 33 | Keyboard Input 34 | ^^^^^^^^^^^^^^ 35 | 36 | .. function:: keypressed(key) 37 | 38 | :param KeyConstant key: The pressed key. 39 | 40 | Forwards a ``love.keypressed(key)`` event to SUIT. 41 | 42 | .. function:: textinput(char) 43 | 44 | :param string char: The pressed character 45 | 46 | Forwards a ``love.textinput(key)`` event to SUIT. 47 | 48 | 49 | Internal Helpers 50 | ---------------- 51 | 52 | .. function:: getOptionsAndSize(...) 53 | 54 | :param mixed ...: Varargs. 55 | :returns: ``options, x,y,w,h``. 56 | 57 | Converts varargs to option table and size definition. Used in the widget 58 | functions. 59 | 60 | .. function:: registerDraw(f, ...) 61 | 62 | :param function f: Function to call in ``draw()``. 63 | :param mixed ...: Arguments to f. 64 | 65 | Registers a function to be executed during :func:`draw()`. Used by widgets to 66 | make themselves visible. 67 | 68 | .. function:: enterFrame() 69 | 70 | Prepares GUI state when entering a frame. 71 | 72 | .. function:: exitFrame() 73 | 74 | Clears GUI state when exiting a frame. 75 | 76 | GUI State 77 | ^^^^^^^^^ 78 | 79 | .. function:: anyHovered() 80 | 81 | :returns: ``true`` if any widget is hovered by the mouse. 82 | 83 | Checks if any widget is hovered by the mouse. 84 | 85 | .. function:: isHovered(id) 86 | 87 | :param mixed id: Identifier of the widget. 88 | :returns: ``true`` if the widget is hovered by the mouse. 89 | 90 | Checks if the widget identified by ``id`` is hovered by the mouse. 91 | 92 | .. function:: wasHovered(id) 93 | 94 | :param mixed id: Identifier of the widget. 95 | :returns: ``true`` if the widget was in the hovered by the mouse in the last frame. 96 | 97 | Checks if the widget identified by ``id`` was hovered by the mouse in the last frame. 98 | 99 | .. function:: isActive(id) 100 | 101 | :param mixed id: Identifier of the widget. 102 | :returns: ``true`` if the widget is in the ``active`` state. 103 | 104 | Checks whether the mouse button is pressed on the widget identified by ``id``. 105 | 106 | 107 | Mouse Input 108 | ^^^^^^^^^^^ 109 | 110 | .. function:: mouseInRect(x,y,w,h) 111 | 112 | :param numbers x,y,w,h: Rectangle definition. 113 | :returns: ``true`` if the mouse cursor is in the rectangle. 114 | 115 | Checks whether the mouse cursor is in the rectangle defined by ``x,y,w,h``. 116 | 117 | .. function:: registerMouseHit(id, ul_x, ul_y, hit) 118 | 119 | :param mixed id: Identifier of the widget. 120 | :param numbers ul_x, ul_y: Upper left corner of the widget. 121 | :param function hit: Function to perform the hit test. 122 | 123 | Registers a hit-test defined by the function ``hit`` for the widget identified 124 | by ``id``. Sets the widget to ``hovered`` if th hit-test returns ``true``. Sets the 125 | widget to ``active`` if the hit-test returns ``true`` and the mouse button is 126 | pressed. 127 | 128 | The hit test receives coordinates in the coordinate system of the widget, i.e. 129 | ``(0,0)`` is the upper left corner of the widget. 130 | 131 | .. function:: registerHitbox(id, x,y,w,h) 132 | 133 | :param mixed id: Identifier of the widget. 134 | :param numbers x,y,w,h: Rectangle definition. 135 | 136 | Registers a hitbox for the widget identified by ``id``. Literally this function:: 137 | 138 | function registerHitbox(id, x,y,w,h) 139 | return registerMouseHit(id, x,y, function(u,v) 140 | return u >= 0 and u <= w and v >= 0 and v <= h 141 | end) 142 | end 143 | 144 | .. function:: mouseReleasedOn(id) 145 | 146 | :param mixed id: Identifier of the widget. 147 | :returns: ``true`` if the mouse was released on the widget. 148 | 149 | Checks whether the mouse button was released on the widget identified by ``id``. 150 | 151 | .. function:: getMousePosition() 152 | 153 | :returns: Mouse positon ``mx, my``. 154 | 155 | Get the mouse position. 156 | 157 | Keyboard Input 158 | ^^^^^^^^^^^^^^ 159 | 160 | .. function:: getPressedKey() 161 | 162 | :returns: KeyConstant 163 | 164 | Get the currently pressed key (if any). 165 | 166 | .. function:: grabKeyboardFocus(id) 167 | 168 | :param mixed id: Identifier of the widget. 169 | 170 | Try to grab keyboard focus. Successful only if the widget is in the ``active`` 171 | state. 172 | 173 | .. function:: hasKeyboardFocus(id) 174 | 175 | :param mixed id: Identifier of the widget. 176 | :returns: ``true`` if the widget has keyboard focus. 177 | 178 | Checks whether the widget identified by ``id`` currently has keyboard focus. 179 | 180 | .. function:: keyPressedOn(id, key) 181 | 182 | :param mixed id: Identifier of the widget. 183 | :param KeyConstant key: Key to query. 184 | :returns: ``true`` if ``key`` was pressed on the widget. 185 | 186 | Checks whether the key ``key`` was pressed while the widget identified by 187 | ``id`` has keyboard focus. 188 | 189 | 190 | Instancing 191 | ---------- 192 | 193 | .. function:: new() 194 | 195 | :returns: Separate UI state. 196 | 197 | Create a separate UI and layout state. Everything that happens in the new 198 | state will not affect any other state. You can use the new state like the 199 | "global" state ``suit``, but call functions with the colon syntax instead of 200 | the dot syntax, e.g.:: 201 | 202 | function love.load() 203 | dress = suit.new() 204 | end 205 | 206 | function love.update() 207 | dress.layout:reset() 208 | dress:Label("Hello, World!", dress.layout:row(200,30)) 209 | dress:Input(input, dress.layout:row()) 210 | end 211 | 212 | function love.draw() 213 | dress:draw() 214 | end 215 | 216 | .. warning:: 217 | 218 | Unlike UI and layout state, the theme might be shared with other states. 219 | Changes in a shared theme will be shared across all themes. 220 | See the :ref:`Instance Theme ` subsection in the 221 | :doc:`gettingstarted` guide. 222 | -------------------------------------------------------------------------------- /demo/suit/docs/layout.rst: -------------------------------------------------------------------------------- 1 | Layout 2 | ====== 3 | 4 | .. note:: 5 | Still under construction... 6 | 7 | Immediate Mode Layouts 8 | ---------------------- 9 | 10 | .. function:: reset([x,y, [pad_x, [pad_y]]]) 11 | 12 | :param numbers x,y: Origin of the layout (optional). 13 | :param pad_x,pad_y: Cell padding (optional). 14 | 15 | Reset the layout, puts the origin at ``(x,y)`` and sets the cell padding to 16 | ``pad_x`` and ``pad_y``. 17 | 18 | If ``x`` and ``y`` are omitted, they default to ``(0,0)``. If ``pad_x`` is 19 | omitted, it defaults to 0. If ``pad_y`` is omitted, it defaults to ``pad_x``. 20 | 21 | .. function:: padding([pad_x, [pad_y]]) 22 | 23 | :param pad_x: Cell padding in x direction (optional). 24 | :param pad_y: Cell padding in y direction (optional, defaults to ``pad_x``). 25 | :returns: The current (or new) cell padding. 26 | 27 | Get and set the current cell padding. 28 | 29 | If given, sets the cell padding to ``pad_x`` and ``pad_y``. 30 | If only ``pad_x`` is given, set both padding in ``x`` and ``y`` direction to ``pad_x``. 31 | 32 | .. function:: size() 33 | 34 | :returns: ``width,height`` - The size of the last cell. 35 | 36 | Get the size of the last cell. 37 | 38 | .. function:: nextRow() 39 | 40 | :returns: ``x,y`` - Upper left corner of the next row cell. 41 | 42 | Get the position of the upper left corner of the next cell in a row layout. 43 | Use for mixing precomputed and immediate mode layouts. 44 | 45 | .. function:: nextCol() 46 | 47 | :returns: ``x,y`` - Upper left corner of the next column cell. 48 | 49 | Get the position of the upper left corner of the next cell in a column layout. 50 | Use for mixing precomputed and immediate mode layouts. 51 | 52 | .. function:: push([x,y]) 53 | 54 | :param numbers x,y: Origin of the layout (optional). 55 | 56 | Saves the layout state (position, padding, sizes, etc.) on a stack, resets the 57 | layout with position ``(x,y)``. 58 | 59 | If ``x`` and ``y`` are omitted, they default to ``(0,0)``. 60 | 61 | Used for nested row/column layouts. 62 | 63 | .. function:: pop() 64 | 65 | Restores the layout parameters from the stack and advances the layout position 66 | according to the size of the popped layout. 67 | 68 | Used for nested row/column layouts. 69 | 70 | .. function:: row(w,h) 71 | 72 | :param mixed w,h: Cell width and height (optional). 73 | :returns: Position and size of the cell: ``x,y,w,h``. 74 | 75 | Creates a new cell below the current cell with width ``w`` and height ``h``. If 76 | either ``w`` or ``h`` is omitted, the value is set the last used value. Both 77 | ``w`` and ``h`` can be a string, which takes the following meaning: 78 | 79 | ``max`` 80 | Maximum of all values since the last reset. 81 | 82 | ``min`` 83 | Mimimum of all values since the last reset. 84 | 85 | ``median`` 86 | Median of all values since the last reset. 87 | 88 | Used to provide the last four arguments to a widget, e.g.:: 89 | 90 | suit.Button("Start Game", suit.layout.row(100,30)) 91 | suit.Button("Options", suit.layout.row()) 92 | suit.Button("Quit", suit.layout.row(nil, "median")) 93 | 94 | .. function:: col(w,h) 95 | 96 | :param mixed w,h: Cell width and height (optional). 97 | :returns: Position and size of the cell: ``x,y,w,h``. 98 | 99 | Creates a new cell right to the current cell with width ``w`` and height ``h``. 100 | If either ``w`` or ``h`` is omitted, the value is set the last used value. Both 101 | ``w`` and ``h`` can be a string, which takes the following meaning: 102 | 103 | ``max`` 104 | Maximum of all values since the last reset. 105 | 106 | ``min`` 107 | Mimimum of all values since the last reset. 108 | 109 | ``median`` 110 | Median of all values since the last reset. 111 | 112 | Used to provide the last four arguments to a widget, e.g.:: 113 | 114 | suit.Button("OK", suit.layout.col(100,30)) 115 | suit.Button("Cancel", suit.layout.col("max")) 116 | 117 | 118 | Precomputed Layouts 119 | ------------------- 120 | 121 | Apart from immediate mode layouts, you can specify layouts in advance. 122 | The specification is a table of tables, where each inner table follows the 123 | convention of :func:`row` and :func:`col`. 124 | The result is a layout definition object that can be used to access the cells. 125 | 126 | There are almost only two reasons to do so: (1) You know the area of your 127 | layout in advance (say, the screen size), and want certain cells to dynamically 128 | fill the available space; (2) You want to animate the cells. 129 | 130 | .. note:: 131 | Unlike immediate mode layouts, precomputed layouts **can not be nested**. 132 | You can mix immediate mode and precomputed layouts to achieve nested 133 | layouts with precomputed cells, however. 134 | 135 | Layout Specifications 136 | ^^^^^^^^^^^^^^^^^^^^^ 137 | 138 | Layout specifications are tables of tables, where the each inner table 139 | corresponds to a cell. The inner tables define the width and height of the cell 140 | according to the rules of :func:`row` and :func:`col`, with one additonal 141 | keyword: 142 | 143 | ``fill`` 144 | Fills the available space, determined by ``min_height`` or ``min_width`` and 145 | the number of cells with property ``fill``. 146 | 147 | For example, this row specification makes the height of the second cell to 148 | ``(300 - 50 - 50) / 1 = 200``:: 149 | 150 | {min_height = 300, 151 | {100, 50}, 152 | {nil, 'fill'}, 153 | {nil, 50}, 154 | } 155 | 156 | This column specification divides the space evenly among two cells:: 157 | 158 | {min_width = 300, 159 | {'fill', 100} 160 | {'fill'} 161 | } 162 | 163 | Apart from ``min_height`` and ``min_width``, layout specifications can also 164 | define the position (upper left corner) of the layout using the ``pos`` keyword:: 165 | 166 | {min_width = 300, pos = {100,100}, 167 | {'fill', 100} 168 | {'fill'} 169 | } 170 | 171 | You can also define a padding:: 172 | 173 | {min_width = 300, pos = {100,100}, padding = {5,5}, 174 | {'fill', 100} 175 | {'fill'} 176 | } 177 | 178 | Layout Definition Objects 179 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 180 | 181 | Once constructed, the cells can be accessed in two ways: 182 | 183 | - Using iterators:: 184 | 185 | for i, x,y,w,h in definition() do 186 | suit.Button("Button "..i, x,y,w,h) 187 | end 188 | 189 | - Using the ``cell(i)`` accessor:: 190 | 191 | suit.Button("Button 1", definition.cell(1)) 192 | suit.Button("Button 3", definition.cell(3)) 193 | suit.Button("Button 2", definition.cell(2)) 194 | 195 | There is actually a third way: Because layout definitions are just tables, you 196 | can access the cells directly:: 197 | 198 | local cell = definition[1] 199 | suit.Button("Button 1", cell[1], cell[2], cell[3], cell[4]) 200 | -- or suit.Button("Button 1", unpack(cell)) 201 | 202 | This is especially useful if you want to animate the cells, for example with a 203 | `tween `_:: 204 | 205 | for i,cell in ipairs(definition) 206 | local destination = {[2] = cell[2]} -- save cell y position 207 | cell[2] = -cell[4] -- move cell just outside of the screen 208 | 209 | -- let the cells fall into the screen one after another 210 | timer.after(i / 10, function() 211 | timer.tween(0.7, cell, destination, 'bounce') 212 | end) 213 | end 214 | 215 | 216 | Constructors 217 | ^^^^^^^^^^^^ 218 | 219 | .. function:: rows(spec) 220 | 221 | :param table spec: Layout specification. 222 | :returns: Layout definition object. 223 | 224 | Defines a row layout. 225 | 226 | .. function:: cols(spec) 227 | 228 | :param table spec: Layout specification. 229 | :returns: Layout definition object. 230 | 231 | Defines a column layout. 232 | -------------------------------------------------------------------------------- /demo/suit/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/hump.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hump.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/hump" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/hump" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /demo/suit/docs/index.rst: -------------------------------------------------------------------------------- 1 | SUIT 2 | ==== 3 | 4 | Simple User Interface Toolkit for `LÖVE `_. 5 | 6 | SUIT up 7 | ------- 8 | 9 | You can download SUIT and view the code on github: `vrld/SUIT 10 | `_. 11 | You may also download the sourcecode as a `zip 12 | `_ or `tar 13 | `_ file. 14 | 15 | Using `Git `_, you can clone the project by running:: 16 | 17 | git clone git://github.com/vrld/SUIT 18 | 19 | Once done, you can check for updates by running:: 20 | 21 | git pull 22 | 23 | 24 | Read on 25 | ------- 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | 30 | Getting Started 31 | Widgets 32 | Layout 33 | Core Functions 34 | Themeing 35 | License 36 | 37 | 38 | Example code 39 | ------------ 40 | 41 | The following code will create this UI: 42 | 43 | .. image:: _static/demo.gif 44 | 45 | :: 46 | 47 | local suit = require 'suit' 48 | 49 | -- generate some assets (below) 50 | function love.load() 51 | snd = generateClickySound() 52 | normal, hovered, active = generateImageButton() 53 | smallerFont = love.graphics.newFont(10) 54 | end 55 | 56 | -- data for a slider, an input box and a checkbox 57 | local slider= {value = 0.5, min = -2, max = 2} 58 | local input = {text = "Hello"} 59 | local chk = {text = "Check?"} 60 | 61 | -- all the UI is defined in love.update or functions that are called from here 62 | function love.update(dt) 63 | -- put the layout origin at position (100,100) 64 | -- cells will grown down and to the right from this point 65 | -- also set cell padding to 20 pixels to the right and to the bottom 66 | suit.layout:reset(100,100, 20,20) 67 | 68 | -- put a button at the layout origin 69 | -- the cell of the button has a size of 200 by 30 pixels 70 | state = suit.Button("Click?", suit.layout:row(200,30)) 71 | 72 | -- if the button was entered, play a sound 73 | if state.entered then love.audio.play(snd) end 74 | 75 | -- if the button was pressed, take damage 76 | if state.hit then print("Ouch!") end 77 | 78 | -- put an input box below the button 79 | -- the cell of the input box has the same size as the cell above 80 | -- if the input cell is submitted, print the text 81 | if suit.Input(input, suit.layout:row()).submitted then 82 | print(input.text) 83 | end 84 | 85 | -- put a button below the input box 86 | -- the width of the cell will be the same as above, the height will be 40 px 87 | if suit.Button("Hover?", suit.layout:row(nil,40)).hovered then 88 | -- if the button is hovered, show two other buttons 89 | -- this will shift all other ui elements down 90 | 91 | -- put a button below the previous button 92 | -- the cell height will be 30 px 93 | -- the label of the button will be aligned top left 94 | suit.Button("You can see", {align='left', valign='top'}, suit.layout:row(nil,30)) 95 | 96 | -- put a button below the previous button 97 | -- the cell size will be the same as the one above 98 | -- the label will be aligned bottom right 99 | suit.Button("...but you can't touch!", {align='right', valign='bottom'}, 100 | suit.layout:row()) 101 | end 102 | 103 | -- put a checkbox below the button 104 | -- the size will be the same as above 105 | -- (NOTE: height depends on whether "Hover?" is hovered) 106 | -- the label "Check?" will be aligned right 107 | suit.Checkbox(chk, {align='right'}, suit.layout:row()) 108 | 109 | -- put a nested layout 110 | -- the size of the cell will be as big as the cell above or as big as the 111 | -- nested content, whichever is bigger 112 | suit.layout:push(suit.layout:row()) 113 | 114 | -- change cell padding to 3 pixels in either direction 115 | suit.layout:padding(3) 116 | 117 | -- put a slider in the cell 118 | -- the inner cell will be 160 px wide and 20 px high 119 | suit.Slider(slider, suit.layout:col(160, 20)) 120 | 121 | -- put a label that shows the slider value to the right of the slider 122 | -- the width of the label will be 40 px 123 | suit.Label(("%.02f"):format(slider.value), suit.layout:col(40)) 124 | 125 | -- close the nested layout 126 | suit.layout:pop() 127 | 128 | -- put an image button below the nested cell 129 | -- the size of the cell will be 200 by 100 px, 130 | -- but the image may be bigger or smaller 131 | -- the button shows the image `normal' when the mouse is outside the image 132 | -- or above a transparent pixel 133 | -- the button shows the image `hovered` if the mouse is above an opaque pixel 134 | -- of the image `normal' 135 | -- the button shows the image `active` if the mouse is above an opaque pixel 136 | -- of the image `normal' and the mouse button is pressed 137 | suit.ImageButton(normal, {hovered = hovered, active = active}, suit.layout:row(200,100)) 138 | 139 | -- if the checkbox is checked, display a precomputed layout 140 | if chk.checked then 141 | -- the precomputed layout will be 3 rows below each other 142 | -- the origin of the layout will be at (400,100) 143 | -- the minimal height of the layout will be 300 px 144 | rows = suit.layout:rows{pos = {400,100}, min_height = 300, 145 | {200, 30}, -- the first cell will measure 200 by 30 px 146 | {30, 'fill'}, -- the second cell will be 30 px wide and fill the 147 | -- remaining vertical space between the other cells 148 | {200, 30}, -- the third cell will be 200 by 30 px 149 | } 150 | 151 | -- the first cell will contain a witty label 152 | -- the label will be aligned left 153 | -- the font of the label will be smaller than the usual font 154 | suit.Label("You uncovered the secret!", {align="left", font = smallerFont}, 155 | rows.cell(1)) 156 | 157 | -- the third cell will contain a label that shows the value of the slider 158 | suit.Label(slider.value, {align='left'}, rows.cell(3)) 159 | 160 | -- the second cell will show a slider 161 | -- the slider will operate on the same data as the first slider 162 | -- the slider will be vertical instead of horizontal 163 | -- the id of the slider will be 'slider two'. this is necessary, because 164 | -- the two sliders should not both react to UI events 165 | suit.Slider(slider, {vertical = true, id = 'slider two'}, rows.cell(2)) 166 | end 167 | end 168 | 169 | function love.draw() 170 | -- draw the gui 171 | suit.draw() 172 | end 173 | 174 | function love.textinput(t) 175 | -- forward text input to SUIT 176 | suit.textinput(t) 177 | end 178 | 179 | function love.keypressed(key) 180 | -- forward keypressed to SUIT 181 | suit.keypressed(key) 182 | end 183 | 184 | -- generate assets (see love.load) 185 | function generateClickySound() 186 | local snd = love.sound.newSoundData(512, 44100, 16, 1) 187 | for i = 0,snd:getSampleCount()-1 do 188 | local t = i / 44100 189 | local s = i / snd:getSampleCount() 190 | snd:setSample(i, (.7*(2*love.math.random()-1) + .3*math.sin(t*9000*math.pi)) * (1-s)^1.2 * .3) 191 | end 192 | return love.audio.newSource(snd) 193 | end 194 | 195 | function generateImageButton() 196 | local metaballs = function(t, r,g,b) 197 | return function(x,y) 198 | local px, py = 2*(x/200-.5), 2*(y/100-.5) 199 | local d1 = math.exp(-((px-.6)^2 + (py-.1)^2)) 200 | local d2 = math.exp(-((px+.7)^2 + (py+.1)^2) * 2) 201 | local d = (d1 + d2)/2 202 | if d > t then 203 | return r,g,b, 255 * ((d-t) / (1-t))^.2 204 | end 205 | return 0,0,0,0 206 | end 207 | end 208 | 209 | local normal, hovered, active = love.image.newImageData(200,100), love.image.newImageData(200,100), love.image.newImageData(200,100) 210 | normal:mapPixel(metaballs(.48, 188,188,188)) 211 | hovered:mapPixel(metaballs(.46, 50,153,187)) 212 | active:mapPixel(metaballs(.43, 255,153,0)) 213 | return love.graphics.newImage(normal), love.graphics.newImage(hovered), love.graphics.newImage(active) 214 | end 215 | 216 | Indices and tables 217 | ------------------ 218 | 219 | * :ref:`genindex` 220 | * :ref:`modindex` 221 | * :ref:`search` 222 | -------------------------------------------------------------------------------- /demo/suit/layout.lua: -------------------------------------------------------------------------------- 1 | -- This file is part of SUIT, copyright (c) 2016 Matthias Richter 2 | 3 | local Layout = {} 4 | function Layout.new(x,y,padx,pady) 5 | return setmetatable({_stack = {}}, {__index = Layout}):reset(x,y,padx,pady) 6 | end 7 | 8 | function Layout:reset(x,y, padx,pady) 9 | self._x = x or 0 10 | self._y = y or 0 11 | self._padx = padx or 0 12 | self._pady = pady or self._padx 13 | self._w = nil 14 | self._h = nil 15 | self._widths = {} 16 | self._heights = {} 17 | 18 | return self 19 | end 20 | 21 | function Layout:padding(padx,pady) 22 | if padx then 23 | self._padx = padx 24 | self._pady = pady or padx 25 | end 26 | return self._padx, self._pady 27 | end 28 | 29 | function Layout:size() 30 | return self._w, self._h 31 | end 32 | 33 | function Layout:nextRow() 34 | return self._x, self._y + self._h + self._pady 35 | end 36 | 37 | function Layout:nextCol() 38 | return self._x + self._w + self._padx, self._y 39 | end 40 | 41 | function Layout:push(x,y) 42 | self._stack[#self._stack+1] = { 43 | self._x, self._y, 44 | self._padx, self._pady, 45 | self._w, self._h, 46 | self._widths, 47 | self._heights, 48 | } 49 | 50 | return self:reset(x,y, padx or self._padx, pady or self._pady) 51 | end 52 | 53 | function Layout:pop() 54 | assert(#self._stack > 0, "Nothing to pop") 55 | local w,h = self._w, self._h 56 | self._x, self._y, 57 | self._padx,self._pady, 58 | self._w, self._h, 59 | self._widths, self._heights = unpack(self._stack[#self._stack]) 60 | 61 | self._w, self._h = math.max(w, self._w or 0), math.max(h, self._h or 0) 62 | 63 | return w, h 64 | end 65 | 66 | --- recursive binary search for position of v 67 | local function insert_sorted_helper(t, i0, i1, v) 68 | if i1 <= i0 then 69 | table.insert(t, i0, v) 70 | return 71 | end 72 | 73 | local i = i0 + math.floor((i1-i0)/2) 74 | if t[i] < v then 75 | return insert_sorted_helper(t, i+1, i1, v) 76 | elseif t[i] > v then 77 | return insert_sorted_helper(t, i0, i-1, v) 78 | else 79 | table.insert(t, i, v) 80 | end 81 | end 82 | 83 | local function insert_sorted(t, v) 84 | if v <= 0 then return end 85 | insert_sorted_helper(t, 1, #t, v) 86 | end 87 | 88 | local function calc_width_height(self, w, h) 89 | if w == "" or w == nil then 90 | w = self._w 91 | elseif w == "max" then 92 | w = self._widths[#self._widths] 93 | elseif w == "min" then 94 | w = self._widths[1] 95 | elseif w == "median" then 96 | w = self._widths[math.ceil(#self._widths/2)] or 0 97 | elseif type(w) ~= "number" then 98 | error("width: invalid value (" .. tostring(w) .. ")", 3) 99 | end 100 | 101 | if h == "" or h == nil then 102 | h = self._h 103 | elseif h == "max" then 104 | h = self._heights[#self._heights] 105 | elseif h == "min" then 106 | h = self._heights[1] 107 | elseif h == "median" then 108 | h = self._heights[math.ceil(#self._heights/2)] or 0 109 | elseif type(h) ~= "number" then 110 | error("width: invalid value (" .. tostring(w) .. ")", 3) 111 | end 112 | 113 | if not w or not h then 114 | error("Invalid cell size", 3) 115 | end 116 | 117 | insert_sorted(self._widths, w) 118 | insert_sorted(self._heights, h) 119 | return w,h 120 | end 121 | 122 | function Layout:row(w, h) 123 | self._y = self._y + self._pady 124 | w,h = calc_width_height(self, w, h) 125 | 126 | local x,y = self._x, self._y + (self._h or 0) 127 | self._y, self._w, self._h = y, w, h 128 | 129 | return x,y,w,h 130 | end 131 | 132 | function Layout:col(w, h) 133 | self._x = self._x + self._padx 134 | w,h = calc_width_height(self, w, h) 135 | 136 | local x,y = self._x + (self._w or 0), self._y 137 | self._x, self._w, self._h = x, w, h 138 | 139 | return x,y,w,h 140 | end 141 | 142 | 143 | local function layout_iterator(t, idx) 144 | idx = (idx or 1) + 1 145 | if t[idx] == nil then return nil end 146 | return idx, unpack(t[idx]) 147 | end 148 | 149 | local function layout_retained_mode(self, t, constructor, string_argument_to_table, fill_width, fill_height) 150 | -- sanity check 151 | local p = t.pos or {0,0} 152 | if type(p) ~= "table" then 153 | error("Invalid argument `pos' (table expected, got "..type(p)..")", 2) 154 | end 155 | local pad = t.padding or {} 156 | if type(p) ~= "table" then 157 | error("Invalid argument `padding' (table expected, got "..type(p)..")", 2) 158 | end 159 | 160 | self:push(p[1] or 0, p[2] or 0) 161 | self:padding(pad[1] or self._padx, pad[2] or self._pady) 162 | 163 | -- first pass: get dimensions, add layout info 164 | local layout = {n_fill_w = 0, n_fill_h = 0} 165 | for i,v in ipairs(t) do 166 | if type(v) == "string" then 167 | v = string_argument_to_table(v) 168 | end 169 | local x,y,w,h = 0,0, v[1], v[2] 170 | if v[1] == "fill" then w = 0 end 171 | if v[2] == "fill" then h = 0 end 172 | 173 | x,y, w,h = constructor(self, w,h) 174 | 175 | if v[1] == "fill" then 176 | w = "fill" 177 | layout.n_fill_w = layout.n_fill_w + 1 178 | end 179 | if v[2] == "fill" then 180 | h = "fill" 181 | layout.n_fill_h = layout.n_fill_h + 1 182 | end 183 | layout[i] = {x,y,w,h, unpack(v,3)} 184 | end 185 | 186 | -- second pass: extend "fill" cells and shift others accordingly 187 | local fill_w = fill_width(layout, t.min_width or 0, self._x + self._w - p[1]) 188 | local fill_h = fill_height(layout, t.min_height or 0, self._y + self._h - p[2]) 189 | local dx,dy = 0,0 190 | for _,v in ipairs(layout) do 191 | v[1], v[2] = v[1] + dx, v[2] + dy 192 | if v[3] == "fill" then 193 | v[3] = fill_w 194 | dx = dx + v[3] 195 | end 196 | if v[4] == "fill" then 197 | v[4] = fill_h 198 | dy = dy + v[4] 199 | end 200 | end 201 | 202 | -- finally: return layout with iterator 203 | local w, h = self:pop() 204 | layout.cell = function(self, i) 205 | if self ~= layout then -- allow either colon or dot syntax 206 | i = self 207 | end 208 | return unpack(layout[i]) 209 | end 210 | layout.size = function() 211 | return w, h 212 | end 213 | return setmetatable(layout, {__call = function() 214 | return layout_iterator, layout, 0 215 | end}) 216 | end 217 | 218 | function Layout:rows(t) 219 | return layout_retained_mode(self, t, self.row, 220 | function(v) return {nil, v} end, 221 | function() return self._widths[#self._widths] end, -- fill width 222 | function(l,mh,h) return (mh - h) / l.n_fill_h end) -- fill height 223 | end 224 | 225 | function Layout:cols(t) 226 | return layout_retained_mode(self, t, self.col, 227 | function(v) return {v} end, 228 | function(l,mw,w) return (mw - w) / l.n_fill_w end, -- fill width 229 | function() return self._heights[#self._heights] end) -- fill height 230 | end 231 | 232 | --[[ "Tests" 233 | do 234 | 235 | L = Layout.new() 236 | 237 | print("immediate mode") 238 | print("--------------") 239 | x,y,w,h = L:row(100,20) -- x,y,w,h = 0,0, 100,20 240 | print(1,x,y,w,h) 241 | x,y,w,h = L:row() -- x,y,w,h = 0, 20, 100,20 (default: reuse last dimensions) 242 | print(2,x,y,w,h) 243 | x,y,w,h = L:col(20) -- x,y,w,h = 100, 20, 20, 20 244 | print(3,x,y,w,h) 245 | x,y,w,h = L:row(nil,30) -- x,y,w,h = 100, 20, 20, 30 246 | print(4,x,y,w,h) 247 | print('','','', L:size()) -- w,h = 20, 30 248 | print() 249 | 250 | L:reset() 251 | 252 | local layout = L:rows{ 253 | pos = {10,10}, -- optional, default {0,0} 254 | 255 | {100, 10}, 256 | {nil, 10}, -- {100, 10} 257 | {100, 20}, -- {100, 20} 258 | {}, -- {100, 20} -- default = last value 259 | {nil, "median"}, -- {100, 20} 260 | "median", -- {100, 20} 261 | "max", -- {100, 20} 262 | "min", -- {100, 10} 263 | "" -- {100, 10} -- default = last value 264 | } 265 | 266 | print("rows") 267 | print("----") 268 | for i,x,y,w,h in layout() do 269 | print(i,x,y,w,h) 270 | end 271 | print() 272 | 273 | -- +-------+-------+----------------+-------+ 274 | -- | | | | | 275 | -- 70 {100, | "max" | "fill" | "min" | 276 | -- | 70} | | | | 277 | -- +--100--+--100--+------220-------+--100--+ 278 | -- 279 | -- `-------------------,--------------------' 280 | -- 520 281 | local layout = L:cols{ 282 | pos = {10,10}, 283 | min_width = 520, 284 | 285 | {100, 70}, 286 | "max", -- {100, 70} 287 | "fill", -- {min_width - width_of_items, 70} = {220, 70} 288 | "min", -- {100,70} 289 | } 290 | 291 | print("cols") 292 | print("----") 293 | for i,x,y,w,h in layout() do 294 | print(i,x,y,w,h) 295 | end 296 | print() 297 | 298 | L:push() 299 | L:row() 300 | 301 | end 302 | --]] 303 | 304 | -- TODO: nesting a la rows{..., cols{...} } ? 305 | 306 | local instance = Layout.new() 307 | return setmetatable({ 308 | new = Layout.new, 309 | reset = function(...) return instance:reset(...) end, 310 | padding = function(...) return instance:padding(...) end, 311 | push = function(...) return instance:push(...) end, 312 | pop = function(...) return instance:pop(...) end, 313 | row = function(...) return instance:row(...) end, 314 | col = function(...) return instance:col(...) end, 315 | rows = function(...) return instance:rows(...) end, 316 | cols = function(...) return instance:cols(...) end, 317 | }, {__call = function(_,...) return Layout.new(...) end}) 318 | -------------------------------------------------------------------------------- /demo/suit/docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # SUIT documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Oct 10 13:10:12 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.mathjax', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'SUIT' 52 | copyright = u'2016, Matthias Richter' 53 | author = u'Matthias Richter' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '1.0' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '1.0' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'alabaster' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | #html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | #html_sidebars = {} 159 | 160 | # Additional templates that should be rendered to pages, maps page names to 161 | # template names. 162 | #html_additional_pages = {} 163 | 164 | # If false, no module index is generated. 165 | #html_domain_indices = True 166 | 167 | # If false, no index is generated. 168 | #html_use_index = True 169 | 170 | # If true, the index is split into individual pages for each letter. 171 | #html_split_index = False 172 | 173 | # If true, links to the reST sources are added to the pages. 174 | #html_show_sourcelink = True 175 | 176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 177 | #html_show_sphinx = True 178 | 179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages will 183 | # contain a tag referring to it. The value of this option must be the 184 | # base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Language to be used for generating the HTML full-text search index. 191 | # Sphinx supports the following languages: 192 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 193 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 194 | #html_search_language = 'en' 195 | 196 | # A dictionary with options for the search language support, empty by default. 197 | # Now only 'ja' uses this config value 198 | #html_search_options = {'type': 'default'} 199 | 200 | # The name of a javascript file (relative to the configuration directory) that 201 | # implements a search results scorer. If empty, the default will be used. 202 | #html_search_scorer = 'scorer.js' 203 | 204 | # Output file base name for HTML help builder. 205 | htmlhelp_basename = 'suitdoc' 206 | 207 | # -- Options for LaTeX output --------------------------------------------- 208 | 209 | latex_elements = { 210 | # The paper size ('letterpaper' or 'a4paper'). 211 | #'papersize': 'letterpaper', 212 | 213 | # The font size ('10pt', '11pt' or '12pt'). 214 | #'pointsize': '10pt', 215 | 216 | # Additional stuff for the LaTeX preamble. 217 | #'preamble': '', 218 | 219 | # Latex figure (float) alignment 220 | #'figure_align': 'htbp', 221 | } 222 | 223 | # Grouping the document tree into LaTeX files. List of tuples 224 | # (source start file, target name, title, 225 | # author, documentclass [howto, manual, or own class]). 226 | latex_documents = [ 227 | (master_doc, 'suit.tex', u'SUIT Documentation', 228 | u'Matthias Richter', 'manual'), 229 | ] 230 | 231 | # The name of an image file (relative to this directory) to place at the top of 232 | # the title page. 233 | #latex_logo = None 234 | 235 | # For "manual" documents, if this is true, then toplevel headings are parts, 236 | # not chapters. 237 | #latex_use_parts = False 238 | 239 | # If true, show page references after internal links. 240 | #latex_show_pagerefs = False 241 | 242 | # If true, show URL addresses after external links. 243 | #latex_show_urls = False 244 | 245 | # Documents to append as an appendix to all manuals. 246 | #latex_appendices = [] 247 | 248 | # If false, no module index is generated. 249 | #latex_domain_indices = True 250 | 251 | 252 | # -- Options for manual page output --------------------------------------- 253 | 254 | # One entry per manual page. List of tuples 255 | # (source start file, name, description, authors, manual section). 256 | man_pages = [ 257 | (master_doc, 'SUIT', u'SUIT Documentation', 258 | [author], 1) 259 | ] 260 | 261 | # If true, show URL addresses after external links. 262 | #man_show_urls = False 263 | 264 | 265 | # -- Options for Texinfo output ------------------------------------------- 266 | 267 | # Grouping the document tree into Texinfo files. List of tuples 268 | # (source start file, target name, title, author, 269 | # dir menu entry, description, category) 270 | texinfo_documents = [ 271 | (master_doc, 'SUIT', u'SUIT Documentation', 272 | author, 'SUIT', 'One line description of project.', 273 | 'Miscellaneous'), 274 | ] 275 | 276 | # Documents to append as an appendix to all manuals. 277 | #texinfo_appendices = [] 278 | 279 | # If false, no module index is generated. 280 | #texinfo_domain_indices = True 281 | 282 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 283 | #texinfo_show_urls = 'footnote' 284 | 285 | # If true, do not generate a @detailmenu in the "Top" node's menu. 286 | #texinfo_no_detailmenu = False 287 | 288 | primary_domain = "js" 289 | highlight_language = "lua" 290 | 291 | import sphinx_rtd_theme 292 | html_theme = 'sphinx_rtd_theme' 293 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 294 | -------------------------------------------------------------------------------- /demo/suit/docs/gettingstarted.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Before actually getting started, it is important to understand the motivation 5 | and mechanics behind SUIT: 6 | 7 | - **Immediate mode is better than retained mode** 8 | - **Layout does not care about widgets** 9 | - **Less is more** 10 | 11 | Immediate mode? 12 | --------------- 13 | 14 | With classical (retained) mode libraries you typically have a stage where you 15 | create the whole UI when the program initializes. This includes what happens 16 | when events like button presses or slider changes occur. After that point, the 17 | GUI is expected to not change very much. This is great for word processors 18 | where the interaction is consistent and straightforward, but bad for games, 19 | where everything changes all the time. 20 | 21 | With immediate mode libraries, on the other hand, the GUI is created every 22 | frame from scratch. Because that would be wasteful, there are no widget 23 | objects. Instead, widgets are created by functions that react to UI state and 24 | present some data. Where this data comes from and how it is maintained does 25 | not concern the widget at all. This is, after all, your job. This gives great 26 | control over what is shown where and when. The widget code can be right next 27 | to the code that does what should happen if the widget state changes. The 28 | layout is also very flexible: adding a widget is one more function call, and if 29 | you want to hide a widget, you simply don't call the corresponding function. 30 | 31 | This separation of data and behaviour is great when a lot of stuff is going on, 32 | but takes a bit of time getting used to. 33 | 34 | 35 | What SUIT is 36 | ^^^^^^^^^^^^ 37 | 38 | SUIT is simple: It provides only a few basic widgets that are important for 39 | games: 40 | 41 | - :func:`Buttons