[*] Rework theme customization.

This commit allows more flexible and consistent theme overriding
rules for each Widget. There are 3 levels of customization.

1. Global theme (yui.theme) this is the default theme for every Ui
2. Ui theme, this overrides the global theme and provides a default
   for every Widget of the Ui
3. Widget theme, this overrides the Ui theme for a single widget

This commit also allows themes to specify a font field.
This replaces the sparse color, font and cornerRadius field often
provided by each Widget.
Widgets and Uis may also partially override a theme by specifying
only a few fields.
This commit is contained in:
Lorenzo Cogotti 2022-10-25 19:06:57 +02:00
parent 84d234fca6
commit 958206f1f7
11 changed files with 73 additions and 56 deletions

View File

@ -41,8 +41,6 @@ function Button:new(args)
self.text = self.text or ""
self.align = self.align or 'center'
self.valign = self.valign or 'center'
self.color = self.color or core.theme.color
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
self.active = false
if not self.notranslate then
self.text = T(self.text)
@ -70,10 +68,10 @@ end
function Button:draw()
local x,y,w,h = self.x,self.y,self.w,self.h
local font = self.font or love.graphics.getFont()
local c = self:colorForState()
local color, font, cornerRadius = core.themeForWidget(self)
local c = core.colorForWidgetState(self, color)
core.drawBox(x,y,w,h, c, self.cornerRadius)
core.drawBox(x,y,w,h, c, cornerRadius)
love.graphics.setColor(c.fg)
love.graphics.setFont(font)

View File

@ -27,7 +27,6 @@ Checkbox.__index = Checkbox
-- @field text (string) text displayed inside the Checkbox
-- @field[opt='center'] valign (string) vertical alignment 'top', 'bottom', 'center'
-- @field[opt='center'] align (string) horizontal alignment, 'left', 'center', 'right'
-- @field cornerRadius (number) radius for rounded corner
-- @field notranslate (boolean) don't translate text
-- @table CheckboxAttributes
@ -41,8 +40,6 @@ function Checkbox:new(args)
self.text = self.notranslate and self.text or T(self.text)
self.align = self.align or 'left'
self.valign = self.valign or 'center'
self.color = self.color or core.theme.color
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
self.checked = self.checked or false
return self
end
@ -64,11 +61,11 @@ end
function Checkbox:draw()
local x,y,w,h = self.x,self.y,self.w,self.h
local c = self:colorForState()
local font = self.font or love.graphics.getFont()
local color, font, cornerRadius = core.themeForWidget(self)
local c = core.colorForWidgetState(self, color)
-- Draw checkbox
core.drawBox(x+h/10,y+h/10,h*.8,h*.8, c, self.cornerRadius)
core.drawBox(x+h/10,y+h/10,h*.8,h*.8, c, cornerRadius)
love.graphics.setColor(c.fg)
if self.checked then
love.graphics.setLineStyle('smooth')

View File

@ -27,7 +27,6 @@ Choice.__index = Choice
-- @field nowrap (boolean) disable choices wrapping
-- @field[opt='center'] valign (string) vertical alignment 'top', 'bottom', 'center'
-- @field[opt='center'] align (string) horizontal alignment, 'left', 'center', 'right'
-- @field cornerRadius (number) radius for rounded corners
-- @field notranslate (boolean) don't translate text
-- @table ChoiceAttributes
@ -39,8 +38,6 @@ function Choice:new(args)
self.align = self.align or 'center'
self.valign = self.valign or 'center'
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
self.color = self.color or core.theme.color
self.hovered = false
self.choices = self.choices or { "" }
self.nowrap = self.nowrap or #self.choices < 2
@ -134,14 +131,14 @@ end
function Choice:draw()
local x,y,w,h = self.x,self.y,self.w,self.h
local font = self.font or love.graphics.getFont()
local c = self:colorForState()
local color, font, cornerRadius = core.themeForWidget(self)
local c = core.colorForWidgetState(self, color)
core.drawBox(x,y,w,h, c, self.cornerRadius)
core.drawBox(x,y,w,h, c, cornerRadius)
if self.ui.focused == self then
-- draw < and > arrows, desaturate color if arrow is disabled
local cc = self.color.hovered
local cc = color.hovered
love.graphics.setLineStyle('smooth')
love.graphics.setLineWidth(3)

View File

@ -1,7 +1,10 @@
local BASE = (...):gsub('core$', '')
--- Local drawing helpers
--
-- @module yui.core
-- @copyright 2022, The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti, Andrea Pasquini
local core = { theme = require(BASE..'theme') }
core.__index = core
local core = {}
-- Helpers for drawing
function core.verticalOffsetForAlign(valign, font, h)
@ -10,10 +13,33 @@ function core.verticalOffsetForAlign(valign, font, h)
elseif valign == 'bottom' then
return h - font:getHeight()
end
-- else: "middle"
-- else: 'middle'
return (h - font:getHeight()) / 2
end
function core.themeForWidget(widget)
local uiTheme = widget.ui.theme
local theme = widget.theme or uiTheme
local color = theme.color or uiTheme.color
local font = theme.font or uiTheme.font or love.graphics.getFont()
local cornerRadius = theme.cornerRadius or uiTheme.cornerRadius
return color, font, cornerRadius
end
function core.colorForWidgetState(widget, color)
color = color or widget.theme.color or widget.ui.theme.color
if widget.active then
return color.active
elseif widget:isFocused() then
return color.hovered
else
return color.normal
end
end
function core.drawBox(x,y,w,h, color, cornerRadius)
w = math.max(cornerRadius/2, w)
if h < cornerRadius/2 then

View File

@ -1,3 +1,14 @@
--- Yui is Yet another User Interface library
--
-- @module yui
-- @copyright 2022, The DoubleFourteen Code Forge
-- @author Lorenzo Cogotti, Andrea Pasquini
--
-- This module exposes every module in Yui,
-- except the local @{yui.core} module and the
-- global @{yui.theme} (still accessible from @{yui.Ui}).
-- Refer to each module documentation.
local BASE = (...)..'.'
return {
@ -13,6 +24,4 @@ return {
Spacer = require(BASE..'spacer'),
Ui = require(BASE..'ui'),
Widget = require(BASE..'widget'),
theme = require(BASE..'theme')
}

View File

@ -31,7 +31,6 @@ end
-- and @{yui.Widget.WidgetCallbacks|callbacks}.
--
-- @field text (string) text displayed inside the Input
-- @field cornerRadius (number) radius for rounded corners
-- @table InputAttributes
@ -41,8 +40,6 @@ function Input:new(args)
self = setmetatable(args, self)
self.text = self.text or ""
self.color = self.color or core.theme.color
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
self.cursor = math.max(1, math.min(utf8.len(self.text)+1,
self.cursor or utf8.len(self.text)+1))
self.candidate = { text = "", start = 0, length = 0 }
@ -155,7 +152,7 @@ function Input:draw()
-- position 6: hello|
local x,y,w,h = self.x,self.y,self.w,self.h
local font = self.font or love.graphics.getFont()
local color, font, cornerRadius = core.themeForWidget(self)
local th = font:getHeight()
local tw = font:getWidth(self.text)
@ -199,7 +196,7 @@ function Input:draw()
end
-- Perform actual draw
core.drawBox(x,y,w,h, self.color.normal, self.cornerRadius)
core.drawBox(x,y,w,h, color.normal, cornerRadius)
-- Apply text margins
x = math.min(x + 3, x + w)
@ -212,7 +209,7 @@ function Input:draw()
x = x - self.drawofs
-- Text
love.graphics.setColor(self.color.normal.fg)
love.graphics.setColor(color.normal.fg)
love.graphics.setFont(font)
love.graphics.print(self.text, x, y + (h-th)/2)
@ -220,7 +217,7 @@ function Input:draw()
-- Candidate text
local ctw = font:getWidth(self.candidate.text)
love.graphics.setColor(self.color.normal.fg)
love.graphics.setColor(color.normal.fg)
love.graphics.print(self.candidate.text, x + tw, y + (h-th)/2)
-- Candidate text rectangle box

View File

@ -36,18 +36,16 @@ function Label:new(args)
self.text = self.notranslate and self.text or T(self.text)
self.align = self.align or 'center'
self.valign = self.valign or 'center'
self.color = self.color or core.theme.color
return self
end
function Label:draw()
local x,y,w,h = self.x,self.y,self.w,self.h
local font = self.font or love.graphics.getFont()
local c = self.color.normal
local color, font, _ = core.themeForWidget(self)
y = y + core.verticalOffsetForAlign(self.valign, font, h)
love.graphics.setColor(c.fg)
love.graphics.setColor(color.normal.fg)
love.graphics.setFont(font)
shadowtext.printf(self.text, x+2, y, w-4, self.align)
end

View File

@ -24,7 +24,6 @@ Slider.__index = Slider
-- @field vertical (boolean) true for vertical slider, false or nil for horizontal slider
-- @field value (number) default value
-- @field step (number) number of slider's steps
-- @field cornerRadius (number) radius for rounded corners
-- @table SliderAttributes
@ -33,8 +32,6 @@ Slider.__index = Slider
function Slider:new(args)
self = setmetatable(args, self)
self.color = self.color or core.theme.color
self.cornerRadius = self.cornerRadius or core.theme.cornerRadius
self.vertical = self.vertical or false
self.min = self.min or 0
self.max = self.max or 1
@ -87,7 +84,8 @@ end
function Slider:draw()
local x,y,w,h = self.x,self.y,self.w,self.h
local r = math.min(w,h) / 2.1
local c = self:colorForState()
local color, _, cornerRadius = core.themeForWidget(self)
local c = core.colorForWidgetState(self, color)
local fraction = (self.value - self.min) / (self.max - self.min)
local xb, yb, wb, hb -- size of the progress bar
@ -99,8 +97,8 @@ function Slider:draw()
xb, yb, wb, hb = x,y, w*fraction, h
end
core.drawBox(x,y,w,h, c, self.cornerRadius)
core.drawBox(xb,yb,wb,hb, {bg=c.fg}, self.cornerRadius)
core.drawBox(x,y,w,h, c, cornerRadius)
core.drawBox(xb,yb,wb,hb, { bg = c.fg }, cornerRadius)
if self:isFocused() then
love.graphics.setColor(c.fg)

View File

@ -1,4 +1,4 @@
--- Global visual theme settings
--- Visual theme settings
--
-- @module yui.theme
-- @copyright 2022, The DoubleFourteen Code Forge
@ -7,8 +7,9 @@
--- Defines common visual attributes and colors applied to every @{yui.Widget|Widget}.
--
-- @field cornerRadius (number) radius for rounded corners
-- @field font (love.graphics.Font) font used for text (defaults to love.graphics.getFont())
-- @field color (@{ColorPalette}) default @{yui.Widget|Widget} color theme
-- @table theme
-- @table Theme
--- Defines which color corresponds to each @{yui.Widget|Widget} state.
--
@ -22,9 +23,12 @@
-- @field bg (table) background color
-- @field fg (table) foreground color (typically used for text)
-- @table Color
local theme = {
cornerRadius = 4,
-- font = nil defaults to love.graphics.getFont()
color = {
normal = {bg = { 0.25, 0.25, 0.25}, fg = {0.73, 0.73, 0.73}},
hovered = {bg = { 0.19, 0.6, 0.73}, fg = {1, 1, 1}},

7
ui.lua
View File

@ -14,14 +14,16 @@ local Widget = require(BASE..'widget')
local Layout = require(BASE..'layout')
local Columns = require(BASE..'columns')
local Rows = require(BASE..'rows')
local theme = require(BASE..'theme')
local gear = require 'lib.gear'
local Timer = gear.Timer
local isinstance = gear.meta.isinstance
local pointinrect = gear.rect.pointinside
local Ui = {}
local Ui = {
theme = theme -- fallback theme
}
Ui.__index = Ui
@ -66,6 +68,7 @@ end
--
-- @field x (number) x position of the Ui
-- @field y (number) y position of the Ui
-- @field theme (@{yui.theme.Theme|Theme}) custom global Ui theme, defaults to @{yui.theme}
-- @table UiAttributes

View File

@ -13,6 +13,7 @@
--- Attributes accepted by all the Widget classes
-- @field w (number) widget width
-- @field h (number) widget height
-- @field theme (@{yui.theme.Theme|Theme}) widget specific theme
-- @field color (@{yui.theme.ColorPalette|ColorPalette}) widget color
-- @table WidgetAttributes
local rectunion = require('lib.gear.rect').union
@ -105,17 +106,6 @@ function Widget:recalculateBounds()
end
end
-- Helper for drawing
function Widget:colorForState()
if self.active then
return self.color.active
elseif self:isFocused() then
return self.color.hovered
else
return self.color.normal
end
end
-- NOP hooks for UI internal use
function Widget:loseFocus() end
function Widget:gainFocus() end