summaryrefslogtreecommitdiff
path: root/tui/layout.go
diff options
context:
space:
mode:
Diffstat (limited to 'tui/layout.go')
-rw-r--r--tui/layout.go361
1 files changed, 361 insertions, 0 deletions
diff --git a/tui/layout.go b/tui/layout.go
new file mode 100644
index 0000000..1b3c3ed
--- /dev/null
+++ b/tui/layout.go
@@ -0,0 +1,361 @@
+package tui
+
+import (
+ "github.com/rivo/uniseg"
+ "strings"
+ "unicode"
+)
+
+type Box struct {
+ Width, Height BoxSize
+ Dir Direction
+ Overflow bool
+ Scroll int
+ Style *Style
+ Margins [4]int
+ text []textRun
+ computedLines [][]textRun
+ children []*Box
+ computedPosition int
+ computedSize [2]int
+ computedRect rect
+}
+
+type BoxSize int
+const (
+ Fill = -(iota + 1)
+ Children
+ TextSize
+)
+
+func (s BoxSize) isFlexible() bool {
+ return s == Fill
+}
+
+type Direction int
+const (
+ Down = iota
+ Up
+ Right
+ Left
+)
+
+func (d Direction) axis() int {
+ switch d {
+ case Down, Up:
+ return 1
+ default:
+ return 0
+ }
+}
+
+func (d Direction) reverse() bool {
+ switch d {
+ case Down, Right:
+ return false
+ default:
+ return true
+ }
+}
+
+type textRun struct {
+ text string
+ style *Style
+}
+
+type rect struct {
+ min [2]int
+ max [2]int
+}
+
+type BoxEvent struct {
+}
+
+type TextEvent struct {
+}
+
+var layout struct {
+ front map[string]*Box
+ back map[string]*Box
+ stack []*Box
+}
+
+func init() {
+ layout.front = make(map[string]*Box)
+ layout.back = make(map[string]*Box)
+}
+
+func top() *Box {
+ return layout.stack[len(layout.stack) - 1]
+}
+
+func Push(id string, box Box) BoxEvent {
+ if len(layout.stack) > 0 {
+ top().children = append(top().children, &box)
+ }
+ layout.stack = append(layout.stack, &box)
+ layout.front[id] = &box
+ for axis, value := range box.axes() {
+ if value >= 0 {
+ box.computedSize[axis] = int(value)
+ }
+ }
+ return BoxEvent {}
+}
+
+func Text(text string, style *Style) TextEvent {
+ top().text = append(top().text, textRun {text, style})
+ return TextEvent {}
+}
+
+func Pop() {
+ if len(layout.stack) > 1 {
+ layout.stack = layout.stack[:len(layout.stack) - 1]
+ }
+}
+
+func (b Box) axes() [2]BoxSize {
+ return [2]BoxSize {b.Width, b.Height}
+}
+
+func (b *Box) marginsSize(axis int) int {
+ return b.Margins[axis * 2] + b.Margins[axis * 2 + 1]
+}
+
+func (b *Box) computeFillSizes(axis int) {
+ for _, c := range b.children {
+ if c.axes()[axis] == Fill {
+ c.computedSize[axis] = b.computedSize[axis]
+ }
+ c.computeFillSizes(axis)
+ }
+}
+
+func (b *Box) computeChildrenSizes(axis int) {
+ size := b.marginsSize(axis)
+ for _, c := range b.children {
+ c.computeChildrenSizes(axis)
+ if b.Dir.axis() == axis {
+ size += c.computedSize[axis]
+ } else {
+ size = max(size, c.computedSize[axis])
+ }
+ }
+ if b.axes()[axis] == Children {
+ b.computedSize[axis] = size
+ }
+}
+
+func (b *Box) solve(axis int) {
+ if b.Dir.axis() == axis && !b.Overflow {
+ size := b.marginsSize(axis)
+ nFlexible := 0
+ for _, c := range b.children {
+ size += c.computedSize[axis]
+ if c.axes()[axis].isFlexible() {
+ nFlexible++
+ }
+ }
+ excess := max(size - b.computedSize[axis], 0)
+ if nFlexible > 0 {
+ contribution := excess / nFlexible
+ for _, c := range b.children {
+ if c.axes()[axis].isFlexible() {
+ if contribution == excess - 1 {
+ contribution = excess
+ }
+ shave := min(c.computedSize[axis], contribution, excess)
+ c.computedSize[axis] -= shave
+ excess -= shave
+ }
+ }
+ }
+ for i := len(b.children) - 1; i >= 0; i-- {
+ c := b.children[i]
+ if excess == 0 {
+ break
+ }
+ shave := min(c.computedSize[axis], excess)
+ c.computedSize[axis] -= shave
+ excess -= shave
+ }
+ } else {
+ maxSize := b.computedSize[axis] - b.marginsSize(axis)
+ for _, c := range b.children {
+ c.computedSize[axis] = min(c.computedSize[axis], maxSize)
+ }
+ }
+ for _, c := range b.children {
+ c.solve(axis)
+ }
+}
+
+func (b *Box) computeText(axis int) {
+ if axis == 0 {
+ maxLine := 0
+ line := 0
+ for _, t := range b.text {
+ g := uniseg.NewGraphemes(t.text)
+ for g.Next() {
+ line += g.Width()
+ if g.LineBreak() == uniseg.LineMustBreak {
+ maxLine = max(maxLine, line)
+ line = 0
+ }
+ }
+ }
+ if b.Width == TextSize {
+ b.computedSize[axis] = maxLine + b.marginsSize(axis)
+ }
+ } else {
+ var (
+ limit = b.computedSize[0]
+ line []textRun
+ text, word strings.Builder
+ lineWidth, wordWidth int
+ run textRun
+ )
+ breakLine := func() {
+ if text.Len() != 0 {
+ line = append(line, textRun {text.String(), run.style})
+ }
+ text.Reset()
+ b.computedLines = append(b.computedLines, line)
+ lineWidth = 0
+ line = nil
+ }
+ flushWord := func() {
+ if lineWidth + wordWidth > limit {
+ breakLine()
+ }
+ g := uniseg.NewGraphemes(word.String())
+ for g.Next() {
+ if g.Width() > limit {
+ continue
+ }
+ if lineWidth == 0 && g.Str() == " " {
+ continue
+ }
+ text.WriteString(g.Str())
+ if g.Width() + lineWidth > limit {
+ breakLine()
+ }
+ lineWidth += g.Width()
+ }
+ wordWidth = 0
+ word.Reset()
+ }
+ for _, run := range b.text {
+ g := uniseg.NewGraphemes(run.text)
+ for g.Next() {
+ if g.LineBreak() == uniseg.LineCanBreak {
+ flushWord()
+ }
+ if lineWidth != 0 || !unicode.IsSpace(g.Runes()[0]) {
+ word.WriteString(g.Str())
+ wordWidth += g.Width()
+ }
+ _, end := g.Positions()
+ if end == len(run.text) {
+ flushWord()
+ break
+ }
+ if g.LineBreak() == uniseg.LineMustBreak {
+ flushWord()
+ breakLine()
+ }
+ }
+ if text.Len() != 0 {
+ line = append(line, textRun {text.String(), run.style})
+ text.Reset()
+ }
+ }
+ if len(line) != 0 || text.Len() != 0 {
+ breakLine()
+ }
+ if b.Height == TextSize {
+ b.computedSize[axis] = len(b.computedLines) + b.marginsSize(axis)
+ }
+ }
+ for _, c := range b.children {
+ c.computeText(axis)
+ }
+}
+
+func (b *Box) computePositions(axis int) {
+ if b.Dir.axis() == axis {
+ pos := 0 + b.Margins[axis * 2]
+ if b.Dir.reverse() {
+ pos = b.computedSize[axis] - b.Margins[axis * 2 + 1]
+ }
+ for _, c := range b.children {
+ if b.Dir.reverse() {
+ pos -= c.computedSize[axis]
+ }
+ c.computedPosition = pos
+ c.computedRect.min[axis] = b.computedRect.min[axis] + pos
+ c.computedRect.max[axis] = c.computedRect.min[axis] + c.computedSize[axis]
+ if !b.Dir.reverse() {
+ pos += c.computedSize[axis]
+ }
+ c.computePositions(axis)
+ }
+ } else {
+ for _, c := range b.children {
+ c.computedRect.min[axis] = b.computedRect.min[axis] + b.Margins[axis * 2]
+ c.computedRect.max[axis] = c.computedRect.min[axis] + c.computedSize[axis]
+ c.computePositions(axis)
+ }
+ }
+}
+
+func (b *Box) drawComputed(parentStyle Style) {
+ style := parentStyle
+ if b.Style != nil {
+ style = *b.Style
+ for y := b.computedRect.min[1]; y < b.computedRect.max[1]; y++ {
+ for x := b.computedRect.min[0]; x < b.computedRect.max[0]; x++ {
+ WriteAt(x, y, " ", style)
+ }
+ }
+ }
+ for i, l := range b.computedLines {
+ x := b.computedRect.min[0] + b.Margins[0]
+ y := b.computedRect.min[1] + b.Margins[2] + i
+ if y > b.computedRect.max[1] - b.Margins[3] {
+ break
+ }
+ for _, t := range l {
+ var s Style
+ if t.style != nil {
+ s = *t.style
+ } else {
+ s = style
+ }
+ x += WriteAt(x, y, t.text, s)
+ }
+ }
+ for _, c := range b.children {
+ c.drawComputed(style)
+ }
+}
+
+func DrawLayout() {
+ defer func() {
+ layout.back = layout.front
+ layout.front = make(map[string]*Box)
+ layout.stack = nil
+ }()
+ if len(layout.stack) == 0 {
+ return
+ }
+ b := layout.stack[0]
+ for axis := 0; axis < 2; axis++ {
+ b.computeFillSizes(axis)
+ b.computeText(axis)
+ b.computeChildrenSizes(axis)
+ b.solve(axis)
+ b.computePositions(axis)
+ }
+ b.drawComputed(DefaultStyle)
+}