Hocus focus

This commit is contained in:
Sasha Koshka 2023-01-30 17:01:47 -05:00
parent 35870951a2
commit 801c3ef6f5
15 changed files with 367 additions and 355 deletions

View File

@ -124,14 +124,14 @@ func (window *Window) handleKeyPress (
modifiers.NumberPad = numberPad
if key == tomo.KeyTab && modifiers.Alt {
if child, ok := window.child.(tomo.Selectable); ok {
direction := tomo.SelectionDirectionForward
if child, ok := window.child.(tomo.Focusable); ok {
direction := tomo.KeynavDirectionForward
if modifiers.Shift {
direction = tomo.SelectionDirectionBackward
direction = tomo.KeynavDirectionBackward
}
if !child.HandleSelection(direction) {
child.HandleDeselection()
if !child.HandleFocus(direction) {
child.HandleUnfocus()
}
}
} else if child, ok := window.child.(tomo.KeyboardTarget); ok {

View File

@ -87,11 +87,11 @@ func (window *Window) Adopt (child tomo.Element) {
if previousChild, ok := window.child.(tomo.Flexible); ok {
previousChild.OnFlexibleHeightChange(nil)
}
if previousChild, ok := window.child.(tomo.Selectable); ok {
previousChild.OnSelectionRequest(nil)
previousChild.OnSelectionMotionRequest(nil)
if previousChild.Selected() {
previousChild.HandleDeselection()
if previousChild, ok := window.child.(tomo.Focusable); ok {
previousChild.OnFocusRequest(nil)
previousChild.OnFocusMotionRequest(nil)
if previousChild.Focused() {
previousChild.HandleUnfocus()
}
}
@ -100,8 +100,8 @@ func (window *Window) Adopt (child tomo.Element) {
if newChild, ok := child.(tomo.Flexible); ok {
newChild.OnFlexibleHeightChange(window.resizeChildToFit)
}
if newChild, ok := child.(tomo.Selectable); ok {
newChild.OnSelectionRequest(window.childSelectionRequestCallback)
if newChild, ok := child.(tomo.Focusable); ok {
newChild.OnFocusRequest(window.childSelectionRequestCallback)
}
if child != nil {
child.OnDamage(window.childDrawCallback)
@ -282,20 +282,20 @@ func (window *Window) childMinimumSizeChangeCallback (width, height int) {
}
func (window *Window) childSelectionRequestCallback () (granted bool) {
if child, ok := window.child.(tomo.Selectable); ok {
child.HandleSelection(tomo.SelectionDirectionNeutral)
if child, ok := window.child.(tomo.Focusable); ok {
child.HandleFocus(tomo.KeynavDirectionNeutral)
}
return true
}
func (window *Window) childSelectionMotionRequestCallback (
direction tomo.SelectionDirection,
direction tomo.KeynavDirection,
) (
granted bool,
) {
if child, ok := window.child.(tomo.Selectable); ok {
if !child.HandleSelection(direction) {
child.HandleDeselection()
if child, ok := window.child.(tomo.Focusable); ok {
if !child.HandleFocus(direction) {
child.HandleUnfocus()
}
return true
}

View File

@ -28,58 +28,61 @@ type Element interface {
OnMinimumSizeChange (callback func ())
}
// SelectionDirection represents a keyboard navigation direction.
type SelectionDirection int
// KeynavDirection represents a keyboard navigation direction.
type KeynavDirection int
const (
SelectionDirectionNeutral SelectionDirection = 0
SelectionDirectionBackward SelectionDirection = -1
SelectionDirectionForward SelectionDirection = 1
KeynavDirectionNeutral KeynavDirection = 0
KeynavDirectionBackward KeynavDirection = -1
KeynavDirectionForward KeynavDirection = 1
)
// Canon returns a well-formed direction.
func (direction SelectionDirection) Canon () (canon SelectionDirection) {
func (direction KeynavDirection) Canon () (canon KeynavDirection) {
if direction > 0 {
return SelectionDirectionForward
return KeynavDirectionForward
} else if direction == 0 {
return SelectionDirectionNeutral
return KeynavDirectionNeutral
} else {
return SelectionDirectionBackward
return KeynavDirectionBackward
}
}
// Selectable represents an element that has keyboard navigation support. This
// Focusable represents an element that has keyboard navigation support. This
// includes inputs, buttons, sliders, etc. as well as any elements that have
// children (so keyboard navigation events can be propagated downward).
type Selectable interface {
type Focusable interface {
Element
// Selected returns whether or not this element is currently selected.
Selected () (selected bool)
// Focused returns whether or not this element is currently focused.
Focused () (selected bool)
// Select selects this element, if its parent element grants the
// Focus focuses this element, if its parent element grants the
// request.
Select ()
Focus ()
// HandleSelection causes this element to mark itself as selected, if it
// can currently be. Otherwise, it will return false and do nothing.
HandleSelection (direction SelectionDirection) (accepted bool)
// HandleFocus causes this element to mark itself as focused. If the
// element does not have children, it is disabled, or there are no more
// selectable children in the given direction, it should return false
// and do nothing. Otherwise, it should select itself and any children
// (if applicable) and return true.
HandleFocus (direction KeynavDirection) (accepted bool)
// HandleDeselection causes this element to mark itself and all of its
// children as deselected.
HandleDeselection ()
// children as unfocused.
HandleUnfocus ()
// 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.
OnSelectionRequest (func () (granted bool))
// OnFocusRequest sets a function to be called when this element wants
// its parent element to focus it. Parent elements should return true if
// the request was granted, and false if it was not.
OnFocusRequest (func () (granted bool))
// OnSelectionMotionRequest sets a function to be called when this
// element wants its parent element to select the element behind or in
// OnFocusMotionRequest sets a function to be called when this
// element wants its parent element to focus 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.
OnSelectionMotionRequest (func (SelectionDirection) (granted bool))
OnFocusMotionRequest (func (direction KeynavDirection) (granted bool))
}
// KeyboardTarget represents an element that can receive keyboard input.

View File

@ -11,9 +11,9 @@ var buttonCase = theme.C("basic", "button")
// Button is a clickable button.
type Button struct {
*core.Core
*core.SelectableCore
*core.FocusableCore
core core.CoreControl
selectableControl core.SelectableCoreControl
focusableControl core.FocusableCoreControl
drawer artist.TextDrawer
pressed bool
@ -26,8 +26,8 @@ type Button struct {
func NewButton (text string) (element *Button) {
element = &Button { }
element.Core, element.core = core.NewCore(element)
element.SelectableCore,
element.selectableControl = core.NewSelectableCore (func () {
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
@ -45,7 +45,7 @@ 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.Focused() { element.Focus() }
if button != tomo.ButtonLeft { return }
element.pressed = true
if element.core.HasImage() {
@ -106,7 +106,7 @@ func (element *Button) OnClick (callback func ()) {
// SetEnabled sets whether this button can be clicked or not.
func (element *Button) SetEnabled (enabled bool) {
element.selectableControl.SetEnabled(enabled)
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the button's label text.
@ -131,7 +131,7 @@ func (element *Button) draw () {
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: buttonCase,
Disabled: !element.Enabled(),
Selected: element.Selected(),
Focused: element.Focused(),
Pressed: element.pressed,
})

View File

@ -11,9 +11,9 @@ var checkboxCase = theme.C("basic", "checkbox")
// Checkbox is a toggle-able checkbox with a label.
type Checkbox struct {
*core.Core
*core.SelectableCore
*core.FocusableCore
core core.CoreControl
selectableControl core.SelectableCoreControl
focusableControl core.FocusableCoreControl
drawer artist.TextDrawer
pressed bool
@ -27,8 +27,8 @@ type Checkbox struct {
func NewCheckbox (text string, checked bool) (element *Checkbox) {
element = &Checkbox { checked: checked }
element.Core, element.core = core.NewCore(element)
element.SelectableCore,
element.selectableControl = core.NewSelectableCore (func () {
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
@ -47,7 +47,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.Focus()
element.pressed = true
if element.core.HasImage() {
element.draw()
@ -113,7 +113,7 @@ func (element *Checkbox) Value () (checked bool) {
// SetEnabled sets whether this checkbox can be toggled or not.
func (element *Checkbox) SetEnabled (enabled bool) {
element.selectableControl.SetEnabled(enabled)
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the checkbox's label text.
@ -150,7 +150,7 @@ func (element *Checkbox) draw () {
pattern, inset := theme.ButtonPattern(theme.PatternState {
Case: checkboxCase,
Disabled: !element.Enabled(),
Selected: element.Selected(),
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, pattern, boxBounds)

View File

@ -14,16 +14,16 @@ type Container struct {
*core.Core
core core.CoreControl
layout tomo.Layout
children []tomo.LayoutEntry
drags [10]tomo.MouseTarget
warping bool
selected bool
selectable bool
flexible bool
layout tomo.Layout
children []tomo.LayoutEntry
drags [10]tomo.MouseTarget
warping bool
focused bool
focusable bool
flexible bool
onSelectionRequest func () (granted bool)
onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool)
onFocusRequest func () (granted bool)
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool)
onFlexibleHeightChange func ()
}
@ -57,14 +57,14 @@ func (element *Container) Adopt (child tomo.Element, expand bool) {
if child0, ok := child.(tomo.Flexible); ok {
child0.OnFlexibleHeightChange(element.updateMinimumSize)
}
if child0, ok := child.(tomo.Selectable); ok {
child0.OnSelectionRequest (func () (granted bool) {
return element.childSelectionRequestCallback(child0)
if child0, ok := child.(tomo.Focusable); ok {
child0.OnFocusRequest (func () (granted bool) {
return element.childFocusRequestCallback(child0)
})
child0.OnSelectionMotionRequest (
func (direction tomo.SelectionDirection) (granted bool) {
if element.onSelectionMotionRequest == nil { return }
return element.onSelectionMotionRequest(direction)
child0.OnFocusMotionRequest (
func (direction tomo.KeynavDirection) (granted bool) {
if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction)
})
}
@ -132,11 +132,11 @@ func (element *Container) Disown (child tomo.Element) {
func (element *Container) clearChildEventHandlers (child tomo.Element) {
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
if child0, ok := child.(tomo.Selectable); ok {
child0.OnSelectionRequest(nil)
child0.OnSelectionMotionRequest(nil)
if child0.Selected() {
child0.HandleDeselection()
if child0, ok := child.(tomo.Focusable); ok {
child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil)
if child0.Focused() {
child0.HandleUnfocus()
}
}
if child0, ok := child.(tomo.Flexible); ok {
@ -238,7 +238,7 @@ func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
}
func (element *Container) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
element.forSelected (func (child tomo.Selectable) bool {
element.forFocused (func (child tomo.Focusable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyDown(key, modifiers)
@ -248,7 +248,7 @@ func (element *Container) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers)
}
func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
element.forSelected (func (child tomo.Selectable) bool {
element.forFocused (func (child tomo.Focusable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyUp(key, modifiers)
@ -257,103 +257,6 @@ func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
})
}
func (element *Container) Selected () (selected bool) {
return element.selected
}
func (element *Container) Select () {
if element.onSelectionRequest != nil {
element.onSelectionRequest()
}
}
func (element *Container) HandleSelection (direction tomo.SelectionDirection) (ok bool) {
if !element.selectable { return false }
direction = direction.Canon()
firstSelected := element.firstSelected()
if firstSelected < 0 {
// no element is currently selected, so we need to select either
// the first or last selectable element depending on the
// direction.
switch direction {
case tomo.SelectionDirectionNeutral, tomo.SelectionDirectionForward:
// if we recieve a neutral or forward direction, select
// the first selectable element.
return element.selectFirstSelectableElement(direction)
case tomo.SelectionDirectionBackward:
// if we recieve a backward direction, select the last
// selectable element.
return element.selectLastSelectableElement(direction)
}
} else {
// an element is currently selected, so we need to move the
// selection in the specified direction
firstSelectedChild :=
element.children[firstSelected].Element.(tomo.Selectable)
// before we move the selection, the currently selected child
// may also be able to move its selection. if the child is able
// to do that, we will let it and not move ours.
if firstSelectedChild.HandleSelection(direction) {
return true
}
// find the previous/next selectable element relative to the
// currently selected element, if it exists.
for index := firstSelected + int(direction);
index < len(element.children) && index >= 0;
index += int(direction) {
child, selectable :=
element.children[index].
Element.(tomo.Selectable)
if selectable && child.HandleSelection(direction) {
// we have found one, so we now actually move
// the selection.
firstSelectedChild.HandleDeselection()
element.selected = true
return true
}
}
}
return false
}
func (element *Container) selectFirstSelectableElement (
direction tomo.SelectionDirection,
) (
ok bool,
) {
element.forSelectable (func (child tomo.Selectable) bool {
if child.HandleSelection(direction) {
element.selected = true
ok = true
return false
}
return true
})
return
}
func (element *Container) selectLastSelectableElement (
direction tomo.SelectionDirection,
) (
ok bool,
) {
element.forSelectableBackward (func (child tomo.Selectable) bool {
if child.HandleSelection(direction) {
element.selected = true
ok = true
return false
}
return true
})
return
}
func (element *Container) FlexibleHeightFor (width int) (height int) {
return element.layout.FlexibleHeightFor(element.children, width)
}
@ -362,37 +265,134 @@ func (element *Container) OnFlexibleHeightChange (callback func ()) {
element.onFlexibleHeightChange = callback
}
func (element *Container) HandleDeselection () {
element.selected = false
element.forSelected (func (child tomo.Selectable) bool {
child.HandleDeselection()
func (element *Container) Focused () (focused bool) {
return element.focused
}
func (element *Container) Focus () {
if element.onFocusRequest != nil {
element.onFocusRequest()
}
}
func (element *Container) HandleFocus (direction tomo.KeynavDirection) (ok bool) {
if !element.focusable { return false }
direction = direction.Canon()
firstFocused := element.firstFocused()
if firstFocused < 0 {
// no element is currently focused, so we need to focus either
// the first or last focusable element depending on the
// direction.
switch direction {
case tomo.KeynavDirectionNeutral, tomo.KeynavDirectionForward:
// if we recieve a neutral or forward direction, focus
// the first focusable element.
return element.focusFirstFocusableElement(direction)
case tomo.KeynavDirectionBackward:
// if we recieve a backward direction, focus the last
// focusable element.
return element.focusLastFocusableElement(direction)
}
} else {
// an element is currently focused, so we need to move the
// focus in the specified direction
firstFocusedChild :=
element.children[firstFocused].Element.(tomo.Focusable)
// before we move the focus, the currently focused child
// may also be able to move its focus. if the child is able
// to do that, we will let it and not move ours.
if firstFocusedChild.HandleFocus(direction) {
return true
}
// find the previous/next focusable element relative to the
// currently focused element, if it exists.
for index := firstFocused + int(direction);
index < len(element.children) && index >= 0;
index += int(direction) {
child, focusable :=
element.children[index].
Element.(tomo.Focusable)
if focusable && child.HandleFocus(direction) {
// we have found one, so we now actually move
// the focus.
firstFocusedChild.HandleUnfocus()
element.focused = true
return true
}
}
}
return false
}
func (element *Container) focusFirstFocusableElement (
direction tomo.KeynavDirection,
) (
ok bool,
) {
element.forFocusable (func (child tomo.Focusable) bool {
if child.HandleFocus(direction) {
element.focused = true
ok = true
return false
}
return true
})
return
}
func (element *Container) focusLastFocusableElement (
direction tomo.KeynavDirection,
) (
ok bool,
) {
element.forFocusableBackward (func (child tomo.Focusable) bool {
if child.HandleFocus(direction) {
element.focused = true
ok = true
return false
}
return true
})
return
}
func (element *Container) HandleUnfocus () {
element.focused = false
element.forFocused (func (child tomo.Focusable) bool {
child.HandleUnfocus()
return true
})
}
func (element *Container) OnSelectionRequest (callback func () (granted bool)) {
element.onSelectionRequest = callback
func (element *Container) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback
}
func (element *Container) OnSelectionMotionRequest (
callback func (direction tomo.SelectionDirection) (granted bool),
func (element *Container) OnFocusMotionRequest (
callback func (direction tomo.KeynavDirection) (granted bool),
) {
element.onSelectionMotionRequest = callback
element.onFocusMotionRequest = callback
}
func (element *Container) forSelected (callback func (child tomo.Selectable) bool) {
func (element *Container) forFocused (callback func (child tomo.Focusable) bool) {
for _, entry := range element.children {
child, selectable := entry.Element.(tomo.Selectable)
if selectable && child.Selected() {
child, focusable := entry.Element.(tomo.Focusable)
if focusable && child.Focused() {
if !callback(child) { break }
}
}
}
func (element *Container) forSelectable (callback func (child tomo.Selectable) bool) {
func (element *Container) forFocusable (callback func (child tomo.Focusable) bool) {
for _, entry := range element.children {
child, selectable := entry.Element.(tomo.Selectable)
if selectable {
child, focusable := entry.Element.(tomo.Focusable)
if focusable {
if !callback(child) { break }
}
}
@ -400,26 +400,26 @@ func (element *Container) forSelectable (callback func (child tomo.Selectable) b
func (element *Container) forFlexible (callback func (child tomo.Flexible) bool) {
for _, entry := range element.children {
child, selectable := entry.Element.(tomo.Flexible)
if selectable {
child, flexible := entry.Element.(tomo.Flexible)
if flexible {
if !callback(child) { break }
}
}
}
func (element *Container) forSelectableBackward (callback func (child tomo.Selectable) bool) {
func (element *Container) forFocusableBackward (callback func (child tomo.Focusable) bool) {
for index := len(element.children) - 1; index >= 0; index -- {
child, selectable := element.children[index].Element.(tomo.Selectable)
if selectable {
child, focusable := element.children[index].Element.(tomo.Focusable)
if focusable {
if !callback(child) { break }
}
}
}
func (element *Container) firstSelected () (index int) {
func (element *Container) firstFocused () (index int) {
for currentIndex, entry := range element.children {
child, selectable := entry.Element.(tomo.Selectable)
if selectable && child.Selected() {
child, focusable := entry.Element.(tomo.Focusable)
if focusable && child.Focused() {
return currentIndex
}
}
@ -427,9 +427,9 @@ func (element *Container) firstSelected () (index int) {
}
func (element *Container) reflectChildProperties () {
element.selectable = false
element.forSelectable (func (tomo.Selectable) bool {
element.selectable = true
element.focusable = false
element.forFocusable (func (tomo.Focusable) bool {
element.focusable = true
return false
})
element.flexible = false
@ -437,22 +437,22 @@ func (element *Container) reflectChildProperties () {
element.flexible = true
return false
})
if !element.selectable {
element.selected = false
if !element.focusable {
element.focused = false
}
}
func (element *Container) childSelectionRequestCallback (
child tomo.Selectable,
func (element *Container) childFocusRequestCallback (
child tomo.Focusable,
) (
granted bool,
) {
if element.onSelectionRequest != nil && element.onSelectionRequest() {
element.forSelected (func (child tomo.Selectable) bool {
child.HandleDeselection()
if element.onFocusRequest != nil && element.onFocusRequest() {
element.forFocused (func (child tomo.Focusable) bool {
child.HandleUnfocus()
return true
})
child.HandleSelection(tomo.SelectionDirectionNeutral)
child.HandleFocus(tomo.KeynavDirectionNeutral)
return true
} else {
return false

View File

@ -12,9 +12,9 @@ var listCase = theme.C("basic", "list")
// List is an element that contains several objects that a user can select.
type List struct {
*core.Core
*core.SelectableCore
*core.FocusableCore
core core.CoreControl
selectableControl core.SelectableCoreControl
focusableControl core.FocusableCoreControl
pressed bool
@ -34,8 +34,8 @@ type List struct {
func NewList (entries ...ListEntry) (element *List) {
element = &List { selectedEntry: -1 }
element.Core, element.core = core.NewCore(element)
element.SelectableCore,
element.selectableControl = core.NewSelectableCore (func () {
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
@ -78,7 +78,7 @@ 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.Focused() { element.Focus() }
if button != tomo.ButtonLeft { return }
element.pressed = true
if element.selectUnderMouse(x, y) && element.core.HasImage() {
@ -377,7 +377,7 @@ func (element *List) draw () {
pattern, inset := theme.ListPattern(theme.PatternState {
Case: listCase,
Disabled: !element.Enabled(),
Selected: element.Selected(),
Focused: element.Focused(),
})
artist.FillRectangle(element.core, pattern, bounds)
@ -394,6 +394,6 @@ func (element *List) draw () {
if entryPosition.Y > bounds.Max.Y { break }
entry.Draw (
innerCanvas, entryPosition,
element.Selected(), element.selectedEntry == index)
element.Focused(), element.selectedEntry == index)
}
}

View File

@ -55,14 +55,14 @@ func (entry *ListEntry) updateBounds () {
func (entry *ListEntry) Draw (
destination tomo.Canvas,
offset image.Point,
selected bool,
focused bool,
on bool,
) (
updatedRegion image.Rectangle,
) {
pattern, _ := theme.ItemPattern(theme.PatternState {
Case: listEntryCase,
Selected: selected,
Focused: focused,
On: on,
})
artist.FillRectangle (
@ -71,7 +71,7 @@ func (entry *ListEntry) Draw (
entry.Bounds().Add(offset))
foreground, _ := theme.ForegroundPattern (theme.PatternState {
Case: listEntryCase,
Selected: selected,
Focused: focused,
On: on,
})
return entry.drawer.Draw (

View File

@ -15,7 +15,7 @@ var scrollBarVerticalCase = theme.C("basic", "scrollBarVertical")
type ScrollContainer struct {
*core.Core
core core.CoreControl
selected bool
focused bool
child tomo.Scrollable
childWidth, childHeight int
@ -40,8 +40,8 @@ type ScrollContainer struct {
bar image.Rectangle
}
onSelectionRequest func () (granted bool)
onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool)
onFocusRequest func () (granted bool)
onFocusMotionRequest func (tomo.KeynavDirection) (granted bool)
}
// NewScrollContainer creates a new scroll container with the specified scroll
@ -80,11 +80,11 @@ func (element *ScrollContainer) Adopt (child tomo.Scrollable) {
child.OnDamage(element.childDamageCallback)
child.OnMinimumSizeChange(element.updateMinimumSize)
child.OnScrollBoundsChange(element.childScrollBoundsChangeCallback)
if newChild, ok := child.(tomo.Selectable); ok {
newChild.OnSelectionRequest (
element.childSelectionRequestCallback)
newChild.OnSelectionMotionRequest (
element.childSelectionMotionRequestCallback)
if newChild, ok := child.(tomo.Focusable); ok {
newChild.OnFocusRequest (
element.childFocusRequestCallback)
newChild.OnFocusMotionRequest (
element.childFocusMotionRequestCallback)
}
// TODO: somehow inform the core that we do not in fact want to
@ -193,82 +193,80 @@ func (element *ScrollContainer) scrollChildBy (x, y int) {
element.child.ScrollTo(scrollPoint)
}
func (element *ScrollContainer) Selected () (selected bool) {
return element.selected
func (element *ScrollContainer) Focused () (focused bool) {
return element.focused
}
func (element *ScrollContainer) Select () {
if element.onSelectionRequest != nil {
element.onSelectionRequest()
func (element *ScrollContainer) Focus () {
if element.onFocusRequest != nil {
element.onFocusRequest()
}
}
func (element *ScrollContainer) HandleSelection (
direction tomo.SelectionDirection,
func (element *ScrollContainer) HandleFocus (
direction tomo.KeynavDirection,
) (
accepted bool,
) {
if child, ok := element.child.(tomo.Selectable); ok {
element.selected = true
return child.HandleSelection(direction)
if child, ok := element.child.(tomo.Focusable); ok {
element.focused = true
return child.HandleFocus(direction)
} else {
element.selected = false
element.focused = false
return false
}
}
func (element *ScrollContainer) HandleDeselection () {
if child, ok := element.child.(tomo.Selectable); ok {
child.HandleDeselection()
func (element *ScrollContainer) HandleUnfocus () {
if child, ok := element.child.(tomo.Focusable); ok {
child.HandleUnfocus()
}
element.selected = false
element.focused = false
}
func (element *ScrollContainer) OnSelectionRequest (callback func () (granted bool)) {
element.onSelectionRequest = callback
func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback
}
func (element *ScrollContainer) OnSelectionMotionRequest (
callback func (direction tomo.SelectionDirection) (granted bool),
func (element *ScrollContainer) OnFocusMotionRequest (
callback func (direction tomo.KeynavDirection) (granted bool),
) {
element.onSelectionMotionRequest = callback
element.onFocusMotionRequest = callback
}
func (element *ScrollContainer) childDamageCallback (region tomo.Canvas) {
element.core.DamageRegion(artist.Paste(element, region, image.Point { }))
}
func (element *ScrollContainer) childSelectionRequestCallback () (granted bool) {
child, ok := element.child.(tomo.Selectable)
func (element *ScrollContainer) childFocusRequestCallback () (granted bool) {
child, ok := element.child.(tomo.Focusable)
if !ok { return false }
if element.onSelectionRequest != nil && element.onSelectionRequest() {
child.HandleSelection(tomo.SelectionDirectionNeutral)
if element.onFocusRequest != nil && element.onFocusRequest() {
child.HandleFocus(tomo.KeynavDirectionNeutral)
return true
} else {
return false
}
}
func (element *ScrollContainer) childSelectionMotionRequestCallback (
direction tomo.SelectionDirection,
func (element *ScrollContainer) childFocusMotionRequestCallback (
direction tomo.KeynavDirection,
) (
granted bool,
) {
if element.onSelectionMotionRequest == nil {
return
}
return element.onSelectionMotionRequest(direction)
if element.onFocusMotionRequest == nil { return }
return element.onFocusMotionRequest(direction)
}
func (element *ScrollContainer) clearChildEventHandlers (child tomo.Scrollable) {
child.OnDamage(nil)
child.OnMinimumSizeChange(nil)
child.OnScrollBoundsChange(nil)
if child0, ok := child.(tomo.Selectable); ok {
child0.OnSelectionRequest(nil)
child0.OnSelectionMotionRequest(nil)
if child0.Selected() {
child0.HandleDeselection()
if child0, ok := child.(tomo.Focusable); ok {
child0.OnFocusRequest(nil)
child0.OnFocusMotionRequest(nil)
if child0.Focused() {
child0.HandleUnfocus()
}
}
if child0, ok := child.(tomo.Flexible); ok {

View File

@ -12,9 +12,9 @@ var switchCase = theme.C("basic", "switch")
// functionally identical to Checkbox, but plays a different semantic role.
type Switch struct {
*core.Core
*core.SelectableCore
*core.FocusableCore
core core.CoreControl
selectableControl core.SelectableCoreControl
focusableControl core.FocusableCoreControl
drawer artist.TextDrawer
pressed bool
@ -28,8 +28,8 @@ type Switch struct {
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 () {
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
@ -49,7 +49,7 @@ func (element *Switch) Resize (width, height int) {
func (element *Switch) HandleMouseDown (x, y int, button tomo.Button) {
if !element.Enabled() { return }
element.Select()
element.Focus()
element.pressed = true
if element.core.HasImage() {
element.draw()
@ -115,7 +115,7 @@ func (element *Switch) Value () (on bool) {
// SetEnabled sets whether this switch can be flipped or not.
func (element *Switch) SetEnabled (enabled bool) {
element.selectableControl.SetEnabled(enabled)
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the checkbox's label text.
@ -171,7 +171,7 @@ func (element *Switch) draw () {
gutterPattern, _ := theme.GutterPattern(theme.PatternState {
Case: switchCase,
Disabled: !element.Enabled(),
Selected: element.Selected(),
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, gutterPattern, gutterBounds)
@ -179,7 +179,7 @@ func (element *Switch) draw () {
handlePattern, _ := theme.HandlePattern(theme.PatternState {
Case: switchCase,
Disabled: !element.Enabled(),
Selected: element.Selected(),
Focused: element.Focused(),
Pressed: element.pressed,
})
artist.FillRectangle(element.core, handlePattern, handleBounds)

View File

@ -12,9 +12,9 @@ var textBoxCase = theme.C("basic", "textBox")
// TextBox is a single-line text input.
type TextBox struct {
*core.Core
*core.SelectableCore
*core.FocusableCore
core core.CoreControl
selectableControl core.SelectableCoreControl
focusableControl core.FocusableCoreControl
cursor int
scroll int
@ -35,8 +35,8 @@ type TextBox struct {
func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { }
element.Core, element.core = core.NewCore(element)
element.SelectableCore,
element.selectableControl = core.NewSelectableCore (func () {
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
@ -61,8 +61,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.Focused() { element.Focus() }
}
func (element *TextBox) HandleMouseUp (x, y int, button tomo.Button) { }
@ -277,13 +277,13 @@ func (element *TextBox) draw () {
// FIXME: take index into account
pattern, inset := theme.InputPattern(theme.PatternState {
Case: textBoxCase,
Case: textBoxCase,
Disabled: !element.Enabled(),
Selected: element.Selected(),
Focused: element.Focused(),
})
artist.FillRectangle(element.core, pattern, bounds)
if len(element.text) == 0 && !element.Selected() {
if len(element.text) == 0 && !element.Focused() {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
offset := image.Point {
@ -314,7 +314,7 @@ func (element *TextBox) draw () {
foreground,
offset.Sub(textBounds.Min))
if element.Selected() {
if element.Focused() {
// cursor
cursorPosition := element.valueDrawer.PositionOf (
element.cursor)

View File

@ -15,9 +15,6 @@ type Core struct {
minimumHeight int
}
selectable bool
selected bool
onMinimumSizeChange func ()
onDamage func (region tomo.Canvas)
}

View File

@ -2,110 +2,110 @@ 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)
// FocusableCore is a struct that can be embedded into objects to make them
// focusable, giving them the default keynav behavior.
type FocusableCore struct {
focused bool
enabled bool
drawFocusChange func ()
onFocusRequest func () (granted bool)
onFocusMotionRequest func(tomo.KeynavDirection) (granted bool)
}
// NewSelectableCore creates a new selectability core and its corresponding
// control. If your element needs to visually update itself when it's selection
// NewFocusableCore creates a new focusability core and its corresponding
// control. If your element needs to visually update itself when it's focus
// state changes (which it should), a callback to draw and push the update can
// be specified.
func NewSelectableCore (
drawSelectionChange func (),
func NewFocusableCore (
drawFocusChange func (),
) (
core *SelectableCore,
control SelectableCoreControl,
core *FocusableCore,
control FocusableCoreControl,
) {
core = &SelectableCore {
drawSelectionChange: drawSelectionChange,
core = &FocusableCore {
drawFocusChange: drawFocusChange,
enabled: true,
}
control = SelectableCoreControl { core: core }
control = FocusableCoreControl { core: core }
return
}
// Selected returns whether or not this element is currently selected.
func (core *SelectableCore) Selected () (selected bool) {
return core.selected
// Focused returns whether or not this element is currently focused.
func (core *FocusableCore) Focused () (focused bool) {
return core.focused
}
// Select selects this element, if its parent element grants the request.
func (core *SelectableCore) Select () {
// Focus focuses this element, if its parent element grants the request.
func (core *FocusableCore) Focus () {
if !core.enabled { return }
if core.onSelectionRequest != nil {
core.onSelectionRequest()
if core.onFocusRequest != nil {
core.onFocusRequest()
}
}
// HandleSelection causes this element to mark itself as selected, if it can
// HandleFocus causes this element to mark itself as focused, if it can
// currently be. Otherwise, it will return false and do nothing.
func (core *SelectableCore) HandleSelection (
direction tomo.SelectionDirection,
func (core *FocusableCore) HandleFocus (
direction tomo.KeynavDirection,
) (
accepted bool,
) {
direction = direction.Canon()
if !core.enabled { return false }
if core.selected && direction != tomo.SelectionDirectionNeutral {
if core.focused && direction != tomo.KeynavDirectionNeutral {
return false
}
core.selected = true
if core.drawSelectionChange != nil { core.drawSelectionChange() }
core.focused = true
if core.drawFocusChange != nil { core.drawFocusChange() }
return true
}
// HandleDeselection causes this element to mark itself as deselected.
func (core *SelectableCore) HandleDeselection () {
core.selected = false
if core.drawSelectionChange != nil { core.drawSelectionChange() }
// HandleUnfocus causes this element to mark itself as unfocused.
func (core *FocusableCore) HandleUnfocus () {
core.focused = false
if core.drawFocusChange != nil { core.drawFocusChange() }
}
// OnSelectionRequest sets a function to be called when this element
// wants its parent element to select it. Parent elements should return
// OnFocusRequest sets a function to be called when this element
// wants its parent element to focus 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
func (core *FocusableCore) OnFocusRequest (callback func () (granted bool)) {
core.onFocusRequest = callback
}
// OnSelectionMotionRequest sets a function to be called when this
// element wants its parent element to select the element behind or in
// OnFocusMotionRequest sets a function to be called when this
// element wants its parent element to focus 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),
func (core *FocusableCore) OnFocusMotionRequest (
callback func (direction tomo.KeynavDirection) (granted bool),
) {
core.onSelectionMotionRequest = callback
core.onFocusMotionRequest = callback
}
// Enabled returns whether or not the element is enabled.
func (core *SelectableCore) Enabled () (enabled bool) {
func (core *FocusableCore) 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
// FocusableCoreControl is a struct that can be used to exert control over a
// focusability core. It must not be directly embedded into an element, but
// instead kept as a private member. When a FocusableCore struct is created, a
// corresponding FocusableCoreControl struct is linked to it and returned
// alongside it.
type SelectableCoreControl struct {
core *SelectableCore
type FocusableCoreControl struct {
core *FocusableCore
}
// SetEnabled sets whether the selectability core is enabled. If the state
// changes, this will call drawSelectionChange.
func (control SelectableCoreControl) SetEnabled (enabled bool) {
// SetEnabled sets whether the focusability core is enabled. If the state
// changes, this will call drawFocusChange.
func (control FocusableCoreControl) 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()
if !enabled { control.core.focused = false }
if control.core.drawFocusChange != nil {
control.core.drawFocusChange()
}
}

View File

@ -26,3 +26,9 @@ var listEntryPattern = artist.NewMultiBordered (
var onListEntryPattern = artist.NewMultiBordered (
artist.Stroke { Pattern: artist.NewUniform(hex(0x6e8079FF)) })
var selectedListEntryPattern = artist.NewMultiBordered (
artist.Stroke { Pattern: artist.NewUniform(hex(0x999C99FF)) })
var selectedOnListEntryPattern = artist.NewMultiBordered (
artist.Stroke { Pattern: artist.NewUniform(hex(0x6e8079FF)) })

View File

@ -31,9 +31,9 @@ type PatternState struct {
// question is capable of being toggled.
On bool
// Selected should be set to true if the element that is using this
// pattern is currently selected.
Selected bool
// Focused should be set to true if the element that is using this
// pattern is currently focused.
Focused bool
// Pressed should be set to true if the element that is using this
// pattern is being pressed down by the mouse. This is only necessary if
@ -123,7 +123,7 @@ func InputPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
if state.Disabled {
return disabledInputPattern, Inset { 1, 1, 1, 1 }
} else {
if state.Selected {
if state.Focused {
return selectedInputPattern, Inset { 1, 1, 1, 1 }
} else {
return inputPattern, Inset { 1, 1, 1, 1 }
@ -133,7 +133,7 @@ func InputPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
// ListPattern returns a background pattern for a list of things.
func ListPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
if state.Selected {
if state.Focused {
return selectedListPattern, Inset { 4, 4, 4, 4 }
} else {
return listPattern, Inset { 4, 4, 4, 4 }
@ -142,10 +142,18 @@ func ListPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
// ItemPattern returns a background pattern for a list item.
func ItemPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
if state.On {
return onListEntryPattern, Inset { 4, 4, 4, 4 }
if state.Focused {
if state.On {
return selectedOnListEntryPattern, Inset { 4, 4, 4, 4 }
} else {
return selectedListEntryPattern, Inset { 4, 4, 4, 4 }
}
} else {
return listEntryPattern, Inset { 4, 4, 4, 4 }
if state.On {
return onListEntryPattern, Inset { 4, 4, 4, 4 }
} else {
return listEntryPattern, Inset { 4, 4, 4, 4 }
}
}
}
@ -155,14 +163,14 @@ func ButtonPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
return disabledButtonPattern, Inset { 1, 1, 1, 1 }
} else {
if state.Pressed {
if state.Selected {
if state.Focused {
return pressedSelectedButtonPattern, Inset {
2, 0, 0, 2 }
} else {
return pressedButtonPattern, Inset { 2, 0, 0, 2 }
}
} else {
if state.Selected {
if state.Focused {
return selectedButtonPattern, Inset { 1, 1, 1, 1 }
} else {
return buttonPattern, Inset { 1, 1, 1, 1 }
@ -187,7 +195,7 @@ func HandlePattern (state PatternState) (pattern artist.Pattern, inset Inset) {
if state.Disabled {
return disabledScrollBarPattern, Inset { 1, 1, 1, 1 }
} else {
if state.Selected {
if state.Focused {
if state.Pressed {
return pressedSelectedScrollBarPattern, Inset { 1, 1, 1, 1 }
} else {
@ -212,7 +220,7 @@ func SunkenPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
// RaisedPattern returns a general purpose pattern that is raised up out of the
// background.
func RaisedPattern (state PatternState) (pattern artist.Pattern, inset Inset) {
if state.Selected {
if state.Focused {
return selectedRaisedPattern, Inset { 1, 1, 1, 1 }
} else {
return raisedPattern, Inset { 1, 1, 1, 1 }