From 9422ff619899bf5aef4d011aa90afe866b52062f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 27 Jan 2023 17:55:49 -0500 Subject: [PATCH] Added a selectability core to reduce complexity of selectables --- elements/basic/button.go | 91 ++++++-------------------- elements/basic/checkbox.go | 124 +++++++++++------------------------- elements/basic/list.go | 85 +++++------------------- elements/basic/switch.go | 1 + elements/basic/textbox.go | 84 +++++------------------- elements/core/selectable.go | 111 ++++++++++++++++++++++++++++++++ examples/checkbox/main.go | 2 +- 7 files changed, 203 insertions(+), 295 deletions(-) create mode 100644 elements/basic/switch.go create mode 100644 elements/core/selectable.go diff --git a/elements/basic/button.go b/elements/basic/button.go index 01a8ab0..f201c45 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -9,24 +9,28 @@ import "git.tebibyte.media/sashakoshka/tomo/elements/core" // Button is a clickable button. type Button struct { *core.Core + *core.SelectableCore core core.CoreControl - - pressed bool - enabled bool - selected bool - - text string + selectableControl core.SelectableCoreControl drawer artist.TextDrawer + + pressed bool + text string onClick func () - onSelectionRequest func () (granted bool) - onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) } // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { - element = &Button { enabled: true } + element = &Button { } 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.SetText(text) return @@ -38,8 +42,8 @@ func (element *Button) Resize (width, height int) { } func (element *Button) HandleMouseDown (x, y int, button tomo.Button) { - if !element.enabled { return } - if !element.selected { element.Select() } + if !element.Enabled() { return } + if !element.Selected() { element.Select() } if button != tomo.ButtonLeft { return } element.pressed = true if element.core.HasImage() { @@ -59,7 +63,7 @@ func (element *Button) HandleMouseUp (x, y int, button tomo.Button) { within := image.Point { x, y }. In(element.Bounds()) - if !element.enabled { return } + if !element.Enabled() { return } if within && element.onClick != nil { element.onClick() } @@ -69,7 +73,7 @@ func (element *Button) HandleMouseMove (x, y int) { } func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Button) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { - if !element.enabled { return } + if !element.Enabled() { return } if key == tomo.KeyEnter { element.pressed = true if element.core.HasImage() { @@ -86,61 +90,13 @@ func (element *Button) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { element.draw() element.core.DamageAll() } - if !element.enabled { return } + if !element.Enabled() { return } if element.onClick != nil { element.onClick() } } } -func (element *Button) Selected () (selected bool) { - return element.selected -} - -func (element *Button) Select () { - if !element.enabled { return } - if element.onSelectionRequest != nil { - element.onSelectionRequest() - } -} - -func (element *Button) HandleSelection ( - direction tomo.SelectionDirection, -) ( - accepted bool, -) { - direction = direction.Canon() - if !element.enabled { return false } - if element.selected && direction != tomo.SelectionDirectionNeutral { - return false - } - - element.selected = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - return true -} - -func (element *Button) HandleDeselection () { - element.selected = false - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *Button) OnSelectionRequest (callback func () (granted bool)) { - element.onSelectionRequest = callback -} - -func (element *Button) OnSelectionMotionRequest ( - callback func (direction tomo.SelectionDirection) (granted bool), -) { - element.onSelectionMotionRequest = callback -} - // OnClick sets the function to be called when the button is clicked. func (element *Button) OnClick (callback func ()) { element.onClick = callback @@ -148,12 +104,7 @@ func (element *Button) OnClick (callback func ()) { // SetEnabled sets whether this button can be clicked or not. func (element *Button) SetEnabled (enabled bool) { - if element.enabled == enabled { return } - element.enabled = enabled - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.selectableControl.SetEnabled(enabled) } // SetText sets the button's label text. @@ -178,7 +129,7 @@ func (element *Button) draw () { artist.FillRectangle ( element.core, theme.ButtonPattern ( - element.enabled, + element.Enabled(), element.Selected(), element.pressed), bounds) @@ -204,6 +155,6 @@ func (element *Button) draw () { offset = offset.Add(theme.SinkOffsetVector()) } - foreground := theme.ForegroundPattern(element.enabled) + foreground := theme.ForegroundPattern(element.Enabled()) element.drawer.Draw(element.core, foreground, offset) } diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index ad45b20..1ea21b8 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -9,25 +9,29 @@ import "git.tebibyte.media/sashakoshka/tomo/elements/core" // Checkbox is a toggle-able checkbox with a label. type Checkbox struct { *core.Core + *core.SelectableCore core core.CoreControl - - pressed bool - checked bool - enabled bool - selected bool - - text string + selectableControl core.SelectableCoreControl drawer artist.TextDrawer + + pressed bool + checked bool + text string - onClick func () - onSelectionRequest func () (granted bool) - onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) + onToggle func () } // NewCheckbox creates a new cbeckbox with the specified label text. func NewCheckbox (text string, checked bool) (element *Checkbox) { - element = &Checkbox { enabled: true, checked: checked } + element = &Checkbox { checked: checked } 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.SetText(text) return @@ -40,6 +44,7 @@ func (element *Checkbox) Resize (width, height int) { } func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) { + if !element.Enabled() { return } element.Select() element.pressed = true if element.core.HasImage() { @@ -49,7 +54,7 @@ func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) { } func (element *Checkbox) HandleMouseUp (x, y int, button tomo.Button) { - if button != tomo.ButtonLeft { return } + if button != tomo.ButtonLeft || !element.pressed { return } element.pressed = false within := image.Point { x, y }. @@ -62,8 +67,8 @@ func (element *Checkbox) HandleMouseUp (x, y int, button tomo.Button) { element.draw() element.core.DamageAll() } - if within && element.onClick != nil { - element.onClick() + if within && element.onToggle != nil { + element.onToggle() } } @@ -88,65 +93,15 @@ func (element *Checkbox) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { element.draw() element.core.DamageAll() } - if element.onClick != nil { - element.onClick() + if element.onToggle != nil { + element.onToggle() } } } -// Selected returns whether or not this element is selected. -func (element *Checkbox) Selected () (selected bool) { - return element.selected -} - -// Select requests that this element be selected. -func (element *Checkbox) Select () { - if !element.enabled { return } - if element.onSelectionRequest != nil { - element.onSelectionRequest() - } -} - -func (element *Checkbox) HandleSelection ( - direction tomo.SelectionDirection, -) ( - accepted bool, -) { - direction = direction.Canon() - if !element.enabled { return false } - if element.selected && direction != tomo.SelectionDirectionNeutral { - return false - } - - element.selected = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - return true -} - -func (element *Checkbox) HandleDeselection () { - element.selected = false - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *Checkbox) OnSelectionRequest (callback func () (granted bool)) { - element.onSelectionRequest = callback -} - -func (element *Checkbox) OnSelectionMotionRequest ( - callback func (direction tomo.SelectionDirection) (granted bool), -) { - element.onSelectionMotionRequest = callback -} - -// OnClick sets the function to be called when the checkbox is toggled. -func (element *Checkbox) OnClick (callback func ()) { - element.onClick = callback +// OnToggle sets the function to be called when the checkbox is toggled. +func (element *Checkbox) OnToggle (callback func ()) { + element.onToggle = callback } // Value reports whether or not the checkbox is currently checked. @@ -156,12 +111,7 @@ func (element *Checkbox) Value () (checked bool) { // SetEnabled sets whether this checkbox can be toggled or not. func (element *Checkbox) SetEnabled (enabled bool) { - if element.enabled == enabled { return } - element.enabled = enabled - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.selectableControl.SetEnabled(enabled) } // SetText sets the checkbox's label text. @@ -171,9 +121,15 @@ func (element *Checkbox) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) textBounds := element.drawer.LayoutBounds() - element.core.SetMinimumSize ( - textBounds.Dy() + theme.Padding() + textBounds.Dx(), - textBounds.Dy()) + + if text == "" { + element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) + } else { + element.core.SetMinimumSize ( + textBounds.Dy() + theme.Padding() + textBounds.Dx(), + textBounds.Dy()) + } + if element.core.HasImage () { element.draw() element.core.DamageAll() @@ -188,16 +144,10 @@ func (element *Checkbox) draw () { artist.FillRectangle ( element.core, theme.ButtonPattern ( - element.enabled, + element.Enabled(), element.Selected(), element.pressed), boxBounds) - - innerBounds := bounds - innerBounds.Min.X += theme.Padding() - innerBounds.Min.Y += theme.Padding() - innerBounds.Max.X -= theme.Padding() - innerBounds.Max.Y -= theme.Padding() textBounds := element.drawer.LayoutBounds() offset := image.Point { @@ -207,7 +157,7 @@ func (element *Checkbox) draw () { offset.Y -= textBounds.Min.Y offset.X -= textBounds.Min.X - foreground := theme.ForegroundPattern(element.enabled) + foreground := theme.ForegroundPattern(element.Enabled()) element.drawer.Draw(element.core, foreground, offset) if element.checked { @@ -217,7 +167,7 @@ func (element *Checkbox) draw () { } artist.FillRectangle ( element.core, - theme.ForegroundPattern(element.enabled), + theme.ForegroundPattern(element.Enabled()), checkBounds) } } diff --git a/elements/basic/list.go b/elements/basic/list.go index 527a899..a9e9ce5 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -10,10 +10,10 @@ import "git.tebibyte.media/sashakoshka/tomo/elements/core" // List is an element that contains several objects that a user can select. type List struct { *core.Core + *core.SelectableCore core core.CoreControl - - enabled bool - selected bool + selectableControl core.SelectableCoreControl + pressed bool contentHeight int @@ -24,16 +24,21 @@ type List struct { scroll int entries []ListEntry - onSelectionRequest func () (granted bool) - onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) onScrollBoundsChange func () onNoEntrySelected func () } // NewList creates a new list element with the specified entries. func NewList (entries ...ListEntry) (element *List) { - element = &List { enabled: true, selectedEntry: -1 } + element = &List { selectedEntry: -1 } element.Core, element.core = core.NewCore(element) + element.SelectableCore, + element.selectableControl = core.NewSelectableCore (func () { + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } + }) element.entries = make([]ListEntry, len(entries)) for index, entry := range entries { @@ -70,8 +75,8 @@ func (element *List) Collapse (width, height int) { } func (element *List) HandleMouseDown (x, y int, button tomo.Button) { - if !element.enabled { return } - if !element.selected { element.Select() } + if !element.Enabled() { return } + if !element.Selected() { element.Select() } if button != tomo.ButtonLeft { return } element.pressed = true if element.selectUnderMouse(x, y) && element.core.HasImage() { @@ -97,7 +102,7 @@ func (element *List) HandleMouseMove (x, y int) { func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *List) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { - if !element.enabled { return } + if !element.Enabled() { return } altered := false switch key { @@ -119,54 +124,6 @@ func (element *List) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) { func (element *List) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { } -func (element *List) Selected () (selected bool) { - return element.selected -} - -func (element *List) Select () { - if !element.enabled { return } - if element.onSelectionRequest != nil { - element.onSelectionRequest() - } -} - -func (element *List) HandleSelection ( - direction tomo.SelectionDirection, -) ( - accepted bool, -) { - direction = direction.Canon() - if !element.enabled { return false } - if element.selected && direction != tomo.SelectionDirectionNeutral { - return false - } - - element.selected = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - return true -} - -func (element *List) HandleDeselection () { - element.selected = false - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *List) OnSelectionRequest (callback func () (granted bool)) { - element.onSelectionRequest = callback -} - -func (element *List) OnSelectionMotionRequest ( - callback func (direction tomo.SelectionDirection) (granted bool), -) { - element.onSelectionMotionRequest = callback -} - // ScrollContentBounds returns the full content size of the element. func (element *List) ScrollContentBounds () (bounds image.Rectangle) { return image.Rect ( @@ -222,16 +179,6 @@ func (element *List) OnScrollBoundsChange (callback func ()) { element.onScrollBoundsChange = callback } -// SetEnabled sets whether this list can be interacted with or not. -func (element *List) SetEnabled (enabled bool) { - if element.enabled == enabled { return } - element.enabled = enabled - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - // OnNoEntrySelected sets a function to be called when the user chooses to // deselect the current selected entry by clicking on empty space within the // list or by pressing the escape key. @@ -416,7 +363,7 @@ func (element *List) draw () { artist.FillRectangle ( element, - theme.ListPattern(element.selected), + theme.ListPattern(element.Selected()), bounds) dot := image.Point { @@ -437,6 +384,6 @@ func (element *List) draw () { } entry.Draw ( element, entryPosition, - element.selectedEntry == index && element.selected) + element.selectedEntry == index && element.Selected()) } } diff --git a/elements/basic/switch.go b/elements/basic/switch.go new file mode 100644 index 0000000..69c8169 --- /dev/null +++ b/elements/basic/switch.go @@ -0,0 +1 @@ +package basic diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index 4ac6f36..500c594 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -10,11 +10,10 @@ import "git.tebibyte.media/sashakoshka/tomo/elements/core" // TextBox is a single-line text input. type TextBox struct { *core.Core + *core.SelectableCore core core.CoreControl + selectableControl core.SelectableCoreControl - enabled bool - selected bool - cursor int scroll int placeholder string @@ -25,8 +24,6 @@ type TextBox struct { onKeyDown func (key tomo.Key, modifiers tomo.Modifiers) (handled bool) onChange func () - onSelectionRequest func () (granted bool) - onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) onScrollBoundsChange func () } @@ -34,8 +31,15 @@ type TextBox struct { // a value. When the value is empty, the placeholder will be displayed in gray // text. func NewTextBox (placeholder, value string) (element *TextBox) { - element = &TextBox { enabled: true } + element = &TextBox { } element.Core, element.core = core.NewCore(element) + element.SelectableCore, + element.selectableControl = core.NewSelectableCore (func () { + if element.core.HasImage () { + element.draw() + element.core.DamageAll() + } + }) element.placeholderDrawer.SetFace(theme.FontFaceRegular()) element.valueDrawer.SetFace(theme.FontFaceRegular()) element.placeholder = placeholder @@ -55,8 +59,8 @@ func (element *TextBox) Resize (width, height int) { } func (element *TextBox) HandleMouseDown (x, y int, button tomo.Button) { - if !element.enabled { return } - if !element.selected { element.Select() } + if !element.Enabled() { return } + if !element.Selected() { element.Select() } } func (element *TextBox) HandleMouseUp (x, y int, button tomo.Button) { } @@ -133,62 +137,6 @@ func (element *TextBox) HandleKeyDown(key tomo.Key, modifiers tomo.Modifiers) { func (element *TextBox) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { } -func (element *TextBox) Selected () (selected bool) { - return element.selected -} - -func (element *TextBox) Select () { - if element.onSelectionRequest != nil { - element.onSelectionRequest() - } -} - -func (element *TextBox) HandleSelection ( - direction tomo.SelectionDirection, -) ( - accepted bool, -) { - direction = direction.Canon() - if !element.enabled { return false } - if element.selected && direction != tomo.SelectionDirectionNeutral { - return false - } - - element.selected = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - return true -} - -func (element *TextBox) HandleDeselection () { - element.selected = false - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *TextBox) OnSelectionRequest (callback func () (granted bool)) { - element.onSelectionRequest = callback -} - -func (element *TextBox) OnSelectionMotionRequest ( - callback func (direction tomo.SelectionDirection) (granted bool), -) { - element.onSelectionMotionRequest = callback -} - -func (element *TextBox) SetEnabled (enabled bool) { - if element.enabled == enabled { return } - element.enabled = enabled - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - func (element *TextBox) SetPlaceholder (placeholder string) { if element.placeholder == placeholder { return } @@ -325,11 +273,11 @@ func (element *TextBox) draw () { artist.FillRectangle ( element.core, theme.InputPattern ( - element.enabled, + element.Enabled(), element.Selected()), bounds) - if len(element.text) == 0 && !element.selected { + if len(element.text) == 0 && !element.Selected() { // draw placeholder textBounds := element.placeholderDrawer.LayoutBounds() offset := image.Point { @@ -348,13 +296,13 @@ func (element *TextBox) draw () { X: theme.Padding() - element.scroll, Y: theme.Padding(), } - foreground := theme.ForegroundPattern(element.enabled) + foreground := theme.ForegroundPattern(element.Enabled()) element.valueDrawer.Draw ( element.core, foreground, offset.Sub(textBounds.Min)) - if element.selected { + if element.Selected() { // cursor cursorPosition := element.valueDrawer.PositionOf ( element.cursor) diff --git a/elements/core/selectable.go b/elements/core/selectable.go new file mode 100644 index 0000000..896ab6d --- /dev/null +++ b/elements/core/selectable.go @@ -0,0 +1,111 @@ +package core + +import "git.tebibyte.media/sashakoshka/tomo" + +// SelectableCore is a struct that can be embedded into objects to make them +// selectable, giving them the default selectability behavior. +type SelectableCore struct { + selected bool + enabled bool + drawSelectionChange func () + onSelectionRequest func () (granted bool) + onSelectionMotionRequest func(tomo.SelectionDirection) (granted bool) +} + +// NewSelectableCore creates a new selectability core and its corresponding +// control. If your element needs to visually update itself when it's selection +// state changes (which it should), a callback to draw and push the update can +// be specified. +func NewSelectableCore ( + drawSelectionChange func (), +) ( + core *SelectableCore, + control SelectableCoreControl, +) { + core = &SelectableCore { + drawSelectionChange: drawSelectionChange, + enabled: true, + } + control = SelectableCoreControl { core: core } + return +} + +// Selected returns whether or not this element is currently selected. +func (core *SelectableCore) Selected () (selected bool) { + return core.selected +} + +// Select selects this element, if its parent element grants the request. +func (core *SelectableCore) Select () { + if !core.enabled { return } + if core.onSelectionRequest != nil { + core.onSelectionRequest() + } +} + +// HandleSelection causes this element to mark itself as selected, if it can +// currently be. Otherwise, it will return false and do nothing. +func (core *SelectableCore) HandleSelection ( + direction tomo.SelectionDirection, +) ( + accepted bool, +) { + direction = direction.Canon() + if !core.enabled { return false } + if core.selected && direction != tomo.SelectionDirectionNeutral { + return false + } + + core.selected = true + if core.drawSelectionChange != nil { core.drawSelectionChange() } + return true +} + +// HandleDeselection causes this element to mark itself as deselected. +func (core *SelectableCore) HandleDeselection () { + core.selected = false + if core.drawSelectionChange != nil { core.drawSelectionChange() } +} + +// OnSelectionRequest sets a function to be called when this element +// wants its parent element to select it. Parent elements should return +// true if the request was granted, and false if it was not. +func (core *SelectableCore) OnSelectionRequest (callback func () (granted bool)) { + core.onSelectionRequest = callback +} + +// OnSelectionMotionRequest sets a function to be called when this +// element wants its parent element to select the element behind or in +// front of it, depending on the specified direction. Parent elements +// should return true if the request was granted, and false if it was +// not. +func (core *SelectableCore) OnSelectionMotionRequest ( + callback func (direction tomo.SelectionDirection) (granted bool), +) { + core.onSelectionMotionRequest = callback +} + +// Enabled returns whether or not the element is enabled. +func (core *SelectableCore) Enabled () (enabled bool) { + return core.enabled +} + +// SelectableCoreControl is a struct that can be used to exert control over a +// selectability core. It must not be directly embedded into an element, but +// instead kept as a private member. When a SelectableCore struct is created, a +// corresponding SelectableCoreControl struct is linked to it and returned +// alongside it. +type SelectableCoreControl struct { + core *SelectableCore +} + +// SetEnabled sets whether the selectability core is enabled. If the state +// changes, this will call drawSelectionChange. +func (control SelectableCoreControl) SetEnabled (enabled bool) { + if control.core.enabled == enabled { return } + control.core.enabled = enabled + if !enabled { control.core.selected = false } + if control.core.drawSelectionChange != nil { + control.core.drawSelectionChange() + } +} diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go index 03a3190..5535e3e 100644 --- a/examples/checkbox/main.go +++ b/examples/checkbox/main.go @@ -30,7 +30,7 @@ func run () { disabledCheckbox.SetEnabled(false) container.Adopt(disabledCheckbox, false) vsync := basic.NewCheckbox("Enable vsync", false) - vsync.OnClick (func () { + vsync.OnToggle (func () { if vsync.Value() { popups.NewDialog ( popups.DialogKindInfo,