From 23e7a95083a8d875420c90e0479647f18a278c5f Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Fri, 11 Apr 2025 18:54:13 +0200 Subject: intiial layout --- internal/tui/layout/container.go | 224 ++++++++++++++++++++++++++++++++++++++ internal/tui/layout/single.go | 4 +- internal/tui/layout/split.go | 229 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 internal/tui/layout/container.go create mode 100644 internal/tui/layout/split.go (limited to 'internal/tui/layout') diff --git a/internal/tui/layout/container.go b/internal/tui/layout/container.go new file mode 100644 index 000000000..db07d49fb --- /dev/null +++ b/internal/tui/layout/container.go @@ -0,0 +1,224 @@ +package layout + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/kujtimiihoxha/termai/internal/tui/styles" +) + +type Container interface { + tea.Model + Sizeable +} +type container struct { + width int + height int + + content tea.Model + + // Style options + paddingTop int + paddingRight int + paddingBottom int + paddingLeft int + + borderTop bool + borderRight bool + borderBottom bool + borderLeft bool + borderStyle lipgloss.Border + borderColor lipgloss.TerminalColor + + backgroundColor lipgloss.TerminalColor +} + +func (c *container) Init() tea.Cmd { + return c.content.Init() +} + +func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + u, cmd := c.content.Update(msg) + c.content = u + return c, cmd +} + +func (c *container) View() string { + style := lipgloss.NewStyle() + width := c.width + height := c.height + // Apply background color if specified + if c.backgroundColor != nil { + style = style.Background(c.backgroundColor) + } + + // 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) + + // Apply border color if specified + if c.borderColor != nil { + style = style.BorderBackground(c.backgroundColor).BorderForeground(c.borderColor) + } + } + 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) { + c.width = width + c.height = height + + // 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, width-horizontalSpace) + contentHeight := max(0, height-verticalSpace) + sizeable.SetSize(contentWidth, contentHeight) + } +} + +func (c *container) GetSize() (int, int) { + return c.width, c.height +} + +func (c *container) BindingKeys() []key.Binding { + if b, ok := c.content.(Bindings); ok { + return b.BindingKeys() + } + return []key.Binding{} +} + +type ContainerOption func(*container) + +func NewContainer(content tea.Model, options ...ContainerOption) Container { + c := &container{ + content: content, + borderColor: styles.BorderColor, + borderStyle: lipgloss.NormalBorder(), + backgroundColor: styles.Background, + } + + 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 WithBorderColor(color lipgloss.TerminalColor) ContainerOption { + return func(c *container) { + c.borderColor = color + } +} + +func WithRoundedBorder() ContainerOption { + return WithBorderStyle(lipgloss.RoundedBorder()) +} + +func WithThickBorder() ContainerOption { + return WithBorderStyle(lipgloss.ThickBorder()) +} + +func WithDoubleBorder() ContainerOption { + return WithBorderStyle(lipgloss.DoubleBorder()) +} + +func WithBackgroundColor(color lipgloss.TerminalColor) ContainerOption { + return func(c *container) { + c.backgroundColor = color + } +} diff --git a/internal/tui/layout/single.go b/internal/tui/layout/single.go index e5c9a61c4..c77fa0d78 100644 --- a/internal/tui/layout/single.go +++ b/internal/tui/layout/single.go @@ -151,7 +151,7 @@ func NewSinglePane(content tea.Model, opts ...SinglePaneOption) SinglePaneLayout return layout } -func WithSignlePaneSize(width, height int) SinglePaneOption { +func WithSinglePaneSize(width, height int) SinglePaneOption { return func(opts *singlePaneLayout) { opts.width = width opts.height = height @@ -170,7 +170,7 @@ func WithSinglePaneBordered(bordered bool) SinglePaneOption { } } -func WithSignlePaneBorderText(borderText map[BorderPosition]string) SinglePaneOption { +func WithSinglePaneBorderText(borderText map[BorderPosition]string) SinglePaneOption { return func(opts *singlePaneLayout) { opts.borderText = borderText } diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go new file mode 100644 index 000000000..2a6822c7e --- /dev/null +++ b/internal/tui/layout/split.go @@ -0,0 +1,229 @@ +package layout + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/kujtimiihoxha/termai/internal/tui/styles" +) + +type SplitPaneLayout interface { + tea.Model + Sizeable +} + +type splitPaneLayout struct { + width int + height int + ratio float64 + verticalRatio float64 + + rightPanel Container + leftPanel Container + bottomPanel Container + + backgroundColor lipgloss.TerminalColor +} + +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: + s.SetSize(msg.Width, msg.Height) + return s, nil + } + + 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 s.backgroundColor != nil && finalView != "" { + style := lipgloss.NewStyle(). + Width(s.width). + Height(s.height). + Background(s.backgroundColor) + + return style.Render(finalView) + } + + return finalView +} + +func (s *splitPaneLayout) SetSize(width, height int) { + 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 + } + + if s.leftPanel != nil { + s.leftPanel.SetSize(leftWidth, topHeight) + } + + if s.rightPanel != nil { + s.rightPanel.SetSize(rightWidth, topHeight) + } + + if s.bottomPanel != nil { + s.bottomPanel.SetSize(width, bottomHeight) + } +} + +func (s *splitPaneLayout) GetSize() (int, int) { + return s.width, s.height +} + +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 80% for top section, 20% for bottom + backgroundColor: styles.Background, + } + 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 WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption { + return func(s *splitPaneLayout) { + s.backgroundColor = color + } +} + +func WithBottomPanel(panel Container) SplitPaneOption { + return func(s *splitPaneLayout) { + s.bottomPanel = panel + } +} + +func WithVerticalRatio(ratio float64) SplitPaneOption { + return func(s *splitPaneLayout) { + s.verticalRatio = ratio + } +} -- cgit v1.2.3