diff --git a/backends/x/event.go b/backends/x/event.go index 8e8c1d5..5b71f77 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -122,7 +122,7 @@ func (window *Window) handleButtonPress ( sum := scrollSum { } sum.add(buttonEvent.Detail) window.compressScrollSum(buttonEvent, &sum) - child.HandleScroll ( + child.HandleMouseScroll ( int(buttonEvent.EventX), int(buttonEvent.EventY), float64(sum.x), float64(sum.y)) diff --git a/backends/x/window.go b/backends/x/window.go index d065357..0a9e4cb 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -76,26 +76,39 @@ func (backend *Backend) NewWindow ( } func (window *Window) Adopt (child tomo.Element) { + // disown previous child if window.child != nil { - window.child.SetParentHooks (tomo.ParentHooks { }) - if previousChild, ok := window.child.(tomo.Selectable); ok { - if previousChild.Selected() { - previousChild.HandleDeselection() - } + window.child.OnDamage(nil) + window.child.OnMinimumSizeChange(nil) + } + if previousChild, ok := window.child.(tomo.Flexible); ok { + previousChild.OnFlexibleHeightChange(nil) + } + if previousChild, ok := window.child.(tomo.Selectable); ok { + previousChild.OnSelectionRequest(nil) + previousChild.OnSelectionMotionRequest(nil) + if previousChild.Selected() { + previousChild.HandleDeselection() } } + + // adopt new child window.child = child - if child != nil { - child.SetParentHooks (tomo.ParentHooks { - Draw: window.childDrawCallback, - MinimumSizeChange: window.childMinimumSizeChangeCallback, - FlexibleHeightChange: window.resizeChildToFit, - SelectionRequest: window.childSelectionRequestCallback, - }) - - window.resizeChildToFit() + if newChild, ok := child.(tomo.Flexible); ok { + newChild.OnFlexibleHeightChange(window.resizeChildToFit) + } + if newChild, ok := child.(tomo.Selectable); ok { + newChild.OnSelectionRequest(window.childSelectionRequestCallback) + } + if child != nil { + child.OnDamage(window.childDrawCallback) + child.OnMinimumSizeChange (func () { + window.childMinimumSizeChangeCallback ( + child.MinimumSize()) + }) + window.resizeChildToFit() + window.childMinimumSizeChangeCallback(child.MinimumSize()) } - window.childMinimumSizeChangeCallback(child.MinimumSize()) } func (window *Window) Child () (child tomo.Element) { @@ -204,7 +217,7 @@ func (window *Window) redrawChildEntirely () { func (window *Window) resizeChildToFit () { window.skipChildDrawCallback = true if child, ok := window.child.(tomo.Flexible); ok { - minimumHeight := child.MinimumHeightFor(window.metrics.width) + minimumHeight := child.FlexibleHeightFor(window.metrics.width) minimumWidth, _ := child.MinimumSize() icccm.WmNormalHintsSet ( diff --git a/element.go b/element.go index c2a3397..916f23a 100644 --- a/element.go +++ b/element.go @@ -2,96 +2,6 @@ package tomo import "image" -// ParentHooks is a struct that contains callbacks that let child elements send -// information to their parent element without the child element knowing -// anything about the parent element or containing any reference to it. When a -// parent element adopts a child element, it must set these callbacks. They are -// allowed to be nil. -type ParentHooks struct { - // Draw is called when a part of the child element's surface is updated. - // The updated region will be passed to the callback as a sub-image. - Draw func (region Canvas) - - // MinimumSizeChange is called when the child element's minimum width - // and/or height changes. When this function is called, the element will - // have already been resized and there is no need to send it a resize - // event. - MinimumSizeChange func (width, height int) - - // FlexibleHeightChange is called when the parameters affecting the - // element's felxible height have changed. - FlexibleHeightChange func () - - // ContentBoundsChange is called by scrollable elements when the - // element's content bounds have changed. When this function is called, - // the element will have already done any drawing necessary. This is - // only intended for updating things like scrollbar positions. - ContentBoundsChange func () - - // SelectionRequest is called when the child element element wants - // itself to be selected. If the parent element chooses to grant the - // request, it must send the child element a selection event and return - // true. - SelectionRequest func () (granted bool) - - // SelectionMotionRequest is called when the child element wants the - // parent element to select the previous/next element in relation to it. - SelectionMotionRequest func (direction SelectionDirection) (granted bool) -} - -// RunDraw runs the Draw hook if it is not nil. If it is nil, it does nothing. -func (hooks ParentHooks) RunDraw (region Canvas) { - if hooks.Draw != nil { - hooks.Draw(region) - } -} - -// RunMinimumSizeChange runs the MinimumSizeChange hook if it is not nil. If it -// is nil, it does nothing. -func (hooks ParentHooks) RunMinimumSizeChange (width, height int) { - if hooks.MinimumSizeChange != nil { - hooks.MinimumSizeChange(width, height) - } -} - -// RunFlexibleHeightChange runs the ExpandingHeightChange hook if it is not -// nil. If it is nil, it does nothing. -func (hooks ParentHooks) RunFlexibleHeightChange () { - if hooks.FlexibleHeightChange != nil { - hooks.FlexibleHeightChange() - } -} - -// RunSelectionRequest runs the SelectionRequest hook if it is not nil. If it is -// nil, it does nothing. -func (hooks ParentHooks) RunSelectionRequest () (granted bool) { - if hooks.SelectionRequest != nil { - granted = hooks.SelectionRequest() - } - return -} - -// RunSelectionMotionRequest runs the SelectionMotionRequest hook if it is not -// nil. If it is nil, it does nothing. -func (hooks ParentHooks) RunSelectionMotionRequest ( - direction SelectionDirection, -) ( - granted bool, -) { - if hooks.SelectionMotionRequest != nil { - granted = hooks.SelectionMotionRequest(direction) - } - return -} - -// RunContentBoundsChange runs the ContentBoundsChange hook if it is not nil. If -// it is nil, it does nothing. -func (hooks ParentHooks) RunContentBoundsChange () { - if hooks.ContentBoundsChange != nil { - hooks.ContentBoundsChange() - } -} - // Element represents a basic on-screen object. type Element interface { // Element must implement the Canvas interface. Elements should start @@ -109,11 +19,13 @@ type Element interface { // element's parent. Resize (width, height int) - // SetParentHooks gives the element callbacks that let it send - // information to its parent element without it knowing anything about - // the parent element or containing any reference to it. When a parent - // element adopts a child element, it must set these callbacks. - SetParentHooks (callbacks ParentHooks) + // 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)) + + // OnMinimumSizeChange sets a function to be called when the element's + // minimum size is changed. + OnMinimumSizeChange (callback func ()) } // SelectionDirection represents a keyboard navigation direction. @@ -156,6 +68,18 @@ type Selectable interface { // HandleDeselection causes this element to mark itself and all of its // children as deselected. HandleDeselection () + + // OnSelectionRequest sets a function to be called when this element + // wants its parent element to select it. Parent elements should return + // true if the request was granted, and false if it was not. + OnSelectionRequest (func () (granted bool)) + + // OnSelectionMotionRequest sets a function to be called when this + // element wants its parent element to select the element behind or in + // front of it, depending on the specified direction. Parent elements + // should return true if the request was granted, and false if it was + // not. + OnSelectionMotionRequest (func (SelectionDirection) (granted bool)) } // KeyboardTarget represents an element that can receive keyboard input. @@ -196,7 +120,7 @@ type MouseTarget interface { // HandleScroll is called when the mouse is scrolled. The X and Y // direction of the scroll event are passed as deltaX and deltaY. - HandleScroll (x, y int, deltaX, deltaY float64) + HandleMouseScroll (x, y int, deltaX, deltaY float64) } // Flexible represents an element who's preferred minimum height can change in @@ -204,10 +128,10 @@ type MouseTarget interface { type Flexible interface { Element - // HeightForWidth returns what the element's minimum height would be if - // resized to a specified width. This does not actually alter the state - // of the element in any way, but it may perform significant work, so it - // should be called sparingly. + // FlexibleHeightFor returns what the element's minimum height would be + // if resized to a specified width. This does not actually alter the + // state of the element in any way, but it may perform significant work, + // so it should be called sparingly. // // It is reccomended that parent containers check for this interface and // take this method's value into account in order to support things like @@ -217,7 +141,11 @@ 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. - MinimumHeightFor (width int) (height int) + 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 ()) } // Scrollable represents an element that can be scrolled. It acts as a viewport @@ -238,4 +166,8 @@ 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 or ScrollViewportBounds are changed. + OnScrollBoundsChange (callback func ()) } diff --git a/elements/basic/button.go b/elements/basic/button.go index bb254de..06c7edd 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -14,10 +14,13 @@ type Button struct { pressed bool enabled bool selected bool - onClick func () text string drawer artist.TextDrawer + + onClick func () + onSelectionRequest func () (granted bool) + onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) } // NewButton creates a new button with the specified label text. @@ -41,7 +44,7 @@ func (element *Button) HandleMouseDown (x, y int, button tomo.Button) { element.pressed = true if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -50,7 +53,7 @@ func (element *Button) HandleMouseUp (x, y int, button tomo.Button) { element.pressed = false if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } within := image.Point { x, y }. @@ -63,7 +66,7 @@ func (element *Button) HandleMouseUp (x, y int, button tomo.Button) { } func (element *Button) HandleMouseMove (x, y int) { } -func (element *Button) HandleScroll (x, y int, deltaX, deltaY float64) { } +func (element *Button) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Button) HandleKeyDown ( key tomo.Key, @@ -75,7 +78,7 @@ func (element *Button) HandleKeyDown ( element.pressed = true if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } } @@ -85,7 +88,7 @@ func (element *Button) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { element.pressed = false if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } if !element.enabled { return } if element.onClick != nil { @@ -100,7 +103,9 @@ func (element *Button) Selected () (selected bool) { func (element *Button) Select () { if !element.enabled { return } - element.core.RequestSelection() + if element.onSelectionRequest != nil { + element.onSelectionRequest() + } } func (element *Button) HandleSelection ( @@ -117,7 +122,7 @@ func (element *Button) HandleSelection ( element.selected = true if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } return true } @@ -126,10 +131,20 @@ func (element *Button) HandleDeselection () { element.selected = false if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } +func (element *Button) OnSelectionRequest (callback func () (granted bool)) { + element.onSelectionRequest = callback +} + +func (element *Button) OnSelectionMotionRequest ( + callback func (direction tomo.SelectionDirection) (granted bool), +) { + element.onSelectionMotionRequest = callback +} + // OnClick sets the function to be called when the button is clicked. func (element *Button) OnClick (callback func ()) { element.onClick = callback @@ -141,7 +156,7 @@ func (element *Button) SetEnabled (enabled bool) { element.enabled = enabled if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -157,7 +172,7 @@ func (element *Button) SetText (text string) { theme.Padding() * 2 + textBounds.Dy()) if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } diff --git a/elements/basic/checkbox.go b/elements/basic/checkbox.go index 8c9422a..27f85d1 100644 --- a/elements/basic/checkbox.go +++ b/elements/basic/checkbox.go @@ -15,10 +15,13 @@ type Checkbox struct { checked bool enabled bool selected bool - onClick func () text string drawer artist.TextDrawer + + onClick func () + onSelectionRequest func () (granted bool) + onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) } // NewCheckbox creates a new cbeckbox with the specified label text. @@ -41,7 +44,7 @@ func (element *Checkbox) HandleMouseDown (x, y int, button tomo.Button) { element.pressed = true if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -57,7 +60,7 @@ func (element *Checkbox) HandleMouseUp (x, y int, button tomo.Button) { if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } if within && element.onClick != nil { element.onClick() @@ -76,7 +79,7 @@ func (element *Checkbox) HandleKeyDown ( element.pressed = true if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } } @@ -87,7 +90,7 @@ func (element *Checkbox) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { element.checked = !element.checked if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } if element.onClick != nil { element.onClick() @@ -102,7 +105,10 @@ func (element *Checkbox) Selected () (selected bool) { // Select requests that this element be selected. func (element *Checkbox) Select () { - element.core.RequestSelection() + if !element.enabled { return } + if element.onSelectionRequest != nil { + element.onSelectionRequest() + } } func (element *Checkbox) HandleSelection ( @@ -119,7 +125,7 @@ func (element *Checkbox) HandleSelection ( element.selected = true if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } return true } @@ -128,10 +134,20 @@ func (element *Checkbox) HandleDeselection () { element.selected = false if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } +func (element *Checkbox) OnSelectionRequest (callback func () (granted bool)) { + element.onSelectionRequest = callback +} + +func (element *Checkbox) OnSelectionMotionRequest ( + callback func (direction tomo.SelectionDirection) (granted bool), +) { + element.onSelectionMotionRequest = callback +} + // OnClick sets the function to be called when the checkbox is toggled. func (element *Checkbox) OnClick (callback func ()) { element.onClick = callback @@ -148,7 +164,7 @@ func (element *Checkbox) SetEnabled (enabled bool) { element.enabled = enabled if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -164,7 +180,7 @@ func (element *Checkbox) SetText (text string) { textBounds.Dy()) if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } diff --git a/elements/basic/container.go b/elements/basic/container.go index 1e4e785..f494192 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -19,6 +19,10 @@ type Container struct { selected bool selectable bool flexible bool + + onSelectionRequest func () (granted bool) + onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) + onFlexibleHeightChange func () } // NewContainer creates a new container. @@ -35,7 +39,7 @@ func (element *Container) SetLayout (layout tomo.Layout) { if element.core.HasImage() { element.recalculate() element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -43,38 +47,38 @@ func (element *Container) SetLayout (layout tomo.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 tomo.Element, expand bool) { - child.SetParentHooks (tomo.ParentHooks { - Draw: func (region tomo.Canvas) { - element.drawChildRegion(child, region) - }, - MinimumSizeChange: func (int, int) { - element.updateMinimumSize() - }, - FlexibleHeightChange: element.updateMinimumSize, - SelectionRequest: func () (granted bool) { - child, selectable := child.(tomo.Selectable) - if !selectable { return } - return element.childSelectionRequestCallback(child) - }, - SelectionMotionRequest: func ( - direction tomo.SelectionDirection, - ) ( - granted bool, - ) { - return element.core.RequestSelectionMotion(direction) - }, + // set event handlers + child.OnDamage (func (region tomo.Canvas) { + element.drawChildRegion(child, region) }) + child.OnMinimumSizeChange(element.updateMinimumSize) + if child0, ok := child.(tomo.Flexible); ok { + child0.OnFlexibleHeightChange(element.updateMinimumSize) + } + if child0, ok := child.(tomo.Selectable); ok { + child0.OnSelectionRequest (func () (granted bool) { + return element.childSelectionRequestCallback(child0) + }) + child0.OnSelectionMotionRequest ( + func (direction tomo.SelectionDirection) (granted bool) { + if element.onSelectionMotionRequest == nil { return } + return element.onSelectionMotionRequest(direction) + }) + } + + // add child element.children = append (element.children, tomo.LayoutEntry { Element: child, Expand: expand, }) + // refresh stale data element.updateMinimumSize() element.reflectChildProperties() if element.core.HasImage() && !element.warping { element.recalculate() element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -97,7 +101,7 @@ func (element *Container) Warp (callback func ()) { if element.core.HasImage() { element.recalculate() element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -106,7 +110,7 @@ func (element *Container) Warp (callback func ()) { func (element *Container) Disown (child tomo.Element) { for index, entry := range element.children { if entry.Element == child { - entry.SetParentHooks(tomo.ParentHooks { }) + element.clearChildEventHandlers(entry.Element) element.children = append ( element.children[:index], element.children[index + 1:]...) @@ -119,7 +123,22 @@ func (element *Container) Disown (child tomo.Element) { if element.core.HasImage() && !element.warping { element.recalculate() element.draw() - element.core.PushAll() + element.core.DamageAll() + } +} + +func (element *Container) clearChildEventHandlers (child tomo.Element) { + child.OnDamage(nil) + child.OnMinimumSizeChange(nil) + if child0, ok := child.(tomo.Selectable); ok { + child0.OnSelectionRequest(nil) + child0.OnSelectionMotionRequest(nil) + if child0.Selected() { + child0.HandleDeselection() + } + } + if child0, ok := child.(tomo.Flexible); ok { + child0.OnFlexibleHeightChange(nil) } } @@ -132,7 +151,7 @@ func (element *Container) DisownAll () { if element.core.HasImage() && !element.warping { element.recalculate() element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -209,11 +228,11 @@ func (element *Container) HandleMouseMove (x, y int) { } } -func (element *Container) HandleScroll (x, y int, deltaX, deltaY float64) { +func (element *Container) HandleMouseScroll (x, y int, deltaX, deltaY float64) { child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget) if !handlesMouse { return } childPosition := element.childPosition(child) - child.HandleScroll(x - childPosition.X, y - childPosition.Y, deltaX, deltaY) + child.HandleMouseScroll(x - childPosition.X, y - childPosition.Y, deltaX, deltaY) } func (element *Container) HandleKeyDown ( @@ -245,7 +264,9 @@ func (element *Container) Selected () (selected bool) { } func (element *Container) Select () { - element.core.RequestSelection() + if element.onSelectionRequest != nil { + element.onSelectionRequest() + } } func (element *Container) HandleSelection (direction tomo.SelectionDirection) (ok bool) { @@ -300,8 +321,12 @@ func (element *Container) HandleSelection (direction tomo.SelectionDirection) (o return false } -func (element *Container) MinimumHeightFor (width int) (height int) { - return element.layout.MinimumHeightFor(element.children, width) +func (element *Container) FlexibleHeightFor (width int) (height int) { + return element.layout.FlexibleHeightFor(element.children, width) +} + +func (element *Container) OnFlexibleHeightChange (callback func ()) { + element.onFlexibleHeightChange = callback } func (element *Container) HandleDeselection () { @@ -312,6 +337,16 @@ func (element *Container) HandleDeselection () { }) } +func (element *Container) OnSelectionRequest (callback func () (granted bool)) { + element.onSelectionRequest = callback +} + +func (element *Container) OnSelectionMotionRequest ( + callback func (direction tomo.SelectionDirection) (granted bool), +) { + element.onSelectionMotionRequest = callback +} + func (element *Container) forSelected (callback func (child tomo.Selectable) bool) { for _, entry := range element.children { child, selectable := entry.Element.(tomo.Selectable) @@ -379,7 +414,7 @@ func (element *Container) childSelectionRequestCallback ( ) ( granted bool, ) { - if element.core.RequestSelection() { + if element.onSelectionRequest != nil && element.onSelectionRequest() { element.forSelected (func (child tomo.Selectable) bool { child.HandleDeselection() return true @@ -394,7 +429,7 @@ func (element *Container) childSelectionRequestCallback ( func (element *Container) updateMinimumSize () { width, height := element.layout.MinimumSize(element.children) if element.flexible { - height = element.layout.MinimumHeightFor(element.children, width) + height = element.layout.FlexibleHeightFor(element.children, width) } element.core.SetMinimumSize(width, height) } @@ -422,7 +457,7 @@ func (element *Container) drawChildRegion (child tomo.Element, region tomo.Canva for _, entry := range element.children { if entry.Element == child { artist.Paste(element.core, region, entry.Position) - element.core.PushRegion ( + element.core.DamageRegion ( region.Bounds().Add(entry.Position)) break } diff --git a/elements/basic/label.go b/elements/basic/label.go index 65ce585..7c2e2f0 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -13,6 +13,8 @@ type Label struct { wrap bool text string drawer artist.TextDrawer + + onFlexibleHeightChange func () } // NewLabel creates a new label. If wrap is set to true, the text inside will be @@ -38,9 +40,9 @@ func (element *Label) Resize (width, height int) { return } -// MinimumHeightFor returns the reccomended height for this element based on the -// given width in order to allow the text to wrap properly. -func (element *Label) MinimumHeightFor (width int) (height int) { +// FlexibleHeightFor returns the reccomended height for this element based on +// the given width in order to allow the text to wrap properly. +func (element *Label) FlexibleHeightFor (width int) (height int) { if element.wrap { return element.drawer.ReccomendedHeightFor(width) } else { @@ -49,6 +51,12 @@ func (element *Label) MinimumHeightFor (width int) (height int) { } } +// OnFlexibleHeightChange sets a function to be called when the parameters +// affecting this element's flexible height are changed. +func (element *Label) OnFlexibleHeightChange (callback func ()) { + element.onFlexibleHeightChange = callback +} + // SetText sets the label's text. func (element *Label) SetText (text string) { if element.text == text { return } @@ -59,7 +67,7 @@ func (element *Label) SetText (text string) { if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -78,7 +86,7 @@ func (element *Label) SetWrap (wrap bool) { if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -88,7 +96,9 @@ func (element *Label) updateMinimumSize () { if em < 1 { em = theme.Padding() } element.core.SetMinimumSize ( em, element.drawer.LineHeight().Round()) - element.core.NotifyFlexibleHeightChange() + if element.onFlexibleHeightChange != nil { + element.onFlexibleHeightChange() + } } else { bounds := element.drawer.LayoutBounds() element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) diff --git a/elements/basic/progressbar.go b/elements/basic/progressbar.go index 6ac9778..4bef1b0 100644 --- a/elements/basic/progressbar.go +++ b/elements/basic/progressbar.go @@ -34,7 +34,7 @@ func (element *ProgressBar) SetProgress (progress float64) { element.progress = progress if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } diff --git a/elements/basic/scrollcontainer.go b/elements/basic/scrollcontainer.go index a48adab..57ae279 100644 --- a/elements/basic/scrollcontainer.go +++ b/elements/basic/scrollcontainer.go @@ -23,6 +23,9 @@ type ScrollContainer struct { enabled bool bounds image.Rectangle } + + onSelectionRequest func () (granted bool) + onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) } // NewScrollContainer creates a new scroll container with the specified scroll @@ -52,23 +55,20 @@ func (element *ScrollContainer) Resize (width, height int) { func (element *ScrollContainer) Adopt (child tomo.Scrollable) { // disown previous child if it exists if element.child != nil { - element.child.SetParentHooks (tomo.ParentHooks { }) - if previousChild, ok := element.child.(tomo.Selectable); ok { - if previousChild.Selected() { - previousChild.HandleDeselection() - } - } + element.clearChildEventHandlers(child) } // adopt new child element.child = child if child != nil { - child.SetParentHooks (tomo.ParentHooks { - // Draw: window.childDrawCallback, - // MinimumSizeChange: window.childMinimumSizeChangeCallback, - // FlexibleHeightChange: window.resizeChildToFit, - // SelectionRequest: window.childSelectionRequestCallback, - }) + child.OnDamage(element.childDamageCallback) + child.OnMinimumSizeChange(element.updateMinimumSize) + if newChild, ok := child.(tomo.Selectable); ok { + newChild.OnSelectionRequest ( + element.childSelectionRequestCallback) + newChild.OnSelectionMotionRequest ( + element.childSelectionMotionRequestCallback) + } // TODO: somehow inform the core that we do not in fact want to // redraw the element. @@ -84,6 +84,104 @@ func (element *ScrollContainer) Adopt (child tomo.Scrollable) { } } +func (element *ScrollContainer) HandleKeyDown ( + key tomo.Key, + modifiers tomo.Modifiers, + repeated bool, +) { + if child, ok := element.child.(tomo.KeyboardTarget); ok { + child.HandleKeyDown(key, modifiers, repeated) + } +} + +func (element *ScrollContainer) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) { + if child, ok := element.child.(tomo.KeyboardTarget); ok { + child.HandleKeyUp(key, modifiers) + } +} + +func (element *ScrollContainer) Selected () (selected bool) { + return element.selected +} + +func (element *ScrollContainer) Select () { + if element.onSelectionRequest != nil { + element.onSelectionRequest() + } +} + +func (element *ScrollContainer) HandleSelection ( + direction tomo.SelectionDirection, +) ( + accepted bool, +) { + if child, ok := element.child.(tomo.Selectable); ok { + element.selected = true + return child.HandleSelection(direction) + } else { + element.selected = false + return false + } +} + +func (element *ScrollContainer) HandleDeselection () { + if child, ok := element.child.(tomo.Selectable); ok { + child.HandleDeselection() + } + element.selected = false +} + +func (element *ScrollContainer) OnSelectionRequest (callback func () (granted bool)) { + element.onSelectionRequest = callback +} + +func (element *ScrollContainer) OnSelectionMotionRequest ( + callback func (direction tomo.SelectionDirection) (granted bool), +) { + element.onSelectionMotionRequest = callback +} + +func (element *ScrollContainer) childDamageCallback (region tomo.Canvas) { + element.core.DamageRegion(artist.Paste(element, region, image.Point { })) +} + +func (element *ScrollContainer) childSelectionRequestCallback () (granted bool) { + child, ok := element.child.(tomo.Selectable) + if !ok { return false } + if element.onSelectionRequest != nil && element.onSelectionRequest() { + child.HandleSelection(tomo.SelectionDirectionNeutral) + return true + } else { + return false + } +} + +func (element *ScrollContainer) childSelectionMotionRequestCallback ( + direction tomo.SelectionDirection, +) ( + granted bool, +) { + if element.onSelectionMotionRequest == nil { + return + } + return element.onSelectionMotionRequest(direction) +} + +func (element *ScrollContainer) clearChildEventHandlers (child tomo.Element) { + child.OnDamage(nil) + child.OnMinimumSizeChange(nil) + if child0, ok := child.(tomo.Selectable); ok { + child0.OnSelectionRequest(nil) + child0.OnSelectionMotionRequest(nil) + if child0.Selected() { + child0.HandleDeselection() + } + } + if child0, ok := child.(tomo.Flexible); ok { + child0.OnFlexibleHeightChange(nil) + } +} + func (element *ScrollContainer) recalculate () { horizontal := &element.horizontal vertical := &element.vertical diff --git a/elements/basic/spacer.go b/elements/basic/spacer.go index 8456088..7cf7844 100644 --- a/elements/basic/spacer.go +++ b/elements/basic/spacer.go @@ -34,7 +34,7 @@ func (element *Spacer) SetLine (line bool) { element.line = line if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } diff --git a/elements/basic/textbox.go b/elements/basic/textbox.go index 722dc76..537b742 100644 --- a/elements/basic/textbox.go +++ b/elements/basic/textbox.go @@ -25,6 +25,9 @@ type TextBox struct { onKeyDown func (tomo.Key, tomo.Modifiers, bool) (bool) onChange func () + onSelectionRequest func () (granted bool) + onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool) + onScrollBoundsChange func () } // NewTextBox creates a new text box with the specified placeholder text, and @@ -119,7 +122,7 @@ func (element *TextBox) HandleKeyDown ( if altered && element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -130,7 +133,9 @@ func (element *TextBox) Selected () (selected bool) { } func (element *TextBox) Select () { - element.core.RequestSelection() + if element.onSelectionRequest != nil { + element.onSelectionRequest() + } } func (element *TextBox) HandleSelection ( @@ -147,7 +152,7 @@ func (element *TextBox) HandleSelection ( element.selected = true if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } return true } @@ -156,16 +161,26 @@ func (element *TextBox) HandleDeselection () { element.selected = false if element.core.HasImage() { element.draw() - element.core.PushAll() + element.core.DamageAll() } } +func (element *TextBox) OnSelectionRequest (callback func () (granted bool)) { + element.onSelectionRequest = callback +} + +func (element *TextBox) OnSelectionMotionRequest ( + callback func (direction tomo.SelectionDirection) (granted bool), +) { + element.onSelectionMotionRequest = callback +} + func (element *TextBox) SetEnabled (enabled bool) { if element.enabled == enabled { return } element.enabled = enabled if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -178,7 +193,7 @@ func (element *TextBox) SetPlaceholder (placeholder string) { element.updateMinimumSize() if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -195,7 +210,7 @@ func (element *TextBox) SetValue (text string) { if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() } } @@ -246,9 +261,11 @@ func (element *TextBox) ScrollTo (position image.Point) { if element.core.HasImage () { element.draw() - element.core.PushAll() + element.core.DamageAll() + } + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() } - element.core.NotifyContentBoundsChange() } // ScrollAxes returns the supported axes for scrolling. @@ -256,6 +273,10 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) { return true, false } +func (element *TextBox) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() element.core.SetMinimumSize ( @@ -286,7 +307,9 @@ func (element *TextBox) scrollToCursor () { element.scroll -= minX - cursorPosition.X if element.scroll < 0 { element.scroll = 0 } } - element.core.NotifyContentBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } } func (element *TextBox) draw () { diff --git a/elements/core/core.go b/elements/core/core.go index 5bf81e9..49de881 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -17,7 +17,9 @@ type Core struct { selectable bool selected bool - hooks tomo.ParentHooks + + onMinimumSizeChange func () + onDamage func (region tomo.Canvas) } // NewCore creates a new element core and its corresponding control. @@ -58,10 +60,16 @@ func (core *Core) MinimumSize () (width, height int) { return core.metrics.minimumWidth, core.metrics.minimumHeight } -// SetParentHooks fulfils the tomo.Element interface. This should not need to be +// OnDamage fulfils the tomo.Element interface. This should not need to be // overridden. -func (core *Core) SetParentHooks (hooks tomo.ParentHooks) { - core.hooks = hooks +func (core *Core) OnDamage (callback func (region tomo.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 @@ -73,41 +81,24 @@ type CoreControl struct { core *Core } -// RequestSelection requests that the element's parent send it a selection -// event. If the request was granted, it returns true. If it was denied, it -// returns false. -func (control CoreControl) RequestSelection () (granted bool) { - return control.core.hooks.RunSelectionRequest() -} - -// RequestSelectionMotion requests that the element's parent deselect this -// element and select the one to the left or right of it, depending on the -// direction. If the requests was granted, it returns true. If it was denied, it -// returns false. -func (control CoreControl) RequestSelectionMotion ( - direction tomo.SelectionDirection, -) ( - granted bool, -) { - return control.core.hooks.RunSelectionMotionRequest(direction) -} - // HasImage returns true if the core has an allocated image buffer, and false if // it doesn't. func (control CoreControl) HasImage () (has bool) { return !control.Bounds().Empty() } -// PushRegion pushes the selected region of pixels to the parent element. This +// DamageRegion pushes the selected region of pixels to the parent element. This // does not need to be called when responding to a resize event. -func (control CoreControl) PushRegion (bounds image.Rectangle) { - control.core.hooks.RunDraw(tomo.Cut(control, bounds)) +func (control CoreControl) DamageRegion (bounds image.Rectangle) { + if control.core.onDamage != nil { + control.core.onDamage(tomo.Cut(control, bounds)) + } } -// PushAll pushes all pixels to the parent element. This does not need to be +// DamageAll pushes all pixels to the parent element. This does not need to be // called when responding to a resize event. -func (control CoreControl) PushAll () { - control.PushRegion(control.Bounds()) +func (control CoreControl) DamageAll () { + control.DamageRegion(control.Bounds()) } // AllocateCanvas resizes the canvas, constraining the width and height so that @@ -128,7 +119,9 @@ func (control CoreControl) SetMinimumSize (width, height int) { core.metrics.minimumWidth = width core.metrics.minimumHeight = height - core.hooks.RunMinimumSizeChange(width, height) + if control.core.onMinimumSizeChange != nil { + control.core.onMinimumSizeChange() + } // if there is an image buffer, and the current size is less // than this new minimum size, send core.parent a resize event. @@ -143,18 +136,6 @@ func (control CoreControl) SetMinimumSize (width, height int) { } } -// NotifyFlexibleHeightChange notifies the parent element that this element's -// flexible height has changed. -func (control CoreControl) NotifyFlexibleHeightChange () { - control.core.hooks.RunFlexibleHeightChange() -} - -// NotifyContentBoundsChange notifies the parent element that this element's -// inner content bounds or scroll position have changed. -func (control CoreControl) NotifyContentBoundsChange () { - control.core.hooks.RunContentBoundsChange() -} - // ConstrainSize contstrains the specified width and height to the minimum width // and height, and returns wether or not anything ended up being constrained. func (control CoreControl) ConstrainSize ( diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 45653aa..df8954f 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -54,7 +54,7 @@ func (element *Mouse) HandleMouseDown (x, y int, button tomo.Button) { func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) { element.drawing = false mousePos := image.Pt(x, y) - element.core.PushRegion (artist.Line ( + element.core.DamageRegion (artist.Line ( element.core, element.color, 1, element.lastMousePos, mousePos)) element.lastMousePos = mousePos @@ -63,10 +63,10 @@ func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) { func (element *Mouse) HandleMouseMove (x, y int) { if !element.drawing { return } mousePos := image.Pt(x, y) - element.core.PushRegion (artist.Line ( + element.core.DamageRegion (artist.Line ( element.core, element.color, 1, element.lastMousePos, mousePos)) element.lastMousePos = mousePos } -func (element *Mouse) HandleScroll (x, y int, deltaX, deltaY float64) { } +func (element *Mouse) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } diff --git a/layout.go b/layout.go index 4482d2e..8ca1f8c 100644 --- a/layout.go +++ b/layout.go @@ -23,8 +23,8 @@ type Layout interface { // needs to properly arrange the given slice of layout entries. MinimumSize (entries []LayoutEntry) (width, height int) - // MinimumHeightFor Returns the minimum height the layout needs to lay + // FlexibleHeightFor Returns the minimum height the layout needs to lay // out the specified elements at the given width, taking into account // flexible elements. - MinimumHeightFor (entries []LayoutEntry, squeeze int) (height int) + FlexibleHeightFor (entries []LayoutEntry, squeeze int) (height int) } diff --git a/layouts/dialog.go b/layouts/dialog.go index f662129..347f5b7 100644 --- a/layouts/dialog.go +++ b/layouts/dialog.go @@ -134,7 +134,9 @@ func (layout Dialog) MinimumSize ( return } -func (layout Dialog) MinimumHeightFor ( +// FlexibleHeightFor Returns the minimum height the layout needs to lay out the +// specified elements at the given width, taking into account flexible elements. +func (layout Dialog) FlexibleHeightFor ( entries []tomo.LayoutEntry, width int, ) ( @@ -147,7 +149,7 @@ func (layout Dialog) MinimumHeightFor ( if len(entries) > 0 { mainChildHeight := 0 if child, flexible := entries[0].Element.(tomo.Flexible); flexible { - mainChildHeight = child.MinimumHeightFor(width) + mainChildHeight = child.FlexibleHeightFor(width) } else { _, mainChildHeight = entries[0].MinimumSize() } diff --git a/layouts/horizontal.go b/layouts/horizontal.go index 8207087..fcda8ff 100644 --- a/layouts/horizontal.go +++ b/layouts/horizontal.go @@ -75,7 +75,9 @@ func (layout Horizontal) MinimumSize ( return } -func (layout Horizontal) MinimumHeightFor ( +// FlexibleHeightFor Returns the minimum height the layout needs to lay out the +// specified elements at the given width, taking into account flexible elements. +func (layout Horizontal) FlexibleHeightFor ( entries []tomo.LayoutEntry, width int, ) ( @@ -100,7 +102,7 @@ func (layout Horizontal) MinimumHeightFor ( entryWidth = expandingElementWidth } if child, flexible := entry.Element.(tomo.Flexible); flexible { - entryHeight = child.MinimumHeightFor(entryWidth) + entryHeight = child.FlexibleHeightFor(entryWidth) } if entryHeight > height { height = entryHeight } diff --git a/layouts/vertical.go b/layouts/vertical.go index e3f4a50..48a473c 100644 --- a/layouts/vertical.go +++ b/layouts/vertical.go @@ -32,7 +32,7 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) { var entryMinHeight int if child, flexible := entry.Element.(tomo.Flexible); flexible { - entryMinHeight = child.MinimumHeightFor(width) + entryMinHeight = child.FlexibleHeightFor(width) } else { _, entryMinHeight = entry.MinimumSize() } @@ -102,9 +102,9 @@ func (layout Vertical) MinimumSize ( return } -// MinimumHeightFor Returns the minimum height the layout needs to lay out the +// FlexibleHeightFor Returns the minimum height the layout needs to lay out the // specified elements at the given width, taking into account flexible elements. -func (layout Vertical) MinimumHeightFor ( +func (layout Vertical) FlexibleHeightFor ( entries []tomo.LayoutEntry, width int, ) ( @@ -118,7 +118,7 @@ func (layout Vertical) MinimumHeightFor ( for index, entry := range entries { child, flexible := entry.Element.(tomo.Flexible) if flexible { - height += child.MinimumHeightFor(width) + height += child.FlexibleHeightFor(width) } else { _, entryHeight := entry.MinimumSize() height += entryHeight