1package ui
2
3import (
4 "math"
5 "strings"
6 "testing"
7
8 "github.com/bnema/nefergui/internal/edit"
9 "github.com/bnema/nefergui/internal/layout"
10)
11
12func TestRTLParagraphCaretUsesPrecedingTrailingEdge(t *testing.T) {
13 value := "مرحبا abc"
14 r := geometryRuntime(t, `input {width:300px;height:28px;font-size:16px}`)
15 view := func(f *Frame) { f.Root().Input("mixed", &value) }
16 r.Build(view)
17 r.route(key("Tab"))
18 r.Build(view)
19 n := r.output.Tree.Children[0]
20 offset := len("مرحبا ")
21 s := r.edits[n.ID]
22 s.Move(offset, false)
23 s.Preedit = "x"
24 r.Redraw()
25 r.Build(view)
26 n = r.output.Tree.Children[0]
27 want := rawTrailingEdge(t, r, n, 5)
28 g := newEditorGeometry(s, n, r.output.Display, false)
29 var first *textBoundary
30 for i := range g.lines[0] {
31 if g.lines[0][i].byteOffset == offset {
32 first = &g.lines[0][i]
33 break
34 }
35 }
36 if first == nil || first.trailing || math.Abs(first.x-want) < 1 {
37 t.Fatalf("fixture must encounter a distinct leading edge first: first=%+v trailing=%g", first, want)
38 }
39 for _, op := range []string{"caret", "preedit-text"} {
40 cmd, ok := displayCommand(r, n.ID, op)
41 if !ok || math.Abs(cmd.Rect.X-want) > .01 {
42 t.Fatalf("%s x=%g want %g", op, cmd.Rect.X, want)
43 }
44 }
45 // Place the right viewport edge between the two positions. Only the correct
46 // trailing edge should require a horizontal scroll.
47 if want <= first.x {
48 t.Fatalf("fixture must place trailing edge right of leading edge")
49 }
50 n.Content.W = (first.x+want)/2 - n.Content.X
51 n.ContentSize.W = 500
52 r.state.scroll = map[string]layout.Size{n.ID: {}}
53 expected := want - (n.Content.X + n.Content.W - 1)
54 if !r.scrollCaret(r.committed, &r.output) || math.Abs(r.state.scroll[n.ID].W-expected) > .01 {
55 t.Fatalf("scroll=%+v want %g", r.state.scroll[n.ID], expected)
56 }
57}
58
59func TestPasswordWrappedNavigationUsesLogicalGraphemeOffsets(t *testing.T) {
60 grapheme := "e\u0301"
61 value := strings.Repeat(grapheme, 24)
62 r := geometryRuntime(t, `textarea {width:45px;height:100px;font-size:16px}`)
63 view := func(f *Frame) { f.Root().Textarea("secret", &value, Password(true)) }
64 r.Build(view)
65 r.route(key("Tab"))
66 r.Build(view)
67 n := r.output.Tree.Children[0]
68 if len(n.Lines) < 3 {
69 t.Fatalf("need wrapped password: %d lines", len(n.Lines))
70 }
71 s := r.edits[n.ID]
72 s.Move(0, false)
73 s.PreferredXValid = false
74 // Each shaped source rune is one bullet, hence one logical grapheme. The
75 // starts of the masked runs independently determine the expected offsets.
76 logical := edit.Boundaries(value)
77 want := logical[n.Lines[1].Runs[0].Start]
78 r.route(key("Down"))
79 r.Build(view)
80 if s.Cursor != want {
81 t.Fatalf("down cursor=%d want logical offset %d", s.Cursor, want)
82 }
83 r.route(key("Up"))
84 r.Build(view)
85 if s.Cursor != 0 {
86 t.Fatalf("up cursor=%d want 0", s.Cursor)
87 }
88}