charmbracelet / bubbles

TUI components for Bubble Tea 🫧
MIT License
5.49k stars 264 forks source link

Bubble Model Interface for Layout Management #197

Open maaslalani opened 2 years ago

maaslalani commented 2 years ago

Bubbles' (visual) components should likely adhere/implement a bubble.Model interface so that we can help with layout management i.e. automatic resizing and layout management which is getting slightly repetitive in most projects with any sort of complicated layout.

The interface will likely have to allow setting widths and heights so that we can resize the visual component and possibly have blur and focus states so the component can act accordingly if we shift focus away from it (i.e. lazy rendering, stop blinking cursor, etc...).

package bubbles

interface Visual {
   SetHeight func(int)
   SetWidth  func(int)
   Height    func() int
   Width     func() int
}

We can then allow for relative / flex-box style layouts by allowing people to have a layout manager which can resize components automatically based on certain attributes when we received a WindowResizeMsg. For example, I could place two containers and give them a relative width of 0.5 each and the layout manager could SetWidth to half of the screen automatically so that layouts are responsive.

It's worth thinking about what logic should go in lipgloss vs bubbles itself.

craiggwilson commented 2 years ago

I have been building a couple of layout components and found I had to resort to reflect to accomplish this type of thing. I didn't need the getters, but the setters were important. The problem was that something implementing tea.Model with non-pointer receivers makes it impossible to also implement Visual, as the setters need pointer receivers. Hence,

type Model struct { }

func (m Model) Init() tea.Cmd { return nil }
func (m Model) Update(tea.Msg) (tea.Model, tea.Cmd) { return m, nil }
func (m Model) View() string { return "" }

func (m *Model) SetHeight(v int) { }
func (m *Model) SetWidth(v int) { }
var m tea.Model = Model{}
if v, ok := m.(Visual); ok {
  fmt.Println("we'll never get here")
}

As such, resorting to reflection to take the pointer to the thing that implements tea.Model works, but well, it's reflection.

I hope I'm wrong and just missing something obvious...

You can see my attempt here. This is far from finished or polished at all. Only been working on it for a couple of nights after work.