1package ui
2
3import (
4 "fmt"
5 "github.com/bnema/nefergui/internal/edit"
6 "strings"
7)
8
9// Control event values are immutable snapshots; querying them never consumes events.
10type ButtonEvent struct{ activated bool }
11
12func (e ButtonEvent) Activated() bool { return e.activated }
13
14type EditEvent struct{ changed, submitted bool }
15
16func (e EditEvent) Changed() bool { return e.changed }
17func (e EditEvent) Submitted() bool { return e.submitted }
18
19type ChangeEvent struct{ changed bool }
20
21func (e ChangeEvent) Changed() bool { return e.changed }
22func (n Node) Button(label string, options ...ButtonOption) ButtonEvent {
23 child := n.buttonLike("button", "button", label, options)
24 return ButtonEvent{activated: activated(child)}
25}
26
27func (n Node) buttonLike(typ, role, label string, options []ButtonOption) Node {
28 opts := make([]ContainerOption, 0, len(options))
29 for _, o := range options {
30 if c, ok := o.(ContainerOption); ok {
31 opts = append(opts, c)
32 }
33 }
34 child := n.child(typ, role, opts)
35 if !child.valid() {
36 return child
37 }
38 child.element.text = label
39 for _, o := range options {
40 if _, common := o.(ContainerOption); !common {
41 o.button(child.element)
42 }
43 }
44 child.frame.compute(child.element) // disabled is set by control-only options
45 return child
46}
47
48func activated(child Node) bool {
49 if !child.valid() || child.element.disabled {
50 return false
51 }
52 for _, input := range child.element.events {
53 if input.Kind == "activate" {
54 return true
55 }
56 }
57 return false
58}
59func (n Node) Input(label string, value *string, options ...EditOption) EditEvent {
60 return n.editControl("input", label, value, options)
61}
62func (n Node) Textarea(label string, value *string, options ...EditOption) EditEvent {
63 return n.editControl("textarea", label, value, options)
64}
65func (n Node) editControl(typ, label string, value *string, options []EditOption) EditEvent {
66 opts := make([]ContainerOption, 0, len(options))
67 for _, o := range options {
68 if c, ok := o.(ContainerOption); ok {
69 opts = append(opts, c)
70 }
71 }
72 child := n.child(typ, "textbox", opts)
73 if !child.valid() {
74 return EditEvent{}
75 }
76 child.element.text = label
77 for _, o := range options {
78 if _, common := o.(ContainerOption); !common {
79 o.edit(child.element)
80 }
81 }
82 var ev EditEvent
83 if value == nil {
84 return ev
85 }
86 id := idKey(child.element.identity)
87 editor := child.frame.edits[id]
88 if editor == nil {
89 editor = &edit.State{}
90 editor.Sync(*value)
91 editor.Move(len(editor.Value), false)
92 child.frame.edits[id] = editor
93 }
94 if editor.Value != *value {
95 editor.Sync(*value)
96 }
97 if !child.element.disabled {
98 for _, input := range child.element.events {
99 switch input.Kind {
100 case "edit-pointer", "edit-word":
101 editor.PreferredXValid = false
102 place := newEditorGeometry(editor, resultByID(child.frame.layout.Tree, id), child.frame.layout.Display, child.element.password).nearest(input.X, input.Y)
103 if input.Kind == "edit-word" {
104 start, end := editor.WordAt(place)
105 editor.Select(start, end)
106 } else {
107 editor.Move(place, input.Shift)
108 }
109 case "text":
110 editor.PreferredXValid = false
111 ev.changed = editor.Insert(input.Text) || ev.changed
112 case "edit-key":
113 if (input.Text == "up" || input.Text == "down") && typ == "textarea" {
114 moveVisualLine(editor, resultByID(child.frame.layout.Tree, id), child.frame.layout.Display, child.element.password, input.Text == "down", input.Shift)
115 } else {
116 editor.PreferredXValid = false
117 if input.Ctrl && input.Text == "v" && child.frame.owner != nil {
118 if async, ok := child.frame.clipboard.(edit.AsyncClipboard); ok {
119 if err := child.frame.owner.startPaste(async, child.element.identity); err != nil && debugDiagnostics {
120 child.frame.diagnostics = append(child.frame.diagnostics, fmt.Sprintf("paste: %v", err))
121 }
122 break
123 }
124 }
125 ev.changed = editKey(editor, input, child.frame.clipboard, child.element.password) || ev.changed
126 }
127 case "paste-result":
128 changed, err := editor.PasteAsync([]byte(input.Text), input.Err)
129 if err != nil && debugDiagnostics {
130 child.frame.diagnostics = append(child.frame.diagnostics, fmt.Sprintf("paste: %v", err))
131 }
132 ev.changed = changed || ev.changed
133 case "ime-done":
134 ev.changed = editor.Done(input.IME) || ev.changed
135 case "submit":
136 ev.submitted = true
137 }
138 }
139 }
140 *value = editor.Value
141 if child.element.password {
142 child.element.text = editor.Mask()
143 } else {
144 child.element.text = editor.Value
145 }
146 // The composition is projected into the display list at the cursor;
147 // it never enters the bound model before IME commit.
148 if ev.changed {
149 editor.Surround(child.frame.ime, child.element.password)
150 }
151 return ev
152}
153func editKey(s *edit.State, ev inputEvent, clipboard edit.Clipboard, password bool) bool {
154 name := strings.ToLower(ev.Text)
155 if ev.Ctrl {
156 switch name {
157 case "a":
158 s.Select(0, len(s.Value))
159 return false
160 case "c":
161 _ = s.Copy(clipboard, password)
162 return false
163 case "x":
164 changed, _ := s.Cut(clipboard, password)
165 return changed
166 case "v":
167 changed, _ := s.Paste(clipboard)
168 return changed
169 case "left":
170 s.Move(s.WordLeft(), ev.Shift)
171 return false
172 case "right":
173 s.Move(s.WordRight(), ev.Shift)
174 return false
175 case "backspace":
176 return s.Delete(true, true)
177 case "delete":
178 return s.Delete(false, true)
179 }
180 }
181 switch name {
182 case "left":
183 s.Left(ev.Shift)
184 case "right":
185 s.Right(ev.Shift)
186 case "home":
187 s.Move(0, ev.Shift)
188 case "end":
189 s.Move(len(s.Value), ev.Shift)
190 case "backspace":
191 return s.Delete(true, false)
192 case "delete":
193 return s.Delete(false, false)
194 }
195 return false
196}
197func (n Node) Checkbox(label string, value *bool, options ...ButtonOption) ChangeEvent {
198 child := n.buttonLike("checkbox", "checkbox", label, options)
199 if child.valid() && value != nil {
200 child.element.checked = *value
201 }
202 if activated(child) && value != nil {
203 *value = !*value
204 child.element.checked = *value
205 return ChangeEvent{true}
206 }
207 return ChangeEvent{}
208}
209func (n Node) Text(text string, options ...ContainerOption) {
210 child := n.child("text", "", options)
211 if child.valid() {
212 child.element.text = text
213 }
214}
215func (n Node) Heading(text string, options ...HeadingOption) {
216 opts := make([]ContainerOption, 0, len(options))
217 for _, o := range options {
218 if c, ok := o.(ContainerOption); ok {
219 opts = append(opts, c)
220 }
221 }
222 child := n.child("heading", "heading", opts)
223 if !child.valid() {
224 return
225 }
226 child.element.text = text
227 child.element.level = 2
228 for _, o := range options {
229 if _, common := o.(ContainerOption); !common {
230 o.heading(child.element)
231 }
232 }
233}