diff --git a/artist/text.go b/artist/text.go index 88616f1..bc8c8cd 100644 --- a/artist/text.go +++ b/artist/text.go @@ -165,6 +165,7 @@ func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) { // have its maximum width set to the given width. This does not alter the // drawer's state. func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) { + if drawer.face == nil { return } if !drawer.layoutClean { drawer.recalculate() } metrics := drawer.face.Metrics() dot := fixed.Point26_6 { 0, metrics.Height } diff --git a/config/config.go b/config/config.go index cdc4868..2937e0c 100644 --- a/config/config.go +++ b/config/config.go @@ -50,3 +50,44 @@ func (Default) ScrollVelocity () int { func (Default) ThemePath () (string) { return "" } + +// Wrapped wraps a configuration and uses Default if it is nil. +type Wrapped struct { + Config +} + +// Padding returns the amount of internal padding elements should have. +// An element's inner content (such as text) should be inset by this +// amount, in addition to the inset returned by the pattern of its +// background. +func (wrapped Wrapped) Padding () int { + return wrapped.ensure().Padding() +} + +// Margin returns how much space should be put in between elements. +func (wrapped Wrapped) Margin () int { + return wrapped.ensure().Margin() +} + +// HandleWidth returns how large grab handles should typically be. This +// is important for accessibility reasons. +func (wrapped Wrapped) HandleWidth () int { + return wrapped.ensure().HandleWidth() +} + +// ScrollVelocity returns how many pixels should be scrolled every time +// a scroll button is pressed. +func (wrapped Wrapped) ScrollVelocity () int { + return wrapped.ensure().ScrollVelocity() +} + +// ThemePath returns the directory path to the theme. +func (wrapped Wrapped) ThemePath () string { + return wrapped.ensure().ThemePath() +} + +func (wrapped Wrapped) ensure () (real Config) { + real = wrapped.Config + if real == nil { real = Default { } } + return +} diff --git a/elements/basic/button.go b/elements/basic/button.go index 28e8566..cf8677e 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -18,18 +18,16 @@ type Button struct { pressed bool text string - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onClick func () } // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { - element = &Button { - c: theme.C("basic", "button"), - } + element = &Button { } + element.theme.Case = theme.C("basic", "button") element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) @@ -103,18 +101,19 @@ func (element *Button) SetText (text string) { // SetTheme sets the element's theme. func (element *Button) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *Button) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -141,7 +140,7 @@ func (element *Button) draw () { Pressed: element.pressed, } - pattern := element.theme.Pattern(theme.PatternButton, element.c, state) + pattern := element.theme.Pattern(theme.PatternButton, state) artist.FillRectangle(element, pattern, bounds) @@ -156,6 +155,10 @@ func (element *Button) draw () { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern(theme.PatternForeground, element.c, state) + if element.pressed { + offset = offset.Add(element.theme.Sink(theme.PatternButton)) + } + + foreground := element.theme.Pattern(theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index d6a7680..9c330b6 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -19,19 +19,16 @@ type Checkbox struct { checked bool text string - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onToggle func () } // NewCheckbox creates a new cbeckbox with the specified label text. func NewCheckbox (text string, checked bool) (element *Checkbox) { - element = &Checkbox { - checked: checked, - c: theme.C("basic", "checkbox"), - } + element = &Checkbox { checked: checked } + element.theme.Case = theme.C("basic", "checkbox") element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) @@ -126,18 +123,19 @@ func (element *Checkbox) SetText (text string) { // SetTheme sets the element's theme. func (element *Checkbox) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *Checkbox) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -172,10 +170,10 @@ func (element *Checkbox) draw () { } backgroundPattern := element.theme.Pattern ( - theme.PatternBackground, element.c, state) + theme.PatternBackground, state) artist.FillRectangle(element, backgroundPattern, bounds) - pattern := element.theme.Pattern(theme.PatternButton, element.c, state) + pattern := element.theme.Pattern(theme.PatternButton, state) artist.FillRectangle(element, pattern, boxBounds) textBounds := element.drawer.LayoutBounds() @@ -186,7 +184,6 @@ func (element *Checkbox) draw () { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, state) + foreground := element.theme.Pattern(theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/container.go b/elements/basic/container.go index 75bf24c..e6f2637 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -24,9 +24,8 @@ type Container struct { focusable bool flexible bool - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) @@ -35,9 +34,8 @@ type Container struct { // NewContainer creates a new container. func NewContainer (layout layouts.Layout) (element *Container) { - element = &Container { - c: theme.C("basic", "container"), - } + element = &Container { } + element.theme.Case = theme.C("basic", "container") element.Core, element.core = core.NewCore(element.redoAll) element.SetLayout(layout) return @@ -57,6 +55,12 @@ func (element *Container) SetLayout (layout layouts.Layout) { // whatever way is defined by the current layout. func (element *Container) Adopt (child elements.Element, expand bool) { // set event handlers + if child0, ok := child.(elements.Themeable); ok { + child0.SetTheme(element.theme.Theme) + } + if child0, ok := child.(elements.Configurable); ok { + child0.SetConfig(element.config.Config) + } child.OnDamage (func (region canvas.Canvas) { element.core.DamageRegion(region.Bounds()) }) @@ -212,7 +216,6 @@ func (element *Container) redoAll () { bounds := element.Bounds() pattern := element.theme.Pattern ( theme.PatternBackground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) @@ -225,10 +228,11 @@ func (element *Container) redoAll () { // SetTheme sets the element's theme. func (element *Container) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new for _, child := range element.children { if child0, ok := child.Element.(elements.Themeable); ok { - child0.SetTheme(element.theme) + child0.SetTheme(element.theme.Theme) } } element.updateMinimumSize() @@ -237,7 +241,8 @@ func (element *Container) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *Container) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new for _, child := range element.children { if child0, ok := child.Element.(elements.Configurable); ok { child0.SetConfig(element.config) diff --git a/elements/basic/label.go b/elements/basic/label.go index 1448309..7d75ae3 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -14,9 +14,8 @@ type Label struct { text string drawer artist.TextDrawer - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onFlexibleHeightChange func () } @@ -24,7 +23,8 @@ type Label struct { // NewLabel creates a new label. If wrap is set to true, the text inside will be // wrapped. func NewLabel (text string, wrap bool) (element *Label) { - element = &Label { c: theme.C("basic", "label") } + element = &Label { } + element.theme.Case = theme.C("basic", "label") element.Core, element.core = core.NewCore(element.handleResize) element.SetWrap(wrap) element.SetText(text) @@ -34,8 +34,7 @@ func NewLabel (text string, wrap bool) (element *Label) { func (element *Label) redo () { face := element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c) + theme.FontSizeNormal) element.drawer.SetFace(face) element.updateMinimumSize() bounds := element.Bounds() @@ -109,11 +108,11 @@ func (element *Label) SetWrap (wrap bool) { // SetTheme sets the element's theme. func (element *Label) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() if element.core.HasImage () { @@ -124,7 +123,8 @@ func (element *Label) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *Label) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() if element.core.HasImage () { @@ -153,7 +153,6 @@ func (element *Label) draw () { pattern := element.theme.Pattern ( theme.PatternBackground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) @@ -161,7 +160,6 @@ func (element *Label) draw () { foreground := element.theme.Pattern ( theme.PatternForeground, - element.c, theme.PatternState { }) element.drawer.Draw(element, foreground, bounds.Min.Sub(textBounds.Min)) } diff --git a/elements/basic/list.go b/elements/basic/list.go index 6da55b3..8a19584 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -26,9 +26,8 @@ type List struct { scroll int entries []ListEntry - config config.Config - theme theme.Theme - c theme.Case + config config.Wrapped + theme theme.Wrapped onScrollBoundsChange func () onNoEntrySelected func () @@ -36,10 +35,8 @@ type List struct { // NewList creates a new list element with the specified entries. func NewList (entries ...ListEntry) (element *List) { - element = &List { - selectedEntry: -1, - c: theme.C("basic", "list"), - } + element = &List { selectedEntry: -1 } + element.theme.Case = theme.C("basic", "list") element.Core, element.core = core.NewCore(element.handleResize) element.FocusableCore, element.focusableControl = core.NewFocusableCore (func () { @@ -71,9 +68,10 @@ func (element *List) handleResize () { // SetTheme sets the element's theme. func (element *List) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new for index, entry := range element.entries { - entry.SetConfig(element.config) + entry.SetTheme(element.theme.Theme) element.entries[index] = entry } element.updateMinimumSize() @@ -82,7 +80,8 @@ func (element *List) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *List) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new for index, entry := range element.entries { entry.SetConfig(element.config) element.entries[index] = entry @@ -206,7 +205,7 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { } func (element *List) scrollViewportHeight () (height int) { - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) return element.Bounds().Dy() - inset[0] - inset[2] } @@ -238,7 +237,7 @@ func (element *List) CountEntries () (count int) { func (element *List) Append (entry ListEntry) { // append entry.Collapse(element.forcedMinimumWidth) - entry.SetTheme(element.theme) + entry.SetTheme(element.theme.Theme) entry.SetConfig(element.config) element.entries = append(element.entries, entry) @@ -332,7 +331,7 @@ func (element *List) Replace (index int, entry ListEntry) { } func (element *List) selectUnderMouse (x, y int) (updated bool) { - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) bounds := inset.Apply(element.Bounds()) mousePoint := image.Pt(x, y) dot := image.Pt ( @@ -374,7 +373,7 @@ func (element *List) changeSelectionBy (delta int) (updated bool) { } func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1]) return entry } @@ -401,7 +400,7 @@ func (element *List) updateMinimumSize () { minimumHeight = element.contentHeight } - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) minimumHeight += inset[0] + inset[2] element.core.SetMinimumSize(minimumWidth, minimumHeight) @@ -410,8 +409,8 @@ func (element *List) updateMinimumSize () { func (element *List) draw () { bounds := element.Bounds() - inset := element.theme.Inset(theme.PatternSunken, element.c) - pattern := element.theme.Pattern (theme.PatternSunken, element.c, theme.PatternState { + inset := element.theme.Inset(theme.PatternSunken) + pattern := element.theme.Pattern (theme.PatternSunken, theme.PatternState { Disabled: !element.Enabled(), Focused: element.Focused(), }) diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index 79d91d8..5248d1b 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -14,9 +14,8 @@ type ListEntry struct { text string forcedMinimumWidth int - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onSelect func () } @@ -25,8 +24,8 @@ func NewListEntry (text string, onSelect func ()) (entry ListEntry) { entry = ListEntry { text: text, onSelect: onSelect, - c: theme.C("basic", "listEntry"), } + entry.theme.Case = theme.C("basic", "listEntry") entry.drawer.SetText([]rune(text)) entry.updateBounds() return @@ -39,16 +38,17 @@ func (entry *ListEntry) Collapse (width int) { } func (entry *ListEntry) SetTheme (new theme.Theme) { - entry.theme = new + if new == entry.theme.Theme { return } + entry.theme.Theme = new entry.drawer.SetFace (entry.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - entry.c)) + theme.FontSizeNormal)) entry.updateBounds() } -func (entry *ListEntry) SetConfig (config config.Config) { - entry.config = config +func (entry *ListEntry) SetConfig (new config.Config) { + if new == entry.config.Config { return } + entry.config.Config = new } func (entry *ListEntry) updateBounds () { @@ -60,7 +60,7 @@ func (entry *ListEntry) updateBounds () { entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx() } - inset := entry.theme.Inset(theme.PatternRaised, entry.c) + inset := entry.theme.Inset(theme.PatternRaised) entry.bounds.Max.Y += inset[0] + inset[2] entry.textPoint = @@ -80,12 +80,12 @@ func (entry *ListEntry) Draw ( Focused: focused, On: on, } - pattern := entry.theme.Pattern (theme.PatternRaised, entry.c, state) + pattern := entry.theme.Pattern (theme.PatternRaised, state) artist.FillRectangle ( destination, pattern, entry.Bounds().Add(offset)) - foreground := entry.theme.Pattern (theme.PatternForeground, entry.c, state) + foreground := entry.theme.Pattern (theme.PatternForeground, state) return entry.drawer.Draw ( destination, foreground, diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 80806ca..f1f2faf 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -12,18 +12,15 @@ type ProgressBar struct { core core.CoreControl progress float64 - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped } // NewProgressBar creates a new progress bar displaying the given progress // level. func NewProgressBar (progress float64) (element *ProgressBar) { - element = &ProgressBar { - progress: progress, - c: theme.C("basic", "progressBar"), - } + element = &ProgressBar { progress: progress } + element.theme.Case = theme.C("basic", "progressBar") element.Core, element.core = core.NewCore(element.draw) return } @@ -40,14 +37,16 @@ func (element *ProgressBar) SetProgress (progress float64) { // SetTheme sets the element's theme. func (element *ProgressBar) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *ProgressBar) SetConfig (new config.Config) { - element.config = new + if new == nil || new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -70,9 +69,8 @@ func (element *ProgressBar) draw () { pattern := element.theme.Pattern ( theme.PatternSunken, - element.c, theme.PatternState { }) - inset := element.theme.Inset(theme.PatternSunken, element.c) + inset := element.theme.Inset(theme.PatternSunken) artist.FillRectangle(element, pattern, bounds) bounds = inset.Apply(bounds) meterBounds := image.Rect ( @@ -80,8 +78,7 @@ func (element *ProgressBar) draw () { bounds.Min.X + int(float64(bounds.Dx()) * element.progress), bounds.Max.Y) accent := element.theme.Pattern ( - theme.PatternSunken, - element.c, + theme.PatternAccent, theme.PatternState { }) artist.FillRectangle(element, accent, meterBounds) } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index 553cc5e..cfc540a 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -20,7 +20,7 @@ type ScrollContainer struct { childWidth, childHeight int horizontal struct { - c theme.Case + theme theme.Wrapped exists bool enabled bool dragging bool @@ -31,7 +31,7 @@ type ScrollContainer struct { } vertical struct { - c theme.Case + theme theme.Wrapped exists bool enabled bool dragging bool @@ -41,9 +41,8 @@ type ScrollContainer struct { bar image.Rectangle } - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) @@ -52,12 +51,12 @@ type ScrollContainer struct { // NewScrollContainer creates a new scroll container with the specified scroll // bars. func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { - element = &ScrollContainer { c: theme.C("basic", "scrollContainer") } - element.horizontal.c = theme.C("basic", "scrollBarHorizontal") - element.vertical.c = theme.C("basic", "scrollBarVertical") + element = &ScrollContainer { } + element.theme.Case = theme.C("basic", "scrollContainer") + element.horizontal.theme.Case = theme.C("basic", "scrollBarHorizontal") + element.vertical.theme.Case = theme.C("basic", "scrollBarVertical") element.Core, element.core = core.NewCore(element.handleResize) - element.updateMinimumSize() element.horizontal.exists = horizontal element.vertical.exists = vertical return @@ -81,6 +80,12 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) { // adopt new child element.child = child if child != nil { + if child0, ok := child.(elements.Themeable); ok { + child0.SetTheme(element.theme.Theme) + } + if child0, ok := child.(elements.Configurable); ok { + child0.SetConfig(element.config.Config) + } child.OnDamage(element.childDamageCallback) child.OnMinimumSizeChange(element.updateMinimumSize) child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback) @@ -102,6 +107,34 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) { } } +// SetTheme sets the element's theme. +func (element *ScrollContainer) SetTheme (new theme.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + if child, ok := element.child.(elements.Themeable); ok { + child.SetTheme(element.theme.Theme) + } + if element.core.HasImage() { + element.recalculate() + element.resizeChildToFit() + element.draw() + } +} + +// SetConfig sets the element's configuration. +func (element *ScrollContainer) SetConfig (new config.Config) { + if new == element.config.Config { return } + element.config.Config = new + if child, ok := element.child.(elements.Configurable); ok { + child.SetConfig(element.config.Config) + } + if element.core.HasImage() { + element.recalculate() + element.resizeChildToFit() + element.draw() + } +} + func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if child, ok := element.child.(elements.KeyboardTarget); ok { child.HandleKeyDown(key, modifiers) @@ -289,8 +322,8 @@ func (element *ScrollContainer) recalculate () { horizontal := &element.horizontal vertical := &element.vertical - gutterInsetHorizontal := element.theme.Inset(theme.PatternGutter, horizontal.c) - gutterInsetVertical := element.theme.Inset(theme.PatternGutter, vertical.c) + gutterInsetHorizontal := horizontal.theme.Inset(theme.PatternGutter) + gutterInsetVertical := vertical.theme.Inset(theme.PatternGutter) bounds := element.Bounds() thicknessHorizontal := @@ -376,7 +409,7 @@ func (element *ScrollContainer) recalculate () { func (element *ScrollContainer) draw () { artist.Paste(element, element.child, image.Point { }) deadPattern := element.theme.Pattern ( - theme.PatternDead, element.c, theme.PatternState { }) + theme.PatternDead, theme.PatternState { }) artist.FillRectangle ( element, deadPattern, image.Rect ( @@ -393,12 +426,10 @@ func (element *ScrollContainer) drawHorizontalBar () { Disabled: !element.horizontal.enabled, Pressed: element.horizontal.dragging, } - gutterPattern := element.theme.Pattern ( - theme.PatternGutter, element.horizontal.c, state) + gutterPattern := element.horizontal.theme.Pattern(theme.PatternGutter, state) artist.FillRectangle(element, gutterPattern, element.horizontal.gutter) - handlePattern := element.theme.Pattern ( - theme.PatternHandle, element.horizontal.c, state) + handlePattern := element.horizontal.theme.Pattern(theme.PatternHandle, state) artist.FillRectangle(element, handlePattern, element.horizontal.bar) } @@ -407,12 +438,10 @@ func (element *ScrollContainer) drawVerticalBar () { Disabled: !element.vertical.enabled, Pressed: element.vertical.dragging, } - gutterPattern := element.theme.Pattern ( - theme.PatternGutter, element.vertical.c, state) + gutterPattern := element.vertical.theme.Pattern(theme.PatternGutter, state) artist.FillRectangle(element, gutterPattern, element.vertical.gutter) - handlePattern := element.theme.Pattern ( - theme.PatternHandle, element.vertical.c, state) + handlePattern := element.vertical.theme.Pattern(theme.PatternHandle, state) artist.FillRectangle(element, handlePattern, element.vertical.bar) } @@ -435,10 +464,8 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) { } func (element *ScrollContainer) updateMinimumSize () { - gutterInsetHorizontal := element.theme.Inset ( - theme.PatternGutter, element.horizontal.c) - gutterInsetVertical := element.theme.Inset ( - theme.PatternGutter, element.vertical.c) + gutterInsetHorizontal := element.horizontal.theme.Inset(theme.PatternGutter) + gutterInsetVertical := element.vertical.theme.Inset(theme.PatternGutter) thicknessHorizontal := element.config.HandleWidth() + diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index 5588e5f..1b14067 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -11,16 +11,16 @@ type Spacer struct { core core.CoreControl line bool - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped } // NewSpacer creates a new spacer. If line is set to true, the spacer will be // filled with a line color, and if compressed to its minimum width or height, // will appear as a line. func NewSpacer (line bool) (element *Spacer) { - element = &Spacer { line: line, c: theme.C("basic", "spacer") } + element = &Spacer { line: line } + element.theme.Case = theme.C("basic", "spacer") element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(1, 1) return @@ -38,13 +38,15 @@ func (element *Spacer) SetLine (line bool) { // SetTheme sets the element's theme. func (element *Spacer) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.redo() } // SetConfig sets the element's configuration. func (element *Spacer) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.redo() } @@ -61,13 +63,11 @@ func (element *Spacer) draw () { if element.line { pattern := element.theme.Pattern ( theme.PatternForeground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) } else { pattern := element.theme.Pattern ( theme.PatternBackground, - element.c, theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) } diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 994b77c..fa7c7d3 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -20,9 +20,8 @@ type Switch struct { checked bool text string - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onToggle func () } @@ -32,8 +31,8 @@ func NewSwitch (text string, on bool) (element *Switch) { element = &Switch { checked: on, text: text, - c: theme.C("basic", "switch"), } + element.theme.Case = theme.C("basic", "switch") element.Core, element.core = core.NewCore(element.draw) element.FocusableCore, element.focusableControl = core.NewFocusableCore(element.redo) @@ -116,18 +115,19 @@ func (element *Switch) SetText (text string) { // SetTheme sets the element's theme. func (element *Switch) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new element.drawer.SetFace (element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c)) + theme.FontSizeNormal)) element.updateMinimumSize() element.redo() } // SetConfig sets the element's configuration. func (element *Switch) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } @@ -165,7 +165,7 @@ func (element *Switch) draw () { Pressed: element.pressed, } backgroundPattern := element.theme.Pattern ( - theme.PatternBackground, element.c, state) + theme.PatternBackground, state) artist.FillRectangle (element, backgroundPattern, bounds) if element.checked { @@ -183,11 +183,11 @@ func (element *Switch) draw () { } gutterPattern := element.theme.Pattern ( - theme.PatternGutter, element.c, state) + theme.PatternGutter, state) artist.FillRectangle(element, gutterPattern, gutterBounds) handlePattern := element.theme.Pattern ( - theme.PatternHandle, element.c, state) + theme.PatternHandle, state) artist.FillRectangle(element, handlePattern, handleBounds) textBounds := element.drawer.LayoutBounds() @@ -199,6 +199,6 @@ func (element *Switch) draw () { offset.X -= textBounds.Min.X foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, state) + theme.PatternForeground, state) element.drawer.Draw(element, foreground, offset) } diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index fbd859b..babefdc 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -23,9 +23,8 @@ type TextBox struct { placeholderDrawer artist.TextDrawer valueDrawer artist.TextDrawer - theme theme.Theme - config config.Config - c theme.Case + config config.Wrapped + theme theme.Wrapped onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool) onChange func () @@ -36,7 +35,8 @@ type TextBox struct { // a value. When the value is empty, the placeholder will be displayed in gray // text. func NewTextBox (placeholder, value string) (element *TextBox) { - element = &TextBox { c: theme.C("basic", "textBox") } + element = &TextBox { } + element.theme.Case = theme.C("basic", "textBox") element.Core, element.core = core.NewCore(element.handleResize) element.FocusableCore, element.focusableControl = core.NewFocusableCore (func () { @@ -252,11 +252,11 @@ func (element *TextBox) scrollToCursor () { // SetTheme sets the element's theme. func (element *TextBox) SetTheme (new theme.Theme) { - element.theme = new + if new == element.theme.Theme { return } + element.theme.Theme = new face := element.theme.FontFace ( theme.FontStyleRegular, - theme.FontSizeNormal, - element.c) + theme.FontSizeNormal) element.placeholderDrawer.SetFace(face) element.valueDrawer.SetFace(face) element.updateMinimumSize() @@ -265,19 +265,19 @@ func (element *TextBox) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *TextBox) SetConfig (new config.Config) { - element.config = new + if new == element.config.Config { return } + element.config.Config = new element.updateMinimumSize() element.redo() } func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() - inset := element.theme.Inset(theme.PatternInput, element.c) element.core.SetMinimumSize ( textBounds.Dx() + - element.config.Padding() * 2 + inset[3] + inset[1], + element.config.Padding() * 2, element.placeholderDrawer.LineHeight().Round() + - element.config.Padding() * 2 + inset[0] + inset[2]) + element.config.Padding() * 2) } func (element *TextBox) redo () { @@ -295,7 +295,7 @@ func (element *TextBox) draw () { Disabled: !element.Enabled(), Focused: element.Focused(), } - pattern := element.theme.Pattern(theme.PatternSunken, element.c, state) + pattern := element.theme.Pattern(theme.PatternSunken, state) artist.FillRectangle(element, pattern, bounds) if len(element.text) == 0 && !element.Focused() { @@ -306,7 +306,7 @@ func (element *TextBox) draw () { Y: element.config.Padding(), }) foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, + theme.PatternForeground, theme.PatternState { Disabled: true }) element.placeholderDrawer.Draw ( element, @@ -320,7 +320,7 @@ func (element *TextBox) draw () { Y: element.config.Padding(), }) foreground := element.theme.Pattern ( - theme.PatternForeground, element.c, state) + theme.PatternForeground, state) element.valueDrawer.Draw ( element, foreground, diff --git a/elements/testing/artist.go b/elements/testing/artist.go index 968143e..6e4a015 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -4,7 +4,6 @@ import "fmt" import "time" import "image" import "image/color" -import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/defaultfont" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -20,8 +19,7 @@ type Artist struct { // NewArtist creates a new artist test element. func NewArtist () (element *Artist) { element = &Artist { } - element.Core, element.core = core.NewCore ( - element.draw, nil, nil, theme.C("testing", "artist")) + element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(480, 600) return } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index d027284..810f103 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -3,6 +3,7 @@ package testing import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" @@ -15,21 +16,33 @@ type Mouse struct { drawing bool color artist.Pattern lastMousePos image.Point + + config config.Config + theme theme.Theme + c theme.Case } // NewMouse creates a new mouse test element. func NewMouse () (element *Mouse) { - element = &Mouse { } - element.Core, element.core = core.NewCore ( - element.draw, - element.redo, - element.redo, - theme.C("testing", "mouse")) + element = &Mouse { c: theme.C("testing", "mouse") } + element.Core, element.core = core.NewCore(element.draw) element.core.SetMinimumSize(32, 32) element.color = artist.NewUniform(color.Black) return } +// SetTheme sets the element's theme. +func (element *Mouse) SetTheme (new theme.Theme) { + element.theme = new + element.redo() +} + +// SetConfig sets the element's configuration. +func (element *Mouse) SetConfig (new config.Config) { + element.config = new + element.redo() +} + func (element *Mouse) redo () { if !element.core.HasImage() { return } element.draw() @@ -38,7 +51,10 @@ func (element *Mouse) redo () { func (element *Mouse) draw () { bounds := element.Bounds() - pattern := element.core.Pattern(theme.PatternAccent, theme.PatternState { }) + pattern := element.theme.Pattern ( + theme.PatternAccent, + element.c, + theme.PatternState { }) artist.FillRectangle(element, pattern, bounds) artist.StrokeRectangle ( element, diff --git a/theme/default.go b/theme/default.go index 5d2eec6..0521dea 100644 --- a/theme/default.go +++ b/theme/default.go @@ -41,7 +41,7 @@ func (Default) Pattern ( case PatternBackground: return backgroundPattern case PatternForeground: - if state.Disabled { + if state.Disabled || c == C("basic", "spacer") { return weakForegroundPattern } else { return foregroundPattern @@ -77,6 +77,16 @@ func (Default) Pattern ( } else { return listPattern } + } else if c == C("basic", "textBox") { + if state.Disabled { + return disabledInputPattern + } else { + if state.Focused { + return selectedInputPattern + } else { + return inputPattern + } + } } else { return sunkenPattern } @@ -86,7 +96,7 @@ func (Default) Pattern ( if state.Disabled { return disabledButtonPattern } else { - if state.Pressed { + if state.Pressed || state.On && c == C("basic", "checkbox") { if state.Focused { return pressedSelectedButtonPattern } else { @@ -144,13 +154,15 @@ func (Default) Inset (pattern Pattern, c Case) Inset { switch pattern { case PatternRaised: if c == C("basic", "listEntry") { - return Inset { 2, 1, 2, 1 } + return Inset { 4, 6, 4, 6 } } else { return Inset { 1, 1, 1, 1 } } case PatternSunken: if c == C("basic", "list") { - return Inset { 4, 6, 4, 6 } + return Inset { 2, 1, 2, 1 } + } else if c == C("basic", "progressBar") { + return Inset { 2, 1, 1, 2 } } else { return Inset { 1, 1, 1, 1 } } diff --git a/theme/state.go b/theme/state.go index 51e0ab2..cd46e75 100644 --- a/theme/state.go +++ b/theme/state.go @@ -46,3 +46,33 @@ type PatternState struct { // or outline. Invalid bool } + +// FontStyle specifies stylistic alterations to a font face. +type FontStyle int; const ( + FontStyleRegular FontStyle = 0 + FontStyleBold FontStyle = 1 + FontStyleItalic FontStyle = 2 + FontStyleBoldItalic FontStyle = 1 | 2 +) + +// FontSize specifies the general size of a font face in a semantic way. +type FontSize int; const ( + // FontSizeNormal is the default font size that should be used for most + // things. + FontSizeNormal FontSize = iota + + // FontSizeLarge is a larger font size suitable for things like section + // headings. + FontSizeLarge + + // FontSizeHuge is a very large font size suitable for things like + // titles, wizard step names, digital clocks, etc. + FontSizeHuge + + // FontSizeSmall is a smaller font size. Try not to use this unless it + // makes a lot of sense to do so, because it can negatively impact + // accessibility. It is useful for things like copyright notices at the + // bottom of some window that the average user doesn't actually care + // about. + FontSizeSmall +) diff --git a/theme/theme.go b/theme/theme.go index bc0f370..5f9f8ce 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -4,36 +4,6 @@ import "image" import "golang.org/x/image/font" import "git.tebibyte.media/sashakoshka/tomo/artist" -// FontStyle specifies stylistic alterations to a font face. -type FontStyle int; const ( - FontStyleRegular FontStyle = 0 - FontStyleBold FontStyle = 1 - FontStyleItalic FontStyle = 2 - FontStyleBoldItalic FontStyle = 1 | 2 -) - -// FontSize specifies the general size of a font face in a semantic way. -type FontSize int; const ( - // FontSizeNormal is the default font size that should be used for most - // things. - FontSizeNormal FontSize = iota - - // FontSizeLarge is a larger font size suitable for things like section - // headings. - FontSizeLarge - - // FontSizeHuge is a very large font size suitable for things like - // titles, wizard step names, digital clocks, etc. - FontSizeHuge - - // FontSizeSmall is a smaller font size. Try not to use this unless it - // makes a lot of sense to do so, because it can negatively impact - // accessibility. It is useful for things like copyright notices at the - // bottom of some window that the average user doesn't actually care - // about. - FontSizeSmall -) - // Pattern lists a number of cannonical pattern types, each with its own ID. // This allows custom elements to follow themes, even those that do not // explicitly support them. @@ -98,3 +68,49 @@ type Theme interface { // sinking effect. Sink (Pattern, Case) image.Point } + +// Wrapped wraps any theme and injects a case into it automatically so that it +// doesn't need to be specified for each query. Additionally, if the underlying +// theme is nil, it just uses the default theme instead. +type Wrapped struct { + Theme + Case +} + +// FontFace returns the proper font for a given style and size. +func (wrapped Wrapped) FontFace (style FontStyle, size FontSize) font.Face { + real := wrapped.ensure() + return real.FontFace(style, size, wrapped.Case) +} + +// Icon returns an appropriate icon given an icon name. +func (wrapped Wrapped) Icon (name string) artist.Pattern { + real := wrapped.ensure() + return real.Icon(name, wrapped.Case) +} + +// Pattern returns an appropriate pattern given a pattern name and state. +func (wrapped Wrapped) Pattern (id Pattern, state PatternState) artist.Pattern { + real := wrapped.ensure() + return real.Pattern(id, wrapped.Case, state) +} + +// Inset returns the area on all sides of a given pattern that is not meant to +// be drawn on. +func (wrapped Wrapped) Inset (id Pattern) Inset { + real := wrapped.ensure() + return real.Inset(id, wrapped.Case) +} + +// Sink returns a vector that should be added to an element's inner content when +// it is pressed down (if applicable) to simulate a 3D sinking effect. +func (wrapped Wrapped) Sink (id Pattern) image.Point { + real := wrapped.ensure() + return real.Sink(id, wrapped.Case) +} + +func (wrapped Wrapped) ensure () (real Theme) { + real = wrapped.Theme + if real == nil { real = Default { } } + return +}