diff --git a/elements/basic/button.go b/elements/basic/button.go index e11a827..3b5cf9b 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -5,8 +5,8 @@ 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/artist" -import "git.tebibyte.media/sashakoshka/tomo/shatter" +// import "git.tebibyte.media/sashakoshka/tomo/artist" +// import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" 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.Core, element.core = core.NewCore(element.drawAll) element.FocusableCore, - element.focusableControl = core.NewFocusableCore (func () { - element.drawAndPush(true) - }) + element.focusableControl = core.NewFocusableCore(element.drawAndPush) element.SetText(text) return } @@ -45,7 +43,7 @@ func (element *Button) HandleMouseDown (x, y int, button input.Button) { if !element.Focused() { element.Focus() } if button != input.ButtonLeft { return } element.pressed = true - element.drawAndPush(true) + element.drawAndPush() } 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 { element.onClick() } - element.drawAndPush(true) + element.drawAndPush() } 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 key == input.KeyEnter { element.pressed = true - element.drawAndPush(true) + element.drawAndPush() } } func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter && element.pressed { element.pressed = false - element.drawAndPush(true) + element.drawAndPush() if !element.Enabled() { return } if element.onClick != nil { element.onClick() @@ -98,7 +96,7 @@ func (element *Button) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) element.updateMinimumSize() - element.drawAndPush(false) + element.drawAndPush() } // SetTheme sets the element's theme. @@ -109,7 +107,7 @@ func (element *Button) SetTheme (new theme.Theme) { theme.FontStyleRegular, theme.FontSizeNormal)) element.updateMinimumSize() - element.drawAndPush(false) + element.drawAndPush() } // SetConfig sets the element's configuration. @@ -117,7 +115,7 @@ func (element *Button) SetConfig (new config.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.drawAndPush(false) + element.drawAndPush() } func (element *Button) updateMinimumSize () { @@ -127,19 +125,6 @@ func (element *Button) updateMinimumSize () { 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 { return theme.State { Disabled: !element.Enabled(), @@ -148,23 +133,28 @@ func (element *Button) state () theme.State { } } -func (element *Button) drawBackground (partial bool) []image.Rectangle { - state := element.state() - bounds := element.Bounds() - pattern := element.theme.Pattern(theme.PatternButton, state) - 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) drawAndPush () { + if element.core.HasImage () { + element.drawAll() + element.core.DamageAll() } } -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() bounds := element.Bounds() foreground := element.theme.Color(theme.ColorForeground, state) @@ -182,17 +172,7 @@ func (element *Button) drawText (partial bool) image.Rectangle { if element.pressed { 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) return region } - -func (element *Button) drawAll () { - element.drawBackground(false) - element.drawText(false) -} diff --git a/elements/basic/container.go b/elements/basic/container.go index 0347def..008a1d8 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -14,22 +14,20 @@ import "git.tebibyte.media/sashakoshka/tomo/elements/core" // them in a layout. type Container struct { *core.Core + *core.Propagator core core.CoreControl layout layouts.Layout children []layouts.LayoutEntry - drags [10]elements.MouseTarget warping bool - focused bool - focusable bool flexible bool config config.Wrapped theme theme.Wrapped + onFlexibleHeightChange func () onFocusRequest func () (granted bool) onFocusMotionRequest func (input.KeynavDirection) (granted bool) - onFlexibleHeightChange func () } // NewContainer creates a new container. @@ -37,6 +35,7 @@ func NewContainer (layout layouts.Layout) (element *Container) { element = &Container { } element.theme.Case = theme.C("basic", "container") element.Core, element.core = core.NewCore(element.redoAll) + element.Propagator = core.NewPropagator(element) element.SetLayout(layout) return } @@ -161,6 +160,9 @@ func (element *Container) clearChildEventHandlers (child elements.Element) { // DisownAll removes all child elements from the container at once. func (element *Container) DisownAll () { + for _, entry := range element.children { + element.clearChildEventHandlers(entry.Element) + } element.children = nil element.updateMinimumSize() @@ -203,17 +205,6 @@ func (element *Container) ChildAt (point image.Point) (child elements.Element) { 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 () { if !element.core.HasImage() { return } // do a layout @@ -236,16 +227,11 @@ func (element *Container) redoAll () { } } - // SetTheme sets the element's theme. func (element *Container) SetTheme (new theme.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new - for _, child := range element.children { - if child0, ok := child.Element.(elements.Themeable); ok { - child0.SetTheme(element.theme.Theme) - } - } + element.Propagator.SetTheme(new) element.updateMinimumSize() element.redoAll() } @@ -253,206 +239,33 @@ func (element *Container) SetTheme (new theme.Theme) { // SetConfig sets the element's configuration. func (element *Container) SetConfig (new config.Config) { if new == element.config.Config { return } - element.config.Config = new - for _, child := range element.children { - if child0, ok := child.Element.(elements.Configurable); ok { - child0.SetConfig(element.config) - } - } + element.Propagator.SetConfig(new) element.updateMinimumSize() 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) { - margin := element.theme.Margin(theme.PatternBackground) - // TODO: have layouts take in x and y margins + margin := element.theme.Margin(theme.PatternBackground) + padding := element.theme.Padding(theme.PatternBackground) return element.layout.FlexibleHeightFor ( element.children, - margin.X, width) + margin, padding, width) } func (element *Container) OnFlexibleHeightChange (callback func ()) { 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)) { element.onFocusRequest = callback + element.Propagator.OnFocusRequest(callback) } func (element *Container) OnFocusMotionRequest ( callback func (direction input.KeynavDirection) (granted bool), ) { element.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 } - } - } + element.Propagator.OnFocusMotionRequest(callback) } 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 () { - element.focusable = false - element.forFocusable (func (elements.Focusable) bool { - element.focusable = true - return false - }) + focusable := false + for _, entry := range element.children { + _, focusable := entry.Element.(elements.Focusable) + if focusable { + focusable = true + break + } + } + if !focusable && element.Focused() { + element.Propagator.HandleUnfocus() + } + element.flexible = false element.forFlexible (func (elements.Flexible) bool { element.flexible = true return false }) - if !element.focusable { - element.focused = false - } } func (element *Container) childFocusRequestCallback ( @@ -505,11 +303,8 @@ func (element *Container) childFocusRequestCallback ( granted bool, ) { if element.onFocusRequest != nil && element.onFocusRequest() { - element.focused = true - element.forFocused (func (child elements.Focusable) bool { - child.HandleUnfocus() - return true - }) + element.Propagator.HandleUnfocus() + element.Propagator.HandleFocus(input.KeynavDirectionNeutral) return true } else { return false @@ -517,20 +312,22 @@ func (element *Container) childFocusRequestCallback ( } func (element *Container) updateMinimumSize () { - margin := element.theme.Margin(theme.PatternBackground) - // TODO: have layouts take in x and y margins - width, height := element.layout.MinimumSize(element.children, margin.X) + margin := element.theme.Margin(theme.PatternBackground) + padding := element.theme.Padding(theme.PatternBackground) + width, height := element.layout.MinimumSize ( + element.children, margin, padding) if element.flexible { height = element.layout.FlexibleHeightFor ( - element.children, - margin.X, width) + element.children, margin, + padding, width) } element.core.SetMinimumSize(width, height) } func (element *Container) doLayout () { 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.children, margin.X, element.Bounds()) + element.children, margin, + padding, element.Bounds()) } diff --git a/elements/core/propagator.go b/elements/core/propagator.go new file mode 100644 index 0000000..291d406 --- /dev/null +++ b/elements/core/propagator.go @@ -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 +} diff --git a/layouts/basic/dialog.go b/layouts/basic/dialog.go index ba75df7..ed3b45e 100644 --- a/layouts/basic/dialog.go +++ b/layouts/basic/dialog.go @@ -1,6 +1,7 @@ package basicLayouts import "image" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/layouts" 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 // the right, the last element being the rightmost one. 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 // 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. func (layout Dialog) Arrange ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(margin) } + if layout.Pad { bounds = padding.Apply(bounds) } controlRowWidth, controlRowHeight := 0, 0 if len(entries) > 1 { controlRowWidth, controlRowHeight = layout.minimumSizeOfControlRow ( - entries[1:], margin) + entries[1:], margin, padding) } if len(entries) > 0 { @@ -38,7 +40,7 @@ func (layout Dialog) Arrange ( main.Bounds.Min = bounds.Min mainHeight := bounds.Dy() - controlRowHeight if layout.Gap { - mainHeight -= margin + mainHeight -= margin.Y } main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight)) entries[0] = main @@ -58,7 +60,7 @@ func (layout Dialog) Arrange ( freeSpace -= entryMinWidth } if index > 0 && layout.Gap { - freeSpace -= margin + freeSpace -= margin.X } } expandingElementWidth := 0 @@ -74,7 +76,7 @@ func (layout Dialog) Arrange ( // set the size and position of each element in the control row 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 entryWidth := 0 @@ -101,7 +103,8 @@ func (layout Dialog) Arrange ( // arrange the given list of entries. func (layout Dialog) MinimumSize ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) { @@ -112,10 +115,10 @@ func (layout Dialog) MinimumSize ( } if len(entries) > 1 { - if layout.Gap { height += margin } + if layout.Gap { height += margin.X } additionalWidth, additionalHeight := layout.minimumSizeOfControlRow ( - entries[1:], margin) + entries[1:], margin, padding) height += additionalHeight if additionalWidth > width { width = additionalWidth @@ -123,8 +126,8 @@ func (layout Dialog) MinimumSize ( } if layout.Pad { - width += margin * 2 - height += margin * 2 + width += padding.Horizontal() + height += padding.Vertical() } return } @@ -133,13 +136,14 @@ func (layout Dialog) MinimumSize ( // specified elements at the given width, taking into account flexible elements. func (layout Dialog) FlexibleHeightFor ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, width int, ) ( height int, ) { if layout.Pad { - width -= margin * 2 + width -= padding.Horizontal() } if len(entries) > 0 { @@ -153,14 +157,14 @@ func (layout Dialog) FlexibleHeightFor ( } if len(entries) > 1 { - if layout.Gap { height += margin } + if layout.Gap { height += margin.Y } _, additionalHeight := layout.minimumSizeOfControlRow ( - entries[1:], margin) + entries[1:], margin, padding) height += additionalHeight } if layout.Pad { - height += margin * 2 + height += padding.Vertical() } return } @@ -169,7 +173,8 @@ func (layout Dialog) FlexibleHeightFor ( // the control row. func (layout Dialog) minimumSizeOfControlRow ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) { @@ -180,7 +185,7 @@ func (layout Dialog) minimumSizeOfControlRow ( } width += entryWidth if layout.Gap && index > 0 { - width += margin + width += margin.X } } return diff --git a/layouts/basic/horizontal.go b/layouts/basic/horizontal.go index 220dcb8..e27c3fe 100644 --- a/layouts/basic/horizontal.go +++ b/layouts/basic/horizontal.go @@ -1,6 +1,7 @@ package basicLayouts import "image" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" @@ -19,19 +20,20 @@ type Horizontal struct { // Arrange arranges a list of entries horizontally. func (layout Horizontal) Arrange ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(margin) } + if layout.Pad { bounds = padding.Apply(bounds) } // get width of expanding elements expandingElementWidth := layout.expandingElementWidth ( - entries, margin, bounds.Dx()) + entries, margin, padding, bounds.Dx()) // set the size and position of each element dot := bounds.Min 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 entryWidth := 0 @@ -51,7 +53,8 @@ func (layout Horizontal) Arrange ( // arrange the given list of entries. func (layout Horizontal) MinimumSize ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) { @@ -62,13 +65,13 @@ func (layout Horizontal) MinimumSize ( } width += entryWidth if layout.Gap && index > 0 { - width += margin + width += margin.X } } if layout.Pad { - width += margin * 2 - height += margin * 2 + width += padding.Horizontal() + height += padding.Vertical() } return } @@ -77,21 +80,22 @@ func (layout Horizontal) MinimumSize ( // specified elements at the given width, taking into account flexible elements. func (layout Horizontal) FlexibleHeightFor ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, width int, ) ( height int, ) { - if layout.Pad { width -= margin * 2 } + if layout.Pad { width -= padding.Horizontal() } // get width of expanding elements expandingElementWidth := layout.expandingElementWidth ( - entries, margin, width) + entries, margin, padding, width) x, y := 0, 0 if layout.Pad { - x += margin - y += margin + x += padding.Horizontal() + y += padding.Vertical() } // set the size and position of each element @@ -106,18 +110,19 @@ func (layout Horizontal) FlexibleHeightFor ( if entryHeight > height { height = entryHeight } x += entryWidth - if index > 0 && layout.Gap { x += margin } + if index > 0 && layout.Gap { x += margin.X } } if layout.Pad { - height += margin * 2 + height += padding.Vertical() } return } func (layout Horizontal) expandingElementWidth ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, freeSpace int, ) ( width int, @@ -134,7 +139,7 @@ func (layout Horizontal) expandingElementWidth ( freeSpace -= entryMinWidth } if index > 0 && layout.Gap { - freeSpace -= margin + freeSpace -= margin.X } } diff --git a/layouts/basic/vertical.go b/layouts/basic/vertical.go index 2002db4..a66c648 100644 --- a/layouts/basic/vertical.go +++ b/layouts/basic/vertical.go @@ -1,6 +1,7 @@ package basicLayouts import "image" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" @@ -19,10 +20,11 @@ type Vertical struct { // Arrange arranges a list of entries vertically. func (layout Vertical) Arrange ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, 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 // for them to collectively occupy, while gathering minimum heights. @@ -45,7 +47,7 @@ func (layout Vertical) Arrange ( freeSpace -= entryMinHeight } 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 dot := bounds.Min 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 entryHeight := 0 @@ -77,7 +79,8 @@ func (layout Vertical) Arrange ( // arrange the given list of entries. func (layout Vertical) MinimumSize ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) { @@ -88,13 +91,13 @@ func (layout Vertical) MinimumSize ( } height += entryHeight if layout.Gap && index > 0 { - height += margin + height += margin.Y } } if layout.Pad { - width += margin * 2 - height += margin * 2 + width += padding.Horizontal() + height += padding.Vertical() } return } @@ -103,14 +106,15 @@ func (layout Vertical) MinimumSize ( // specified elements at the given width, taking into account flexible elements. func (layout Vertical) FlexibleHeightFor ( entries []layouts.LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, width int, ) ( height int, ) { if layout.Pad { - width -= margin * 2 - height += margin * 2 + width -= padding.Horizontal() + height += padding.Vertical() } for index, entry := range entries { @@ -123,7 +127,7 @@ func (layout Vertical) FlexibleHeightFor ( } if layout.Gap && index > 0 { - height += margin + height += margin.Y } } return diff --git a/layouts/layout.go b/layouts/layout.go index 1e44f9b..786a2b3 100644 --- a/layouts/layout.go +++ b/layouts/layout.go @@ -1,6 +1,7 @@ package layouts import "image" +import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/elements" // LayoutEntry associates an element with layout and positioning information so @@ -11,6 +12,10 @@ type LayoutEntry struct { 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 // to determine the minimum amount of room it needs to do so. type Layout interface { @@ -18,18 +23,30 @@ type Layout interface { // 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 // 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 // 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 // out the specified elements at the given width, taking into account // flexible elements. FlexibleHeightFor ( entries []LayoutEntry, - margin int, + margin image.Point, + padding artist.Inset, squeeze int, ) ( height int, diff --git a/theme/default.go b/theme/default.go index 061db58..7233cc5 100644 --- a/theme/default.go +++ b/theme/default.go @@ -6,6 +6,7 @@ import _ "embed" import _ "image/png" import "image/color" 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/canvas" import "git.tebibyte.media/sashakoshka/tomo/defaultfont" @@ -88,6 +89,13 @@ func (Default) Icon (string, IconSize, Case) canvas.Image { 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 ID. func (Default) Pattern (id Pattern, state State, c Case) artist.Pattern { diff --git a/theme/theme.go b/theme/theme.go index 840c599..85437b8 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -3,6 +3,7 @@ package theme import "image" import "image/color" 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/canvas" @@ -83,6 +84,10 @@ type Theme interface { // Icon returns an appropriate icon given an icon name, size, and case. 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, // and state. diff --git a/theme/wrapped.go b/theme/wrapped.go index 731c6fc..200a5e2 100644 --- a/theme/wrapped.go +++ b/theme/wrapped.go @@ -3,6 +3,7 @@ package theme import "image" import "image/color" 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/canvas" @@ -26,6 +27,12 @@ func (wrapped Wrapped) Icon (name string, size IconSize) canvas.Image { 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. func (wrapped Wrapped) Pattern (id Pattern, state State) artist.Pattern { real := wrapped.ensure()