Added a case specifier to the theme API

This will allow themes to pull off some cool dirty tricks without
screwing anything up
This commit is contained in:
Sasha Koshka 2023-01-30 01:30:13 -05:00
parent 2c55824920
commit 174beba79f
12 changed files with 147 additions and 31 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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,

View File

@ -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 (

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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 (

View File

@ -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