package ui import ( "github.com/gdamore/tcell/v2" "rickub.com/turbo-editors/turbo-core/theme" ) // Label is a line of static text in a dialog. The focus ring skips it. type Label struct { FocusBox text string } // NewLabel returns a label showing text. func NewLabel(text string) *Label { return &Label{text: text} } // SetText changes what the label shows. func (l *Label) SetText(text string) { l.text = text } // CanFocus is false: a label is decoration, so Tab walks past it. func (l *Label) CanFocus() bool { return false } // Draw paints the text. func (l *Label) Draw(p *Painter, th *theme.Theme) { area := p.Sub(l.Bounds()) area.TextLimited(0, 0, area.Size().W, l.text, th.Style(theme.KeyDialogLabel)) } // Button is a push button. Its label may carry a hot key, as in "~O~K". type Button struct { FocusBox label string action func() Default bool // Enter presses this one even when the focus is elsewhere } // NewButton returns a button that runs action when pressed. // // ok := ui.NewButton("~O~K", func() { dialog.Close(ui.ResultOK) }) // ok.Default = true func NewButton(label string, action func()) *Button { return &Button{label: label, action: action} } // Label returns the button's label, markers included. func (b *Button) Label() string { return b.label } // ButtonWidth returns how wide a button with this label needs to be, brackets // and padding included. Dialogs use it to lay a row of buttons out. func ButtonWidth(label string) int { return LabelWidth(label) + 4 } // Press runs the button's action. func (b *Button) Press() { if b.action != nil { b.action() } } // Draw paints the button, framed in brackets the way Turbo Vision drew them. func (b *Button) Draw(p *Painter, th *theme.Theme) { area := p.Sub(b.Bounds()) normal := th.Style(theme.KeyButton) if b.Focused() { normal = th.Style(theme.KeyButtonFocused) } area.Clear(normal) marker := " " if b.Focused() || b.Default { marker = "►" } area.Text(0, 0, marker, normal) DrawLabel(area, 2, 0, b.label, normal, th.Style(theme.KeyButtonShortcut)) } // HandleKey presses the button on Enter or Space when it has the focus. func (b *Button) HandleKey(ev *tcell.EventKey) bool { if !b.Focused() { return false } if ev.Key() == tcell.KeyEnter || (ev.Key() == tcell.KeyRune && ev.Rune() == ' ') { b.Press() return true } return false } // HandleMouse presses the button when it is clicked. func (b *Button) HandleMouse(ev *tcell.EventMouse) bool { if !b.hit(ev) { return false } if ev.Buttons() == tcell.Button1 { b.Press() } return true } // InputLine is a single-line text field. // // It keeps its own cursor and horizontal scroll, so a path longer than the // field stays editable; the selection Turbo Vision offered is deliberately // left out, since a dialog field rarely needs it. type InputLine struct { FocusBox text []rune cursor int offset int // OnChange is called after every edit, which is what lets a file dialog // refilter its list as the user types. OnChange func(string) } // NewInputLine returns a field holding text, with the cursor at its end. func NewInputLine(text string) *InputLine { runes := []rune(text) return &InputLine{text: runes, cursor: len(runes)} } // Text returns what the field holds. func (i *InputLine) Text() string { return string(i.text) } // SetText replaces the content and puts the cursor at its end. func (i *InputLine) SetText(text string) { i.text = []rune(text) i.cursor = len(i.text) i.offset = 0 i.notify() } // notify tells the owner the content changed. func (i *InputLine) notify() { if i.OnChange != nil { i.OnChange(string(i.text)) } } // Draw paints the field and, when focused, puts the terminal cursor in it. func (i *InputLine) Draw(p *Painter, th *theme.Theme) { area := p.Sub(i.Bounds()) width := area.Size().W style := th.Style(theme.KeyInput) if i.Focused() { style = th.Style(theme.KeyInputFocused) } area.Clear(style) i.scrollToCursor(width) visible := i.text[min(i.offset, len(i.text)):] area.Text(0, 0, string(visible), style) if i.Focused() { area.ShowCursor(i.cursor-i.offset, 0) } } // scrollToCursor slides the visible window so the cursor is inside it. func (i *InputLine) scrollToCursor(width int) { if width <= 0 { return } i.offset = min(i.offset, i.cursor) if i.cursor-i.offset >= width { i.offset = i.cursor - width + 1 } } // HandleKey edits the field. func (i *InputLine) HandleKey(ev *tcell.EventKey) bool { if !i.Focused() { return false } switch ev.Key() { case tcell.KeyLeft: i.cursor = max(i.cursor-1, 0) case tcell.KeyRight: i.cursor = min(i.cursor+1, len(i.text)) case tcell.KeyHome: i.cursor = 0 case tcell.KeyEnd: i.cursor = len(i.text) case tcell.KeyBackspace, tcell.KeyBackspace2: i.deleteAt(i.cursor - 1) case tcell.KeyDelete: i.deleteAt(i.cursor) case tcell.KeyCtrlU: i.SetText("") case tcell.KeyRune: // Alt-letter belongs to the dialog's buttons, not to the text: typing // it here would swallow the shortcut before it could be matched. if ev.Modifiers()&(tcell.ModAlt|tcell.ModCtrl) != 0 { return false } i.insert(ev.Rune()) default: return false } return true } // insert puts a character in at the cursor. func (i *InputLine) insert(r rune) { i.text = append(i.text[:i.cursor], append([]rune{r}, i.text[i.cursor:]...)...) i.cursor++ i.notify() } // deleteAt removes the character at an index, ignoring one out of range. func (i *InputLine) deleteAt(index int) { if index < 0 || index >= len(i.text) { return } i.text = append(i.text[:index], i.text[index+1:]...) i.cursor = index i.notify() } // HandleMouse puts the cursor where the field was clicked. func (i *InputLine) HandleMouse(ev *tcell.EventMouse) bool { if !i.hit(ev) { return false } if ev.Buttons() == tcell.Button1 { x, _ := ev.Position() i.cursor = min(max(x-i.Bounds().X+i.offset, 0), len(i.text)) } return true } // CheckBox is an on/off box with a label beside it. type CheckBox struct { FocusBox label string checked bool // OnToggle is called whenever the box changes state. OnToggle func(bool) } // NewCheckBox returns a box, initially in the given state. func NewCheckBox(label string, checked bool) *CheckBox { return &CheckBox{label: label, checked: checked} } // Checked reports whether the box is ticked. func (c *CheckBox) Checked() bool { return c.checked } // SetChecked sets the box's state and calls OnToggle. func (c *CheckBox) SetChecked(checked bool) { c.checked = checked if c.OnToggle != nil { c.OnToggle(checked) } } // Toggle flips the box. func (c *CheckBox) Toggle() { c.SetChecked(!c.checked) } // Draw paints the box and its label. func (c *CheckBox) Draw(p *Painter, th *theme.Theme) { area := p.Sub(c.Bounds()) style := th.Style(theme.KeyCheckbox) if c.Focused() { style = th.Style(theme.KeyCheckboxFocused) } area.Clear(style) mark := ' ' if c.checked { mark = '×' } area.Text(0, 0, "[", style) area.SetCell(1, 0, mark, style) area.Text(2, 0, "] ", style) DrawLabel(area, 4, 0, c.label, style, style) } // HandleKey toggles the box on Space or Enter. func (c *CheckBox) HandleKey(ev *tcell.EventKey) bool { if !c.Focused() { return false } if ev.Key() == tcell.KeyEnter || (ev.Key() == tcell.KeyRune && ev.Rune() == ' ') { c.Toggle() return true } return false } // HandleMouse toggles the box when it is clicked. func (c *CheckBox) HandleMouse(ev *tcell.EventMouse) bool { if !c.hit(ev) { return false } if ev.Buttons() == tcell.Button1 { c.Toggle() } return true }