diff --git a/backends/x/event.go b/backends/x/event.go index 7f8a889..7c62293 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -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 { diff --git a/backends/x/window.go b/backends/x/window.go index d12f9a3..30ef7ce 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -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 } diff --git a/element.go b/element.go index cc932f9..192e71f 100644 --- a/element.go +++ b/element.go @@ -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. diff --git a/elements/basic/button.go b/elements/basic/button.go index 098e421..62e39c2 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -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, }) diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index a40e582..115b79a 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -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) diff --git a/elements/basic/container.go b/elements/basic/container.go index 3b5e461..a223e70 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -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 diff --git a/elements/basic/list.go b/elements/basic/list.go index 2d6ac7a..a871c94 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -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) } } diff --git a/elements/basic/listentry.go b/elements/basic/listentry.go index 2e6bfb6..45a14a0 100644 --- a/elements/basic/listentry.go +++ b/elements/basic/listentry.go @@ -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 ( diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index c7d5be7..ad18cf5 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -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 { diff --git a/elements/basic/switch.go b/elements/basic/switch.go index c7e073a..9fdf1b2 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -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) diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index de78bfe..703f504 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -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) diff --git a/elements/core/core.go b/elements/core/core.go index 49de881..a3d0c68 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -15,9 +15,6 @@ type Core struct { minimumHeight int } - selectable bool - selected bool - onMinimumSizeChange func () onDamage func (region tomo.Canvas) } diff --git a/elements/core/selectable.go b/elements/core/selectable.go index 896ab6d..1c28649 100644 --- a/elements/core/selectable.go +++ b/elements/core/selectable.go @@ -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() } } diff --git a/theme/list.go b/theme/list.go index d2ee5b0..2168a7a 100644 --- a/theme/list.go +++ b/theme/list.go @@ -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)) }) diff --git a/theme/patterns.go b/theme/patterns.go index 451a135..517dc8a 100644 --- a/theme/patterns.go +++ b/theme/patterns.go @@ -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 }