turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

controls.go · 305 lines · 7.4 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 6h ago1package ui
2
3import (
4 "github.com/gdamore/tcell/v2"
5
6 "codeberg.org/turbo-editors/turbo-core/theme"
7)
8
9// Label is a line of static text in a dialog. The focus ring skips it.
10type Label struct {
11 FocusBox
12 text string
13}
14
15// NewLabel returns a label showing text.
16func NewLabel(text string) *Label { return &Label{text: text} }
17
18// SetText changes what the label shows.
19func (l *Label) SetText(text string) { l.text = text }
20
21// CanFocus is false: a label is decoration, so Tab walks past it.
22func (l *Label) CanFocus() bool { return false }
23
24// Draw paints the text.
25func (l *Label) Draw(p *Painter, th *theme.Theme) {
26 area := p.Sub(l.Bounds())
27 area.TextLimited(0, 0, area.Size().W, l.text, th.Style(theme.KeyDialogLabel))
28}
29
30// Button is a push button. Its label may carry a hot key, as in "~O~K".
31type Button struct {
32 FocusBox
33 label string
34 action func()
35 Default bool // Enter presses this one even when the focus is elsewhere
36}
37
38// NewButton returns a button that runs action when pressed.
39//
40// ok := ui.NewButton("~O~K", func() { dialog.Close(ui.ResultOK) })
41// ok.Default = true
42func NewButton(label string, action func()) *Button {
43 return &Button{label: label, action: action}
44}
45
46// Label returns the button's label, markers included.
47func (b *Button) Label() string { return b.label }
48
49// ButtonWidth returns how wide a button with this label needs to be, brackets
50// and padding included. Dialogs use it to lay a row of buttons out.
51func ButtonWidth(label string) int { return LabelWidth(label) + 4 }
52
53// Press runs the button's action.
54func (b *Button) Press() {
55 if b.action != nil {
56 b.action()
57 }
58}
59
60// Draw paints the button, framed in brackets the way Turbo Vision drew them.
61func (b *Button) Draw(p *Painter, th *theme.Theme) {
62 area := p.Sub(b.Bounds())
63
64 normal := th.Style(theme.KeyButton)
65 if b.Focused() {
66 normal = th.Style(theme.KeyButtonFocused)
67 }
68 area.Clear(normal)
69
70 marker := " "
71 if b.Focused() || b.Default {
72 marker = "►"
73 }
74 area.Text(0, 0, marker, normal)
75 DrawLabel(area, 2, 0, b.label, normal, th.Style(theme.KeyButtonShortcut))
76}
77
78// HandleKey presses the button on Enter or Space when it has the focus.
79func (b *Button) HandleKey(ev *tcell.EventKey) bool {
80 if !b.Focused() {
81 return false
82 }
83 if ev.Key() == tcell.KeyEnter || (ev.Key() == tcell.KeyRune && ev.Rune() == ' ') {
84 b.Press()
85 return true
86 }
87 return false
88}
89
90// HandleMouse presses the button when it is clicked.
91func (b *Button) HandleMouse(ev *tcell.EventMouse) bool {
92 if !b.hit(ev) {
93 return false
94 }
95 if ev.Buttons() == tcell.Button1 {
96 b.Press()
97 }
98 return true
99}
100
101// InputLine is a single-line text field.
102//
103// It keeps its own cursor and horizontal scroll, so a path longer than the
104// field stays editable; the selection Turbo Vision offered is deliberately
105// left out, since a dialog field rarely needs it.
106type InputLine struct {
107 FocusBox
108 text []rune
109 cursor int
110 offset int
111
112 // OnChange is called after every edit, which is what lets a file dialog
113 // refilter its list as the user types.
114 OnChange func(string)
115}
116
117// NewInputLine returns a field holding text, with the cursor at its end.
118func NewInputLine(text string) *InputLine {
119 runes := []rune(text)
120 return &InputLine{text: runes, cursor: len(runes)}
121}
122
123// Text returns what the field holds.
124func (i *InputLine) Text() string { return string(i.text) }
125
126// SetText replaces the content and puts the cursor at its end.
127func (i *InputLine) SetText(text string) {
128 i.text = []rune(text)
129 i.cursor = len(i.text)
130 i.offset = 0
131 i.notify()
132}
133
134// notify tells the owner the content changed.
135func (i *InputLine) notify() {
136 if i.OnChange != nil {
137 i.OnChange(string(i.text))
138 }
139}
140
141// Draw paints the field and, when focused, puts the terminal cursor in it.
142func (i *InputLine) Draw(p *Painter, th *theme.Theme) {
143 area := p.Sub(i.Bounds())
144 width := area.Size().W
145
146 style := th.Style(theme.KeyInput)
147 if i.Focused() {
148 style = th.Style(theme.KeyInputFocused)
149 }
150 area.Clear(style)
151
152 i.scrollToCursor(width)
153 visible := i.text[min(i.offset, len(i.text)):]
154 area.Text(0, 0, string(visible), style)
155
156 if i.Focused() {
157 area.ShowCursor(i.cursor-i.offset, 0)
158 }
159}
160
161// scrollToCursor slides the visible window so the cursor is inside it.
162func (i *InputLine) scrollToCursor(width int) {
163 if width <= 0 {
164 return
165 }
166 i.offset = min(i.offset, i.cursor)
167 if i.cursor-i.offset >= width {
168 i.offset = i.cursor - width + 1
169 }
170}
171
172// HandleKey edits the field.
173func (i *InputLine) HandleKey(ev *tcell.EventKey) bool {
174 if !i.Focused() {
175 return false
176 }
177
178 switch ev.Key() {
179 case tcell.KeyLeft:
180 i.cursor = max(i.cursor-1, 0)
181 case tcell.KeyRight:
182 i.cursor = min(i.cursor+1, len(i.text))
183 case tcell.KeyHome:
184 i.cursor = 0
185 case tcell.KeyEnd:
186 i.cursor = len(i.text)
187 case tcell.KeyBackspace, tcell.KeyBackspace2:
188 i.deleteAt(i.cursor - 1)
189 case tcell.KeyDelete:
190 i.deleteAt(i.cursor)
191 case tcell.KeyCtrlU:
192 i.SetText("")
193 case tcell.KeyRune:
194 // Alt-letter belongs to the dialog's buttons, not to the text: typing
195 // it here would swallow the shortcut before it could be matched.
196 if ev.Modifiers()&(tcell.ModAlt|tcell.ModCtrl) != 0 {
197 return false
198 }
199 i.insert(ev.Rune())
200 default:
201 return false
202 }
203 return true
204}
205
206// insert puts a character in at the cursor.
207func (i *InputLine) insert(r rune) {
208 i.text = append(i.text[:i.cursor], append([]rune{r}, i.text[i.cursor:]...)...)
209 i.cursor++
210 i.notify()
211}
212
213// deleteAt removes the character at an index, ignoring one out of range.
214func (i *InputLine) deleteAt(index int) {
215 if index < 0 || index >= len(i.text) {
216 return
217 }
218 i.text = append(i.text[:index], i.text[index+1:]...)
219 i.cursor = index
220 i.notify()
221}
222
223// HandleMouse puts the cursor where the field was clicked.
224func (i *InputLine) HandleMouse(ev *tcell.EventMouse) bool {
225 if !i.hit(ev) {
226 return false
227 }
228 if ev.Buttons() == tcell.Button1 {
229 x, _ := ev.Position()
230 i.cursor = min(max(x-i.Bounds().X+i.offset, 0), len(i.text))
231 }
232 return true
233}
234
235// CheckBox is an on/off box with a label beside it.
236type CheckBox struct {
237 FocusBox
238 label string
239 checked bool
240
241 // OnToggle is called whenever the box changes state.
242 OnToggle func(bool)
243}
244
245// NewCheckBox returns a box, initially in the given state.
246func NewCheckBox(label string, checked bool) *CheckBox {
247 return &CheckBox{label: label, checked: checked}
248}
249
250// Checked reports whether the box is ticked.
251func (c *CheckBox) Checked() bool { return c.checked }
252
253// SetChecked sets the box's state and calls OnToggle.
254func (c *CheckBox) SetChecked(checked bool) {
255 c.checked = checked
256 if c.OnToggle != nil {
257 c.OnToggle(checked)
258 }
259}
260
261// Toggle flips the box.
262func (c *CheckBox) Toggle() { c.SetChecked(!c.checked) }
263
264// Draw paints the box and its label.
265func (c *CheckBox) Draw(p *Painter, th *theme.Theme) {
266 area := p.Sub(c.Bounds())
267
268 style := th.Style(theme.KeyCheckbox)
269 if c.Focused() {
270 style = th.Style(theme.KeyCheckboxFocused)
271 }
272 area.Clear(style)
273
274 mark := ' '
275 if c.checked {
276 mark = '×'
277 }
278 area.Text(0, 0, "[", style)
279 area.SetCell(1, 0, mark, style)
280 area.Text(2, 0, "] ", style)
281 DrawLabel(area, 4, 0, c.label, style, style)
282}
283
284// HandleKey toggles the box on Space or Enter.
285func (c *CheckBox) HandleKey(ev *tcell.EventKey) bool {
286 if !c.Focused() {
287 return false
288 }
289 if ev.Key() == tcell.KeyEnter || (ev.Key() == tcell.KeyRune && ev.Rune() == ' ') {
290 c.Toggle()
291 return true
292 }
293 return false
294}
295
296// HandleMouse toggles the box when it is clicked.
297func (c *CheckBox) HandleMouse(ev *tcell.EventMouse) bool {
298 if !c.hit(ev) {
299 return false
300 }
301 if ev.Buttons() == tcell.Button1 {
302 c.Toggle()
303 }
304 return true
305}