From 2bd7d0fad57fcf854eaf7b82b35e0246ea191793 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 20 Apr 2023 18:40:05 -0400 Subject: [PATCH] Added a toggle button and lamp pattern --- backends/x/entity.go | 14 ++ backends/x/system.go | 7 +- default/theme/assets/wintergreen.png | Bin 1702 -> 1818 bytes default/theme/default.go | 6 +- elements/list.go | 2 - elements/togglebutton.go | 279 +++++++++++++++++++++++++++ examples/switch/main.go | 1 + theme.go | 3 + 8 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 elements/togglebutton.go diff --git a/backends/x/entity.go b/backends/x/entity.go index bcd5ee1..b8a097b 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -79,6 +79,20 @@ func (entity *entity) propagate (callback func (*entity) bool) bool { return callback(entity) } + +func (entity *entity) propagateAlt (callback func (*entity) bool) bool { + if !callback(entity) { + return false + } + + for _, child := range entity.children { + if !child.propagate(callback) { + return false + } + } + + return true +} func (entity *entity) childAt (point image.Point) *entity { for _, child := range entity.children { if point.In(child.bounds) { diff --git a/backends/x/system.go b/backends/x/system.go index ac2b5a7..fcc9e4a 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -76,7 +76,7 @@ func (system *system) focus (entity *entity) { func (system *system) focusNext () { found := system.focused == nil focused := false - system.propagate (func (entity *entity) bool { + system.propagateAlt (func (entity *entity) bool { if found { // looking for the next element to select child, ok := entity.element.(tomo.Focusable) @@ -118,6 +118,11 @@ func (system *system) propagate (callback func (*entity) bool) { system.child.propagate(callback) } +func (system *system) propagateAlt (callback func (*entity) bool) { + if system.child == nil { return } + system.child.propagateAlt(callback) +} + func (system *system) childAt (point image.Point) *entity { if system.child == nil { return nil } return system.child.childAt(point) diff --git a/default/theme/assets/wintergreen.png b/default/theme/assets/wintergreen.png index 9669391db7835572184aa52e78b1a7d48edd0634..7297649eeeb3ad250866618acd22000ec9386743 100644 GIT binary patch delta 1756 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 GoqqtAR bounds.Max.Y { - // TODO element.scroll.Y += target.Max.Y - bounds.Max.Y element.entity.Invalidate() element.entity.InvalidateLayout() diff --git a/elements/togglebutton.go b/elements/togglebutton.go new file mode 100644 index 0000000..8ee204d --- /dev/null +++ b/elements/togglebutton.go @@ -0,0 +1,279 @@ +package elements + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/default/config" +import "git.tebibyte.media/sashakoshka/tomo/textdraw" + +// ToggleButton is a togglable button. +type ToggleButton struct { + entity tomo.FocusableEntity + drawer textdraw.Drawer + + enabled bool + pressed bool + on bool + text string + + config config.Wrapped + theme theme.Wrapped + + showText bool + hasIcon bool + iconId tomo.Icon + + onToggle func () +} + +// NewToggleButton creates a new toggle button with the specified label text. +func NewToggleButton (text string, on bool) (element *ToggleButton) { + element = &ToggleButton { + showText: true, + enabled: true, + on: on, + } + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) + element.theme.Case = tomo.C("tomo", "toggleButton") + element.drawer.SetFace (element.theme.FontFace ( + tomo.FontStyleRegular, + tomo.FontSizeNormal)) + element.SetText(text) + return +} + +// Entity returns this element's entity. +func (element *ToggleButton) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *ToggleButton) Draw (destination canvas.Canvas) { + state := element.state() + bounds := element.entity.Bounds() + pattern := element.theme.Pattern(tomo.PatternButton, state) + + lampPattern := element.theme.Pattern(tomo.PatternLamp, state) + lampPadding := element.theme.Padding(tomo.PatternLamp).Horizontal() + lampBounds := bounds + lampBounds.Max.X = lampBounds.Min.X + lampPadding + bounds.Min.X += lampPadding + + pattern.Draw(destination, bounds) + lampPattern.Draw(destination, lampBounds) + + foreground := element.theme.Color(tomo.ColorForeground, state) + sink := element.theme.Sink(tomo.PatternButton) + margin := element.theme.Margin(tomo.PatternButton) + + offset := image.Pt ( + bounds.Dx() / 2, + bounds.Dy() / 2).Add(bounds.Min) + + if element.showText { + textBounds := element.drawer.LayoutBounds() + offset.X -= textBounds.Dx() / 2 + offset.Y -= textBounds.Dy() / 2 + offset.Y -= textBounds.Min.Y + offset.X -= textBounds.Min.X + } + + if element.hasIcon { + icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + if icon != nil { + iconBounds := icon.Bounds() + addedWidth := iconBounds.Dx() + iconOffset := offset + + if element.showText { + addedWidth += margin.X + } + + iconOffset.X -= addedWidth / 2 + iconOffset.Y = + bounds.Min.Y + + (bounds.Dy() - + iconBounds.Dy()) / 2 + if element.pressed { + iconOffset = iconOffset.Add(sink) + } + offset.X += addedWidth / 2 + + icon.Draw(destination, foreground, iconOffset) + } + } + + if element.showText { + if element.pressed { + offset = offset.Add(sink) + } + element.drawer.Draw(destination, foreground, offset) + } +} + +// OnToggle sets the function to be called when the button is toggled. +func (element *ToggleButton) OnToggle (callback func ()) { + element.onToggle = callback +} + +// Value reports whether or not the button is currently on. +func (element *ToggleButton) Value () (on bool) { + return element.on +} + +// Focus gives this element input focus. +func (element *ToggleButton) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this button is enabled or not. +func (element *ToggleButton) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this button can be toggled or not. +func (element *ToggleButton) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + +// SetText sets the button's label text. +func (element *ToggleButton) SetText (text string) { + if element.text == text { return } + element.text = text + element.drawer.SetText([]rune(text)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetIcon sets the icon of the button. Passing theme.IconNone removes the +// current icon if it exists. +func (element *ToggleButton) SetIcon (id tomo.Icon) { + if id == tomo.IconNone { + element.hasIcon = false + } else { + if element.hasIcon && element.iconId == id { return } + element.hasIcon = true + element.iconId = id + } + element.updateMinimumSize() + element.entity.Invalidate() +} + +// ShowText sets whether or not the button's text will be displayed. +func (element *ToggleButton) ShowText (showText bool) { + if element.showText == showText { return } + element.showText = showText + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetTheme sets the element's theme. +func (element *ToggleButton) SetTheme (new tomo.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.drawer.SetFace (element.theme.FontFace ( + tomo.FontStyleRegular, + tomo.FontSizeNormal)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetConfig sets the element's configuration. +func (element *ToggleButton) SetConfig (new tomo.Config) { + if new == element.config.Config { return } + element.config.Config = new + element.updateMinimumSize() + element.entity.Invalidate() +} + +func (element *ToggleButton) HandleFocusChange () { + element.entity.Invalidate() +} + +func (element *ToggleButton) HandleMouseDown ( + position image.Point, + button input.Button, + modifiers input.Modifiers, +) { + if !element.Enabled() { return } + element.Focus() + if button != input.ButtonLeft { return } + element.pressed = true + element.entity.Invalidate() +} + +func (element *ToggleButton) HandleMouseUp ( + position image.Point, + button input.Button, + modifiers input.Modifiers, +) { + if button != input.ButtonLeft { return } + element.pressed = false + within := position.In(element.entity.Bounds()) + if element.Enabled() && within { + element.on = !element.on + if element.onToggle != nil { + element.onToggle() + } + } + element.entity.Invalidate() +} + +func (element *ToggleButton) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + if !element.Enabled() { return } + if key == input.KeyEnter { + element.pressed = true + element.entity.Invalidate() + } +} + +func (element *ToggleButton) HandleKeyUp(key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter && element.pressed { + element.pressed = false + element.entity.Invalidate() + if !element.Enabled() { return } + element.on = !element.on + if element.onToggle != nil { + element.onToggle() + } + } +} + +func (element *ToggleButton) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternButton) + margin := element.theme.Margin(tomo.PatternButton) + lampPadding := element.theme.Padding(tomo.PatternLamp) + + textBounds := element.drawer.LayoutBounds() + minimumSize := textBounds.Sub(textBounds.Min) + + if element.hasIcon { + icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + if icon != nil { + bounds := icon.Bounds() + if element.showText { + minimumSize.Max.X += bounds.Dx() + minimumSize.Max.X += margin.X + } else { + minimumSize.Max.X = bounds.Dx() + } + } + } + + minimumSize.Max.X += lampPadding.Horizontal() + minimumSize = padding.Inverse().Apply(minimumSize) + element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) +} + +func (element *ToggleButton) state () tomo.State { + return tomo.State { + Disabled: !element.Enabled(), + Focused: element.entity.Focused(), + Pressed: element.pressed, + On: element.on, + } +} diff --git a/examples/switch/main.go b/examples/switch/main.go index 70c2b88..42cfbe5 100644 --- a/examples/switch/main.go +++ b/examples/switch/main.go @@ -18,6 +18,7 @@ func run () { container.Adopt(elements.NewSwitch("hahahah", false)) container.Adopt(elements.NewSwitch("hehehehheheh", false)) container.Adopt(elements.NewSwitch("you can flick da swicth", false)) + container.Adopt(elements.NewToggleButton("like a switch, but not", false)) window.OnClose(tomo.Stop) window.Show() diff --git a/theme.go b/theme.go index 15a651c..6cf01dd 100644 --- a/theme.go +++ b/theme.go @@ -79,6 +79,9 @@ type Pattern int; const ( // PatternTableCell is a table cell background. PatternTableCell + + // PatternLamp is an indicator light pattern. + PatternLamp ) // IconSize is a type representing valid icon sizes.