From b08cbea32054face18c57ace83e52a6b1edce7d7 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 14 Mar 2023 17:08:39 -0400 Subject: [PATCH 01/17] Overhauled the element interfaces Instead of the previous parenting model where parents would set child callbacks during adoption by probing for callback setters, child elements will instead probe their parents for notify methods listed in the standard parent interfaces. This means that an element cannot be half-parented to something, nor can it be parented to two things at once. Parent elements may themselves fulfill these interfaces, or they can pass a hook that fulfills them to the child. --- elements/container.go | 47 ++++++++++++++++++++++++++ elements/element.go | 78 +++++++++++++++++-------------------------- elements/window.go | 11 +++--- 3 files changed, 84 insertions(+), 52 deletions(-) create mode 100644 elements/container.go diff --git a/elements/container.go b/elements/container.go new file mode 100644 index 0000000..036c87b --- /dev/null +++ b/elements/container.go @@ -0,0 +1,47 @@ +package element + +import "git.tebibyte.media/sashakoshka/tomo/input" + +// Parent represents a type capable of containing child elements. +type Parent interface { + // NotifyMinimumSizeChange notifies the container that a child element's + // minimum size has changed. This method is expected to be called by + // child elements when their minimum size changes. + NotifyMinimumSizeChange (child Element) +} + +// FocusableParent represents a parent with keyboard navigation support. +type FocusableParent interface { + Parent + + // RequestFocus notifies the parent that a child element is requesting + // keyboard focus. If the parent grants the request, the method will + // return true and the child element should behave as if a HandleFocus + // call was made. + RequestFocus (child Focusable, direction input.KeynavDirection) (granted bool) +} + +// FlexibleParent represents a parent that accounts for elements with +// flexible height. +type FlexibleParent interface { + Parent + + // NotifyFlexibleHeightChange notifies the parent that the parameters + // affecting a child's flexible height have changed. This method is + // expected to be called by flexible child element when their content + // changes. + NotifyFlexibleHeightChange (child Flexible) +} + +// ScrollableParent represents a parent that can change the scroll +// position of its child element(s). +type ScrollableParent interface { + Parent + + // NotifyScrollBoundsChange notifies the parent that a child's scroll + // content bounds or viewport bounds have changed. This is expected to + // be called by child elements when they change their supported scroll + // axes, their scroll position (either autonomously or as a result of a + // call to ScrollTo()), or their content size. + NotifyScrollBoundsChange (child Scrollable) +} diff --git a/elements/element.go b/elements/element.go index 3df2466..5f6d298 100644 --- a/elements/element.go +++ b/elements/element.go @@ -10,27 +10,26 @@ import "git.tebibyte.media/sashakoshka/tomo/config" type Element interface { // Bounds reports the element's bounding box. This must reflect the // bounding last given to the element by DrawTo. - Bounds () (bounds image.Rectangle) - - // DrawTo gives the element a canvas to draw on, along with a bounding - // box to be used for laying out the element. This should only be called - // by the parent element. This is typically a region of the parent - // element's canvas. - DrawTo (canvas canvas.Canvas, bounds image.Rectangle) - - // OnDamage sets a function to be called when an area of the element is - // drawn on and should be pushed to the screen. - OnDamage (callback func (region canvas.Canvas)) - + Bounds () image.Rectangle + // MinimumSize specifies the minimum amount of pixels this element's // width and height may be set to. If the element is given a resize // event with dimensions smaller than this, it will use its minimum // instead of the offending dimension(s). MinimumSize () (width, height int) - // OnMinimumSizeChange sets a function to be called when the element's - // minimum size is changed. - OnMinimumSizeChange (callback func ()) + // SetParent sets the parent container of the element. This should only + // be called by the parent when the element is adopted. If parent is set + // to nil, it will mark itself as not having a parent. If this method is + // passed a non-nil value and the element already has a parent, it will + // panic. + SetParent (Parent) + + // DrawTo gives the element a canvas to draw on, along with a bounding + // box to be used for laying out the element. This should only be called + // by the parent element. This is typically a region of the parent + // element's canvas. + DrawTo (canvas canvas.Canvas, bounds image.Rectangle, onDamage func (region image.Rectangle)) } // Focusable represents an element that has keyboard navigation support. This @@ -41,7 +40,7 @@ type Focusable interface { // Focused returns whether or not this element or any of its children // are currently focused. - Focused () (selected bool) + Focused () bool // Focus focuses this element, if its parent element grants the // request. @@ -57,20 +56,6 @@ type Focusable interface { // HandleDeselection causes this element to mark itself and all of its // children as unfocused. 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. - 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. - OnFocusMotionRequest (func (direction input.KeynavDirection) (granted bool)) } // KeyboardTarget represents an element that can receive keyboard input. @@ -93,9 +78,6 @@ type KeyboardTarget interface { type MouseTarget interface { Element - // Each of these handler methods is passed the position of the mouse - // cursor at the time of the event as x, y. - // HandleMouseDown is called when a mouse button is pressed down on this // element. HandleMouseDown (x, y int, button input.Button) @@ -103,15 +85,25 @@ type MouseTarget interface { // HandleMouseUp is called when a mouse button is released that was // originally pressed down on this element. HandleMouseUp (x, y int, button input.Button) +} - // HandleMouseMove is called when the mouse is moved over this element, +// MotionTarget represents an element that can receive mouse motion events. +type MotionTarget interface { + Element + + // HandleMotion 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. - HandleMouseMove (x, y int) + HandleMotion (x, y int) +} + +// ScrollTarget represents an element that can receive mouse scroll events. +type ScrollTarget interface { + Element // HandleScroll is called when the mouse is scrolled. The X and Y // direction of the scroll event are passed as deltaX and deltaY. - HandleMouseScroll (x, y int, deltaX, deltaY float64) + HandleScroll (x, y int, deltaX, deltaY float64) } // Flexible represents an element who's preferred minimum height can change in @@ -132,11 +124,7 @@ type Flexible interface { // // It is important to note that if a parent container checks for // flexible chilren, it itself will likely need to be flexible. - FlexibleHeightFor (width int) (height int) - - // OnFlexibleHeightChange sets a function to be called when the - // parameters affecting this element's flexible height are changed. - OnFlexibleHeightChange (callback func ()) + FlexibleHeightFor (width int) int } // Scrollable represents an element that can be scrolled. It acts as a viewport @@ -145,11 +133,11 @@ type Scrollable interface { Element // ScrollContentBounds returns the full content size of the element. - ScrollContentBounds () (bounds image.Rectangle) + ScrollContentBounds () image.Rectangle // ScrollViewportBounds returns the size and position of the element's // viewport relative to ScrollBounds. - ScrollViewportBounds () (bounds image.Rectangle) + ScrollViewportBounds () image.Rectangle // ScrollTo scrolls the viewport to the specified point relative to // ScrollBounds. @@ -157,10 +145,6 @@ type Scrollable interface { // ScrollAxes returns the supported axes for scrolling. ScrollAxes () (horizontal, vertical bool) - - // OnScrollBoundsChange sets a function to be called when the element's - // ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed. - OnScrollBoundsChange (callback func ()) } // Collapsible represents an element who's minimum width and height can be diff --git a/elements/window.go b/elements/window.go index 618d795..95f0f0c 100644 --- a/elements/window.go +++ b/elements/window.go @@ -4,19 +4,20 @@ import "image" // Window represents a top-level container generated by the currently running // backend. It can contain a single element. It is hidden by default, and must -// be explicitly shown with the Show() method. If it contains no element, it -// displays a black (or transprent) background. +// be explicitly shown with the Show() method. type Window interface { + Parent + // Adopt sets the root element of the window. There can only be one of // these at one time. - Adopt (child Element) + Adopt (Element) // Child returns the root element of the window. - Child () (child Element) + Child () Element // SetTitle sets the title that appears on the window's title bar. This // method might have no effect with some backends. - SetTitle (title string) + SetTitle (string) // SetIcon taks in a list different sizes of the same icon and selects // the best one to display on the window title bar, dock, or whatever is From a34e8768aba87cc026e2b78416041512a5251644 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 14 Mar 2023 18:30:32 -0400 Subject: [PATCH 02/17] Redid cores to conform to the new API changes --- elements/container.go | 2 +- elements/core/core.go | 66 +++++++++++++++++---------- elements/core/propagator.go | 90 +++++++++++++++---------------------- elements/core/selectable.go | 42 +++++------------ 4 files changed, 93 insertions(+), 107 deletions(-) diff --git a/elements/container.go b/elements/container.go index 036c87b..1001e7c 100644 --- a/elements/container.go +++ b/elements/container.go @@ -1,4 +1,4 @@ -package element +package elements import "git.tebibyte.media/sashakoshka/tomo/input" diff --git a/elements/core/core.go b/elements/core/core.go index b5c6259..2de8ff2 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -3,31 +3,38 @@ package core import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/elements" // Core is a struct that implements some core functionality common to most // widgets. It is meant to be embedded directly into a struct. type Core struct { canvas canvas.Canvas bounds image.Rectangle + parent elements.Parent + outer elements.Element metrics struct { minimumWidth int minimumHeight int } - drawSizeChange func () - onMinimumSizeChange func () - onDamage func (region canvas.Canvas) + drawSizeChange func () + onDamage func (region image.Rectangle) } -// NewCore creates a new element core and its corresponding control. +// NewCore creates a new element core and its corresponding control given the +// element that it will be a part of. If outer is nil, this function will return +// nil. func NewCore ( + outer elements.Element, drawSizeChange func (), ) ( core *Core, control CoreControl, ) { + if outer == nil { return } core = &Core { + outer: outer, drawSizeChange: drawSizeChange, } control = CoreControl { core: core } @@ -47,28 +54,32 @@ func (core *Core) MinimumSize () (width, height int) { return core.metrics.minimumWidth, core.metrics.minimumHeight } +// MinimumSize fulfils the tomo.Element interface. This should not need to be +// overridden, unless you want to detect when the element is parented or +// unparented. +func (core *Core) SetParent (parent elements.Parent) { + if core.parent != nil { + panic("core.SetParent: element already has a parent") + } + + core.parent = parent +} + // DrawTo fulfills the tomo.Element interface. This should not need to be // overridden. -func (core *Core) DrawTo (canvas canvas.Canvas, bounds image.Rectangle) { - core.canvas = canvas - core.bounds = bounds +func (core *Core) DrawTo ( + canvas canvas.Canvas, + bounds image.Rectangle, + onDamage func (region image.Rectangle), +) { + core.canvas = canvas + core.bounds = bounds + core.onDamage = onDamage if core.drawSizeChange != nil && core.canvas != nil { core.drawSizeChange() } } -// OnDamage fulfils the tomo.Element interface. This should not need to be -// overridden. -func (core *Core) OnDamage (callback func (region canvas.Canvas)) { - core.onDamage = callback -} - -// OnMinimumSizeChange fulfils the tomo.Element interface. This should not need -// to be overridden. -func (core *Core) OnMinimumSizeChange (callback func ()) { - core.onMinimumSizeChange = callback -} - // CoreControl is a struct that can exert control over a Core struct. It can be // used as a canvas. It must not be directly embedded into an element, but // instead kept as a private member. When a Core struct is created, a @@ -106,6 +117,16 @@ func (control CoreControl) Buffer () (data []color.RGBA, stride int) { return control.core.canvas.Buffer() } +// Parent returns the element's parent. +func (control CoreControl) Parent () elements.Parent { + return control.core.parent +} + +// Outer returns the outer element given when the control was constructed. +func (control CoreControl) Outer () elements.Element { + return control.core.outer +} + // HasImage returns true if the core has an allocated image buffer, and false if // it doesn't. func (control CoreControl) HasImage () (has bool) { @@ -118,8 +139,7 @@ func (control CoreControl) DamageRegion (regions ...image.Rectangle) { if control.core.canvas == nil { return } if control.core.onDamage != nil { for _, region := range regions { - control.core.onDamage ( - canvas.Cut(control.core.canvas, region)) + control.core.onDamage(region) } } } @@ -141,8 +161,8 @@ func (control CoreControl) SetMinimumSize (width, height int) { core.metrics.minimumWidth = width core.metrics.minimumHeight = height - if control.core.onMinimumSizeChange != nil { - control.core.onMinimumSizeChange() + if control.core.parent != nil { + control.core.parent.NotifyMinimumSizeChange(control.core.outer) } } diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 2375f5b..33ba869 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -6,9 +6,9 @@ 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 +// Container represents an object that can provide access to a list of child // elements. -type Parent interface { +type Container interface { Child (index int) elements.Element CountChildren () int } @@ -18,20 +18,20 @@ type Parent interface { // 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) + core CoreControl + container Container + drags [10]elements.MouseTarget + focused 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 } +// NewPropagator creates a new event propagator that uses the specified +// container to access a list of child elements that will have events propagated +// to them. If container is nil, the function will return nil. +func NewPropagator (container Container, core CoreControl) (propagator *Propagator) { + if container == nil { return nil } propagator = &Propagator { - parent: parent, + core: core, + container: container, } return } @@ -47,8 +47,12 @@ func (propagator *Propagator) Focused () (focused bool) { // Focus focuses this element, if its parent element grants the // request. func (propagator *Propagator) Focus () { - if propagator.onFocusRequest != nil { - propagator.onFocusRequest() + if propagator.focused == true { return } + parent := propagator.core.Parent() + if parent, ok := parent.(elements.FocusableParent); ok && parent != nil { + propagator.focused = parent.RequestFocus ( + propagator.core.Outer().(elements.Focusable), + input.KeynavDirectionNeutral) } } @@ -86,7 +90,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.parent.Child(firstFocused). + propagator.container.Child(firstFocused). (elements.Focusable) // before we move the focus, the currently focused child @@ -99,11 +103,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.parent.CountChildren() && index >= 0; + index < propagator.container.CountChildren() && index >= 0; index += int(direction) { child, focusable := - propagator.parent.Child(index). + propagator.container.Child(index). (elements.Focusable) if focusable && child.HandleFocus(direction) { // we have found one, so we now actually move @@ -128,22 +132,6 @@ func (propagator *Propagator) HandleUnfocus () { 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 { @@ -194,18 +182,16 @@ func (propagator *Propagator) HandleMouseUp (x, y int, button input.Button) { func (propagator *Propagator) HandleMouseMove (x, y int) { handled := false for _, child := range propagator.drags { - if child != nil { - child.HandleMouseMove(x, y) + if child, ok := child.(elements.MotionTarget); ok { + child.HandleMotion(x, y) handled = true } } - if handled { - child, handlesMouse := - propagator.childAt(image.Pt(x, y)). - (elements.MouseTarget) - if handlesMouse { - child.HandleMouseMove(x, y) + if !handled { + child := propagator.childAt(image.Pt(x, y)) + if child, ok := child.(elements.MotionTarget); ok { + child.HandleMotion(x, y) } } } @@ -213,11 +199,9 @@ func (propagator *Propagator) HandleMouseMove (x, y int) { // 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) + child := propagator.childAt(image.Pt(x, y)) + if child, ok := child.(elements.ScrollTarget); ok { + child.HandleScroll(x, y, deltaX, deltaY) } } @@ -281,16 +265,16 @@ func (propagator *Propagator) focusLastFocusableElement ( // ----------- 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) + for index := 0; index < propagator.container.CountChildren(); index ++ { + child := propagator.container.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) + for index := propagator.container.CountChildren() - 1; index > 0; index -- { + child := propagator.container.Child(index) if child == nil { continue } if !callback(child) { break } } @@ -327,8 +311,8 @@ func (propagator *Propagator) forFocusable (callback func (child elements.Focusa } func (propagator *Propagator) firstFocused () int { - for index := 0; index < propagator.parent.CountChildren(); index ++ { - child, focusable := propagator.parent.Child(index).(elements.Focusable) + for index := 0; index < propagator.container.CountChildren(); index ++ { + child, focusable := propagator.container.Child(index).(elements.Focusable) if focusable && child.Focused() { return index } diff --git a/elements/core/selectable.go b/elements/core/selectable.go index 824dcae..c8fc30e 100644 --- a/elements/core/selectable.go +++ b/elements/core/selectable.go @@ -2,15 +2,15 @@ package core // import "runtime/debug" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/elements" // FocusableCore is a struct that can be embedded into objects to make them // focusable, giving them the default keynav behavior. type FocusableCore struct { + core CoreControl focused bool enabled bool drawFocusChange func () - onFocusRequest func () (granted bool) - onFocusMotionRequest func(input.KeynavDirection) (granted bool) } // NewFocusableCore creates a new focusability core and its corresponding @@ -18,16 +18,18 @@ type FocusableCore struct { // state changes (which it should), a callback to draw and push the update can // be specified. func NewFocusableCore ( + core CoreControl, drawFocusChange func (), ) ( - core *FocusableCore, + focusable *FocusableCore, control FocusableCoreControl, ) { - core = &FocusableCore { + focusable = &FocusableCore { + core: core, drawFocusChange: drawFocusChange, enabled: true, } - control = FocusableCoreControl { core: core } + control = FocusableCoreControl { core: focusable } return } @@ -39,13 +41,11 @@ func (core *FocusableCore) Focused () (focused bool) { // Focus focuses this element, if its parent element grants the request. func (core *FocusableCore) Focus () { if !core.enabled || core.focused { return } - if core.onFocusRequest != nil { - if core.onFocusRequest() { - core.focused = true - if core.drawFocusChange != nil { - core.drawFocusChange() - } - } + parent := core.core.Parent() + if parent, ok := parent.(elements.FocusableParent); ok && parent != nil { + core.focused = parent.RequestFocus ( + core.core.Outer().(elements.Focusable), + input.KeynavDirectionNeutral) } } @@ -76,24 +76,6 @@ func (core *FocusableCore) HandleUnfocus () { if core.drawFocusChange != nil { core.drawFocusChange() } } -// OnFocusRequest sets a function to be called when this element -// wants its parent element to focus it. Parent elements should return -// true if the request was granted, and false if it was not. -func (core *FocusableCore) OnFocusRequest (callback func () (granted bool)) { - core.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 (core *FocusableCore) OnFocusMotionRequest ( - callback func (direction input.KeynavDirection) (granted bool), -) { - core.onFocusMotionRequest = callback -} - // Enabled returns whether or not the element is enabled. func (core *FocusableCore) Enabled () (enabled bool) { return core.enabled From 14ad35d85c3d22bd72ed67e8443cbd7789f1ced3 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 14 Mar 2023 18:54:24 -0400 Subject: [PATCH 03/17] X backend now conforms to new API changes --- backends/x/event.go | 44 ++++++++++++------------ backends/x/window.go | 80 ++++++++++++++++++++------------------------ backends/x/x.go | 6 ++-- 3 files changed, 62 insertions(+), 68 deletions(-) diff --git a/backends/x/event.go b/backends/x/event.go index 5f6b773..d5da4ad 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -14,7 +14,7 @@ type scrollSum struct { const scrollDistance = 16 -func (sum *scrollSum) add (button xproto.Button, window *Window, state uint16) { +func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) { shift := (state & xproto.ModMaskShift) > 0 || (state & window.backend.modifierMasks.shiftLock) > 0 @@ -44,7 +44,7 @@ func (sum *scrollSum) add (button xproto.Button, window *Window, state uint16) { } -func (window *Window) handleExpose ( +func (window *window) handleExpose ( connection *xgbutil.XUtil, event xevent.ExposeEvent, ) { @@ -52,7 +52,7 @@ func (window *Window) handleExpose ( window.pushRegion(region) } -func (window *Window) handleConfigureNotify ( +func (window *window) handleConfigureNotify ( connection *xgbutil.XUtil, event xevent.ConfigureNotifyEvent, ) { @@ -81,7 +81,7 @@ func (window *Window) handleConfigureNotify ( } } -func (window *Window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) { +func (window *window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (found bool) { nextEvents := xevent.Peek(window.backend.connection) if len(nextEvents) > 0 { untypedEvent := nextEvents[0] @@ -97,7 +97,7 @@ func (window *Window) exposeEventFollows (event xproto.ConfigureNotifyEvent) (fo return false } -func (window *Window) modifiersFromState ( +func (window *window) modifiersFromState ( state uint16, ) ( modifiers input.Modifiers, @@ -114,7 +114,7 @@ func (window *Window) modifiersFromState ( } } -func (window *Window) handleKeyPress ( +func (window *window) handleKeyPress ( connection *xgbutil.XUtil, event xevent.KeyPressEvent, ) { @@ -141,7 +141,7 @@ func (window *Window) handleKeyPress ( } } -func (window *Window) handleKeyRelease ( +func (window *window) handleKeyRelease ( connection *xgbutil.XUtil, event xevent.KeyReleaseEvent, ) { @@ -175,23 +175,25 @@ func (window *Window) handleKeyRelease ( } } -func (window *Window) handleButtonPress ( +func (window *window) handleButtonPress ( connection *xgbutil.XUtil, event xevent.ButtonPressEvent, ) { if window.child == nil { return } - if child, ok := window.child.(elements.MouseTarget); ok { - buttonEvent := *event.ButtonPressEvent - if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { + buttonEvent := *event.ButtonPressEvent + if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { + if child, ok := window.child.(elements.ScrollTarget); ok { sum := scrollSum { } sum.add(buttonEvent.Detail, window, buttonEvent.State) window.compressScrollSum(buttonEvent, &sum) - child.HandleMouseScroll ( + child.HandleScroll ( int(buttonEvent.EventX), int(buttonEvent.EventY), float64(sum.x), float64(sum.y)) - } else { + } + } else { + if child, ok := window.child.(elements.MouseTarget); ok { child.HandleMouseDown ( int(buttonEvent.EventX), int(buttonEvent.EventY), @@ -201,7 +203,7 @@ func (window *Window) handleButtonPress ( } -func (window *Window) handleButtonRelease ( +func (window *window) handleButtonRelease ( connection *xgbutil.XUtil, event xevent.ButtonReleaseEvent, ) { @@ -217,21 +219,21 @@ func (window *Window) handleButtonRelease ( } } -func (window *Window) handleMotionNotify ( +func (window *window) handleMotionNotify ( connection *xgbutil.XUtil, event xevent.MotionNotifyEvent, ) { if window.child == nil { return } - if child, ok := window.child.(elements.MouseTarget); ok { + if child, ok := window.child.(elements.MotionTarget); ok { motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) - child.HandleMouseMove ( + child.HandleMotion ( int(motionEvent.EventX), int(motionEvent.EventY)) } } -func (window *Window) compressExpose ( +func (window *window) compressExpose ( firstEvent xproto.ExposeEvent, ) ( lastEvent xproto.ExposeEvent, @@ -268,7 +270,7 @@ func (window *Window) compressExpose ( return } -func (window *Window) compressConfigureNotify ( +func (window *window) compressConfigureNotify ( firstEvent xproto.ConfigureNotifyEvent, ) ( lastEvent xproto.ConfigureNotifyEvent, @@ -296,7 +298,7 @@ func (window *Window) compressConfigureNotify ( return } -func (window *Window) compressScrollSum ( +func (window *window) compressScrollSum ( firstEvent xproto.ButtonPressEvent, sum *scrollSum, ) { @@ -323,7 +325,7 @@ func (window *Window) compressScrollSum ( return } -func (window *Window) compressMotionNotify ( +func (window *window) compressMotionNotify ( firstEvent xproto.MotionNotifyEvent, ) ( lastEvent xproto.MotionNotifyEvent, diff --git a/backends/x/window.go b/backends/x/window.go index 67d352a..ecd4e86 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -14,7 +14,7 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/elements" // import "runtime/debug" -type Window struct { +type window struct { backend *Backend xWindow *xwindow.Window xCanvas *xgraphics.Image @@ -40,7 +40,7 @@ func (backend *Backend) NewWindow ( ) { if backend == nil { panic("nil backend") } - window := &Window { backend: backend } + window := &window { backend: backend } window.xWindow, err = xwindow.Generate(backend.connection) if err != nil { return } @@ -90,18 +90,15 @@ func (backend *Backend) NewWindow ( return } -func (window *Window) Adopt (child elements.Element) { +func (window *window) NotifyMinimumSizeChange (child elements.Element) { + window.childMinimumSizeChangeCallback(child.MinimumSize()) +} + +func (window *window) Adopt (child elements.Element) { // disown previous child if window.child != nil { - window.child.OnDamage(nil) - window.child.OnMinimumSizeChange(nil) - } - if previousChild, ok := window.child.(elements.Focusable); ok { - previousChild.OnFocusRequest(nil) - previousChild.OnFocusMotionRequest(nil) - if previousChild.Focused() { - previousChild.HandleUnfocus() - } + window.child.SetParent(nil) + window.child.DrawTo(nil, image.Rectangle { }, nil) } // adopt new child @@ -112,15 +109,7 @@ func (window *Window) Adopt (child elements.Element) { if newChild, ok := child.(elements.Configurable); ok { newChild.SetConfig(window.config) } - if newChild, ok := child.(elements.Focusable); ok { - newChild.OnFocusRequest(window.childSelectionRequestCallback) - } if child != nil { - child.OnDamage(window.childDrawCallback) - child.OnMinimumSizeChange (func () { - window.childMinimumSizeChangeCallback ( - child.MinimumSize()) - }) if !window.childMinimumSizeChangeCallback(child.MinimumSize()) { window.resizeChildToFit() window.redrawChildEntirely() @@ -128,19 +117,19 @@ func (window *Window) Adopt (child elements.Element) { } } -func (window *Window) Child () (child elements.Element) { +func (window *window) Child () (child elements.Element) { child = window.child return } -func (window *Window) SetTitle (title string) { +func (window *window) SetTitle (title string) { ewmh.WmNameSet ( window.backend.connection, window.xWindow.Id, title) } -func (window *Window) SetIcon (sizes []image.Image) { +func (window *window) SetIcon (sizes []image.Image) { wmIcons := []ewmh.WmIcon { } for _, icon := range sizes { @@ -179,7 +168,7 @@ func (window *Window) SetIcon (sizes []image.Image) { wmIcons) } -func (window *Window) Show () { +func (window *window) Show () { if window.child == nil { window.xCanvas.For (func (x, y int) xgraphics.BGRA { return xgraphics.BGRA { } @@ -191,35 +180,35 @@ func (window *Window) Show () { window.xWindow.Map() } -func (window *Window) Hide () { +func (window *window) Hide () { window.xWindow.Unmap() } -func (window *Window) Close () { +func (window *window) Close () { if window.onClose != nil { window.onClose() } delete(window.backend.windows, window.xWindow.Id) window.xWindow.Destroy() } -func (window *Window) OnClose (callback func ()) { +func (window *window) OnClose (callback func ()) { window.onClose = callback } -func (window *Window) SetTheme (theme theme.Theme) { +func (window *window) SetTheme (theme theme.Theme) { window.theme = theme if child, ok := window.child.(elements.Themeable); ok { child.SetTheme(theme) } } -func (window *Window) SetConfig (config config.Config) { +func (window *window) SetConfig (config config.Config) { window.config = config if child, ok := window.child.(elements.Configurable); ok { child.SetConfig(config) } } -func (window *Window) reallocateCanvas () { +func (window *window) reallocateCanvas () { window.canvas.Reallocate(window.metrics.width, window.metrics.height) previousWidth, previousHeight := 0, 0 @@ -250,23 +239,28 @@ func (window *Window) reallocateCanvas () { } -func (window *Window) redrawChildEntirely () { - window.pushRegion(window.paste(window.canvas)) - +func (window *window) redrawChildEntirely () { + window.paste(window.canvas.Bounds()) + window.pushRegion(window.canvas.Bounds()) } -func (window *Window) resizeChildToFit () { +func (window *window) resizeChildToFit () { window.skipChildDrawCallback = true - window.child.DrawTo(window.canvas, window.canvas.Bounds()) + window.child.DrawTo ( + window.canvas, + window.canvas.Bounds(), + window.childDrawCallback) window.skipChildDrawCallback = false } -func (window *Window) childDrawCallback (region canvas.Canvas) { +func (window *window) childDrawCallback (region image.Rectangle) { if window.skipChildDrawCallback { return } - window.pushRegion(window.paste(region)) + window.paste(region) + window.pushRegion(region) } -func (window *Window) paste (canvas canvas.Canvas) (updatedRegion image.Rectangle) { +func (window *window) paste (region image.Rectangle) { + canvas := canvas.Cut(window.canvas, region) data, stride := canvas.Buffer() bounds := canvas.Bounds().Intersect(window.xCanvas.Bounds()) @@ -286,11 +280,9 @@ func (window *Window) paste (canvas canvas.Canvas) (updatedRegion image.Rectangl dstData[index + 3] = rgba.A } } - - return bounds } -func (window *Window) childMinimumSizeChangeCallback (width, height int) (resized bool) { +func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) { icccm.WmNormalHintsSet ( window.backend.connection, window.xWindow.Id, @@ -312,14 +304,14 @@ func (window *Window) childMinimumSizeChangeCallback (width, height int) (resize return false } -func (window *Window) childSelectionRequestCallback () (granted bool) { +func (window *window) childSelectionRequestCallback () (granted bool) { if _, ok := window.child.(elements.Focusable); ok { return true } return false } -func (window *Window) childSelectionMotionRequestCallback ( +func (window *window) childSelectionMotionRequestCallback ( direction input.KeynavDirection, ) ( granted bool, @@ -333,7 +325,7 @@ func (window *Window) childSelectionMotionRequestCallback ( return true } -func (window *Window) pushRegion (region image.Rectangle) { +func (window *window) pushRegion (region image.Rectangle) { if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) if ok { diff --git a/backends/x/x.go b/backends/x/x.go index eefd914..d72ce1f 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -30,7 +30,7 @@ type Backend struct { theme theme.Theme config config.Config - windows map[xproto.Window] *Window + windows map[xproto.Window] *window open bool } @@ -38,7 +38,7 @@ type Backend struct { // NewBackend instantiates an X backend. func NewBackend () (output tomo.Backend, err error) { backend := &Backend { - windows: map[xproto.Window] *Window { }, + windows: map[xproto.Window] *window { }, doChannel: make(chan func (), 0), theme: theme.Default { }, config: config.Default { }, @@ -79,7 +79,7 @@ func (backend *Backend) Stop () { if !backend.open { return } backend.open = false - toClose := []*Window { } + toClose := []*window { } for _, window := range backend.windows { toClose = append(toClose, window) } From f4799ba03d7b61f1399f752fc3b77f62389ccecb Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Tue, 14 Mar 2023 19:41:36 -0400 Subject: [PATCH 04/17] Testing elements now conform to new API --- elements/testing/artist.go | 2 +- elements/testing/mouse.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/elements/testing/artist.go b/elements/testing/artist.go index fe5c162..2af49a5 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -23,7 +23,7 @@ type Artist struct { // NewArtist creates a new artist test element. func NewArtist () (element *Artist) { element = &Artist { } - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.core.SetMinimumSize(240, 240) return } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 194518a..0cd9ecd 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -24,7 +24,7 @@ type Mouse struct { // NewMouse creates a new mouse test element. func NewMouse () (element *Mouse) { element = &Mouse { c: theme.C("testing", "mouse") } - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.core.SetMinimumSize(32, 32) return } @@ -82,7 +82,7 @@ func (element *Mouse) HandleMouseUp (x, y int, button input.Button) { element.lastMousePos = mousePos } -func (element *Mouse) HandleMouseMove (x, y int) { +func (element *Mouse) HandleMotion (x, y int) { if !element.drawing { return } mousePos := image.Pt(x, y) element.core.DamageRegion (shapes.ColorLine ( @@ -90,5 +90,3 @@ func (element *Mouse) HandleMouseMove (x, y int) { element.lastMousePos, mousePos)) element.lastMousePos = mousePos } - -func (element *Mouse) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } From 0015820face52d4d471f0deae8bc6c5c62fef3be Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 01:41:23 -0400 Subject: [PATCH 05/17] Basic elements now conform to new API --- elements/basic/button.go | 7 +- elements/basic/checkbox.go | 7 +- elements/basic/container.go | 80 +++++------------ elements/basic/documentContainer.go | 101 ++++++--------------- elements/basic/icon.go | 2 +- elements/basic/image.go | 2 +- elements/basic/label.go | 2 +- elements/basic/list.go | 38 ++++---- elements/basic/progressbar.go | 2 +- elements/basic/scrollbar.go | 2 +- elements/basic/scrollcontainer.go | 131 ++++++++++------------------ elements/basic/slider.go | 4 +- elements/basic/spacer.go | 2 +- elements/basic/switch.go | 4 +- elements/basic/textbox.go | 25 +++--- 15 files changed, 137 insertions(+), 272 deletions(-) diff --git a/elements/basic/button.go b/elements/basic/button.go index e496876..7b63b48 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -35,9 +35,9 @@ type Button struct { func NewButton (text string) (element *Button) { element = &Button { showText: true } element.theme.Case = theme.C("basic", "button") - element.Core, element.core = core.NewCore(element.drawAll) + element.Core, element.core = core.NewCore(element, element.drawAll) element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.drawAndPush) + element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush) element.SetText(text) return } @@ -61,9 +61,6 @@ func (element *Button) HandleMouseUp (x, y int, button input.Button) { element.drawAndPush() } -func (element *Button) HandleMouseMove (x, y int) { } -func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } - func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if !element.Enabled() { return } if key == input.KeyEnter { diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index 14690bb..b686ffa 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -29,9 +29,9 @@ type Checkbox struct { func NewCheckbox (text string, checked bool) (element *Checkbox) { element = &Checkbox { checked: checked } element.theme.Case = theme.C("basic", "checkbox") - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.redo) + element.focusableControl = core.NewFocusableCore(element.core, element.redo) element.SetText(text) return } @@ -65,9 +65,6 @@ func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) { } } -func (element *Checkbox) HandleMouseMove (x, y int) { } -func (element *Checkbox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } - func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter { element.pressed = true diff --git a/elements/basic/container.go b/elements/basic/container.go index ec8a024..8875fa9 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -32,8 +32,8 @@ type Container struct { 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.Core, element.core = core.NewCore(element, element.redoAll) + element.Propagator = core.NewPropagator(element, element.core) element.SetLayout(layout) return } @@ -51,33 +51,13 @@ func (element *Container) SetLayout (layout layouts.Layout) { // the element will expand (instead of contract to its minimum size), in // whatever way is defined by the current layout. func (element *Container) Adopt (child elements.Element, expand bool) { - // set event handlers if child0, ok := child.(elements.Themeable); ok { child0.SetTheme(element.theme.Theme) } if child0, ok := child.(elements.Configurable); ok { child0.SetConfig(element.config.Config) } - child.OnDamage (func (region canvas.Canvas) { - element.core.DamageRegion(region.Bounds()) - }) - child.OnMinimumSizeChange (func () { - // TODO: this could probably stand to be more efficient. I mean - // seriously? - element.updateMinimumSize() - element.redoAll() - element.core.DamageAll() - }) - if child0, ok := child.(elements.Focusable); ok { - child0.OnFocusRequest (func () (granted bool) { - return element.childFocusRequestCallback(child0) - }) - child0.OnFocusMotionRequest ( - func (direction input.KeynavDirection) (granted bool) { - if element.onFocusMotionRequest == nil { return } - return element.onFocusMotionRequest(direction) - }) - } + child.SetParent(element) // add child element.children = append (element.children, layouts.LayoutEntry { @@ -136,14 +116,12 @@ func (element *Container) Disown (child elements.Element) { } func (element *Container) clearChildEventHandlers (child elements.Element) { - child.DrawTo(nil, image.Rectangle { }) - child.OnDamage(nil) - child.OnMinimumSizeChange(nil) - if child0, ok := child.(elements.Focusable); ok { - child0.OnFocusRequest(nil) - child0.OnFocusMotionRequest(nil) - if child0.Focused() { - child0.HandleUnfocus() + child.DrawTo(nil, image.Rectangle { }, nil) + child.SetParent(nil) + + if child, ok := child.(elements.Focusable); ok { + if child.Focused() { + child.HandleUnfocus() } } } @@ -200,7 +178,7 @@ func (element *Container) redoAll () { // remove child canvasses so that any operations done in here will not // cause a child to draw to a wack ass canvas. for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds) + entry.DrawTo(nil, entry.Bounds, nil) } // do a layout @@ -220,10 +198,20 @@ func (element *Container) redoAll () { for _, entry := range element.children { entry.DrawTo ( canvas.Cut(element.core, entry.Bounds), - entry.Bounds) + entry.Bounds, func (region image.Rectangle) { + element.core.DamageRegion(region) + }) } } +// NotifyMinimumSizeChange notifies the container that the minimum size of a +// child element has changed. +func (element *Container) NotifyMinimumSizeChange (child elements.Element) { + element.updateMinimumSize() + element.redoAll() + element.core.DamageAll() +} + // SetTheme sets the element's theme. func (element *Container) SetTheme (new theme.Theme) { if new == element.theme.Theme { return } @@ -241,32 +229,6 @@ func (element *Container) SetConfig (new config.Config) { element.redoAll() } -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) childFocusRequestCallback ( - child elements.Focusable, -) ( - granted bool, -) { - if element.onFocusRequest != nil && element.onFocusRequest() { - element.Propagator.HandleUnfocus() - element.Propagator.HandleFocus(input.KeynavDirectionNeutral) - return true - } else { - return false - } -} - func (element *Container) updateMinimumSize () { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) diff --git a/elements/basic/documentContainer.go b/elements/basic/documentContainer.go index 81e8c60..96d3558 100644 --- a/elements/basic/documentContainer.go +++ b/elements/basic/documentContainer.go @@ -1,7 +1,6 @@ package basicElements 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/canvas" @@ -22,18 +21,14 @@ type DocumentContainer struct { config config.Wrapped theme theme.Wrapped - - onFocusRequest func () (granted bool) - onFocusMotionRequest func (input.KeynavDirection) (granted bool) - onScrollBoundsChange func () } // NewDocumentContainer creates a new document container. func NewDocumentContainer () (element *DocumentContainer) { element = &DocumentContainer { } element.theme.Case = theme.C("basic", "documentContainer") - element.Core, element.core = core.NewCore(element.redoAll) - element.Propagator = core.NewPropagator(element) + element.Core, element.core = core.NewCore(element, element.redoAll) + element.Propagator = core.NewPropagator(element, element.core) return } @@ -46,29 +41,12 @@ func (element *DocumentContainer) Adopt (child elements.Element) { if child0, ok := child.(elements.Configurable); ok { child0.SetConfig(element.config.Config) } - child.OnDamage (func (region canvas.Canvas) { - element.core.DamageRegion(region.Bounds()) - }) - child.OnMinimumSizeChange (func () { - element.redoAll() - element.core.DamageAll() - }) - if child0, ok := child.(elements.Flexible); ok { - child0.OnFlexibleHeightChange (func () { - element.redoAll() - element.core.DamageAll() - }) - } - if child0, ok := child.(elements.Focusable); ok { - child0.OnFocusRequest (func () (granted bool) { - return element.childFocusRequestCallback(child0) - }) - child0.OnFocusMotionRequest ( - func (direction input.KeynavDirection) (granted bool) { - if element.onFocusMotionRequest == nil { return } - return element.onFocusMotionRequest(direction) - }) - } + // if child0, ok := child.(elements.Flexible); ok { + // child0.OnFlexibleHeightChange (func () { + // element.redoAll() + // element.core.DamageAll() + // }) + // } // add child element.children = append (element.children, layouts.LayoutEntry { @@ -123,14 +101,12 @@ func (element *DocumentContainer) Disown (child elements.Element) { } func (element *DocumentContainer) clearChildEventHandlers (child elements.Element) { - child.DrawTo(nil, image.Rectangle { }) - child.OnDamage(nil) - child.OnMinimumSizeChange(nil) - if child0, ok := child.(elements.Focusable); ok { - child0.OnFocusRequest(nil) - child0.OnFocusMotionRequest(nil) - if child0.Focused() { - child0.HandleUnfocus() + child.DrawTo(nil, image.Rectangle { }, nil) + child.SetParent(nil) + + if child, ok := child.(elements.Focusable); ok { + if child.Focused() { + child.HandleUnfocus() } } } @@ -204,14 +180,14 @@ func (element *DocumentContainer) redoAll () { artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...) element.partition() - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } func (element *DocumentContainer) partition () { for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds) + entry.DrawTo(nil, entry.Bounds, nil) } // cut our canvas up and give peices to child elements @@ -219,11 +195,20 @@ func (element *DocumentContainer) partition () { if entry.Bounds.Overlaps(element.Bounds()) { entry.DrawTo ( canvas.Cut(element.core, entry.Bounds), - entry.Bounds) + entry.Bounds, func (region image.Rectangle) { + element.core.DamageRegion(region) + }) } } } +// NotifyMinimumSizeChange notifies the container that the minimum size of a +// child element has changed. +func (element *DocumentContainer) NotifyMinimumSizeChange (child elements.Element) { + element.redoAll() + element.core.DamageAll() +} + // SetTheme sets the element's theme. func (element *DocumentContainer) SetTheme (new theme.Theme) { if new == element.theme.Theme { return } @@ -239,18 +224,6 @@ func (element *DocumentContainer) SetConfig (new config.Config) { element.redoAll() } -func (element *DocumentContainer) OnFocusRequest (callback func () (granted bool)) { - element.onFocusRequest = callback - element.Propagator.OnFocusRequest(callback) -} - -func (element *DocumentContainer) OnFocusMotionRequest ( - callback func (direction input.KeynavDirection) (granted bool), -) { - element.onFocusMotionRequest = callback - element.Propagator.OnFocusMotionRequest(callback) -} - // ScrollContentBounds returns the full content size of the element. func (element *DocumentContainer) ScrollContentBounds () image.Rectangle { return element.contentBounds @@ -295,12 +268,6 @@ func (element *DocumentContainer) ScrollAxes () (horizontal, vertical bool) { return false, true } -// OnScrollBoundsChange sets a function to be called when the element's -// ScrollContentBounds, ScrollViewportBounds, or ScrollAxes are changed. -func (element *DocumentContainer) OnScrollBoundsChange(callback func()) { - element.onScrollBoundsChange = callback -} - func (element *DocumentContainer) reflectChildProperties () { focusable := false for _, entry := range element.children { @@ -315,20 +282,6 @@ func (element *DocumentContainer) reflectChildProperties () { } } -func (element *DocumentContainer) childFocusRequestCallback ( - child elements.Focusable, -) ( - granted bool, -) { - if element.onFocusRequest != nil && element.onFocusRequest() { - element.Propagator.HandleUnfocus() - element.Propagator.HandleFocus(input.KeynavDirectionNeutral) - return true - } else { - return false - } -} - func (element *DocumentContainer) doLayout () { margin := element.theme.Margin(theme.PatternBackground) padding := element.theme.Padding(theme.PatternBackground) diff --git a/elements/basic/icon.go b/elements/basic/icon.go index bd92298..3b6750f 100644 --- a/elements/basic/icon.go +++ b/elements/basic/icon.go @@ -19,7 +19,7 @@ func NewIcon (id theme.Icon, size theme.IconSize) (element *Icon) { size: size, } element.theme.Case = theme.C("basic", "icon") - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.updateMinimumSize() return } diff --git a/elements/basic/image.go b/elements/basic/image.go index 2e5d4fd..35bfb94 100644 --- a/elements/basic/image.go +++ b/elements/basic/image.go @@ -13,7 +13,7 @@ type Image struct { func NewImage (image image.Image) (element *Image) { element = &Image { buffer: canvas.FromImage(image) } - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) bounds := image.Bounds() element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) return diff --git a/elements/basic/label.go b/elements/basic/label.go index c14674d..bd99b56 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -29,7 +29,7 @@ type Label struct { func NewLabel (text string, wrap bool) (element *Label) { element = &Label { } element.theme.Case = theme.C("basic", "label") - element.Core, element.core = core.NewCore(element.handleResize) + element.Core, element.core = core.NewCore(element, element.handleResize) element.SetWrap(wrap) element.SetText(text) return diff --git a/elements/basic/list.go b/elements/basic/list.go index 0f4c009..45b7951 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -7,6 +7,7 @@ import "git.tebibyte.media/sashakoshka/tomo/theme" import "git.tebibyte.media/sashakoshka/tomo/config" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/core" // List is an element that contains several objects that a user can select. @@ -29,7 +30,6 @@ type List struct { config config.Wrapped theme theme.Wrapped - onScrollBoundsChange func () onNoEntrySelected func () } @@ -37,9 +37,9 @@ type List struct { func NewList (entries ...ListEntry) (element *List) { element = &List { selectedEntry: -1 } element.theme.Case = theme.C("basic", "list") - element.Core, element.core = core.NewCore(element.handleResize) + element.Core, element.core = core.NewCore(element, element.handleResize) element.FocusableCore, - element.focusableControl = core.NewFocusableCore (func () { + element.focusableControl = core.NewFocusableCore (element.core, func () { if element.core.HasImage () { element.draw() element.core.DamageAll() @@ -64,8 +64,8 @@ func (element *List) handleResize () { element.scroll = element.maxScrollHeight() } element.draw() - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -102,8 +102,8 @@ func (element *List) redo () { element.draw() element.core.DamageAll() } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -210,8 +210,8 @@ func (element *List) ScrollTo (position image.Point) { element.draw() element.core.DamageAll() } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -233,10 +233,6 @@ func (element *List) maxScrollHeight () (height int) { return } -func (element *List) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - // OnNoEntrySelected sets a function to be called when the user chooses to // deselect the current selected entry by clicking on empty space within the // list or by pressing the escape key. @@ -263,8 +259,8 @@ func (element *List) Append (entry ListEntry) { element.draw() element.core.DamageAll() } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -296,8 +292,8 @@ func (element *List) Insert (index int, entry ListEntry) { element.draw() element.core.DamageAll() } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -319,8 +315,8 @@ func (element *List) Remove (index int) { element.draw() element.core.DamageAll() } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -341,8 +337,8 @@ func (element *List) Replace (index int, entry ListEntry) { element.draw() element.core.DamageAll() } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 9178e3e..02a5ff2 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -20,7 +20,7 @@ type ProgressBar struct { func NewProgressBar (progress float64) (element *ProgressBar) { element = &ProgressBar { progress: progress } element.theme.Case = theme.C("basic", "progressBar") - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) return } diff --git a/elements/basic/scrollbar.go b/elements/basic/scrollbar.go index 4f9af08..8ced13a 100644 --- a/elements/basic/scrollbar.go +++ b/elements/basic/scrollbar.go @@ -49,7 +49,7 @@ func NewScrollBar (vertical bool) (element *ScrollBar) { } else { element.theme.Case = theme.C("basic", "scrollBarVertical") } - element.Core, element.core = core.NewCore(element.handleResize) + element.Core, element.core = core.NewCore(element, element.handleResize) element.updateMinimumSize() return } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index e333dbb..cf86e90 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -31,12 +31,12 @@ type ScrollContainer struct { func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { element = &ScrollContainer { } element.theme.Case = theme.C("basic", "scrollContainer") - element.Core, element.core = core.NewCore(element.redoAll) - element.Propagator = core.NewPropagator(element) + element.Core, element.core = core.NewCore(element, element.redoAll) + element.Propagator = core.NewPropagator(element, element.core) if horizontal { element.horizontal = NewScrollBar(false) - element.setChildEventHandlers(element.horizontal) + element.setUpChild(element.horizontal) element.horizontal.OnScroll (func (viewport image.Point) { if element.child != nil { element.child.ScrollTo(viewport) @@ -50,7 +50,7 @@ func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { } if vertical { element.vertical = NewScrollBar(true) - element.setChildEventHandlers(element.vertical) + element.setUpChild(element.vertical) element.vertical.OnScroll (func (viewport image.Point) { if element.child != nil { element.child.ScrollTo(viewport) @@ -72,13 +72,13 @@ func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { func (element *ScrollContainer) Adopt (child elements.Scrollable) { // disown previous child if it exists if element.child != nil { - element.clearChildEventHandlers(child) + element.disownChild(child) } // adopt new child element.child = child if child != nil { - element.setChildEventHandlers(child) + element.setUpChild(child) } element.updateEnabled() @@ -89,50 +89,47 @@ func (element *ScrollContainer) Adopt (child elements.Scrollable) { } } -func (element *ScrollContainer) setChildEventHandlers (child elements.Element) { - if child0, ok := child.(elements.Themeable); ok { - child0.SetTheme(element.theme.Theme) +func (element *ScrollContainer) setUpChild (child elements.Element) { + child.SetParent(element) + if child, ok := child.(elements.Themeable); ok { + child.SetTheme(element.theme.Theme) } - if child0, ok := child.(elements.Configurable); ok { - child0.SetConfig(element.config.Config) - } - child.OnDamage (func (region canvas.Canvas) { - element.core.DamageRegion(region.Bounds()) - }) - child.OnMinimumSizeChange (func () { - element.updateMinimumSize() - element.redoAll() - element.core.DamageAll() - }) - if child0, ok := child.(elements.Focusable); ok { - child0.OnFocusRequest (func () (granted bool) { - return element.childFocusRequestCallback(child0) - }) - child0.OnFocusMotionRequest ( - func (direction input.KeynavDirection) (granted bool) { - if element.onFocusMotionRequest == nil { return } - return element.onFocusMotionRequest(direction) - }) - } - if child0, ok := child.(elements.Scrollable); ok { - child0.OnScrollBoundsChange(element.childScrollBoundsChangeCallback) + if child, ok := child.(elements.Configurable); ok { + child.SetConfig(element.config.Config) } } -func (element *ScrollContainer) clearChildEventHandlers (child elements.Scrollable) { - child.DrawTo(nil, image.Rectangle { }) - child.OnDamage(nil) - child.OnMinimumSizeChange(nil) - child.OnScrollBoundsChange(nil) - if child0, ok := child.(elements.Focusable); ok { - child0.OnFocusRequest(nil) - child0.OnFocusMotionRequest(nil) - if child0.Focused() { - child0.HandleUnfocus() +func (element *ScrollContainer) disownChild (child elements.Scrollable) { + child.DrawTo(nil, image.Rectangle { }, nil) + child.SetParent(nil) + if child, ok := child.(elements.Focusable); ok { + if child.Focused() { + child.HandleUnfocus() } } } +// NotifyMinimumSizeChange notifies the container that the minimum size of a +// child element has changed. +func (element *ScrollContainer) NotifyMinimumSizeChange (child elements.Element) { + element.redoAll() + element.core.DamageAll() +} + +// NotifyScrollBoundsChange notifies the container that the scroll bounds or +// axes of a child have changed. +func (element *ScrollContainer) NotifyScrollBoundsChange (child elements.Scrollable) { + element.updateEnabled() + viewportBounds := element.child.ScrollViewportBounds() + contentBounds := element.child.ScrollContentBounds() + if element.horizontal != nil { + element.horizontal.SetBounds(contentBounds, viewportBounds) + } + if element.vertical != nil { + element.vertical.SetBounds(contentBounds, viewportBounds) + } +} + // SetTheme sets the element's theme. func (element *ScrollContainer) SetTheme (new theme.Theme) { if new == element.theme.Theme { return } @@ -157,18 +154,6 @@ func (element *ScrollContainer) HandleMouseScroll ( element.scrollChildBy(int(deltaX), int(deltaY)) } -func (element *ScrollContainer) OnFocusRequest (callback func () (granted bool)) { - element.onFocusRequest = callback - element.Propagator.OnFocusRequest(callback) -} - -func (element *ScrollContainer) OnFocusMotionRequest ( - callback func (direction input.KeynavDirection) (granted bool), -) { - element.onFocusMotionRequest = callback - element.Propagator.OnFocusMotionRequest(callback) -} - // CountChildren returns the amount of children contained within this element. func (element *ScrollContainer) CountChildren () (count int) { return 3 @@ -199,25 +184,25 @@ func (element *ScrollContainer) redoAll () { if !element.core.HasImage() { return } zr := image.Rectangle { } - if element.child != nil { element.child.DrawTo(nil, zr) } - if element.horizontal != nil { element.horizontal.DrawTo(nil, zr) } - if element.vertical != nil { element.vertical.DrawTo(nil, zr) } + if element.child != nil { element.child.DrawTo(nil, zr, nil) } + if element.horizontal != nil { element.horizontal.DrawTo(nil, zr, nil) } + if element.vertical != nil { element.vertical.DrawTo(nil, zr, nil) } childBounds, horizontalBounds, verticalBounds := element.layout() if element.child != nil { element.child.DrawTo ( canvas.Cut(element.core, childBounds), - childBounds) + childBounds, element.childDamageCallback) } if element.horizontal != nil { element.horizontal.DrawTo ( canvas.Cut(element.core, horizontalBounds), - horizontalBounds) + horizontalBounds, element.childDamageCallback) } if element.vertical != nil { element.vertical.DrawTo ( canvas.Cut(element.core, verticalBounds), - verticalBounds) + verticalBounds, element.childDamageCallback) } element.draw() } @@ -230,18 +215,8 @@ func (element *ScrollContainer) scrollChildBy (x, y int) { element.child.ScrollTo(scrollPoint) } -func (element *ScrollContainer) childFocusRequestCallback ( - child elements.Focusable, -) ( - granted bool, -) { - if element.onFocusRequest != nil && element.onFocusRequest() { - element.Propagator.HandleUnfocus() - element.Propagator.HandleFocus(input.KeynavDirectionNeutral) - return true - } else { - return false - } +func (element *ScrollContainer) childDamageCallback (region image.Rectangle) { + element.core.DamageRegion(region) } func (element *ScrollContainer) layout () ( @@ -308,18 +283,6 @@ func (element *ScrollContainer) updateMinimumSize () { element.core.SetMinimumSize(width, height) } -func (element *ScrollContainer) childScrollBoundsChangeCallback () { - element.updateEnabled() - viewportBounds := element.child.ScrollViewportBounds() - contentBounds := element.child.ScrollContentBounds() - if element.horizontal != nil { - element.horizontal.SetBounds(contentBounds, viewportBounds) - } - if element.vertical != nil { - element.vertical.SetBounds(contentBounds, viewportBounds) - } -} - func (element *ScrollContainer) updateEnabled () { horizontal, vertical := element.child.ScrollAxes() if element.horizontal != nil { diff --git a/elements/basic/slider.go b/elements/basic/slider.go index 6ad8ff1..fef9fe4 100644 --- a/elements/basic/slider.go +++ b/elements/basic/slider.go @@ -39,9 +39,9 @@ func NewSlider (value float64, vertical bool) (element *Slider) { } else { element.theme.Case = theme.C("basic", "sliderHorizontal") } - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.redo) + element.focusableControl = core.NewFocusableCore(element.core, element.redo) element.updateMinimumSize() return } diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index 4d710b3..4fa5013 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -20,7 +20,7 @@ type Spacer struct { func NewSpacer (line bool) (element *Spacer) { element = &Spacer { line: line } element.theme.Case = theme.C("basic", "spacer") - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.updateMinimumSize() return } diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 80491de..01462d2 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -33,9 +33,9 @@ func NewSwitch (text string, on bool) (element *Switch) { text: text, } element.theme.Case = theme.C("basic", "switch") - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.redo) + element.focusableControl = core.NewFocusableCore(element.core, element.redo) element.drawer.SetText([]rune(text)) element.updateMinimumSize() return diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index a76358f..601e270 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -6,6 +6,7 @@ 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/canvas" +import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/fixedutil" @@ -33,7 +34,6 @@ type TextBox struct { onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool) onChange func () - onScrollBoundsChange func () } // NewTextBox creates a new text box with the specified placeholder text, and @@ -42,9 +42,9 @@ type TextBox struct { func NewTextBox (placeholder, value string) (element *TextBox) { element = &TextBox { } element.theme.Case = theme.C("basic", "textBox") - element.Core, element.core = core.NewCore(element.handleResize) + element.Core, element.core = core.NewCore(element, element.handleResize) element.FocusableCore, - element.focusableControl = core.NewFocusableCore (func () { + element.focusableControl = core.NewFocusableCore (element.core, func () { if element.core.HasImage () { element.draw() element.core.DamageAll() @@ -60,8 +60,8 @@ func NewTextBox (placeholder, value string) (element *TextBox) { func (element *TextBox) handleResize () { element.scrollToCursor() element.draw() - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -187,9 +187,10 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) element.scrollToCursor() } - if (textChanged || scrollMemory != element.scroll) && - element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if (textChanged || scrollMemory != element.scroll) { + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) + } } if altered { @@ -274,8 +275,8 @@ func (element *TextBox) ScrollTo (position image.Point) { if element.scroll > maxPosition { element.scroll = maxPosition } element.redo() - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() + if parent, ok := element.core.Parent().(elements.ScrollableParent); ok { + parent.NotifyScrollBoundsChange(element) } } @@ -284,10 +285,6 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) { return true, false } -func (element *TextBox) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - func (element *TextBox) runOnChange () { if element.onChange != nil { element.onChange() From 275e113e3bd066b7da731e36f6b38132f179a77e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 01:42:07 -0400 Subject: [PATCH 06/17] Fun elements now conform to new API --- elements/fun/clock.go | 2 +- elements/fun/piano.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/elements/fun/clock.go b/elements/fun/clock.go index e2c3677..a9e9974 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -23,7 +23,7 @@ type AnalogClock struct { func NewAnalogClock (newTime time.Time) (element *AnalogClock) { element = &AnalogClock { } element.theme.Case = theme.C("fun", "clock") - element.Core, element.core = core.NewCore(element.draw) + element.Core, element.core = core.NewCore(element, element.draw) element.core.SetMinimumSize(64, 64) return } diff --git a/elements/fun/piano.go b/elements/fun/piano.go index e18cac8..60435f4 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -53,12 +53,12 @@ func NewPiano (low, high music.Octave) (element *Piano) { } element.theme.Case = theme.C("fun", "piano") - element.Core, element.core = core.NewCore (func () { + element.Core, element.core = core.NewCore (element, func () { element.recalculate() element.draw() }) element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.redo) + element.focusableControl = core.NewFocusableCore(element.core, element.redo) element.updateMinimumSize() return } From 1a6622464823fb81be73d542279fee3b3760bb6b Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 01:43:32 -0400 Subject: [PATCH 07/17] X backend window sets itself as parent (oops) --- backends/x/window.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backends/x/window.go b/backends/x/window.go index ecd4e86..5af14e2 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -103,6 +103,7 @@ func (window *window) Adopt (child elements.Element) { // adopt new child window.child = child + child.SetParent(window) if newChild, ok := child.(elements.Themeable); ok { newChild.SetTheme(window.theme) } From 2f60abdfa31de235a873ccc801a113c808a04e71 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 01:46:58 -0400 Subject: [PATCH 08/17] Core properly sets nil parent --- elements/core/core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elements/core/core.go b/elements/core/core.go index 2de8ff2..731aea1 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -58,7 +58,7 @@ func (core *Core) MinimumSize () (width, height int) { // overridden, unless you want to detect when the element is parented or // unparented. func (core *Core) SetParent (parent elements.Parent) { - if core.parent != nil { + if parent != nil && core.parent != nil { panic("core.SetParent: element already has a parent") } From ef325d5161083badb5c9b0ff74a492b35a6165b0 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 17:08:43 -0400 Subject: [PATCH 09/17] Found a flaw in the focusing model, rectifying. Still need to fix on X backend window, that will be in the next commit. --- elements/container.go | 10 +++++++++- elements/core/propagator.go | 37 +++++++++++++++++++++++++++++++++++++ elements/core/selectable.go | 3 +-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/elements/container.go b/elements/container.go index 1001e7c..ec09d8a 100644 --- a/elements/container.go +++ b/elements/container.go @@ -18,7 +18,15 @@ type FocusableParent interface { // keyboard focus. If the parent grants the request, the method will // return true and the child element should behave as if a HandleFocus // call was made. - RequestFocus (child Focusable, direction input.KeynavDirection) (granted bool) + RequestFocus (child Focusable) (granted bool) + + // RequestFocusMotion notifies the parent that a child element wants the + // focus to be moved to the next focusable element. + RequestFocusNext (child Focusable) + + // RequestFocusMotion notifies the parent that a child element wants the + // focus to be moved to the previous focusable element. + RequestFocusPrevious (child Focusable) } // FlexibleParent represents a parent that accounts for elements with diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 33ba869..76284d6 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -122,6 +122,43 @@ func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (acc return false } +// RequestFocus notifies the parent that a child element is requesting +// keyboard focus. If the parent grants the request, the method will +// return true and the child element should behave as if a HandleFocus +// call was made. +func (propagator *Propagator) RequestFocus ( + child elements.Focusable, +) ( + granted bool, +) { + // TODO implement this, and also implement it for the x backend window + if parent, ok := propagator.core.Parent().(elements.FocusableParent); ok { + if parent.RequestFocus(propagator) { + propagator.focused = true + granted = true + } + } + return +} + +// RequestFocusMotion notifies the parent that a child element wants the +// focus to be moved to the next focusable element. +func (propagator *Propagator) RequestFocusNext (child elements.Focusable) { + if !propagator.focused { return } + if parent, ok := propagator.core.Parent().(elements.FocusableParent); ok { + parent.RequestFocusNext(propagator) + } +} + +// RequestFocusMotion notifies the parent that a child element wants the +// focus to be moved to the previous focusable element. +func (propagator *Propagator) RequestFocusPrevious (child elements.Focusable) { + if !propagator.focused { return } + if parent, ok := propagator.core.Parent().(elements.FocusableParent); ok { + parent.RequestFocusPrevious(propagator) + } +} + // HandleDeselection causes this element to mark itself and all of its children // as unfocused. func (propagator *Propagator) HandleUnfocus () { diff --git a/elements/core/selectable.go b/elements/core/selectable.go index c8fc30e..903841a 100644 --- a/elements/core/selectable.go +++ b/elements/core/selectable.go @@ -44,8 +44,7 @@ func (core *FocusableCore) Focus () { parent := core.core.Parent() if parent, ok := parent.(elements.FocusableParent); ok && parent != nil { core.focused = parent.RequestFocus ( - core.core.Outer().(elements.Focusable), - input.KeynavDirectionNeutral) + core.core.Outer().(elements.Focusable)) } } From c1b3562d101ca3a5432ba82a2ff126da5137ff6e Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 23:47:13 -0400 Subject: [PATCH 10/17] It compiles --- backends/x/window.go | 45 ++++++++++++++++++++----------------- elements/container.go | 2 -- elements/core/propagator.go | 10 ++++----- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/backends/x/window.go b/backends/x/window.go index 5af14e2..92b2251 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -94,6 +94,30 @@ func (window *window) NotifyMinimumSizeChange (child elements.Element) { window.childMinimumSizeChangeCallback(child.MinimumSize()) } +func (window *window) RequestFocus ( + child elements.Focusable, +) ( + granted bool, +) { + return true +} + +func (window *window) RequestFocusNext (child elements.Focusable) { + if child, ok := window.child.(elements.Focusable); ok { + if !child.HandleFocus(input.KeynavDirectionForward) { + child.HandleUnfocus() + } + } +} + +func (window *window) RequestFocusPrevious (child elements.Focusable) { + if child, ok := window.child.(elements.Focusable); ok { + if !child.HandleFocus(input.KeynavDirectionBackward) { + child.HandleUnfocus() + } + } +} + func (window *window) Adopt (child elements.Element) { // disown previous child if window.child != nil { @@ -305,27 +329,6 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize return false } -func (window *window) childSelectionRequestCallback () (granted bool) { - if _, ok := window.child.(elements.Focusable); ok { - return true - } - return false -} - -func (window *window) childSelectionMotionRequestCallback ( - direction input.KeynavDirection, -) ( - granted bool, -) { - if child, ok := window.child.(elements.Focusable); ok { - if !child.HandleFocus(direction) { - child.HandleUnfocus() - } - return true - } - return true -} - func (window *window) pushRegion (region image.Rectangle) { if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) diff --git a/elements/container.go b/elements/container.go index ec09d8a..6b6235b 100644 --- a/elements/container.go +++ b/elements/container.go @@ -1,7 +1,5 @@ package elements -import "git.tebibyte.media/sashakoshka/tomo/input" - // Parent represents a type capable of containing child elements. type Parent interface { // NotifyMinimumSizeChange notifies the container that a child element's diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 76284d6..d0f7bc0 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -51,8 +51,7 @@ func (propagator *Propagator) Focus () { parent := propagator.core.Parent() if parent, ok := parent.(elements.FocusableParent); ok && parent != nil { propagator.focused = parent.RequestFocus ( - propagator.core.Outer().(elements.Focusable), - input.KeynavDirectionNeutral) + propagator.core.Outer().(elements.Focusable)) } } @@ -131,9 +130,8 @@ func (propagator *Propagator) RequestFocus ( ) ( granted bool, ) { - // TODO implement this, and also implement it for the x backend window if parent, ok := propagator.core.Parent().(elements.FocusableParent); ok { - if parent.RequestFocus(propagator) { + if parent.RequestFocus(propagator.core.Outer().(elements.Focusable)) { propagator.focused = true granted = true } @@ -146,7 +144,7 @@ func (propagator *Propagator) RequestFocus ( func (propagator *Propagator) RequestFocusNext (child elements.Focusable) { if !propagator.focused { return } if parent, ok := propagator.core.Parent().(elements.FocusableParent); ok { - parent.RequestFocusNext(propagator) + parent.RequestFocusNext(propagator.core.Outer().(elements.Focusable)) } } @@ -155,7 +153,7 @@ func (propagator *Propagator) RequestFocusNext (child elements.Focusable) { func (propagator *Propagator) RequestFocusPrevious (child elements.Focusable) { if !propagator.focused { return } if parent, ok := propagator.core.Parent().(elements.FocusableParent); ok { - parent.RequestFocusPrevious(propagator) + parent.RequestFocusPrevious(propagator.core.Outer().(elements.Focusable)) } } From 639baecee5dfd180382612a83a9e7a17f61d8f8c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 23:49:57 -0400 Subject: [PATCH 11/17] Propagator unfocuses children before focusing a new one --- elements/core/propagator.go | 1 + 1 file changed, 1 insertion(+) diff --git a/elements/core/propagator.go b/elements/core/propagator.go index d0f7bc0..7afb353 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -132,6 +132,7 @@ func (propagator *Propagator) RequestFocus ( ) { if parent, ok := propagator.core.Parent().(elements.FocusableParent); ok { if parent.RequestFocus(propagator.core.Outer().(elements.Focusable)) { + propagator.HandleUnfocus() propagator.focused = true granted = true } From 8aaa0179027d470605f5184c0764105a5bd0fbe1 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 23:56:00 -0400 Subject: [PATCH 12/17] Re-added OnScrollBoundsChange methods because they are useful --- elements/basic/documentContainer.go | 8 ++++++++ elements/basic/list.go | 9 ++++++++- elements/basic/textbox.go | 7 +++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/elements/basic/documentContainer.go b/elements/basic/documentContainer.go index 96d3558..a5ddda9 100644 --- a/elements/basic/documentContainer.go +++ b/elements/basic/documentContainer.go @@ -21,6 +21,8 @@ type DocumentContainer struct { config config.Wrapped theme theme.Wrapped + + onScrollBoundsChange func () } // NewDocumentContainer creates a new document container. @@ -255,6 +257,12 @@ func (element *DocumentContainer) ScrollTo (position image.Point) { } } +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *DocumentContainer) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + func (element *DocumentContainer) maxScrollHeight () (height int) { padding := element.theme.Padding(theme.PatternSunken) viewportHeight := element.Bounds().Dy() - padding.Vertical() diff --git a/elements/basic/list.go b/elements/basic/list.go index 45b7951..03d0bd6 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -30,7 +30,8 @@ type List struct { config config.Wrapped theme theme.Wrapped - onNoEntrySelected func () + onNoEntrySelected func () + onScrollBoundsChange func () } // NewList creates a new list element with the specified entries. @@ -240,6 +241,12 @@ func (element *List) OnNoEntrySelected (callback func ()) { element.onNoEntrySelected = callback } +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *List) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + // CountEntries returns the amount of entries in the list. func (element *List) CountEntries () (count int) { return len(element.entries) diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index 601e270..4646324 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -34,6 +34,7 @@ type TextBox struct { onKeyDown func (key input.Key, modifiers input.Modifiers) (handled bool) onChange func () + onScrollBoundsChange func () } // NewTextBox creates a new text box with the specified placeholder text, and @@ -241,6 +242,12 @@ func (element *TextBox) OnChange (callback func ()) { element.onChange = callback } +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *TextBox) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + // ScrollContentBounds returns the full content size of the element. func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) { bounds = element.valueDrawer.LayoutBounds() From 1239f4e03df4852a7e8adeff595a6596f8934b8c Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Wed, 15 Mar 2023 23:57:22 -0400 Subject: [PATCH 13/17] Made DocumentContainer satisfy FlexibleParent --- elements/basic/documentContainer.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/elements/basic/documentContainer.go b/elements/basic/documentContainer.go index a5ddda9..194488a 100644 --- a/elements/basic/documentContainer.go +++ b/elements/basic/documentContainer.go @@ -43,12 +43,6 @@ func (element *DocumentContainer) Adopt (child elements.Element) { if child0, ok := child.(elements.Configurable); ok { child0.SetConfig(element.config.Config) } - // if child0, ok := child.(elements.Flexible); ok { - // child0.OnFlexibleHeightChange (func () { - // element.redoAll() - // element.core.DamageAll() - // }) - // } // add child element.children = append (element.children, layouts.LayoutEntry { @@ -211,6 +205,16 @@ func (element *DocumentContainer) NotifyMinimumSizeChange (child elements.Elemen element.core.DamageAll() } +// NotifyFlexibleHeightChange notifies the parent that the parameters +// affecting a child's flexible height have changed. This method is +// expected to be called by flexible child element when their content +// changes. +func (element *DocumentContainer) NotifyFlexibleHeightChange (child elements.Flexible) { + element.redoAll() + element.core.DamageAll() +} + + // SetTheme sets the element's theme. func (element *DocumentContainer) SetTheme (new theme.Theme) { if new == element.theme.Theme { return } From 5ca9206f6591cfffa1d6fb139215d2e2ac2fd691 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 16 Mar 2023 00:24:40 -0400 Subject: [PATCH 14/17] DocumentContainer properly adopts children now --- elements/basic/documentContainer.go | 2 ++ elements/basic/list.go | 4 +--- elements/basic/scrollbar.go | 4 ++-- elements/basic/scrollcontainer.go | 2 +- elements/basic/slider.go | 4 ++-- elements/basic/switch.go | 3 --- elements/basic/textbox.go | 4 +--- elements/core/propagator.go | 6 +++--- elements/core/selectable.go | 2 +- 9 files changed, 13 insertions(+), 18 deletions(-) diff --git a/elements/basic/documentContainer.go b/elements/basic/documentContainer.go index 194488a..d82eaf5 100644 --- a/elements/basic/documentContainer.go +++ b/elements/basic/documentContainer.go @@ -49,6 +49,8 @@ func (element *DocumentContainer) Adopt (child elements.Element) { Element: child, }) + child.SetParent(element) + // refresh stale data element.reflectChildProperties() if element.core.HasImage() && !element.warping { diff --git a/elements/basic/list.go b/elements/basic/list.go index 03d0bd6..6ae823f 100644 --- a/elements/basic/list.go +++ b/elements/basic/list.go @@ -148,7 +148,7 @@ func (element *List) HandleMouseUp (x, y int, button input.Button) { element.pressed = false } -func (element *List) HandleMouseMove (x, y int) { +func (element *List) HandleMotion (x, y int) { if element.pressed { if element.selectUnderMouse(x, y) && element.core.HasImage() { element.draw() @@ -157,8 +157,6 @@ func (element *List) HandleMouseMove (x, y int) { } } -func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } - func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if !element.Enabled() { return } diff --git a/elements/basic/scrollbar.go b/elements/basic/scrollbar.go index 8ced13a..d554f80 100644 --- a/elements/basic/scrollbar.go +++ b/elements/basic/scrollbar.go @@ -115,13 +115,13 @@ func (element *ScrollBar) HandleMouseUp (x, y int, button input.Button) { } } -func (element *ScrollBar) HandleMouseMove (x, y int) { +func (element *ScrollBar) HandleMotion (x, y int) { if element.dragging { element.dragTo(image.Pt(x, y)) } } -func (element *ScrollBar) HandleMouseScroll (x, y int, deltaX, deltaY float64) { +func (element *ScrollBar) HandleScroll (x, y int, deltaX, deltaY float64) { if element.vertical { element.scrollBy(int(deltaY)) } else { diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index cf86e90..df02fde 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -147,7 +147,7 @@ func (element *ScrollContainer) SetConfig (new config.Config) { element.redoAll() } -func (element *ScrollContainer) HandleMouseScroll ( +func (element *ScrollContainer) HandleScroll ( x, y int, deltaX, deltaY float64, ) { diff --git a/elements/basic/slider.go b/elements/basic/slider.go index fef9fe4..7951af7 100644 --- a/elements/basic/slider.go +++ b/elements/basic/slider.go @@ -68,7 +68,7 @@ func (element *Slider) HandleMouseUp (x, y int, button input.Button) { element.redo() } -func (element *Slider) HandleMouseMove (x, y int) { +func (element *Slider) HandleMotion (x, y int) { if element.dragging { element.dragging = true element.value = element.valueFor(x, y) @@ -79,7 +79,7 @@ func (element *Slider) HandleMouseMove (x, y int) { } } -func (element *Slider) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } +func (element *Slider) HandleScroll (x, y int, deltaX, deltaY float64) { } func (element *Slider) HandleKeyDown (key input.Key, modifiers input.Modifiers) { switch key { diff --git a/elements/basic/switch.go b/elements/basic/switch.go index 01462d2..1133524 100644 --- a/elements/basic/switch.go +++ b/elements/basic/switch.go @@ -67,9 +67,6 @@ func (element *Switch) HandleMouseUp (x, y int, button input.Button) { } } -func (element *Switch) HandleMouseMove (x, y int) { } -func (element *Switch) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } - func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter { element.pressed = true diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index 4646324..393a0c7 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -80,7 +80,7 @@ func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { } } -func (element *TextBox) HandleMouseMove (x, y int) { +func (element *TextBox) HandleMotion (x, y int) { if !element.Enabled() { return } if element.dragging { @@ -115,8 +115,6 @@ func (element *TextBox) HandleMouseUp (x, y int, button input.Button) { } } -func (element *TextBox) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } - func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) { if element.onKeyDown != nil && element.onKeyDown(key, modifiers) { return diff --git a/elements/core/propagator.go b/elements/core/propagator.go index 7afb353..b6e8dc5 100644 --- a/elements/core/propagator.go +++ b/elements/core/propagator.go @@ -212,10 +212,10 @@ func (propagator *Propagator) HandleMouseUp (x, y int, button input.Button) { } } -// HandleMouseMove propagates the mouse event to the element that was last +// HandleMotion 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) { +func (propagator *Propagator) HandleMotion (x, y int) { handled := false for _, child := range propagator.drags { if child, ok := child.(elements.MotionTarget); ok { @@ -234,7 +234,7 @@ func (propagator *Propagator) HandleMouseMove (x, y int) { // HandleScroll propagates the mouse event to the element under the mouse // pointer. -func (propagator *Propagator) HandleMouseScroll (x, y int, deltaX, deltaY float64) { +func (propagator *Propagator) HandleScroll (x, y int, deltaX, deltaY float64) { child := propagator.childAt(image.Pt(x, y)) if child, ok := child.(elements.ScrollTarget); ok { child.HandleScroll(x, y, deltaX, deltaY) diff --git a/elements/core/selectable.go b/elements/core/selectable.go index 903841a..37947aa 100644 --- a/elements/core/selectable.go +++ b/elements/core/selectable.go @@ -42,7 +42,7 @@ func (core *FocusableCore) Focused () (focused bool) { func (core *FocusableCore) Focus () { if !core.enabled || core.focused { return } parent := core.core.Parent() - if parent, ok := parent.(elements.FocusableParent); ok && parent != nil { + if parent, ok := parent.(elements.FocusableParent); ok { core.focused = parent.RequestFocus ( core.core.Outer().(elements.Focusable)) } From bffdb000ed07d4aff9e36bcaad965b916d1b3fe9 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 16 Mar 2023 00:25:36 -0400 Subject: [PATCH 15/17] Piano element handles motion events --- elements/fun/piano.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/elements/fun/piano.go b/elements/fun/piano.go index 60435f4..f7ca895 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -88,13 +88,11 @@ func (element *Piano) HandleMouseUp (x, y int, button input.Button) { element.redo() } -func (element *Piano) HandleMouseMove (x, y int) { +func (element *Piano) HandleMotion (x, y int) { if element.pressed == nil { return } element.pressUnderMouseCursor(image.Pt(x, y)) } -func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } - func (element *Piano) pressUnderMouseCursor (point image.Point) { // find out which note is being pressed newKey := (*pianoKey)(nil) From 40aa1a788bede28c396b236e731e1dc82e6b2a3a Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 16 Mar 2023 00:26:54 -0400 Subject: [PATCH 16/17] Renamed some oddly named files --- elements/core/{selectable.go => focusable.go} | 0 elements/{container.go => parent.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename elements/core/{selectable.go => focusable.go} (100%) rename elements/{container.go => parent.go} (100%) diff --git a/elements/core/selectable.go b/elements/core/focusable.go similarity index 100% rename from elements/core/selectable.go rename to elements/core/focusable.go diff --git a/elements/container.go b/elements/parent.go similarity index 100% rename from elements/container.go rename to elements/parent.go From 0ebf0bc8145598403b2165207185d189ba24a843 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Thu, 16 Mar 2023 00:30:59 -0400 Subject: [PATCH 17/17] Raycaster example now works --- examples/raycaster/game.go | 8 ++++++-- examples/raycaster/raycaster.go | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/raycaster/game.go b/examples/raycaster/game.go index e0413c1..2c66cf3 100644 --- a/examples/raycaster/game.go +++ b/examples/raycaster/game.go @@ -31,7 +31,11 @@ func NewGame (world World, textures Textures) (game *Game) { return } -func (game *Game) DrawTo (canvas canvas.Canvas, bounds image.Rectangle) { +func (game *Game) DrawTo ( + canvas canvas.Canvas, + bounds image.Rectangle, + onDamage func (image.Rectangle), +) { if canvas == nil { select { case game.stopChan <- true: @@ -41,7 +45,7 @@ func (game *Game) DrawTo (canvas canvas.Canvas, bounds image.Rectangle) { game.running = true go game.run() } - game.Raycaster.DrawTo(canvas, bounds) + game.Raycaster.DrawTo(canvas, bounds, onDamage) } func (game *Game) Stamina () float64 { diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go index 3ccd703..54ee19d 100644 --- a/examples/raycaster/raycaster.go +++ b/examples/raycaster/raycaster.go @@ -49,9 +49,9 @@ func NewRaycaster (world World, textures Textures) (element *Raycaster) { textures: textures, renderDistance: 8, } - element.Core, element.core = core.NewCore(element.drawAll) + element.Core, element.core = core.NewCore(element, element.drawAll) element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.Draw) + element.focusableControl = core.NewFocusableCore(element.core, element.Draw) element.core.SetMinimumSize(64, 64) return }