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}