├── README.md ├── LICENSE └── layouter.lua /README.md: -------------------------------------------------------------------------------- 1 | # Layouter 2 | 3 | Layouter is a simple **UI grid layout library** for LÖVE 2D game engine. 4 | 5 | It currently supports these element types: 6 | 7 | - text (including spacer = blank text) 8 | - text button 9 | - image 10 | 11 | ## Screenshots 12 | ![image](https://github.com/nekromoff/layouter/assets/8550349/4f31272d-c69b-4bba-a109-d14f823fb5c8) 13 | 14 | ## Usage 15 | 16 | ### 1. Include layouter 17 | ``` 18 | layouter = require 'layouter' 19 | ``` 20 | 21 | ### 2. Initialize layouter 22 | Defaults=15px, white background, black color, debug disabled: 23 | ``` 24 | layouter.initialize() 25 | ``` 26 | or with a custom look (font=20px, background=black, color=white, debug mode enabled=draw grid): 27 | ``` 28 | layouter.initialize({font=love.graphics.newFont(20), background={0,0,0}, color={255,255,255}, debug=true}) 29 | ``` 30 | 31 | ### 3. Add elements to your layout 32 | #### Text element 33 | ``` 34 | layouter.add('Hello world!') 35 | ``` 36 | #### Text element with options 37 | ``` 38 | layouter.add({content = 'Second text paragraph', font = love.graphics.newFont(50), color = {13, 46, 63}}) 39 | ``` 40 | #### Spacer (blank paragraph) 41 | ``` 42 | layouter.add() 43 | ``` 44 | #### Image with required custom key (key can be used to replace or remove it later) 45 | ``` 46 | layouter.add({content = love.graphics.newImage('logo.png'), type = 'image', key = 'logo'}) 47 | ``` 48 | #### Button 49 | ``` 50 | layouter.add({content = 'Start game', type = 'button', callback = function() startGame() end}) 51 | ``` 52 | #### Button that replaces itself on a click with a text 53 | Note the automatically assigned key `eastereggs` that is created from the text. 54 | ``` 55 | layouter.add({content = '* easter! Eggs $@', type = 'button', callback = function() layouter.replace('eastereggs', 'Currently does nothing.') end}) 56 | ``` 57 | 58 | ### 4. Prepare your layout 59 | Set where to draw your elements, how they should be aligned horizontaly or vertically and if auto spacing (based on number of elements) should be done. 60 | x=position X, y=position Y, direction=`horizontal` or `vertical`, spacing=`auto` for automatical centering, optionally also padding=(in pixels) 61 | ``` 62 | layouter.prepare({x = layouter.COLUMN6, y = layouter.ROW4, direction = 'vertical', spacing = 'auto'}) 63 | ``` 64 | 65 | ### 5.Draw your layout 66 | ``` 67 | function love.draw() 68 | layouter.draw() 69 | end 70 | ``` 71 | 72 | ### 6. Process mouse clicks for buttons 73 | This functions needs to be called to enable interaction for buttons. 74 | ``` 75 | function love.mousepressed(x, y, mouse_button, is_touch) 76 | layouter.processMouse(x, y, mouse_button, is_touch) 77 | end 78 | ``` 79 | 80 | ### Grid positioning - columns and rows 81 | The grid comes with 24 columns and 16 rows. 82 | Layouter automatically calculates sizes of columns and rows and generates helper variables. 83 | You can use these helper variables to position your elements and layout easily: 84 | ``` 85 | -- columns: 86 | layouter.COLUMN1 -- first column's right side, i.e. will position element right after first column 87 | -- ... 88 | layouter.COLUMN8 -- X position after width of eight columns 89 | 90 | --- rows: 91 | layouter.ROW1 -- Y position after height of a first row 92 | -- ... 93 | layouter.ROW4 -- Y position after height of four rows 94 | ``` 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /layouter.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Layouter 3 | UI Grid Layout Library for LÖVE 2D Game Engine 4 | © Daniel Duris, dusoft@staznosti.sk, 2023+ 5 | License: GNU LGPL 3.0 6 | ]]-- 7 | 8 | local layouter = { 9 | _NAME = "Layouter", 10 | _DESCRIPTION = 'UI Grid Layout Library for LÖVE 2D Game Engine', 11 | _URL = 'https://github.com/nekromoff/layouter', 12 | _VERSION = 1, 13 | _LICENSE = "LGPL 3.0", 14 | _LOVE = 11.4, 15 | --------------------------- 16 | ROWS = 16, 17 | COLUMNS = 24, 18 | elements = {}, 19 | _layout = {}, 20 | _helpers = {}, 21 | } 22 | 23 | -- @param table options 24 | layouter.initialize = function(options) 25 | local options = options or {} 26 | layouter.font = options.font or love.graphics.newFont(15) 27 | layouter.background = options.background or {255, 255, 255} 28 | layouter.color = options.color or {0, 0, 0} 29 | layouter.debug = options.debug or false 30 | local width, height = love.window.getMode() 31 | layouter.COLUMN_WIDTH = layouter._helpers.math_round(width / layouter.COLUMNS) 32 | layouter.ROW_HEIGHT = layouter._helpers.math_round(height / layouter.ROWS) 33 | -- precalculate pixels for each column/row to allow for using shortcuts, e.g. layouter.COLUMN5 or layouter.ROW2 34 | -- (yeah, similar to Bootstrap CSS grid) 35 | for column = 1, layouter.COLUMNS do 36 | layouter['COLUMN'..column] = layouter.COLUMN_WIDTH * column 37 | end 38 | for row = 1, layouter.ROWS do 39 | layouter['ROW'..row] = layouter.ROW_HEIGHT * row 40 | end 41 | layouter.reset() 42 | end 43 | 44 | layouter.reset = function() 45 | layouter.elements = {} 46 | layouter._layout = {} 47 | layouter._previous = nil 48 | end 49 | 50 | -- add element to layout 51 | -- @param nil/string/table element options 52 | layouter.add = function (element) 53 | local element = layouter._createElement(element) 54 | layouter.elements[#layouter.elements + 1] = element 55 | end 56 | 57 | -- replace existing element in layout by a new one 58 | -- @param string existing element_key 59 | -- @param nil/string/table element options 60 | layouter.replace = function(element_key, element) 61 | local element = layouter._createElement(element) 62 | for temp_key, temp_element in pairs(layouter.elements) do 63 | if temp_element.key == element_key then 64 | layouter.elements[temp_key] = element 65 | end 66 | end 67 | -- call prepare automatically to update screen on next draw (prepare remembers previous state) 68 | layouter.prepare() 69 | end 70 | 71 | -- remove existing element from layout 72 | -- @param string existing element_key 73 | layouter.remove = function(element_key) 74 | for key, element in pairs(layouter.elements) do 75 | if element.key == element_key then 76 | table.remove(layouter.elements, key) 77 | end 78 | end 79 | end 80 | 81 | -- setup an element to be added to layout 82 | -- @param string existing element_key 83 | layouter._createElement = function(element) 84 | -- change empty content for adding text to proper table 85 | if element == nil then 86 | element = {} 87 | element.content = ''; 88 | elseif type(element) == 'string' then -- change simplified format for adding text to proper table 89 | local temp_content = element 90 | element = {} 91 | element.content = temp_content 92 | end 93 | element.content = element.content or '' 94 | element.type = element.type or 'text' 95 | element.callback = element.callback or false 96 | element.font = element.font or layouter.font 97 | element.color = element.color or layouter.color 98 | element.background = element.background or layouter.background 99 | if element.key == nil then 100 | -- generate unique identifier / key using only a-z0-9_ 101 | element.key = string.gsub(element.content:lower(), '%W', '') 102 | end 103 | element.x = element.x or false 104 | element.y = element.y or false 105 | -- if exact location is provided, precalculate dimensions 106 | if element.x and element.y and element.width == nil and element.height == nil then 107 | if element.type == 'image' then 108 | element.width, element.height = element.content:getDimensions() 109 | else 110 | temp_text = love.graphics.newText(element.font, element.content) 111 | element.width, element.height = temp_text:getDimensions() 112 | end 113 | end 114 | return element 115 | end 116 | 117 | -- prepares a layout, does all computations for elements, assigns positions, does autosizing etc. 118 | -- @param table layout options 119 | layouter.prepare = function(layout) 120 | layouter._layout = {} 121 | local layout = layout or {} 122 | -- if previous state exists and was not reset, use it 123 | if layouter._previous then 124 | layout.x = layouter._previous.x 125 | layout.y = layouter._previous.y 126 | layout.direction = layouter._previous.direction 127 | layout.spacing = layouter._previous.spacing 128 | layout.padding = layouter._previous.padding 129 | end 130 | layout.x = layout.x or false 131 | layout.y = layout.y or false 132 | layout.width, layout.height = love.window.getMode() 133 | layout.direction = layout.direction or 'vertical' 134 | layout.spacing = layout.spacing or {width = layout.width, height = layout.height} 135 | layout.padding = layout.padding or 10 136 | if layout.spacing == 'auto' then 137 | layout.spacing = {width = layout.width - layout.x * 2, height = layout.height - layout.y * 2} 138 | end 139 | -- remember current state 140 | layouter._previous = {x = layout.x, y = layout.y, direction = layout.direction, spacing = layout.spacing, padding = layout.padding} 141 | if layout.direction == 'horizontal' then 142 | fit_width = layout.spacing.width / layouter._helpers.table_length(layouter.elements) - layout.padding * 2 143 | fit_height = layouter.font:getHeight() + layout.padding * 2 144 | else -- vertical 145 | fit_width = layout.spacing.width - layout.padding * 2 146 | fit_height = layout.spacing.height / layouter._helpers.table_length(layouter.elements) - layout.padding * 2 147 | end 148 | local last_x = layout.x 149 | local last_y = layout.y 150 | for key, element in ipairs(layouter.elements) do 151 | local prepared_element = layouter._helpers.table_copy(element) 152 | -- do automatic layout, if x and y not set directly 153 | if prepared_element.x == false and prepared_element.y == false then 154 | if layout.direction == 'horizontal' then 155 | prepared_element.width = fit_width 156 | prepared_element.height = fit_height 157 | prepared_element.x = layout.padding + last_x 158 | prepared_element.y = layout.y 159 | else 160 | prepared_element.width = fit_width 161 | prepared_element.height = fit_height 162 | prepared_element.x = layout.x + layout.padding 163 | prepared_element.y = layout.padding + last_y 164 | end 165 | last_x = prepared_element.x + prepared_element.width + layout.padding 166 | last_y = prepared_element.y + prepared_element.height + layout.padding 167 | end 168 | layouter._layout[#layouter._layout + 1] = prepared_element 169 | end 170 | end 171 | 172 | -- draw a layout, to be used in love.draw() function 173 | layouter.draw = function() 174 | local x, y = love.mouse.getPosition() 175 | love.graphics.clear(layouter.background) 176 | love.graphics.setColor(love.math.colorFromBytes(layouter.color)) 177 | if layouter.debug then 178 | love.graphics.setColor(love.math.colorFromBytes(176, 176, 176)) 179 | love.graphics.setFont(layouter.font) 180 | for column = 0, layouter.COLUMNS do 181 | for row = 0, layouter.ROWS do 182 | love.graphics.line(column * layouter.COLUMN_WIDTH, 0, column * layouter.COLUMN_WIDTH, 16 * layouter.ROW_HEIGHT) 183 | love.graphics.line(0, row * layouter.ROW_HEIGHT, 24 * layouter.COLUMN_WIDTH, row * layouter.ROW_HEIGHT) 184 | love.graphics.print(column..','..row, column * layouter.COLUMN_WIDTH, row * layouter.ROW_HEIGHT) 185 | end 186 | end 187 | end 188 | love.graphics.setColor(love.math.colorFromBytes(layouter.color)) 189 | for key, element in pairs(layouter._layout) do 190 | local content_y = layouter._helpers.math_round(element.y + element.height / 5 * 2) 191 | if element.type == 'button' then 192 | if x >= element.x and x <= element.x + element.width and y >= element.y and y <= element.y + element.height then 193 | love.graphics.setColor(love.math.colorFromBytes(element.color)) 194 | love.graphics.rectangle('fill', element.x, element.y, element.width, element.height) 195 | love.graphics.setColor(love.math.colorFromBytes(element.background)) 196 | love.graphics.printf(element.content, element.font, element.x, content_y, element.width, 'center') 197 | else 198 | love.graphics.setColor(love.math.colorFromBytes(element.color)) 199 | love.graphics.rectangle('line', element.x, element.y, element.width, element.height) 200 | love.graphics.printf(element.content, element.font, element.x, content_y, element.width, 'center') 201 | end 202 | elseif element.type == 'text' then 203 | love.graphics.setColor(love.math.colorFromBytes(element.color)) 204 | love.graphics.printf(element.content, element.font, element.x, content_y, element.width, 'center') 205 | else -- image 206 | love.graphics.setColor(love.math.colorFromBytes(element.background)) 207 | love.graphics.draw(element.content, element.x, element.y) 208 | end 209 | end 210 | end 211 | 212 | -- process mouse callbacks, to be used in love.mousepressed() function 213 | -- currently supports only default (usually left) button 214 | layouter.processMouse = function(x, y, mouse_button, is_touch) 215 | if mouse_button ~= 1 then 216 | return 217 | end 218 | for element_key, element in pairs(layouter._layout) do 219 | -- for position debug 220 | -- print (element.x..'> '..x..' <'..element.x + element.width, element.y..'> '..y..' <'..element.y + element.height) 221 | if element.callback ~= false and x >= element.x and x <= element.x + element.width and y >= element.y and y <= element.y + element.height then 222 | element.callback() 223 | end 224 | end 225 | end 226 | 227 | layouter._helpers.math_round = function(number, decimal_places) 228 | local multiplicator = 10 ^ (decimal_places or 0) 229 | return math.floor(number * multiplicator + 0.5) / multiplicator 230 | end 231 | 232 | layouter._helpers.table_length = function(table) 233 | local count = 0 234 | for _ in pairs(table) do 235 | count = count + 1 236 | end 237 | return count 238 | end 239 | 240 | layouter._helpers.table_copy = function (orig) 241 | local orig_type = type(orig) 242 | local copy 243 | if orig_type == 'table' then 244 | copy = {} 245 | for orig_key, orig_value in next, orig, nil do 246 | copy[layouter._helpers.table_copy(orig_key)] = layouter._helpers.table_copy(orig_value) 247 | end 248 | setmetatable(copy, layouter._helpers.table_copy(getmetatable(orig))) 249 | else -- number, string, boolean, etc 250 | copy = orig 251 | end 252 | return copy 253 | end 254 | 255 | return layouter 256 | --------------------------------------------------------------------------------