Added a selectability core to reduce complexity of selectables

This commit is contained in:
Sasha Koshka 2023-01-27 17:55:49 -05:00
parent b2b2a80a06
commit 9422ff6198
7 changed files with 203 additions and 295 deletions

View File

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

View File

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

View File

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

1
elements/basic/switch.go Normal file
View File

@ -0,0 +1 @@
package basic

View File

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

111
elements/core/selectable.go Normal file
View File

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

View File

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