Custom themes
A theme is any value that implements
bellui.Theme. It has
four methods, one for each kind of value a widget asks for:
| Method | Returns | Looked up by |
|---|---|---|
Color(name, variant) | a color.Color | bellui.ThemeColorName, plus the light or dark variant |
Size(name) | a float32 | bellui.ThemeSizeName |
Font(style) | a font resource | bellui.TextStyle |
Icon(name) | an icon resource | bellui.ThemeIconName |
The names are constants in the
theme package, such
as theme.ColorNamePrimary, theme.SizeNamePadding and
theme.IconNameHome.
Wrap the default theme
You rarely want to define every value. Embed an existing theme and override only what you change. Every method you do not write falls through to the embedded theme:
type brandTheme struct {
// The theme being customised. Every method brandTheme does not
// define comes from it.
bellui.Theme
}
var (
_ bellui.Theme = brandTheme{}
_ theme.RenderStyleTheme = brandTheme{}
)
func newBrandTheme() brandTheme {
return brandTheme{Theme: theme.DefaultTheme()}
}
func (t brandTheme) Color(n bellui.ThemeColorName, v bellui.ThemeVariant) color.Color {
switch n {
case theme.ColorNamePrimary:
if v == theme.VariantDark {
return color.NRGBA{R: 0xff, G: 0xb3, B: 0x80, A: 0xff}
}
return color.NRGBA{R: 0xb3, G: 0x4a, B: 0x00, A: 0xff}
case theme.ColorNameForegroundOnPrimary:
if v == theme.VariantDark {
return color.NRGBA{R: 0x3a, G: 0x18, B: 0x00, A: 0xff}
}
return color.White
}
return t.Theme.Color(n, v)
}
func (t brandTheme) Size(n bellui.ThemeSizeName) float32 {
switch n {
case theme.SizeNameText:
return 15
case theme.SizeNameButtonRadius, theme.SizeNameInputRadius:
return 0 // square corners
}
return t.Theme.Size(n)
}
func (t brandTheme) Icon(n bellui.ThemeIconName) bellui.Resource {
if n == theme.IconNameConfirm {
return t.Theme.Icon(theme.IconNameMailSend)
}
return t.Theme.Icon(n)
}
func (t brandTheme) Font(s bellui.TextStyle) bellui.Resource {
// Return your own bundled font resources here; this keeps the defaults.
return t.Theme.Font(s)
}
// RenderStyle keeps the default theme's modern widget look. Without it a
// wrapping theme falls back to the classic look.
func (t brandTheme) RenderStyle() theme.RenderStyle {
if rs, ok := t.Theme.(theme.RenderStyleTheme); ok {
return rs.RenderStyle()
}
return theme.RenderStyleModern
}Install it once, before you show the first window:
a.Settings().SetTheme(newBrandTheme())Some things to know:
- Check the variant.
Colorreceives the variant the app is using. Return a different colour fortheme.VariantDarkwhen the light one would not have enough contrast. - Keep the modern look. Widgets choose between the “classic” and “modern”
drawing style by asking whether the theme implements
theme.RenderStyleTheme. Embeddingbellui.Themedoes not pass that method through, so a wrapper without its ownRenderStylemethod gets the classic style. Add the method, as above. - Pair colours. When you change a colour, also change the colour drawn on
top of it.
ColorNamePrimarygoes withColorNameForegroundOnPrimary, for example. - Sizes are in device-independent units, not pixels. See Scaling.
Apply a theme to part of the window
container.NewThemeOverride
draws its content with a different theme from the rest of the app:
// A compact tool row: only this part of the window uses the dense theme.
tools := container.NewThemeOverride(container.NewHBox(
widget.NewButton("Cut", func() {}),
widget.NewButton("Copy", func() {}),
widget.NewButton("Paste", func() {}),
), theme.DenseTheme())Everything inside the container uses the theme you pass, including widgets in
nested containers. If you add objects to the content later, call Refresh on
the ThemeOverride so they pick up its theme. Use overrides sparingly: a
window with several looks is harder to read.
Generate colours from a seed colour
The default theme offers eight named primary colours. To start from any other colour, generate the MD3 scheme yourself and serve it from your theme:
type seedTheme struct {
bellui.Theme // sizes, fonts and icons
light, dark *theme.MD3Scheme // colours, generated from the seed
}
func newSeedTheme(seed color.NRGBA) *seedTheme {
palettes := theme.CorePalettesFromPrimary(seed)
return &seedTheme{
Theme: theme.DefaultTheme(),
light: theme.LightSchemeFromPalettes(palettes),
dark: theme.DarkSchemeFromPalettes(palettes),
}
}
func (t *seedTheme) Color(n bellui.ThemeColorName, v bellui.ThemeVariant) color.Color {
scheme := t.light
if v == theme.VariantDark {
scheme = t.dark
}
if c := theme.ColorFromMD3Scheme(scheme, n, v); c != nil {
return c
}
return t.Theme.Color(n, v) // names the scheme does not cover, such as warning
}
func (t *seedTheme) RenderStyle() theme.RenderStyle { return theme.RenderStyleModern }theme.CorePalettesFromPrimaryturns the seed colour into the six MD3 tonal palettes.theme.LightSchemeFromPalettesandtheme.DarkSchemeFromPalettesassign tones from those palettes to each colour role, giving anMD3Scheme.theme.ColorFromMD3Schemelooks a colour name up in a scheme. It understands both the MD3 role names and the general names such asColorNameBackground. It returnsnilfor names the scheme does not cover, such asColorNameWarning, so fall back to the embedded theme.
The seed colour sets the hue. The primary palette always gets enough colour intensity to read as an accent, even from a greyish seed.
a.Settings().SetTheme(newSeedTheme(color.NRGBA{R: 0x00, G: 0x7a, B: 0x6e, A: 0xff}))Load a theme from JSON
theme.FromJSON
builds a theme from JSON. Anything the JSON does not set comes from the
default theme:
const themeJSON = `{
"Colors": {
"primary": "#6a1b9a"
},
"Colors-dark": {
"primary": "#ce93d8",
"background": "#121016"
},
"Sizes": {
"text": 15,
"padding": 8
}
}`The keys are the string values of the theme names: "primary" for
ColorNamePrimary, "text" for SizeNameText, and so on.
Colorsapplies to both variants.Colors-lightandColors-darkapply to one variant and win overColors.- Colours are hex strings:
#rgb,#rgba,#rrggbbor#rrggbbaa, with or without the#. Sizesholds numbers.FontsandIconsmap names to resource URIs, such asfile:///…. Icon keys are icon names. Font keys areregular,bold,boldItalicandmonospace. Other styles use one of these: italic, medium and symbol text useregular, and bold monospace text usesbold.
If the JSON cannot be parsed, FromJSON returns the error together with the
default theme, so the app still has a usable theme:
th, err := theme.FromJSON(themeJSON)
if err != nil {
bellui.LogError("Theme JSON is invalid, using the default theme", err)
}
a.Settings().SetTheme(th) // th is the default theme when err != niltheme.FromJSONWithFallback
takes the theme to fall back to instead. FromJSONReader and
FromJSONReaderWithFallback read from an io.Reader.
RenderStyle method, as in Wrap the default theme.Check a theme
theme.ValidateTheme
asks a theme for every known colour, in both variants, and every known size.
It returns one line for each colour that is nil or transparent and each
size that is 0. Call it from a test of your theme. A size you set to 0 on
purpose, such as a corner radius, is listed too.