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