summaryrefslogtreecommitdiff
path: root/tui
diff options
context:
space:
mode:
Diffstat (limited to 'tui')
-rw-r--r--tui/builder.go39
-rw-r--r--tui/draw.go6
-rw-r--r--tui/geometry.go32
-rw-r--r--tui/layout.go131
-rw-r--r--tui/style.go1
-rw-r--r--tui/text_input.go140
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()
}