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}