From 68d49e1b025c0877226919d15b887516acae26f5 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Fri, 17 May 2024 03:51:24 -0400 Subject: [PATCH] Add flow layout --- layouts/flow.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 layouts/flow.go diff --git a/layouts/flow.go b/layouts/flow.go new file mode 100644 index 0000000..10411cb --- /dev/null +++ b/layouts/flow.go @@ -0,0 +1,127 @@ +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 { + if flow.v() { + return Column { } + } else { + return Row { } + } +}