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:
return nil // FIXME: if we exited due to an error say so
}
for _, window := range backend.windows {
window.afterEvent()
}
backend.afterEvent()
}
}
@ -78,3 +76,9 @@ func (backend *Backend) Do (callback func ()) {
func (backend *Backend) assert () {
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
minSize image.Point
userMinSize image.Point
minSizeQueued bool
focusQueued *bool
padding tomo.Inset
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 {
backend: backend,
color: color.White,
outer: outer,
drawer: outer,
}
if outer == nil {
box.drawer = box
box.outer = box
}
box.invalidateMinimum()
return box
}
func (backend *Backend) NewBox() tomo.Box {
box := backend.newBox(nil)
return box
}
@ -105,23 +119,20 @@ func (this *box) SetColor (c color.Color) {
func (this *box) SetBorder (border ...tomo.Border) {
this.border = border
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *box) SetMinimumSize (size image.Point) {
if this.minSize == size { return }
this.minSize = size
if this.parent != nil {
this.parent.notifyMinimumSizeChange(this)
}
if this.userMinSize == size { return }
this.userMinSize = size
this.invalidateMinimum()
}
func (this *box) SetPadding (padding tomo.Inset) {
if this.padding == padding { return }
this.padding = padding
this.invalidateLayout()
this.recalculateMinimumSize()
this.invalidateMinimum()
}
func (this *box) SetDNDData (dat data.Data) {
@ -133,9 +144,11 @@ func (this *box) SetDNDAccept (types ...data.Mime) {
}
func (this *box) SetFocused (focused bool) {
if this.parent == nil { return }
window := this.parent.window()
if window == nil { return }
if this.parent == nil || this.parent.window () == nil {
focusedCopy := focused
this.focusQueued = &focusedCopy
return
}
if !this.focusable { return }
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 () {
if this.canvas == nil { return }
if this.drawer != nil {
@ -273,6 +310,17 @@ func (this *box) setParent (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 () {
this.doLayout()
this.doDraw()
@ -288,10 +336,12 @@ func (this *box) invalidateDraw () {
this.parent.window().invalidateDraw(this.outer)
}
func (this *box) recalculateMinimumSize () {
if this.outer != anyBox(this) {
this.outer.recalculateMinimumSize()
func (this *box) invalidateMinimum () {
if this.parent == nil || this.parent.window() == nil {
this.minSizeQueued = true
return
}
this.parent.window().invalidateMinimum(this.outer)
}
func (this *box) canBeFocused () bool {

View File

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

View File

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

View File

@ -40,13 +40,17 @@ type parent interface {
type anyBox interface {
tomo.Box
canvas.Drawer
doDraw ()
doLayout ()
doMinimumSize ()
contentMinimum () image.Point
setParent (parent)
flushActionQueue ()
recursiveRedo ()
canBeFocused () bool
boxUnder (image.Point) anyBox
recalculateMinimumSize ()
propagate (func (anyBox) bool) bool
propagateAlt (func (anyBox) bool) bool
@ -83,10 +87,11 @@ func (window *window) SetRoot (root tomo.Object) {
} else {
box := assertAnyBox(root.GetBox())
box.setParent(window)
box.flushActionQueue()
window.invalidateLayout(box)
window.root = box
}
window.recalculateMinimumSize()
window.minimumClean = false
}
func (window *window) window () *window {
@ -98,7 +103,11 @@ func (window *window) canvas () canvas.Canvas {
}
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) {
@ -106,8 +115,6 @@ func (window *window) invalidateDraw (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.invalidateDraw(box)
}
@ -214,6 +221,12 @@ func (window *window) afterEvent () {
return
}
for len(window.needMinimum) > 0 {
window.needMinimum.Pop().doMinimumSize()
}
if !window.minimumClean {
window.doMinimumSize()
}
for len(window.needLayout) > 0 {
window.needLayout.Pop().doLayout()
}

View File

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

View File

@ -46,9 +46,17 @@ type window struct {
focused 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
needDraw boxSet
needRedo bool
minimumClean bool
}
func (backend *Backend) NewWindow (
@ -130,7 +138,7 @@ func (backend *Backend) newWindow (
// Connect(backend.x, window.xWindow.Id)
window.metrics.bounds = bounds
window.setMinimumSize(image.Pt(8, 8))
window.doMinimumSize()
backend.windows[window.xWindow.Id] = window
@ -353,15 +361,14 @@ func (window *window) pushRegion (region image.Rectangle) {
subCanvas.(*xcanvas.Canvas).Push(window.xWindow.Id)
}
func (window *window) recalculateMinimumSize () {
rootMinimum := image.Point { }
func (window *window) doMinimumSize () {
window.minimumClean = true
size := image.Point { }
if window.root != nil {
rootMinimum = window.root.MinimumSize()
}
window.setMinimumSize(rootMinimum)
size = window.root.MinimumSize()
}
func (window *window) setMinimumSize (size image.Point) {
if size.X < 8 { size.X = 8 }
if size.Y < 8 { size.Y = 8 }
icccm.WmNormalHintsSet (