package tui import ( "github.com/rivo/uniseg" "unicode" ) type Box struct { id string Width, Height BoxSize Dir Direction Overflow bool Style *Style Margins [4]int Scroll *ScrollState NoWrap bool 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 textRun struct { text string style *Style hasCursor bool } type ScrollState struct { at string offset int absolute int atFirst bool atLast bool } var Selected string func (s *ScrollState) ToStart() { *s = ScrollState {} } func (s *ScrollState) AtFirst() bool { return s.atFirst } func (s *ScrollState) AtLast() bool { return s.atLast } func (s *ScrollState) Scroll(amnt int) { s.offset += amnt s.absolute += amnt } func (s *ScrollState) Get() int { return s.absolute } 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) { if len(layout.stack) > 0 { top().children = append(top().children, &box) } layout.stack = append(layout.stack, &box) layout.front[id] = &box box.id = id for axis, value := range box.axes() { if value >= 0 { box.computedSize[axis] = int(value) } } } func Text(text string, style *Style) { top().text = append(top().text, textRun {text, style, false}) } func Cursor() { top().text = append(top().text, textRun {"", nil, true}) } 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() { if g.Str() == "\n" { maxLine = max(maxLine, line) line = 0 } else { line += g.Width() } } } maxLine = max(maxLine, line) if b.Width == TextSize { b.computedSize[axis] = maxLine + b.marginsSize(axis) } } else { var ( limit = b.computedSize[0] line, word builder ) for _, run := range b.text { if run.hasCursor { word.addRun(run) word.addRun(textRun {"", nil, false}) } g := uniseg.NewGraphemes(run.text) for g.Next() { if !b.NoWrap { word.add(g.Str(), run.style) if word.width >= limit { line.addRuns(word.flush()) } else if line.width + word.width > limit { b.computedLines = append(b.computedLines, line.flush()) } if unicode.IsSpace(g.Runes()[0]) { line.addRuns(word.flush()) } } else { line.add(g.Str(), run.style) } if g.Str() == "\n" { line.addRuns(word.flush()) b.computedLines = append(b.computedLines, line.flush()) } } } line.addRuns(word.flush()) b.computedLines = append(b.computedLines, line.flush()) 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 if b.Scroll != nil && b.Scroll.at != "" && b.Scroll.at == c.id { p := c.computedPosition if b.Dir.reverse() { p = b.computedSize[axis] - p } b.Scroll.absolute = p if !b.Dir.reverse() { b.Scroll.absolute += b.Scroll.offset } else { b.Scroll.absolute += b.Scroll.offset } } if !b.Dir.reverse() { pos += c.computedSize[axis] } } if b.Scroll != nil { var first, last *Box if len(b.children) > 0 { first = b.children[0] last = b.children[len(b.children) - 1] } if last != nil { lastPos := last.computedPosition if b.Dir.reverse() { lastPos = b.computedSize[axis] - lastPos } else { lastPos += last.computedSize[axis] } b.Scroll.absolute = min(b.Scroll.absolute, lastPos - b.computedSize[axis]) } b.Scroll.absolute = max(b.Scroll.absolute, 0) var firstShown, lastShown *Box for _, c := range b.children { if !b.Dir.reverse() { c.computedPosition -= b.Scroll.absolute } else { c.computedPosition += b.Scroll.absolute } pos := c.computedPosition + b.computedRect.min[axis] if c.computedPosition >= 0 && pos < b.computedRect.max[axis] { if firstShown == nil { firstShown = c } lastShown = c } } b.Scroll.atFirst = firstShown == first b.Scroll.atLast = lastShown == last if firstShown != nil { p := first.computedPosition if b.Dir.reverse() { p = b.computedSize[axis] - p } b.Scroll.offset = -p b.Scroll.at = first.id } if b.Scroll.absolute == 0 { b.Scroll.at = "" b.Scroll.offset = 0 } } for _, c := range b.children { c.computedRect.min[axis] = b.computedRect.min[axis] + c.computedPosition c.computedRect.max[axis] = c.computedRect.min[axis] + 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(parentRect rect, parentStyle Style) { var viewRect rect = parentRect; for i := 0; i < 2; i++ { bMin := b.computedRect.min[i] + b.Margins[i * 2] viewRect.min[i] = max(bMin, parentRect.min[i]) } for i := 0; i < 2; i++ { bMax := b.computedRect.max[i] - b.Margins[i * 2 + 1] viewRect.max[i] = min(bMax, parentRect.max[i]) } style := parentStyle if b.Style != nil { style = *b.Style for y := viewRect.min[1]; y < viewRect.max[1]; y++ { for x := viewRect.min[0]; x < viewRect.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 } if y < b.computedRect.min[1] + b.Margins[2] { continue } if y >= parentRect.max[1] { break } if y < parentRect.min[1] { continue } for _, t := range l { var s Style if t.style != nil { s = *t.style } else { s = style } if t.hasCursor { ShowCursor(x, y) } g := uniseg.NewGraphemes(t.text) for g.Next() { if x + g.Width() <= b.computedRect.max[0] { x += WriteAt(x, y, g.Str(), s) } else { break } } } } for _, c := range b.children { c.drawComputed(viewRect, 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) } var parentRect rect size := Size() parentRect.max = [2]int {size.Width, size.Height} b.computedRect = parentRect b.drawComputed(parentRect, DefaultStyle) }