| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- package layout
- import (
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/sst/opencode/internal/theme"
- )
- type FlexDirection int
- const (
- FlexDirectionHorizontal FlexDirection = iota
- FlexDirectionVertical
- )
- type FlexChildSize struct {
- Fixed bool
- Size int
- }
- var FlexChildSizeGrow = FlexChildSize{Fixed: false}
- func FlexChildSizeFixed(size int) FlexChildSize {
- return FlexChildSize{Fixed: true, Size: size}
- }
- type FlexLayout interface {
- tea.ViewModel
- Sizeable
- SetChildren(panes []tea.ViewModel) tea.Cmd
- SetSizes(sizes []FlexChildSize) tea.Cmd
- SetDirection(direction FlexDirection) tea.Cmd
- }
- type flexLayout struct {
- width int
- height int
- direction FlexDirection
- children []tea.ViewModel
- sizes []FlexChildSize
- }
- type FlexLayoutOption func(*flexLayout)
- func (f *flexLayout) View() string {
- if len(f.children) == 0 {
- return ""
- }
- t := theme.CurrentTheme()
- views := make([]string, 0, len(f.children))
- for i, child := range f.children {
- if child == nil {
- continue
- }
- alignment := lipgloss.Center
- if alignable, ok := child.(Alignable); ok {
- alignment = alignable.Alignment()
- }
- var childWidth, childHeight int
- if f.direction == FlexDirectionHorizontal {
- childWidth, childHeight = f.calculateChildSize(i)
- view := lipgloss.PlaceHorizontal(
- childWidth,
- alignment,
- child.View(),
- // TODO: make configurable WithBackgroundStyle
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
- )
- views = append(views, view)
- } else {
- childWidth, childHeight = f.calculateChildSize(i)
- view := lipgloss.Place(
- f.width,
- childHeight,
- lipgloss.Center,
- alignment,
- child.View(),
- // TODO: make configurable WithBackgroundStyle
- lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(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) calculateChildSize(index int) (width, height int) {
- if index >= len(f.children) {
- return 0, 0
- }
- totalFixed := 0
- flexCount := 0
- for i, child := range f.children {
- if child == 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
- currentX, currentY := 0, 0
- for i, child := range f.children {
- if child != nil {
- paneWidth, paneHeight := f.calculateChildSize(i)
- alignment := lipgloss.Center
- if alignable, ok := child.(Alignable); ok {
- alignment = alignable.Alignment()
- }
- // Calculate actual position based on alignment
- actualX, actualY := currentX, currentY
- if f.direction == FlexDirectionHorizontal {
- // In horizontal layout, vertical alignment affects Y position
- // (lipgloss.Center is used for vertical alignment in JoinHorizontal)
- actualY = (f.height - paneHeight) / 2
- } else {
- // In vertical layout, horizontal alignment affects X position
- contentWidth := paneWidth
- if alignable, ok := child.(Alignable); ok {
- if alignable.MaxWidth() > 0 && contentWidth > alignable.MaxWidth() {
- contentWidth = alignable.MaxWidth()
- }
- }
- switch alignment {
- case lipgloss.Center:
- actualX = (f.width - contentWidth) / 2
- case lipgloss.Right:
- actualX = f.width - contentWidth
- case lipgloss.Left:
- actualX = 0
- }
- }
- // Set position if the pane is Alignable
- if c, ok := child.(Alignable); ok {
- c.SetPosition(actualX, actualY)
- }
- if sizeable, ok := child.(Sizeable); ok {
- cmd := sizeable.SetSize(paneWidth, paneHeight)
- cmds = append(cmds, cmd)
- }
- // Update position for next pane
- if f.direction == FlexDirectionHorizontal {
- currentX += paneWidth
- } else {
- currentY += paneHeight
- }
- }
- }
- return tea.Batch(cmds...)
- }
- func (f *flexLayout) GetSize() (int, int) {
- return f.width, f.height
- }
- func (f *flexLayout) SetChildren(children []tea.ViewModel) tea.Cmd {
- f.children = children
- if f.width > 0 && f.height > 0 {
- return f.SetSize(f.width, f.height)
- }
- return nil
- }
- func (f *flexLayout) SetSizes(sizes []FlexChildSize) 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 NewFlexLayout(children []tea.ViewModel, options ...FlexLayoutOption) FlexLayout {
- layout := &flexLayout{
- children: children,
- direction: FlexDirectionHorizontal,
- sizes: []FlexChildSize{},
- }
- for _, option := range options {
- option(layout)
- }
- return layout
- }
- func WithDirection(direction FlexDirection) FlexLayoutOption {
- return func(f *flexLayout) {
- f.direction = direction
- }
- }
- func WithChildren(children ...tea.ViewModel) FlexLayoutOption {
- return func(f *flexLayout) {
- f.children = children
- }
- }
- func WithSizes(sizes ...FlexChildSize) FlexLayoutOption {
- return func(f *flexLayout) {
- f.sizes = sizes
- }
- }
|