Compare commits

...

2 Commits

Author SHA1 Message Date
1cb9e8091e Combine Row/Column layouts into Contract layout 2024-05-17 03:56:49 -04:00
68d49e1b02 Add flow layout 2024-05-17 03:51:24 -04:00
2 changed files with 208 additions and 78 deletions

123
layouts/flow.go Normal file
View File

@ -0,0 +1,123 @@
package layouts
import "image"
import "git.tebibyte.media/tomo/tomo"
// Flow is a grid layout where the number of rows and columns changes depending
// on the size of the container. It is designed to be used with an overflowing
// container. If the container does not overflow in the correct direction, the
// layout will behave like Contract.
type Flow bool
// FlowVertical is a vertical flow layout.
const FlowVertical Flow = true
// FlowHorizontal is a horizontal flow layout.
const FlowHorizontal Flow = false
func (flow Flow) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
// TODO: write down somewhere that layout minimums aren't taken into
// account when the respective direction is overflowed
return flow.fallback().MinimumSize(hints, boxes)
}
func (flow Flow) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
if flow.v() && !hints.OverflowY || flow.h() && !hints.OverflowX {
flow.fallback().Arrange(hints, boxes)
}
// find a minor size value that will fit all boxes
minorSize := 0
for _, box := range boxes {
boxSize := flow.minor(box.MinimumSize())
if boxSize > minorSize { minorSize = boxSize }
}
if minorSize == 0 { return }
minorSteps :=
(flow.deltaMinor(hints.Bounds) + flow.minor(hints.Gap)) /
(minorSize + flow.minor(hints.Gap))
// arrange
point := hints.Bounds.Min
index := 0
for index < len(boxes) {
// get a slice of boxes for this major step
stepIndexEnd := index + minorSteps
if stepIndexEnd >= len(boxes) { stepIndexEnd = len(boxes) - 1 }
step := boxes[index:stepIndexEnd]
index += minorSteps
// find a major size that will fit all boxes on this major step
majorSize := 0
for _, box := range step {
boxSize := flow.major(box.MinimumSize())
if boxSize > majorSize { majorSize = boxSize }
}
if majorSize == 0 { continue }
// arrange all boxes on this major step
var size image.Point
size = flow.incrMajor(size, majorSize)
size = flow.incrMinor(size, minorSize)
for _, box := range step {
bounds := image.Rectangle { Min: point, Max: point.Add(size) }
box.SetBounds(bounds)
point = flow.incrMinor(point, minorSize + flow.minor(hints.Gap))
}
if flow.v() {
point.Y += majorSize + hints.Gap.Y
point.X = hints.Bounds.Min.X
} else {
point.X += majorSize + hints.Gap.X
point.Y = hints.Bounds.Min.Y
}
}
}
func (flow Flow) v () bool { return flow == FlowVertical }
func (flow Flow) h () bool { return flow == FlowHorizontal }
func (flow Flow) minor (point image.Point) int {
if flow.v() {
return point.X
} else {
return point.Y
}
}
func (flow Flow) major (point image.Point) int {
if flow.v() {
return point.Y
} else {
return point.X
}
}
func (flow Flow) incrMinor (point image.Point, delta int) image.Point {
if flow.v() {
point.X += delta
return point
} else {
point.Y += delta
return point
}
}
func (flow Flow) incrMajor (point image.Point, delta int) image.Point {
if flow.v() {
point.Y += delta
return point
} else {
point.X += delta
return point
}
}
func (flow Flow) deltaMinor (rectangle image.Rectangle) int {
if flow.v() {
return rectangle.Dx()
} else {
return rectangle.Dy()
}
}
func (flow Flow) fallback () tomo.Layout {
return Contract(flow)
}

View File

@ -3,51 +3,18 @@ package layouts
import "image" import "image"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
type Row struct { } // Contract is a layout that arranges boxes in a simple row or column according
// to their minimum sizes.
type Contract bool
func (Row) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point { // ContractVertical is a vertical contracted layout.
dot := image.Point { } const ContractVertical Contract = true
for _, box := range boxes {
minimum := box.MinimumSize()
dot.X += minimum.X
if dot.Y < minimum.Y {
dot.Y = minimum.Y
}
}
dot.X += hints.Gap.X * (len(boxes) - 1)
return dot
}
func (Row) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) { // ContractHorizontal is a horizontal contracted layout.
// TODO respect alignment value const ContractHorizontal Contract = false
dot := hints.Bounds.Min
for index, box := range boxes {
if index > 0 { dot.X += hints.Gap.X }
minimum := box.MinimumSize()
box.SetBounds(image.Rectangle {
Min: dot,
Max: dot.Add(image.Pt(minimum.X, hints.Bounds.Dy())),
})
dot.X += minimum.X
}
width := dot.X - hints.Bounds.Min.X func (contract Contract) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
offset := 0 if contract.v() {
switch hints.AlignX {
case tomo.AlignMiddle:
offset = (hints.Bounds.Dx() - width) / 2
case tomo.AlignEnd:
offset = hints.Bounds.Dx() - width
}
for _, box := range boxes {
box.SetBounds(box.Bounds().Add(image.Pt(offset, 0)))
}
}
type Column struct { }
func (Column) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
dot := image.Point { } dot := image.Point { }
for _, box := range boxes { for _, box := range boxes {
minimum := box.MinimumSize() minimum := box.MinimumSize()
@ -58,10 +25,22 @@ func (Column) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point
} }
dot.Y += hints.Gap.Y * (len(boxes) - 1) dot.Y += hints.Gap.Y * (len(boxes) - 1)
return dot return dot
} else {
dot := image.Point { }
for _, box := range boxes {
minimum := box.MinimumSize()
dot.X += minimum.X
if dot.Y < minimum.Y {
dot.Y = minimum.Y
}
}
dot.X += hints.Gap.X * (len(boxes) - 1)
return dot
}
} }
func (Column) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) { func (contract Contract) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
// TODO respect alignment value if contract.v() {
dot := hints.Bounds.Min dot := hints.Bounds.Min
for index, box := range boxes { for index, box := range boxes {
if index > 0 { dot.Y += hints.Gap.Y } if index > 0 { dot.Y += hints.Gap.Y }
@ -85,4 +64,32 @@ func (Column) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
for _, box := range boxes { for _, box := range boxes {
box.SetBounds(box.Bounds().Add(image.Pt(0, offset))) box.SetBounds(box.Bounds().Add(image.Pt(0, offset)))
} }
} else {
dot := hints.Bounds.Min
for index, box := range boxes {
if index > 0 { dot.X += hints.Gap.X }
minimum := box.MinimumSize()
box.SetBounds(image.Rectangle {
Min: dot,
Max: dot.Add(image.Pt(minimum.X, hints.Bounds.Dy())),
})
dot.X += minimum.X
}
width := dot.X - hints.Bounds.Min.X
offset := 0
switch hints.AlignX {
case tomo.AlignMiddle:
offset = (hints.Bounds.Dx() - width) / 2
case tomo.AlignEnd:
offset = hints.Bounds.Dx() - width
}
for _, box := range boxes {
box.SetBounds(box.Bounds().Add(image.Pt(offset, 0)))
}
}
} }
func (contract Contract) v () bool { return contract == ContractVertical }
func (contract Contract) h () bool { return contract == ContractHorizontal }