diff options
| author | adamdottv <[email protected]> | 2025-06-05 15:44:20 -0500 |
|---|---|---|
| committer | adamdottv <[email protected]> | 2025-06-11 11:43:28 -0500 |
| commit | 95d5e1f2318e0c62f19196122fc2a448f1114cfd (patch) | |
| tree | 75369872d32e10896e9263ddbf32cf36e7e418ac /packages/tui/internal/layout | |
| parent | 979bad3e64e3fff43d41094a79c73deb31e82ec8 (diff) | |
| download | opencode-95d5e1f2318e0c62f19196122fc2a448f1114cfd.tar.gz opencode-95d5e1f2318e0c62f19196122fc2a448f1114cfd.zip | |
wip: refactoring tui
Diffstat (limited to 'packages/tui/internal/layout')
| -rw-r--r-- | packages/tui/internal/layout/container.go | 52 | ||||
| -rw-r--r-- | packages/tui/internal/layout/flex.go | 248 | ||||
| -rw-r--r-- | packages/tui/internal/layout/layout.go | 29 | ||||
| -rw-r--r-- | packages/tui/internal/layout/overlay.go | 7 | ||||
| -rw-r--r-- | packages/tui/internal/layout/split.go | 283 |
5 files changed, 329 insertions, 290 deletions
diff --git a/packages/tui/internal/layout/container.go b/packages/tui/internal/layout/container.go index 00166b1d8..1c12ef6a1 100644 --- a/packages/tui/internal/layout/container.go +++ b/packages/tui/internal/layout/container.go @@ -13,6 +13,8 @@ type Container interface { Bindings Focus() Blur() + MaxWidth() int + Alignment() lipgloss.Position } type container struct { @@ -32,6 +34,9 @@ type container struct { borderLeft bool borderStyle lipgloss.Border + maxWidth int + align lipgloss.Position + focused bool } @@ -51,6 +56,11 @@ func (c *container) View() string { 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 @@ -74,7 +84,7 @@ func (c *container) View() string { if c.focused { style = style.BorderBackground(t.Background()).BorderForeground(t.Primary()) } else { - style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal()) + style = style.BorderBackground(t.Background()).BorderForeground(t.Border()) } } style = style. @@ -92,6 +102,12 @@ 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 @@ -113,7 +129,7 @@ func (c *container) SetSize(width, height int) tea.Cmd { } // Set content size with adjusted dimensions - contentWidth := max(0, width-horizontalSpace) + contentWidth := max(0, effectiveWidth-horizontalSpace) contentHeight := max(0, height-verticalSpace) return sizeable.SetSize(contentWidth, contentHeight) } @@ -124,6 +140,14 @@ func (c *container) GetSize() (int, int) { return c.width, c.height } +func (c *container) MaxWidth() int { + return c.maxWidth +} + +func (c *container) Alignment() lipgloss.Position { + return c.align +} + func (c *container) BindingKeys() []key.Binding { if b, ok := c.content.(Bindings); ok { return b.BindingKeys() @@ -228,3 +252,27 @@ func WithThickBorder() ContainerOption { 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) +} diff --git a/packages/tui/internal/layout/flex.go b/packages/tui/internal/layout/flex.go new file mode 100644 index 000000000..a0882196e --- /dev/null +++ b/packages/tui/internal/layout/flex.go @@ -0,0 +1,248 @@ +package layout + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/sst/opencode/internal/theme" +) + +type FlexDirection int + +const ( + FlexDirectionHorizontal FlexDirection = iota + FlexDirectionVertical +) + +type FlexPaneSize struct { + Fixed bool + Size int +} + +var FlexPaneSizeGrow = FlexPaneSize{Fixed: false} + +func FlexPaneSizeFixed(size int) FlexPaneSize { + return FlexPaneSize{Fixed: true, Size: size} +} + +type FlexLayout interface { + tea.Model + Sizeable + Bindings + SetPanes(panes []Container) tea.Cmd + SetPaneSizes(sizes []FlexPaneSize) tea.Cmd + SetDirection(direction FlexDirection) tea.Cmd +} + +type flexLayout struct { + width int + height int + direction FlexDirection + panes []Container + sizes []FlexPaneSize +} + +type FlexLayoutOption func(*flexLayout) + +func (f *flexLayout) Init() tea.Cmd { + var cmds []tea.Cmd + for _, pane := range f.panes { + if pane != nil { + cmds = append(cmds, pane.Init()) + } + } + return tea.Batch(cmds...) +} + +func (f *flexLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + return f, f.SetSize(msg.Width, msg.Height) + } + + for i, pane := range f.panes { + if pane != nil { + u, cmd := pane.Update(msg) + f.panes[i] = u.(Container) + if cmd != nil { + cmds = append(cmds, cmd) + } + } + } + + return f, tea.Batch(cmds...) +} + +func (f *flexLayout) View() string { + t := theme.CurrentTheme() + + if len(f.panes) == 0 { + return "" + } + + views := make([]string, 0, len(f.panes)) + for i, pane := range f.panes { + if pane == nil { + continue + } + + var paneWidth, paneHeight int + if f.direction == FlexDirectionHorizontal { + paneWidth, paneHeight = f.calculatePaneSize(i) + view := lipgloss.PlaceHorizontal( + paneWidth, + pane.Alignment(), + pane.View(), + lipgloss.WithWhitespaceBackground(t.Background()), + ) + views = append(views, view) + } else { + paneWidth, paneHeight = f.calculatePaneSize(i) + view := lipgloss.Place( + f.width, + paneHeight, + lipgloss.Center, + pane.Alignment(), + pane.View(), + lipgloss.WithWhitespaceBackground(t.Background()), + ) + views = append(views, view) + } + } + + if f.direction == FlexDirectionHorizontal { + return lipgloss.JoinHorizontal(lipgloss.Center, views...) + } + return lipgloss.JoinVertical(lipgloss.Center, views...) +} + +func (f *flexLayout) calculatePaneSize(index int) (width, height int) { + if index >= len(f.panes) { + return 0, 0 + } + + totalFixed := 0 + flexCount := 0 + + for i, pane := range f.panes { + if pane == nil { + continue + } + if i < len(f.sizes) && f.sizes[i].Fixed { + if f.direction == FlexDirectionHorizontal { + totalFixed += f.sizes[i].Size + } else { + totalFixed += f.sizes[i].Size + } + } else { + flexCount++ + } + } + + if f.direction == FlexDirectionHorizontal { + height = f.height + if index < len(f.sizes) && f.sizes[index].Fixed { + width = f.sizes[index].Size + } else if flexCount > 0 { + remainingSpace := f.width - totalFixed + width = remainingSpace / flexCount + } + } else { + width = f.width + if index < len(f.sizes) && f.sizes[index].Fixed { + height = f.sizes[index].Size + } else if flexCount > 0 { + remainingSpace := f.height - totalFixed + height = remainingSpace / flexCount + } + } + + return width, height +} + +func (f *flexLayout) SetSize(width, height int) tea.Cmd { + f.width = width + f.height = height + + var cmds []tea.Cmd + for i, pane := range f.panes { + if pane != nil { + paneWidth, paneHeight := f.calculatePaneSize(i) + cmd := pane.SetSize(paneWidth, paneHeight) + cmds = append(cmds, cmd) + } + } + return tea.Batch(cmds...) +} + +func (f *flexLayout) GetSize() (int, int) { + return f.width, f.height +} + +func (f *flexLayout) SetPanes(panes []Container) tea.Cmd { + f.panes = panes + if f.width > 0 && f.height > 0 { + return f.SetSize(f.width, f.height) + } + return nil +} + +func (f *flexLayout) SetPaneSizes(sizes []FlexPaneSize) tea.Cmd { + f.sizes = sizes + if f.width > 0 && f.height > 0 { + return f.SetSize(f.width, f.height) + } + return nil +} + +func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd { + f.direction = direction + if f.width > 0 && f.height > 0 { + return f.SetSize(f.width, f.height) + } + return nil +} + +func (f *flexLayout) BindingKeys() []key.Binding { + keys := []key.Binding{} + for _, pane := range f.panes { + if pane != nil { + if b, ok := pane.(Bindings); ok { + keys = append(keys, b.BindingKeys()...) + } + } + } + return keys +} + +func NewFlexLayout(options ...FlexLayoutOption) FlexLayout { + layout := &flexLayout{ + direction: FlexDirectionHorizontal, + panes: []Container{}, + sizes: []FlexPaneSize{}, + } + for _, option := range options { + option(layout) + } + return layout +} + +func WithDirection(direction FlexDirection) FlexLayoutOption { + return func(f *flexLayout) { + f.direction = direction + } +} + +func WithPanes(panes ...Container) FlexLayoutOption { + return func(f *flexLayout) { + f.panes = panes + } +} + +func WithPaneSizes(sizes ...FlexPaneSize) FlexLayoutOption { + return func(f *flexLayout) { + f.sizes = sizes + } +} + diff --git a/packages/tui/internal/layout/layout.go b/packages/tui/internal/layout/layout.go index 495a3fbc5..05f07f1a2 100644 --- a/packages/tui/internal/layout/layout.go +++ b/packages/tui/internal/layout/layout.go @@ -7,6 +7,35 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +var Current *LayoutInfo + +func init() { + Current = &LayoutInfo{ + Size: LayoutSizeNormal, + Viewport: Dimensions{Width: 80, Height: 25}, + Container: Dimensions{Width: 80, Height: 25}, + } +} + +type LayoutSize string + +const ( + LayoutSizeSmall LayoutSize = "small" + LayoutSizeNormal LayoutSize = "normal" + LayoutSizeLarge LayoutSize = "large" +) + +type Dimensions struct { + Width int + Height int +} + +type LayoutInfo struct { + Size LayoutSize + Viewport Dimensions + Container Dimensions +} + type Focusable interface { Focus() tea.Cmd Blur() tea.Cmd diff --git a/packages/tui/internal/layout/overlay.go b/packages/tui/internal/layout/overlay.go index 031cdecfe..bc349096a 100644 --- a/packages/tui/internal/layout/overlay.go +++ b/packages/tui/internal/layout/overlay.go @@ -17,18 +17,15 @@ import ( // https://github.com/charmbracelet/lipgloss/pull/102 // as well as the lipgloss library, with some modification for what I needed. -// Split a string into lines, additionally returning the size of the widest -// line. +// Split a string into lines, additionally returning the size of the widest line. func getLines(s string) (lines []string, widest int) { lines = strings.Split(s, "\n") - for _, l := range lines { w := ansi.PrintableRuneWidth(l) if widest < w { widest = w } } - return lines, widest } @@ -49,7 +46,7 @@ func PlaceOverlay( var shadowbg string = "" shadowchar := lipgloss.NewStyle(). - Background(t.BackgroundDarker()). + Background(t.BackgroundElement()). Foreground(t.Background()). Render("░") bgchar := baseStyle.Render(" ") diff --git a/packages/tui/internal/layout/split.go b/packages/tui/internal/layout/split.go deleted file mode 100644 index 42b8deeab..000000000 --- a/packages/tui/internal/layout/split.go +++ /dev/null @@ -1,283 +0,0 @@ -package layout - -import ( - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sst/opencode/internal/theme" -) - -type SplitPaneLayout interface { - tea.Model - Sizeable - Bindings - SetLeftPanel(panel Container) tea.Cmd - SetRightPanel(panel Container) tea.Cmd - SetBottomPanel(panel Container) tea.Cmd - - ClearLeftPanel() tea.Cmd - ClearRightPanel() tea.Cmd - ClearBottomPanel() tea.Cmd -} - -type splitPaneLayout struct { - width int - height int - ratio float64 - verticalRatio float64 - - rightPanel Container - leftPanel Container - bottomPanel Container -} - -type SplitPaneOption func(*splitPaneLayout) - -func (s *splitPaneLayout) Init() tea.Cmd { - var cmds []tea.Cmd - - if s.leftPanel != nil { - cmds = append(cmds, s.leftPanel.Init()) - } - - if s.rightPanel != nil { - cmds = append(cmds, s.rightPanel.Init()) - } - - if s.bottomPanel != nil { - cmds = append(cmds, s.bottomPanel.Init()) - } - - return tea.Batch(cmds...) -} - -func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - return s, s.SetSize(msg.Width, msg.Height) - } - - if s.rightPanel != nil { - u, cmd := s.rightPanel.Update(msg) - s.rightPanel = u.(Container) - if cmd != nil { - cmds = append(cmds, cmd) - } - } - - if s.leftPanel != nil { - u, cmd := s.leftPanel.Update(msg) - s.leftPanel = u.(Container) - if cmd != nil { - cmds = append(cmds, cmd) - } - } - - if s.bottomPanel != nil { - u, cmd := s.bottomPanel.Update(msg) - s.bottomPanel = u.(Container) - if cmd != nil { - cmds = append(cmds, cmd) - } - } - - return s, tea.Batch(cmds...) -} - -func (s *splitPaneLayout) View() string { - var topSection string - - if s.leftPanel != nil && s.rightPanel != nil { - leftView := s.leftPanel.View() - rightView := s.rightPanel.View() - topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView) - } else if s.leftPanel != nil { - topSection = s.leftPanel.View() - } else if s.rightPanel != nil { - topSection = s.rightPanel.View() - } else { - topSection = "" - } - - var finalView string - - if s.bottomPanel != nil && topSection != "" { - bottomView := s.bottomPanel.View() - finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView) - } else if s.bottomPanel != nil { - finalView = s.bottomPanel.View() - } else { - finalView = topSection - } - - if finalView != "" { - t := theme.CurrentTheme() - - style := lipgloss.NewStyle(). - Width(s.width). - Height(s.height). - Background(t.Background()) - - return style.Render(finalView) - } - - return finalView -} - -func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd { - s.width = width - s.height = height - - var topHeight, bottomHeight int - if s.bottomPanel != nil { - topHeight = int(float64(height) * s.verticalRatio) - bottomHeight = height - topHeight - } else { - topHeight = height - bottomHeight = 0 - } - - var leftWidth, rightWidth int - if s.leftPanel != nil && s.rightPanel != nil { - leftWidth = int(float64(width) * s.ratio) - rightWidth = width - leftWidth - } else if s.leftPanel != nil { - leftWidth = width - rightWidth = 0 - } else if s.rightPanel != nil { - leftWidth = 0 - rightWidth = width - } - - var cmds []tea.Cmd - if s.leftPanel != nil { - cmd := s.leftPanel.SetSize(leftWidth, topHeight) - cmds = append(cmds, cmd) - } - - if s.rightPanel != nil { - cmd := s.rightPanel.SetSize(rightWidth, topHeight) - cmds = append(cmds, cmd) - } - - if s.bottomPanel != nil { - cmd := s.bottomPanel.SetSize(width, bottomHeight) - cmds = append(cmds, cmd) - } - return tea.Batch(cmds...) -} - -func (s *splitPaneLayout) GetSize() (int, int) { - return s.width, s.height -} - -func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd { - s.leftPanel = panel - if s.width > 0 && s.height > 0 { - return s.SetSize(s.width, s.height) - } - return nil -} - -func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd { - s.rightPanel = panel - if s.width > 0 && s.height > 0 { - return s.SetSize(s.width, s.height) - } - return nil -} - -func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd { - s.bottomPanel = panel - if s.width > 0 && s.height > 0 { - return s.SetSize(s.width, s.height) - } - return nil -} - -func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd { - s.leftPanel = nil - if s.width > 0 && s.height > 0 { - return s.SetSize(s.width, s.height) - } - return nil -} - -func (s *splitPaneLayout) ClearRightPanel() tea.Cmd { - s.rightPanel = nil - if s.width > 0 && s.height > 0 { - return s.SetSize(s.width, s.height) - } - return nil -} - -func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd { - s.bottomPanel = nil - if s.width > 0 && s.height > 0 { - return s.SetSize(s.width, s.height) - } - return nil -} - -func (s *splitPaneLayout) BindingKeys() []key.Binding { - keys := []key.Binding{} - if s.leftPanel != nil { - if b, ok := s.leftPanel.(Bindings); ok { - keys = append(keys, b.BindingKeys()...) - } - } - if s.rightPanel != nil { - if b, ok := s.rightPanel.(Bindings); ok { - keys = append(keys, b.BindingKeys()...) - } - } - if s.bottomPanel != nil { - if b, ok := s.bottomPanel.(Bindings); ok { - keys = append(keys, b.BindingKeys()...) - } - } - return keys -} - -func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout { - - layout := &splitPaneLayout{ - ratio: 0.7, - verticalRatio: 0.9, // Default 90% for top section, 10% for bottom - } - for _, option := range options { - option(layout) - } - return layout -} - -func WithLeftPanel(panel Container) SplitPaneOption { - return func(s *splitPaneLayout) { - s.leftPanel = panel - } -} - -func WithRightPanel(panel Container) SplitPaneOption { - return func(s *splitPaneLayout) { - s.rightPanel = panel - } -} - -func WithRatio(ratio float64) SplitPaneOption { - return func(s *splitPaneLayout) { - s.ratio = ratio - } -} - -func WithBottomPanel(panel Container) SplitPaneOption { - return func(s *splitPaneLayout) { - s.bottomPanel = panel - } -} - -func WithVerticalRatio(ratio float64) SplitPaneOption { - return func(s *splitPaneLayout) { - s.verticalRatio = ratio - } -} |
