From 174beba79ffe139aa618f058913ccc2a263a1e8a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 30 Jan 2023 01:30:13 -0500 Subject: [PATCH] Added a case specifier to the theme API This will allow themes to pull off some cool dirty tricks without screwing anything up --- elements/basic/button.go | 6 ++- elements/basic/checkbox.go | 8 +++- elements/basic/container.go | 6 ++- elements/basic/label.go | 10 ++++- elements/basic/list.go | 16 ++++++-- elements/basic/listentry.go | 9 ++++- elements/basic/scrollcontainer.go | 65 ++++++++++++++++++++++++------- elements/basic/spacer.go | 4 ++ elements/basic/switch.go | 9 ++++- elements/basic/textbox.go | 13 ++++++- elements/fun/clock.go | 14 +++++-- theme/patterns.go | 18 +++++++++ 12 files changed, 147 insertions(+), 31 deletions(-) diff --git a/elements/basic/button.go b/elements/basic/button.go index 29b78a5..098e421 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -6,6 +6,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var buttonCase = theme.C("basic", "button") + // Button is a clickable button. type Button struct { *core.Core @@ -114,7 +116,7 @@ func (element *Button) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) textBounds := element.drawer.LayoutBounds() - _, inset := theme.ButtonPattern(theme.PatternState { }) + _, inset := theme.ButtonPattern(theme.PatternState { Case: buttonCase }) minimumSize := inset.Inverse().Apply(textBounds).Inset(-theme.Padding()) element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) if element.core.HasImage () { @@ -127,6 +129,7 @@ func (element *Button) draw () { bounds := element.core.Bounds() pattern, inset := theme.ButtonPattern(theme.PatternState { + Case: buttonCase, Disabled: !element.Enabled(), Selected: element.Selected(), Pressed: element.pressed, @@ -148,6 +151,7 @@ func (element *Button) draw () { offset.X -= textBounds.Min.X foreground, _ := theme.ForegroundPattern (theme.PatternState { + Case: buttonCase, Disabled: !element.Enabled(), }) element.drawer.Draw(element.core, foreground, offset) diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index 5c2f7ba..a40e582 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -6,6 +6,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var checkboxCase = theme.C("basic", "checkbox") + // Checkbox is a toggle-able checkbox with a label. type Checkbox struct { *core.Core @@ -140,10 +142,13 @@ func (element *Checkbox) draw () { bounds := element.core.Bounds() boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()) - backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { }) + backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { + Case: checkboxCase, + }) artist.FillRectangle ( element.core, backgroundPattern, bounds) pattern, inset := theme.ButtonPattern(theme.PatternState { + Case: checkboxCase, Disabled: !element.Enabled(), Selected: element.Selected(), Pressed: element.pressed, @@ -159,6 +164,7 @@ func (element *Checkbox) draw () { offset.X -= textBounds.Min.X foreground, _ := theme.ForegroundPattern (theme.PatternState { + Case: checkboxCase, Disabled: !element.Enabled(), }) element.drawer.Draw(element.core, foreground, offset) diff --git a/elements/basic/container.go b/elements/basic/container.go index 121807f..3b5e461 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -6,6 +6,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var containerCase = theme.C("basic", "container") + // Container is an element capable of containg other elements, and arranging // them in a layout. type Container struct { @@ -473,7 +475,9 @@ func (element *Container) recalculate () { func (element *Container) draw () { bounds := element.core.Bounds() - pattern, _ := theme.BackgroundPattern(theme.PatternState { }) + pattern, _ := theme.BackgroundPattern (theme.PatternState { + Case: containerCase, + }) artist.FillRectangle(element.core, pattern, bounds) for _, entry := range element.children { diff --git a/elements/basic/label.go b/elements/basic/label.go index f955a9d..d5a9e5d 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -5,6 +5,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var labelCase = theme.C("basic", "label") + // Label is a simple text box. type Label struct { *core.Core @@ -108,12 +110,16 @@ func (element *Label) updateMinimumSize () { func (element *Label) draw () { bounds := element.core.Bounds() - pattern, _ := theme.BackgroundPattern(theme.PatternState { }) + pattern, _ := theme.BackgroundPattern(theme.PatternState { + Case: labelCase, + }) artist.FillRectangle(element.core, pattern, bounds) textBounds := element.drawer.LayoutBounds() - foreground, _ := theme.ForegroundPattern (theme.PatternState { }) + foreground, _ := theme.ForegroundPattern (theme.PatternState { + Case: labelCase, + }) element.drawer.Draw (element.core, foreground, image.Point { X: 0 - textBounds.Min.X, Y: 0 - textBounds.Min.Y, diff --git a/elements/basic/list.go b/elements/basic/list.go index b8ac963..6acb191 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -7,6 +7,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var listCase = theme.C("basic", "list") + // List is an element that contains several objects that a user can select. type List struct { *core.Core @@ -164,7 +166,9 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) { } func (element *List) scrollViewportHeight () (height int) { - _, inset := theme.ListPattern(theme.PatternState { }) + _, inset := theme.ListPattern(theme.PatternState { + Case: listCase, + }) return element.Bounds().Dy() - inset[0] - inset[2] } @@ -330,7 +334,9 @@ func (element *List) changeSelectionBy (delta int) (updated bool) { } func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - _, inset := theme.ListPattern(theme.PatternState { }) + _, inset := theme.ListPattern(theme.PatternState { + Case: listCase, + }) entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1]) return entry } @@ -357,7 +363,9 @@ func (element *List) updateMinimumSize () { minimumHeight = element.contentHeight } - _, inset := theme.ListPattern(theme.PatternState { }) + _, inset := theme.ListPattern(theme.PatternState { + Case: listCase, + }) minimumWidth += inset[1] + inset[3] minimumHeight += inset[0] + inset[2] @@ -368,6 +376,7 @@ func (element *List) draw () { bounds := element.Bounds() pattern, inset := theme.ListPattern(theme.PatternState { + Case: listCase, Disabled: !element.Enabled(), Selected: element.Selected(), }) @@ -386,6 +395,7 @@ func (element *List) draw () { if element.selectedEntry == index { pattern, _ := theme.ItemPattern(theme.PatternState { + Case: listEntryCase, On: true, }) artist.FillRectangle ( diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index 84a0718..aa5da13 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -5,6 +5,8 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" +var listEntryCase = theme.C("basic", "listEntry") + // ListEntry is an item that can be added to a list. type ListEntry struct { drawer artist.TextDrawer @@ -41,7 +43,8 @@ func (entry *ListEntry) updateBounds () { entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx() } - _, inset := theme.ItemPattern(theme.PatternState { }) + _, inset := theme.ItemPattern(theme.PatternState { + }) entry.bounds.Max.Y += inset[0] + inset[2] entry.textPoint = @@ -56,7 +59,9 @@ func (entry *ListEntry) Draw ( ) ( updatedRegion image.Rectangle, ) { - foreground, _ := theme.ForegroundPattern (theme.PatternState { }) + foreground, _ := theme.ForegroundPattern (theme.PatternState { + Case: listEntryCase, + }) return entry.drawer.Draw ( destination, foreground, diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index 4f26a81..c7d5be7 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -6,6 +6,10 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var scrollContainerCase = theme.C("basic", "scrollContainer") +var scrollBarHorizontalCase = theme.C("basic", "scrollBarHorizontal") +var scrollBarVerticalCase = theme.C("basic", "scrollBarVertical") + // ScrollContainer is a container that is capable of holding a scrollable // element. type ScrollContainer struct { @@ -273,12 +277,24 @@ func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable) } func (element *ScrollContainer) recalculate () { - _, gutterInset := theme.GutterPattern(theme.PatternState { }) + _, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState { + Case: scrollBarHorizontalCase, + }) + _, gutterInsetVertical := theme.GutterPattern(theme.PatternState { + Case: scrollBarHorizontalCase, + }) horizontal := &element.horizontal vertical := &element.vertical bounds := element.Bounds() - thickness := theme.HandleWidth() + gutterInset[3] + gutterInset[1] + thicknessHorizontal := + theme.HandleWidth() + + gutterInsetHorizontal[3] + + gutterInsetHorizontal[1] + thicknessVertical := + theme.HandleWidth() + + gutterInsetVertical[3] + + gutterInsetVertical[1] // calculate child size element.childWidth = bounds.Dx() @@ -292,24 +308,24 @@ func (element *ScrollContainer) recalculate () { // if enabled, give substance to the gutters if horizontal.exists { - horizontal.gutter.Min.Y = bounds.Max.Y - thickness + horizontal.gutter.Min.Y = bounds.Max.Y - thicknessHorizontal horizontal.gutter.Max.X = bounds.Max.X horizontal.gutter.Max.Y = bounds.Max.Y if vertical.exists { - horizontal.gutter.Max.X -= thickness + horizontal.gutter.Max.X -= thicknessVertical } - element.childHeight -= thickness - horizontal.track = gutterInset.Apply(horizontal.gutter) + element.childHeight -= thicknessHorizontal + horizontal.track = gutterInsetHorizontal.Apply(horizontal.gutter) } if vertical.exists { - vertical.gutter.Min.X = bounds.Max.X - thickness + vertical.gutter.Min.X = bounds.Max.X - thicknessVertical vertical.gutter.Max.X = bounds.Max.X vertical.gutter.Max.Y = bounds.Max.Y if horizontal.exists { - vertical.gutter.Max.Y -= thickness + vertical.gutter.Max.Y -= thicknessHorizontal } - element.childWidth -= thickness - vertical.track = gutterInset.Apply(vertical.gutter) + element.childWidth -= thicknessVertical + vertical.track = gutterInsetVertical.Apply(vertical.gutter) } // if enabled, calculate the positions of the bars @@ -351,7 +367,9 @@ func (element *ScrollContainer) recalculate () { func (element *ScrollContainer) draw () { artist.Paste(element.core, element.child, image.Point { }) - deadPattern, _ := theme.DeadPattern(theme.PatternState { }) + deadPattern, _ := theme.DeadPattern(theme.PatternState { + Case: scrollContainerCase, + }) artist.FillRectangle ( element, deadPattern, image.Rect ( @@ -365,11 +383,13 @@ func (element *ScrollContainer) draw () { func (element *ScrollContainer) drawHorizontalBar () { gutterPattern, _ := theme.GutterPattern (theme.PatternState { + Case: scrollBarHorizontalCase, Disabled: !element.horizontal.enabled, }) artist.FillRectangle(element, gutterPattern, element.horizontal.gutter) handlePattern, _ := theme.HandlePattern (theme.PatternState { + Case: scrollBarHorizontalCase, Disabled: !element.horizontal.enabled, Pressed: element.horizontal.dragging, }) @@ -378,11 +398,13 @@ func (element *ScrollContainer) drawHorizontalBar () { func (element *ScrollContainer) drawVerticalBar () { gutterPattern, _ := theme.GutterPattern (theme.PatternState { + Case: scrollBarVerticalCase, Disabled: !element.vertical.enabled, }) artist.FillRectangle(element, gutterPattern, element.vertical.gutter) handlePattern, _ := theme.HandlePattern (theme.PatternState { + Case: scrollBarVerticalCase, Disabled: !element.vertical.enabled, Pressed: element.vertical.dragging, }) @@ -408,11 +430,24 @@ func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) { } func (element *ScrollContainer) updateMinimumSize () { - _, gutterInset := theme.GutterPattern(theme.PatternState { }) - thickness := theme.HandleWidth() + gutterInset[3] + gutterInset[1] + _, gutterInsetHorizontal := theme.GutterPattern(theme.PatternState { + Case: scrollBarHorizontalCase, + }) + _, gutterInsetVertical := theme.GutterPattern(theme.PatternState { + Case: scrollBarHorizontalCase, + }) + + thicknessHorizontal := + theme.HandleWidth() + + gutterInsetHorizontal[3] + + gutterInsetHorizontal[1] + thicknessVertical := + theme.HandleWidth() + + gutterInsetVertical[3] + + gutterInsetVertical[1] - width := thickness - height := thickness + width := thicknessHorizontal + height := thicknessVertical if element.child != nil { childWidth, childHeight := element.child.MinimumSize() width += childWidth diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index a5df990..31883e5 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -4,6 +4,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var spacerCase = theme.C("basic", "spacer") + // Spacer can be used to put space between two elements.. type Spacer struct { *core.Core @@ -43,11 +45,13 @@ func (element *Spacer) draw () { if element.line { pattern, _ := theme.ForegroundPattern(theme.PatternState { + Case: spacerCase, Disabled: true, }) artist.FillRectangle(element.core, pattern, bounds) } else { pattern, _ := theme.BackgroundPattern(theme.PatternState { + Case: spacerCase, Disabled: true, }) artist.FillRectangle(element.core, pattern, bounds) diff --git a/elements/basic/switch.go b/elements/basic/switch.go index c1244bc..c7e073a 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -6,6 +6,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var switchCase = theme.C("basic", "switch") + // Switch is a toggle-able on/off switch with an optional label. It is // functionally identical to Checkbox, but plays a different semantic role. type Switch struct { @@ -147,7 +149,9 @@ func (element *Switch) draw () { bounds := element.core.Bounds() handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()) gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()) - backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { }) + backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { + Case: switchCase, + }) artist.FillRectangle ( element.core, backgroundPattern, bounds) if element.checked { @@ -165,6 +169,7 @@ func (element *Switch) draw () { } gutterPattern, _ := theme.GutterPattern(theme.PatternState { + Case: switchCase, Disabled: !element.Enabled(), Selected: element.Selected(), Pressed: element.pressed, @@ -172,6 +177,7 @@ func (element *Switch) draw () { artist.FillRectangle(element.core, gutterPattern, gutterBounds) handlePattern, _ := theme.HandlePattern(theme.PatternState { + Case: switchCase, Disabled: !element.Enabled(), Selected: element.Selected(), Pressed: element.pressed, @@ -187,6 +193,7 @@ func (element *Switch) draw () { offset.X -= textBounds.Min.X foreground, _ := theme.ForegroundPattern (theme.PatternState { + Case: switchCase, Disabled: !element.Enabled(), }) element.drawer.Draw(element.core, foreground, offset) diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index cd2136f..de78bfe 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -7,6 +7,8 @@ import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var textBoxCase = theme.C("basic", "textBox") + // TextBox is a single-line text input. type TextBox struct { *core.Core @@ -237,7 +239,9 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) { func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() - _, inset := theme.InputPattern(theme.PatternState { }) + _, inset := theme.InputPattern(theme.PatternState { + Case: textBoxCase, + }) element.core.SetMinimumSize ( textBounds.Dx() + theme.Padding() * 2 + inset[3] + inset[1], @@ -273,6 +277,7 @@ func (element *TextBox) draw () { // FIXME: take index into account pattern, inset := theme.InputPattern(theme.PatternState { + Case: textBoxCase, Disabled: !element.Enabled(), Selected: element.Selected(), }) @@ -286,6 +291,7 @@ func (element *TextBox) draw () { Y: theme.Padding() + inset[0], } foreground, _ := theme.ForegroundPattern(theme.PatternState { + Case: textBoxCase, Disabled: true, }) element.placeholderDrawer.Draw ( @@ -300,6 +306,7 @@ func (element *TextBox) draw () { Y: theme.Padding() + inset[0], } foreground, _ := theme.ForegroundPattern(theme.PatternState { + Case: textBoxCase, Disabled: !element.Enabled(), }) element.valueDrawer.Draw ( @@ -311,7 +318,9 @@ func (element *TextBox) draw () { // cursor cursorPosition := element.valueDrawer.PositionOf ( element.cursor) - foreground, _ := theme.ForegroundPattern(theme.PatternState { }) + foreground, _ := theme.ForegroundPattern(theme.PatternState { + Case: textBoxCase, + }) artist.Line ( element.core, foreground, 1, diff --git a/elements/fun/clock.go b/elements/fun/clock.go index 8a7a537..8bc71f7 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -7,6 +7,8 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements/core" +var clockCase = theme.C("fun", "clock") + // AnalogClock can display the time of day in an analog format. type AnalogClock struct { *core.Core @@ -41,13 +43,19 @@ func (element *AnalogClock) SetTime (newTime time.Time) { func (element *AnalogClock) draw () { bounds := element.core.Bounds() - pattern, inset := theme.SunkenPattern(theme.PatternState { }) + pattern, inset := theme.SunkenPattern(theme.PatternState { + Case: clockCase, + }) artist.FillRectangle(element, pattern, bounds) bounds = inset.Apply(bounds) - foreground, _ := theme.ForegroundPattern(theme.PatternState { }) - accent, _ := theme.AccentPattern(theme.PatternState { }) + foreground, _ := theme.ForegroundPattern(theme.PatternState { + Case: clockCase, + }) + accent, _ := theme.AccentPattern(theme.PatternState { + Case: clockCase, + }) for hour := 0; hour < 12; hour ++ { element.radialLine ( diff --git a/theme/patterns.go b/theme/patterns.go index 14b15c2..b81f46d 100644 --- a/theme/patterns.go +++ b/theme/patterns.go @@ -3,10 +3,28 @@ package theme import "image" import "git.tebibyte.media/sashakoshka/tomo/artist" +// Case sepecifies what kind of element is using a pattern. It contains a +// namespace parameter and an element parameter. The element parameter does not +// necissarily need to match an element name, but if it can, it should. Both +// parameters should be written in camel case. Themes can change their styling +// based on this parameter for fine-grained control over the look and feel of +// specific elements. +type Case struct { Namespace, Element string } + +// C can be used as shorthand to generate a case struct as used in PatternState. +func C (namespace, element string) (c Case) { + return Case { + Namespace: namespace, + Element: element, + } +} + // PatternState lists parameters which can change the appearance of some // patterns. For example, passing a PatternState with Selected set to true may // result in a pattern that has a colored border within it. type PatternState struct { + Case + // On should be set to true if the element that is using this pattern is // in some sort of "on" state, such as if a checkbox is checked or a // switch is toggled on. This is only necessary if the element in