diff options
| author | citrons <citrons@mondecitronne.com> | 2025-05-31 17:16:38 -0500 |
|---|---|---|
| committer | citrons <citrons@mondecitronne.com> | 2025-05-31 17:16:57 -0500 |
| commit | b11c892158772f508e494b2726a5d4db1bb74d23 (patch) | |
| tree | 16113353422520259ccc7937e9085a68ac6663a6 /tui | |
| parent | 0a58e68ad438ff43fa5bbecdb8914aa00cab5099 (diff) | |
text input box
Diffstat (limited to 'tui')
| -rw-r--r-- | tui/builder.go | 39 | ||||
| -rw-r--r-- | tui/draw.go | 6 | ||||
| -rw-r--r-- | tui/geometry.go | 32 | ||||
| -rw-r--r-- | tui/layout.go | 131 | ||||
| -rw-r--r-- | tui/style.go | 1 | ||||
| -rw-r--r-- | tui/text_input.go | 140 |
6 files changed, 231 insertions, 118 deletions
diff --git a/tui/builder.go b/tui/builder.go new file mode 100644 index 0000000..e969cec --- /dev/null +++ b/tui/builder.go @@ -0,0 +1,39 @@ +package tui + +import ( + "github.com/rivo/uniseg" +) + +type builder struct { + runs []textRun + width int +} + +func (b *builder) add(s string, style *Style) { + b.width += uniseg.StringWidth(s) + if len(b.runs) == 0 || style != b.runs[len(b.runs) - 1].style { + b.runs = append(b.runs, textRun {s, style, false}) + } else { + run := b.runs[len(b.runs) - 1] + run.text += s + b.runs[len(b.runs) - 1] = run + } +} + +func (b *builder) addRun(run textRun) { + b.width += uniseg.StringWidth(run.text) + b.runs = append(b.runs, run) +} + +func (b *builder) addRuns(runs []textRun) { + for _, run := range runs { + b.addRun(run) + } +} + +func (b *builder) flush() []textRun { + runs := b.runs + b.width = 0 + b.runs = nil + return runs +} diff --git a/tui/draw.go b/tui/draw.go index 37a678e..302f876 100644 --- a/tui/draw.go +++ b/tui/draw.go @@ -131,8 +131,11 @@ func writeClearCursor() { scr.writer.WriteString(terminfo.Get(caps.CursorInvisible)) } -func writeCursor(x int, y int) { +func writeShowCursor() { scr.writer.WriteString(terminfo.Get(caps.CursorNormal)) +} + +func writeCursor(x int, y int) { scr.writer.WriteString(terminfo.Get(caps.CursorAddress, y, x)) } @@ -194,6 +197,7 @@ func Present() error { } } if scr.showCursor { + writeShowCursor() writeCursor(scr.cursor.x, scr.cursor.y) } scr.prevSize = s diff --git a/tui/geometry.go b/tui/geometry.go new file mode 100644 index 0000000..b20960b --- /dev/null +++ b/tui/geometry.go @@ -0,0 +1,32 @@ +package tui + +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 rect struct { + min [2]int + max [2]int +}
\ No newline at end of file diff --git a/tui/layout.go b/tui/layout.go index 0681e8e..e2c3e0f 100644 --- a/tui/layout.go +++ b/tui/layout.go @@ -2,7 +2,6 @@ package tui import ( "github.com/rivo/uniseg" - "strings" "unicode" ) @@ -16,9 +15,6 @@ type Box struct { Scroll *ScrollState text []textRun computedLines [][]textRun - cursorLine int - cursorCol int - hasCursor bool children []*Box computedPosition int computedSize [2]int @@ -36,43 +32,12 @@ 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 hasCursor bool } -type rect struct { - min [2]int - max [2]int -} - type ScrollState struct { at string offset int @@ -81,6 +46,8 @@ type ScrollState struct { atLast bool } +var Selected string + func (s *ScrollState) ToStart() { *s = ScrollState {} } @@ -237,85 +204,32 @@ func (b *Box) computeText(axis int) { } else { var ( limit = b.computedSize[0] - line []textRun - text, word strings.Builder - lineWidth, wordWidth int - hasCursor bool - cursorAt int - run textRun + line, word builder ) - flushText := func() { - if text.Len() != 0 { - line = append(line, textRun {text.String(), run.style, false}) - } - text.Reset() - } - flushLine := func() { - flushText() - b.computedLines = append(b.computedLines, line) - lineWidth = 0 - line = nil - } - breakWord := func() { - if lineWidth + wordWidth > limit { - flushLine() - } - g := uniseg.NewGraphemes(word.String()) - pos := 0 - for g.Next() { - if hasCursor && pos == cursorAt { - b.cursorLine = len(b.computedLines) - b.cursorCol = lineWidth - b.hasCursor = true - hasCursor = false - } - - if g.Width() > limit { - continue - } - if lineWidth == 0 && g.Str() == " " { - continue - } - text.WriteString(g.Str()) - if g.Width() + lineWidth > limit { - flushLine() - } - lineWidth += g.Width() - - pos++ - } - wordWidth = 0 - word.Reset() - } for _, run := range b.text { if run.hasCursor { - hasCursor = true - cursorAt = wordWidth + word.addRun(run) + word.addRun(textRun {"", nil, false}) } g := uniseg.NewGraphemes(run.text) for g.Next() { - if g.LineBreak() == uniseg.LineCanBreak { - breakWord() + 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 lineWidth != 0 || !unicode.IsSpace(g.Runes()[0]) { - word.WriteString(g.Str()) - wordWidth += g.Width() + if unicode.IsSpace(g.Runes()[0]) { + line.addRuns(word.flush()) } - _, end := g.Positions() - if end == len(run.text) { - breakWord() - break - } - if g.LineBreak() == uniseg.LineMustBreak { - breakWord() - flushLine() + if g.Str() == "\n" { + line.addRuns(word.flush()) + b.computedLines = append(b.computedLines, line.flush()) } } - flushText() - } - if len(line) != 0 || text.Len() != 0 { - flushLine() } + line.addRuns(word.flush()) + b.computedLines = append(b.computedLines, line.flush()) if b.Height == TextSize { b.computedSize[axis] = len(b.computedLines) + b.marginsSize(axis) } @@ -458,17 +372,12 @@ func (b *Box) drawComputed(parentRect rect, parentStyle Style) { } else { s = style } + if t.hasCursor { + ShowCursor(x, y) + } x += WriteAt(x, y, t.text, s) } } - if b.hasCursor { - cx := b.cursorCol + b.computedRect.min[0] + b.Margins[0] - cy := b.cursorLine + b.computedRect.min[1] + b.Margins[2] - if cx >= viewRect.min[0] && cx < viewRect.max[0] && - cy >= viewRect.min[1] && cy < viewRect.max[1] { - ShowCursor(cx, cy) - } - } for _, c := range b.children { c.drawComputed(viewRect, style) } diff --git a/tui/style.go b/tui/style.go index ac67233..aa58448 100644 --- a/tui/style.go +++ b/tui/style.go @@ -7,6 +7,7 @@ type Style struct { Italic bool Underline bool Strikethrough bool + selected bool } const ( diff --git a/tui/text_input.go b/tui/text_input.go index a8c7d6d..3bceb24 100644 --- a/tui/text_input.go +++ b/tui/text_input.go @@ -1,15 +1,143 @@ package tui import ( -// "github.com/rivo/uniseg" -// "strings" + "github.com/rivo/uniseg" + "zgo.at/termfo/keys" + "strings" ) type TextInput struct { - Contents string - Cursor int + id string + linesBefore []string + beforeCursor string + selection string + selectionBefore bool + afterCursor string + linesAfter []string } -func (t *TextInput) Update(ev Event) { - t.Cursor = max(t.Cursor, 0) +func (t *TextInput) Text() string { + return t.beforeCursor + t.selection + t.afterCursor +} + +func (t *TextInput) SetText(text string) { + t.beforeCursor = "" + t.afterCursor = text +} + +func toGraphemes(s string) []string { + g := uniseg.NewGraphemes(s) + var result []string + for g.Next() { + result = append(result, g.Str()) + } + return result +} + +func splitRight(s string) (string, string) { + if s == "" { + return "", "" + } + gs := toGraphemes(s) + return strings.Join(gs[:len(gs) - 1], ""), gs[len(gs) - 1] +} + +func splitLeft(s string) (string, string) { + if s == "" { + return "", "" + } + cluster, rest, _, _ := uniseg.FirstGraphemeCluster([]byte(s), 0) + return string(cluster), string(rest) +} + +func (t *TextInput) Left(selection bool, word bool) { + t.Deselect() + var right string + t.beforeCursor, right = splitRight(t.beforeCursor) + t.afterCursor = right + t.afterCursor +} + +func (t *TextInput) Right(selection bool, word bool) { + t.Deselect() + var left string + left, t.afterCursor = splitLeft(t.afterCursor) + t.beforeCursor += left +} + +func (t *TextInput) Start(selection bool) { + t.afterCursor = t.beforeCursor + t.afterCursor + t.beforeCursor = "" +} + +func (t *TextInput) End(selection bool) { + t.beforeCursor = t.beforeCursor + t.afterCursor + t.afterCursor = "" +} + +func (t *TextInput) Selection() string { + return t.selection +} + +func (t *TextInput) Deselect() { + if t.selectionBefore { + t.beforeCursor += t.selection + } else { + t.afterCursor = t.selection + t.afterCursor + } + t.selection = "" +} + +func (t *TextInput) Write(text string) { + t.selection = "" + t.beforeCursor += text +} + +func (t *TextInput) Update(ev Event) (usedKeybind bool) { + if Selected != t.id { + return + } + if ev.TextInput != 0 { + t.Write(string(ev.TextInput)) + } + + selection := ev.Key & keys.Shift != 0 + word := ev.Key & keys.Ctrl != 0 + switch ev.Key.WithoutMods() { + case keys.Left: + t.Left(selection, word) + case keys.Right: + t.Right(selection, word) + case 'a': + if ev.Key & keys.Ctrl != 0 { + t.Start(selection) + } + case 'e': + if ev.Key & keys.Ctrl != 0 { + t.End(selection) + } + case keys.Backspace: + if t.selection != "" { + t.selection = "" + } else { + t.beforeCursor, _ = splitRight(t.beforeCursor) + } + default: + return false + } + return true +} + +func (t *TextInput) Show(id string) { + t.id = id + Push(id, Box {Width: Fill, Height: 4})//TextSize}) + Text(t.beforeCursor, nil) + if t.selectionBefore { + Text(t.selection, &Style {Bg: Blue, Fg: White, selected: true}) + } + Cursor() + if !t.selectionBefore { + Text(t.selection, &Style {Bg: Blue, Fg: White, selected: true}) + } + Text(t.afterCursor, nil) + Pop() } |
