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

80
box.go
View File

@ -15,6 +15,10 @@ type box struct {
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,
} }
if outer == nil {
box.drawer = box box.drawer = box
box.outer = 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
canvas.Drawer
doDraw () doDraw ()
doLayout () doLayout ()
doMinimumSize ()
contentMinimum () image.Point
setParent (parent) setParent (parent)
flushActionQueue ()
recursiveRedo () recursiveRedo ()
canBeFocused () bool canBeFocused () bool
boxUnder (image.Point) anyBox boxUnder (image.Point) anyBox
recalculateMinimumSize ()
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
// minimums, we need to start at the deeper parts of the layout tree and
// 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 needLayout boxSet
needDraw boxSet
needRedo bool 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 (