Rework intro and tutorial.

This commit is contained in:
Matthias Richter 2016-01-02 02:54:55 +01:00
parent 84eb1ef804
commit 59f3ca741f
2 changed files with 193 additions and 115 deletions

View file

@ -4,31 +4,39 @@ Getting Started
Before actually getting started, it is important to understand the motivation Before actually getting started, it is important to understand the motivation
and mechanics behind SUIT: and mechanics behind SUIT:
- **SUIT is an immediate mode GUI library** - **Immediate mode is better than retained mode**
- **Layout does not care about widgets**
- **Less is more** - **Less is more**
- **Layouting must be easy**
Immediate mode? Immediate mode?
--------------- ---------------
With classical (retained) mode libraries you typically have a stage where you With classical (retained) mode libraries you typically have a stage where you
create the whole UI when the program initializes. After that point, the GUI create the whole UI when the program initializes. This includes what happens
is expected to not change very much. when events like button presses or slider changes occur. After that point, the
GUI is expected to not change very much. This is great for word processors
where the interaction is consistent and straightforward, but bad for games,
where everything changes all the time.
With immediate mode libraries, on the other hand, the GUI is created every With immediate mode libraries, on the other hand, the GUI is created every
frame from scratch. There are no widget objects, only functions that draw the frame from scratch. Because that would be wasteful, there are no widget
widget and update some internal GUI state. This allows to put the widgets in objects. Instead, widgets are created by functions that react to UI state and
their immediate conceptual context (instead of a construction stage). It also present some data. Where this data comes from and how it is maintained does
makes the UI very flexible: Don't want to draw a widget? Simply remove the not concern the widget at all. This is, after all, your job. This gives great
call. Handling the mutable data (e.g., text of an input box) of each widget is control over what is shown where and when. The widget code can be right next
your responsibility. This separation of data and behaviour gives you greater to the code that does what should happen if the widget state changes. The
control of what is happening when an where, but can take a bit of time getting layout is also very flexible: adding a widget is one more function call, and if
used to - especially if you have used retained mode libraries before. you want to hide a widget, you simply don't call the corresponding function.
This separation of data and behaviour is great when a lot of stuff is going on,
but takes a bit of time getting used to.
What SUIT is What SUIT is
^^^^^^^^^^^^ ^^^^^^^^^^^^
SUIT is simple: It provides only the most important widgets for games: SUIT is simple: It provides only a few basic widgets that are important for
games:
- :func:`Buttons <Button>` (including :func:`Image Buttons <ImageButton>`) - :func:`Buttons <Button>` (including :func:`Image Buttons <ImageButton>`)
- :func:`Text Labels <Label>` - :func:`Text Labels <Label>`
@ -36,14 +44,14 @@ SUIT is simple: It provides only the most important widgets for games:
- :func:`Text Input <Input>` - :func:`Text Input <Input>`
- :func:`Value Sliders <Slider>` - :func:`Value Sliders <Slider>`
SUIT is comfortable: It features a simple, yet effective row/column-based SUIT is comfortable: It has a straightforward, yet effective row/column-based
layouting engine. layout engine.
SUIT is adaptable: You can easily alter the color scheme, change how widgets SUIT is adaptable: It is possible to change the color scheme, single drawing
are drawn or swap the whole theme. functions for all widget or the whole theme.
SUIT is hackable: The core library can be used to construct new widgets with SUIT is hackable: Custom widgets can leverage the extensive :doc:`core library
relative ease. <core>`.
**SUIT is good at games!** **SUIT is good at games!**
@ -51,22 +59,22 @@ relative ease.
What SUIT is not What SUIT is not
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
SUIT is not a complete GUI library: It does not provide dropdowns, sub-windows, SUIT is not a complete GUI library: It does not provide dropdowns, table views,
radio buttons, menu bars, ribbons, etc. menu bars, modal dialogs, etc.
SUIT is not a complete GUI library: SUIT spits separation of concerns, MVC and SUIT is not a complete GUI library: It does not have a markup language or tools
other good OO practices in the face. to design a user interface.
SUIT is not a complete GUI library: There is no markup language to generate or SUIT is not a complete GUI library: It does not take control of the runtime.
style the GUI. You have to do everything yourself [1]_.
**SUIT is not good at "serious" applications!** **SUIT is not good at word processors!**
Hello, World Hello, World
------------ ------------
SUIT is simple: Load the library, define your GUI in ``love.update()``, and SUITing up is is straightforward: Define your GUI in ``love.update()``, and
draw it in ``love.draw()``:: draw it in ``love.draw()``::
suit = require 'suit' suit = require 'suit'
@ -78,6 +86,7 @@ draw it in ``love.draw()``::
show_message = true show_message = true
end end
-- if the button was pressed at least one time, but a label below
if show_message then if show_message then
suit.Label("How are you today?", 100,150, 300,30) suit.Label("How are you today?", 100,150, 300,30)
end end
@ -87,31 +96,37 @@ draw it in ``love.draw()``::
suit.core.draw() suit.core.draw()
end end
As you can see, each widget is created by a function call (:func:`suit.Button As written above, the two widgets are each created by a function call
<Button>` and :func:`suit.Label <Label>`). The first argument is always the (:func:`suit.Button <Button>` and :func:`suit.Label <Label>`). The first
"payload" of the widget, and the last four arguments define the position and argument to a widget function is always the "payload" of the widget, and the
dimension of the widget. The widget returns a table indicating their updated last four arguments define the position and dimension of the widget. The
GUI state. The most important is ``hit``, which signals that the mouse was function returns a table that indicates the UI state of the widget.
clicked and released on the widget. See :doc:`Widgets <widgets>` for more info. Here, the state ``hit`` is used to figure out if the mouse was clicked and
released on the button. See :doc:`Widgets <widgets>` for more info on widget
states.
Mutable state Mutable state
------------- -------------
Widgets that mutate some state - input boxes and sliders - receive a table Widgets that mutate some state - input boxes, checkboxes and sliders - receive
argument as payload, e.g.:: a table argument as payload, e.g.::
local slider = {value = 1, max = 2} local slider = {value = 1, min = 0, max = 2}
function love.update(dt) function love.update(dt)
suit.Slider(slider, 100,100, 200,30) suit.Slider(slider, 100,100, 200,30)
suit.Label(tostring(slider.value), 300,100, 100,30) suit.Label(tostring(slider.value), 300,100, 100,30)
end end
The widget function updates the payload when some user interaction occurs. In
the above example, ``slider.value`` may be changed by the :func:`Slider`
widget. The value is then shown by a :func:`Label` next to the slider.
Options Options
------- -------
You can define optional, well, options after the payload. These options usually You can define optional, well, options after the payload. Most options affect
affect how the widget is drawn. For example, to align the label text to the how the widget is drawn. For example, to align the label text to the left in
left in the above example, you would write:: the above example, you would write::
local slider = {value = 1, max = 2} local slider = {value = 1, max = 2}
function love.update(dt) function love.update(dt)
@ -119,14 +134,14 @@ left in the above example, you would write::
suit.Label(tostring(slider.value), {align = "left"}, 300,100, 100,30) suit.Label(tostring(slider.value), {align = "left"}, 300,100, 100,30)
end end
Which options are available and what they are doing depends on the widget and What options are available and what they are doing depends on the widget and
the theme. the theme. See :doc:`Widgets <widgets>` for more info on widget options.
Keyboard input Keyboard input
-------------- --------------
The input widget requires that you forward ``keypressed`` and ``textinput`` The :func:`input widget <Input>` requires that you forward the ``keypressed``
events to SUIT:: and ``textinput`` events to SUIT::
local input = {text = ""} local input = {text = ""}
function love.update(dt) function love.update(dt)
@ -143,15 +158,22 @@ events to SUIT::
suit.core.keypressed(key) suit.core.keypressed(key)
end end
The slider widget can also react to keyboard input. The mouse state is
automatically updated, but you can provide your own version of reality if you
need to. See the :doc:`Core functions <core>` for more details.
Layout Layout
------ ------
It is tedious to write down the position and size of each widget. It is also It is tedious to calculate the position and size of each widget you want to put
not very easy to figure out what those numbers mean when you look at your code on the screen. Especially when all you want is to put three buttons beneath
after not touching it for some time. SUIT offers a simple, yet effective each other. SUIT implements a simple, yet effective layout engine. All the
layouting engine to put widgets in rows or columns. If you have ever dabbled engine does is put cells next to each other (below or right). It does not care
with `Qt's <http://qt.io>`_ ``QBoxLayout``, you already know 78.42% [1]_ what you put into those cells, but assumes that you probably need them for
of what you need to know. widgets. Cells are reported by four numbers (left, top, width and height) that
you can directly pass as the final four arguments to the widget functions.
If you have ever dabbled with `Qt's <http://qt.io>`_ ``QBoxLayout``, you
already know 89% [2]_ of what you need to know.
The first example can be written as follows:: The first example can be written as follows::
@ -159,15 +181,20 @@ The first example can be written as follows::
local show_message = false local show_message = false
function love.update(dt) function love.update(dt)
suit.layout.reset(100,100) -- reset layout origin to x=100, y=100 -- put the layout origin at position (100,100)
suit.layout.padding(10,10) -- padding of 10x10 pixels -- cells will grow down and to the right of the origin
suit.layout.reset(100,100)
-- add a new row with width=300 and height=30 and put a button in it -- put 10 extra pixels between cells in each direction
suit.layout.padding(10,10)
-- construct a cell of size 300x30 px and put the button into it
if suit.Button("Hello, World!", suit.layout.row(300,30)).hit then if suit.Button("Hello, World!", suit.layout.row(300,30)).hit then
show_message = true show_message = true
end end
-- add another row of the same size below the first row -- add another cell below the first cell
-- the size of the cell is the same as the first cell
if show_message then if show_message then
suit.Label("How are you today?", suit.layout.row()) suit.Label("How are you today?", suit.layout.row())
end end
@ -177,49 +204,52 @@ The first example can be written as follows::
suit.core.draw() suit.core.draw()
end end
At the beginning of each frame, the layout has to be reset. You can provide an At the beginning of each frame, the layout origin (and some internal layout
optional starting position and padding as arguments. Rows and columns are added state) has to be reset. You can also define optional padding between cells.
using ``layout.row(w,h)`` and ``layout.col(w,h)``. If omitted, the width and Cells are added using ``layout.row(w,h)`` (which puts the new cell below the
height of the cell are copied from the previous cell. There are also special old cell) and ``layout.col(w,h)`` (which puts the new cell to the right of the
identifiers that calculate the size from all cells since the last ``reset()``: old cell). If omitted, the width and height of the new cell are copied from
``max``, ``min`` and ``median``. They do what you expect them to do. the old cell. There are also special identifiers that calculate the size from
the sizes of all cells that were created since the last ``reset()``: ``max``,
``min`` and ``median``. They do what you expect them to do.
It is also possible to nest rows and columns and to let cells dynamically fill It is also possible to nest cells and to let cells dynamically fill the
available space. Refer to the :doc:`Layout <layout>` documentation for more available space (but you have to tell how much space there is beforehand).
information. Refer to the :doc:`Layout <layout>` documentation for more information.
Themeing Themeing
-------- --------
SUIT allows to customize the appearance of any widget (except SUIT lets you customize how any widget (except :func:`ImageButton`) is drawn.
:func:`ImageButton`). Each widget is drawn by a function of the same name in Each widget (except, :func:`you know <ImageButton>`) is drawn by a function in
the ``theme``-table of the core module. So, a button is drawn by the function the table ``suit.core.theme``. Conveniently, the name of the function
``suit.core.theme.Button``. You can overwrite these functions or swap the whole responsible for drawing a widget is named after it, so, a button is drawn by
table to achieve a different look. the function ``suit.core.theme.Button``. If you want to change how a button is
drawn, simply overwrite the function. If you want to redecorate completely, it
might be easiest to start from scratch and swap the whole table.
However, most of the time, especially when prototyping, you probably don't want However, if you just don't like the colors, the default theme is open to change.
to do this. For this reason, the default theme can be customized by modifying a It requires you to change the background (``bg``), foreground (``fg``) and
color scheme, contained in the table ``suit.core.theme.color``:: border color of three possible widget states: ``normal``, when nothing out of
the ordinary happened, ``hot``, when the mouse hovers above a widget, and
``active``, when the mouse hovers above, and the mouse button is pressed (but
not yet released) on the widget. The colors are saved in the table
``suit.core.theme.color``. The default color scheme is this::
theme.color = { suit.core.theme.color = {
normal = {bg = {78,78,78}, fg = {200,200,200}, border={20,20,20}}, normal = {bg = {78,78,78}, fg = {200,200,200}, border={20,20,20}},
hot = {bg = {98,98,98}, fg = {69,201,84}, border={30,30,30}}, hot = {bg = {98,98,98}, fg = {69,201,84}, border={30,30,30}},
active = {bg = {88,88,88}, fg = {49,181,64}, border={10,10,10}} active = {bg = {88,88,88}, fg = {49,181,64}, border={10,10,10}}
} }
The keys ``normal``, ``hot`` and ``active`` correspond to different widget states: You can also do minimally invasive surgery::
When the mouse is above a widget, it is ``hot``, if the mouse is pressed (but
not released) on a widget, it is ``active``, and otherwise it is in the
``normal`` state.
Each state defines a background (``bg``), foreground (``fg``) and border color.
You can change the colors directly by overwriting the values::
function love.load() function love.load()
suit.core.theme.color.normal.fg = {255,255,255} suit.core.theme.color.normal.fg = {255,255,255}
suit.core.theme.color.hot = {bg = {200,230,255}, {fg = {0,0,0}, border = {120,140,180}}} suit.core.theme.color.hot = {bg = {200,230,255}, {fg = {0,0,0}, border = {120,140,180}}}
end end
.. [1] Determined by rigorous scientific experiments [2]_. .. [1] But it thinks you can handle that.
.. [2] Significance level p = 0.5 [1]_. .. [2] Proportion determined by rigorous scientific experiments [3]_.
.. [3] And theoretic reasoning. Mostly that, actually.

View file

@ -39,73 +39,120 @@ Example code
------------ ------------
:: ::
suit = require 'suit' local suit = require 'suit'
-- generate some assets (below)
function love.load() function love.load()
-- generate some assets
snd = generateClickySound() snd = generateClickySound()
normal, hot = generateImageButton() normal, hot = generateImageButton()
smallerFont = love.graphics.newFont(10) smallerFont = love.graphics.newFont(10)
end end
-- mutable widget data -- data for a slider, an input box and a checkbox
local slider= {value = .5, max = 2} local slider= {value = 0.5, min = -2, max = 2}
local input = {text = "Hello"} local input = {text = "Hello"}
local chk = {text = "Check me out"} local chk = {text = "Check?"}
-- all the UI is defined in love.update or functions that are called from here
function love.update(dt) function love.update(dt)
-- new layout at 100,100 with a padding of 20x20 px -- put the layout origin at position (100,100)
-- cells will grown down and to the right from this point
-- also set cell padding to 20 pixels to the right and to the bottom
suit.layout.reset(100,100, 20,20) suit.layout.reset(100,100, 20,20)
-- Button -- put a button at the layout origin
state = suit.Button("Hover me!", suit.layout.row(200,30)) -- the cell of the button has a size of 200 by 30 pixels
if state.entered then state = suit.Button("Click?", suit.layout.row(200,30))
love.audio.play(snd)
end
if state.hit then
print("Ouch!")
end
-- Input box -- if the button was entered, play a sound
if state.entered then love.audio.play(snd) end
-- if the button was pressed, take damage
if state.hit then print("Ouch!") end
-- put an input box below the button
-- the cell of the input box has the same size as the cell above
-- if the input cell is submitted, print the text
if suit.Input(input, suit.layout.row()).submitted then if suit.Input(input, suit.layout.row()).submitted then
print(input.text) print(input.text)
end end
-- dynamically add widgets -- put a button below the input box
if suit.Button("test2", suit.layout.row(nil,40)).hovered then -- the width of the cell will be the same as above, the height will be 40 px
-- drawing options can be provided for each widget ... optionally if suit.Button("Hover?", suit.layout.row(nil,40)).hovered then
-- if the button is hovered, show two other buttons
-- this will shift all other ui elements down
-- put a button below the previous button
-- the cell height will be 30 px
-- the label of the button will be aligned top left
suit.Button("You can see", {align='left', valign='top'}, suit.layout.row(nil,30)) suit.Button("You can see", {align='left', valign='top'}, suit.layout.row(nil,30))
suit.Button("...but you can't touch!", {align='right', valign='bottom'}, suit.layout.row(nil,30))
-- put a button below the previous button
-- the cell size will be the same as the one above
-- the label will be aligned bottom right
suit.Button("...but you can't touch!", {align='right', valign='bottom'},
suit.layout.row())
end end
-- Checkbox -- put a checkbox below the button
-- the size will be the same as above
-- (NOTE: height depends on whether "Hover?" is hovered)
-- the label "Check?" will be aligned right
suit.Checkbox(chk, {align='right'}, suit.layout.row()) suit.Checkbox(chk, {align='right'}, suit.layout.row())
-- nested layouts -- put a nested layout
-- the size of the cell will be as big as the cell above or as big as the
-- nested content, whichever is bigger
suit.layout.push(suit.layout.row()) suit.layout.push(suit.layout.row())
-- put a slider in the cell
-- the inner cell will be 160 px wide and 20 px high
suit.Slider(slider, suit.layout.col(160, 20)) suit.Slider(slider, suit.layout.col(160, 20))
-- put a label that shows the slider value to the right of the slider
-- the width of the label will be 40 px
suit.Label(("%.02f"):format(slider.value), suit.layout.col(40)) suit.Label(("%.02f"):format(slider.value), suit.layout.col(40))
-- close the nested layout
suit.layout.pop() suit.layout.pop()
-- image buttons -- put an image button below the nested cell
-- the size of the cell will be 200 by 100 px,
-- but the image may be bigger or smaller
-- the button shows the image `normal' when the mouse is outside the image
-- or above a transparent pixel
-- the button shows the image `hot` if the mouse is above an opaque pixel
-- of the image `normal'
suit.ImageButton({normal, hot = hot}, suit.layout.row(200,100)) suit.ImageButton({normal, hot = hot}, suit.layout.row(200,100))
-- if the checkbox is checked, display a precomputed layout
if chk.checked then if chk.checked then
-- precomputed layout can fill up available space -- the precomputed layout will be 3 rows below each other
suit.layout.reset() -- the origin of the layout will be at (400,100)
rows = suit.layout.rows{pos = {400,100}, -- the minimal height of the layout will be 300 px
min_height = 300, rows = suit.layout.rows{pos = {400,100}, min_height = 300,
{200, 30}, {200, 30}, -- the first cell will measure 200 by 30 px
{30, 'fill'}, {30, 'fill'}, -- the second cell will be 30 px wide and fill the
{200, 30}, -- remaining vertical space between the other cells
{200, 30}, -- the third cell will be 200 by 30 px
} }
suit.Label("You uncovered the secret!", {align="left", font = smallerFont}, rows.cell(1))
-- the first cell will contain a witty label
-- the label will be aligned left
-- the font of the label will be smaller than the usual font
suit.Label("You uncovered the secret!", {align="left", font = smallerFont},
rows.cell(1))
-- the third cell will contain a label that shows the value of the slider
suit.Label(slider.value, {align='left'}, rows.cell(3)) suit.Label(slider.value, {align='left'}, rows.cell(3))
-- give different id to slider on same object so they don't grab -- the second cell will show a slider
-- each others user interaction -- the slider will operate on the same data as the first slider
suit.Slider(slider, {id = 'vs', vertical=true}, rows.cell(2)) -- the slider will be vertical instead of horizontal
print(rows.cell(3)) -- the id of the slider will be 'slider two'. this is necessary, because
-- the two sliders should not both react to UI events
suit.Slider(slider, {vertical = true, id = 'slider two'}, rows.cell(2))
end end
end end
@ -114,12 +161,13 @@ Example code
suit.core.draw() suit.core.draw()
end end
-- forward keyboard events
function love.textinput(t) function love.textinput(t)
-- forward text input to SUIT
suit.core.textinput(t) suit.core.textinput(t)
end end
function love.keypressed(key) function love.keypressed(key)
-- forward keypressed to SUIT
suit.core.keypressed(key) suit.core.keypressed(key)
end end