Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flexbox layout support #173

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

MichaelMure
Copy link

@MichaelMure MichaelMure commented Feb 26, 2023

fix #166

@meowgorithm @muesli This is not ready to be merged, but I'd like to have your opinion on it.

For some context, I'm the author of https://github.com/MichaelMure/git-bug. Also by necessity (as Charm didn't exist or barely at that time), https://github.com/MichaelMure/go-term-text (equivalent of muesli/reflow, muesli/ansi) and https://github.com/MichaelMure/go-term-markdown (equivalent of charmbracelet/glow). Nowadays I'd like to transition to or leverage Charm for git-bug.

I nerd sniped myself into implementing this for two reasons:

  • to get a better sense of the Charm ecosystem (and what best for that than the hard way)
  • because I thought it was cool

As such, well, job done. I scratched that itch, and I'd be fine if this is not merged if it doesn't fit this project goals.

Now, onto the actual meat of this:

I find the idea of mirroring CSS to be brilliant. It's one of those things that makes so much sense retrospectively. I sense however that lipgloss currently fall a bit short how its promise (or what I expected that promise to be): what makes the web model great for interfaces is CSS and HTML. Cascading style sheets and a hierarchy of content and containers.

What I mean by that is that lipgloss is great for styling and make a decent attempt at shaping content (width/height, alignment...), but fell quite short for laying out. JoinHorizontal and JoinVertical provides the beginning of that, but it's quite primitive: they lead to a world where we keep doing math with sizes, remaining space, and assembling fragment manually.

This is IMHO limiting, and why I toyed with this idea of flexbox. What if we had strong layout support, directly in the bedrock that lipgloss is? We could easily build interfaces where containers would grow and shrink nicely, or wrap and adapt if the available space is not enough. We'd get powerful capabilities for spacing containers, adapting their flow into columns, rows, changing their order ... and all that would be as easily tweak-able as a style.
image

I can see that happening in two ways:

  1. Consider lipgloss as CSS and HTML. Style would learn to have children styles (thus making a hierarchy of content). Style.SetString() would become a first-class way to set content. Style.Render() would learn to layout children (classic layout, flexbox, or even CSS grids).
  2. Keep a separation between styling and content. We'd have another base primitive to represent content and hierarchy. That would define containers and texts. Each of those would have a Style attached to set styling and layout properties. That new primitive would know how to leverage the layout functions like JoinHorizontal, flexbox ... to do the final render.

This hierarchy is a requirement, because a single layer of layout is still limiting. This page layout image I used higher is actually two flexbox. As the article container grow, it pushes the footer down. If the global container get squished, it might squish that article. A recursive layout is necessary to have those interactions both way.

I've included a toy program in test-flexbox to get a sense of the capabilities. You might still see some issues popping up. Also, not all flexbox is implemented.

@MichaelMure
Copy link
Author

The second idea could be something like that (largely pseudo code):

type Container struct {
	Style
	children []*Container
}

func (c *Container) PushFront(child *Container) {
	c.children = append([]*Container{child}, c.children...)
}

func (c *Container) PushBack(child *Container) {
	c.children = append(c.children, child)
}

func (c *Container) Render() string {
	// Step 1: cascade/inherit styles

	// TODO

	// Step 2: render
	switch c.GetDisplay() {
	default:
		fallthrough
	case DisplayBlock:
		return JoinVertical(Left, c.children)
	case DisplayFlex:
		return Flexbox(c.style, c.children)
	}
}

@muesli
Copy link
Member

muesli commented Mar 7, 2023

Nice work on the layout management! I guess I'll need a bit more time to digest all of this and play with it, before I can provide some proper feedback. Looks cool at first glance, tho! 😃

@mieubrisse
Copy link

FWIW I tried to build a decently complex Charm app and hit the same problem that @MichaelMure did, with styling+layout being the same thing in lipgloss. A core reason this is problematic is because only a child knows how to reflow its contents, but the parent will be the one determining the sizes. If all the parent gets from the child is a string, then it can't do things like preserve padding, border, etc. in an intelligent way.

In my case, I ended up going a bit deeper and built https://github.com/mieubrisse/box-layout-test , which implements a two-pass component/layout system (like how sizing works in the browser) on top of bubbletea. Basically, step one is to get all the items' desired sizes, and then step two is to actually fit them within the viewport. Definitely still in-progress, though I think I have the mechanics working well.

Regardless of what the implementation is though, definite +1 to Michael's point that having styling & layout be the same in bubbletea is a big hindrance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feat: flexbox
3 participants