diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-18 20:17:38 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-21 13:42:27 +0200 |
| commit | 333ea6ec4b2abfc2c1a9c3f6b0918ca5d296347f (patch) | |
| tree | e0d456417368e8716c81ee43b82be3d6ed39c59e /internal/tui/layout | |
| parent | 05d0e86f10369fd0e51a924ac88029fb92591499 (diff) | |
| download | opencode-333ea6ec4b2abfc2c1a9c3f6b0918ca5d296347f.tar.gz opencode-333ea6ec4b2abfc2c1a9c3f6b0918ca5d296347f.zip | |
implement patch, update ui, improve rendering
Diffstat (limited to 'internal/tui/layout')
| -rw-r--r-- | internal/tui/layout/bento.go | 392 | ||||
| -rw-r--r-- | internal/tui/layout/border.go | 121 | ||||
| -rw-r--r-- | internal/tui/layout/container.go | 5 | ||||
| -rw-r--r-- | internal/tui/layout/grid.go | 254 | ||||
| -rw-r--r-- | internal/tui/layout/layout.go | 6 | ||||
| -rw-r--r-- | internal/tui/layout/single.go | 189 | ||||
| -rw-r--r-- | internal/tui/layout/split.go | 37 |
7 files changed, 26 insertions, 978 deletions
diff --git a/internal/tui/layout/bento.go b/internal/tui/layout/bento.go deleted file mode 100644 index c47c4e090..000000000 --- a/internal/tui/layout/bento.go +++ /dev/null @@ -1,392 +0,0 @@ -package layout - -import ( - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -type paneID string - -const ( - BentoLeftPane paneID = "left" - BentoRightTopPane paneID = "right-top" - BentoRightBottomPane paneID = "right-bottom" -) - -type BentoPanes map[paneID]tea.Model - -const ( - defaultLeftWidthRatio = 0.2 - defaultRightTopHeightRatio = 0.85 - - minLeftWidth = 10 - minRightBottomHeight = 10 -) - -type BentoLayout interface { - tea.Model - Sizeable - Bindings -} - -type BentoKeyBindings struct { - SwitchPane key.Binding - SwitchPaneBack key.Binding - HideCurrentPane key.Binding - ShowAllPanes key.Binding -} - -var defaultBentoKeyBindings = BentoKeyBindings{ - SwitchPane: key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "switch pane"), - ), - SwitchPaneBack: key.NewBinding( - key.WithKeys("shift+tab"), - key.WithHelp("shift+tab", "switch pane back"), - ), - HideCurrentPane: key.NewBinding( - key.WithKeys("X"), - key.WithHelp("X", "hide current pane"), - ), - ShowAllPanes: key.NewBinding( - key.WithKeys("R"), - key.WithHelp("R", "show all panes"), - ), -} - -type bentoLayout struct { - width int - height int - - leftWidthRatio float64 - rightTopHeightRatio float64 - - currentPane paneID - panes map[paneID]SinglePaneLayout - hiddenPanes map[paneID]bool -} - -func (b *bentoLayout) GetSize() (int, int) { - return b.width, b.height -} - -func (b *bentoLayout) Init() tea.Cmd { - var cmds []tea.Cmd - for _, pane := range b.panes { - cmd := pane.Init() - if cmd != nil { - cmds = append(cmds, cmd) - } - } - return tea.Batch(cmds...) -} - -func (b *bentoLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - b.SetSize(msg.Width, msg.Height) - return b, nil - case tea.KeyMsg: - switch { - case key.Matches(msg, defaultBentoKeyBindings.SwitchPane): - return b, b.SwitchPane(false) - case key.Matches(msg, defaultBentoKeyBindings.SwitchPaneBack): - return b, b.SwitchPane(true) - case key.Matches(msg, defaultBentoKeyBindings.HideCurrentPane): - return b, b.HidePane(b.currentPane) - case key.Matches(msg, defaultBentoKeyBindings.ShowAllPanes): - for id := range b.hiddenPanes { - delete(b.hiddenPanes, id) - } - b.SetSize(b.width, b.height) - return b, nil - } - } - - var cmds []tea.Cmd - for id, pane := range b.panes { - u, cmd := pane.Update(msg) - b.panes[id] = u.(SinglePaneLayout) - if cmd != nil { - cmds = append(cmds, cmd) - } - } - return b, tea.Batch(cmds...) -} - -func (b *bentoLayout) View() string { - if b.width <= 0 || b.height <= 0 { - return "" - } - - for id, pane := range b.panes { - if b.currentPane == id { - pane.Focus() - } else { - pane.Blur() - } - } - - leftVisible := false - rightTopVisible := false - rightBottomVisible := false - - var leftPane, rightTopPane, rightBottomPane string - - if pane, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] { - leftPane = pane.View() - leftVisible = true - } - - if pane, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] { - rightTopPane = pane.View() - rightTopVisible = true - } - - if pane, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] { - rightBottomPane = pane.View() - rightBottomVisible = true - } - - if leftVisible { - if rightTopVisible || rightBottomVisible { - rightSection := "" - if rightTopVisible && rightBottomVisible { - rightSection = lipgloss.JoinVertical(lipgloss.Top, rightTopPane, rightBottomPane) - } else if rightTopVisible { - rightSection = rightTopPane - } else { - rightSection = rightBottomPane - } - return lipgloss.NewStyle().Width(b.width).Height(b.height).Render( - lipgloss.JoinHorizontal(lipgloss.Left, leftPane, rightSection), - ) - } else { - return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(leftPane) - } - } else if rightTopVisible || rightBottomVisible { - if rightTopVisible && rightBottomVisible { - return lipgloss.NewStyle().Width(b.width).Height(b.height).Render( - lipgloss.JoinVertical(lipgloss.Top, rightTopPane, rightBottomPane), - ) - } else if rightTopVisible { - return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(rightTopPane) - } else { - return lipgloss.NewStyle().Width(b.width).Height(b.height).Render(rightBottomPane) - } - } - return "" -} - -func (b *bentoLayout) SetSize(width int, height int) { - if width < 0 || height < 0 { - return - } - b.width = width - b.height = height - - leftExists := false - rightTopExists := false - rightBottomExists := false - - if _, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] { - leftExists = true - } - if _, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] { - rightTopExists = true - } - if _, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] { - rightBottomExists = true - } - - leftWidth := 0 - rightWidth := 0 - rightTopHeight := 0 - rightBottomHeight := 0 - - if leftExists && (rightTopExists || rightBottomExists) { - leftWidth = int(float64(width) * b.leftWidthRatio) - if leftWidth < minLeftWidth && width >= minLeftWidth { - leftWidth = minLeftWidth - } - rightWidth = width - leftWidth - - if rightTopExists && rightBottomExists { - rightTopHeight = int(float64(height) * b.rightTopHeightRatio) - rightBottomHeight = height - rightTopHeight - - if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight { - rightBottomHeight = minRightBottomHeight - rightTopHeight = height - rightBottomHeight - } - } else if rightTopExists { - rightTopHeight = height - } else if rightBottomExists { - rightBottomHeight = height - } - } else if leftExists { - leftWidth = width - } else if rightTopExists || rightBottomExists { - rightWidth = width - - if rightTopExists && rightBottomExists { - rightTopHeight = int(float64(height) * b.rightTopHeightRatio) - rightBottomHeight = height - rightTopHeight - - if rightBottomHeight < minRightBottomHeight && height >= minRightBottomHeight { - rightBottomHeight = minRightBottomHeight - rightTopHeight = height - rightBottomHeight - } - } else if rightTopExists { - rightTopHeight = height - } else if rightBottomExists { - rightBottomHeight = height - } - } - - if pane, ok := b.panes[BentoLeftPane]; ok && !b.hiddenPanes[BentoLeftPane] { - pane.SetSize(leftWidth, height) - } - if pane, ok := b.panes[BentoRightTopPane]; ok && !b.hiddenPanes[BentoRightTopPane] { - pane.SetSize(rightWidth, rightTopHeight) - } - if pane, ok := b.panes[BentoRightBottomPane]; ok && !b.hiddenPanes[BentoRightBottomPane] { - pane.SetSize(rightWidth, rightBottomHeight) - } -} - -func (b *bentoLayout) HidePane(pane paneID) tea.Cmd { - if len(b.panes)-len(b.hiddenPanes) == 1 { - return nil - } - if _, ok := b.panes[pane]; ok { - b.hiddenPanes[pane] = true - } - b.SetSize(b.width, b.height) - return b.SwitchPane(false) -} - -func (b *bentoLayout) SwitchPane(back bool) tea.Cmd { - orderForward := []paneID{BentoLeftPane, BentoRightTopPane, BentoRightBottomPane} - orderBackward := []paneID{BentoLeftPane, BentoRightBottomPane, BentoRightTopPane} - - order := orderForward - if back { - order = orderBackward - } - - currentIdx := -1 - for i, id := range order { - if id == b.currentPane { - currentIdx = i - break - } - } - - if currentIdx == -1 { - for _, id := range order { - if _, exists := b.panes[id]; exists { - if _, hidden := b.hiddenPanes[id]; !hidden { - b.currentPane = id - break - } - } - } - } else { - startIdx := currentIdx - for { - currentIdx = (currentIdx + 1) % len(order) - - nextID := order[currentIdx] - if _, exists := b.panes[nextID]; exists { - if _, hidden := b.hiddenPanes[nextID]; !hidden { - b.currentPane = nextID - break - } - } - - if currentIdx == startIdx { - break - } - } - } - - var cmds []tea.Cmd - for id, pane := range b.panes { - if _, ok := b.hiddenPanes[id]; ok { - continue - } - if id == b.currentPane { - cmds = append(cmds, pane.Focus()) - } else { - cmds = append(cmds, pane.Blur()) - } - } - - return tea.Batch(cmds...) -} - -func (s *bentoLayout) BindingKeys() []key.Binding { - bindings := KeyMapToSlice(defaultBentoKeyBindings) - if b, ok := s.panes[s.currentPane].(Bindings); ok { - bindings = append(bindings, b.BindingKeys()...) - } - return bindings -} - -type BentoLayoutOption func(*bentoLayout) - -func NewBentoLayout(panes BentoPanes, opts ...BentoLayoutOption) BentoLayout { - p := make(map[paneID]SinglePaneLayout, len(panes)) - for id, pane := range panes { - if sp, ok := pane.(SinglePaneLayout); !ok { - p[id] = NewSinglePane( - pane, - WithSinglePaneFocusable(true), - WithSinglePaneBordered(true), - ) - } else { - p[id] = sp - } - } - if len(p) == 0 { - panic("no panes provided for BentoLayout") - } - layout := &bentoLayout{ - panes: p, - hiddenPanes: make(map[paneID]bool), - currentPane: BentoLeftPane, - leftWidthRatio: defaultLeftWidthRatio, - rightTopHeightRatio: defaultRightTopHeightRatio, - } - - for _, opt := range opts { - opt(layout) - } - - return layout -} - -func WithBentoLayoutLeftWidthRatio(ratio float64) BentoLayoutOption { - return func(b *bentoLayout) { - if ratio > 0 && ratio < 1 { - b.leftWidthRatio = ratio - } - } -} - -func WithBentoLayoutRightTopHeightRatio(ratio float64) BentoLayoutOption { - return func(b *bentoLayout) { - if ratio > 0 && ratio < 1 { - b.rightTopHeightRatio = ratio - } - } -} - -func WithBentoLayoutCurrentPane(pane paneID) BentoLayoutOption { - return func(b *bentoLayout) { - b.currentPane = pane - } -} diff --git a/internal/tui/layout/border.go b/internal/tui/layout/border.go deleted file mode 100644 index ea9f5e0bc..000000000 --- a/internal/tui/layout/border.go +++ /dev/null @@ -1,121 +0,0 @@ -package layout - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/lipgloss" - "github.com/kujtimiihoxha/opencode/internal/tui/styles" -) - -type BorderPosition int - -const ( - TopLeftBorder BorderPosition = iota - TopMiddleBorder - TopRightBorder - BottomLeftBorder - BottomMiddleBorder - BottomRightBorder -) - -var ( - ActiveBorder = styles.Blue - InactivePreviewBorder = styles.Grey -) - -type BorderOptions struct { - Active bool - EmbeddedText map[BorderPosition]string - ActiveColor lipgloss.TerminalColor - InactiveColor lipgloss.TerminalColor - ActiveBorder lipgloss.Border - InactiveBorder lipgloss.Border -} - -func Borderize(content string, opts BorderOptions) string { - if opts.EmbeddedText == nil { - opts.EmbeddedText = make(map[BorderPosition]string) - } - if opts.ActiveColor == nil { - opts.ActiveColor = ActiveBorder - } - if opts.InactiveColor == nil { - opts.InactiveColor = InactivePreviewBorder - } - if opts.ActiveBorder == (lipgloss.Border{}) { - opts.ActiveBorder = lipgloss.ThickBorder() - } - if opts.InactiveBorder == (lipgloss.Border{}) { - opts.InactiveBorder = lipgloss.NormalBorder() - } - - var ( - thickness = map[bool]lipgloss.Border{ - true: opts.ActiveBorder, - false: opts.InactiveBorder, - } - color = map[bool]lipgloss.TerminalColor{ - true: opts.ActiveColor, - false: opts.InactiveColor, - } - border = thickness[opts.Active] - style = lipgloss.NewStyle().Foreground(color[opts.Active]) - width = lipgloss.Width(content) - ) - - encloseInSquareBrackets := func(text string) string { - if text != "" { - return fmt.Sprintf("%s%s%s", - style.Render(border.TopRight), - text, - style.Render(border.TopLeft), - ) - } - return text - } - buildHorizontalBorder := func(leftText, middleText, rightText, leftCorner, inbetween, rightCorner string) string { - leftText = encloseInSquareBrackets(leftText) - middleText = encloseInSquareBrackets(middleText) - rightText = encloseInSquareBrackets(rightText) - // Calculate length of border between embedded texts - remaining := max(0, width-lipgloss.Width(leftText)-lipgloss.Width(middleText)-lipgloss.Width(rightText)) - leftBorderLen := max(0, (width/2)-lipgloss.Width(leftText)-(lipgloss.Width(middleText)/2)) - rightBorderLen := max(0, remaining-leftBorderLen) - // Then construct border string - s := leftText + - style.Render(strings.Repeat(inbetween, leftBorderLen)) + - middleText + - style.Render(strings.Repeat(inbetween, rightBorderLen)) + - rightText - // Make it fit in the space available between the two corners. - s = lipgloss.NewStyle(). - Inline(true). - MaxWidth(width). - Render(s) - // Add the corners - return style.Render(leftCorner) + s + style.Render(rightCorner) - } - // Stack top border, content and horizontal borders, and bottom border. - return strings.Join([]string{ - buildHorizontalBorder( - opts.EmbeddedText[TopLeftBorder], - opts.EmbeddedText[TopMiddleBorder], - opts.EmbeddedText[TopRightBorder], - border.TopLeft, - border.Top, - border.TopRight, - ), - lipgloss.NewStyle(). - BorderForeground(color[opts.Active]). - Border(border, false, true, false, true).Render(content), - buildHorizontalBorder( - opts.EmbeddedText[BottomLeftBorder], - opts.EmbeddedText[BottomMiddleBorder], - opts.EmbeddedText[BottomRightBorder], - border.BottomLeft, - border.Bottom, - border.BottomRight, - ), - }, "\n") -} diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go index c86d954ea..fdb9ab403 100644 --- a/internal/tui/layout/container.go +++ b/internal/tui/layout/container.go @@ -86,7 +86,7 @@ func (c *container) View() string { return style.Render(c.content.View()) } -func (c *container) SetSize(width, height int) { +func (c *container) SetSize(width, height int) tea.Cmd { c.width = width c.height = height @@ -113,8 +113,9 @@ func (c *container) SetSize(width, height int) { // Set content size with adjusted dimensions contentWidth := max(0, width-horizontalSpace) contentHeight := max(0, height-verticalSpace) - sizeable.SetSize(contentWidth, contentHeight) + return sizeable.SetSize(contentWidth, contentHeight) } + return nil } func (c *container) GetSize() (int, int) { diff --git a/internal/tui/layout/grid.go b/internal/tui/layout/grid.go deleted file mode 100644 index 6be493e2c..000000000 --- a/internal/tui/layout/grid.go +++ /dev/null @@ -1,254 +0,0 @@ -package layout - -import ( - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -type GridLayout interface { - tea.Model - Sizeable - Bindings - Panes() [][]tea.Model -} - -type gridLayout struct { - width int - height int - - rows int - columns int - - panes [][]tea.Model - - gap int - bordered bool - focusable bool - - currentRow int - currentColumn int - - activeColor lipgloss.TerminalColor -} - -type GridOption func(*gridLayout) - -func (g *gridLayout) Init() tea.Cmd { - var cmds []tea.Cmd - for i := range g.panes { - for j := range g.panes[i] { - if g.panes[i][j] != nil { - cmds = append(cmds, g.panes[i][j].Init()) - } - } - } - return tea.Batch(cmds...) -} - -func (g *gridLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - - switch msg := msg.(type) { - case tea.WindowSizeMsg: - g.SetSize(msg.Width, msg.Height) - return g, nil - case tea.KeyMsg: - if key.Matches(msg, g.nextPaneBinding()) { - return g.focusNextPane() - } - } - - // Update all panes - for i := range g.panes { - for j := range g.panes[i] { - if g.panes[i][j] != nil { - var cmd tea.Cmd - g.panes[i][j], cmd = g.panes[i][j].Update(msg) - if cmd != nil { - cmds = append(cmds, cmd) - } - } - } - } - - return g, tea.Batch(cmds...) -} - -func (g *gridLayout) focusNextPane() (tea.Model, tea.Cmd) { - if !g.focusable { - return g, nil - } - - var cmds []tea.Cmd - - // Blur current pane - if g.currentRow < len(g.panes) && g.currentColumn < len(g.panes[g.currentRow]) { - if currentPane, ok := g.panes[g.currentRow][g.currentColumn].(Focusable); ok { - cmds = append(cmds, currentPane.Blur()) - } - } - - // Find next valid pane - g.currentColumn++ - if g.currentColumn >= len(g.panes[g.currentRow]) { - g.currentColumn = 0 - g.currentRow++ - if g.currentRow >= len(g.panes) { - g.currentRow = 0 - } - } - - // Focus next pane - if g.currentRow < len(g.panes) && g.currentColumn < len(g.panes[g.currentRow]) { - if nextPane, ok := g.panes[g.currentRow][g.currentColumn].(Focusable); ok { - cmds = append(cmds, nextPane.Focus()) - } - } - - return g, tea.Batch(cmds...) -} - -func (g *gridLayout) nextPaneBinding() key.Binding { - return key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "next pane"), - ) -} - -func (g *gridLayout) View() string { - if len(g.panes) == 0 { - return "" - } - - // Calculate dimensions for each cell - cellWidth := (g.width - (g.columns-1)*g.gap) / g.columns - cellHeight := (g.height - (g.rows-1)*g.gap) / g.rows - - // Render each row - rows := make([]string, g.rows) - for i := range g.rows { - // Render each column in this row - cols := make([]string, len(g.panes[i])) - for j := range g.panes[i] { - if g.panes[i][j] == nil { - cols[j] = "" - continue - } - - // Set size for each pane - if sizable, ok := g.panes[i][j].(Sizeable); ok { - effectiveWidth, effectiveHeight := cellWidth, cellHeight - if g.bordered { - effectiveWidth -= 2 - effectiveHeight -= 2 - } - sizable.SetSize(effectiveWidth, effectiveHeight) - } - - // Render the pane - content := g.panes[i][j].View() - - // Apply border if needed - if g.bordered { - isFocused := false - if focusable, ok := g.panes[i][j].(Focusable); ok { - isFocused = focusable.IsFocused() - } - - borderText := map[BorderPosition]string{} - if bordered, ok := g.panes[i][j].(Bordered); ok { - borderText = bordered.BorderText() - } - - content = Borderize(content, BorderOptions{ - Active: isFocused, - EmbeddedText: borderText, - }) - } - - cols[j] = content - } - - // Join columns with gap - rows[i] = lipgloss.JoinHorizontal(lipgloss.Top, cols...) - } - - // Join rows with gap - return lipgloss.JoinVertical(lipgloss.Left, rows...) -} - -func (g *gridLayout) SetSize(width, height int) { - g.width = width - g.height = height -} - -func (g *gridLayout) GetSize() (int, int) { - return g.width, g.height -} - -func (g *gridLayout) BindingKeys() []key.Binding { - var bindings []key.Binding - bindings = append(bindings, g.nextPaneBinding()) - - // Collect bindings from all panes - for i := range g.panes { - for j := range g.panes[i] { - if g.panes[i][j] != nil { - if bindable, ok := g.panes[i][j].(Bindings); ok { - bindings = append(bindings, bindable.BindingKeys()...) - } - } - } - } - - return bindings -} - -func (g *gridLayout) Panes() [][]tea.Model { - return g.panes -} - -// NewGridLayout creates a new grid layout with the given number of rows and columns -func NewGridLayout(rows, cols int, panes [][]tea.Model, opts ...GridOption) GridLayout { - grid := &gridLayout{ - rows: rows, - columns: cols, - panes: panes, - gap: 1, - } - - for _, opt := range opts { - opt(grid) - } - - return grid -} - -// WithGridGap sets the gap between cells -func WithGridGap(gap int) GridOption { - return func(g *gridLayout) { - g.gap = gap - } -} - -// WithGridBordered sets whether cells should have borders -func WithGridBordered(bordered bool) GridOption { - return func(g *gridLayout) { - g.bordered = bordered - } -} - -// WithGridFocusable sets whether the grid supports focus navigation -func WithGridFocusable(focusable bool) GridOption { - return func(g *gridLayout) { - g.focusable = focusable - } -} - -// WithGridActiveColor sets the active border color -func WithGridActiveColor(color lipgloss.TerminalColor) GridOption { - return func(g *gridLayout) { - g.activeColor = color - } -} diff --git a/internal/tui/layout/layout.go b/internal/tui/layout/layout.go index 2f17c4a0e..495a3fbc5 100644 --- a/internal/tui/layout/layout.go +++ b/internal/tui/layout/layout.go @@ -13,12 +13,8 @@ type Focusable interface { IsFocused() bool } -type Bordered interface { - BorderText() map[BorderPosition]string -} - type Sizeable interface { - SetSize(width, height int) + SetSize(width, height int) tea.Cmd GetSize() (int, int) } diff --git a/internal/tui/layout/single.go b/internal/tui/layout/single.go deleted file mode 100644 index c77fa0d78..000000000 --- a/internal/tui/layout/single.go +++ /dev/null @@ -1,189 +0,0 @@ -package layout - -import ( - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -type SinglePaneLayout interface { - tea.Model - Focusable - Sizeable - Bindings - Pane() tea.Model -} - -type singlePaneLayout struct { - width int - height int - - focusable bool - focused bool - - bordered bool - borderText map[BorderPosition]string - - content tea.Model - - padding []int - - activeColor lipgloss.TerminalColor -} - -type SinglePaneOption func(*singlePaneLayout) - -func (s *singlePaneLayout) Init() tea.Cmd { - return s.content.Init() -} - -func (s *singlePaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - s.SetSize(msg.Width, msg.Height) - return s, nil - } - u, cmd := s.content.Update(msg) - s.content = u - return s, cmd -} - -func (s *singlePaneLayout) View() string { - style := lipgloss.NewStyle().Width(s.width).Height(s.height) - if s.bordered { - style = style.Width(s.width - 2).Height(s.height - 2) - } - if s.padding != nil { - style = style.Padding(s.padding...) - } - content := style.Render(s.content.View()) - if s.bordered { - if s.borderText == nil { - s.borderText = map[BorderPosition]string{} - } - if bordered, ok := s.content.(Bordered); ok { - s.borderText = bordered.BorderText() - } - return Borderize(content, BorderOptions{ - Active: s.focused, - EmbeddedText: s.borderText, - }) - } - return content -} - -func (s *singlePaneLayout) Blur() tea.Cmd { - if s.focusable { - s.focused = false - } - if blurable, ok := s.content.(Focusable); ok { - return blurable.Blur() - } - return nil -} - -func (s *singlePaneLayout) Focus() tea.Cmd { - if s.focusable { - s.focused = true - } - if focusable, ok := s.content.(Focusable); ok { - return focusable.Focus() - } - return nil -} - -func (s *singlePaneLayout) SetSize(width, height int) { - s.width = width - s.height = height - childWidth, childHeight := s.width, s.height - if s.bordered { - childWidth -= 2 - childHeight -= 2 - } - if s.padding != nil { - if len(s.padding) == 1 { - childWidth -= s.padding[0] * 2 - childHeight -= s.padding[0] * 2 - } else if len(s.padding) == 2 { - childWidth -= s.padding[0] * 2 - childHeight -= s.padding[1] * 2 - } else if len(s.padding) == 3 { - childWidth -= s.padding[0] * 2 - childHeight -= s.padding[1] + s.padding[2] - } else if len(s.padding) == 4 { - childWidth -= s.padding[0] + s.padding[2] - childHeight -= s.padding[1] + s.padding[3] - } - } - if s.content != nil { - if c, ok := s.content.(Sizeable); ok { - c.SetSize(childWidth, childHeight) - } - } -} - -func (s *singlePaneLayout) IsFocused() bool { - return s.focused -} - -func (s *singlePaneLayout) GetSize() (int, int) { - return s.width, s.height -} - -func (s *singlePaneLayout) BindingKeys() []key.Binding { - if b, ok := s.content.(Bindings); ok { - return b.BindingKeys() - } - return []key.Binding{} -} - -func (s *singlePaneLayout) Pane() tea.Model { - return s.content -} - -func NewSinglePane(content tea.Model, opts ...SinglePaneOption) SinglePaneLayout { - layout := &singlePaneLayout{ - content: content, - } - for _, opt := range opts { - opt(layout) - } - return layout -} - -func WithSinglePaneSize(width, height int) SinglePaneOption { - return func(opts *singlePaneLayout) { - opts.width = width - opts.height = height - } -} - -func WithSinglePaneFocusable(focusable bool) SinglePaneOption { - return func(opts *singlePaneLayout) { - opts.focusable = focusable - } -} - -func WithSinglePaneBordered(bordered bool) SinglePaneOption { - return func(opts *singlePaneLayout) { - opts.bordered = bordered - } -} - -func WithSinglePaneBorderText(borderText map[BorderPosition]string) SinglePaneOption { - return func(opts *singlePaneLayout) { - opts.borderText = borderText - } -} - -func WithSinglePanePadding(padding ...int) SinglePaneOption { - return func(opts *singlePaneLayout) { - opts.padding = padding - } -} - -func WithSinglePaneActiveColor(color lipgloss.TerminalColor) SinglePaneOption { - return func(opts *singlePaneLayout) { - opts.activeColor = color - } -} diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go index bfb616a53..a41df6ab8 100644 --- a/internal/tui/layout/split.go +++ b/internal/tui/layout/split.go @@ -11,9 +11,9 @@ type SplitPaneLayout interface { tea.Model Sizeable Bindings - SetLeftPanel(panel Container) - SetRightPanel(panel Container) - SetBottomPanel(panel Container) + SetLeftPanel(panel Container) tea.Cmd + SetRightPanel(panel Container) tea.Cmd + SetBottomPanel(panel Container) tea.Cmd } type splitPaneLayout struct { @@ -53,8 +53,7 @@ func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: - s.SetSize(msg.Width, msg.Height) - return s, nil + return s, s.SetSize(msg.Width, msg.Height) } if s.rightPanel != nil { @@ -122,7 +121,7 @@ func (s *splitPaneLayout) View() string { return finalView } -func (s *splitPaneLayout) SetSize(width, height int) { +func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd { s.width = width s.height = height @@ -147,42 +146,50 @@ func (s *splitPaneLayout) SetSize(width, height int) { rightWidth = width } + var cmds []tea.Cmd if s.leftPanel != nil { - s.leftPanel.SetSize(leftWidth, topHeight) + cmd := s.leftPanel.SetSize(leftWidth, topHeight) + cmds = append(cmds, cmd) } if s.rightPanel != nil { - s.rightPanel.SetSize(rightWidth, topHeight) + cmd := s.rightPanel.SetSize(rightWidth, topHeight) + cmds = append(cmds, cmd) } if s.bottomPanel != nil { - s.bottomPanel.SetSize(width, bottomHeight) + 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) { +func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd { s.leftPanel = panel if s.width > 0 && s.height > 0 { - s.SetSize(s.width, s.height) + return s.SetSize(s.width, s.height) } + return nil } -func (s *splitPaneLayout) SetRightPanel(panel Container) { +func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd { s.rightPanel = panel if s.width > 0 && s.height > 0 { - s.SetSize(s.width, s.height) + return s.SetSize(s.width, s.height) } + return nil } -func (s *splitPaneLayout) SetBottomPanel(panel Container) { +func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd { s.bottomPanel = panel if s.width > 0 && s.height > 0 { - s.SetSize(s.width, s.height) + return s.SetSize(s.width, s.height) } + return nil } func (s *splitPaneLayout) BindingKeys() []key.Binding { |
