Merge pull request 'atomize-parent-hooks' (#4) from atomize-parent-hooks into main

Reviewed-on: sashakoshka/tomo#4
This commit is contained in:
Sasha Koshka 2023-01-19 22:35:50 +00:00
commit 71d50cab4b
17 changed files with 385 additions and 258 deletions

View File

@ -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))

View File

@ -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 (

View File

@ -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 ())
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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())

View File

@ -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()
}
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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 () {

View File

@ -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 (

View File

@ -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) { }

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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 }

View File

@ -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