container.go

  1package layout
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/key"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/crush/internal/tui/styles"
  7	"github.com/charmbracelet/crush/internal/tui/util"
  8	"github.com/charmbracelet/lipgloss/v2"
  9)
 10
 11type Container interface {
 12	util.Model
 13	Sizeable
 14	Help
 15	Positionable
 16	Focusable
 17}
 18type container struct {
 19	width     int
 20	height    int
 21	isFocused bool
 22
 23	x, y int
 24
 25	content util.Model
 26
 27	// Style options
 28	paddingTop    int
 29	paddingRight  int
 30	paddingBottom int
 31	paddingLeft   int
 32
 33	borderTop    bool
 34	borderRight  bool
 35	borderBottom bool
 36	borderLeft   bool
 37	borderStyle  lipgloss.Border
 38}
 39
 40type ContainerOption func(*container)
 41
 42func NewContainer(content util.Model, options ...ContainerOption) Container {
 43	c := &container{
 44		content:     content,
 45		borderStyle: lipgloss.NormalBorder(),
 46	}
 47
 48	for _, option := range options {
 49		option(c)
 50	}
 51
 52	return c
 53}
 54
 55func (c *container) Init() tea.Cmd {
 56	return c.content.Init()
 57}
 58
 59func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 60	switch msg := msg.(type) {
 61	case tea.KeyPressMsg:
 62		if c.IsFocused() {
 63			u, cmd := c.content.Update(msg)
 64			c.content = u.(util.Model)
 65			return c, cmd
 66		}
 67		return c, nil
 68	default:
 69		u, cmd := c.content.Update(msg)
 70		c.content = u.(util.Model)
 71		return c, cmd
 72	}
 73}
 74
 75func (c *container) View() tea.View {
 76	t := styles.CurrentTheme()
 77	width := c.width
 78	height := c.height
 79
 80	style := t.S().Base
 81
 82	// Apply border if any side is enabled
 83	if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
 84		// Adjust width and height for borders
 85		if c.borderTop {
 86			height--
 87		}
 88		if c.borderBottom {
 89			height--
 90		}
 91		if c.borderLeft {
 92			width--
 93		}
 94		if c.borderRight {
 95			width--
 96		}
 97		style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
 98		style = style.BorderBackground(t.BgBase).BorderForeground(t.Border)
 99	}
100	style = style.
101		Width(width).
102		Height(height).
103		PaddingTop(c.paddingTop).
104		PaddingRight(c.paddingRight).
105		PaddingBottom(c.paddingBottom).
106		PaddingLeft(c.paddingLeft)
107
108	contentView := c.content.View()
109	view := tea.NewView(style.Render(contentView.String()))
110	cursor := contentView.Cursor()
111	view.SetCursor(cursor)
112	return view
113}
114
115func (c *container) SetSize(width, height int) tea.Cmd {
116	c.width = width
117	c.height = height
118
119	// If the content implements Sizeable, adjust its size to account for padding and borders
120	if sizeable, ok := c.content.(Sizeable); ok {
121		// Calculate horizontal space taken by padding and borders
122		horizontalSpace := c.paddingLeft + c.paddingRight
123		if c.borderLeft {
124			horizontalSpace++
125		}
126		if c.borderRight {
127			horizontalSpace++
128		}
129
130		// Calculate vertical space taken by padding and borders
131		verticalSpace := c.paddingTop + c.paddingBottom
132		if c.borderTop {
133			verticalSpace++
134		}
135		if c.borderBottom {
136			verticalSpace++
137		}
138
139		// Set content size with adjusted dimensions
140		contentWidth := max(0, width-horizontalSpace)
141		contentHeight := max(0, height-verticalSpace)
142		return sizeable.SetSize(contentWidth, contentHeight)
143	}
144	return nil
145}
146
147func (c *container) GetSize() (int, int) {
148	return c.width, c.height
149}
150
151func (c *container) SetPosition(x, y int) tea.Cmd {
152	c.x = x
153	c.y = y
154	if positionable, ok := c.content.(Positionable); ok {
155		return positionable.SetPosition(x, y)
156	}
157	return nil
158}
159
160func (c *container) Bindings() []key.Binding {
161	if b, ok := c.content.(Help); ok {
162		return b.Bindings()
163	}
164	return nil
165}
166
167// Blur implements Container.
168func (c *container) Blur() tea.Cmd {
169	c.isFocused = false
170	if focusable, ok := c.content.(Focusable); ok {
171		return focusable.Blur()
172	}
173	return nil
174}
175
176// Focus implements Container.
177func (c *container) Focus() tea.Cmd {
178	c.isFocused = true
179	if focusable, ok := c.content.(Focusable); ok {
180		return focusable.Focus()
181	}
182	return nil
183}
184
185// IsFocused implements Container.
186func (c *container) IsFocused() bool {
187	isFocused := c.isFocused
188	if focusable, ok := c.content.(Focusable); ok {
189		isFocused = isFocused || focusable.IsFocused()
190	}
191	return isFocused
192}
193
194// Padding options
195func WithPadding(top, right, bottom, left int) ContainerOption {
196	return func(c *container) {
197		c.paddingTop = top
198		c.paddingRight = right
199		c.paddingBottom = bottom
200		c.paddingLeft = left
201	}
202}
203
204func WithPaddingAll(padding int) ContainerOption {
205	return WithPadding(padding, padding, padding, padding)
206}
207
208func WithPaddingHorizontal(padding int) ContainerOption {
209	return func(c *container) {
210		c.paddingLeft = padding
211		c.paddingRight = padding
212	}
213}
214
215func WithPaddingVertical(padding int) ContainerOption {
216	return func(c *container) {
217		c.paddingTop = padding
218		c.paddingBottom = padding
219	}
220}
221
222func WithBorder(top, right, bottom, left bool) ContainerOption {
223	return func(c *container) {
224		c.borderTop = top
225		c.borderRight = right
226		c.borderBottom = bottom
227		c.borderLeft = left
228	}
229}
230
231func WithBorderAll() ContainerOption {
232	return WithBorder(true, true, true, true)
233}
234
235func WithBorderHorizontal() ContainerOption {
236	return WithBorder(true, false, true, false)
237}
238
239func WithBorderVertical() ContainerOption {
240	return WithBorder(false, true, false, true)
241}
242
243func WithBorderStyle(style lipgloss.Border) ContainerOption {
244	return func(c *container) {
245		c.borderStyle = style
246	}
247}
248
249func WithRoundedBorder() ContainerOption {
250	return WithBorderStyle(lipgloss.RoundedBorder())
251}
252
253func WithThickBorder() ContainerOption {
254	return WithBorderStyle(lipgloss.ThickBorder())
255}
256
257func WithDoubleBorder() ContainerOption {
258	return WithBorderStyle(lipgloss.DoubleBorder())
259}