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	Positional
 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) Cursor() *tea.Cursor {
 76	if cursor, ok := c.content.(util.Cursor); ok {
 77		return cursor.Cursor()
 78	}
 79	return nil
 80}
 81
 82func (c *container) View() string {
 83	t := styles.CurrentTheme()
 84	width := c.width
 85	height := c.height
 86
 87	style := t.S().Base
 88
 89	// Apply border if any side is enabled
 90	if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
 91		// Adjust width and height for borders
 92		if c.borderTop {
 93			height--
 94		}
 95		if c.borderBottom {
 96			height--
 97		}
 98		if c.borderLeft {
 99			width--
100		}
101		if c.borderRight {
102			width--
103		}
104		style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
105		style = style.BorderBackground(t.BgBase).BorderForeground(t.Border)
106	}
107	style = style.
108		Width(width).
109		Height(height).
110		PaddingTop(c.paddingTop).
111		PaddingRight(c.paddingRight).
112		PaddingBottom(c.paddingBottom).
113		PaddingLeft(c.paddingLeft)
114
115	contentView := c.content.View()
116	return style.Render(contentView)
117}
118
119func (c *container) SetSize(width, height int) tea.Cmd {
120	c.width = width
121	c.height = height
122
123	// If the content implements Sizeable, adjust its size to account for padding and borders
124	if sizeable, ok := c.content.(Sizeable); ok {
125		// Calculate horizontal space taken by padding and borders
126		horizontalSpace := c.paddingLeft + c.paddingRight
127		if c.borderLeft {
128			horizontalSpace++
129		}
130		if c.borderRight {
131			horizontalSpace++
132		}
133
134		// Calculate vertical space taken by padding and borders
135		verticalSpace := c.paddingTop + c.paddingBottom
136		if c.borderTop {
137			verticalSpace++
138		}
139		if c.borderBottom {
140			verticalSpace++
141		}
142
143		// Set content size with adjusted dimensions
144		contentWidth := max(0, width-horizontalSpace)
145		contentHeight := max(0, height-verticalSpace)
146		return sizeable.SetSize(contentWidth, contentHeight)
147	}
148	return nil
149}
150
151func (c *container) GetSize() (int, int) {
152	return c.width, c.height
153}
154
155func (c *container) SetPosition(x, y int) tea.Cmd {
156	c.x = x
157	c.y = y
158	if positionable, ok := c.content.(Positional); ok {
159		return positionable.SetPosition(x, y)
160	}
161	return nil
162}
163
164func (c *container) Bindings() []key.Binding {
165	if b, ok := c.content.(Help); ok {
166		return b.Bindings()
167	}
168	return nil
169}
170
171// Blur implements Container.
172func (c *container) Blur() tea.Cmd {
173	c.isFocused = false
174	if focusable, ok := c.content.(Focusable); ok {
175		return focusable.Blur()
176	}
177	return nil
178}
179
180// Focus implements Container.
181func (c *container) Focus() tea.Cmd {
182	c.isFocused = true
183	if focusable, ok := c.content.(Focusable); ok {
184		return focusable.Focus()
185	}
186	return nil
187}
188
189// IsFocused implements Container.
190func (c *container) IsFocused() bool {
191	isFocused := c.isFocused
192	if focusable, ok := c.content.(Focusable); ok {
193		isFocused = isFocused || focusable.IsFocused()
194	}
195	return isFocused
196}
197
198// Padding options
199func WithPadding(top, right, bottom, left int) ContainerOption {
200	return func(c *container) {
201		c.paddingTop = top
202		c.paddingRight = right
203		c.paddingBottom = bottom
204		c.paddingLeft = left
205	}
206}
207
208func WithPaddingAll(padding int) ContainerOption {
209	return WithPadding(padding, padding, padding, padding)
210}
211
212func WithPaddingHorizontal(padding int) ContainerOption {
213	return func(c *container) {
214		c.paddingLeft = padding
215		c.paddingRight = padding
216	}
217}
218
219func WithPaddingVertical(padding int) ContainerOption {
220	return func(c *container) {
221		c.paddingTop = padding
222		c.paddingBottom = padding
223	}
224}
225
226func WithBorder(top, right, bottom, left bool) ContainerOption {
227	return func(c *container) {
228		c.borderTop = top
229		c.borderRight = right
230		c.borderBottom = bottom
231		c.borderLeft = left
232	}
233}
234
235func WithBorderAll() ContainerOption {
236	return WithBorder(true, true, true, true)
237}
238
239func WithBorderHorizontal() ContainerOption {
240	return WithBorder(true, false, true, false)
241}
242
243func WithBorderVertical() ContainerOption {
244	return WithBorder(false, true, false, true)
245}
246
247func WithBorderStyle(style lipgloss.Border) ContainerOption {
248	return func(c *container) {
249		c.borderStyle = style
250	}
251}
252
253func WithRoundedBorder() ContainerOption {
254	return WithBorderStyle(lipgloss.RoundedBorder())
255}
256
257func WithThickBorder() ContainerOption {
258	return WithBorderStyle(lipgloss.ThickBorder())
259}
260
261func WithDoubleBorder() ContainerOption {
262	return WithBorderStyle(lipgloss.DoubleBorder())
263}