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 := scrollSum { }
sum.add(buttonEvent.Detail) sum.add(buttonEvent.Detail)
window.compressScrollSum(buttonEvent, &sum) window.compressScrollSum(buttonEvent, &sum)
child.HandleScroll ( child.HandleMouseScroll (
int(buttonEvent.EventX), int(buttonEvent.EventX),
int(buttonEvent.EventY), int(buttonEvent.EventY),
float64(sum.x), float64(sum.y)) float64(sum.x), float64(sum.y))

View File

@ -76,26 +76,39 @@ func (backend *Backend) NewWindow (
} }
func (window *Window) Adopt (child tomo.Element) { func (window *Window) Adopt (child tomo.Element) {
// disown previous child
if window.child != nil { if window.child != nil {
window.child.SetParentHooks (tomo.ParentHooks { }) window.child.OnDamage(nil)
if previousChild, ok := window.child.(tomo.Selectable); ok { window.child.OnMinimumSizeChange(nil)
if previousChild.Selected() { }
previousChild.HandleDeselection() 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 window.child = child
if child != nil { if newChild, ok := child.(tomo.Flexible); ok {
child.SetParentHooks (tomo.ParentHooks { newChild.OnFlexibleHeightChange(window.resizeChildToFit)
Draw: window.childDrawCallback, }
MinimumSizeChange: window.childMinimumSizeChangeCallback, if newChild, ok := child.(tomo.Selectable); ok {
FlexibleHeightChange: window.resizeChildToFit, newChild.OnSelectionRequest(window.childSelectionRequestCallback)
SelectionRequest: window.childSelectionRequestCallback, }
}) if child != nil {
child.OnDamage(window.childDrawCallback)
window.resizeChildToFit() child.OnMinimumSizeChange (func () {
window.childMinimumSizeChangeCallback (
child.MinimumSize())
})
window.resizeChildToFit()
window.childMinimumSizeChangeCallback(child.MinimumSize())
} }
window.childMinimumSizeChangeCallback(child.MinimumSize())
} }
func (window *Window) Child () (child tomo.Element) { func (window *Window) Child () (child tomo.Element) {
@ -204,7 +217,7 @@ func (window *Window) redrawChildEntirely () {
func (window *Window) resizeChildToFit () { func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true window.skipChildDrawCallback = true
if child, ok := window.child.(tomo.Flexible); ok { if child, ok := window.child.(tomo.Flexible); ok {
minimumHeight := child.MinimumHeightFor(window.metrics.width) minimumHeight := child.FlexibleHeightFor(window.metrics.width)
minimumWidth, _ := child.MinimumSize() minimumWidth, _ := child.MinimumSize()
icccm.WmNormalHintsSet ( icccm.WmNormalHintsSet (

View File

@ -2,96 +2,6 @@ package tomo
import "image" 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. // Element represents a basic on-screen object.
type Element interface { type Element interface {
// Element must implement the Canvas interface. Elements should start // Element must implement the Canvas interface. Elements should start
@ -109,11 +19,13 @@ type Element interface {
// element's parent. // element's parent.
Resize (width, height int) Resize (width, height int)
// SetParentHooks gives the element callbacks that let it send // OnDamage sets a function to be called when an area of the element is
// information to its parent element without it knowing anything about // drawn on and should be pushed to the screen.
// the parent element or containing any reference to it. When a parent OnDamage (callback func (region Canvas))
// element adopts a child element, it must set these callbacks.
SetParentHooks (callbacks ParentHooks) // OnMinimumSizeChange sets a function to be called when the element's
// minimum size is changed.
OnMinimumSizeChange (callback func ())
} }
// SelectionDirection represents a keyboard navigation direction. // SelectionDirection represents a keyboard navigation direction.
@ -156,6 +68,18 @@ type Selectable interface {
// HandleDeselection causes this element to mark itself and all of its // HandleDeselection causes this element to mark itself and all of its
// children as deselected. // children as deselected.
HandleDeselection () 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. // 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 // HandleScroll is called when the mouse is scrolled. The X and Y
// direction of the scroll event are passed as deltaX and deltaY. // 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 // Flexible represents an element who's preferred minimum height can change in
@ -204,10 +128,10 @@ type MouseTarget interface {
type Flexible interface { type Flexible interface {
Element Element
// HeightForWidth returns what the element's minimum height would be if // FlexibleHeightFor returns what the element's minimum height would be
// resized to a specified width. This does not actually alter the state // if resized to a specified width. This does not actually alter the
// of the element in any way, but it may perform significant work, so it // state of the element in any way, but it may perform significant work,
// should be called sparingly. // so it should be called sparingly.
// //
// It is reccomended that parent containers check for this interface and // It is reccomended that parent containers check for this interface and
// take this method's value into account in order to support things like // 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 // It is important to note that if a parent container checks for
// flexible chilren, it itself will likely need to be flexible. // 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 // 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 returns the supported axes for scrolling.
ScrollAxes () (horizontal, vertical bool) 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 pressed bool
enabled bool enabled bool
selected bool selected bool
onClick func ()
text string text string
drawer artist.TextDrawer 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. // 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 element.pressed = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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 element.pressed = false
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
within := image.Point { x, y }. 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) 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 ( func (element *Button) HandleKeyDown (
key tomo.Key, key tomo.Key,
@ -75,7 +78,7 @@ func (element *Button) HandleKeyDown (
element.pressed = true element.pressed = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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 element.pressed = false
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
if !element.enabled { return } if !element.enabled { return }
if element.onClick != nil { if element.onClick != nil {
@ -100,7 +103,9 @@ func (element *Button) Selected () (selected bool) {
func (element *Button) Select () { func (element *Button) Select () {
if !element.enabled { return } if !element.enabled { return }
element.core.RequestSelection() if element.onSelectionRequest != nil {
element.onSelectionRequest()
}
} }
func (element *Button) HandleSelection ( func (element *Button) HandleSelection (
@ -117,7 +122,7 @@ func (element *Button) HandleSelection (
element.selected = true element.selected = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
return true return true
} }
@ -126,10 +131,20 @@ func (element *Button) HandleDeselection () {
element.selected = false element.selected = false
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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. // OnClick sets the function to be called when the button is clicked.
func (element *Button) OnClick (callback func ()) { func (element *Button) OnClick (callback func ()) {
element.onClick = callback element.onClick = callback
@ -141,7 +156,7 @@ func (element *Button) SetEnabled (enabled bool) {
element.enabled = enabled element.enabled = enabled
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -157,7 +172,7 @@ func (element *Button) SetText (text string) {
theme.Padding() * 2 + textBounds.Dy()) theme.Padding() * 2 + textBounds.Dy())
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }

View File

@ -15,10 +15,13 @@ type Checkbox struct {
checked bool checked bool
enabled bool enabled bool
selected bool selected bool
onClick func ()
text string text string
drawer artist.TextDrawer 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. // 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 element.pressed = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
if within && element.onClick != nil { if within && element.onClick != nil {
element.onClick() element.onClick()
@ -76,7 +79,7 @@ func (element *Checkbox) HandleKeyDown (
element.pressed = true element.pressed = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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 element.checked = !element.checked
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
if element.onClick != nil { if element.onClick != nil {
element.onClick() element.onClick()
@ -102,7 +105,10 @@ func (element *Checkbox) Selected () (selected bool) {
// Select requests that this element be selected. // Select requests that this element be selected.
func (element *Checkbox) Select () { func (element *Checkbox) Select () {
element.core.RequestSelection() if !element.enabled { return }
if element.onSelectionRequest != nil {
element.onSelectionRequest()
}
} }
func (element *Checkbox) HandleSelection ( func (element *Checkbox) HandleSelection (
@ -119,7 +125,7 @@ func (element *Checkbox) HandleSelection (
element.selected = true element.selected = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
return true return true
} }
@ -128,10 +134,20 @@ func (element *Checkbox) HandleDeselection () {
element.selected = false element.selected = false
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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. // OnClick sets the function to be called when the checkbox is toggled.
func (element *Checkbox) OnClick (callback func ()) { func (element *Checkbox) OnClick (callback func ()) {
element.onClick = callback element.onClick = callback
@ -148,7 +164,7 @@ func (element *Checkbox) SetEnabled (enabled bool) {
element.enabled = enabled element.enabled = enabled
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -164,7 +180,7 @@ func (element *Checkbox) SetText (text string) {
textBounds.Dy()) textBounds.Dy())
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }

View File

@ -19,6 +19,10 @@ type Container struct {
selected bool selected bool
selectable bool selectable bool
flexible bool flexible bool
onSelectionRequest func () (granted bool)
onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool)
onFlexibleHeightChange func ()
} }
// NewContainer creates a new container. // NewContainer creates a new container.
@ -35,7 +39,7 @@ func (element *Container) SetLayout (layout tomo.Layout) {
if element.core.HasImage() { if element.core.HasImage() {
element.recalculate() element.recalculate()
element.draw() 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 // the element will expand (instead of contract to its minimum size), in
// whatever way is defined by the current layout. // whatever way is defined by the current layout.
func (element *Container) Adopt (child tomo.Element, expand bool) { func (element *Container) Adopt (child tomo.Element, expand bool) {
child.SetParentHooks (tomo.ParentHooks { // set event handlers
Draw: func (region tomo.Canvas) { child.OnDamage (func (region tomo.Canvas) {
element.drawChildRegion(child, region) 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)
},
}) })
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.children = append (element.children, tomo.LayoutEntry {
Element: child, Element: child,
Expand: expand, Expand: expand,
}) })
// refresh stale data
element.updateMinimumSize() element.updateMinimumSize()
element.reflectChildProperties() element.reflectChildProperties()
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.recalculate() element.recalculate()
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -97,7 +101,7 @@ func (element *Container) Warp (callback func ()) {
if element.core.HasImage() { if element.core.HasImage() {
element.recalculate() element.recalculate()
element.draw() 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) { func (element *Container) Disown (child tomo.Element) {
for index, entry := range element.children { for index, entry := range element.children {
if entry.Element == child { if entry.Element == child {
entry.SetParentHooks(tomo.ParentHooks { }) element.clearChildEventHandlers(entry.Element)
element.children = append ( element.children = append (
element.children[:index], element.children[:index],
element.children[index + 1:]...) element.children[index + 1:]...)
@ -119,7 +123,22 @@ func (element *Container) Disown (child tomo.Element) {
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.recalculate() element.recalculate()
element.draw() 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 { if element.core.HasImage() && !element.warping {
element.recalculate() element.recalculate()
element.draw() 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) child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
if !handlesMouse { return } if !handlesMouse { return }
childPosition := element.childPosition(child) 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 ( func (element *Container) HandleKeyDown (
@ -245,7 +264,9 @@ func (element *Container) Selected () (selected bool) {
} }
func (element *Container) Select () { func (element *Container) Select () {
element.core.RequestSelection() if element.onSelectionRequest != nil {
element.onSelectionRequest()
}
} }
func (element *Container) HandleSelection (direction tomo.SelectionDirection) (ok bool) { func (element *Container) HandleSelection (direction tomo.SelectionDirection) (ok bool) {
@ -300,8 +321,12 @@ func (element *Container) HandleSelection (direction tomo.SelectionDirection) (o
return false return false
} }
func (element *Container) MinimumHeightFor (width int) (height int) { func (element *Container) FlexibleHeightFor (width int) (height int) {
return element.layout.MinimumHeightFor(element.children, width) return element.layout.FlexibleHeightFor(element.children, width)
}
func (element *Container) OnFlexibleHeightChange (callback func ()) {
element.onFlexibleHeightChange = callback
} }
func (element *Container) HandleDeselection () { 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) { func (element *Container) forSelected (callback func (child tomo.Selectable) bool) {
for _, entry := range element.children { for _, entry := range element.children {
child, selectable := entry.Element.(tomo.Selectable) child, selectable := entry.Element.(tomo.Selectable)
@ -379,7 +414,7 @@ func (element *Container) childSelectionRequestCallback (
) ( ) (
granted bool, granted bool,
) { ) {
if element.core.RequestSelection() { if element.onSelectionRequest != nil && element.onSelectionRequest() {
element.forSelected (func (child tomo.Selectable) bool { element.forSelected (func (child tomo.Selectable) bool {
child.HandleDeselection() child.HandleDeselection()
return true return true
@ -394,7 +429,7 @@ func (element *Container) childSelectionRequestCallback (
func (element *Container) updateMinimumSize () { func (element *Container) updateMinimumSize () {
width, height := element.layout.MinimumSize(element.children) width, height := element.layout.MinimumSize(element.children)
if element.flexible { if element.flexible {
height = element.layout.MinimumHeightFor(element.children, width) height = element.layout.FlexibleHeightFor(element.children, width)
} }
element.core.SetMinimumSize(width, height) element.core.SetMinimumSize(width, height)
} }
@ -422,7 +457,7 @@ func (element *Container) drawChildRegion (child tomo.Element, region tomo.Canva
for _, entry := range element.children { for _, entry := range element.children {
if entry.Element == child { if entry.Element == child {
artist.Paste(element.core, region, entry.Position) artist.Paste(element.core, region, entry.Position)
element.core.PushRegion ( element.core.DamageRegion (
region.Bounds().Add(entry.Position)) region.Bounds().Add(entry.Position))
break break
} }

View File

@ -13,6 +13,8 @@ type Label struct {
wrap bool wrap bool
text string text string
drawer artist.TextDrawer drawer artist.TextDrawer
onFlexibleHeightChange func ()
} }
// NewLabel creates a new label. If wrap is set to true, the text inside will be // 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 return
} }
// MinimumHeightFor returns the reccomended height for this element based on the // FlexibleHeightFor returns the reccomended height for this element based on
// given width in order to allow the text to wrap properly. // the given width in order to allow the text to wrap properly.
func (element *Label) MinimumHeightFor (width int) (height int) { func (element *Label) FlexibleHeightFor (width int) (height int) {
if element.wrap { if element.wrap {
return element.drawer.ReccomendedHeightFor(width) return element.drawer.ReccomendedHeightFor(width)
} else { } 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. // SetText sets the label's text.
func (element *Label) SetText (text string) { func (element *Label) SetText (text string) {
if element.text == text { return } if element.text == text { return }
@ -59,7 +67,7 @@ func (element *Label) SetText (text string) {
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -78,7 +86,7 @@ func (element *Label) SetWrap (wrap bool) {
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -88,7 +96,9 @@ func (element *Label) updateMinimumSize () {
if em < 1 { em = theme.Padding() } if em < 1 { em = theme.Padding() }
element.core.SetMinimumSize ( element.core.SetMinimumSize (
em, element.drawer.LineHeight().Round()) em, element.drawer.LineHeight().Round())
element.core.NotifyFlexibleHeightChange() if element.onFlexibleHeightChange != nil {
element.onFlexibleHeightChange()
}
} else { } else {
bounds := element.drawer.LayoutBounds() bounds := element.drawer.LayoutBounds()
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())

View File

@ -34,7 +34,7 @@ func (element *ProgressBar) SetProgress (progress float64) {
element.progress = progress element.progress = progress
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }

View File

@ -23,6 +23,9 @@ type ScrollContainer struct {
enabled bool enabled bool
bounds image.Rectangle bounds image.Rectangle
} }
onSelectionRequest func () (granted bool)
onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool)
} }
// NewScrollContainer creates a new scroll container with the specified scroll // 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) { func (element *ScrollContainer) Adopt (child tomo.Scrollable) {
// disown previous child if it exists // disown previous child if it exists
if element.child != nil { if element.child != nil {
element.child.SetParentHooks (tomo.ParentHooks { }) element.clearChildEventHandlers(child)
if previousChild, ok := element.child.(tomo.Selectable); ok {
if previousChild.Selected() {
previousChild.HandleDeselection()
}
}
} }
// adopt new child // adopt new child
element.child = child element.child = child
if child != nil { if child != nil {
child.SetParentHooks (tomo.ParentHooks { child.OnDamage(element.childDamageCallback)
// Draw: window.childDrawCallback, child.OnMinimumSizeChange(element.updateMinimumSize)
// MinimumSizeChange: window.childMinimumSizeChangeCallback, if newChild, ok := child.(tomo.Selectable); ok {
// FlexibleHeightChange: window.resizeChildToFit, newChild.OnSelectionRequest (
// SelectionRequest: window.childSelectionRequestCallback, element.childSelectionRequestCallback)
}) newChild.OnSelectionMotionRequest (
element.childSelectionMotionRequestCallback)
}
// TODO: somehow inform the core that we do not in fact want to // TODO: somehow inform the core that we do not in fact want to
// redraw the element. // 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 () { func (element *ScrollContainer) recalculate () {
horizontal := &element.horizontal horizontal := &element.horizontal
vertical := &element.vertical vertical := &element.vertical

View File

@ -34,7 +34,7 @@ func (element *Spacer) SetLine (line bool) {
element.line = line element.line = line
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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) onKeyDown func (tomo.Key, tomo.Modifiers, bool) (bool)
onChange func () 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 // 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 () { if altered && element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -130,7 +133,9 @@ func (element *TextBox) Selected () (selected bool) {
} }
func (element *TextBox) Select () { func (element *TextBox) Select () {
element.core.RequestSelection() if element.onSelectionRequest != nil {
element.onSelectionRequest()
}
} }
func (element *TextBox) HandleSelection ( func (element *TextBox) HandleSelection (
@ -147,7 +152,7 @@ func (element *TextBox) HandleSelection (
element.selected = true element.selected = true
if element.core.HasImage() { if element.core.HasImage() {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
return true return true
} }
@ -156,16 +161,26 @@ func (element *TextBox) HandleDeselection () {
element.selected = false element.selected = false
if element.core.HasImage() { if element.core.HasImage() {
element.draw() 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) { func (element *TextBox) SetEnabled (enabled bool) {
if element.enabled == enabled { return } if element.enabled == enabled { return }
element.enabled = enabled element.enabled = enabled
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -178,7 +193,7 @@ func (element *TextBox) SetPlaceholder (placeholder string) {
element.updateMinimumSize() element.updateMinimumSize()
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -195,7 +210,7 @@ func (element *TextBox) SetValue (text string) {
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
} }
} }
@ -246,9 +261,11 @@ func (element *TextBox) ScrollTo (position image.Point) {
if element.core.HasImage () { if element.core.HasImage () {
element.draw() element.draw()
element.core.PushAll() element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
} }
element.core.NotifyContentBoundsChange()
} }
// ScrollAxes returns the supported axes for scrolling. // ScrollAxes returns the supported axes for scrolling.
@ -256,6 +273,10 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
return true, false return true, false
} }
func (element *TextBox) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
func (element *TextBox) updateMinimumSize () { func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds() textBounds := element.placeholderDrawer.LayoutBounds()
element.core.SetMinimumSize ( element.core.SetMinimumSize (
@ -286,7 +307,9 @@ func (element *TextBox) scrollToCursor () {
element.scroll -= minX - cursorPosition.X element.scroll -= minX - cursorPosition.X
if element.scroll < 0 { element.scroll = 0 } if element.scroll < 0 { element.scroll = 0 }
} }
element.core.NotifyContentBoundsChange() if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
} }
func (element *TextBox) draw () { func (element *TextBox) draw () {

View File

@ -17,7 +17,9 @@ type Core struct {
selectable bool selectable bool
selected bool selected bool
hooks tomo.ParentHooks
onMinimumSizeChange func ()
onDamage func (region tomo.Canvas)
} }
// NewCore creates a new element core and its corresponding control. // 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 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. // overridden.
func (core *Core) SetParentHooks (hooks tomo.ParentHooks) { func (core *Core) OnDamage (callback func (region tomo.Canvas)) {
core.hooks = hooks 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 // CoreControl is a struct that can exert control over a Core struct. It can be
@ -73,41 +81,24 @@ type CoreControl struct {
core *Core 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 // HasImage returns true if the core has an allocated image buffer, and false if
// it doesn't. // it doesn't.
func (control CoreControl) HasImage () (has bool) { func (control CoreControl) HasImage () (has bool) {
return !control.Bounds().Empty() 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. // does not need to be called when responding to a resize event.
func (control CoreControl) PushRegion (bounds image.Rectangle) { func (control CoreControl) DamageRegion (bounds image.Rectangle) {
control.core.hooks.RunDraw(tomo.Cut(control, bounds)) 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. // called when responding to a resize event.
func (control CoreControl) PushAll () { func (control CoreControl) DamageAll () {
control.PushRegion(control.Bounds()) control.DamageRegion(control.Bounds())
} }
// AllocateCanvas resizes the canvas, constraining the width and height so that // 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.minimumWidth = width
core.metrics.minimumHeight = height 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 // if there is an image buffer, and the current size is less
// than this new minimum size, send core.parent a resize event. // 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 // ConstrainSize contstrains the specified width and height to the minimum width
// and height, and returns wether or not anything ended up being constrained. // and height, and returns wether or not anything ended up being constrained.
func (control CoreControl) ConstrainSize ( 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) { func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) {
element.drawing = false element.drawing = false
mousePos := image.Pt(x, y) mousePos := image.Pt(x, y)
element.core.PushRegion (artist.Line ( element.core.DamageRegion (artist.Line (
element.core, element.color, 1, element.core, element.color, 1,
element.lastMousePos, mousePos)) element.lastMousePos, mousePos))
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) { func (element *Mouse) HandleMouseMove (x, y int) {
if !element.drawing { return } if !element.drawing { return }
mousePos := image.Pt(x, y) mousePos := image.Pt(x, y)
element.core.PushRegion (artist.Line ( element.core.DamageRegion (artist.Line (
element.core, element.color, 1, element.core, element.color, 1,
element.lastMousePos, mousePos)) element.lastMousePos, mousePos))
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. // needs to properly arrange the given slice of layout entries.
MinimumSize (entries []LayoutEntry) (width, height int) 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 // out the specified elements at the given width, taking into account
// flexible elements. // 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 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, entries []tomo.LayoutEntry,
width int, width int,
) ( ) (
@ -147,7 +149,7 @@ func (layout Dialog) MinimumHeightFor (
if len(entries) > 0 { if len(entries) > 0 {
mainChildHeight := 0 mainChildHeight := 0
if child, flexible := entries[0].Element.(tomo.Flexible); flexible { if child, flexible := entries[0].Element.(tomo.Flexible); flexible {
mainChildHeight = child.MinimumHeightFor(width) mainChildHeight = child.FlexibleHeightFor(width)
} else { } else {
_, mainChildHeight = entries[0].MinimumSize() _, mainChildHeight = entries[0].MinimumSize()
} }

View File

@ -75,7 +75,9 @@ func (layout Horizontal) MinimumSize (
return 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, entries []tomo.LayoutEntry,
width int, width int,
) ( ) (
@ -100,7 +102,7 @@ func (layout Horizontal) MinimumHeightFor (
entryWidth = expandingElementWidth entryWidth = expandingElementWidth
} }
if child, flexible := entry.Element.(tomo.Flexible); flexible { if child, flexible := entry.Element.(tomo.Flexible); flexible {
entryHeight = child.MinimumHeightFor(entryWidth) entryHeight = child.FlexibleHeightFor(entryWidth)
} }
if entryHeight > height { height = entryHeight } if entryHeight > height { height = entryHeight }

View File

@ -32,7 +32,7 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
var entryMinHeight int var entryMinHeight int
if child, flexible := entry.Element.(tomo.Flexible); flexible { if child, flexible := entry.Element.(tomo.Flexible); flexible {
entryMinHeight = child.MinimumHeightFor(width) entryMinHeight = child.FlexibleHeightFor(width)
} else { } else {
_, entryMinHeight = entry.MinimumSize() _, entryMinHeight = entry.MinimumSize()
} }
@ -102,9 +102,9 @@ func (layout Vertical) MinimumSize (
return 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. // specified elements at the given width, taking into account flexible elements.
func (layout Vertical) MinimumHeightFor ( func (layout Vertical) FlexibleHeightFor (
entries []tomo.LayoutEntry, entries []tomo.LayoutEntry,
width int, width int,
) ( ) (
@ -118,7 +118,7 @@ func (layout Vertical) MinimumHeightFor (
for index, entry := range entries { for index, entry := range entries {
child, flexible := entry.Element.(tomo.Flexible) child, flexible := entry.Element.(tomo.Flexible)
if flexible { if flexible {
height += child.MinimumHeightFor(width) height += child.FlexibleHeightFor(width)
} else { } else {
_, entryHeight := entry.MinimumSize() _, entryHeight := entry.MinimumSize()
height += entryHeight height += entryHeight