Changed the way minimum sizes are calculated

Boxes that need their minimum size to be updated now use a map like
for layout and drawing. Size set with MinimumSize is now treated as
separate from the content size and the larger size is used.
This commit is contained in:
Sasha Koshka 2023-08-17 23:20:08 -04:00
parent 00629a863d
commit ff8875535d
7 changed files with 157 additions and 92 deletions

View File

@ -48,9 +48,7 @@ func (backend *Backend) Run () error {
case <- pingQuit: case <- pingQuit:
return nil // FIXME: if we exited due to an error say so return nil // FIXME: if we exited due to an error say so
} }
for _, window := range backend.windows { backend.afterEvent()
window.afterEvent()
}
} }
} }
@ -78,3 +76,9 @@ func (backend *Backend) Do (callback func ()) {
func (backend *Backend) assert () { func (backend *Backend) assert () {
if backend == nil { panic("nil backend") } if backend == nil { panic("nil backend") }
} }
func (backend *Backend) afterEvent () {
for _, window := range backend.windows {
window.afterEvent()
}
}

88
box.go
View File

@ -13,8 +13,12 @@ type box struct {
parent parent parent parent
outer anyBox outer anyBox
bounds image.Rectangle bounds image.Rectangle
minSize image.Point minSize image.Point
userMinSize image.Point
minSizeQueued bool
focusQueued *bool
padding tomo.Inset padding tomo.Inset
border []tomo.Border border []tomo.Border
@ -45,13 +49,23 @@ type box struct {
} }
} }
func (backend *Backend) NewBox() tomo.Box { func (backend *Backend) newBox (outer anyBox) *box {
box := &box { box := &box {
backend: backend, backend: backend,
color: color.White, color: color.White,
outer: outer,
drawer: outer,
} }
box.drawer = box if outer == nil {
box.outer = box box.drawer = box
box.outer = box
}
box.invalidateMinimum()
return box
}
func (backend *Backend) NewBox() tomo.Box {
box := backend.newBox(nil)
return box return box
} }
@ -105,23 +119,20 @@ func (this *box) SetColor (c color.Color) {
func (this *box) SetBorder (border ...tomo.Border) { func (this *box) SetBorder (border ...tomo.Border) {
this.border = border this.border = border
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *box) SetMinimumSize (size image.Point) { func (this *box) SetMinimumSize (size image.Point) {
if this.minSize == size { return } if this.userMinSize == size { return }
this.minSize = size this.userMinSize = size
this.invalidateMinimum()
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
} }
func (this *box) SetPadding (padding tomo.Inset) { func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return } if this.padding == padding { return }
this.padding = padding this.padding = padding
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *box) SetDNDData (dat data.Data) { func (this *box) SetDNDData (dat data.Data) {
@ -133,9 +144,11 @@ func (this *box) SetDNDAccept (types ...data.Mime) {
} }
func (this *box) SetFocused (focused bool) { func (this *box) SetFocused (focused bool) {
if this.parent == nil { return } if this.parent == nil || this.parent.window () == nil {
window := this.parent.window() focusedCopy := focused
if window == nil { return } this.focusQueued = &focusedCopy
return
}
if !this.focusable { return } if !this.focusable { return }
if this.Focused () && !focused { if this.Focused () && !focused {
@ -251,6 +264,30 @@ func (this *box) drawBorders (can canvas.Canvas) {
} }
} }
func (this *box) contentMinimum () image.Point {
var minimum image.Point
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
return minimum
}
func (this *box) doMinimumSize () {
this.minSize = this.outer.contentMinimum()
if this.minSize.X < this.userMinSize.X {
this.minSize.X = this.userMinSize.X
}
if this.minSize.Y < this.userMinSize.Y {
this.minSize.Y = this.userMinSize.Y
}
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
}
func (this *box) doDraw () { func (this *box) doDraw () {
if this.canvas == nil { return } if this.canvas == nil { return }
if this.drawer != nil { if this.drawer != nil {
@ -273,6 +310,17 @@ func (this *box) setParent (parent parent) {
this.parent = parent this.parent = parent
} }
func (this *box) flushActionQueue () {
if this.parent == nil || this.parent.window() == nil { return }
if this.minSizeQueued {
this.invalidateMinimum()
}
if this.focusQueued != nil {
this.SetFocused(*this.focusQueued)
}
}
func (this *box) recursiveRedo () { func (this *box) recursiveRedo () {
this.doLayout() this.doLayout()
this.doDraw() this.doDraw()
@ -288,10 +336,12 @@ func (this *box) invalidateDraw () {
this.parent.window().invalidateDraw(this.outer) this.parent.window().invalidateDraw(this.outer)
} }
func (this *box) recalculateMinimumSize () { func (this *box) invalidateMinimum () {
if this.outer != anyBox(this) { if this.parent == nil || this.parent.window() == nil {
this.outer.recalculateMinimumSize() this.minSizeQueued = true
return
} }
this.parent.window().invalidateMinimum(this.outer)
} }
func (this *box) canBeFocused () bool { func (this *box) canBeFocused () bool {

View File

@ -8,11 +8,9 @@ type canvasBox struct {
} }
func (backend *Backend) NewCanvasBox () tomo.CanvasBox { func (backend *Backend) NewCanvasBox () tomo.CanvasBox {
box := &canvasBox { this := &canvasBox { }
box: backend.NewBox().(*box), this.box = backend.newBox(this)
} return this
box.outer = box
return box
} }
func (this *canvasBox) Box () tomo.Box { func (this *canvasBox) Box () tomo.Box {

View File

@ -24,13 +24,9 @@ type containerBox struct {
} }
func (backend *Backend) NewContainerBox() tomo.ContainerBox { func (backend *Backend) NewContainerBox() tomo.ContainerBox {
box := &containerBox { this := &containerBox { propagateEvents: true }
box: backend.NewBox().(*box), this.box = backend.newBox(this)
propagateEvents: true, return this
}
box.drawer = box
box.outer = box
return box
} }
func (this *containerBox) SetOverflow (horizontal, vertical bool) { func (this *containerBox) SetOverflow (horizontal, vertical bool) {
@ -69,7 +65,7 @@ func (this *containerBox) SetGap (gap image.Point) {
if this.gap == gap { return } if this.gap == gap { return }
this.gap = gap this.gap = gap
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Add (child tomo.Object) { func (this *containerBox) Add (child tomo.Object) {
@ -77,9 +73,10 @@ func (this *containerBox) Add (child tomo.Object) {
if indexOf(this.children, tomo.Box(box)) > -1 { return } if indexOf(this.children, tomo.Box(box)) > -1 { return }
box.setParent(this) box.setParent(this)
box.flushActionQueue()
this.children = append(this.children, box) this.children = append(this.children, box)
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Delete (child tomo.Object) { func (this *containerBox) Delete (child tomo.Object) {
@ -90,7 +87,7 @@ func (this *containerBox) Delete (child tomo.Object) {
box.setParent(nil) box.setParent(nil)
this.children = remove(this.children, index) this.children = remove(this.children, index)
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Insert (child, before tomo.Object) { func (this *containerBox) Insert (child, before tomo.Object) {
@ -104,7 +101,7 @@ func (this *containerBox) Insert (child, before tomo.Object) {
box.setParent(this) box.setParent(this)
this.children = insert(this.children, index, tomo.Box(box)) this.children = insert(this.children, index, tomo.Box(box))
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Clear () { func (this *containerBox) Clear () {
@ -113,7 +110,7 @@ func (this *containerBox) Clear () {
} }
this.children = nil this.children = nil
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Length () int { func (this *containerBox) Length () int {
@ -130,7 +127,7 @@ func (this *containerBox) At (index int) tomo.Object {
func (this *containerBox) SetLayout (layout tomo.Layout) { func (this *containerBox) SetLayout (layout tomo.Layout) {
this.layout = layout this.layout = layout
this.invalidateLayout() this.invalidateLayout()
this.recalculateMinimumSize() this.invalidateMinimum()
} }
func (this *containerBox) Draw (can canvas.Canvas) { func (this *containerBox) Draw (can canvas.Canvas) {
@ -148,6 +145,13 @@ func (this *containerBox) Draw (can canvas.Canvas) {
} }
} }
func (this *containerBox) flushActionQueue () {
for _, box := range this.children {
box.(anyBox).flushActionQueue()
}
this.box.flushActionQueue()
}
func (this *containerBox) window () *window { func (this *containerBox) window () *window {
if this.parent == nil { return nil } if this.parent == nil { return nil }
return this.parent.window() return this.parent.window()
@ -158,8 +162,7 @@ func (this *containerBox) canvas () canvas.Canvas {
} }
func (this *containerBox) notifyMinimumSizeChange (child anyBox) { func (this *containerBox) notifyMinimumSizeChange (child anyBox) {
this.recalculateMinimumSize() this.invalidateMinimum()
size := child.MinimumSize() size := child.MinimumSize()
bounds := child.Bounds() bounds := child.Bounds()
if bounds.Dx() < size.X || bounds.Dy() < size.Y { if bounds.Dx() < size.X || bounds.Dy() < size.Y {
@ -179,18 +182,15 @@ func (this *containerBox) layoutHints () tomo.LayoutHints {
} }
} }
func (this *containerBox) recalculateMinimumSize () { func (this *containerBox) contentMinimum () image.Point {
if this.layout == nil { minimum := this.box.contentMinimum()
this.SetMinimumSize(image.Point { }) if this.layout != nil {
return minimum = minimum.Add (
this.layout.MinimumSize (
this.layoutHints(),
this.children))
} }
minimum := this.layout.MinimumSize(this.layoutHints(), this.children) return minimum
minimum.X += this.padding.Horizontal()
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
} }
func (this *containerBox) doLayout () { func (this *containerBox) doLayout () {

View File

@ -40,13 +40,17 @@ type parent interface {
type anyBox interface { type anyBox interface {
tomo.Box tomo.Box
doDraw () canvas.Drawer
doLayout ()
setParent (parent) doDraw ()
recursiveRedo () doLayout ()
canBeFocused () bool doMinimumSize ()
boxUnder (image.Point) anyBox contentMinimum () image.Point
recalculateMinimumSize () setParent (parent)
flushActionQueue ()
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point) anyBox
propagate (func (anyBox) bool) bool propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool propagateAlt (func (anyBox) bool) bool
@ -83,10 +87,11 @@ func (window *window) SetRoot (root tomo.Object) {
} else { } else {
box := assertAnyBox(root.GetBox()) box := assertAnyBox(root.GetBox())
box.setParent(window) box.setParent(window)
box.flushActionQueue()
window.invalidateLayout(box) window.invalidateLayout(box)
window.root = box window.root = box
} }
window.recalculateMinimumSize() window.minimumClean = false
} }
func (window *window) window () *window { func (window *window) window () *window {
@ -98,7 +103,11 @@ func (window *window) canvas () canvas.Canvas {
} }
func (window *window) notifyMinimumSizeChange (anyBox) { func (window *window) notifyMinimumSizeChange (anyBox) {
window.recalculateMinimumSize() window.minimumClean = false
}
func (window *window) invalidateMinimum (box anyBox) {
window.needMinimum.Add(box)
} }
func (window *window) invalidateDraw (box anyBox) { func (window *window) invalidateDraw (box anyBox) {
@ -106,8 +115,6 @@ func (window *window) invalidateDraw (box anyBox) {
} }
func (window *window) invalidateLayout (box anyBox) { func (window *window) invalidateLayout (box anyBox) {
// TODO: use a priority queue for this and have the value be the amount
// of parents a box has
window.needLayout.Add(box) window.needLayout.Add(box)
window.invalidateDraw(box) window.invalidateDraw(box)
} }
@ -214,6 +221,12 @@ func (window *window) afterEvent () {
return return
} }
for len(window.needMinimum) > 0 {
window.needMinimum.Pop().doMinimumSize()
}
if !window.minimumClean {
window.doMinimumSize()
}
for len(window.needLayout) > 0 { for len(window.needLayout) > 0 {
window.needLayout.Pop().doLayout() window.needLayout.Pop().doLayout()
} }

View File

@ -39,14 +39,12 @@ type textBox struct {
} }
func (backend *Backend) NewTextBox() tomo.TextBox { func (backend *Backend) NewTextBox() tomo.TextBox {
box := &textBox { this := &textBox {
box: backend.NewBox().(*box),
textColor: color.Black, textColor: color.Black,
dotColor: color.RGBA { B: 255, G: 255, A: 255 }, dotColor: color.RGBA { B: 255, G: 255, A: 255 },
} }
box.box.drawer = box this.box = backend.newBox(this)
box.outer = box return this
return box
} }
func (this *textBox) SetOverflow (horizontal, vertical bool) { func (this *textBox) SetOverflow (horizontal, vertical bool) {
@ -74,7 +72,7 @@ func (this *textBox) SetText (text string) {
if this.text == text { return } if this.text == text { return }
this.text = text this.text = text
this.drawer.SetText([]rune(text)) this.drawer.SetText([]rune(text))
this.recalculateMinimumSize() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
@ -88,14 +86,14 @@ func (this *textBox) SetFace (face font.Face) {
if this.face == face { return } if this.face == face { return }
this.face = face this.face = face
this.drawer.SetFace(face) this.drawer.SetFace(face)
this.recalculateMinimumSize() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
func (this *textBox) SetWrap (wrap bool) { func (this *textBox) SetWrap (wrap bool) {
if this.wrap == wrap { return } if this.wrap == wrap { return }
this.drawer.SetWrap(wrap) this.drawer.SetWrap(wrap)
this.recalculateMinimumSize() this.invalidateMinimum()
this.invalidateLayout() this.invalidateLayout()
} }
@ -271,7 +269,7 @@ func (this *textBox) normalizedLayoutBoundsSpace () image.Rectangle {
return bounds.Sub(bounds.Min) return bounds.Sub(bounds.Min)
} }
func (this *textBox) recalculateMinimumSize () { func (this *textBox) contentMinimum () image.Point {
minimum := image.Pt ( minimum := image.Pt (
this.drawer.Em().Round(), this.drawer.Em().Round(),
this.drawer.LineHeight().Round()) this.drawer.LineHeight().Round())
@ -284,12 +282,7 @@ func (this *textBox) recalculateMinimumSize () {
minimum.Y = textSize.Y minimum.Y = textSize.Y
} }
minimum.X += this.padding.Horizontal() return minimum.Add(this.box.contentMinimum())
minimum.Y += this.padding.Vertical()
borderSum := this.borderSum()
minimum.X += borderSum.Horizontal()
minimum.Y += borderSum.Vertical()
this.SetMinimumSize(minimum)
} }
func (this *textBox) doLayout () { func (this *textBox) doLayout () {

View File

@ -46,9 +46,17 @@ type window struct {
focused anyBox focused anyBox
hovered anyBox hovered anyBox
needDraw boxSet // TODO: needMinimum and needLayout should be priority queues. for the
needLayout boxSet // minimums, we need to start at the deeper parts of the layout tree and
needRedo bool // go upward towards the top. for the layouts, we need to start at the
// top of the layout tree and progressively go deeper. this will
// eliminate redundant layout calculations.
needMinimum boxSet
needLayout boxSet
needDraw boxSet
needRedo bool
minimumClean bool
} }
func (backend *Backend) NewWindow ( func (backend *Backend) NewWindow (
@ -130,7 +138,7 @@ func (backend *Backend) newWindow (
// Connect(backend.x, window.xWindow.Id) // Connect(backend.x, window.xWindow.Id)
window.metrics.bounds = bounds window.metrics.bounds = bounds
window.setMinimumSize(image.Pt(8, 8)) window.doMinimumSize()
backend.windows[window.xWindow.Id] = window backend.windows[window.xWindow.Id] = window
@ -353,15 +361,14 @@ func (window *window) pushRegion (region image.Rectangle) {
subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id) subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id)
} }
func (window *window) recalculateMinimumSize () { func (window *window) doMinimumSize () {
rootMinimum := image.Point { } window.minimumClean = true
if window.root != nil {
rootMinimum = window.root.MinimumSize() size := image.Point { }
} if window.root != nil {
window.setMinimumSize(rootMinimum) size = window.root.MinimumSize()
} }
func (window *window) setMinimumSize (size image.Point) {
if size.X < 8 { size.X = 8 } if size.X < 8 { size.X = 8 }
if size.Y < 8 { size.Y = 8 } if size.Y < 8 { size.Y = 8 }
icccm.WmNormalHintsSet ( icccm.WmNormalHintsSet (