Merge pull request 'atomize-element-interface' (#3) from atomize-element-interface into main

Reviewed-on: sashakoshka/tomo#3
This commit is contained in:
Sasha Koshka 2023-01-17 19:33:23 +00:00
commit a1ab5f4353
14 changed files with 272 additions and 51 deletions

View File

@ -14,10 +14,11 @@ type characterLayout struct {
} }
type wordLayout struct { type wordLayout struct {
position image.Point position image.Point
width int width int
spaceAfter int spaceAfter int
text []characterLayout breaksAfter int
text []characterLayout
} }
// Align specifies a text alignment method. // Align specifies a text alignment method.
@ -167,13 +168,16 @@ func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) { func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
if !drawer.layoutClean { drawer.recalculate() } if !drawer.layoutClean { drawer.recalculate() }
metrics := drawer.face.Metrics() metrics := drawer.face.Metrics()
dot := fixed.Point26_6 { 0, 0 } dot := fixed.Point26_6 { 0, metrics.Height }
for _, word := range drawer.layout { for _, word := range drawer.layout {
dot.X += fixed.Int26_6((word.width + word.spaceAfter) << 6) if word.width + dot.X.Round() > width {
if word.width + word.position.X > width && word.position.X > 0 {
dot.Y += metrics.Height dot.Y += metrics.Height
dot.X = fixed.Int26_6(word.width << 6) dot.X = 0
}
dot.X += fixed.I(word.width + word.spaceAfter)
if word.breaksAfter > 0 {
dot.Y += fixed.I(word.breaksAfter).Mul(metrics.Height)
dot.X = 0
} }
} }
@ -246,6 +250,7 @@ func (drawer *TextDrawer) recalculate () {
if character == '\n' { if character == '\n' {
dot.Y += metrics.Height dot.Y += metrics.Height
dot.X = 0 dot.X = 0
word.breaksAfter ++
previousCharacter = character previousCharacter = character
index ++ index ++
} else { } else {
@ -290,7 +295,6 @@ func (drawer *TextDrawer) recalculate () {
if drawer.wrap { if drawer.wrap {
drawer.layoutBounds.Max.X = drawer.width drawer.layoutBounds.Max.X = drawer.width
println("aaa")
} else { } else {
drawer.layoutBounds.Max.X = horizontalExtent drawer.layoutBounds.Max.X = horizontalExtent
} }

View File

@ -89,6 +89,7 @@ func (window *Window) Adopt (child tomo.Element) {
child.SetParentHooks (tomo.ParentHooks { child.SetParentHooks (tomo.ParentHooks {
Draw: window.childDrawCallback, Draw: window.childDrawCallback,
MinimumSizeChange: window.childMinimumSizeChangeCallback, MinimumSizeChange: window.childMinimumSizeChangeCallback,
FlexibleHeightChange: window.resizeChildToFit,
SelectionRequest: window.childSelectionRequestCallback, SelectionRequest: window.childSelectionRequestCallback,
}) })
@ -202,11 +203,27 @@ func (window *Window) redrawChildEntirely () {
func (window *Window) resizeChildToFit () { func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true window.skipChildDrawCallback = true
if child, ok := window.child.(tomo.Expanding); ok { if child, ok := window.child.(tomo.Flexible); ok {
minimumHeight := child.MinimumHeightFor(window.metrics.width) minimumHeight := child.MinimumHeightFor(window.metrics.width)
_, minimumWidth := child.MinimumSize() minimumWidth, _ := child.MinimumSize()
window.childMinimumSizeChangeCallback (
minimumWidth, minimumHeight) icccm.WmNormalHintsSet (
window.backend.connection,
window.xWindow.Id,
&icccm.NormalHints {
Flags: icccm.SizeHintPMinSize,
MinWidth: uint(minimumWidth),
MinHeight: uint(minimumHeight),
})
if window.metrics.height >= minimumHeight &&
window.metrics.width >= minimumWidth {
window.child.Resize (
window.metrics.width,
window.metrics.height)
window.redrawChildEntirely()
}
} else { } else {
window.child.Resize ( window.child.Resize (
window.metrics.width, window.metrics.width,
@ -261,6 +278,20 @@ func (window *Window) childSelectionRequestCallback () (granted bool) {
return true return true
} }
func (window *Window) childSelectionMotionRequestCallback (
direction tomo.SelectionDirection,
) (
granted bool,
) {
if child, ok := window.child.(tomo.Selectable); ok {
if !child.HandleSelection(direction) {
child.HandleDeselection()
}
return true
}
return true
}
func (window *Window) pushRegion (region image.Rectangle) { func (window *Window) pushRegion (region image.Rectangle) {
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)

View File

@ -3,7 +3,8 @@ package tomo
// ParentHooks is a struct that contains callbacks that let child elements send // ParentHooks is a struct that contains callbacks that let child elements send
// information to their parent element without the child element knowing // information to their parent element without the child element knowing
// anything about the parent element or containing any reference to it. When a // anything about the parent element or containing any reference to it. When a
// parent element adopts a child element, it must set these callbacks. // parent element adopts a child element, it must set these callbacks. They are
// allowed to be nil.
type ParentHooks struct { type ParentHooks struct {
// Draw is called when a part of the child element's surface is updated. // 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. // The updated region will be passed to the callback as a sub-image.
@ -14,12 +15,20 @@ type ParentHooks struct {
// have already been resized and there is no need to send it a resize // have already been resized and there is no need to send it a resize
// event. // event.
MinimumSizeChange func (width, height int) MinimumSizeChange func (width, height int)
// FlexibleHeightChange is called when the parameters affecting the
// element's expanding height have changed.
FlexibleHeightChange func ()
// SelectionRequest is called when the child element element wants // SelectionRequest is called when the child element element wants
// itself to be selected. If the parent element chooses to grant the // itself to be selected. If the parent element chooses to grant the
// request, it must send the child element a selection event and return // request, it must send the child element a selection event and return
// true. // true.
SelectionRequest func () (granted bool) 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. // RunDraw runs the Draw hook if it is not nil. If it is nil, it does nothing.
@ -37,6 +46,14 @@ func (hooks ParentHooks) RunMinimumSizeChange (width, height int) {
} }
} }
// 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 // RunSelectionRequest runs the SelectionRequest hook if it is not nil. If it is
// nil, it does nothing. // nil, it does nothing.
func (hooks ParentHooks) RunSelectionRequest () (granted bool) { func (hooks ParentHooks) RunSelectionRequest () (granted bool) {
@ -46,6 +63,19 @@ func (hooks ParentHooks) RunSelectionRequest () (granted bool) {
return 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
}
// 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
@ -153,9 +183,9 @@ type MouseTarget interface {
HandleScroll (x, y int, deltaX, deltaY float64) HandleScroll (x, y int, deltaX, deltaY float64)
} }
// Expanding represents an element who's preferred minimum height can change in // Flexible represents an element who's preferred minimum height can change in
// response to its width. // response to its width.
type Expanding interface { type Flexible interface {
Element Element
// HeightForWidth returns what the element's minimum height would be if // HeightForWidth returns what the element's minimum height would be if
@ -170,6 +200,6 @@ type Expanding interface {
// minimum size that the element may be resized to. // minimum size that the element may be resized to.
// //
// It is important to note that if a parent container checks for // It is important to note that if a parent container checks for
// expanding chilren, it itself will likely need to be expanding. // flexible chilren, it itself will likely need to be flexible.
MinimumHeightFor (width int) (height int) MinimumHeightFor (width int) (height int)
} }

View File

@ -103,6 +103,7 @@ func (element *Button) HandleSelection (
) ( ) (
accepted bool, accepted bool,
) { ) {
direction = direction.Canon()
if !element.enabled { return false } if !element.enabled { return false }
if element.selected && direction != tomo.SelectionDirectionNeutral { if element.selected && direction != tomo.SelectionDirectionNeutral {
return false return false

View File

@ -18,6 +18,7 @@ type Container struct {
warping bool warping bool
selected bool selected bool
selectable bool selectable bool
flexible bool
} }
// NewContainer creates a new container. // NewContainer creates a new container.
@ -43,16 +44,24 @@ func (element *Container) SetLayout (layout tomo.Layout) {
// 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 { child.SetParentHooks (tomo.ParentHooks {
Draw: func (region tomo.Canvas) {
element.drawChildRegion(child, region)
},
MinimumSizeChange: func (int, int) { MinimumSizeChange: func (int, int) {
element.updateMinimumSize() element.updateMinimumSize()
}, },
FlexibleHeightChange: element.updateMinimumSize,
SelectionRequest: func () (granted bool) { SelectionRequest: func () (granted bool) {
child, selectable := child.(tomo.Selectable) child, selectable := child.(tomo.Selectable)
if !selectable { return } if !selectable { return }
return element.childSelectionRequestCallback(child) return element.childSelectionRequestCallback(child)
}, },
Draw: func (region tomo.Canvas) { SelectionMotionRequest: func (
element.drawChildRegion(child, region) direction tomo.SelectionDirection,
) (
granted bool,
) {
return element.core.RequestSelectionMotion(direction)
}, },
}) })
element.children = append (element.children, tomo.LayoutEntry { element.children = append (element.children, tomo.LayoutEntry {
@ -61,7 +70,7 @@ func (element *Container) Adopt (child tomo.Element, expand bool) {
}) })
element.updateMinimumSize() element.updateMinimumSize()
element.updateSelectable() element.reflectChildProperties()
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.recalculate() element.recalculate()
element.draw() element.draw()
@ -106,7 +115,7 @@ func (element *Container) Disown (child tomo.Element) {
} }
element.updateMinimumSize() element.updateMinimumSize()
element.updateSelectable() element.reflectChildProperties()
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.recalculate() element.recalculate()
element.draw() element.draw()
@ -119,7 +128,7 @@ func (element *Container) DisownAll () {
element.children = nil element.children = nil
element.updateMinimumSize() element.updateMinimumSize()
element.updateSelectable() element.reflectChildProperties()
if element.core.HasImage() && !element.warping { if element.core.HasImage() && !element.warping {
element.recalculate() element.recalculate()
element.draw() element.draw()
@ -176,8 +185,6 @@ func (element *Container) Resize (width, height int) {
element.draw() element.draw()
} }
// TODO: implement KeyboardTarget
func (element *Container) HandleMouseDown (x, y int, button tomo.Button) { func (element *Container) HandleMouseDown (x, y int, button tomo.Button) {
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 }
@ -223,7 +230,7 @@ func (element *Container) HandleKeyDown (
}) })
} }
func (element *Container) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { func (element *Container) HandleKeyUp (key tomo.Key, modifiers tomo.Modifiers) {
element.forSelected (func (child tomo.Selectable) bool { element.forSelected (func (child tomo.Selectable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget) child0, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard { if handlesKeyboard {
@ -293,6 +300,10 @@ func (element *Container) HandleSelection (direction tomo.SelectionDirection) (o
return false return false
} }
func (element *Container) MinimumHeightFor (width int) (height int) {
return element.layout.MinimumHeightFor(element.children, width)
}
func (element *Container) HandleDeselection () { func (element *Container) HandleDeselection () {
element.selected = false element.selected = false
element.forSelected (func (child tomo.Selectable) bool { element.forSelected (func (child tomo.Selectable) bool {
@ -319,6 +330,15 @@ func (element *Container) forSelectable (callback func (child tomo.Selectable) b
} }
} }
func (element *Container) forFlexible (callback func (child tomo.Flexible) bool) {
for _, entry := range element.children {
child, selectable := entry.Element.(tomo.Flexible)
if selectable {
if !callback(child) { break }
}
}
}
func (element *Container) forSelectableBackward (callback func (child tomo.Selectable) bool) { func (element *Container) forSelectableBackward (callback func (child tomo.Selectable) bool) {
for index := len(element.children) - 1; index >= 0; index -- { for index := len(element.children) - 1; index >= 0; index -- {
child, selectable := element.children[index].Element.(tomo.Selectable) child, selectable := element.children[index].Element.(tomo.Selectable)
@ -338,12 +358,17 @@ func (element *Container) firstSelected () (index int) {
return -1 return -1
} }
func (element *Container) updateSelectable () { func (element *Container) reflectChildProperties () {
element.selectable = false element.selectable = false
element.forSelectable (func (tomo.Selectable) bool { element.forSelectable (func (tomo.Selectable) bool {
element.selectable = true element.selectable = true
return false return false
}) })
element.flexible = false
element.forFlexible (func (tomo.Flexible) bool {
element.flexible = true
return false
})
if !element.selectable { if !element.selectable {
element.selected = false element.selected = false
} }
@ -367,8 +392,11 @@ func (element *Container) childSelectionRequestCallback (
} }
func (element *Container) updateMinimumSize () { func (element *Container) updateMinimumSize () {
element.core.SetMinimumSize ( width, height := element.layout.MinimumSize(element.children)
element.layout.MinimumSize(element.children, 1e9)) if element.flexible {
height = element.layout.MinimumHeightFor(element.children, width)
}
element.core.SetMinimumSize(width, height)
} }
func (element *Container) recalculate () { func (element *Container) recalculate () {

View File

@ -27,6 +27,7 @@ func NewLabel (text string, wrap bool) (element *Label) {
return return
} }
// Resize resizes the label and re-wraps the text if wrapping is enabled.
func (element *Label) Resize (width, height int) { func (element *Label) Resize (width, height int) {
element.core.AllocateCanvas(width, height) element.core.AllocateCanvas(width, height)
if element.wrap { if element.wrap {
@ -37,6 +38,17 @@ func (element *Label) Resize (width, height int) {
return 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) {
if element.wrap {
return element.drawer.ReccomendedHeightFor(width)
} else {
_, height = element.MinimumSize()
return
}
}
// 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 }
@ -76,6 +88,7 @@ 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()
} 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

@ -80,6 +80,18 @@ func (control CoreControl) RequestSelection () (granted bool) {
return control.core.hooks.RunSelectionRequest() 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) {
@ -101,7 +113,6 @@ func (control CoreControl) PushAll () {
// AllocateCanvas resizes the canvas, constraining the width and height so that // AllocateCanvas resizes the canvas, constraining the width and height so that
// they are not less than the specified minimum width and height. // they are not less than the specified minimum width and height.
func (control *CoreControl) AllocateCanvas (width, height int) { func (control *CoreControl) AllocateCanvas (width, height int) {
width, height, _ = control.ConstrainSize(width, height)
control.core.canvas = tomo.NewBasicCanvas(width, height) control.core.canvas = tomo.NewBasicCanvas(width, height)
control.BasicCanvas = control.core.canvas control.BasicCanvas = control.core.canvas
} }
@ -132,6 +143,12 @@ 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()
}
// 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

@ -21,7 +21,7 @@ func run () {
world.Stages = map [string] func () { world.Stages = map [string] func () {
"start": func () { "start": func () {
label := basic.NewLabel ( label := basic.NewLabel (
"you are standing next to a river.", false) "you are standing next to a river.", true)
button0 := basic.NewButton("go in the river") button0 := basic.NewButton("go in the river")
button0.OnClick(world.SwitchFunc("wet")) button0.OnClick(world.SwitchFunc("wet"))
@ -41,7 +41,7 @@ func run () {
"wet": func () { "wet": func () {
label := basic.NewLabel ( label := basic.NewLabel (
"you get completely soaked.\n" + "you get completely soaked.\n" +
"you die of hypothermia.", false) "you die of hypothermia.", true)
button0 := basic.NewButton("try again") button0 := basic.NewButton("try again")
button0.OnClick(world.SwitchFunc("start")) button0.OnClick(world.SwitchFunc("start"))
@ -58,7 +58,7 @@ func run () {
"house": func () { "house": func () {
label := basic.NewLabel ( label := basic.NewLabel (
"you are standing in front of a delapidated " + "you are standing in front of a delapidated " +
"house.", false) "house.", true)
button1 := basic.NewButton("go inside") button1 := basic.NewButton("go inside")
button1.OnClick(world.SwitchFunc("inside")) button1.OnClick(world.SwitchFunc("inside"))
@ -78,7 +78,7 @@ func run () {
"it is dark, but rays of light stream " + "it is dark, but rays of light stream " +
"through the window.\n" + "through the window.\n" +
"there is nothing particularly interesting " + "there is nothing particularly interesting " +
"here.", false) "here.", true)
button0 := basic.NewButton("go back outside") button0 := basic.NewButton("go back outside")
button0.OnClick(world.SwitchFunc("house")) button0.OnClick(world.SwitchFunc("house"))
@ -92,7 +92,7 @@ func run () {
"bear": func () { "bear": func () {
label := basic.NewLabel ( label := basic.NewLabel (
"you come face to face with a bear.\n" + "you come face to face with a bear.\n" +
"it eats you (it was hungry).", false) "it eats you (it was hungry).", true)
button0 := basic.NewButton("try again") button0 := basic.NewButton("try again")
button0.OnClick(world.SwitchFunc("start")) button0.OnClick(world.SwitchFunc("start"))

View File

@ -3,7 +3,6 @@ package main
import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic" import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x" import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () { func main () {
@ -11,15 +10,15 @@ func main () {
} }
func run () { func run () {
window, _ := tomo.NewWindow(2, 2) window, _ := tomo.NewWindow(256, 2)
window.SetTitle("horizontal stack") window.SetTitle("horizontal stack")
container := basic.NewContainer(layouts.Horizontal { true, true }) container := basic.NewContainer(layouts.Horizontal { true, true })
window.Adopt(container) window.Adopt(container)
container.Adopt(testing.NewMouse(), true) container.Adopt(basic.NewLabel("this is sample text", true), true)
container.Adopt(basic.NewLabel("<- left\nright ->", false), false) container.Adopt(basic.NewLabel("this is sample text", true), true)
container.Adopt(testing.NewMouse(), true) container.Adopt(basic.NewLabel("this is sample text", true), true)
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)
window.Show() window.Show()

View File

@ -9,7 +9,7 @@ func main () {
} }
func run () { func run () {
window, _ := tomo.NewWindow(480, 360) window, _ := tomo.NewWindow(480, 2)
window.SetTitle("example label") window.SetTitle("example label")
window.Adopt(basic.NewLabel(text, true)) window.Adopt(basic.NewLabel(text, true))
window.OnClose(tomo.Stop) window.OnClose(tomo.Stop)

View File

@ -17,7 +17,7 @@ func run () {
container := basic.NewContainer(layouts.Vertical { true, true }) container := basic.NewContainer(layouts.Vertical { true, true })
window.Adopt(container) window.Adopt(container)
label := basic.NewLabel("it is a label hehe", false) label := basic.NewLabel("it is a label hehe", true)
button := basic.NewButton("drawing pad") button := basic.NewButton("drawing pad")
okButton := basic.NewButton("OK") okButton := basic.NewButton("OK")
button.OnClick (func () { button.OnClick (func () {

View File

@ -20,8 +20,11 @@ type Layout interface {
Arrange (entries []LayoutEntry, width, height int) Arrange (entries []LayoutEntry, width, height int)
// MinimumSize returns the minimum width and height that the layout // MinimumSize returns the minimum width and height that the layout
// needs to properly arrange the given slice of layout entries, given a // needs to properly arrange the given slice of layout entries.
// "suqeeze" width so that the height can be determined for elements MinimumSize (entries []LayoutEntry) (width, height int)
// fulfilling the Expanding interface.
MinimumSize (entries []LayoutEntry, squeeze int) (width, height int) // MinimumHeightFor 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)
} }

View File

@ -72,7 +72,6 @@ func (layout Horizontal) Arrange (entries []tomo.LayoutEntry, width, height int)
// arrange the given list of entries. // arrange the given list of entries.
func (layout Horizontal) MinimumSize ( func (layout Horizontal) MinimumSize (
entries []tomo.LayoutEntry, entries []tomo.LayoutEntry,
squeeze int,
) ( ) (
width, height int, width, height int,
) { ) {
@ -93,3 +92,62 @@ func (layout Horizontal) MinimumSize (
} }
return return
} }
func (layout Horizontal) MinimumHeightFor (
entries []tomo.LayoutEntry,
width int,
) (
height int,
) {
// TODO: maybe put calculating the expanding element width in a separate
// method
if layout.Pad {
width -= theme.Padding() * 2
}
freeSpace := width
expandingElements := 0
// count the number of expanding elements and the amount of free space
// for them to collectively occupy
for index, entry := range entries {
if entry.Expand {
expandingElements ++
} else {
entryMinWidth, _ := entry.MinimumSize()
freeSpace -= entryMinWidth
}
if index > 0 && layout.Gap {
freeSpace -= theme.Padding()
}
}
expandingElementWidth := 0
if expandingElements > 0 {
expandingElementWidth = freeSpace / expandingElements
}
x, y := 0, 0
if layout.Pad {
x += theme.Padding()
y += theme.Padding()
}
// set the size and position of each element
for index, entry := range entries {
entryWidth, entryHeight := entry.MinimumSize()
if entry.Expand {
entryWidth = expandingElementWidth
}
if child, flexible := entry.Element.(tomo.Flexible); flexible {
entryHeight = child.MinimumHeightFor(entryWidth)
}
if entryHeight > height { height = entryHeight }
x += entryWidth
if index > 0 && layout.Gap { x += theme.Padding() }
}
if layout.Pad {
height += theme.Padding() * 2
}
return
}

View File

@ -26,12 +26,21 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
expandingElements := 0 expandingElements := 0
// count the number of expanding elements and the amount of free space // count the number of expanding elements and the amount of free space
// for them to collectively occupy // for them to collectively occupy, while gathering minimum heights.
minimumHeights := make([]int, len(entries))
for index, entry := range entries { for index, entry := range entries {
var entryMinHeight int
if child, flexible := entry.Element.(tomo.Flexible); flexible {
entryMinHeight = child.MinimumHeightFor(width)
} else {
_, entryMinHeight = entry.MinimumSize()
}
minimumHeights[index] = entryMinHeight
if entry.Expand { if entry.Expand {
expandingElements ++ expandingElements ++
} else { } else {
_, entryMinHeight := entry.MinimumSize()
freeSpace -= entryMinHeight freeSpace -= entryMinHeight
} }
if index > 0 && layout.Gap { if index > 0 && layout.Gap {
@ -58,7 +67,7 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
if entry.Expand { if entry.Expand {
entryHeight = expandingElementHeight entryHeight = expandingElementHeight
} else { } else {
_, entryHeight = entry.MinimumSize() entryHeight = minimumHeights[index]
} }
y += entryHeight y += entryHeight
entryBounds := entry.Bounds() entryBounds := entry.Bounds()
@ -72,7 +81,6 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
// arrange the given list of entries. // arrange the given list of entries.
func (layout Vertical) MinimumSize ( func (layout Vertical) MinimumSize (
entries []tomo.LayoutEntry, entries []tomo.LayoutEntry,
squeeze int,
) ( ) (
width, height int, width, height int,
) { ) {
@ -93,3 +101,32 @@ func (layout Vertical) MinimumSize (
} }
return return
} }
// MinimumHeightFor 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 (
entries []tomo.LayoutEntry,
width int,
) (
height int,
) {
if layout.Pad {
width -= theme.Padding() * 2
height += theme.Padding() * 2
}
for index, entry := range entries {
child, flexible := entry.Element.(tomo.Flexible)
if flexible {
height += child.MinimumHeightFor(width)
} else {
_, entryHeight := entry.MinimumSize()
height += entryHeight
}
if layout.Gap && index > 0 {
height += theme.Padding()
}
}
return
}