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 9669391..7297649 100644 Binary files a/default/theme/assets/wintergreen.png and b/default/theme/assets/wintergreen.png differ diff --git a/default/theme/default.go b/default/theme/default.go index 1cb4c69..39923ed 100644 --- a/default/theme/default.go +++ b/default/theme/default.go @@ -16,7 +16,7 @@ import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" //go:embed assets/wintergreen.png var defaultAtlasBytes []byte var defaultAtlas canvas.Canvas -var defaultTextures [16][9]artist.Pattern +var defaultTextures [17][9]artist.Pattern //go:embed assets/wintergreen-icons-small.png var defaultIconsSmallAtlasBytes []byte var defaultIconsSmall [640]binaryIcon @@ -111,6 +111,8 @@ func init () { atlasCol(14, artist.Inset { 4, 4, 4, 4 }) // PatternTableCell: atlasCol(15, artist.Inset { 4, 4, 4, 4 }) + // PatternLamp: + atlasCol(16, artist.Inset { 4, 3, 4, 3 }) // PatternButton: basic.checkbox atlasCol(9, artist.Inset { 3, 3, 3, 3 }) @@ -233,6 +235,7 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P case tomo.PatternMercury: return defaultTextures[13][offset] case tomo.PatternTableHead: return defaultTextures[14][offset] case tomo.PatternTableCell: return defaultTextures[15][offset] + case tomo.PatternLamp: return defaultTextures[16][offset] default: return patterns.Uhex(0xFF00FFFF) } } @@ -284,6 +287,7 @@ func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset { case tomo.PatternGutter: return artist.I(0) case tomo.PatternLine: return artist.I(1) case tomo.PatternMercury: return artist.I(5) + case tomo.PatternLamp: return artist.I(5, 5, 5, 6) default: return artist.I(8) } } diff --git a/elements/list.go b/elements/list.go index a1b1bc4..2ef3f4f 100644 --- a/elements/list.go +++ b/elements/list.go @@ -262,12 +262,10 @@ func (element *List) scrollToSelected () { padding := element.theme.Padding(tomo.PatternBackground) bounds := padding.Apply(element.entity.Bounds()) if target.Min.Y < bounds.Min.Y { - // TODO element.scroll.Y -= bounds.Min.Y - target.Min.Y element.entity.Invalidate() element.entity.InvalidateLayout() } else if target.Max.Y > 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.