From c159cb078c4a0c66ac01772c6abcd11b1cc3a276 Mon Sep 17 00:00:00 2001 From: Lorenzo Cogotti Date: Wed, 24 Aug 2022 15:47:50 +0200 Subject: [PATCH] [*] Add examples. --- examples/basicmenu.lua | 124 ++++++++++++++++ examples/helloworld.lua | 51 +++++++ examples/init.lua | 79 +++++++++++ examples/optionsmenu.lua | 264 +++++++++++++++++++++++++++++++++++ fonts/PixelDroidMenu.ttf | Bin 0 -> 12304 bytes fonts/README.ACKNOWLEDGEMENT | 8 ++ main.lua | 1 + pics/hello_world.png | Bin 0 -> 9034 bytes 8 files changed, 527 insertions(+) create mode 100644 examples/basicmenu.lua create mode 100644 examples/helloworld.lua create mode 100644 examples/init.lua create mode 100644 examples/optionsmenu.lua create mode 100644 fonts/PixelDroidMenu.ttf create mode 100644 fonts/README.ACKNOWLEDGEMENT create mode 100644 main.lua create mode 100644 pics/hello_world.png diff --git a/examples/basicmenu.lua b/examples/basicmenu.lua new file mode 100644 index 0000000..f8370eb --- /dev/null +++ b/examples/basicmenu.lua @@ -0,0 +1,124 @@ +--- Basic menu example +-- +-- Trivial menu made of buttons. +-- Illustrates onHit() and onEnter()/onLeave() +-- events. +-- +-- Relevant code to build UI inside: makeMainMenu(). +-- +-- Layout: Rows +-- Widget: Button + +local yui = require 'lib.yui' + +local Button = yui.Button +local Rows = yui.Rows +local Ui = yui.Ui + +local GUI_WIDTH = 500 +local GUI_HEIGHT = 300 +local FONT_SIZE = 32 + +local function centerRectOnScreen(w, h) + local x = math.floor((love.graphics.getWidth() - w) / 2) + local y = math.floor((love.graphics.getHeight() - h) / 2) + + return x, y +end + +-- Creates main menu. +local function makeMainMenu() + -- Position the UI + local x,y = centerRectOnScreen(GUI_WIDTH, GUI_HEIGHT) + local rh = GUI_HEIGHT / 5 -- 5 elements along height + + -- Keep track what's being focused... + widgetEnter = function (w) + guiStatus.current = "You're hovering \""..w.text.."\", feel like pressing it?" + end + widgetLeave = function (w) + guiStatus.previous = "So you left \""..w.text.."\"..." + end + + return Ui.new { + x = x, y = y, -- Place UI at the calculated spot + + -- Place the elements in rows from top to bottom + Rows { + Button { + -- Provide first button's size... + w = GUI_WIDTH, h = rh, + + text = "Start game", + + onHit = function () + guiStatus = { current = "Game started!" } + end, + onEnter = widgetEnter, + onLeave = widgetLeave + }, + Button { + -- ...subsequent widgets _in the same layout_ + -- take last widget's size by default. + text = "Continue", + + onHit = function() + guiStatus = { current = "Loading game..." } + end, + onEnter = widgetEnter, + onLeave = widgetLeave + }, + Button { + text = "Options", + + onHit = function() + guiStatus = { current = "Options pressed." } + end, + onEnter = widgetEnter, + onLeave = widgetLeave + }, + Button { + text = "Credits", + + onHit = function() + guiStatus = { current = "Showcasing credits 8)" } + end, + onEnter = widgetEnter, + onLeave = widgetLeave + }, + Button { + text = "Quit", + + onHit = function () love.event.quit() end, + onEnter = widgetEnter, + onLeave = widgetLeave + } + } + } +end + +function love.load() + guiStatus = { current = "Menu is open." } + guiFont = love.graphics.newFont('fonts/PixelDroidMenu.ttf', FONT_SIZE) + gui = makeMainMenu() +end + +function love.update(dt) + -- Let the UI update its status + gui:update(dt) +end + +function love.draw() + love.graphics.setFont(guiFont) + + -- Print UI status + local y = 0 + if guiStatus.previous ~= nil then + love.graphics.print(guiStatus.previous, 0, y) + y = y + guiFont:getHeight() + end + love.graphics.print(guiStatus.current, 0, y) + + -- Draw UI + gui:draw() +end diff --git a/examples/helloworld.lua b/examples/helloworld.lua new file mode 100644 index 0000000..a7e5ffe --- /dev/null +++ b/examples/helloworld.lua @@ -0,0 +1,51 @@ +-- the ubiquitous "Hello, World!" demo. +-- 'Nuff said. +-- +-- Layout: Rows +-- Widgets: Label, Button +-- Relevant UI construction code in: love.load() + +local yui = require 'lib.yui' + +-- Some convenience aliases +local Ui = yui.Ui +local Rows = yui.Rows +local Button, Label = yui.Button, yui.Label + +local function centerRectOnScreen(w, h) + local x = math.floor((love.graphics.getWidth() - w) / 2) + local y = math.floor((love.graphics.getHeight() - h) / 2) + + return x, y +end + +function love.load() + local W, H = 400, 80 -- pick arbitrary UI size + local x, y = centerRectOnScreen(W, H) + + gui = Ui.new { + x = x, y = y, + + Rows { + Label { + w = W, h = H, + + text = "Hello, World!" + }, + Button { + text = "OBEY", + onHit = function () love.event.quit() end + } + } + } +end + +function love.update(dt) + gui:update(dt) +end + +function love.draw() + -- Pretty black out there, isn't it? + -- See more complete examples for shinier stuff :) + gui:draw() +end diff --git a/examples/init.lua b/examples/init.lua new file mode 100644 index 0000000..83a4580 --- /dev/null +++ b/examples/init.lua @@ -0,0 +1,79 @@ +local BASE = (...)..'.' + +local endswith = require('lib.gear.strings').endswith +local yui = require 'lib.yui' + +local Button = yui.Button +local Rows = yui.Rows +local Ui = yui.Ui + +local Examples = {} +Examples.__index = Examples + + +local function isexample(file) + return file ~= 'init.lua' and endswith(file, '.lua') +end + +local function loadexample(file) + love.event.clear() + for i in ipairs(love.handlers) do + love.handlers[i] = nil + end + + -- Restart to next example. + require(BASE..file) + if love.load then love.load() end +end + +local W = 400 +local RH = 32 + +local function makeSelectionMenu() + local menu = Rows {} + + local files = love.filesystem.getDirectoryItems('examples') + table.sort(files) + + for _,file in ipairs(files) do + if isexample(file) then + local name = file:sub(1, -5) + + menu[#menu+1] = Button { + w = W, h = RH, + + text = name, + notranslate = true, + onHit = function() loadexample(name) end + } + end + end + menu[#menu+1] = Button { + w = W, h = RH, + + text = "Quit", + onHit = function() love.event.quit() end + } + + local x = math.floor(love.graphics.getWidth() - W) / 2 + local y = math.floor(love.graphics.getHeight() - RH * #menu) / 2 + + return Ui.new { + x = x, y = y, + menu + } +end + +function love.load() + gui = makeSelectionMenu() +end + +function love.update(dt) + gui:update(dt) +end + +function love.draw() + gui:draw() +end + +return Examples diff --git a/examples/optionsmenu.lua b/examples/optionsmenu.lua new file mode 100644 index 0000000..d1a39da --- /dev/null +++ b/examples/optionsmenu.lua @@ -0,0 +1,264 @@ +--- Options screen example +-- +-- A fairly complete configuration menu to demonstrate +-- several types of widgets coexisting. +-- Many widgets' behavior is tuned by their attributes. +-- +-- Relevant code to build UI inside: makeOptionsMenu(). +-- +-- Layout: Rows, Columns +-- Widget: Button, Checkbox, Choice, Input, Label, Slider, Spacer + +local yui = require 'lib.yui' + +local Button = yui.Button +local Checkbox = yui.Checkbox +local Choice = yui.Choice +local Columns = yui.Columns +local Input = yui.Input +local Label = yui.Label +local Rows = yui.Rows +local Slider = yui.Slider +local Spacer = yui.Spacer +local Ui = yui.Ui + +local GUI_WIDTH = 500 +local GUI_HEIGHT = 300 +local FONT_SIZE = 32 + +-- A dummy save file, for demo purposes. +local dummySave = {} + +-- Dummy config load mock. +function dummySave.load() + local config = {} + + -- The default configuration + local defconfig = { + name = "Player 1", + graphics = 'medium', + lang = 'en', + fullscreen = false, + musicVolume = 80, + sfxVolume = 90 + } + + local saved = dummySave.latest or defconfig + + print("Loading configuration: {") + for k,v in pairs(saved) do + config[k] = v + + print(("\t%s = %s,"):format(k, v)) + end + print("}") + + return config +end +-- Dummy config save mock +function dummySave.save(config) + dummySave.latest = {} + + print("Saving configuration: {") + for k,v in pairs(config) do + dummySave.latest[k] = v + + print(("\t%s = %s,"):format(k, v)) + end + print("}") +end + +-- Center rectangle on screen. +local function centerRectOnScreen(w, h) + local x = math.floor((love.graphics.getWidth() - w) / 2) + local y = math.floor((love.graphics.getHeight() - h) / 2) + + return x, y +end + +-- Actual menu creation. +local function makeOptionsMenu() + -- Position the UI + local x,y = centerRectOnScreen(GUI_WIDTH, GUI_HEIGHT) + local vpad = 8 + local hpad = 4 + + local w = (GUI_WIDTH - hpad) / 2 -- cut away padding from widget width + local h = (GUI_HEIGHT - vpad) / 9 -- 9 rows (Spacer counts as 2) + + -- Load configuration "from disk" + local config = dummySave.load() + + -- Make configuration editable via UI + return Ui.new { + x = x, y = y, + + Rows { + padding = vpad, + + Columns { + padding = hpad, + + Label { + w = w, h = h, + text = "Name" + }, + Input { + text = config.name, -- populate initial value + + onChange = function(_, text) + config.name = text -- write changed value back + end + } + }, + Columns { + padding = hpad, + + Label { + w = w, h = h, + text = "Language" + }, + Choice { + -- We don't want languages to be translated. + notranslate = true, + + choices = { + { text = "English", value = 'en' }, -- just kidding, only English in demo :) + -- { text = "Français", value = 'fr' }, + -- { text = "Italiano", value = 'it' }, + -- { text = "日本語", value = 'ja' } + }, + default = config.lang, + nowrap = true, + + onChange = function(_, choice) config.lang = choice.value end + } + }, + Columns { + padding = hpad, + + Label { + w = w, h = h, + text = "Graphics" + }, + Choice { + choices = { + -- Text may be localized, + -- so map entries to well known enum values. + { text = "Fastest", value = 'low' }, + { text = "Normal", value = 'medium' }, + { text = "Best", value = 'high' } + }, + default = config.graphics, + nowrap = true, + + onChange = function(_, choice) config.graphics = choice.value end + } + }, + Columns { + padding = hpad, + + Label { + w = w, h = h, + text = "Fullscreen" + }, + Checkbox { + checked = config.fullscreen, + + onChange = function(_, checked) config.fullscreen = checked end + } + }, + Columns { + padding = hpad, + + Label { + w = w, h = h, + text = "Music" + }, + Slider { + min = 0, max = 100, + value = config.musicVolume, + + onChange = function(_, value) config.musicVolume = value end + } + }, + Columns { + padding = hpad, + + Label { + w = w, h = h, + text = "Effects" + }, + Slider { + min = 0, max = 100, + value = config.sfxVolume, + + onChange = function(_, value) config.sfxVolume = value end + } + }, + + Spacer { h = 2*h }, -- leave 2 rows out. + + Columns { + padding = hpad, + + Button { + w = w, h = h, + text = "Back", + + cancelfocus = true, -- gets focused on ESC. + + onHit = function() love.event.quit() end + }, + Button { + text = "Apply", + + onHit = function() + -- Save configuration "on disk" + dummySave.save(config) + end + } + } + } + } +end + +function love.load() + guiFont = love.graphics.newFont('fonts/PixelDroidMenu.ttf', FONT_SIZE) + gui = makeOptionsMenu() +end + +function love.update(dt) + -- Let the UI update its status + gui:update(dt) +end + +-- Options menu contains a text input field, +-- this requires propagating LÖVE input events to UI. +function love.keypressed(key, scan, isrepeat) + gui:keypressed(key, scan, isrepeat) +end +function love.keyreleased(key, scan) + gui:keyreleased(key, scan) +end +function love.textinput(text) + gui:textinput(text) +end +function love.textedited(text, start, len) + gui:textedited(text, start, len) +end + +function love.draw() + love.graphics.setFont(guiFont) + + -- Draw menu title + local title = "Options" + local tw = guiFont:getWidth(title) + local th = guiFont:getHeight() + local tx = (love.graphics.getWidth() - tw) / 2 + local ty = th*2 + love.graphics.print(title, tx,ty) + + -- Draw UI + gui:draw() +end diff --git a/fonts/PixelDroidMenu.ttf b/fonts/PixelDroidMenu.ttf new file mode 100644 index 0000000000000000000000000000000000000000..14651392b9806bad568a88387c2f1bba765757f1 GIT binary patch literal 12304 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 M8{ewD=@j`t0N6!&n*aa+ literal 0 HcmV?d00001