diff --git a/assets/agricola-rough.otf b/assets/agricola-rough.otf new file mode 100644 index 0000000..92052c3 Binary files /dev/null and b/assets/agricola-rough.otf differ diff --git a/assets/crossbow.png b/assets/crossbow.png new file mode 100644 index 0000000..27bc0b5 Binary files /dev/null and b/assets/crossbow.png differ diff --git a/assets/sparkling-sabre.png b/assets/sparkling-sabre.png new file mode 100644 index 0000000..95bcd21 Binary files /dev/null and b/assets/sparkling-sabre.png differ diff --git a/assets/switch-weapon.png b/assets/switch-weapon.png new file mode 100644 index 0000000..e357a1a Binary files /dev/null and b/assets/switch-weapon.png differ diff --git a/composer/attributes.lua b/composer/attributes.lua index 8647b2b..ead8488 100644 --- a/composer/attributes.lua +++ b/composer/attributes.lua @@ -1,162 +1,41 @@ -local _PATH = (...):match("(.-)[^%.]+$") -local F = require(_PATH .. "functions") +local PATH = (...):match('(.-)[^%.]+$') +local Object = require(PATH .. 'classic') +local F = require(PATH .. 'functions') ---[[ --- ID --- Use the ID attribute to assign an identifier to an element. This identifier --- can be used to easily retrieve an element from the layout. ---]] +--[[ MARGIN ]]-- -local ID = {} +local Margin = Object:extend() -function ID:new(value) - return setmetatable({ - value = value - }, ID) +function Margin:new(t, l, b, r) + self.t = t or 0 + self.l = l or self.t + self.b = b or self.t + self.r = r or self.l end -function ID:__tostring() - return F.describe("ID", self) -end - -setmetatable(ID, { - __call = ID.new -}) - ---[[ --- Margin --- The Margin attribute is only used by the Border element and defines spacing --- between the Border and it's child element. The arguments are defined in --- order left, top, right, bottom. --- --- Examples: --- * Margin() => Margin(0, 0, 0, 0) --- * Margin(5) => Margin(5, 5, 5, 5) --- * Margin(5, 10) => Margin(5, 10, 5, 10) --- * Margin(5, 10, 8) => Margin(5, 10, 8, 0) --- * Margin(5, 10, 8, 6) ---]] - -local Margin = {} - -function Margin:new(l, t, r, b) - l = l or 0 - r = r or l - t = t or r - b = b or t - - return setmetatable({ - l = l, - r = r, - t = t, - b = b, - }, Margin) +function Margin:unpack() + return self.t, self.l, self.b, self.r end function Margin:__tostring() - return F.describe("Margin", self) -end - -setmetatable(Margin, { - __call = Margin.new -}) - ---[[ --- MinSize --- Use the MinSize attribute to specify a minimum size of an element in pixels. --- --- Examples: --- * MinSize() => MinSize(0, 0) --- * MinSize(10) => MinSize(10, 10) --- * MinSize(10, 50) ---]] - -local MinSize = {} - -function MinSize:new(x, y) - x = x or 0 - y = y or x - - return setmetatable({ - x = x, - y = y, - }, MinSize) -end - -function MinSize:__tostring() - return F.describe("MinSize", self) -end - -setmetatable(MinSize, { - __call = MinSize.new -}) - ---[[ --- ExpSize --- The ExpSize attribute is only meant for internal use. This attribute --- represents the expanded size of the element taking into account MinSize, --- Stretch and Margin attributes. ---]] - -local ExpSize = {} - -function ExpSize:new(x, y) - return setmetatable({ - x = x, - y = y, - }, ExpSize) + return F.describe('Margin', self) end -function ExpSize:__tostring() - return F.describe("ExpSize", self) -end - -setmetatable(ExpSize, { - __call = ExpSize.new -}) - ---[[ --- Stretch --- The Stretch attribute defines how an element stretches inside its container. --- The x and y value should either be 0 to not expand MinSize or 1 to stretch --- to expand to the full size of the container. --- --- Examples: --- * Stretch() => Stretch(0, 0) --- * Stretch(1) => Stretch(1, 1) --- * Stretch(0, 1) ---]] +--[[ SPACING ]]-- -local Stretch = {} +local Spacing = Object:extend() -function Stretch:new(x, y) - x = x or 0 - y = y or x - - return setmetatable({ - x = x, - y = y, - }, Stretch) +function Spacing:new(v) + self.v = v end -function Stretch:__tostring() - return F.describe("Stretch", self) +function Spacing:__tostring() + return F.describe('Spacing', self) end -setmetatable(Stretch, { - __call = Stretch.new -}) - ---[[ --- Module ---]] +--[[ MODULE ]]-- return { - ID = ID, - Margin = Margin, - MinSize = MinSize, - ExpSize = ExpSize, - Stretch = Stretch, + Margin = Margin, + Spacing = Spacing, } - --- \ No newline at end of file diff --git a/composer/classic.lua b/composer/classic.lua new file mode 100644 index 0000000..57d9ef5 --- /dev/null +++ b/composer/classic.lua @@ -0,0 +1,57 @@ +-- +-- classic +-- +-- Copyright (c) 2014, rxi +-- +-- This module is free software; you can redistribute it and/or modify it under +-- the terms of the MIT license. See LICENSE for details. +-- + +local Object = {} +Object.__index = Object + +function Object:new() end + +function Object:extend() + local cls = {} + for k, v in pairs(self) do + if k:find('__') == 1 then cls[k] = v end + end + cls.__index = cls + cls.super = self + setmetatable(cls, self) + return cls +end + +function Object:implement(...) + for _, cls in pairs({...}) do + for k, v in pairs(cls) do + if self[k] == nil and type(v) == 'function' then + self[k] = v + end + end + end +end + +function Object:is(T) + local mt = getmetatable(self) + + while mt do + if mt == T then return true end + mt = getmetatable(mt) + end + + return false +end + +function Object:__tostring() + return 'Object' +end + +function Object:__call(...) + local obj = setmetatable({}, self) + obj:new(...) + return obj +end + +return Object diff --git a/composer/controls.lua b/composer/controls.lua new file mode 100644 index 0000000..706a7e1 --- /dev/null +++ b/composer/controls.lua @@ -0,0 +1,700 @@ +local PATH = (...):match('(.-)[^%.]+$') +local Object = require(PATH .. 'classic') +local F = require(PATH .. 'functions') +local Theme = require(PATH .. 'theme') +local core = require(PATH .. 'core') +local layout = require(PATH .. 'layout') +local utf8 = require 'utf8' + +local mfloor, mceil, mmax, mmin = math.floor, math.ceil, math.max, math.min +local tconcat = table.concat + +local getColorsForState = function(state) + return Theme[state] or Theme['normal'] +end + +local round = function(x) + return x >= 0 and mfloor(x + 0.5) or mceil(x - 0.5) +end + +local getTextSize = function(text, font) + return { w = font:getWidth(text), h = font:getHeight() } +end + +local split = function(str, pos) + local offset = utf8.offset(str, pos) or 0 + return str:sub(1, offset - 1), str:sub(offset) +end + +local getAlignment = function(align) + local alignments = { 'center', 'left', 'right' } + + if not align then return alignments[1] end + + for _, alignment in ipairs(alignments) do + if align == alignment then return alignment end + end + + error('invalid alignment, valid values are: ' .. tconcat(alignments, ', ')) +end + +local parseFont = function(font_info) + local font_size = 16 + local font_name = nil + + if type(font_info) == 'string' then + font_name = font_info + elseif type(font_info) == 'number' then + font_size = font_info + elseif type(font_info) == 'table' then + for _, v in ipairs(font_info) do + if type(v) == 'number' then font_size = v + elseif type(v) == 'string' then font_name = v + else error('value should be either a string, number or table containing a string and number') end + end + end + + if font_name == nil then + return love.graphics.newFont(font_size) + else + return love.graphics.newFont(font_name, font_size) + end +end + +local drawRect = function(frame, corner_r) + local corner_r = corner_r or 0 + local line_width = love.graphics.getLineWidth() + + local x = frame.x + line_width / 2 + local y = frame.y + line_width / 2 + local w = frame.w - line_width + local h = frame.h - line_width + + love.graphics.rectangle('line', x, y, w, h, corner_r, corner_r) +end + +--[[ RECT ]]-- + +local Rect = Object:extend() + +function Rect:new(x, y, w, h) + self.x = x + self.y = y + self.w = w + self.h = h +end + +function Rect:midXY() return self.x + self.w / 2, self.y + self.h / 2 end + +function Rect:maxX() return self.x + self.w end + +function Rect:maxY() return self.y + self.h end + +function Rect:containsPoint(x, y) + return ( + x >= self.x and x < self.x + self.w and + y >= self.y and y < self.y + self.h + ) +end + +function Rect:unpack() return self.x, self.y, self.w, self.h end + +function Rect:__tostring() + return '{ ' .. table.concat({ self.x, self.y, self.w, self.h }, ', ') .. ' }' +end + +--[[ CONTROL ]]-- + +local Control = Object:extend() + +function Control:new() + self.color = F.randomColor() + self.state = 'normal' + self.frame = Rect(0, 0, 0, 0) +end + +function Control:setFrame(x, y, w, h) + self.frame = Rect(x, y, w, h) +end + +function Control:setEnabled(is_enabled) + if is_enabled then self.state = 'normal' else self.state = 'disabled' end +end + +function Control:update(dt) + if self.state == 'disabled' then return end + + local m_x, m_y = core.getMousePosition() + local next_state = self.frame:containsPoint(m_x, m_y) and 'hovered' or 'normal' + + if next_state == 'hovered' and core.isMouseButtonDown() then + next_state = 'active' + end + + if self.state == 'active' then + core.setActive(self) + + if next_state == 'hovered' then self:hit() end + end + + self.state = next_state +end + +-- can be overidden by subclasses to respond on hit +function Control:hit() end + +function Control:draw() + love.graphics.setColor(unpack(self.color)) + -- love.graphics.rectangle('fill', self.frame:unpack()) +end + +function Control:sizeThatFits(w, h) + return w, h +end + +function Control:__tostring() + return F.describe('Control', self) +end + +--[[ LABEL ]]-- + +local Label = Control:extend() + +function Label:new(opts) + Control.new(self) + + local opts = opts or {} + + self.text = opts.text or '' + self.align = getAlignment(opts.align) + self.font = parseFont(opts.font) + self.text_size = getTextSize(self.text, self.font) +end + +function Label:draw() + local c = getColorsForState('normal') + love.graphics.setColor(c.fg) + love.graphics.setFont(self.font) + love.graphics.printf( + self.text, + self.font, + mfloor(self.frame.x), + mfloor(self.frame.y + (self.frame.h - self.text_size.h) / 2), + self.frame.w, + self.align + ) +end + +function Label:sizeThatFits(w, h) + if w == math.huge then w = self.text_size.w end + if h == math.huge then h = self.text_size.h end + return w, h +end + +function Label:__tostring() + return F.describe('Label', self) +end + +--[[ BUTTON ]]-- + +local Button = Control:extend() + +Button.PADDING = 10 + +function Button:new(opts) + Control.new(self) + + local opts = opts or {} + + self.text = opts.text or '' + self.font = parseFont(opts.font) + self.text_size = getTextSize(self.text, self.font) + self.corner_radius = opts.corner_radius or 0 +end + +function Button:draw() + local state = 'normal' + + local c = getColorsForState(self.state) + local text_x, text_y = self.frame:midXY(self.frame) + + local x, y, w, h = self.frame:unpack() + local r = self.corner_radius + + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x, y, w, h, r, r) + + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', x, y, w, h, r, r) + + love.graphics.setFont(self.font) + love.graphics.print( + self.text, + mfloor(text_x), + mfloor(text_y), + 0, 1, 1, + mceil(self.text_size.w / 2), + mceil(self.text_size.h / 2) + ) +end + +function Button:sizeThatFits(w, h) + if w == math.huge then w = self.text_size.w + Button.PADDING * 2 end + if h == math.huge then h = self.text_size.h + Button.PADDING * 2 end + return w, h +end + +function Button:__tostring() + return F.describe('Button', self) +end + +--[[ CHECK BOX ]]-- + +local Checkbox = Control:extend() +Checkbox.SIZE = 20 + +function Checkbox:new(opts) + Control.new(self) + + local opts = opts or {} + + self.checked = opts.checked == true + self.corner_radius = opts.corner_radius or 0 +end + +function Checkbox:hit() + if self.state == 'disabled' then return end + + self.checked = not self.checked +end + +function Checkbox:draw() + local c = getColorsForState(self.state) + + local x, y, w, h = self.frame:unpack() + + -- for the checkbox, the active state contains only the checkmark image, so + -- when in active state, use normal image for background + local state = self.state == 'active' and 'normal' or self.state + local r = self.corner_radius + + local dx, dy = (w - Checkbox.SIZE) / 2, (h - Checkbox.SIZE) / 2 + + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x + dx, y + dy, Checkbox.SIZE, Checkbox.SIZE, r, r) + + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', x + dx, y + dy, Checkbox.SIZE, Checkbox.SIZE, r, r) + + -- if checked, draw checkmark + if self.checked then + local lw = love.graphics.getLineWidth() + local x, y = self.frame:midXY() + local r = Checkbox.SIZE / 2 - lw * 2 + love.graphics.circle('fill', x, y, r) + end +end + +function Checkbox:sizeThatFits(w, h) + if w == math.huge then w = Checkbox.SIZE end + if h == math.huge then h = Checkbox.SIZE end + return w, h +end + +function Checkbox:__tostring() + return F.describe('Checkbox', self) +end + +--[[ IMAGE BUTTON ]]-- + +local ImageButton = Control:extend() + +function ImageButton:new(opts) + Control.new(self) + + local opts = opts or {} + + self.image = opts.image and love.graphics.newImage(opts.image) + self.corner_radius = opts.corner_radius or 0 +end + +function ImageButton:draw() + local c = getColorsForState(self.state) + + local x, y, w, h = self.frame:unpack() + local r = self.corner_radius + + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x, y, w, h, r, r) + + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', x, y, w, h, r, r) + + -- draw the image + local image = self.image + if image then + local iw, ih = image:getDimensions() + local ox = (iw - self.frame.w) / 2 + local oy = (ih - self.frame.h) / 2 + love.graphics.draw(image, self.frame.x, self.frame.y, 0, 1, 1, ox, oy) + end +end + +function ImageButton:sizeThatFits(w, h) + local img_w, img_h = self.image:getDimensions() + if w == math.huge then w = img_w end + if h == math.huge then h = img_h end + return w, h +end + +function ImageButton:__tostring() + return F.describe('ImageButton', self) +end + +--[[ PROGRESS ]]-- + +local Progress = Control:extend() +Progress.BAR_HEIGHT = 20 +Progress.DEFAULT_WIDTH = 150 + +function Progress:new(opts) + Control.new(self) + + local opts = opts or {} + + self.corner_radius = opts.corner_radius or 0 + self.value = opts.value or 0 +end + +function Progress:setValue(x) + self.value = mmin(mmax(x, 0), 1.0) +end + +function Progress:getValue() + return self.value +end + +function Progress:update(dt) + Control.update(self, dt) + + if self.state == 'active' or self.state == 'hovered' then + self.state = 'normal' + end +end + +function Progress:draw() + local c = getColorsForState('active') + + local x, y, w, h = self.frame:unpack() + local r = self.corner_radius + + local bar_y = y + mfloor(h - Progress.BAR_HEIGHT) / 2 + local bar_w = self.value * w + + if self.value > 0 then + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x, bar_y, bar_w, Progress.BAR_HEIGHT, r, r) + end + + c = getColorsForState('normal') + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', x, bar_y, w, Progress.BAR_HEIGHT, r, r) +end + +function Progress:__tostring() + return F.describe('Progress', self) +end + +function Progress:sizeThatFits(w, h) + if w == math.huge then w = Progress.DEFAULT_WIDTH end + if h == math.huge then h = Progress.BAR_HEIGHT end + return w, h +end + +--[[ SLIDER ]]-- + +local Slider = Control:extend() +Slider.BAR_HEIGHT = 10 +Slider.KNOB_HEIGHT = 30 +Slider.KNOB_WIDTH = 12 +Slider.SPACING = 10 +Slider.DEFAULT_WIDTH = 150 + +function Slider:new(opts) + Control.new(self) + + local opts = opts or {} + + self.corner_radius = opts.corner_radius or 0 + self.min = opts.min or 0 + self.max = opts.max or 10 + self.step = opts.step or 20 + self.value = opts.value or 0 + self.font = parseFont(opts.font) + + self.text_size = getTextSize(tostring(self.max), self.font) + self.text_size.w = self.text_size.w +end + +function Slider:getValue() + return self.min + self.value +end + +function Slider:setValue(x) + self.value = mmin(mmax(x, self.min), self.max) - self.min +end + +function Slider:update(dt) + Control.update(self, dt) + + if self.state == 'active' then + local m_x, m_y = core.getMousePosition() + self.value = round((m_x - self.frame.x) / (self.frame.w - self.text_size.w - Slider.SPACING) * self.step) + self.value = math.min(self.value, self.max) + end +end + +function Slider:draw() + local c = (self.state == 'active' and + getColorsForState('hovered') or + getColorsForState(self.state) + ) + + local x, y, w, h = self.frame:unpack() + local r = self.corner_radius + + local bar_y = y + (h - Slider.BAR_HEIGHT) / 2 + local bar_step = (w - self.text_size.w - Slider.SPACING) / self.step + local bar_w = self.value * bar_step + + -- draw bar fill based on current value + if self.value > 0 then + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x, bar_y, bar_w, Slider.BAR_HEIGHT, r, r) + end + + -- draw bar outline + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', x, bar_y, w - self.text_size.w - Slider.SPACING, Slider.BAR_HEIGHT, r, r) + + -- draw knob + if self.state == 'active' or self.state == 'hovered' then + c = getColorsForState(self.state) + local knob_x = x + bar_w - Slider.KNOB_WIDTH / 2 + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', knob_x, y, Slider.KNOB_WIDTH, h, r, r) + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', knob_x, y, Slider.KNOB_WIDTH, h, r, r) + end + + -- draw value on right side + if self.state == 'active' or self.state == 'hovered' then + c = getColorsForState('normal') + end + love.graphics.setColor(c.fg) + love.graphics.setFont(self.font) + love.graphics.printf( + tostring(self.value), + self.font, + self.frame:maxX() - self.text_size.w, + mfloor(self.frame.y + (self.frame.h - self.text_size.h) / 2), + self.frame.w, + self.align + ) +end + +function Slider:sizeThatFits(w, h) + if w == math.huge then w = Slider.DEFAULT_WIDTH end + if h == math.huge then h = mmax(Slider.KNOB_HEIGHT, self.text_size.h) end + return w, h +end + +function Slider:__tostring() + return F.describe('Slider', self) +end + +--[[ SPACE ]]-- + +local Space = Control:extend() + +function Space:__tostring() + return F.describe('Space', self) +end + +--[[ INPUT ]]-- + +local Input = Control:extend() +Input.SPACING = 5 +Input.DEFAULT_WIDTH = 150 + +function Input:new(opts) + Control.new(self) + + local opts = opts or {} + + self.corner_radius = opts.corner_radius or 0 + self.text = opts.text or '' + self.align = getAlignment(opts.align) + self.font = parseFont(opts.font) + self.text_size = getTextSize(self.text, self.font) + + self.cursor = mmax(1, utf8.len(self.text) + 1) + self.draw_offset = 0 + -- cursor is position *before* the character (including EOS) i.e. in "hello": + -- position 1: |hello + -- position 2: h|ello + -- ... + -- position 6: hello| + + self.cursor_pos = 0 + if self.cursor > 1 then + local s = self.text:sub(1, utf8.offset(self.text, self.cursor) - 1) + self.cursor_pos = self.font:getWidth(s) + end +end + +function Input:hit() + -- move cursor to mouse position + self.cursor = utf8.len(self.text) + 1 + + local x, _ = core.getMousePosition() - self.frame.x + for i = 1, utf8.len(self.text) + 1 do + local s = self.text:sub(0, utf8.offset(self.text, i) - 1) + if self.font:getWidth(s) >= x then + self.cursor = i - 1 + break + end + end +end + +function Input:update(dt) + Control.update(self, dt) + + if self.cursor > 0 and core.getActive() == self then + local s = self.text:sub(1, utf8.offset(self.text, self.cursor) - 1) + self.cursor_pos = self.font:getWidth(s) + else + self.cursor_pos = 0 + end + + local wm = self.frame.w - Input.SPACING + + if self.cursor_pos - self.draw_offset < 0 then + self.draw_offset = self.cursor_pos + end + if self.cursor_pos - self.draw_offset > wm then + self.draw_offset = self.cursor_pos - wm + end + if self.text_size.w - self.draw_offset < wm and self.text_size.w > wm then + self.draw_offset = self.text_size.w - wm + end + + if self.cursor_pos < self.frame.w then + self.draw_offset = 0 + end + + if core.getActive() ~= self then return end + + local keycode, char = core.getPressedKey() + + if char and char ~= '' then + local a, b = split(self.text, self.cursor) + self.text = tconcat({ a, char, b }) + self.cursor = self.cursor + utf8.len(char) + end + + if keycode == 'backspace' then + local a, b = split(self.text, self.cursor) + self.text = tconcat({ split(a, utf8.len(a)), b }) + self.cursor = mmax(1, self.cursor - 1) + elseif keycode == 'delete' then + local a, b = split(self.text, self.cursor) + local _, b = split(b, 2) + self.text = tconcat({ a, b }) + end + + if keycode == 'left' then + self.cursor = mmax(self.cursor - 1, 1) + elseif keycode == 'right' then + self.cursor = mmin(self.cursor + 1, utf8.len(self.text) + 1) + elseif keycode == 'home' then + self.cursor = 0 + elseif keycode == 'end' then + self.cursor = utf8.len(self.text) + 1 + end +end + +function Input:draw() + local x, y, w, h = self.frame:unpack() + local r = self.corner_radius + + -- draw fill + local c = getColorsForState('normal') + love.graphics.setColor(c.bg) + love.graphics.rectangle('fill', x, y, w, h, r, r) + + -- draw outline + local c = core.getActive() == self and + getColorsForState('active') or + getColorsForState(self.state) + love.graphics.setColor(c.fg) + love.graphics.rectangle('line', x, y, w, h, r, r) + + -- set scissors + local sx, sy, sw, sh = love.graphics.getScissor() + love.graphics.setScissor(x - 1, y, w + 2, h) + + -- draw text + local tx = mfloor(x - self.draw_offset + Input.SPACING) + local ty = mfloor(y + (h - self.text_size.h) / 2) + c = getColorsForState('normal') + love.graphics.setColor(c.fg) + love.graphics.setFont(self.font) + love.graphics.print(self.text, tx, ty) + + -- draw cursor + if core.getActive() == self and (love.timer.getTime() % 1) > 0.5 then + local c = self.text:sub(self.cursor - 1, self.cursor - 1) + local ws = self.cursor == 1 and 0 or self.font:getWidth(c) + + if ws == 0 then x = x + Input.SPACING end + + local lw = love.graphics.getLineWidth() + local ls = love.graphics.getLineStyle() + + love.graphics.setLineWidth(1) + love.graphics.setLineStyle('rough') + love.graphics.line( + x + self.cursor_pos + ws / 2 - self.draw_offset, y + (h - self.text_size.h) / 2, + x + self.cursor_pos + ws / 2 - self.draw_offset, y + (h + self.text_size.h) / 2 + ) + love.graphics.setLineStyle(ls) + love.graphics.setLineWidth(lw) + end + + love.graphics.setScissor(sx, sy, sw, sh) +end + +function Input:sizeThatFits(w, h) + if w == math.huge then w = Input.DEFAULT_WIDTH end + if h == math.huge then h = self.text_size.h + Input.SPACING * 2 end + return w, h +end + +function Input:__tostring() + return F.describe('Input', self) +end + +--[[ MODULE ]]-- + +return { + Control = Control, + Label = function(opts) return layout.Elem(Label(opts)) end, + Button = function(opts) return layout.Elem(Button(opts)) end, + ImageButton = ImageButton, + Checkbox = Checkbox, + Progress = Progress, + Slider = Slider, + Input = Input, + Space = function() return layout.Elem(Space()) end, +} diff --git a/composer/core.lua b/composer/core.lua new file mode 100644 index 0000000..993f70e --- /dev/null +++ b/composer/core.lua @@ -0,0 +1,70 @@ +local mouse = { x = 0, y = 0, is_down = is_down } +local key = { down = nil, char = nil } +local active = nil + +local function updateMouse(x, y, is_down) + mouse = { x = x, y = y, is_down = is_down } +end + +local function getMousePosition() + return mouse.x, mouse.y +end + +local function getPressedKey() + return key.down, key.char +end + +local function isMouseButtonDown() + return mouse.is_down +end + +local function init() + if _G.love then + local update = _G.love.update + local keypressed = _G.love.keypressed + local textinput = _G.love.textinput or function(t) end + + _G.love.update = function(dt) + update(dt) + + -- update default mouse position for next frame + mouse = { + x = love.mouse.getX(), + y = love.mouse.getY(), + is_down = love.mouse.isDown(1), + } + + key = { code = nil, char = nil } + end + + _G.love.keypressed = function(k) + keypressed(k) + + key.down = k + end + + _G.love.textinput = function(t) + textinput(t) + + key.char = t + end + end +end + +local function setActive(control) + active = control +end + +local function getActive() + return active +end + +return { + init = init, + updateMouse = updateMouse, + getMousePosition = getMousePosition, + getPressedKey = getPressedKey, + isMouseButtonDown = isMouseButtonDown, + setActive = setActive, + getActive = getActive, +} diff --git a/composer/functions.lua b/composer/functions.lua index 1a6ed96..91af21c 100644 --- a/composer/functions.lua +++ b/composer/functions.lua @@ -1,80 +1,4 @@ -local function sum(tbl) - local total = 0 - for _, v in ipairs(tbl) do - total = total + v - end - return total -end - --- source: https://gist.github.com/w13b3/5d8a80fae57ab9d51e285f909e2862e0 -function zip(...) - local idx, ret, args = 1, {}, {...} - - while true do -- loop smallest table-times - local sub_table = {} - for _, table_ in ipairs(args) do - value = table_[idx] -- becomes nil if index is out of range - if value == nil then break end -- break for-loop - table.insert(sub_table, value) - end - if value == nil then break end -- break while-loop - table.insert(ret, sub_table) -- insert the sub result - idx = idx + 1 - end - - return ret -end - -local function spread(target_size, ...) - local sizes, stretches = unpack(zip(...)) - local fixed_size = sum(sizes) - local flexy_size = math.max(0, target_size - fixed_size) - local num_flexy_parts = sum(stretches) - - local flexy_add = 0 - if num_flexy_parts > 0 then - flexy_add = math.floor(flexy_size / num_flexy_parts) - end - - local flexy_adds = {} - for i, st in ipairs(stretches) do - flexy_adds[#flexy_adds + 1] = st * flexy_add - end - - local flexy_err = flexy_size - sum(flexy_adds) - if flexy_err ~= 0 then - local v = stretches[#stretches] - - for i = #stretches, 0, -1 do - if i ~= 0 then - flexy_adds[i] = flexy_adds[i] + flexy_err - break - end - end - - for _, stretch in ipairs(stretches) do - if flexy_err == 0 then break end - end - end - - local spreads = {} - for _, sz_fa in ipairs(zip(sizes, flexy_adds)) do - spreads[#spreads + 1] = sum(sz_fa) - end - - return spreads -end - --- remove a value from a table if function fn returns true -local function removeMatch(tbl, fn) - for i = #tbl, 1, -1 do - local value = tbl[i] - if fn(value) then - table.remove(tbl, i) - return value - end - end -end +local mrandom = math.random local function describe(name, tbl) local values = {} @@ -85,10 +9,13 @@ local function describe(name, tbl) return name .. " { " .. table.concat(values, ", ") .. " }" end +local function randomColor() + return { mrandom(255) / 255, mrandom(255) / 255, mrandom(255) / 255 } +end + -- the module return { - zip = zip, - spread = spread, + randomColor = randomColor, removeMatch = removeMatch, describe = describe, } \ No newline at end of file diff --git a/composer/init.lua b/composer/init.lua index 6f386cb..fe8cdf3 100644 --- a/composer/init.lua +++ b/composer/init.lua @@ -1,5 +1,7 @@ local PATH = (...):gsub('%.init$', '') +local core = require(PATH .. '.core') + local composer = { _VERSION = "0.1.0", _DESCRIPTION = "A simple layout engine.", @@ -29,9 +31,8 @@ local composer = { ]], } -local Loader = require(PATH .. ".loader") -composer.require = Loader.require -composer.unrequire = Loader.unrequire -composer.load = Loader.load +composer.load = require(PATH .. '.loader') +composer.init = core.init +composer.updateMouse = core.updateMouse return composer diff --git a/composer/layout.lua b/composer/layout.lua index 3755d01..7bc0c6e 100644 --- a/composer/layout.lua +++ b/composer/layout.lua @@ -1,324 +1,300 @@ -local _PATH = (...):match("(.-)[^%.]+$") -local attr = require(_PATH .. "attributes") -local F = require(_PATH .. "functions") - -local Stretch = attr.Stretch -local MinSize = attr.MinSize -local Margin = attr.Margin -local ExpSize = attr.ExpSize -local ID = attr.ID - ---[[ --- Rect ---]] - -local Rect = {} -Rect.__index = Rect - -function Rect:new(x, y, w, h) - return setmetatable({ - x = x, - y = y, - w = w, - h = h, - }, Rect) -end - -function Rect:__tostring() - return F.describe("Rect", self) -end +local PATH = (...):match('(.-)[^%.]+$') +local F = require(PATH .. 'functions') +local Object = require(PATH .. 'classic') +local Rect = require(PATH .. 'rect') +local attr = require(PATH .. 'attributes') -setmetatable(Rect, { - __call = Rect.new -}) +--[[ LAYOUT ]]-- ---[[ --- Layout ---]] +local Layout = Object:extend() +local HStack = Layout:extend() +local VStack = Layout:extend() +local Elem = Layout:extend() -local Layout = {} -Layout.__index = Layout +--[[ HStack ]]-- -function Layout:new(...) - local stretch = Stretch(1, 1) - local min_size = MinSize(0, 0) - local children = {} +function HStack:new(...) + self.frame = Rect(0, 0, 0, 0) + self.spacing = attr.Spacing(0) + self.children = {} + self.size = math.huge + + for _, arg in pairs({...}) do + if arg.is ~= nil then + if arg:is(attr.Spacing) then + self.spacing = arg + end + else + if type(arg) == 'table' then + for _, child in ipairs(arg) do + assert(child:is(Layout), 'child should be of type Layout') + end - for _, arg in ipairs({...}) do - if getmetatable(arg) == Stretch then - stretch = arg - elseif getmetatable(arg) == MinSize then - min_size = arg - elseif type(arg) == "table" then - children = arg + self.children = arg + end end end - - return setmetatable({ - stretch = stretch, - min_size = min_size, - children = children, - exp_size = nil, - rect = Rect(0, 0, 0, 0), - }, Layout) end -function Layout:reshape(x, y, w, h) - self:expand() - self:layout(Rect(x, y, w, h)) -end +function HStack:layoutChildren() + local fixed_w = math.max(#self.children - 1, 0) * self.spacing.v + local flex_cols = 0 + local col_widths = {} -function Layout:expand() - for _, child in ipairs(self.children) do - child:expand() - end + -- calculate fixed width total and flex column count + for idx, child in ipairs(self.children) do + col_widths[idx] = child.size - self:expandChildren() -end - -function Layout:expandChildren() - error("implementation required by subclasses") -end - -function Layout:layout(rect) - self:layoutChildren(rect) - for _, child in ipairs(self.children) do - child:layout(child.rect) + if child.size ~= math.huge then + fixed_w = fixed_w + child.size + else + local w, h = child:sizeThatFits(child.size, math.huge) + if w == math.huge then + flex_cols = flex_cols + 1 + else + col_widths[idx] = w + fixed_w = fixed_w + w + end + end end -end -function Layout:layoutChildren(rect) - error("implementation required by subclasses") -end + -- calculate flex width for each flex column + local x, y, w, _ = self.frame:unpack() + if fixed_w == math.huge then fixed_w = 0 end + local flex_w = math.floor((w - fixed_w) / flex_cols) -function Layout:__tostring() - return F.describe("Layout", self) -end - -setmetatable(Layout, { - __call = Layout.new, -}) - ---[[ --- Border --- A Border element can be used to add a margin around a child element. The --- Border element should only contain a single child element. ---]] - -local Border = {} -Border.__index = Border - -function Border:new(...) - local args = {...} + -- set column frames and update child layouts + for idx, child in ipairs(self.children) do + local w = col_widths[idx] + if w == math.huge then w = flex_w end - local margin = F.removeMatch(args, function(v) - return getmetatable(v) == Margin - end) - - local this = Layout.new(self, unpack(args)) - - this.margin = margin or Margin(0) + local h = (child.size == math.huge and + child:sizeThatFits(w, child.size) or + self.frame.h + ) + if h == math.huge then h = self.frame.h end - return setmetatable(this, Border) + child.frame = Rect(x, y, w, h) + child:layoutChildren() + x = x + w + self.spacing.v + end end -function Border:expandChildren() - local w, h = self.margin.l + self.margin.r, self.margin.t + self.margin.b +function HStack:update(dt) for _, child in ipairs(self.children) do - w = math.max(w, child.exp_size.x) - h = math.max(h, child.exp_size.y) + child:update(dt) end - self.exp_size = ExpSize(w, h) end -function Border:layoutChildren(rect) - for _, child in ipairs(self.children) do - child.rect = Rect( - rect.x + self.margin.l, - rect.y + self.margin.t, - rect.w - self.margin.l - self.margin.r, - rect.h - self.margin.t - self.margin.b - ) +function HStack:draw() + for _, child in ipairs(self.children) do + child:draw() end end -function Border:__tostring() - return F.describe("Border", self) -end +function HStack:sizeThatFits(w, h) + local cw, ch = 0, 0 -setmetatable(Border, { - __index = Layout, - __call = Border.new, -}) + for _, col in ipairs(self.children) do + if col.size ~= math.huge then + cw = cw + col.size + else + local tw, th = col:sizeThatFits(w, h) + cw = cw + tw + ch = math.max(ch, th) + end + end ---[[ --- HStack --- A HStack element can contain multiple child elements that will be arranged --- horizontally. ---]] + return math.min(w, cw), math.min(ch, h) +end -local HStack = {} -HStack.__index = HStack +function HStack:__tostring() + return F.describe('HStack', self) +end -function HStack:new(...) - local this = Layout.new(self, ...) +--[[ VStack ]]-- - return setmetatable(this, HStack) -end +function VStack:new(...) + self.frame = Rect(0, 0, 0, 0) + self.spacing = attr.Spacing(0) + self.children = {} + self.size = math.huge + + for _, arg in pairs({...}) do + if arg.is ~= nil then + if arg:is(attr.Spacing) then + self.spacing = arg + end + else + if type(arg) == 'table' then + for _, child in ipairs(arg) do + assert(child:is(Layout), 'child should be of type Layout') + end -function HStack:expandChildren() - local w, h = 0, 0 - for _, child in ipairs(self.children) do - w = w + child.exp_size.x - h = math.max(h, child.exp_size.y) + self.children = arg + end + end end - self.exp_size = ExpSize(w, h) end -function HStack:layoutChildren(rect) - local x, y, h = rect.x, rect.y, 0 - local ch_widths = {} - for _, child in ipairs(self.children) do - ch_widths[#ch_widths + 1] = { child.exp_size.x, child.stretch.x } - end - - if #ch_widths > 0 then - ch_widths = F.spread(rect.w, unpack(ch_widths)) - end +function VStack:layoutChildren() + local fixed_h = math.max(#self.children - 1, 0) * self.spacing.v + local flex_rows = 0 + local row_heights = {} - local results = F.zip(self.children, ch_widths) - for _, result in ipairs(results) do - local ch, w = unpack(result) + -- calculate fixed height total and flex row count + for idx, child in ipairs(self.children) do + row_heights[idx] = child.size - if ch.stretch.y == 1 then - h = rect.h + if child.size ~= math.huge then + fixed_h = fixed_h + child.size else - h = ch.exp_size.y + local w, h = child:sizeThatFits(math.huge, child.size) + if h == math.huge then + flex_rows = flex_rows + 1 + else + row_heights[idx] = h + fixed_h = fixed_h + h + end end - ch.rect = Rect(x, y, w - 1, h) - - x = x + w end -end - -function HStack:__tostring() - return F.describe("HStack", self) -end -setmetatable(HStack, { - __index = Layout, - __call = HStack.new -}) + -- calculate flex height for each flex row + local x, y, _, h = self.frame:unpack() + local flex_h = math.floor((h - fixed_h) / flex_rows) ---[[ --- VStack --- A VStack element can contain multiple child elements that will be arranged --- vertically. ---]] + -- set row frames and update child layouts + for idx, child in ipairs(self.children) do + local h = row_heights[idx] + if h == math.huge then h = flex_h end -local VStack = {} -VStack.__index = VStack + local w = (child.size == math.huge and + child:sizeThatFits(child.size, h) or + self.frame.w + ) + if w == math.huge then w = self.frame.w end -function VStack:new(...) - local this = Layout.new(self, ...) + child.frame = Rect(x, y, w, h) + child:layoutChildren() + y = y + h + self.spacing.v + end - return setmetatable(this, VStack) + print() end -function VStack:expandChildren() - local w, h = 0, 0 +function VStack:update(dt) for _, child in ipairs(self.children) do - w = math.max(w, child.exp_size.x) - h = h + child.exp_size.y + child:update(dt) end - self.exp_size = ExpSize(w, h - 1) end -function VStack:layoutChildren(rect) - local x, y, w = rect.x, rect.y, 0 - local ch_heights = {} +function VStack:draw() for _, child in ipairs(self.children) do - ch_heights[#ch_heights + 1] = { child.exp_size.y, child.stretch.y } - end - - if #ch_heights > 0 then - ch_heights = F.spread(rect.h, unpack(ch_heights)) + child:draw() end +end - local results = F.zip(self.children, ch_heights) - for _, result in ipairs(results) do - local ch, h = unpack(result) +function VStack:sizeThatFits(w, h) + local rw, rh = 0, 0 - if ch.stretch.x ~= 0 then - w = rect.w - else - w = ch.exp_size.x + for _, row in ipairs(self.children) do + if row.size ~= math.huge then + rh = rh + row.size + elseif row.child then + local tw, th = row.child:sizeThatFits(w, h) + rh = rh + th + rw = math.max(rw, tw) end - ch.rect = Rect(x, y, w, h - 1) - - y = y + h end + + return math.min(w, rw), math.min(rh, h) end function VStack:__tostring() - return F.describe("VStack", self) + return F.describe('VStack', self) end -setmetatable(VStack, { - __index = Layout, - __call = VStack.new, -}) +--[[ LAYOUT ]]-- ---[[ --- Elem --- An Elem element can be contained in a VStack, HStack or Border. A custom --- widget can be assigned to an element. ---]] +function Layout:new(...) + self.size = math.huge + self.frame = Rect(0, 0, 0, 0) + self.child = nil + self.color = F.randomColor() + self.margin = attr.Margin(0) -local Elem = {} -Elem.__index = Elem + for _, arg in ipairs({...}) do + if arg == nil then goto continue end + + if type(arg) == 'number' then + self.size = arg + elseif arg.is ~= nil then + if arg:is(attr.Margin) then + self.margin = arg + else + self.child = arg + end + end -function Elem:new(widget, ...) - local args = { ... } + ::continue:: + end +end - local id = F.removeMatch(args, function(v) - return getmetatable(v) == ID - end) +function Layout:resize(w, h) + self.frame = Rect(self.frame.x, self.frame.y, w, h):inset(self.margin:unpack()) + self:layoutChildren() +end - local this = Layout.new(self, unpack(args)) - - this.id = id - this.rect = Rect(0, 0, 0, 0) - this.widget = widget +function Layout:layoutChildren() + self.child.frame = self.frame + self.child:layoutChildren() +end - return setmetatable(this, Elem) +function Layout:update(dt) + self.child:update(dt) end -function Elem:expandChildren() - self.exp_size = self.min_size +function Layout:draw() + -- local l = love.graphics.getLineWidth() + -- love.graphics.setColor(unpack(self.color)) + -- local x, y, w, h = self.frame:unpack() + -- love.graphics.rectangle('line', x + l / 2, y + l / 2, math.max(w - l, 0), math.max(h - l, 0)) + + self.child:draw() end -function Elem:layoutChildren(rect) - -- body +function Layout:__tostring() + return F.describe('Layout', self) end -function Elem:__tostring() - return F.describe("Elem", self) +--[[ ELEM ]]-- + +function Elem:layoutChildren() + if self.child then + if self.child:is(Layout) then + self.child.frame = self.frame + self.child:layoutChildren() + else + -- either Control or Dummy + local x, y, w, h = self.frame:unpack() + self.child:setFrame(x, y, w, h) + end + end +end + +function Elem:sizeThatFits(w, h) + return self.child:sizeThatFits(w, h) end -setmetatable(Elem, { - __index = Layout, - __call = Elem.new, -}) +function Elem:__tostring() + return F.describe('Elem', self) +end ---[[ --- The module ---]] +--[[ MODULE ]]-- return { - Rect = Rect, - Border = Border, - HStack = HStack, + Layout = Layout, VStack = VStack, + HStack = HStack, Elem = Elem, -} \ No newline at end of file +} diff --git a/composer/loader.lua b/composer/loader.lua index 1873488..c3b7721 100644 --- a/composer/loader.lua +++ b/composer/loader.lua @@ -1,65 +1,42 @@ -local _PATH = (...):match("(.-)[^%.]+$") -local layout = require(_PATH .. "layout") -local Elem = layout.Elem +local PATH = (...):match('(.-)[^%.]+$') +local controls = require(PATH .. 'controls') -local ATTRIBUTE_IMPORTS = [[ +local ATTR_IMPORTS = [[ local Margin = attr.Margin -local Stretch = attr.Stretch -local MinSize = attr.MinSize -local ID = attr.ID +local Spacing = attr.Spacing ]] local LAYOUT_IMPORTS = [[ -local Border = layout.Border -local VStack = layout.VStack +local Layout = layout.Layout local HStack = layout.HStack +local Col = layout.Col +local VStack = layout.VStack +local Row = layout.Row local Elem = layout.Elem ]] +local CONTROL_IMPORTS = [[ +local Button = controls.Button +local Label = controls.Label +local ImageButton = controls.ImageButton +local Checkbox = controls.Checkbox +local Slider = controls.Slider +local Progress = controls.Progress +local Input = controls.Input +local Space = controls.Space +]] + -- this pattern matches the full component directive with square hooks -local PATH_DIRECTIVE_PATTERN = "%[%[.-%]%]" +local PATH_DIRECTIVE_PATTERN = '%[%[.-%]%]' -- ignore commented out directives -local PATH_DIRECTIVE_EXCLUDE_PATTERN = "%-%-%s-%[%[.-%]%]" +local PATH_DIRECTIVE_EXCLUDE_PATTERN = '%-%-%s-%[%[.-%]%]' -- this pattern is used to capture the path for a directive pattern match -local PATH_CAPTURE_PATTERN = "\"(.-)\"" - --- all required custom controls are stored in this registry -local registry = {} - --- internal function to recursively retrieve a list of elements from a parent --- element -local function getElementsAndWidgets(parent, elements, widgets) - elements = elements or {} - widgets = widgets or {} - - for _, child in ipairs(parent.children) do - if getmetatable(child) == layout.Elem then - elements[#elements + 1] = child - if child.widget ~= nil then - widgets[#widgets + 1] = child.widget - end - else - getElementsAndWidgets(child, elements, widgets) - end - end - - return elements, widgets -end - --- add widgets at given path to the internal control registry -local function require(path) - registry[path] = true -end - --- remove widgets at given path from the internal control registry -local function unrequire(path) - registry[path] = nil -end +local PATH_CAPTURE_PATTERN = '\"(.-)\"' -- internal function to recursively load components from a file at path local function loadComponent(path) - if love.filesystem.getInfo(path, "file") == nil then - error("file does not exist: " .. path) + if love.filesystem.getInfo(path, 'file') == nil then + error('file does not exist: ' .. path) end local contents, _ = love.filesystem.read(path) @@ -70,7 +47,7 @@ local function loadComponent(path) local exc_match_s, exc_match_e = string.find(contents, PATH_DIRECTIVE_EXCLUDE_PATTERN) local ignore = exc_match_e == match_e and exc_match_s < match_s - local component_text = "" + local component_text = '' if not ignore then local component_path = string.sub(contents, match_s, match_e, 1) local component_path = string.match(component_path, PATH_CAPTURE_PATTERN) @@ -83,101 +60,62 @@ local function loadComponent(path) return contents end +local function getControls(element, control_list) + local control_list = control_list or {} + + if element:is(controls.Control) then + control_list[#control_list + 1] = element + else + if element.child then + return getControls(element.child, control_list) + elseif element.children then + for _, child in ipairs(element.children) do + getControls(child, control_list) + end + end + end + + return control_list +end + -- load a layout file at given path; optionally set debug to true to log the -- full content including engine imports and required imports local function load(path, is_debug) + -- first load the layout file local contents = loadComponent(path) - local attr_path = _PATH .. "attributes" - local layout_path = _PATH .. "layout" - - local imports = { - "--[[ " .. attr_path .. " ]]--", - "local attr = require \"" .. attr_path .. "\"", - ATTRIBUTE_IMPORTS, - "--[[ " .. layout_path .. " ]]--", - "local layout = require \"" .. layout_path .. "\"", + -- combine layout file with composer modules into one file + local contents = table.concat({ + 'local layout = require "' .. PATH .. 'layout"', LAYOUT_IMPORTS, + 'local controls = require "' .. PATH .. 'controls"', + CONTROL_IMPORTS, + 'local attr = require "' .. PATH .. 'attributes"', + ATTR_IMPORTS, + contents, + }, '\n') + + -- log file if debug flag is set to true + if is_debug == true then print(contents) end + + -- load combined file + local ui = loadstring(contents)() + + -- get list of controls + local controls = getControls(ui) + + -- return a facade + return { + draw = function() ui:draw() end, + update = function(dt) ui:update(dt) end, + getControl = function() end, + eachControl = function(fn) + for _, control in ipairs(controls) do + fn(control) + end + end, + resize = function(w, h) ui:resize(w, h) end } - - for path, _ in pairs(registry) do - imports[#imports + 1] = "--[[ " .. path .. " ]]--" - imports[#imports + 1] = love.filesystem.read(path) - end - - imports[#imports + 1] = "--[[ " .. path .. " ]]--" - imports[#imports + 1] = "return " .. contents - - contents = table.concat(imports, "\n\n") - - if is_debug == true then - print(contents) - end - - local hud_contents = loadstring(contents) - local hud = hud_contents() - - -- create a list of elements for use with the eachElement() function - -- create a list of wdgets - local elements, widgets = getElementsAndWidgets(hud) - - hud.resize = function(w, h, fn) - fn = fn or function() end - - hud:reshape(0, 0, w, h) - for _, e in ipairs(elements) do - fn(e) - end - end - - hud.update = function(dt) - for _, widget in ipairs(widgets) do - widget.update(dt) - end - end - - hud.draw = function() - for _, widget in ipairs(widgets) do - widget.draw() - end - end - - -- create a table of elements by id for use with the getElement() function - local elements_by_id = {} - for _, element in ipairs(elements) do - if element.id ~= nil then - elements_by_id[element.id.value] = element - end - end - - hud.getElement = function(id, fn) - local e = elements_by_id[id] - if e then fn(e) end - end - - hud.eachElement = function(fn) - for _, e in ipairs(elements) do - fn(e) - end - end - - hud.getWidget = function(element_id, fn) - local e = elements_by_id[element_id] - if e and e.widget ~= nil then fn(e.widget) end - end - - hud.eachWidget = function(fn) - for _, w in ipairs(widgets) do - fn(w) - end - end - - return hud end --- The module -return { - require = require, - unrequire = unrequire, - load = load, -} \ No newline at end of file +return load diff --git a/composer/rect.lua b/composer/rect.lua new file mode 100644 index 0000000..88464a4 --- /dev/null +++ b/composer/rect.lua @@ -0,0 +1,95 @@ +-- based on the RectCut algorithm, but adjusted for easy integration with LÖVE +-- https://halt.software/dead-simple-layouts/ + +local Rect = {} +Rect.__index = Rect + +function Rect.new(x, y, w, h) + assert(w >= 0 and h >= 0, 'width and height cannot be negative') + + return setmetatable({ + x = x, + y = y, + w = w, + h = h, + }, Rect) +end + +-- cut out the left of the rect, returning the left piece and modifying the +-- original rect +function Rect:cutLeft(a) + local x = self.x + self.x = self.x + a + self.w = math.max(self.w - a, 0) + return Rect(x, self.y, a, self.h) +end + +-- cut out the right of the rect, returning the right piece and modifying the +-- original rect +function Rect:cutRight(a) + local x = self.x + self.w + self.w = math.max(self.w - a, 0) + return Rect(x - a , self.y, a, self.h) +end + +-- cut out the top of the rect, returning the top piece and modifying the +-- original rect +function Rect:cutTop(a) + print('cutTop', a) + + local y = self.y + self.y = self.y + a + self.h = math.max(self.h - a, 0) + return Rect(self.x, y, self.w, a) +end + +-- cut out the bottom of the rect, returning the bottom piece and modifying the +-- original rect +function Rect:cutBottom(a) + print('cutBottom', a) + + local y = self.y + self.h + self.h = math.max(self.h - a, 0) + return Rect(self.x, y - a, self.w, a) +end + +-- cut out the left of the rect, leaving the original unmodified +function Rect:getLeft(a) + return Rect(self.x, self.y, a, self.h) +end + +-- cut out the right of the rect, leaving the original unmodified +function Rect:getRight(a) + return Rect(self.x + self.w - a , self.y, a, self.h) +end + +-- cut out the top of the rect, leaving the original unmodified +function Rect:getTop(a) + return Rect(self.x, self.y, self.w, a) +end + +-- cut out the bottom of the rect, leaving the original unmodified +function Rect:getBottom(a) + return Rect(self.x, self.y + self.h - a, self.w, a) +end + +-- inset rect, leaving the original unmodified +function Rect:inset(top, left, bottom, right) + left = left or top + bottom = bottom or top + right = right or left + return Rect(self.x + left, self.y + top, self.w - (right + left), self.h - (bottom + top)) +end + +-- return x, y, w & h +function Rect:unpack() + return self.x, self.y, self.w, self.h +end + +function Rect:__tostring() + return 'Rect { x = ' .. self.x .. ', y = ' .. self.y .. ', w = ' .. self.w .. ', h = ' .. self.h .. ' }' +end + +return setmetatable(Rect, { + __call = function(_, ...) return Rect.new(...) end, +}) diff --git a/composer/theme.lua b/composer/theme.lua new file mode 100644 index 0000000..c5a5ea7 --- /dev/null +++ b/composer/theme.lua @@ -0,0 +1,18 @@ +return { + normal = { + fg = { 0.9, 0.9, 0.9, 1.0 }, + bg = { 0.4, 0.4, 0.4, 1.0 }, + }, + hovered = { + fg = { 1.0, 0.7, 0.1, 1.0 }, + bg = { 0.6, 0.4, 0.1, 1.0 }, + }, + active = { + fg = { 0.0, 1.0, 1.0, 1.0 }, + bg = { 0.0, 0.6, 0.6, 1.0 }, + }, + disabled = { + fg = { 0.9, 0.9, 0.9, 0.5 }, + bg = { 0.4, 0.4, 0.4, 0.5 }, + }, +} diff --git a/examples/example1.lua b/examples/example1.lua index a54530b..5afbca8 100644 --- a/examples/example1.lua +++ b/examples/example1.lua @@ -1,12 +1,114 @@ -Border(Margin(0), { - VStack({ - Label("Hello", MinSize(0, 50), Stretch(1, 0), ID("test1")), - Button("press G to see next layout", ID("test2")), - Label("...", ID("test3")), - HStack(MinSize(0, 80), Stretch(1, 0), { - Button("Composer", MinSize(120, 50), Stretch(0, 1)), - Label("Says", MinSize(40), Stretch(1)), - Button("Hello", MinSize(50), Stretch(0, 1)), - }) - }), -}) \ No newline at end of file +-- hovered, active, normal, disabled + +local function makeButton(title, is_enabled) + local button = Button { + text = title, + font = { 12 }, + corner_radius = 3, + } + + -- button:setEnabled(is_enabled ~= false) + + return button +end + +local function makeImageButton(image_path) + return ImageButton { + image = image_path, + corner_radius = 0, + } +end + +local function makeCheckbox() + return Checkbox { corner_radius = 3, checked = true } +end + +local function makeLabel(text) + return Label { text = text, font = { 16 }, align = 'left' } +end + +local function makeInput(text) + return Input { corner_radius = 3, text = text, font = { 20 }, align = 'left' } +end + +local function makeSlider() + return Slider { corner_radius = 3, value = 3, step = 10 } +end + +local function makeProgress() + return Progress { corner_radius = 3, value = 0.1 } +end + +local function makeSpace() + return Space() +end +--[[ +if row no height: + if row no children: fill + if row children: calc total height of children & make row same size +else use row height + +if col no height + if col no children: fill + if col children: calc total width of children and make col same size +else use col height + +]] +return Layout(Margin(10), VStack({ + makeSpace(), + makeButton('HELLO'), + HStack({ + makeSpace(), + VStack(Spacing(10), { + makeLabel('HELLO HERE I AM'), + makeButton('HELLO'), + makeButton('HERE'), + makeButton('WE ARE'), + }), + makeSpace(), + }), + makeSpace(), + -- makeButton('BUTTON'), + -- makeLabel('HI'), + -- makeButton('BUTTON'), + -- makeSpace(), + --[[ + Elem(), + Elem(HStack(Spacing(10), { + Elem(), + Elem(makeButton('BUTTON')), + Elem(), + })), + Elem(HStack(Spacing(10), { + Elem(), + Elem(makeCheckbox()), + Elem(), + })), + Elem(HStack(Spacing(10), { + Elem(), + Elem(makeLabel('Label')), + Elem(), + })), + Elem(HStack(Spacing(10), { + Elem(), + Elem(makeImageButton('assets/sparkling-sabre.png')), + Elem(), + })), + Elem(HStack { + Elem(), + Elem(makeSlider()), + Elem(), + }), + Elem(HStack { + Elem(), + Elem(makeInput('blaat')), + Elem(), + }), + Elem(HStack { + Elem(), + Elem(makeProgress()), + Elem(), + }), + Elem(), + --]] +})) diff --git a/examples/example2.lua b/examples/example2.lua index e256271..9be0f9e 100644 --- a/examples/example2.lua +++ b/examples/example2.lua @@ -1,8 +1,18 @@ -Border(Margin(0), { - VStack({ - Label("TOP", MinSize(0, 50), Stretch(1, 0), ID("test1")), - Button("press G to see next layout", ID("test2")), - Label("BOTTOM", ID("test3")), - [[ "examples/shared.lua" ]], - }), -}) \ No newline at end of file +local function makeButton(image_path) + return ImageButton { + image = image_path, + corner_radius = 30, + } +end + +return Layout(Margin(10), Rows { + Row(80, Cols(Spacing(1), { + Col(Label { text = 'MENU', font = { 'assets/agricola-rough.otf', 20 }, align = 'center' }), + Col(), + Col(80, makeButton('assets/crossbow.png')), + Col(80, makeButton('assets/sparkling-sabre.png')), + Col(80, makeButton('assets/switch-weapon.png')), + })), + Row(), + Row(80), +}) diff --git a/examples/example3.lua b/examples/example3.lua index 01d3d21..1e7e1d6 100644 --- a/examples/example3.lua +++ b/examples/example3.lua @@ -1,20 +1,13 @@ -Border(Margin(40, 40, 0, 0), { - VStack({ - Button("TOP", MinSize(100, 50), Stretch(1, 0)), - [[ "examples/shared.lua" ]], - Label("Hello", MinSize(100, 50), Stretch(1, 0)), - FlexibleSpace(), - Label("press G to see next layout", MinSize(100, 50), Stretch(1, 0)), - [[ "examples/shared.lua" ]], - Button("...", ID("button"), MinSize(100, 50), Stretch(1, 0)), - HStack(Stretch(1, 0), { - FlexibleSpace(), - ImageButton("gfx/ankh.png", MinSize(60), Stretch(0)), - ImageButton("gfx/crossed-swords.png", MinSize(60), Stretch(0)), - ImageButton("gfx/high-shot.png", MinSize(60), Stretch(0)), - ImageButton("gfx/cultist.png", MinSize(60), Stretch(0)), - ImageButton("gfx/hand.png", MinSize(60), Stretch(0)), - FlexibleSpace(), - }) - }) -}) \ No newline at end of file +return Layout(Rows { + Row(), + Row(400, Cols { + Col(), + Col(400, Rows(Spacing(10), { + Row(100, Button { text = 'BUTTON 1', font = 20 }), + Row(100, Button { text = 'BUTTON 2', font = 20 }), + Row(100, Button { text = 'BUTTON 3', font = 20 }), + })), + Col(), + }), + Row(), +}) diff --git a/examples/example4.lua b/examples/example4.lua new file mode 100644 index 0000000..dc7a634 --- /dev/null +++ b/examples/example4.lua @@ -0,0 +1,20 @@ +local function makeButton(text) + return Button { + text = text, + font = { 'assets/agricola-rough.otf', 24 }, + corner_radius = 10, + } +end + +return Layout(Cols { + Col(), + Col(300, Rows(Spacing(8), { + Row(Label { text = 'MY AWESOME GAME', font = { 'assets/agricola-rough.otf', 48 }, align = 'center' }), + Row(70, makeButton('NEW GAME')), + Row(70, makeButton('CONTINUE')), + Row(70, makeButton('SETTINGS')), + Row(70, makeButton('QUIT')), + Row(), + })), + Col(), +}) diff --git a/examples/example5.lua b/examples/example5.lua new file mode 100644 index 0000000..a1b63c1 --- /dev/null +++ b/examples/example5.lua @@ -0,0 +1,25 @@ +local function makeLabel(text) + return Label { text = text, font = { 16 }, align = 'left' } +end + +local function makeButton(text) + return Button { + text = text, + font = { 'assets/agricola-rough.otf', 24 }, + corner_radius = 10, + } +end + +return Layout(Rows { + Row(), + Row(Rows { + Row(makeButton('BLAAT')), + Row(100, Rows { + Row(), + Row(makeButton('BOTTOM')), + Row(), + }), + Row(makeLabel('HI')), + }), + Row(), +}) diff --git a/examples/example6.lua b/examples/example6.lua new file mode 100644 index 0000000..f6bea74 --- /dev/null +++ b/examples/example6.lua @@ -0,0 +1,21 @@ +local function makeLabel(text) + return Label { text = text, font = { 16 }, align = 'left' } +end + +local function makeButton(text) + return Button { + text = text, + font = { 'assets/agricola-rough.otf', 24 }, + corner_radius = 10, + } +end + +return Layout(Cols { + Col(), + Col(Cols { + Col(makeButton('BLAAT')), + Col(500), + Col(makeLabel('HI')), + }), + Col(), +}) diff --git a/examples/shared.lua b/examples/shared.lua index b757fdb..880a6a4 100644 --- a/examples/shared.lua +++ b/examples/shared.lua @@ -1,11 +1,13 @@ -HStack(Stretch(1, 0), { - FlexibleSpace(), - Button("1", MinSize(30), Stretch(0)), - Button("2", MinSize(30), Stretch(0)), - Button("3", MinSize(30), Stretch(0)), - Button("4", MinSize(30), Stretch(0)), - Button("5", MinSize(30), Stretch(0)), - Button("6", MinSize(30), Stretch(0)), - Button("7", MinSize(30), Stretch(0)), - FlexibleSpace(), -}) \ No newline at end of file +Rows({ -- should perhaps be a Layout instead as root? + Row(30, Cols { + Col(), + Col(30, Button({ 'text' = '1' })), + Col(30, Button({ 'text' = '2' })), + Col(30, Button({ 'text' = '3' })), + Col(30, Button({ 'text' = '4' })), + Col(30, Button({ 'text' = '5' })), + Col(30, Button({ 'text' = '6' })), + Col(30, Button({ 'text' = '7' })), + Col(), + }) +}) diff --git a/gfx/ankh.png b/gfx/ankh.png deleted file mode 100644 index 5355819..0000000 Binary files a/gfx/ankh.png and /dev/null differ diff --git a/gfx/backpack.png b/gfx/backpack.png deleted file mode 100644 index 4df568d..0000000 Binary files a/gfx/backpack.png and /dev/null differ diff --git a/gfx/backstab.png b/gfx/backstab.png deleted file mode 100644 index 4bdb645..0000000 Binary files a/gfx/backstab.png and /dev/null differ diff --git a/gfx/cog.png b/gfx/cog.png deleted file mode 100644 index b94f57c..0000000 Binary files a/gfx/cog.png and /dev/null differ diff --git a/gfx/conversation.png b/gfx/conversation.png deleted file mode 100644 index 1fdd1b6..0000000 Binary files a/gfx/conversation.png and /dev/null differ diff --git a/gfx/crossed-swords.png b/gfx/crossed-swords.png deleted file mode 100644 index 1bf59bc..0000000 Binary files a/gfx/crossed-swords.png and /dev/null differ diff --git a/gfx/cultist.png b/gfx/cultist.png deleted file mode 100644 index b1ceb6f..0000000 Binary files a/gfx/cultist.png and /dev/null differ diff --git a/gfx/death-skull.png b/gfx/death-skull.png deleted file mode 100644 index 17b1e3e..0000000 Binary files a/gfx/death-skull.png and /dev/null differ diff --git a/gfx/fairy-wand.png b/gfx/fairy-wand.png deleted file mode 100644 index a3567ba..0000000 Binary files a/gfx/fairy-wand.png and /dev/null differ diff --git a/gfx/hand.png b/gfx/hand.png deleted file mode 100644 index d96569a..0000000 Binary files a/gfx/hand.png and /dev/null differ diff --git a/gfx/high-shot.png b/gfx/high-shot.png deleted file mode 100644 index 45c52c2..0000000 Binary files a/gfx/high-shot.png and /dev/null differ diff --git a/gfx/holy-hand-grenade.png b/gfx/holy-hand-grenade.png deleted file mode 100644 index 41ba7ea..0000000 Binary files a/gfx/holy-hand-grenade.png and /dev/null differ diff --git a/gfx/holy-symbol.png b/gfx/holy-symbol.png deleted file mode 100644 index b965d4d..0000000 Binary files a/gfx/holy-symbol.png and /dev/null differ diff --git a/gfx/magic-potion.png b/gfx/magic-potion.png deleted file mode 100644 index 3064ab5..0000000 Binary files a/gfx/magic-potion.png and /dev/null differ diff --git a/gfx/magnifying-glass.png b/gfx/magnifying-glass.png deleted file mode 100644 index b3ea148..0000000 Binary files a/gfx/magnifying-glass.png and /dev/null differ diff --git a/gfx/night-sleep.png b/gfx/night-sleep.png deleted file mode 100644 index 1cce2d4..0000000 Binary files a/gfx/night-sleep.png and /dev/null differ diff --git a/gfx/scroll-unfurled.png b/gfx/scroll-unfurled.png deleted file mode 100644 index b6661c3..0000000 Binary files a/gfx/scroll-unfurled.png and /dev/null differ diff --git a/gfx/skills.png b/gfx/skills.png deleted file mode 100644 index 1b54a40..0000000 Binary files a/gfx/skills.png and /dev/null differ diff --git a/gfx/snatch.png b/gfx/snatch.png deleted file mode 100644 index d118f44..0000000 Binary files a/gfx/snatch.png and /dev/null differ diff --git a/gfx/spell-book.png b/gfx/spell-book.png deleted file mode 100644 index 84652b3..0000000 Binary files a/gfx/spell-book.png and /dev/null differ diff --git a/gfx/switch-weapon.png b/gfx/switch-weapon.png deleted file mode 100644 index 9a1959b..0000000 Binary files a/gfx/switch-weapon.png and /dev/null differ diff --git a/main.lua b/main.lua index 58f4e16..035a2c7 100644 --- a/main.lua +++ b/main.lua @@ -1,6 +1,6 @@ io.stdout:setvbuf('no') -- show debug output live in SublimeText console -local composer = require "composer" +local composer = require 'composer' local layout_idx = 0 local layout = nil @@ -8,31 +8,30 @@ local layout = nil local window_w = 0 local window_h = 0 +local scale = 1.0 + local function updateLayout() local is_debug = true layout_idx = layout_idx + 1 - if layout_idx > 3 then layout_idx = 1 end + if layout_idx > 6 then layout_idx = 1 end - local path = "examples/example" .. layout_idx .. ".lua" - print("loading:", path) + local path = 'examples/example' .. layout_idx .. '.lua' + print('loading: ', path) layout = composer.load(path, is_debug) - layout.getElement("test1", function(e) - e.widget.setText("this text is changed using the ID") + layout.getControl('test1', function(e) + e.widget.setText('this text is changed using the ID') end) - layout.getElement("button", function(e) - e.widget.setTitle("these colors are changed using the ID") + layout.getControl('button', function(e) + e.widget.setTitle('these colors are changed using the ID') e.widget.setColors({ 0.5, 0.0, 0.0 }, { 0.0, 0.5, 0.0, 0.5 }) end) end local function resizeLayout() - layout.resize(window_w, window_h, function(e) - e.widget.setFrame(e.rect.x, e.rect.y, e.rect.w, e.rect.h) - print(e) - end) + layout.resize(window_w / scale, window_h / scale) end function love.load(args) @@ -41,6 +40,7 @@ function love.load(args) resizable = true, minwidth = desktop_w * 0.4, minheight = desktop_h * 0.5, + msaa = 8, } love.window.setMode(desktop_w * 0.6, desktop_h * 0.7, flags) @@ -54,33 +54,43 @@ function love.load(args) return love.keyboard.released[key] == true end + math.randomseed(os.time()) + love.graphics.setLineWidth(2) + + -- required for composer to hook up updates for mouse position + composer.init() + -- add custom controls to the layout engine loader - composer.require("widgets/widgets.lua") updateLayout() window_w, window_h = love.window.getMode() resizeLayout() - love.window.setTitle("Composer v" .. composer._VERSION) - ---[[ print("\n\n") - for k, v in pairs(_G.package.loaded) do - print(k, v) - end - print("\n\n") ---]] + love.window.setTitle('Composer v' .. composer._VERSION) end function love.update(dt) + -- we only need to update mouse position manually if scale is not equal to 1 + composer.updateMouse( + love.mouse.getX() / scale, + love.mouse.getY() / scale, + love.mouse.isDown(1) + ) + layout.update(dt) end function love.draw() + love.graphics.push() + love.graphics.scale(scale) + -- draw white background - love.graphics.setColor(1.0, 1.0, 1.0) - love.graphics.rectangle("fill", 0, 0, window_w, window_h) + love.graphics.setColor(0.0, 0.0, 0.0) + love.graphics.rectangle('fill', 0, 0, window_w, window_h) - layout:draw() + layout.draw() + + love.graphics.pop() end function love.resize(w, h) @@ -88,11 +98,11 @@ function love.resize(w, h) resizeLayout() end -function love.keypressed(key, code) - if key == "g" then +function love.keypressed(key) + if key == 'tab' then updateLayout() local window_w, window_h = love.window.getMode() resizeLayout(window_w, window_h) end -end \ No newline at end of file +end diff --git a/widgets/ui.lua b/widgets/ui.lua deleted file mode 100644 index 67b2a15..0000000 --- a/widgets/ui.lua +++ /dev/null @@ -1,285 +0,0 @@ -local imageButton = function(image_path, fg_color, bg_color, ix, iy) - local frame = { 0, 0, 0, 0 } - local bg_color = bg_color or { 0.0, 0.0, 0.0, 0.0 } - local fg_color = fg_color or { 1.0, 1.0, 1.0, 1.0 } - local drawable = love.graphics.newImage(image_path) - - local image_w, image_h = drawable:getDimensions() - local sx, sy = 1, 1 - local ix = ix or 0 - local iy = iy or ix - - local is_highlight = false - - local setFrame = function(x, y, w, h) - w, h = w - 1, h - 1 - frame = { x, y, w, h } - sx = (w - ix) / image_w - sy = (h - iy) / image_h - end - - local update = function(dt) - local mouse_x, mouse_y = love.mouse.getPosition() - local x, y, w, h = unpack(frame) - - is_highlight = ( - mouse_x > x and mouse_x < x + w and - mouse_y > y and mouse_y < y + h - ) - end - - local draw = function() - local x, y, w, h = unpack(frame) - - local fg_color = is_highlight and { 0.0, 1.0, 1.0 } or fg_color - - love.graphics.setColor(unpack(bg_color)) - love.graphics.rectangle("fill", x, y, w, h) - - -- draw the drawable - love.graphics.setColor(unpack(fg_color)) - love.graphics.draw( - drawable, - x + math.floor(ix / 2), - y + math.floor(iy / 2), - 0, - sx, - sy - ) - - love.graphics.rectangle("line", x, y, w, h) - end - - return { - setFrame = setFrame, - update = update, - draw = draw, - } -end - -local label = function(text, fg_color, bg_color) - local frame = { 0, 0, 0, 0 } - local fg_color = fg_color or { 1.0, 1.0, 1.0, 1.0 } - local bg_color = bg_color or { 1.0, 1.0, 1.0, 0.0 } - local text = text or "" - - local font = love.graphics.getFont() - local text_w = font:getWidth(text) - local text_h = font:getHeight() - - local setFrame = function(x, y, w, h) - frame = { x, y, w - 1, h - 1 } - end - - local setText = function(new_text) - text = new_text or "" - text_w = font:getWidth(text) - text_h = font:getHeight() - end - - local setFont = function(new_font) - font = new_font - text_w = font:getWidth(text) - text_h = font:getHeight() - end - - local update = function(dt) - -- body - end - - local draw = function() - local x, y, w, h = unpack(frame) - - -- draw background - love.graphics.setColor(unpack(bg_color)) - love.graphics.rectangle("fill", x, y, w, h) - - -- draw text - love.graphics.setFont(font) - love.graphics.setColor(unpack(fg_color)) - love.graphics.print( - text, - math.floor(x + w / 2), - math.floor(y + h / 2), - 0, 1, 1, - math.ceil(text_w / 2), - math.ceil(text_h / 2) - ) - end - - return { - setFrame = setFrame, - update = update, - draw = draw, - -- - setText = setText, - setFont = setFont, - } -end - -local button = function(title, fg_color, bg_color) - local frame = { 0, 0, 0, 0 } - local fg_color = fg_color or { 0.0, 0.0, 0.0, 1.0 } - local bg_color = bg_color or { 1.0, 1.0, 1.0, 1.0 } - local title = title or "" - - local font = love.graphics.getFont() - local text_w = font:getWidth(title) - local text_h = font:getHeight() - - local is_highlight = false - - local setFrame = function(x, y, w, h) - frame = { x, y, w, h } - end - - local setTitle = function(new_title) - title = new_title or title - text_w = font:getWidth(title) - end - - local setColors = function(new_fg_color, new_bg_color) - fg_color = new_fg_color or fg_color - bg_color = new_bg_color or bg_color - end - - local update = function(dt) - local mouse_x, mouse_y = love.mouse.getPosition() - local x, y, w, h = unpack(frame) - - is_highlight = ( - mouse_x > x and mouse_x < x + w and - mouse_y > y and mouse_y < y + h - ) - end - - local draw = function() - local x, y, w, h = unpack(frame) - - local fg_color = is_highlight and { 0.0, 1.0, 1.0 } or fg_color - - -- draw background - love.graphics.setColor(unpack(bg_color)) - love.graphics.rectangle("fill", x, y, w, h) - - -- draw title - love.graphics.setColor(unpack(fg_color)) - love.graphics.print( - title, - math.floor(x + w / 2), - math.floor(y + h / 2), - 0, 1, 1, - math.ceil(text_w / 2), - math.ceil(text_h / 2) - ) - end - - return { - setTitle = setTitle, - setColors = setColors, - setFrame = setFrame, - update = update, - draw = draw, - } -end - -local spacing = function() - return { - setFrame = function() end, - update = function() end, - draw = function() end, - } -end - -local textView = function(fg_color, bg_color) - local frame = { 0, 0, 0, 0 } - local bg_color = bg_color or { 0.0, 0.0, 0.0, 0.8 } - local fg_color = fg_color or { 1.0, 1.0, 1.0, 1.0 } - local btn_size = 30 - - local font = love.graphics.getFont() - local text_h = font:getHeight() - local text_margin = 10 - - local line_buffer = {} - - local button_up = imageButton( - "gfx/controls/arrow_up.png", - fg_color, - bg_color, - 8 - ) - - local button_down = imageButton( - "gfx/controls/arrow_down.png", - fg_color, - bg_color, - 8 - ) - - local addText = function(text) - line_buffer[#line_buffer + 1] = love.graphics.newText(font, text) - while #line_buffer > 5 do - table.remove(line_buffer, 1) - end - end - - local setFont = function(new_font) - font = new_font - text_h = font:getHeight() - end - - local setFrame = function(x, y, w, h) - frame = { x, y, w - 1, h - 1 } - - btn_size = math.min(h / 2, 24) - local btn_y = y + h - btn_size * 2 - local btn_x = x + w - btn_size - - button_up.setFrame(btn_x, btn_y, btn_size, btn_size) - button_down.setFrame(btn_x, btn_y + btn_size, btn_size, btn_size) - end - - local update = function(dt) - button_up.update(dt) - button_down.update(dt) - end - - local draw = function() - local x, y, w, h = unpack(frame) - - local bg_w = w - btn_size - - love.graphics.setColor(unpack(bg_color)) - love.graphics.rectangle("fill", x, y, bg_w, h) - - button_up.draw() - button_down.draw() - - love.graphics.setColor(unpack(fg_color)) - love.graphics.rectangle("line", x, y, bg_w, h) - - x, y = x + text_margin, y + text_margin - for _, line in ipairs(line_buffer) do - love.graphics.draw(line, x + 0.5, y + 0.5) - y = y + text_h - end - end - - return { - setFrame = setFrame, - update = update, - draw = draw, - -- - addText = addText, - setFont = setFont, - } -end - -return { - Label = label, - Button = button, - ImageButton = imageButton, - TextView = textView, - Spacing = spacing, -} \ No newline at end of file diff --git a/widgets/widgets.lua b/widgets/widgets.lua deleted file mode 100644 index 8f8a5b5..0000000 --- a/widgets/widgets.lua +++ /dev/null @@ -1,44 +0,0 @@ -local UI = require "widgets.ui" - -function TextView(text, ...) - assert(type(text) == "string", "text is required") - - local textView = UI.TextView() - return Elem(textView, ...) -end - -function Label(text, ...) - assert(type(text) == "string", "text is required") - - local label = UI.Label(text, { 1.0, 1.0, 1.0 }, { 0.3, 0.0, 0.0, 1.0 }) - return Elem(label, ...) -end - -function Button(title, ...) - assert(type(title) == "string", "title is required") - - local button = UI.Button(title, { 0.0, 0.0, 0.0 }, { 0.0, 0.5, 0.6, 1.0 }) - return Elem(button, ...) -end - -function ImageButton(path, ...) - assert(type(path) == "string", "path is required") - - local imageButton = UI.ImageButton( - path, - { 1.0, 1.0, 1.0 }, - { 0.0, 0.0, 0.0 }, - 10 - ) - return Elem(imageButton, ...) -end - -function FixedSpace(w, h) - w = w or 0 - h = h or w - return Elem(UI.Spacing(), MinSize(w, h), Stretch(0, 0)) -end - -function FlexibleSpace() - return Elem(UI.Spacing(), MinSize(0, 0), Stretch(1, 1)) -end \ No newline at end of file