button.go

  1package core
  2
  3import (
  4	"github.com/charmbracelet/bubbles/key"
  5	tea "github.com/charmbracelet/bubbletea"
  6	"github.com/charmbracelet/lipgloss"
  7	"github.com/kujtimiihoxha/termai/internal/tui/styles"
  8)
  9
 10// ButtonKeyMap defines key bindings for the button component
 11type ButtonKeyMap struct {
 12	Enter key.Binding
 13}
 14
 15// DefaultButtonKeyMap returns default key bindings for the button
 16func DefaultButtonKeyMap() ButtonKeyMap {
 17	return ButtonKeyMap{
 18		Enter: key.NewBinding(
 19			key.WithKeys("enter"),
 20			key.WithHelp("enter", "select"),
 21		),
 22	}
 23}
 24
 25// ShortHelp returns keybinding help
 26func (k ButtonKeyMap) ShortHelp() []key.Binding {
 27	return []key.Binding{k.Enter}
 28}
 29
 30// FullHelp returns full help info for keybindings
 31func (k ButtonKeyMap) FullHelp() [][]key.Binding {
 32	return [][]key.Binding{
 33		{k.Enter},
 34	}
 35}
 36
 37// ButtonState represents the state of a button
 38type ButtonState int
 39
 40const (
 41	// ButtonNormal is the default state
 42	ButtonNormal ButtonState = iota
 43	// ButtonHovered is when the button is focused/hovered
 44	ButtonHovered
 45	// ButtonPressed is when the button is being pressed
 46	ButtonPressed
 47	// ButtonDisabled is when the button is disabled
 48	ButtonDisabled
 49)
 50
 51// ButtonVariant defines the visual style variant of a button
 52type ButtonVariant int
 53
 54const (
 55	// ButtonPrimary uses primary color styling
 56	ButtonPrimary ButtonVariant = iota
 57	// ButtonSecondary uses secondary color styling
 58	ButtonSecondary
 59	// ButtonDanger uses danger/error color styling
 60	ButtonDanger
 61	// ButtonWarning uses warning color styling
 62	ButtonWarning
 63	// ButtonNeutral uses neutral color styling
 64	ButtonNeutral
 65)
 66
 67// ButtonMsg is sent when a button is clicked
 68type ButtonMsg struct {
 69	ID      string
 70	Payload any
 71}
 72
 73// ButtonCmp represents a clickable button component
 74type ButtonCmp struct {
 75	id         string
 76	label      string
 77	width      int
 78	height     int
 79	state      ButtonState
 80	variant    ButtonVariant
 81	keyMap     ButtonKeyMap
 82	payload    any
 83	style      lipgloss.Style
 84	hoverStyle lipgloss.Style
 85}
 86
 87// NewButtonCmp creates a new button component
 88func NewButtonCmp(id, label string) *ButtonCmp {
 89	b := &ButtonCmp{
 90		id:      id,
 91		label:   label,
 92		state:   ButtonNormal,
 93		variant: ButtonPrimary,
 94		keyMap:  DefaultButtonKeyMap(),
 95		width:   len(label) + 4, // add some padding
 96		height:  1,
 97	}
 98	b.updateStyles()
 99	return b
100}
101
102// WithVariant sets the button variant
103func (b *ButtonCmp) WithVariant(variant ButtonVariant) *ButtonCmp {
104	b.variant = variant
105	b.updateStyles()
106	return b
107}
108
109// WithPayload sets the payload sent with button events
110func (b *ButtonCmp) WithPayload(payload any) *ButtonCmp {
111	b.payload = payload
112	return b
113}
114
115// WithWidth sets a custom width
116func (b *ButtonCmp) WithWidth(width int) *ButtonCmp {
117	b.width = width
118	b.updateStyles()
119	return b
120}
121
122// updateStyles recalculates styles based on current state and variant
123func (b *ButtonCmp) updateStyles() {
124	// Base styles
125	b.style = styles.Regular.
126		Padding(0, 1).
127		Width(b.width).
128		Align(lipgloss.Center).
129		BorderStyle(lipgloss.RoundedBorder())
130
131	b.hoverStyle = b.style.
132		Bold(true)
133
134	// Variant-specific styling
135	switch b.variant {
136	case ButtonPrimary:
137		b.style = b.style.
138			Foreground(styles.Base).
139			Background(styles.Primary).
140			BorderForeground(styles.Primary)
141
142		b.hoverStyle = b.hoverStyle.
143			Foreground(styles.Base).
144			Background(styles.Blue).
145			BorderForeground(styles.Blue)
146
147	case ButtonSecondary:
148		b.style = b.style.
149			Foreground(styles.Base).
150			Background(styles.Secondary).
151			BorderForeground(styles.Secondary)
152
153		b.hoverStyle = b.hoverStyle.
154			Foreground(styles.Base).
155			Background(styles.Mauve).
156			BorderForeground(styles.Mauve)
157
158	case ButtonDanger:
159		b.style = b.style.
160			Foreground(styles.Base).
161			Background(styles.Error).
162			BorderForeground(styles.Error)
163
164		b.hoverStyle = b.hoverStyle.
165			Foreground(styles.Base).
166			Background(styles.Red).
167			BorderForeground(styles.Red)
168
169	case ButtonWarning:
170		b.style = b.style.
171			Foreground(styles.Text).
172			Background(styles.Warning).
173			BorderForeground(styles.Warning)
174
175		b.hoverStyle = b.hoverStyle.
176			Foreground(styles.Text).
177			Background(styles.Peach).
178			BorderForeground(styles.Peach)
179
180	case ButtonNeutral:
181		b.style = b.style.
182			Foreground(styles.Text).
183			Background(styles.Grey).
184			BorderForeground(styles.Grey)
185
186		b.hoverStyle = b.hoverStyle.
187			Foreground(styles.Text).
188			Background(styles.DarkGrey).
189			BorderForeground(styles.DarkGrey)
190	}
191
192	// Disabled style override
193	if b.state == ButtonDisabled {
194		b.style = b.style.
195			Foreground(styles.SubText0).
196			Background(styles.LightGrey).
197			BorderForeground(styles.LightGrey)
198	}
199}
200
201// SetSize sets the button size
202func (b *ButtonCmp) SetSize(width, height int) {
203	b.width = width
204	b.height = height
205	b.updateStyles()
206}
207
208// Focus sets the button to focused state
209func (b *ButtonCmp) Focus() tea.Cmd {
210	if b.state != ButtonDisabled {
211		b.state = ButtonHovered
212	}
213	return nil
214}
215
216// Blur sets the button to normal state
217func (b *ButtonCmp) Blur() tea.Cmd {
218	if b.state != ButtonDisabled {
219		b.state = ButtonNormal
220	}
221	return nil
222}
223
224// Disable sets the button to disabled state
225func (b *ButtonCmp) Disable() {
226	b.state = ButtonDisabled
227	b.updateStyles()
228}
229
230// Enable enables the button if disabled
231func (b *ButtonCmp) Enable() {
232	if b.state == ButtonDisabled {
233		b.state = ButtonNormal
234		b.updateStyles()
235	}
236}
237
238// IsDisabled returns whether the button is disabled
239func (b *ButtonCmp) IsDisabled() bool {
240	return b.state == ButtonDisabled
241}
242
243// IsFocused returns whether the button is focused
244func (b *ButtonCmp) IsFocused() bool {
245	return b.state == ButtonHovered
246}
247
248// Init initializes the button
249func (b *ButtonCmp) Init() tea.Cmd {
250	return nil
251}
252
253// Update handles messages and user input
254func (b *ButtonCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
255	// Skip updates if disabled
256	if b.state == ButtonDisabled {
257		return b, nil
258	}
259
260	switch msg := msg.(type) {
261	case tea.KeyMsg:
262		// Handle key presses when focused
263		if b.state == ButtonHovered {
264			switch {
265			case key.Matches(msg, b.keyMap.Enter):
266				b.state = ButtonPressed
267				return b, func() tea.Msg {
268					return ButtonMsg{
269						ID:      b.id,
270						Payload: b.payload,
271					}
272				}
273			}
274		}
275	}
276
277	return b, nil
278}
279
280// View renders the button
281func (b *ButtonCmp) View() string {
282	if b.state == ButtonHovered || b.state == ButtonPressed {
283		return b.hoverStyle.Render(b.label)
284	}
285	return b.style.Render(b.label)
286}
287