1// Copyright 2016 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package theme provides widget themes.
6package theme
7
8import (
9	"image"
10	"image/color"
11
12	"golang.org/x/exp/shiny/unit"
13	"golang.org/x/image/font"
14	"golang.org/x/image/font/inconsolata"
15	"golang.org/x/image/math/fixed"
16)
17
18// FontFaceOptions allows asking for font face variants, such as style (e.g.
19// italic) or weight (e.g. bold).
20//
21// TODO: include font.Hinting and font.Stretch typed fields?
22//
23// TODO: include font size? If so, directly as "12pt" or indirectly as an enum
24// (Heading1, Heading2, Body, etc)?
25type FontFaceOptions struct {
26	Style  font.Style
27	Weight font.Weight
28}
29
30// FontFaceCatalog provides a theme's font faces.
31//
32// AcquireFontFace returns a font.Face. ReleaseFontFace should be called, with
33// the same options, once a widget's measure, layout or paint is done with the
34// font.Face returned.
35//
36// A FontFaceCatalog is safe for use by multiple goroutines simultaneously, but
37// in general, a font.Face is not safe for concurrent use, as its methods may
38// re-use implementation-specific caches and mask image buffers.
39type FontFaceCatalog interface {
40	AcquireFontFace(FontFaceOptions) font.Face
41	ReleaseFontFace(FontFaceOptions, font.Face)
42
43	// TODO: add a "Metrics(FontFaceOptions) font.Metrics" method?
44}
45
46// Color is a theme-dependent color, such as "the foreground color". Combining
47// a Color with a Theme results in a color.Color in the sense of the standard
48// library's image/color package. It can also result in an *image.Uniform,
49// suitable for passing as the src argument to image/draw functions.
50type Color interface {
51	Color(*Theme) color.Color
52	Uniform(*Theme) *image.Uniform
53}
54
55// StaticColor adapts a color.Color to a theme Color.
56func StaticColor(c color.Color) Color { return staticColor{image.Uniform{c}} }
57
58type staticColor struct {
59	u image.Uniform
60}
61
62func (s staticColor) Color(*Theme) color.Color      { return s.u.C }
63func (s staticColor) Uniform(*Theme) *image.Uniform { return &s.u }
64
65// Palette provides a theme's color palette. The array is indexed by
66// PaletteIndex constants such as Accent and Foreground.
67//
68// The colors are expressed as image.Uniform values so that they can be easily
69// passed as the src argument to image/draw functions.
70type Palette [PaletteLen]image.Uniform
71
72func (p *Palette) Light() *image.Uniform      { return &p[Light] }
73func (p *Palette) Neutral() *image.Uniform    { return &p[Neutral] }
74func (p *Palette) Dark() *image.Uniform       { return &p[Dark] }
75func (p *Palette) Accent() *image.Uniform     { return &p[Accent] }
76func (p *Palette) Foreground() *image.Uniform { return &p[Foreground] }
77func (p *Palette) Background() *image.Uniform { return &p[Background] }
78
79// PaletteIndex is both an integer index into a Palette array and a Color.
80type PaletteIndex int
81
82func (i PaletteIndex) Color(t *Theme) color.Color      { return t.GetPalette()[i].C }
83func (i PaletteIndex) Uniform(t *Theme) *image.Uniform { return &t.GetPalette()[i] }
84
85const (
86	// Light, Neutral and Dark are three color tones used to fill in widgets
87	// such as buttons, menu bars and panels.
88	Light   = PaletteIndex(0)
89	Neutral = PaletteIndex(1)
90	Dark    = PaletteIndex(2)
91
92	// Accent is the color used to accentuate selections or suggestions.
93	Accent = PaletteIndex(3)
94
95	// Foreground is the color used for text, dividers and icons.
96	Foreground = PaletteIndex(4)
97
98	// Background is the color used behind large blocks of text. Short,
99	// non-editable label text will typically be on the Neutral color.
100	Background = PaletteIndex(5)
101
102	PaletteLen = 6
103)
104
105// DefaultDPI is the fallback value of a theme's DPI, if the underlying context
106// does not provide a DPI value.
107const DefaultDPI = 72.0
108
109var (
110	// DefaultFontFaceCatalog is a catalog for a basic font face.
111	DefaultFontFaceCatalog FontFaceCatalog = defaultFontFaceCatalog{}
112
113	// DefaultPalette is the default theme's palette.
114	DefaultPalette = Palette{
115		Light:      image.Uniform{C: color.RGBA{0xf5, 0xf5, 0xf5, 0xff}}, // Material Design "Grey 100".
116		Neutral:    image.Uniform{C: color.RGBA{0xee, 0xee, 0xee, 0xff}}, // Material Design "Grey 200".
117		Dark:       image.Uniform{C: color.RGBA{0xe0, 0xe0, 0xe0, 0xff}}, // Material Design "Grey 300".
118		Accent:     image.Uniform{C: color.RGBA{0x21, 0x96, 0xf3, 0xff}}, // Material Design "Blue 500".
119		Foreground: image.Uniform{C: color.RGBA{0x00, 0x00, 0x00, 0xff}}, // Material Design "Black".
120		Background: image.Uniform{C: color.RGBA{0xff, 0xff, 0xff, 0xff}}, // Material Design "White".
121	}
122
123	// Default uses the default DPI, FontFaceCatalog and Palette.
124	//
125	// The nil-valued pointer is a valid receiver for a Theme's methods.
126	Default *Theme
127)
128
129// Note that a *basicfont.Face such as inconsolata.Regular8x16 is stateless and
130// safe to use concurrently, so defaultFontFaceCatalog.ReleaseFontFace can be a
131// no-op.
132
133type defaultFontFaceCatalog struct{}
134
135func (defaultFontFaceCatalog) AcquireFontFace(FontFaceOptions) font.Face {
136	return inconsolata.Regular8x16
137}
138
139func (defaultFontFaceCatalog) ReleaseFontFace(FontFaceOptions, font.Face) {}
140
141// Theme is used for measuring, laying out and painting widgets. It consists of
142// a screen DPI resolution, a set of font faces and colors.
143type Theme struct {
144	// DPI is the screen resolution, in dots (i.e. pixels) per inch.
145	//
146	// A zero value means to use the DefaultDPI.
147	DPI float64
148
149	// FontFaceCatalog provides a theme's font faces.
150	//
151	// A zero value means to use the DefaultFontFaceCatalog.
152	FontFaceCatalog FontFaceCatalog
153
154	// Palette provides a theme's color palette.
155	//
156	// A zero value means to use the DefaultPalette.
157	Palette *Palette
158}
159
160// GetDPI returns the theme's DPI, or the default DPI if the field value is
161// zero.
162func (t *Theme) GetDPI() float64 {
163	if t != nil && t.DPI != 0 {
164		return t.DPI
165	}
166	return DefaultDPI
167}
168
169// GetFontFaceCatalog returns the theme's font face catalog, or the default
170// catalog if the field value is zero.
171func (t *Theme) GetFontFaceCatalog() FontFaceCatalog {
172	if t != nil && t.FontFaceCatalog != nil {
173		return t.FontFaceCatalog
174	}
175	return DefaultFontFaceCatalog
176}
177
178// GetPalette returns the theme's palette, or the default palette if the field
179// value is zero.
180func (t *Theme) GetPalette() *Palette {
181	if t != nil && t.Palette != nil {
182		return t.Palette
183	}
184	return &DefaultPalette
185}
186
187// AcquireFontFace calls the same method on the result of GetFontFaceCatalog.
188func (t *Theme) AcquireFontFace(o FontFaceOptions) font.Face {
189	return t.GetFontFaceCatalog().AcquireFontFace(o)
190}
191
192// ReleaseFontFace calls the same method on the result of GetFontFaceCatalog.
193func (t *Theme) ReleaseFontFace(o FontFaceOptions, f font.Face) {
194	t.GetFontFaceCatalog().ReleaseFontFace(o, f)
195}
196
197// Pixels implements the unit.Converter interface.
198func (t *Theme) Pixels(v unit.Value) fixed.Int26_6 {
199	c := t.Convert(v, unit.Px)
200	return fixed.Int26_6(c.F * 64)
201}
202
203// Convert implements the unit.Converter interface.
204func (t *Theme) Convert(v unit.Value, to unit.Unit) unit.Value {
205	if v.U == to {
206		return v
207	}
208	return unit.Value{
209		F: v.F * t.pixelsPer(v.U) / t.pixelsPer(to),
210		U: to,
211	}
212}
213
214// pixelsPer returns the number of pixels in the unit u.
215func (t *Theme) pixelsPer(u unit.Unit) float64 {
216	switch u {
217	case unit.Px:
218		return 1
219	case unit.Dp:
220		return t.GetDPI() / unit.DensityIndependentPixelsPerInch
221	case unit.Pt:
222		return t.GetDPI() / unit.PointsPerInch
223	case unit.Mm:
224		return t.GetDPI() / unit.MillimetresPerInch
225	case unit.In:
226		return t.GetDPI()
227	}
228
229	f := t.AcquireFontFace(FontFaceOptions{})
230	defer t.ReleaseFontFace(FontFaceOptions{}, f)
231
232	// The 64 is because Height is in 26.6 fixed-point units.
233	h := float64(f.Metrics().Height) / 64
234	switch u {
235	case unit.Em:
236		return h
237	case unit.Ex:
238		return h / 2
239	case unit.Ch:
240		if advance, ok := f.GlyphAdvance('0'); ok {
241			return float64(advance) / 64
242		}
243		return h / 2
244	}
245	return 1
246}
247