Overhauled the theme system

Also added a toggle switch :)
This commit is contained in:
2023-01-29 01:49:01 -05:00
parent 9422ff6198
commit 92aeb48a1f
20 changed files with 611 additions and 251 deletions

View File

@@ -114,9 +114,9 @@ func (element *Button) SetText (text string) {
element.text = text
element.drawer.SetText([]rune(text))
textBounds := element.drawer.LayoutBounds()
element.core.SetMinimumSize (
theme.Padding() * 2 + textBounds.Dx(),
theme.Padding() * 2 + textBounds.Dy())
_, inset := theme.ButtonPattern(theme.PatternState { })
minimumSize := inset.Inverse().Apply(textBounds).Inset(-theme.Padding())
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
@@ -126,24 +126,20 @@ func (element *Button) SetText (text string) {
func (element *Button) draw () {
bounds := element.core.Bounds()
artist.FillRectangle (
element.core,
theme.ButtonPattern (
element.Enabled(),
element.Selected(),
element.pressed),
bounds)
pattern, inset := theme.ButtonPattern(theme.PatternState {
Disabled: !element.Enabled(),
Selected: element.Selected(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, pattern, bounds)
innerBounds := bounds
innerBounds.Min.X += theme.Padding()
innerBounds.Min.Y += theme.Padding()
innerBounds.Max.X -= theme.Padding()
innerBounds.Max.Y -= theme.Padding()
innerBounds := inset.Apply(bounds)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
X: theme.Padding() + (innerBounds.Dx() - textBounds.Dx()) / 2,
Y: theme.Padding() + (innerBounds.Dy() - textBounds.Dy()) / 2,
X: innerBounds.Min.X + (innerBounds.Dx() - textBounds.Dx()) / 2,
Y: innerBounds.Min.X + (innerBounds.Dy() - textBounds.Dy()) / 2,
}
// account for the fact that the bounding rectangle will be shifted over
@@ -151,10 +147,8 @@ func (element *Button) draw () {
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
if element.pressed {
offset = offset.Add(theme.SinkOffsetVector())
}
foreground := theme.ForegroundPattern(element.Enabled())
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Disabled: !element.Enabled(),
})
element.drawer.Draw(element.core, foreground, offset)
}

View File

@@ -140,14 +140,15 @@ func (element *Checkbox) draw () {
bounds := element.core.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy())
artist.FillRectangle ( element.core, theme.BackgroundPattern(), bounds)
artist.FillRectangle (
element.core,
theme.ButtonPattern (
element.Enabled(),
element.Selected(),
element.pressed),
boxBounds)
backgroundPattern, _ := theme.BackgroundPattern(theme.PatternState { })
artist.FillRectangle ( element.core, backgroundPattern, bounds)
pattern, inset := theme.ButtonPattern(theme.PatternState {
Disabled: !element.Enabled(),
Selected: element.Selected(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, pattern, boxBounds)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
@@ -157,17 +158,13 @@ func (element *Checkbox) draw () {
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := theme.ForegroundPattern(element.Enabled())
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Disabled: !element.Enabled(),
})
element.drawer.Draw(element.core, foreground, offset)
if element.checked {
checkBounds := boxBounds.Inset(4)
if element.pressed {
checkBounds = checkBounds.Add(theme.SinkOffsetVector())
}
artist.FillRectangle (
element.core,
theme.ForegroundPattern(element.Enabled()),
checkBounds)
checkBounds := inset.Apply(boxBounds).Inset(2)
artist.FillRectangle(element.core, foreground, checkBounds)
}
}

View File

@@ -473,10 +473,8 @@ func (element *Container) recalculate () {
func (element *Container) draw () {
bounds := element.core.Bounds()
artist.FillRectangle (
element.core,
theme.BackgroundPattern(),
bounds)
pattern, _ := theme.BackgroundPattern(theme.PatternState { })
artist.FillRectangle(element.core, pattern, bounds)
for _, entry := range element.children {
artist.Paste(element.core, entry, entry.Position)

View File

@@ -108,14 +108,12 @@ func (element *Label) updateMinimumSize () {
func (element *Label) draw () {
bounds := element.core.Bounds()
artist.FillRectangle (
element.core,
theme.BackgroundPattern(),
bounds)
pattern, _ := theme.BackgroundPattern(theme.PatternState { })
artist.FillRectangle(element.core, pattern, bounds)
textBounds := element.drawer.LayoutBounds()
foreground := theme.ForegroundPattern(true)
foreground, _ := theme.ForegroundPattern (theme.PatternState { })
element.drawer.Draw (element.core, foreground, image.Point {
X: 0 - textBounds.Min.X,
Y: 0 - textBounds.Min.Y,

View File

@@ -164,7 +164,8 @@ func (element *List) ScrollAxes () (horizontal, vertical bool) {
}
func (element *List) scrollViewportHeight () (height int) {
return element.Bounds().Dy() - theme.Padding()
_, inset := theme.ListPattern(theme.PatternState { })
return element.Bounds().Dy() - theme.Padding() - inset[0] - inset[2]
}
func (element *List) maxScrollHeight () (height int) {
@@ -328,7 +329,8 @@ func (element *List) changeSelectionBy (delta int) (updated bool) {
}
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
entry.Collapse(element.forcedMinimumWidth)
_, inset := theme.ListPattern(theme.PatternState { })
entry.Collapse(element.forcedMinimumWidth - inset[3] - inset[1])
return entry
}
@@ -355,17 +357,24 @@ func (element *List) updateMinimumSize () {
minimumHeight = element.contentHeight + theme.Padding()
}
_, inset := theme.ListPattern(theme.PatternState { })
minimumWidth += inset[1] + inset[3]
minimumHeight += inset[0] + inset[2]
element.core.SetMinimumSize(minimumWidth, minimumHeight)
}
func (element *List) draw () {
bounds := element.Bounds()
artist.FillRectangle (
element,
theme.ListPattern(element.Selected()),
bounds)
pattern, inset := theme.ListPattern(theme.PatternState {
Disabled: !element.Enabled(),
Selected: element.Selected(),
})
artist.FillRectangle(element.core, pattern, bounds)
bounds = inset.Apply(bounds)
dot := image.Point {
bounds.Min.X,
bounds.Min.Y - element.scroll + theme.Padding() / 2,
@@ -377,9 +386,12 @@ func (element *List) draw () {
if entryPosition.Y > bounds.Max.Y { break }
if element.selectedEntry == index {
pattern, _ := theme.ItemPattern(theme.PatternState {
On: true,
})
artist.FillRectangle (
element,
theme.ListEntryPattern(true),
pattern,
entry.Bounds().Add(entryPosition))
}
entry.Draw (

View File

@@ -44,8 +44,11 @@ func (entry *ListEntry) updateBounds () {
entry.drawer.LayoutBounds().Dx() + padding * 2
}
_, inset := theme.ItemPattern(theme.PatternState { })
entry.bounds.Max.Y += inset[0] + inset[2]
entry.textPoint =
image.Pt(padding, padding / 2).
image.Pt(inset[3] + padding, inset[0] + padding / 2).
Sub(entry.drawer.LayoutBounds().Min)
}
@@ -56,9 +59,10 @@ func (entry *ListEntry) Draw (
) (
updatedRegion image.Rectangle,
) {
foreground, _ := theme.ForegroundPattern (theme.PatternState { })
return entry.drawer.Draw (
destination,
theme.ForegroundPattern(true),
foreground,
offset.Add(entry.textPoint))
}

View File

@@ -41,16 +41,13 @@ func (element *ProgressBar) SetProgress (progress float64) {
func (element *ProgressBar) draw () {
bounds := element.core.Bounds()
artist.FillRectangle (
element.core,
theme.SunkenPattern(false),
bounds)
pattern, inset := theme.SunkenPattern(theme.PatternState { })
artist.FillRectangle(element.core, pattern, bounds)
bounds = inset.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X + 2, bounds.Min.Y + 2,
bounds.Min.X - 1 + int(float64(bounds.Dx()) * element.progress),
bounds.Dy() - 1)
artist.FillRectangle (
element.core,
theme.AccentPattern(),
meterBounds)
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
accent, _ := theme.AccentPattern(theme.PatternState { })
artist.FillRectangle(element.core, accent, meterBounds)
}

View File

@@ -22,6 +22,7 @@ type ScrollContainer struct {
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
@@ -31,6 +32,7 @@ type ScrollContainer struct {
dragging bool
dragOffset int
gutter image.Rectangle
track image.Rectangle
bar image.Rectangle
}
@@ -271,10 +273,12 @@ func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable)
}
func (element *ScrollContainer) recalculate () {
_, gutterInset := theme.GutterPattern(theme.PatternState { })
horizontal := &element.horizontal
vertical := &element.vertical
bounds := element.Bounds()
thickness := theme.ScrollBarWidth()
thickness := theme.HandleWidth() + gutterInset[3] + gutterInset[1]
// calculate child size
element.childWidth = bounds.Dx()
@@ -295,6 +299,7 @@ func (element *ScrollContainer) recalculate () {
horizontal.gutter.Max.X -= thickness
}
element.childHeight -= thickness
horizontal.track = gutterInset.Apply(horizontal.gutter)
}
if vertical.exists {
vertical.gutter.Min.X = bounds.Max.X - thickness
@@ -304,49 +309,51 @@ func (element *ScrollContainer) recalculate () {
vertical.gutter.Max.Y -= thickness
}
element.childWidth -= thickness
vertical.track = gutterInset.Apply(vertical.gutter)
}
// if enabled, calculate the positions of the bars
contentBounds := element.child.ScrollContentBounds()
viewportBounds := element.child.ScrollViewportBounds()
if horizontal.exists && horizontal.enabled {
horizontal.bar.Min.Y = horizontal.gutter.Min.Y
horizontal.bar.Max.Y = horizontal.gutter.Max.Y
horizontal.bar.Min.Y = horizontal.track.Min.Y
horizontal.bar.Max.Y = horizontal.track.Max.Y
scale := float64(horizontal.gutter.Dx()) /
scale := float64(horizontal.track.Dx()) /
float64(contentBounds.Dx())
horizontal.bar.Min.X = int(float64(viewportBounds.Min.X) * scale)
horizontal.bar.Max.X = int(float64(viewportBounds.Max.X) * scale)
horizontal.bar.Min.X += horizontal.gutter.Min.X
horizontal.bar.Max.X += horizontal.gutter.Min.X
horizontal.bar.Min.X += horizontal.track.Min.X
horizontal.bar.Max.X += horizontal.track.Min.X
}
if vertical.exists && vertical.enabled {
vertical.bar.Min.X = vertical.gutter.Min.X
vertical.bar.Max.X = vertical.gutter.Max.X
vertical.bar.Min.X = vertical.track.Min.X
vertical.bar.Max.X = vertical.track.Max.X
scale := float64(vertical.gutter.Dy()) /
scale := float64(vertical.track.Dy()) /
float64(contentBounds.Dy())
vertical.bar.Min.Y = int(float64(viewportBounds.Min.Y) * scale)
vertical.bar.Max.Y = int(float64(viewportBounds.Max.Y) * scale)
vertical.bar.Min.Y += vertical.gutter.Min.Y
vertical.bar.Max.Y += vertical.gutter.Min.Y
vertical.bar.Min.Y += vertical.track.Min.Y
vertical.bar.Max.Y += vertical.track.Min.Y
}
// if the scroll bars are out of bounds, don't display them.
if horizontal.bar.Dx() >= horizontal.gutter.Dx() {
if horizontal.bar.Dx() >= horizontal.track.Dx() {
horizontal.bar = image.Rectangle { }
}
if vertical.bar.Dy() >= vertical.gutter.Dy() {
if vertical.bar.Dy() >= vertical.track.Dy() {
vertical.bar = image.Rectangle { }
}
}
func (element *ScrollContainer) draw () {
artist.Paste(element.core, element.child, image.Point { })
deadPattern, _ := theme.DeadPattern(theme.PatternState { })
artist.FillRectangle (
element, theme.DeadPattern(),
element, deadPattern,
image.Rect (
element.vertical.gutter.Min.X,
element.horizontal.gutter.Min.Y,
@@ -357,35 +364,35 @@ func (element *ScrollContainer) draw () {
}
func (element *ScrollContainer) drawHorizontalBar () {
artist.FillRectangle (
element,
theme.ScrollGutterPattern(true, element.horizontal.enabled),
element.horizontal.gutter)
artist.FillRectangle (
element,
theme.ScrollBarPattern (
true, element.horizontal.enabled,
element.horizontal.dragging),
element.horizontal.bar)
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
Disabled: !element.horizontal.enabled,
})
artist.FillRectangle(element, gutterPattern, element.horizontal.gutter)
handlePattern, _ := theme.HandlePattern (theme.PatternState {
Disabled: !element.horizontal.enabled,
Pressed: element.horizontal.dragging,
})
artist.FillRectangle(element, handlePattern, element.horizontal.bar)
}
func (element *ScrollContainer) drawVerticalBar () {
artist.FillRectangle (
element,
theme.ScrollGutterPattern(false, element.vertical.enabled),
element.vertical.gutter)
artist.FillRectangle (
element,
theme.ScrollBarPattern (
false, element.vertical.enabled,
element.vertical.dragging),
element.vertical.bar)
gutterPattern, _ := theme.GutterPattern (theme.PatternState {
Disabled: !element.vertical.enabled,
})
artist.FillRectangle(element, gutterPattern, element.vertical.gutter)
handlePattern, _ := theme.HandlePattern (theme.PatternState {
Disabled: !element.vertical.enabled,
Pressed: element.vertical.dragging,
})
artist.FillRectangle(element, handlePattern, element.vertical.bar)
}
func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
scrollX :=
float64(element.child.ScrollContentBounds().Dx()) /
float64(element.horizontal.gutter.Dx()) *
float64(element.horizontal.track.Dx()) *
float64(mousePosition.X - element.horizontal.dragOffset)
scrollY := element.child.ScrollViewportBounds().Min.Y
element.child.ScrollTo(image.Pt(int(scrollX), scrollY))
@@ -394,15 +401,18 @@ func (element *ScrollContainer) dragHorizontalBar (mousePosition image.Point) {
func (element *ScrollContainer) dragVerticalBar (mousePosition image.Point) {
scrollY :=
float64(element.child.ScrollContentBounds().Dy()) /
float64(element.vertical.gutter.Dy()) *
float64(element.vertical.track.Dy()) *
float64(mousePosition.Y - element.vertical.dragOffset)
scrollX := element.child.ScrollViewportBounds().Min.X
element.child.ScrollTo(image.Pt(scrollX, int(scrollY)))
}
func (element *ScrollContainer) updateMinimumSize () {
width := theme.ScrollBarWidth()
height := theme.ScrollBarWidth()
_, gutterInset := theme.GutterPattern(theme.PatternState { })
thickness := theme.HandleWidth() + gutterInset[3] + gutterInset[1]
width := thickness
height := thickness
if element.child != nil {
childWidth, childHeight := element.child.MinimumSize()
width += childWidth

View File

@@ -42,14 +42,14 @@ func (element *Spacer) draw () {
bounds := element.core.Bounds()
if element.line {
artist.FillRectangle (
element.core,
theme.ForegroundPattern(false),
bounds)
pattern, _ := theme.ForegroundPattern(theme.PatternState {
Disabled: true,
})
artist.FillRectangle(element.core, pattern, bounds)
} else {
artist.FillRectangle (
element.core,
theme.BackgroundPattern(),
bounds)
pattern, _ := theme.BackgroundPattern(theme.PatternState {
Disabled: true,
})
artist.FillRectangle(element.core, pattern, bounds)
}
}

View File

@@ -1 +1,193 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// 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 {
*core.Core
*core.SelectableCore
core core.CoreControl
selectableControl core.SelectableCoreControl
drawer artist.TextDrawer
pressed bool
checked bool
text string
onToggle func ()
}
// NewSwitch creates a new switch with the specified label text.
func NewSwitch (text string, on bool) (element *Switch) {
element = &Switch { checked: on, text: text }
element.Core, element.core = core.NewCore(element)
element.SelectableCore,
element.selectableControl = core.NewSelectableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.drawer.SetFace(theme.FontFaceRegular())
element.drawer.SetText([]rune(text))
element.calculateMinimumSize()
return
}
// Resize changes this element's size.
func (element *Switch) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
}
func (element *Switch) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
element.Select()
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Switch) HandleMouseUp (x, y int, button tomo.Button) {
if button != tomo.ButtonLeft || !element.pressed { return }
element.pressed = false
within := image.Point { x, y }.
In(element.Bounds())
if within {
element.checked = !element.checked
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Switch) HandleMouseMove (x, y int) { }
func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Switch) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
func (element *Switch) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onToggle != nil {
element.onToggle()
}
}
}
// OnToggle sets the function to be called when the switch is flipped.
func (element *Switch) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the switch is currently on.
func (element *Switch) Value () (on bool) {
return element.checked
}
// SetEnabled sets whether this switch can be flipped or not.
func (element *Switch) SetEnabled (enabled bool) {
element.selectableControl.SetEnabled(enabled)
}
// SetText sets the checkbox's label text.
func (element *Switch) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.calculateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Switch) calculateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
lineHeight := element.drawer.LineHeight().Round()
if element.text == "" {
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
} else {
element.core.SetMinimumSize (
lineHeight * 2 + theme.Padding() + textBounds.Dx(),
lineHeight)
}
}
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 { })
artist.FillRectangle ( element.core, backgroundPattern, bounds)
if element.checked {
handleBounds.Min.X += bounds.Dy()
handleBounds.Max.X += bounds.Dy()
if element.pressed {
handleBounds.Min.X -= 2
handleBounds.Max.X -= 2
}
} else {
if element.pressed {
handleBounds.Min.X += 2
handleBounds.Max.X += 2
}
}
gutterPattern, _ := theme.GutterPattern(theme.PatternState {
Disabled: !element.Enabled(),
Selected: element.Selected(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, gutterPattern, gutterBounds)
handlePattern, _ := theme.HandlePattern(theme.PatternState {
Disabled: !element.Enabled(),
Selected: element.Selected(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, handlePattern, handleBounds)
textBounds := element.drawer.LayoutBounds()
offset := image.Point {
X: bounds.Dy() * 2 + theme.Padding(),
}
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Disabled: !element.Enabled(),
})
element.drawer.Draw(element.core, foreground, offset)
}

View File

@@ -237,11 +237,12 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) {
func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds()
_, inset := theme.InputPattern(theme.PatternState { })
element.core.SetMinimumSize (
textBounds.Dx() +
theme.Padding() * 2,
theme.Padding() * 2 + inset[3] + inset[1],
element.placeholderDrawer.LineHeight().Round() +
theme.Padding() * 2)
theme.Padding() * 2 + inset[0] + inset[2])
}
func (element *TextBox) runOnChange () {
@@ -270,21 +271,23 @@ func (element *TextBox) scrollToCursor () {
func (element *TextBox) draw () {
bounds := element.core.Bounds()
artist.FillRectangle (
element.core,
theme.InputPattern (
element.Enabled(),
element.Selected()),
bounds)
// FIXME: take index into account
pattern, inset := theme.InputPattern(theme.PatternState {
Disabled: !element.Enabled(),
Selected: element.Selected(),
})
artist.FillRectangle(element.core, pattern, bounds)
if len(element.text) == 0 && !element.Selected() {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
offset := image.Point {
X: theme.Padding(),
Y: theme.Padding(),
X: theme.Padding() + inset[3],
Y: theme.Padding() + inset[0],
}
foreground := theme.ForegroundPattern(false)
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Disabled: true,
})
element.placeholderDrawer.Draw (
element.core,
foreground,
@@ -293,10 +296,12 @@ func (element *TextBox) draw () {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
offset := image.Point {
X: theme.Padding() - element.scroll,
Y: theme.Padding(),
X: theme.Padding() + inset[3] - element.scroll,
Y: theme.Padding() + inset[0],
}
foreground := theme.ForegroundPattern(element.Enabled())
foreground, _ := theme.ForegroundPattern(theme.PatternState {
Disabled: !element.Enabled(),
})
element.valueDrawer.Draw (
element.core,
foreground,
@@ -306,9 +311,10 @@ func (element *TextBox) draw () {
// cursor
cursorPosition := element.valueDrawer.PositionOf (
element.cursor)
foreground, _ := theme.ForegroundPattern(theme.PatternState { })
artist.Line (
element.core,
theme.ForegroundPattern(true), 1,
foreground, 1,
cursorPosition.Add(offset),
image.Pt (
cursorPosition.X,