Merge pull request 'make-containers-better' (#10) from make-containers-better into main

Reviewed-on: sashakoshka/tomo#10
This commit is contained in:
Sasha Koshka 2023-03-04 21:26:33 +00:00
commit cad10a1fb1
10 changed files with 514 additions and 339 deletions

View File

@ -5,8 +5,8 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/artist" // import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/shatter" // import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/elements/core"
@ -33,9 +33,7 @@ func NewButton (text string) (element *Button) {
element.theme.Case = theme.C("basic", "button") element.theme.Case = theme.C("basic", "button")
element.Core, element.core = core.NewCore(element.drawAll) element.Core, element.core = core.NewCore(element.drawAll)
element.FocusableCore, element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () { element.focusableControl = core.NewFocusableCore(element.drawAndPush)
element.drawAndPush(true)
})
element.SetText(text) element.SetText(text)
return return
} }
@ -45,7 +43,7 @@ func (element *Button) HandleMouseDown (x, y int, button input.Button) {
if !element.Focused() { element.Focus() } if !element.Focused() { element.Focus() }
if button != input.ButtonLeft { return } if button != input.ButtonLeft { return }
element.pressed = true element.pressed = true
element.drawAndPush(true) element.drawAndPush()
} }
func (element *Button) HandleMouseUp (x, y int, button input.Button) { func (element *Button) HandleMouseUp (x, y int, button input.Button) {
@ -56,7 +54,7 @@ func (element *Button) HandleMouseUp (x, y int, button input.Button) {
if element.Enabled() && within && element.onClick != nil { if element.Enabled() && within && element.onClick != nil {
element.onClick() element.onClick()
} }
element.drawAndPush(true) element.drawAndPush()
} }
func (element *Button) HandleMouseMove (x, y int) { } func (element *Button) HandleMouseMove (x, y int) { }
@ -66,14 +64,14 @@ func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers)
if !element.Enabled() { return } if !element.Enabled() { return }
if key == input.KeyEnter { if key == input.KeyEnter {
element.pressed = true element.pressed = true
element.drawAndPush(true) element.drawAndPush()
} }
} }
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed { if key == input.KeyEnter && element.pressed {
element.pressed = false element.pressed = false
element.drawAndPush(true) element.drawAndPush()
if !element.Enabled() { return } if !element.Enabled() { return }
if element.onClick != nil { if element.onClick != nil {
element.onClick() element.onClick()
@ -98,7 +96,7 @@ func (element *Button) SetText (text string) {
element.text = text element.text = text
element.drawer.SetText([]rune(text)) element.drawer.SetText([]rune(text))
element.updateMinimumSize() element.updateMinimumSize()
element.drawAndPush(false) element.drawAndPush()
} }
// SetTheme sets the element's theme. // SetTheme sets the element's theme.
@ -109,7 +107,7 @@ func (element *Button) SetTheme (new theme.Theme) {
theme.FontStyleRegular, theme.FontStyleRegular,
theme.FontSizeNormal)) theme.FontSizeNormal))
element.updateMinimumSize() element.updateMinimumSize()
element.drawAndPush(false) element.drawAndPush()
} }
// SetConfig sets the element's configuration. // SetConfig sets the element's configuration.
@ -117,7 +115,7 @@ func (element *Button) SetConfig (new config.Config) {
if new == element.config.Config { return } if new == element.config.Config { return }
element.config.Config = new element.config.Config = new
element.updateMinimumSize() element.updateMinimumSize()
element.drawAndPush(false) element.drawAndPush()
} }
func (element *Button) updateMinimumSize () { func (element *Button) updateMinimumSize () {
@ -127,19 +125,6 @@ func (element *Button) updateMinimumSize () {
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
} }
func (element *Button) drawAndPush (partial bool) {
if element.core.HasImage () {
if partial {
element.core.DamageRegion (append (
element.drawBackground(true),
element.drawText(true))...)
} else {
element.drawAll()
element.core.DamageAll()
}
}
}
func (element *Button) state () theme.State { func (element *Button) state () theme.State {
return theme.State { return theme.State {
Disabled: !element.Enabled(), Disabled: !element.Enabled(),
@ -148,23 +133,28 @@ func (element *Button) state () theme.State {
} }
} }
func (element *Button) drawBackground (partial bool) []image.Rectangle { func (element *Button) drawAndPush () {
state := element.state() if element.core.HasImage () {
bounds := element.Bounds() element.drawAll()
pattern := element.theme.Pattern(theme.PatternButton, state) element.core.DamageAll()
static := element.theme.Hints(theme.PatternButton).StaticInset
if partial && static != (artist.Inset { }) {
tiles := shatter.Shatter(bounds, static.Apply(bounds))
artist.Draw(element.core, pattern, tiles...)
return tiles
} else {
pattern.Draw(element.core, bounds)
return []image.Rectangle { bounds }
} }
} }
func (element *Button) drawText (partial bool) image.Rectangle { func (element *Button) drawAll () {
element.drawBackground()
element.drawText()
}
func (element *Button) drawBackground () []image.Rectangle {
state := element.state()
bounds := element.Bounds()
pattern := element.theme.Pattern(theme.PatternButton, state)
pattern.Draw(element.core, bounds)
return []image.Rectangle { bounds }
}
func (element *Button) drawText () image.Rectangle {
state := element.state() state := element.state()
bounds := element.Bounds() bounds := element.Bounds()
foreground := element.theme.Color(theme.ColorForeground, state) foreground := element.theme.Color(theme.ColorForeground, state)
@ -182,17 +172,7 @@ func (element *Button) drawText (partial bool) image.Rectangle {
if element.pressed { if element.pressed {
offset = offset.Add(sink) offset = offset.Add(sink)
} }
if partial {
pattern := element.theme.Pattern(theme.PatternButton, state)
pattern.Draw(element.core, region)
}
element.drawer.Draw(element.core, foreground, offset) element.drawer.Draw(element.core, foreground, offset)
return region return region
} }
func (element *Button) drawAll () {
element.drawBackground(false)
element.drawText(false)
}

View File

@ -14,22 +14,20 @@ import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// them in a layout. // them in a layout.
type Container struct { type Container struct {
*core.Core *core.Core
*core.Propagator
core core.CoreControl core core.CoreControl
layout layouts.Layout layout layouts.Layout
children []layouts.LayoutEntry children []layouts.LayoutEntry
drags [10]elements.MouseTarget
warping bool warping bool
focused bool
focusable bool
flexible bool flexible bool
config config.Wrapped config config.Wrapped
theme theme.Wrapped theme theme.Wrapped
onFlexibleHeightChange func ()
onFocusRequest func () (granted bool) onFocusRequest func () (granted bool)
onFocusMotionRequest func (input.KeynavDirection) (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool)
onFlexibleHeightChange func ()
} }
// NewContainer creates a new container. // NewContainer creates a new container.
@ -37,6 +35,7 @@ func NewContainer (layout layouts.Layout) (element *Container) {
element = &Container { } element = &Container { }
element.theme.Case = theme.C("basic", "container") element.theme.Case = theme.C("basic", "container")
element.Core, element.core = core.NewCore(element.redoAll) element.Core, element.core = core.NewCore(element.redoAll)
element.Propagator = core.NewPropagator(element)
element.SetLayout(layout) element.SetLayout(layout)
return return
} }
@ -161,6 +160,9 @@ func (element *Container) clearChildEventHandlers (child elements.Element) {
// DisownAll removes all child elements from the container at once. // DisownAll removes all child elements from the container at once.
func (element *Container) DisownAll () { func (element *Container) DisownAll () {
for _, entry := range element.children {
element.clearChildEventHandlers(entry.Element)
}
element.children = nil element.children = nil
element.updateMinimumSize() element.updateMinimumSize()
@ -203,17 +205,6 @@ func (element *Container) ChildAt (point image.Point) (child elements.Element) {
return return
} }
func (element *Container) childPosition (child elements.Element) (position image.Point) {
for _, entry := range element.children {
if entry.Element == child {
position = entry.Bounds.Min
break
}
}
return
}
func (element *Container) redoAll () { func (element *Container) redoAll () {
if !element.core.HasImage() { return } if !element.core.HasImage() { return }
// do a layout // do a layout
@ -236,16 +227,11 @@ func (element *Container) redoAll () {
} }
} }
// SetTheme sets the element's theme. // SetTheme sets the element's theme.
func (element *Container) SetTheme (new theme.Theme) { func (element *Container) SetTheme (new theme.Theme) {
if new == element.theme.Theme { return } if new == element.theme.Theme { return }
element.theme.Theme = new element.theme.Theme = new
for _, child := range element.children { element.Propagator.SetTheme(new)
if child0, ok := child.Element.(elements.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
}
element.updateMinimumSize() element.updateMinimumSize()
element.redoAll() element.redoAll()
} }
@ -253,206 +239,33 @@ func (element *Container) SetTheme (new theme.Theme) {
// SetConfig sets the element's configuration. // SetConfig sets the element's configuration.
func (element *Container) SetConfig (new config.Config) { func (element *Container) SetConfig (new config.Config) {
if new == element.config.Config { return } if new == element.config.Config { return }
element.config.Config = new element.Propagator.SetConfig(new)
for _, child := range element.children {
if child0, ok := child.Element.(elements.Configurable); ok {
child0.SetConfig(element.config)
}
}
element.updateMinimumSize() element.updateMinimumSize()
element.redoAll() element.redoAll()
} }
func (element *Container) HandleMouseDown (x, y int, button input.Button) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
if !handlesMouse { return }
element.drags[button] = child
child.HandleMouseDown(x, y, button)
}
func (element *Container) HandleMouseUp (x, y int, button input.Button) {
child := element.drags[button]
if child == nil { return }
element.drags[button] = nil
child.HandleMouseUp(x, y, button)
}
func (element *Container) HandleMouseMove (x, y int) {
for _, child := range element.drags {
if child == nil { continue }
child.HandleMouseMove(x, y)
}
}
func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(elements.MouseTarget)
if !handlesMouse { return }
child.HandleMouseScroll(x, y, deltaX, deltaY)
}
func (element *Container) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
element.forFocused (func (child elements.Focusable) bool {
child0, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyDown(key, modifiers)
}
return true
})
}
func (element *Container) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
element.forFocused (func (child elements.Focusable) bool {
child0, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyUp(key, modifiers)
}
return true
})
}
func (element *Container) FlexibleHeightFor (width int) (height int) { func (element *Container) FlexibleHeightFor (width int) (height int) {
margin := element.theme.Margin(theme.PatternBackground) margin := element.theme.Margin(theme.PatternBackground)
// TODO: have layouts take in x and y margins padding := element.theme.Padding(theme.PatternBackground)
return element.layout.FlexibleHeightFor ( return element.layout.FlexibleHeightFor (
element.children, element.children,
margin.X, width) margin, padding, width)
} }
func (element *Container) OnFlexibleHeightChange (callback func ()) { func (element *Container) OnFlexibleHeightChange (callback func ()) {
element.onFlexibleHeightChange = callback element.onFlexibleHeightChange = callback
} }
func (element *Container) Focused () (focused bool) {
return element.focused
}
func (element *Container) Focus () {
if element.onFocusRequest != nil {
element.onFocusRequest()
}
}
func (element *Container) HandleFocus (direction input.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 input.KeynavDirectionNeutral, input.KeynavDirectionForward:
// if we recieve a neutral or forward direction, focus
// the first focusable element.
return element.focusFirstFocusableElement(direction)
case input.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.(elements.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.(elements.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 input.KeynavDirection,
) (
ok bool,
) {
element.forFocusable (func (child elements.Focusable) bool {
if child.HandleFocus(direction) {
element.focused = true
ok = true
return false
}
return true
})
return
}
func (element *Container) focusLastFocusableElement (
direction input.KeynavDirection,
) (
ok bool,
) {
element.forFocusableBackward (func (child elements.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 elements.Focusable) bool {
child.HandleUnfocus()
return true
})
}
func (element *Container) OnFocusRequest (callback func () (granted bool)) { func (element *Container) OnFocusRequest (callback func () (granted bool)) {
element.onFocusRequest = callback element.onFocusRequest = callback
element.Propagator.OnFocusRequest(callback)
} }
func (element *Container) OnFocusMotionRequest ( func (element *Container) OnFocusMotionRequest (
callback func (direction input.KeynavDirection) (granted bool), callback func (direction input.KeynavDirection) (granted bool),
) { ) {
element.onFocusMotionRequest = callback element.onFocusMotionRequest = callback
} element.Propagator.OnFocusMotionRequest(callback)
func (element *Container) forFocused (callback func (child elements.Focusable) bool) {
for _, entry := range element.children {
child, focusable := entry.Element.(elements.Focusable)
if focusable && child.Focused() {
if !callback(child) { break }
}
}
}
func (element *Container) forFocusable (callback func (child elements.Focusable) bool) {
for _, entry := range element.children {
child, focusable := entry.Element.(elements.Focusable)
if focusable {
if !callback(child) { break }
}
}
} }
func (element *Container) forFlexible (callback func (child elements.Flexible) bool) { func (element *Container) forFlexible (callback func (child elements.Flexible) bool) {
@ -464,39 +277,24 @@ func (element *Container) forFlexible (callback func (child elements.Flexible) b
} }
} }
func (element *Container) forFocusableBackward (callback func (child elements.Focusable) bool) {
for index := len(element.children) - 1; index >= 0; index -- {
child, focusable := element.children[index].Element.(elements.Focusable)
if focusable {
if !callback(child) { break }
}
}
}
func (element *Container) firstFocused () (index int) {
for currentIndex, entry := range element.children {
child, focusable := entry.Element.(elements.Focusable)
if focusable && child.Focused() {
return currentIndex
}
}
return -1
}
func (element *Container) reflectChildProperties () { func (element *Container) reflectChildProperties () {
element.focusable = false focusable := false
element.forFocusable (func (elements.Focusable) bool { for _, entry := range element.children {
element.focusable = true _, focusable := entry.Element.(elements.Focusable)
return false if focusable {
}) focusable = true
break
}
}
if !focusable && element.Focused() {
element.Propagator.HandleUnfocus()
}
element.flexible = false element.flexible = false
element.forFlexible (func (elements.Flexible) bool { element.forFlexible (func (elements.Flexible) bool {
element.flexible = true element.flexible = true
return false return false
}) })
if !element.focusable {
element.focused = false
}
} }
func (element *Container) childFocusRequestCallback ( func (element *Container) childFocusRequestCallback (
@ -505,11 +303,8 @@ func (element *Container) childFocusRequestCallback (
granted bool, granted bool,
) { ) {
if element.onFocusRequest != nil && element.onFocusRequest() { if element.onFocusRequest != nil && element.onFocusRequest() {
element.focused = true element.Propagator.HandleUnfocus()
element.forFocused (func (child elements.Focusable) bool { element.Propagator.HandleFocus(input.KeynavDirectionNeutral)
child.HandleUnfocus()
return true
})
return true return true
} else { } else {
return false return false
@ -517,20 +312,22 @@ func (element *Container) childFocusRequestCallback (
} }
func (element *Container) updateMinimumSize () { func (element *Container) updateMinimumSize () {
margin := element.theme.Margin(theme.PatternBackground) margin := element.theme.Margin(theme.PatternBackground)
// TODO: have layouts take in x and y margins padding := element.theme.Padding(theme.PatternBackground)
width, height := element.layout.MinimumSize(element.children, margin.X) width, height := element.layout.MinimumSize (
element.children, margin, padding)
if element.flexible { if element.flexible {
height = element.layout.FlexibleHeightFor ( height = element.layout.FlexibleHeightFor (
element.children, element.children, margin,
margin.X, width) padding, width)
} }
element.core.SetMinimumSize(width, height) element.core.SetMinimumSize(width, height)
} }
func (element *Container) doLayout () { func (element *Container) doLayout () {
margin := element.theme.Margin(theme.PatternBackground) margin := element.theme.Margin(theme.PatternBackground)
// TODO: have layouts take in x and y margins padding := element.theme.Padding(theme.PatternBackground)
element.layout.Arrange ( element.layout.Arrange (
element.children, margin.X, element.Bounds()) element.children, margin,
padding, element.Bounds())
} }

347
elements/core/propagator.go Normal file
View File

@ -0,0 +1,347 @@
package core
import "image"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/config"
import "git.tebibyte.media/sashakoshka/tomo/elements"
// Parent represents an object that can provide access to a list of child
// elements.
type Parent interface {
Child (index int) elements.Element
CountChildren () int
}
// Propagator is a struct that can be embedded into elements that contain one or
// more children in order to propagate events to them without having to write
// all of the event handlers. It also implements standard behavior for focus
// propagation and keyboard navigation.
type Propagator struct {
parent Parent
drags [10]elements.MouseTarget
focused bool
onFocusRequest func () (granted bool)
}
// NewPropagator creates a new event propagator that uses the specified parent
// to access a list of child elements that will have events propagated to them.
// If parent is nil, the function will return nil.
func NewPropagator (parent Parent) (propagator *Propagator) {
if parent == nil { return nil }
propagator = &Propagator {
parent: parent,
}
return
}
// ----------- Interface fulfillment methods ----------- //
// Focused returns whether or not this element or any of its children
// are currently focused.
func (propagator *Propagator) Focused () (focused bool) {
return propagator.focused
}
// Focus focuses this element, if its parent element grants the
// request.
func (propagator *Propagator) Focus () {
if propagator.onFocusRequest != nil {
propagator.onFocusRequest()
}
}
// HandleFocus causes this element to mark itself as focused. If the
// element does not have children or there are no more focusable children in
// the given direction, it should return false and do nothing. Otherwise, it
// marks itself as focused along with any applicable children and returns
// true.
func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) {
direction = direction.Canon()
firstFocused := propagator.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 input.KeynavDirectionForward:
// if we recieve a forward direction, focus the first
// focusable element.
return propagator.focusFirstFocusableElement(direction)
case input.KeynavDirectionBackward:
// if we recieve a backward direction, focus the last
// focusable element.
return propagator.focusLastFocusableElement(direction)
case input.KeynavDirectionNeutral:
// if we recieve a neutral direction, just focus this
// element and nothing else.
propagator.focused = true
return true
}
} else {
// an element is currently focused, so we need to move the
// focus in the specified direction
firstFocusedChild :=
propagator.parent.Child(firstFocused).
(elements.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 < propagator.parent.CountChildren() && index >= 0;
index += int(direction) {
child, focusable :=
propagator.parent.Child(index).
(elements.Focusable)
if focusable && child.HandleFocus(direction) {
// we have found one, so we now actually move
// the focus.
firstFocusedChild.HandleUnfocus()
propagator.focused = true
return true
}
}
}
return false
}
// HandleDeselection causes this element to mark itself and all of its children
// as unfocused.
func (propagator *Propagator) HandleUnfocus () {
propagator.forFocusable (func (child elements.Focusable) bool {
child.HandleUnfocus()
return true
})
propagator.focused = false
}
// 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. If the parent element returns true, the
// element acts as if a HandleFocus call was made with KeynavDirectionNeutral.
func (propagator *Propagator) OnFocusRequest (callback func () (granted bool)) {
propagator.onFocusRequest = callback
}
// 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 (propagator *Propagator) OnFocusMotionRequest (
callback func (direction input.KeynavDirection) (granted bool),
) { }
// HandleKeyDown propogates the keyboard event to the currently selected child.
func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
propagator.forFocused (func (child elements.Focusable) bool {
typedChild, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard {
typedChild.HandleKeyDown(key, modifiers)
}
return true
})
}
// HandleKeyUp propogates the keyboard event to the currently selected child.
func (propagator *Propagator) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
propagator.forFocused (func (child elements.Focusable) bool {
typedChild, handlesKeyboard := child.(elements.KeyboardTarget)
if handlesKeyboard {
typedChild.HandleKeyUp(key, modifiers)
}
return true
})
}
// HandleMouseDown propagates the mouse event to the element under the mouse
// pointer.
func (propagator *Propagator) HandleMouseDown (x, y int, button input.Button) {
child, handlesMouse :=
propagator.childAt(image.Pt(x, y)).
(elements.MouseTarget)
if handlesMouse {
propagator.drags[button] = child
child.HandleMouseDown(x, y, button)
}
}
// HandleMouseUp propagates the mouse event to the element that the released
// mouse button was originally pressed on.
func (propagator *Propagator) HandleMouseUp (x, y int, button input.Button) {
child := propagator.drags[button]
if child != nil {
propagator.drags[button] = nil
child.HandleMouseUp(x, y, button)
}
}
// HandleMouseMove propagates the mouse event to the element that was last
// pressed down by the mouse if the mouse is currently being held down, else it
// propagates the event to whichever element is underneath the mouse pointer.
func (propagator *Propagator) HandleMouseMove (x, y int) {
handled := false
for _, child := range propagator.drags {
if child != nil {
child.HandleMouseMove(x, y)
handled = true
}
}
if handled {
child, handlesMouse :=
propagator.childAt(image.Pt(x, y)).
(elements.MouseTarget)
if handlesMouse {
child.HandleMouseMove(x, y)
}
}
}
// HandleScroll propagates the mouse event to the element under the mouse
// pointer.
func (propagator *Propagator) HandleMouseScroll (x, y int, deltaX, deltaY float64) {
child, handlesMouse :=
propagator.childAt(image.Pt(x, y)).
(elements.MouseTarget)
if handlesMouse {
child.HandleMouseScroll(x, y, deltaX, deltaY)
}
}
// SetTheme sets the theme of all children to the specified theme.
func (propagator *Propagator) SetTheme (theme theme.Theme) {
propagator.forChildren (func (child elements.Element) bool {
typedChild, themeable := child.(elements.Themeable)
if themeable {
typedChild.SetTheme(theme)
}
return true
})
}
// SetConfig sets the theme of all children to the specified config.
func (propagator *Propagator) SetConfig (config config.Config) {
propagator.forChildren (func (child elements.Element) bool {
typedChild, configurable := child.(elements.Configurable)
if configurable {
typedChild.SetConfig(config)
}
return true
})
}
// ----------- Focusing utilities ----------- //
func (propagator *Propagator) focusFirstFocusableElement (
direction input.KeynavDirection,
) (
ok bool,
) {
propagator.forFocusable (func (child elements.Focusable) bool {
if child.HandleFocus(direction) {
propagator.focused = true
ok = true
return false
}
return true
})
return
}
func (propagator *Propagator) focusLastFocusableElement (
direction input.KeynavDirection,
) (
ok bool,
) {
propagator.forChildrenReverse (func (child elements.Element) bool {
typedChild, focusable := child.(elements.Focusable)
if focusable && typedChild.HandleFocus(direction) {
propagator.focused = true
ok = true
return false
}
return true
})
return
}
// ----------- Iterator utilities ----------- //
func (propagator *Propagator) forChildren (callback func (child elements.Element) bool) {
for index := 0; index < propagator.parent.CountChildren(); index ++ {
child := propagator.parent.Child(index)
if child == nil { continue }
if !callback(child) { break }
}
}
func (propagator *Propagator) forChildrenReverse (callback func (child elements.Element) bool) {
for index := propagator.parent.CountChildren() - 1; index > 0; index -- {
child := propagator.parent.Child(index)
if child == nil { continue }
if !callback(child) { break }
}
}
func (propagator *Propagator) childAt (position image.Point) (child elements.Element) {
propagator.forChildren (func (current elements.Element) bool {
if position.In(current.Bounds()) {
child = current
}
return true
})
return
}
func (propagator *Propagator) forFocused (callback func (child elements.Focusable) bool) {
propagator.forChildren (func (child elements.Element) bool {
typedChild, focusable := child.(elements.Focusable)
if focusable && typedChild.Focused() {
if !callback(typedChild) { return false }
}
return true
})
}
func (propagator *Propagator) forFocusable (callback func (child elements.Focusable) bool) {
propagator.forChildren (func (child elements.Element) bool {
typedChild, focusable := child.(elements.Focusable)
if focusable {
if !callback(typedChild) { return false }
}
return true
})
}
func (propagator *Propagator) forFlexible (callback func (child elements.Flexible) bool) {
propagator.forChildren (func (child elements.Element) bool {
typedChild, flexible := child.(elements.Flexible)
if flexible {
if !callback(typedChild) { return false }
}
return true
})
}
func (propagator *Propagator) firstFocused () int {
for index := 0; index < propagator.parent.CountChildren(); index ++ {
child, focusable := propagator.parent.Child(index).(elements.Focusable)
if focusable && child.Focused() {
return index
}
}
return -1
}

View File

@ -1,6 +1,7 @@
package basicLayouts package basicLayouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements"
@ -10,7 +11,7 @@ import "git.tebibyte.media/sashakoshka/tomo/elements"
// arranged at the bottom in a row called the control row, which is aligned to // arranged at the bottom in a row called the control row, which is aligned to
// the right, the last element being the rightmost one. // the right, the last element being the rightmost one.
type Dialog struct { type Dialog struct {
// If Gap is true, a gap will be placed between each element. // If Mergin is true, a margin will be placed between each element.
Gap bool Gap bool
// If Pad is true, there will be padding running along the inside of the // If Pad is true, there will be padding running along the inside of the
@ -21,16 +22,17 @@ type Dialog struct {
// Arrange arranges a list of entries into a dialog. // Arrange arranges a list of entries into a dialog.
func (layout Dialog) Arrange ( func (layout Dialog) Arrange (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
bounds image.Rectangle, bounds image.Rectangle,
) { ) {
if layout.Pad { bounds = bounds.Inset(margin) } if layout.Pad { bounds = padding.Apply(bounds) }
controlRowWidth, controlRowHeight := 0, 0 controlRowWidth, controlRowHeight := 0, 0
if len(entries) > 1 { if len(entries) > 1 {
controlRowWidth, controlRowWidth,
controlRowHeight = layout.minimumSizeOfControlRow ( controlRowHeight = layout.minimumSizeOfControlRow (
entries[1:], margin) entries[1:], margin, padding)
} }
if len(entries) > 0 { if len(entries) > 0 {
@ -38,7 +40,7 @@ func (layout Dialog) Arrange (
main.Bounds.Min = bounds.Min main.Bounds.Min = bounds.Min
mainHeight := bounds.Dy() - controlRowHeight mainHeight := bounds.Dy() - controlRowHeight
if layout.Gap { if layout.Gap {
mainHeight -= margin mainHeight -= margin.Y
} }
main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight)) main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight))
entries[0] = main entries[0] = main
@ -58,7 +60,7 @@ func (layout Dialog) Arrange (
freeSpace -= entryMinWidth freeSpace -= entryMinWidth
} }
if index > 0 && layout.Gap { if index > 0 && layout.Gap {
freeSpace -= margin freeSpace -= margin.X
} }
} }
expandingElementWidth := 0 expandingElementWidth := 0
@ -74,7 +76,7 @@ func (layout Dialog) Arrange (
// set the size and position of each element in the control row // set the size and position of each element in the control row
for index, entry := range entries[1:] { for index, entry := range entries[1:] {
if index > 0 && layout.Gap { dot.X += margin } if index > 0 && layout.Gap { dot.X += margin.X }
entry.Bounds.Min = dot entry.Bounds.Min = dot
entryWidth := 0 entryWidth := 0
@ -101,7 +103,8 @@ func (layout Dialog) Arrange (
// arrange the given list of entries. // arrange the given list of entries.
func (layout Dialog) MinimumSize ( func (layout Dialog) MinimumSize (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
) ( ) (
width, height int, width, height int,
) { ) {
@ -112,10 +115,10 @@ func (layout Dialog) MinimumSize (
} }
if len(entries) > 1 { if len(entries) > 1 {
if layout.Gap { height += margin } if layout.Gap { height += margin.X }
additionalWidth, additionalWidth,
additionalHeight := layout.minimumSizeOfControlRow ( additionalHeight := layout.minimumSizeOfControlRow (
entries[1:], margin) entries[1:], margin, padding)
height += additionalHeight height += additionalHeight
if additionalWidth > width { if additionalWidth > width {
width = additionalWidth width = additionalWidth
@ -123,8 +126,8 @@ func (layout Dialog) MinimumSize (
} }
if layout.Pad { if layout.Pad {
width += margin * 2 width += padding.Horizontal()
height += margin * 2 height += padding.Vertical()
} }
return return
} }
@ -133,13 +136,14 @@ func (layout Dialog) MinimumSize (
// specified elements at the given width, taking into account flexible elements. // specified elements at the given width, taking into account flexible elements.
func (layout Dialog) FlexibleHeightFor ( func (layout Dialog) FlexibleHeightFor (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
width int, width int,
) ( ) (
height int, height int,
) { ) {
if layout.Pad { if layout.Pad {
width -= margin * 2 width -= padding.Horizontal()
} }
if len(entries) > 0 { if len(entries) > 0 {
@ -153,14 +157,14 @@ func (layout Dialog) FlexibleHeightFor (
} }
if len(entries) > 1 { if len(entries) > 1 {
if layout.Gap { height += margin } if layout.Gap { height += margin.Y }
_, additionalHeight := layout.minimumSizeOfControlRow ( _, additionalHeight := layout.minimumSizeOfControlRow (
entries[1:], margin) entries[1:], margin, padding)
height += additionalHeight height += additionalHeight
} }
if layout.Pad { if layout.Pad {
height += margin * 2 height += padding.Vertical()
} }
return return
} }
@ -169,7 +173,8 @@ func (layout Dialog) FlexibleHeightFor (
// the control row. // the control row.
func (layout Dialog) minimumSizeOfControlRow ( func (layout Dialog) minimumSizeOfControlRow (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
) ( ) (
width, height int, width, height int,
) { ) {
@ -180,7 +185,7 @@ func (layout Dialog) minimumSizeOfControlRow (
} }
width += entryWidth width += entryWidth
if layout.Gap && index > 0 { if layout.Gap && index > 0 {
width += margin width += margin.X
} }
} }
return return

View File

@ -1,6 +1,7 @@
package basicLayouts package basicLayouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements"
@ -19,19 +20,20 @@ type Horizontal struct {
// Arrange arranges a list of entries horizontally. // Arrange arranges a list of entries horizontally.
func (layout Horizontal) Arrange ( func (layout Horizontal) Arrange (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
bounds image.Rectangle, bounds image.Rectangle,
) { ) {
if layout.Pad { bounds = bounds.Inset(margin) } if layout.Pad { bounds = padding.Apply(bounds) }
// get width of expanding elements // get width of expanding elements
expandingElementWidth := layout.expandingElementWidth ( expandingElementWidth := layout.expandingElementWidth (
entries, margin, bounds.Dx()) entries, margin, padding, bounds.Dx())
// set the size and position of each element // set the size and position of each element
dot := bounds.Min dot := bounds.Min
for index, entry := range entries { for index, entry := range entries {
if index > 0 && layout.Gap { dot.X += margin } if index > 0 && layout.Gap { dot.X += margin.X }
entry.Bounds.Min = dot entry.Bounds.Min = dot
entryWidth := 0 entryWidth := 0
@ -51,7 +53,8 @@ func (layout Horizontal) Arrange (
// arrange the given list of entries. // arrange the given list of entries.
func (layout Horizontal) MinimumSize ( func (layout Horizontal) MinimumSize (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
) ( ) (
width, height int, width, height int,
) { ) {
@ -62,13 +65,13 @@ func (layout Horizontal) MinimumSize (
} }
width += entryWidth width += entryWidth
if layout.Gap && index > 0 { if layout.Gap && index > 0 {
width += margin width += margin.X
} }
} }
if layout.Pad { if layout.Pad {
width += margin * 2 width += padding.Horizontal()
height += margin * 2 height += padding.Vertical()
} }
return return
} }
@ -77,21 +80,22 @@ func (layout Horizontal) MinimumSize (
// specified elements at the given width, taking into account flexible elements. // specified elements at the given width, taking into account flexible elements.
func (layout Horizontal) FlexibleHeightFor ( func (layout Horizontal) FlexibleHeightFor (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
width int, width int,
) ( ) (
height int, height int,
) { ) {
if layout.Pad { width -= margin * 2 } if layout.Pad { width -= padding.Horizontal() }
// get width of expanding elements // get width of expanding elements
expandingElementWidth := layout.expandingElementWidth ( expandingElementWidth := layout.expandingElementWidth (
entries, margin, width) entries, margin, padding, width)
x, y := 0, 0 x, y := 0, 0
if layout.Pad { if layout.Pad {
x += margin x += padding.Horizontal()
y += margin y += padding.Vertical()
} }
// set the size and position of each element // set the size and position of each element
@ -106,18 +110,19 @@ func (layout Horizontal) FlexibleHeightFor (
if entryHeight > height { height = entryHeight } if entryHeight > height { height = entryHeight }
x += entryWidth x += entryWidth
if index > 0 && layout.Gap { x += margin } if index > 0 && layout.Gap { x += margin.X }
} }
if layout.Pad { if layout.Pad {
height += margin * 2 height += padding.Vertical()
} }
return return
} }
func (layout Horizontal) expandingElementWidth ( func (layout Horizontal) expandingElementWidth (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
freeSpace int, freeSpace int,
) ( ) (
width int, width int,
@ -134,7 +139,7 @@ func (layout Horizontal) expandingElementWidth (
freeSpace -= entryMinWidth freeSpace -= entryMinWidth
} }
if index > 0 && layout.Gap { if index > 0 && layout.Gap {
freeSpace -= margin freeSpace -= margin.X
} }
} }

View File

@ -1,6 +1,7 @@
package basicLayouts package basicLayouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements"
@ -19,10 +20,11 @@ type Vertical struct {
// Arrange arranges a list of entries vertically. // Arrange arranges a list of entries vertically.
func (layout Vertical) Arrange ( func (layout Vertical) Arrange (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
bounds image.Rectangle, bounds image.Rectangle,
) { ) {
if layout.Pad { bounds = bounds.Inset(margin) } if layout.Pad { bounds = padding.Apply(bounds) }
// count the number of expanding elements and the amount of free space // count the number of expanding elements and the amount of free space
// for them to collectively occupy, while gathering minimum heights. // for them to collectively occupy, while gathering minimum heights.
@ -45,7 +47,7 @@ func (layout Vertical) Arrange (
freeSpace -= entryMinHeight freeSpace -= entryMinHeight
} }
if index > 0 && layout.Gap { if index > 0 && layout.Gap {
freeSpace -= margin freeSpace -= margin.Y
} }
} }
@ -57,7 +59,7 @@ func (layout Vertical) Arrange (
// set the size and position of each element // set the size and position of each element
dot := bounds.Min dot := bounds.Min
for index, entry := range entries { for index, entry := range entries {
if index > 0 && layout.Gap { dot.Y += margin } if index > 0 && layout.Gap { dot.Y += margin.Y }
entry.Bounds.Min = dot entry.Bounds.Min = dot
entryHeight := 0 entryHeight := 0
@ -77,7 +79,8 @@ func (layout Vertical) Arrange (
// arrange the given list of entries. // arrange the given list of entries.
func (layout Vertical) MinimumSize ( func (layout Vertical) MinimumSize (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
) ( ) (
width, height int, width, height int,
) { ) {
@ -88,13 +91,13 @@ func (layout Vertical) MinimumSize (
} }
height += entryHeight height += entryHeight
if layout.Gap && index > 0 { if layout.Gap && index > 0 {
height += margin height += margin.Y
} }
} }
if layout.Pad { if layout.Pad {
width += margin * 2 width += padding.Horizontal()
height += margin * 2 height += padding.Vertical()
} }
return return
} }
@ -103,14 +106,15 @@ func (layout Vertical) MinimumSize (
// specified elements at the given width, taking into account flexible elements. // specified elements at the given width, taking into account flexible elements.
func (layout Vertical) FlexibleHeightFor ( func (layout Vertical) FlexibleHeightFor (
entries []layouts.LayoutEntry, entries []layouts.LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
width int, width int,
) ( ) (
height int, height int,
) { ) {
if layout.Pad { if layout.Pad {
width -= margin * 2 width -= padding.Horizontal()
height += margin * 2 height += padding.Vertical()
} }
for index, entry := range entries { for index, entry := range entries {
@ -123,7 +127,7 @@ func (layout Vertical) FlexibleHeightFor (
} }
if layout.Gap && index > 0 { if layout.Gap && index > 0 {
height += margin height += margin.Y
} }
} }
return return

View File

@ -1,6 +1,7 @@
package layouts package layouts
import "image" import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements"
// LayoutEntry associates an element with layout and positioning information so // LayoutEntry associates an element with layout and positioning information so
@ -11,6 +12,10 @@ type LayoutEntry struct {
Expand bool Expand bool
} }
// TODO: have layouts take in artist.Inset for margin and padding
// TODO: create a layout that only displays the first element and full screen.
// basically a blank layout for containers that only ever have one element.
// Layout is capable of arranging elements within a container. It is also able // Layout is capable of arranging elements within a container. It is also able
// to determine the minimum amount of room it needs to do so. // to determine the minimum amount of room it needs to do so.
type Layout interface { type Layout interface {
@ -18,18 +23,30 @@ type Layout interface {
// and changes the position of the entiries in the slice so that they // and changes the position of the entiries in the slice so that they
// are properly laid out. The given width and height should not be less // are properly laid out. The given width and height should not be less
// than what is returned by MinimumSize. // than what is returned by MinimumSize.
Arrange (entries []LayoutEntry, margin int, bounds image.Rectangle) Arrange (
entries []LayoutEntry,
margin image.Point,
padding artist.Inset,
bounds image.Rectangle,
)
// MinimumSize returns the minimum width and height that the layout // MinimumSize returns the minimum width and height that the layout
// needs to properly arrange the given slice of layout entries. // needs to properly arrange the given slice of layout entries.
MinimumSize (entries []LayoutEntry, margin int) (width, height int) MinimumSize (
entries []LayoutEntry,
margin image.Point,
padding artist.Inset,
) (
width, height int,
)
// FlexibleHeightFor Returns the minimum height the layout needs to lay // FlexibleHeightFor Returns the minimum height the layout needs to lay
// out the specified elements at the given width, taking into account // out the specified elements at the given width, taking into account
// flexible elements. // flexible elements.
FlexibleHeightFor ( FlexibleHeightFor (
entries []LayoutEntry, entries []LayoutEntry,
margin int, margin image.Point,
padding artist.Inset,
squeeze int, squeeze int,
) ( ) (
height int, height int,

View File

@ -6,6 +6,7 @@ import _ "embed"
import _ "image/png" import _ "image/png"
import "image/color" import "image/color"
import "golang.org/x/image/font" import "golang.org/x/image/font"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/defaultfont" import "git.tebibyte.media/sashakoshka/tomo/defaultfont"
@ -88,6 +89,13 @@ func (Default) Icon (string, IconSize, Case) canvas.Image {
return nil return nil
} }
// MimeIcon returns an icon from the default set corresponding to the given mime.
// type.
func (Default) MimeIcon (data.Mime, IconSize, Case) canvas.Image {
// TODO
return nil
}
// Pattern returns a pattern from the default theme corresponding to the given // Pattern returns a pattern from the default theme corresponding to the given
// pattern ID. // pattern ID.
func (Default) Pattern (id Pattern, state State, c Case) artist.Pattern { func (Default) Pattern (id Pattern, state State, c Case) artist.Pattern {

View File

@ -3,6 +3,7 @@ package theme
import "image" import "image"
import "image/color" import "image/color"
import "golang.org/x/image/font" import "golang.org/x/image/font"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/canvas"
@ -83,6 +84,10 @@ type Theme interface {
// Icon returns an appropriate icon given an icon name, size, and case. // Icon returns an appropriate icon given an icon name, size, and case.
Icon (string, IconSize, Case) canvas.Image Icon (string, IconSize, Case) canvas.Image
// Icon returns an appropriate icon given a file mime type, size, and,
// case.
MimeIcon (data.Mime, IconSize, Case) canvas.Image
// Pattern returns an appropriate pattern given a pattern name, case, // Pattern returns an appropriate pattern given a pattern name, case,
// and state. // and state.

View File

@ -3,6 +3,7 @@ package theme
import "image" import "image"
import "image/color" import "image/color"
import "golang.org/x/image/font" import "golang.org/x/image/font"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/canvas"
@ -26,6 +27,12 @@ func (wrapped Wrapped) Icon (name string, size IconSize) canvas.Image {
return real.Icon(name, size, wrapped.Case) return real.Icon(name, size, wrapped.Case)
} }
// MimeIcon returns an appropriate icon given file mime type.
func (wrapped Wrapped) MimeIcon (mime data.Mime, size IconSize) canvas.Image {
real := wrapped.ensure()
return real.MimeIcon(mime, size, wrapped.Case)
}
// Pattern returns an appropriate pattern given a pattern name and state. // Pattern returns an appropriate pattern given a pattern name and state.
func (wrapped Wrapped) Pattern (id Pattern, state State) artist.Pattern { func (wrapped Wrapped) Pattern (id Pattern, state State) artist.Pattern {
real := wrapped.ensure() real := wrapped.ensure()