summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/layout
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tui/internal/layout')
-rw-r--r--packages/tui/internal/layout/container.go52
-rw-r--r--packages/tui/internal/layout/flex.go248
-rw-r--r--packages/tui/internal/layout/layout.go29
-rw-r--r--packages/tui/internal/layout/overlay.go7
-rw-r--r--packages/tui/internal/layout/split.go283
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
- }
-}