blob: 073bace86131570c8dfc7492dcdaabe0933c3b49 [file] [log] [blame] [edit]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package theme provides widget themes.
package theme
import (
"image"
"image/color"
"golang.org/x/exp/shiny/unit"
"golang.org/x/image/font"
"golang.org/x/image/font/inconsolata"
"golang.org/x/image/math/fixed"
)
// FontFaceOptions allows asking for font face variants, such as style (e.g.
// italic) or weight (e.g. bold).
//
// TODO: include font.Hinting and font.Stretch typed fields?
//
// TODO: include font size? If so, directly as "12pt" or indirectly as an enum
// (Heading1, Heading2, Body, etc)?
type FontFaceOptions struct {
Style font.Style
Weight font.Weight
}
// FontFaceCatalog provides a theme's font faces.
//
// AcquireFontFace returns a font.Face. ReleaseFontFace should be called, with
// the same options, once a widget's measure, layout or paint is done with the
// font.Face returned.
//
// A FontFaceCatalog is safe for use by multiple goroutines simultaneously, but
// in general, a font.Face is not safe for concurrent use, as its methods may
// re-use implementation-specific caches and mask image buffers.
type FontFaceCatalog interface {
AcquireFontFace(FontFaceOptions) font.Face
ReleaseFontFace(FontFaceOptions, font.Face)
// TODO: add a "Metrics(FontFaceOptions) font.Metrics" method?
}
// Color is a theme-dependent color, such as "the foreground color". Combining
// a Color with a Theme results in a color.Color in the sense of the standard
// library's image/color package. It can also result in an *image.Uniform,
// suitable for passing as the src argument to image/draw functions.
type Color interface {
Color(*Theme) color.Color
Uniform(*Theme) *image.Uniform
}
// StaticColor adapts a color.Color to a theme Color.
func StaticColor(c color.Color) Color { return staticColor{image.Uniform{c}} }
type staticColor struct {
u image.Uniform
}
func (s staticColor) Color(*Theme) color.Color { return s.u.C }
func (s staticColor) Uniform(*Theme) *image.Uniform { return &s.u }
// Palette provides a theme's color palette. The array is indexed by
// PaletteIndex constants such as Accent and Foreground.
//
// The colors are expressed as image.Uniform values so that they can be easily
// passed as the src argument to image/draw functions.
type Palette [PaletteLen]image.Uniform
func (p *Palette) Light() *image.Uniform { return &p[Light] }
func (p *Palette) Neutral() *image.Uniform { return &p[Neutral] }
func (p *Palette) Dark() *image.Uniform { return &p[Dark] }
func (p *Palette) Accent() *image.Uniform { return &p[Accent] }
func (p *Palette) Foreground() *image.Uniform { return &p[Foreground] }
func (p *Palette) Background() *image.Uniform { return &p[Background] }
// PaletteIndex is both an integer index into a Palette array and a Color.
type PaletteIndex int
func (i PaletteIndex) Color(t *Theme) color.Color { return t.GetPalette()[i].C }
func (i PaletteIndex) Uniform(t *Theme) *image.Uniform { return &t.GetPalette()[i] }
const (
// Light, Neutral and Dark are three color tones used to fill in widgets
// such as buttons, menu bars and panels.
Light = PaletteIndex(0)
Neutral = PaletteIndex(1)
Dark = PaletteIndex(2)
// Accent is the color used to accentuate selections or suggestions.
Accent = PaletteIndex(3)
// Foreground is the color used for text, dividers and icons.
Foreground = PaletteIndex(4)
// Background is the color used behind large blocks of text. Short,
// non-editable label text will typically be on the Neutral color.
Background = PaletteIndex(5)
PaletteLen = 6
)
// DefaultDPI is the fallback value of a theme's DPI, if the underlying context
// does not provide a DPI value.
const DefaultDPI = 72.0
var (
// DefaultFontFaceCatalog is a catalog for a basic font face.
DefaultFontFaceCatalog FontFaceCatalog = defaultFontFaceCatalog{}
// DefaultPalette is the default theme's palette.
DefaultPalette = Palette{
Light: image.Uniform{C: color.RGBA{0xf5, 0xf5, 0xf5, 0xff}}, // Material Design "Grey 100".
Neutral: image.Uniform{C: color.RGBA{0xee, 0xee, 0xee, 0xff}}, // Material Design "Grey 200".
Dark: image.Uniform{C: color.RGBA{0xe0, 0xe0, 0xe0, 0xff}}, // Material Design "Grey 300".
Accent: image.Uniform{C: color.RGBA{0x21, 0x96, 0xf3, 0xff}}, // Material Design "Blue 500".
Foreground: image.Uniform{C: color.RGBA{0x00, 0x00, 0x00, 0xff}}, // Material Design "Black".
Background: image.Uniform{C: color.RGBA{0xff, 0xff, 0xff, 0xff}}, // Material Design "White".
}
// Default uses the default DPI, FontFaceCatalog and Palette.
//
// The nil-valued pointer is a valid receiver for a Theme's methods.
Default *Theme
)
// Note that a *basicfont.Face such as inconsolata.Regular8x16 is stateless and
// safe to use concurrently, so defaultFontFaceCatalog.ReleaseFontFace can be a
// no-op.
type defaultFontFaceCatalog struct{}
func (defaultFontFaceCatalog) AcquireFontFace(FontFaceOptions) font.Face {
return inconsolata.Regular8x16
}
func (defaultFontFaceCatalog) ReleaseFontFace(FontFaceOptions, font.Face) {}
// Theme is used for measuring, laying out and painting widgets. It consists of
// a screen DPI resolution, a set of font faces and colors.
type Theme struct {
// DPI is the screen resolution, in dots (i.e. pixels) per inch.
//
// A zero value means to use the DefaultDPI.
DPI float64
// FontFaceCatalog provides a theme's font faces.
//
// A zero value means to use the DefaultFontFaceCatalog.
FontFaceCatalog FontFaceCatalog
// Palette provides a theme's color palette.
//
// A zero value means to use the DefaultPalette.
Palette *Palette
}
// GetDPI returns the theme's DPI, or the default DPI if the field value is
// zero.
func (t *Theme) GetDPI() float64 {
if t != nil && t.DPI != 0 {
return t.DPI
}
return DefaultDPI
}
// GetFontFaceCatalog returns the theme's font face catalog, or the default
// catalog if the field value is zero.
func (t *Theme) GetFontFaceCatalog() FontFaceCatalog {
if t != nil && t.FontFaceCatalog != nil {
return t.FontFaceCatalog
}
return DefaultFontFaceCatalog
}
// GetPalette returns the theme's palette, or the default palette if the field
// value is zero.
func (t *Theme) GetPalette() *Palette {
if t != nil && t.Palette != nil {
return t.Palette
}
return &DefaultPalette
}
// AcquireFontFace calls the same method on the result of GetFontFaceCatalog.
func (t *Theme) AcquireFontFace(o FontFaceOptions) font.Face {
return t.GetFontFaceCatalog().AcquireFontFace(o)
}
// ReleaseFontFace calls the same method on the result of GetFontFaceCatalog.
func (t *Theme) ReleaseFontFace(o FontFaceOptions, f font.Face) {
t.GetFontFaceCatalog().ReleaseFontFace(o, f)
}
// Pixels implements the unit.Converter interface.
func (t *Theme) Pixels(v unit.Value) fixed.Int26_6 {
c := t.Convert(v, unit.Px)
return fixed.Int26_6(c.F * 64)
}
// Convert implements the unit.Converter interface.
func (t *Theme) Convert(v unit.Value, to unit.Unit) unit.Value {
if v.U == to {
return v
}
return unit.Value{
F: v.F * t.pixelsPer(v.U) / t.pixelsPer(to),
U: to,
}
}
// pixelsPer returns the number of pixels in the unit u.
func (t *Theme) pixelsPer(u unit.Unit) float64 {
switch u {
case unit.Px:
return 1
case unit.Dp:
return t.GetDPI() / unit.DensityIndependentPixelsPerInch
case unit.Pt:
return t.GetDPI() / unit.PointsPerInch
case unit.Mm:
return t.GetDPI() / unit.MillimetresPerInch
case unit.In:
return t.GetDPI()
}
f := t.AcquireFontFace(FontFaceOptions{})
defer t.ReleaseFontFace(FontFaceOptions{}, f)
// The 64 is because Height is in 26.6 fixed-point units.
h := float64(f.Metrics().Height) / 64
switch u {
case unit.Em:
return h
case unit.Ex:
return h / 2
case unit.Ch:
if advance, ok := f.GlyphAdvance('0'); ok {
return float64(advance) / 64
}
return h / 2
}
return 1
}