From 285cb4810f89bfb742e81c868445c3e7e4262ba8 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 2 Mar 2023 16:48:37 -0500 Subject: [PATCH 01/20] Remove margin from layout interface Layouts will need to store margin and padding values within themseleves. --- layouts/layout.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/layouts/layout.go b/layouts/layout.go index 1e44f9b..9921a88 100644 --- a/layouts/layout.go +++ b/layouts/layout.go @@ -18,18 +18,17 @@ 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, 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) (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, squeeze int, ) ( height int, From e9e6e4fbe71bd13dafccdb98093bbddd6e2bcf87 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 2 Mar 2023 17:58:42 -0500 Subject: [PATCH 02/20] Added padding/margin distinction to layouts --- layouts/basic/dialog.go | 20 ++++++++++++-------- layouts/basic/horizontal.go | 17 ++++++++++------- layouts/basic/vertical.go | 13 ++++++++----- layouts/layout.go | 17 +++++++++++++++-- 4 files changed, 45 insertions(+), 22 deletions(-) diff --git a/layouts/basic/dialog.go b/layouts/basic/dialog.go index ba75df7..f7fdc4f 100644 --- a/layouts/basic/dialog.go +++ b/layouts/basic/dialog.go @@ -10,7 +10,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 @@ -22,15 +22,16 @@ type Dialog struct { func (layout Dialog) Arrange ( entries []layouts.LayoutEntry, margin int, + padding int, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(margin) } + if layout.Pad { bounds = bounds.Inset(padding) } controlRowWidth, controlRowHeight := 0, 0 if len(entries) > 1 { controlRowWidth, controlRowHeight = layout.minimumSizeOfControlRow ( - entries[1:], margin) + entries[1:], margin, padding) } if len(entries) > 0 { @@ -102,6 +103,7 @@ func (layout Dialog) Arrange ( func (layout Dialog) MinimumSize ( entries []layouts.LayoutEntry, margin int, + padding int, ) ( width, height int, ) { @@ -115,7 +117,7 @@ func (layout Dialog) MinimumSize ( if layout.Gap { height += margin } additionalWidth, additionalHeight := layout.minimumSizeOfControlRow ( - entries[1:], margin) + entries[1:], margin, padding) height += additionalHeight if additionalWidth > width { width = additionalWidth @@ -123,8 +125,8 @@ func (layout Dialog) MinimumSize ( } if layout.Pad { - width += margin * 2 - height += margin * 2 + width += padding * 2 + height += padding * 2 } return } @@ -134,6 +136,7 @@ func (layout Dialog) MinimumSize ( func (layout Dialog) FlexibleHeightFor ( entries []layouts.LayoutEntry, margin int, + padding int, width int, ) ( height int, @@ -155,12 +158,12 @@ func (layout Dialog) FlexibleHeightFor ( if len(entries) > 1 { if layout.Gap { height += margin } _, additionalHeight := layout.minimumSizeOfControlRow ( - entries[1:], margin) + entries[1:], margin, padding) height += additionalHeight } if layout.Pad { - height += margin * 2 + height += padding * 2 } return } @@ -170,6 +173,7 @@ func (layout Dialog) FlexibleHeightFor ( func (layout Dialog) minimumSizeOfControlRow ( entries []layouts.LayoutEntry, margin int, + padding int, ) ( width, height int, ) { diff --git a/layouts/basic/horizontal.go b/layouts/basic/horizontal.go index 220dcb8..d37e27d 100644 --- a/layouts/basic/horizontal.go +++ b/layouts/basic/horizontal.go @@ -20,13 +20,14 @@ type Horizontal struct { func (layout Horizontal) Arrange ( entries []layouts.LayoutEntry, margin int, + padding int, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(margin) } + if layout.Pad { bounds = bounds.Inset(padding) } // 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 @@ -78,20 +79,21 @@ func (layout Horizontal) MinimumSize ( func (layout Horizontal) FlexibleHeightFor ( entries []layouts.LayoutEntry, margin int, + padding int, width int, ) ( height int, ) { - if layout.Pad { width -= margin * 2 } + if layout.Pad { width -= padding * 2 } // 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 + y += padding } // set the size and position of each element @@ -110,7 +112,7 @@ func (layout Horizontal) FlexibleHeightFor ( } if layout.Pad { - height += margin * 2 + height += padding * 2 } return } @@ -118,6 +120,7 @@ func (layout Horizontal) FlexibleHeightFor ( func (layout Horizontal) expandingElementWidth ( entries []layouts.LayoutEntry, margin int, + padding int, freeSpace int, ) ( width int, diff --git a/layouts/basic/vertical.go b/layouts/basic/vertical.go index 2002db4..dc43da8 100644 --- a/layouts/basic/vertical.go +++ b/layouts/basic/vertical.go @@ -20,9 +20,10 @@ type Vertical struct { func (layout Vertical) Arrange ( entries []layouts.LayoutEntry, margin int, + padding int, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(margin) } + if layout.Pad { bounds = bounds.Inset(padding) } // count the number of expanding elements and the amount of free space // for them to collectively occupy, while gathering minimum heights. @@ -78,6 +79,7 @@ func (layout Vertical) Arrange ( func (layout Vertical) MinimumSize ( entries []layouts.LayoutEntry, margin int, + padding int, ) ( width, height int, ) { @@ -93,8 +95,8 @@ func (layout Vertical) MinimumSize ( } if layout.Pad { - width += margin * 2 - height += margin * 2 + width += padding * 2 + height += padding * 2 } return } @@ -104,13 +106,14 @@ func (layout Vertical) MinimumSize ( func (layout Vertical) FlexibleHeightFor ( entries []layouts.LayoutEntry, margin int, + padding int, width int, ) ( height int, ) { if layout.Pad { - width -= margin * 2 - height += margin * 2 + width -= padding * 2 + height += padding * 2 } for index, entry := range entries { diff --git a/layouts/layout.go b/layouts/layout.go index 9921a88..e847e27 100644 --- a/layouts/layout.go +++ b/layouts/layout.go @@ -11,6 +11,8 @@ type LayoutEntry struct { Expand bool } +// TODO: have layouts take in artist.Inset for margin and padding + // 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,17 +20,28 @@ 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, bounds image.Rectangle) + Arrange ( + entries []LayoutEntry, + margin, padding int, + 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) (width, height int) + MinimumSize ( + entries []LayoutEntry, + margin, padding int, + ) ( + 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, + padding int, squeeze int, ) ( height int, From 38baa97e76d3e785870b23efe6481cd3151739bf Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 2 Mar 2023 18:59:08 -0500 Subject: [PATCH 03/20] ContainerCore and ContainerCoreControl WIP --- elements/core/container.go | 55 ++++++++++++++++++++++++++++++++++++++ layouts/layout.go | 2 ++ 2 files changed, 57 insertions(+) create mode 100644 elements/core/container.go diff --git a/elements/core/container.go b/elements/core/container.go new file mode 100644 index 0000000..7bdc08f --- /dev/null +++ b/elements/core/container.go @@ -0,0 +1,55 @@ +package core + +import "image" +import "git.tebibyte.media/sashakoshka/tomo/layouts" +import "git.tebibyte.media/sashakoshka/tomo/elements" + +// ContainerCore is a struct that can be embedded into an object to allow it to +// have one or more children. It also implements Flexible and Focusable and +// provides the standard behavior for selecting multiple children, and +// propagating user input events to them. +type ContainerCore struct { + bounds image.Rectangle + layout layouts.Layout + children []layouts.LayoutEntry + drags [10]elements.MouseTarget + warping bool + focused bool + focusable bool + flexible bool +} + +func NewContainerCore ( + layout layouts.Layout, + onFocusChange func (), + onLayoutChange func (), +) ( + core *ContainerCore, + control ContainerCoreControl, +) { + core = &ContainerCore { + layout: layout, + } + control = ContainerCoreControl { + core: core, + } + return +} + +// TODO fulfill interfaces here. accessors and mutators need to be in the +// container core control, because elements will have different ways of adopting +// and disowning child elements. + +type ContainerCoreControl struct { + core *ContainerCore +} + +// Resize sets the size of the control, and +func (control ContainerCoreControl) Resize (bounds image.Rectangle) { + // TODO do a layout + // TODO call onLayoutChange +} + +func (control ContainerCoreControl) Adopt (element elements.Element, expand bool) { + +} diff --git a/layouts/layout.go b/layouts/layout.go index e847e27..5a5d511 100644 --- a/layouts/layout.go +++ b/layouts/layout.go @@ -12,6 +12,8 @@ type LayoutEntry struct { } // 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. From 538123dcd53330d6cc60e21fdb89f4ea1e5438e9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Mar 2023 20:16:36 -0500 Subject: [PATCH 04/20] No that was a bad idea time to do something else --- elements/core/container.go | 55 -------------------------------------- 1 file changed, 55 deletions(-) delete mode 100644 elements/core/container.go diff --git a/elements/core/container.go b/elements/core/container.go deleted file mode 100644 index 7bdc08f..0000000 --- a/elements/core/container.go +++ /dev/null @@ -1,55 +0,0 @@ -package core - -import "image" -import "git.tebibyte.media/sashakoshka/tomo/layouts" -import "git.tebibyte.media/sashakoshka/tomo/elements" - -// ContainerCore is a struct that can be embedded into an object to allow it to -// have one or more children. It also implements Flexible and Focusable and -// provides the standard behavior for selecting multiple children, and -// propagating user input events to them. -type ContainerCore struct { - bounds image.Rectangle - layout layouts.Layout - children []layouts.LayoutEntry - drags [10]elements.MouseTarget - warping bool - focused bool - focusable bool - flexible bool -} - -func NewContainerCore ( - layout layouts.Layout, - onFocusChange func (), - onLayoutChange func (), -) ( - core *ContainerCore, - control ContainerCoreControl, -) { - core = &ContainerCore { - layout: layout, - } - control = ContainerCoreControl { - core: core, - } - return -} - -// TODO fulfill interfaces here. accessors and mutators need to be in the -// container core control, because elements will have different ways of adopting -// and disowning child elements. - -type ContainerCoreControl struct { - core *ContainerCore -} - -// Resize sets the size of the control, and -func (control ContainerCoreControl) Resize (bounds image.Rectangle) { - // TODO do a layout - // TODO call onLayoutChange -} - -func (control ContainerCoreControl) Adopt (element elements.Element, expand bool) { - -} From 6bb5b2d79cd18133353ae5b50b5928a26b8e128f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Mar 2023 20:31:30 -0500 Subject: [PATCH 05/20] Created the stub for Propagator Unlike the previous poorly-defined ContainerCore idea, this struct has one sole responsibility and that is propagating events to children. There may be another struct called like ChildManager or something in the future that also abstracts away logic for adoption, canvas cutting, disowning, layout, etc. --- elements/core/propagator.go | 100 ++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 elements/core/propagator.go diff --git a/elements/core/propagator.go b/elements/core/propagator.go new file mode 100644 index 0000000..84296eb --- /dev/null +++ b/elements/core/propagator.go @@ -0,0 +1,100 @@ +package core + +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" + +// ChildIterator represents an object that can iterate over a list of children, +// calling a specified iterator function for each one. When keepGoing is false, +// the iterator stops the current loop and OverChildren returns. +type ChildIterator interface { + OverChildren (func (child elements.Element) (keepGoing bool)) +} + +// 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 { + iterator ChildIterator + drags [10]elements.MouseTarget +} + +// NewPropagator creates a new event propagator that uses the specified iterator +// to access a list of child elements that will have events propagated to them. +func NewPropagator (iterator ChildIterator) (propagator *Propagator) { + propagator = &Propagator { + iterator: iterator, + } + return +} + +// Focused returns whether or not this element or any of its children +// are currently focused. +func (propagator *Propagator) Focused () (focused bool) + +// Focus focuses this element, if its parent element grants the +// request. +func (propagator *Propagator) Focus () + +// 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. +func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) + +// HandleDeselection causes this element to mark itself and all of its +// children as unfocused. +func (propagator *Propagator) HandleUnfocus () + +// 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 must act as if a HandleFocus call +// was made with KeynavDirectionNeutral. +func (propagator *Propagator) OnFocusRequest (func () (granted bool)) + +// 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 (func (direction input.KeynavDirection) (granted bool)) + +// HandleKeyDown is called when a key is pressed down or repeated while +// this element has keyboard focus. It is important to note that not +// every key down event is guaranteed to be paired with exactly one key +// up event. This is the reason a list of modifier keys held down at the +// time of the key press is given. +func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) + +// HandleKeyUp is called when a key is released while this element has +// keyboard focus. +func (propagator *Propagator) HandleKeyUp (key input.Key, modifiers input.Modifiers) + +// HandleMouseDown is called when a mouse button is pressed down on this +// element. +func (propagator *Propagator) HandleMouseDown (x, y int, button input.Button) + +// HandleMouseUp is called when a mouse button is released that was +// originally pressed down on this element. +func (propagator *Propagator) HandleMouseUp (x, y int, button input.Button) + +// HandleMouseMove is called when the mouse is moved over this element, +// or the mouse is moving while being held down and originally pressed +// down on this element. +func (propagator *Propagator) HandleMouseMove (x, y int) + +// HandleScroll is called when the mouse is scrolled. The X and Y +// direction of the scroll event are passed as deltaX and deltaY. +func (propagator *Propagator) HandleMouseScroll (x, y int, deltaX, deltaY float64) + +// SetTheme sets the element's theme to something fulfilling the +// theme.Theme interface. +func (propagator *Propagator) SetTheme (theme.Theme) + +// SetConfig sets the element's configuration to something fulfilling +// the config.Config interface. +func (propagator *Propagator) SetConfig (config.Config) From b6eb158964b684d4b129978b3b410d03b7920707 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 3 Mar 2023 23:48:10 -0500 Subject: [PATCH 06/20] Tidied up documentation on Propagator --- elements/core/propagator.go | 121 +++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 84296eb..daefd7b 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -23,7 +23,9 @@ type Propagator struct { // NewPropagator creates a new event propagator that uses the specified iterator // to access a list of child elements that will have events propagated to them. +// If iterator is nil, the function will return nil. func NewPropagator (iterator ChildIterator) (propagator *Propagator) { + if iterator == nil { return nil } propagator = &Propagator { iterator: iterator, } @@ -32,69 +34,88 @@ func NewPropagator (iterator ChildIterator) (propagator *Propagator) { // Focused returns whether or not this element or any of its children // are currently focused. -func (propagator *Propagator) Focused () (focused bool) +func (propagator *Propagator) Focused () (focused bool) { + +} // Focus focuses this element, if its parent element grants the // request. -func (propagator *Propagator) Focus () +func (propagator *Propagator) Focus () { + +} // 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. -func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) +// 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) { + +} -// HandleDeselection causes this element to mark itself and all of its -// children as unfocused. -func (propagator *Propagator) HandleUnfocus () +// HandleDeselection causes this element to mark itself and all of its children +// as unfocused. +func (propagator *Propagator) HandleUnfocus () { + +} -// 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 must act as if a HandleFocus call -// was made with KeynavDirectionNeutral. -func (propagator *Propagator) OnFocusRequest (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. If the parent element returns true, the +// element acts as if a HandleFocus call was made with KeynavDirectionNeutral. +func (propagator *Propagator) OnFocusRequest (func () (granted bool)) { + +} -// 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 (func (direction input.KeynavDirection) (granted bool)) +// 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 (func (direction input.KeynavDirection) (granted bool)) { + +} -// HandleKeyDown is called when a key is pressed down or repeated while -// this element has keyboard focus. It is important to note that not -// every key down event is guaranteed to be paired with exactly one key -// up event. This is the reason a list of modifier keys held down at the -// time of the key press is given. -func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) +// HandleKeyDown propogates the keyboard event to the currently selected child. +func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + +} -// HandleKeyUp is called when a key is released while this element has -// keyboard focus. -func (propagator *Propagator) HandleKeyUp (key input.Key, modifiers input.Modifiers) +// HandleKeyUp propogates the keyboard event to the currently selected child. +func (propagator *Propagator) HandleKeyUp (key input.Key, modifiers input.Modifiers) { + +} -// HandleMouseDown is called when a mouse button is pressed down on this -// element. -func (propagator *Propagator) HandleMouseDown (x, y int, button input.Button) +// HandleMouseDown propagates the mouse event to the element under the mouse +// pointer. +func (propagator *Propagator) HandleMouseDown (x, y int, button input.Button) { + +} -// HandleMouseUp is called when a mouse button is released that was -// originally pressed down on this element. -func (propagator *Propagator) HandleMouseUp (x, y int, button input.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) { + +} -// HandleMouseMove is called when the mouse is moved over this element, -// or the mouse is moving while being held down and originally pressed -// down on this element. -func (propagator *Propagator) HandleMouseMove (x, y int) +// 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) { + +} -// HandleScroll is called when the mouse is scrolled. The X and Y -// direction of the scroll event are passed as deltaX and deltaY. -func (propagator *Propagator) HandleMouseScroll (x, y int, deltaX, deltaY float64) +// HandleScroll propagates the mouse event to the element under the mouse +// pointer. +func (propagator *Propagator) HandleMouseScroll (x, y int, deltaX, deltaY float64) { + +} -// SetTheme sets the element's theme to something fulfilling the -// theme.Theme interface. -func (propagator *Propagator) SetTheme (theme.Theme) +// SetTheme sets the theme of all children to the specified theme. +func (propagator *Propagator) SetTheme (theme.Theme) { + +} -// SetConfig sets the element's configuration to something fulfilling -// the config.Config interface. -func (propagator *Propagator) SetConfig (config.Config) +// SetConfig sets the theme of all children to the specified config. +func (propagator *Propagator) SetConfig (config.Config) { + +} From 5af8d7fd97824ceb448b05cdf3b743bb4f445f9e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 00:18:27 -0500 Subject: [PATCH 07/20] Implemented keyboard, mouse, theme, and config event propagation --- elements/core/propagator.go | 155 ++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 16 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index daefd7b..4c6b2ad 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -1,5 +1,6 @@ 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" @@ -19,6 +20,7 @@ type ChildIterator interface { type Propagator struct { iterator ChildIterator drags [10]elements.MouseTarget + focused bool } // NewPropagator creates a new event propagator that uses the specified iterator @@ -32,16 +34,18 @@ func NewPropagator (iterator ChildIterator) (propagator *Propagator) { 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 () { - + // TODO } // HandleFocus causes this element to mark itself as focused. If the @@ -50,13 +54,13 @@ func (propagator *Propagator) Focus () { // marks itself as focused along with any applicable children and returns // true. func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) { - + // TODO } // HandleDeselection causes this element to mark itself and all of its children // as unfocused. func (propagator *Propagator) HandleUnfocus () { - + // TODO } // OnFocusRequest sets a function to be called when this element wants its @@ -64,7 +68,7 @@ func (propagator *Propagator) HandleUnfocus () { // 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 (func () (granted bool)) { - + // TODO } // OnFocusMotionRequest sets a function to be called when this element wants its @@ -72,50 +76,169 @@ func (propagator *Propagator) OnFocusRequest (func () (granted bool)) { // the specified direction. Parent elements should return true if the request // was granted, and false if it was not. func (propagator *Propagator) OnFocusMotionRequest (func (direction input.KeynavDirection) (granted bool)) { - + // TODO } // 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) { - +func (propagator *Propagator) SetTheme (theme theme.Theme) { + propagator.iterator.OverChildren (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) { - +func (propagator *Propagator) SetConfig (config config.Config) { + propagator.iterator.OverChildren (func (child elements.Element) bool { + typedChild, configurable := child.(elements.Configurable) + if configurable { + typedChild.SetConfig(config) + } + return true + }) +} + +// ----------- Iterator utilities ----------- // + +func (propagator *Propagator) childAt (position image.Point) (child elements.Element) { + propagator.iterator.OverChildren (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.iterator.OverChildren (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.iterator.OverChildren (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.iterator.OverChildren (func (child elements.Element) bool { + typedChild, flexible := child.(elements.Flexible) + if flexible { + if !callback(typedChild) { return false } + } + return true + }) +} + +// func (propagator *Propagator) 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 (propagator *Propagator) firstFocused () (index int) { + index = -1 + currentIndex := 0 + propagator.forFocusable (func (child elements.Focusable) bool { + if child.Focused() { + index = currentIndex + return false + } + currentIndex ++ + return true + }) + return } From c13cdd570d21a047e3945d18734b8dd6755c3fdb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 00:38:37 -0500 Subject: [PATCH 08/20] Implemented all focus methods except for HandleFocus I am dreading this --- elements/core/propagator.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 4c6b2ad..7f7c2c8 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -21,6 +21,9 @@ type Propagator struct { iterator ChildIterator drags [10]elements.MouseTarget focused bool + + onFocusRequest func () (granted bool) + onFocusMotionRequest func (input.KeynavDirection) (granted bool) } // NewPropagator creates a new event propagator that uses the specified iterator @@ -45,7 +48,9 @@ func (propagator *Propagator) Focused () (focused bool) { // Focus focuses this element, if its parent element grants the // request. func (propagator *Propagator) Focus () { - // TODO + if propagator.onFocusRequest != nil { + propagator.onFocusRequest() + } } // HandleFocus causes this element to mark itself as focused. If the @@ -60,23 +65,29 @@ func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (acc // HandleDeselection causes this element to mark itself and all of its children // as unfocused. func (propagator *Propagator) HandleUnfocus () { - // TODO + 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 (func () (granted bool)) { - // TODO +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 (func (direction input.KeynavDirection) (granted bool)) { - // TODO +func (propagator *Propagator) OnFocusMotionRequest ( + callback func (direction input.KeynavDirection) (granted bool), +) { + propagator.onFocusMotionRequest = callback } // HandleKeyDown propogates the keyboard event to the currently selected child. From 1d9fb6024ddca938f0e80fb2ee1e7669089d2a1c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 00:57:17 -0500 Subject: [PATCH 09/20] Fully implemented Propagator --- elements/core/propagator.go | 111 ++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 11 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 7f7c2c8..10b2566 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -10,7 +10,9 @@ import "git.tebibyte.media/sashakoshka/tomo/elements" // calling a specified iterator function for each one. When keepGoing is false, // the iterator stops the current loop and OverChildren returns. type ChildIterator interface { - OverChildren (func (child elements.Element) (keepGoing bool)) + OverChildren (func (child elements.Element) (keepGoing bool)) + Child (index int) elements.Element + CountChildren () int } // Propagator is a struct that can be embedded into elements that contain one or @@ -59,7 +61,58 @@ func (propagator *Propagator) Focus () { // marks itself as focused along with any applicable children and returns // true. func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) { - // TODO + 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.KeynavDirectionNeutral, input.KeynavDirectionForward: + // if we recieve a neutral or 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) + } + } else { + // an element is currently focused, so we need to move the + // focus in the specified direction + firstFocusedChild := + propagator.iterator.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.iterator.CountChildren() && index >= 0; + index += int(direction) { + + child, focusable := + propagator.iterator.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 @@ -189,8 +242,53 @@ func (propagator *Propagator) SetConfig (config config.Config) { }) } +// ----------- 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, +) { + focusables := []elements.Focusable { } + propagator.forFocusable (func (child elements.Focusable) bool { + focusables = append(focusables, child) + return true + }) + + for index := len(focusables) - 1; index >= 0; index -- { + child, focusable := focusables[index].(elements.Focusable) + if focusable && child.HandleFocus(direction) { + propagator.focused = true + ok = true + break + } + } + return +} + + // ----------- Iterator utilities ----------- // +// TODO: remove ChildIterator.OverChildren, reimplement that here, rework these +// methods based on that, add a reverse iteration method, and then rework +// focusLastFocusableElement based on that. + func (propagator *Propagator) childAt (position image.Point) (child elements.Element) { propagator.iterator.OverChildren (func (current elements.Element) bool { if position.In(current.Bounds()) { @@ -231,15 +329,6 @@ func (propagator *Propagator) forFlexible (callback func (child elements.Flexibl }) } -// func (propagator *Propagator) 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 (propagator *Propagator) firstFocused () (index int) { index = -1 currentIndex := 0 From 56e11ae1de72970d87a819ecbdefdaffe53f5499 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 01:05:37 -0500 Subject: [PATCH 10/20] Cleaned up the (ChildIterator -> Parent) interface --- elements/core/propagator.go | 74 +++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 10b2566..d9b9081 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -6,11 +6,9 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/elements" -// ChildIterator represents an object that can iterate over a list of children, -// calling a specified iterator function for each one. When keepGoing is false, -// the iterator stops the current loop and OverChildren returns. -type ChildIterator interface { - OverChildren (func (child elements.Element) (keepGoing bool)) +// Parent represents an object that can provide access to a list of child +// elements. +type Parent interface { Child (index int) elements.Element CountChildren () int } @@ -20,7 +18,7 @@ type ChildIterator interface { // all of the event handlers. It also implements standard behavior for focus // propagation and keyboard navigation. type Propagator struct { - iterator ChildIterator + parent Parent drags [10]elements.MouseTarget focused bool @@ -28,13 +26,13 @@ type Propagator struct { onFocusMotionRequest func (input.KeynavDirection) (granted bool) } -// NewPropagator creates a new event propagator that uses the specified iterator +// 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 iterator is nil, the function will return nil. -func NewPropagator (iterator ChildIterator) (propagator *Propagator) { - if iterator == nil { return nil } +// If parent is nil, the function will return nil. +func NewPropagator (parent Parent) (propagator *Propagator) { + if parent == nil { return nil } propagator = &Propagator { - iterator: iterator, + parent: parent, } return } @@ -83,7 +81,7 @@ func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (acc // an element is currently focused, so we need to move the // focus in the specified direction firstFocusedChild := - propagator.iterator.Child(firstFocused). + propagator.parent.Child(firstFocused). (elements.Focusable) // before we move the focus, the currently focused child @@ -96,11 +94,11 @@ func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (acc // find the previous/next focusable element relative to the // currently focused element, if it exists. for index := firstFocused + int(direction); - index < propagator.iterator.CountChildren() && index >= 0; + index < propagator.parent.CountChildren() && index >= 0; index += int(direction) { child, focusable := - propagator.iterator.Child(index). + propagator.parent.Child(index). (elements.Focusable) if focusable && child.HandleFocus(direction) { // we have found one, so we now actually move @@ -222,7 +220,7 @@ func (propagator *Propagator) HandleMouseScroll (x, y int, deltaX, deltaY float6 // SetTheme sets the theme of all children to the specified theme. func (propagator *Propagator) SetTheme (theme theme.Theme) { - propagator.iterator.OverChildren (func (child elements.Element) bool { + propagator.forChildren (func (child elements.Element) bool { typedChild, themeable := child.(elements.Themeable) if themeable { typedChild.SetTheme(theme) @@ -233,7 +231,7 @@ func (propagator *Propagator) SetTheme (theme theme.Theme) { // SetConfig sets the theme of all children to the specified config. func (propagator *Propagator) SetConfig (config config.Config) { - propagator.iterator.OverChildren (func (child elements.Element) bool { + propagator.forChildren (func (child elements.Element) bool { typedChild, configurable := child.(elements.Configurable) if configurable { typedChild.SetConfig(config) @@ -265,32 +263,38 @@ func (propagator *Propagator) focusLastFocusableElement ( ) ( ok bool, ) { - focusables := []elements.Focusable { } - propagator.forFocusable (func (child elements.Focusable) bool { - focusables = append(focusables, child) - return true - }) - - for index := len(focusables) - 1; index >= 0; index -- { - child, focusable := focusables[index].(elements.Focusable) - if focusable && child.HandleFocus(direction) { + propagator.forChildrenReverse (func (child elements.Element) bool { + typedChild, focusable := child.(elements.Focusable) + if focusable && typedChild.HandleFocus(direction) { propagator.focused = true ok = true - break + return false } - } + return true + }) return } - // ----------- Iterator utilities ----------- // -// TODO: remove ChildIterator.OverChildren, reimplement that here, rework these -// methods based on that, add a reverse iteration method, and then rework -// focusLastFocusableElement based on that. +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.iterator.OverChildren (func (current elements.Element) bool { + propagator.forChildren (func (current elements.Element) bool { if position.In(current.Bounds()) { child = current } @@ -300,7 +304,7 @@ func (propagator *Propagator) childAt (position image.Point) (child elements.Ele } func (propagator *Propagator) forFocused (callback func (child elements.Focusable) bool) { - propagator.iterator.OverChildren (func (child elements.Element) bool { + propagator.forChildren (func (child elements.Element) bool { typedChild, focusable := child.(elements.Focusable) if focusable && typedChild.Focused() { if !callback(typedChild) { return false } @@ -310,7 +314,7 @@ func (propagator *Propagator) forFocused (callback func (child elements.Focusabl } func (propagator *Propagator) forFocusable (callback func (child elements.Focusable) bool) { - propagator.iterator.OverChildren (func (child elements.Element) bool { + propagator.forChildren (func (child elements.Element) bool { typedChild, focusable := child.(elements.Focusable) if focusable { if !callback(typedChild) { return false } @@ -320,7 +324,7 @@ func (propagator *Propagator) forFocusable (callback func (child elements.Focusa } func (propagator *Propagator) forFlexible (callback func (child elements.Flexible) bool) { - propagator.iterator.OverChildren (func (child elements.Element) bool { + propagator.forChildren (func (child elements.Element) bool { typedChild, flexible := child.(elements.Flexible) if flexible { if !callback(typedChild) { return false } From 165d0835bfbf51d6c9377ddf68755a8c297ce4c0 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 01:20:23 -0500 Subject: [PATCH 11/20] Worked Propagator into basic.Container --- elements/basic/container.go | 196 ++++-------------------------------- elements/core/propagator.go | 5 +- 2 files changed, 22 insertions(+), 179 deletions(-) diff --git a/elements/basic/container.go b/elements/basic/container.go index 0347def..37601a4 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -14,11 +14,11 @@ 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 @@ -27,9 +27,9 @@ type Container struct { 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. @@ -241,11 +241,7 @@ func (element *Container) redoAll () { 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,188 +249,34 @@ 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) + // TODO: have layouts take in entire margins/padding return element.layout.FlexibleHeightFor ( element.children, - margin.X, width) + margin.X, padding.Horizontal(), 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 + element.Propagator.OnFocusMotionRequest(callback) } func (element *Container) forFocused (callback func (child elements.Focusable) bool) { @@ -517,20 +359,24 @@ 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) + // TODO: have layouts take in entire margins/padding + width, height := element.layout.MinimumSize ( + element.children, margin.X, padding.Horizontal()) if element.flexible { height = element.layout.FlexibleHeightFor ( - element.children, - margin.X, width) + element.children, margin.X, + padding.Horizontal(), 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) + // TODO: have layouts take in entire margins/padding element.layout.Arrange ( - element.children, margin.X, element.Bounds()) + element.children, margin.X, + padding.Horizontal(), element.Bounds()) } diff --git a/elements/core/propagator.go b/elements/core/propagator.go index d9b9081..d82db32 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -23,7 +23,6 @@ type Propagator struct { focused bool onFocusRequest func () (granted bool) - onFocusMotionRequest func (input.KeynavDirection) (granted bool) } // NewPropagator creates a new event propagator that uses the specified parent @@ -137,9 +136,7 @@ func (propagator *Propagator) OnFocusRequest (callback func () (granted bool)) { // was granted, and false if it was not. func (propagator *Propagator) OnFocusMotionRequest ( callback func (direction input.KeynavDirection) (granted bool), -) { - propagator.onFocusMotionRequest = callback -} +) { } // HandleKeyDown propogates the keyboard event to the currently selected child. func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) { From 252433f13d7cf38eb314e79fec00018007336395 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 01:26:23 -0500 Subject: [PATCH 12/20] Cleaned up Container somewhat --- elements/basic/container.go | 75 +++++++++---------------------------- 1 file changed, 18 insertions(+), 57 deletions(-) diff --git a/elements/basic/container.go b/elements/basic/container.go index 37601a4..eeeebfd 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -203,17 +203,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,7 +225,6 @@ func (element *Container) redoAll () { } } - // SetTheme sets the element's theme. func (element *Container) SetTheme (new theme.Theme) { if new == element.theme.Theme { return } @@ -279,24 +267,6 @@ func (element *Container) OnFocusMotionRequest ( 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) { for _, entry := range element.children { child, flexible := entry.Element.(elements.Flexible) @@ -306,31 +276,16 @@ 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 - }) + for _, entry := range element.children { + _, focusable := entry.Element.(elements.Focusable) + if focusable { + element.focusable = true + break + } + } + element.flexible = false element.forFlexible (func (elements.Flexible) bool { element.flexible = true @@ -348,16 +303,22 @@ func (element *Container) childFocusRequestCallback ( ) { if element.onFocusRequest != nil && element.onFocusRequest() { element.focused = true - element.forFocused (func (child elements.Focusable) bool { - child.HandleUnfocus() - return true - }) + element.unfocusAllChildren() return true } else { return false } } +func (element *Container) unfocusAllChildren() { + for _, entry := range element.children { + child, focusable := entry.Element.(elements.Focusable) + if focusable && child.Focused() { + child.HandleUnfocus() + } + } +} + func (element *Container) updateMinimumSize () { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) From be286fa86c56ec0b8288941ad96a5f055d8e6a3d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 01:27:16 -0500 Subject: [PATCH 13/20] The container actually creates a propagator now lmao --- elements/basic/container.go | 1 + 1 file changed, 1 insertion(+) diff --git a/elements/basic/container.go b/elements/basic/container.go index eeeebfd..e2cbe26 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -37,6 +37,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 } From 90ce0d7281f92c98fb58ab8e64dd3fcb8d535f25 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 01:29:45 -0500 Subject: [PATCH 14/20] Fixed Propagator.forChildren --- elements/core/propagator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index d82db32..b0b5898 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -277,8 +277,8 @@ func (propagator *Propagator) focusLastFocusableElement ( 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 } + if child == nil { continue } + if !callback(child) { break } } } From 5fc5af92df645959ddb3077b3342b38591ef487d Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 01:42:14 -0500 Subject: [PATCH 15/20] Layouts now take in proper margin and padding values --- elements/basic/container.go | 15 ++++++-------- layouts/basic/dialog.go | 39 +++++++++++++++++++------------------ layouts/basic/horizontal.go | 38 +++++++++++++++++++----------------- layouts/basic/vertical.go | 31 +++++++++++++++-------------- layouts/layout.go | 11 +++++++---- 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/elements/basic/container.go b/elements/basic/container.go index e2cbe26..22aa8a5 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -246,10 +246,9 @@ func (element *Container) SetConfig (new config.Config) { func (element *Container) FlexibleHeightFor (width int) (height int) { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) - // TODO: have layouts take in entire margins/padding return element.layout.FlexibleHeightFor ( element.children, - margin.X, padding.Horizontal(), width) + margin, padding, width) } func (element *Container) OnFlexibleHeightChange (callback func ()) { @@ -323,13 +322,12 @@ func (element *Container) unfocusAllChildren() { func (element *Container) updateMinimumSize () { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) - // TODO: have layouts take in entire margins/padding width, height := element.layout.MinimumSize ( - element.children, margin.X, padding.Horizontal()) + element.children, margin, padding) if element.flexible { height = element.layout.FlexibleHeightFor ( - element.children, margin.X, - padding.Horizontal(), width) + element.children, margin, + padding, width) } element.core.SetMinimumSize(width, height) } @@ -337,8 +335,7 @@ func (element *Container) updateMinimumSize () { func (element *Container) doLayout () { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) - // TODO: have layouts take in entire margins/padding element.layout.Arrange ( - element.children, margin.X, - padding.Horizontal(), element.Bounds()) + element.children, margin, + padding, element.Bounds()) } diff --git a/layouts/basic/dialog.go b/layouts/basic/dialog.go index f7fdc4f..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" @@ -21,11 +22,11 @@ type Dialog struct { // Arrange arranges a list of entries into a dialog. func (layout Dialog) Arrange ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(padding) } + if layout.Pad { bounds = padding.Apply(bounds) } controlRowWidth, controlRowHeight := 0, 0 if len(entries) > 1 { @@ -39,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 @@ -59,7 +60,7 @@ func (layout Dialog) Arrange ( freeSpace -= entryMinWidth } if index > 0 && layout.Gap { - freeSpace -= margin + freeSpace -= margin.X } } expandingElementWidth := 0 @@ -75,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 @@ -102,8 +103,8 @@ func (layout Dialog) Arrange ( // arrange the given list of entries. func (layout Dialog) MinimumSize ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) { @@ -114,7 +115,7 @@ 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, padding) @@ -125,8 +126,8 @@ func (layout Dialog) MinimumSize ( } if layout.Pad { - width += padding * 2 - height += padding * 2 + width += padding.Horizontal() + height += padding.Vertical() } return } @@ -135,14 +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, - padding int, + margin image.Point, + padding artist.Inset, width int, ) ( height int, ) { if layout.Pad { - width -= margin * 2 + width -= padding.Horizontal() } if len(entries) > 0 { @@ -156,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, padding) height += additionalHeight } if layout.Pad { - height += padding * 2 + height += padding.Vertical() } return } @@ -172,8 +173,8 @@ func (layout Dialog) FlexibleHeightFor ( // the control row. func (layout Dialog) minimumSizeOfControlRow ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) { @@ -184,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 d37e27d..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,11 +20,11 @@ type Horizontal struct { // Arrange arranges a list of entries horizontally. func (layout Horizontal) Arrange ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(padding) } + if layout.Pad { bounds = padding.Apply(bounds) } // get width of expanding elements expandingElementWidth := layout.expandingElementWidth ( @@ -32,7 +33,7 @@ func (layout Horizontal) Arrange ( // 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 @@ -52,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, ) { @@ -63,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 } @@ -78,13 +80,13 @@ func (layout Horizontal) MinimumSize ( // specified elements at the given width, taking into account flexible elements. func (layout Horizontal) FlexibleHeightFor ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, width int, ) ( height int, ) { - if layout.Pad { width -= padding * 2 } + if layout.Pad { width -= padding.Horizontal() } // get width of expanding elements expandingElementWidth := layout.expandingElementWidth ( @@ -92,8 +94,8 @@ func (layout Horizontal) FlexibleHeightFor ( x, y := 0, 0 if layout.Pad { - x += padding - y += padding + x += padding.Horizontal() + y += padding.Vertical() } // set the size and position of each element @@ -108,19 +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 += padding * 2 + height += padding.Vertical() } return } func (layout Horizontal) expandingElementWidth ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, freeSpace int, ) ( width int, @@ -137,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 dc43da8..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,11 +20,11 @@ type Vertical struct { // Arrange arranges a list of entries vertically. func (layout Vertical) Arrange ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, bounds image.Rectangle, ) { - if layout.Pad { bounds = bounds.Inset(padding) } + 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. @@ -46,7 +47,7 @@ func (layout Vertical) Arrange ( freeSpace -= entryMinHeight } if index > 0 && layout.Gap { - freeSpace -= margin + freeSpace -= margin.Y } } @@ -58,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 @@ -78,8 +79,8 @@ func (layout Vertical) Arrange ( // arrange the given list of entries. func (layout Vertical) MinimumSize ( entries []layouts.LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) { @@ -90,13 +91,13 @@ func (layout Vertical) MinimumSize ( } height += entryHeight if layout.Gap && index > 0 { - height += margin + height += margin.Y } } if layout.Pad { - width += padding * 2 - height += padding * 2 + width += padding.Horizontal() + height += padding.Vertical() } return } @@ -105,15 +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, - padding int, + margin image.Point, + padding artist.Inset, width int, ) ( height int, ) { if layout.Pad { - width -= padding * 2 - height += padding * 2 + width -= padding.Horizontal() + height += padding.Vertical() } for index, entry := range entries { @@ -126,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 5a5d511..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 @@ -24,7 +25,8 @@ type Layout interface { // than what is returned by MinimumSize. Arrange ( entries []LayoutEntry, - margin, padding int, + margin image.Point, + padding artist.Inset, bounds image.Rectangle, ) @@ -32,7 +34,8 @@ type Layout interface { // needs to properly arrange the given slice of layout entries. MinimumSize ( entries []LayoutEntry, - margin, padding int, + margin image.Point, + padding artist.Inset, ) ( width, height int, ) @@ -42,8 +45,8 @@ type Layout interface { // flexible elements. FlexibleHeightFor ( entries []LayoutEntry, - margin int, - padding int, + margin image.Point, + padding artist.Inset, squeeze int, ) ( height int, From dc5ddfc0bdfb252010d7e7cbea8e926aaf91e843 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 01:48:16 -0500 Subject: [PATCH 16/20] Propagator no longer segfaults when handling keynav --- elements/core/propagator.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index b0b5898..c5a3f0b 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -330,16 +330,12 @@ func (propagator *Propagator) forFlexible (callback func (child elements.Flexibl }) } -func (propagator *Propagator) firstFocused () (index int) { - index = -1 - currentIndex := 0 - propagator.forFocusable (func (child elements.Focusable) bool { - if child.Focused() { - index = currentIndex - return false +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 } - currentIndex ++ - return true - }) - return + } + return -1 } From 4f6f4e1f1abc17343d284ea51192143b50449089 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 02:04:47 -0500 Subject: [PATCH 17/20] Me when I make the exact mistake twice --- elements/core/propagator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index c5a3f0b..b4bcd15 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -285,8 +285,8 @@ func (propagator *Propagator) forChildren (callback func (child elements.Element 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 } + if child == nil { continue } + if !callback(child) { break } } } From 9c12cd7e184e386ed7fded2ac4566e7b3a9d4761 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 02:20:48 -0500 Subject: [PATCH 18/20] Fixed cringe bug with focus requests being improperly handled --- elements/basic/container.go | 25 +++++++------------------ elements/core/propagator.go | 12 +++++++++--- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/elements/basic/container.go b/elements/basic/container.go index 22aa8a5..c59d713 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -20,8 +20,6 @@ type Container struct { layout layouts.Layout children []layouts.LayoutEntry warping bool - focused bool - focusable bool flexible bool config config.Wrapped @@ -277,23 +275,23 @@ func (element *Container) forFlexible (callback func (child elements.Flexible) b } func (element *Container) reflectChildProperties () { - element.focusable = false + focusable := false for _, entry := range element.children { _, focusable := entry.Element.(elements.Focusable) if focusable { - element.focusable = true + 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 ( @@ -302,23 +300,14 @@ func (element *Container) childFocusRequestCallback ( granted bool, ) { if element.onFocusRequest != nil && element.onFocusRequest() { - element.focused = true - element.unfocusAllChildren() + element.Propagator.HandleUnfocus() + element.Propagator.HandleFocus(input.KeynavDirectionNeutral) return true } else { return false } } -func (element *Container) unfocusAllChildren() { - for _, entry := range element.children { - child, focusable := entry.Element.(elements.Focusable) - if focusable && child.Focused() { - child.HandleUnfocus() - } - } -} - func (element *Container) updateMinimumSize () { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index b4bcd15..291d406 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -66,15 +66,21 @@ func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (acc // 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. + 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 From 531b0ffce95db58250eb40396d4a0ff7528f626f Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 10:44:45 -0500 Subject: [PATCH 19/20] Fixed Container not clearing child event handlers in DisownAll --- elements/basic/container.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/elements/basic/container.go b/elements/basic/container.go index c59d713..008a1d8 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -160,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() From 912a3f9f66cbe2a44ffd8f23f9a7332243f37a52 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Sat, 4 Mar 2023 16:18:43 -0500 Subject: [PATCH 20/20] oops lmao --- elements/basic/button.go | 78 +++++++++++++++------------------------- theme/default.go | 8 +++++ theme/theme.go | 5 +++ theme/wrapped.go | 7 ++++ 4 files changed, 49 insertions(+), 49 deletions(-) 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/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()