| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- package layout
- import (
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/sst/opencode/internal/theme"
- )
- type Container interface {
- tea.Model
- tea.ViewModel
- Sizeable
- Focusable
- Alignable
- }
- type container struct {
- width int
- height int
- x int
- y int
- content tea.ViewModel
- paddingTop int
- paddingRight int
- paddingBottom int
- paddingLeft int
- borderTop bool
- borderRight bool
- borderBottom bool
- borderLeft bool
- borderStyle lipgloss.Border
- maxWidth int
- align lipgloss.Position
- focused bool
- }
- func (c *container) Init() tea.Cmd {
- if model, ok := c.content.(tea.Model); ok {
- return model.Init()
- }
- return nil
- }
- func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- if model, ok := c.content.(tea.Model); ok {
- u, cmd := model.Update(msg)
- c.content = u.(tea.ViewModel)
- return c, cmd
- }
- return c, nil
- }
- func (c *container) View() string {
- t := theme.CurrentTheme()
- style := lipgloss.NewStyle()
- width := c.width
- height := c.height
- // Apply max width constraint if set
- if c.maxWidth > 0 && width > c.maxWidth {
- width = c.maxWidth
- }
- style = style.Background(t.Background())
- // Apply border if any side is enabled
- if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
- // Adjust width and height for borders
- if c.borderTop {
- height--
- }
- if c.borderBottom {
- height--
- }
- if c.borderLeft {
- width--
- }
- if c.borderRight {
- width--
- }
- style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
- // Use primary color for border if focused
- if c.focused {
- style = style.BorderBackground(t.Background()).BorderForeground(t.Primary())
- } else {
- style = style.BorderBackground(t.Background()).BorderForeground(t.Border())
- }
- }
- style = style.
- Width(width).
- Height(height).
- PaddingTop(c.paddingTop).
- PaddingRight(c.paddingRight).
- PaddingBottom(c.paddingBottom).
- PaddingLeft(c.paddingLeft)
- return style.Render(c.content.View())
- }
- func (c *container) SetSize(width, height int) tea.Cmd {
- c.width = width
- c.height = height
- // Apply max width constraint if set
- effectiveWidth := width
- if c.maxWidth > 0 && width > c.maxWidth {
- effectiveWidth = c.maxWidth
- }
- // If the content implements Sizeable, adjust its size to account for padding and borders
- if sizeable, ok := c.content.(Sizeable); ok {
- // Calculate horizontal space taken by padding and borders
- horizontalSpace := c.paddingLeft + c.paddingRight
- if c.borderLeft {
- horizontalSpace++
- }
- if c.borderRight {
- horizontalSpace++
- }
- // Calculate vertical space taken by padding and borders
- verticalSpace := c.paddingTop + c.paddingBottom
- if c.borderTop {
- verticalSpace++
- }
- if c.borderBottom {
- verticalSpace++
- }
- // Set content size with adjusted dimensions
- contentWidth := max(0, effectiveWidth-horizontalSpace)
- contentHeight := max(0, height-verticalSpace)
- return sizeable.SetSize(contentWidth, contentHeight)
- }
- return nil
- }
- func (c *container) GetSize() (int, int) {
- return min(c.width, c.maxWidth), c.height
- }
- func (c *container) MaxWidth() int {
- return c.maxWidth
- }
- func (c *container) Alignment() lipgloss.Position {
- return c.align
- }
- // Focus sets the container as focused
- func (c *container) Focus() tea.Cmd {
- c.focused = true
- if focusable, ok := c.content.(Focusable); ok {
- return focusable.Focus()
- }
- return nil
- }
- // Blur removes focus from the container
- func (c *container) Blur() tea.Cmd {
- c.focused = false
- if blurable, ok := c.content.(Focusable); ok {
- return blurable.Blur()
- }
- return nil
- }
- func (c *container) IsFocused() bool {
- if blurable, ok := c.content.(Focusable); ok {
- return blurable.IsFocused()
- }
- return c.focused
- }
- // GetPosition returns the x, y coordinates of the container
- func (c *container) GetPosition() (x, y int) {
- return c.x, c.y
- }
- func (c *container) SetPosition(x, y int) {
- c.x = x
- c.y = y
- }
- type ContainerOption func(*container)
- func NewContainer(content tea.ViewModel, options ...ContainerOption) Container {
- c := &container{
- content: content,
- borderStyle: lipgloss.NormalBorder(),
- }
- for _, option := range options {
- option(c)
- }
- return c
- }
- // Padding options
- func WithPadding(top, right, bottom, left int) ContainerOption {
- return func(c *container) {
- c.paddingTop = top
- c.paddingRight = right
- c.paddingBottom = bottom
- c.paddingLeft = left
- }
- }
- func WithPaddingAll(padding int) ContainerOption {
- return WithPadding(padding, padding, padding, padding)
- }
- func WithPaddingHorizontal(padding int) ContainerOption {
- return func(c *container) {
- c.paddingLeft = padding
- c.paddingRight = padding
- }
- }
- func WithPaddingVertical(padding int) ContainerOption {
- return func(c *container) {
- c.paddingTop = padding
- c.paddingBottom = padding
- }
- }
- func WithBorder(top, right, bottom, left bool) ContainerOption {
- return func(c *container) {
- c.borderTop = top
- c.borderRight = right
- c.borderBottom = bottom
- c.borderLeft = left
- }
- }
- func WithBorderAll() ContainerOption {
- return WithBorder(true, true, true, true)
- }
- func WithBorderHorizontal() ContainerOption {
- return WithBorder(true, false, true, false)
- }
- func WithBorderVertical() ContainerOption {
- return WithBorder(false, true, false, true)
- }
- func WithBorderStyle(style lipgloss.Border) ContainerOption {
- return func(c *container) {
- c.borderStyle = style
- }
- }
- func WithRoundedBorder() ContainerOption {
- return WithBorderStyle(lipgloss.RoundedBorder())
- }
- func WithThickBorder() ContainerOption {
- return WithBorderStyle(lipgloss.ThickBorder())
- }
- func WithDoubleBorder() ContainerOption {
- return WithBorderStyle(lipgloss.DoubleBorder())
- }
- func WithMaxWidth(maxWidth int) ContainerOption {
- return func(c *container) {
- c.maxWidth = maxWidth
- }
- }
- func WithAlign(align lipgloss.Position) ContainerOption {
- return func(c *container) {
- c.align = align
- }
- }
- func WithAlignLeft() ContainerOption {
- return WithAlign(lipgloss.Left)
- }
- func WithAlignCenter() ContainerOption {
- return WithAlign(lipgloss.Center)
- }
- func WithAlignRight() ContainerOption {
- return WithAlign(lipgloss.Right)
- }
|