charmbracelet / lipgloss

Style definitions for nice terminal layouts πŸ‘„
MIT License
8.09k stars 229 forks source link

feat: flexbox #166

Open MichaelMure opened 1 year ago

MichaelMure commented 1 year ago

Expanding on https://github.com/charmbracelet/lipgloss/issues/162#issuecomment-1400256922, I thought that it would make sense as its own issue. Disclaimer: I'm a lipgloss noob.

Here is a proof of concept to illustrate what I'm talking about. Obviously, a lot could be changed or improved.

func TestExample(t *testing.T) {
    style1 := NewStyle().
        FlexGrow(2).
        Border(blockBorder, true).
        SetString("content 1")

    style2 := NewStyle().
        FlexGrow(1).
        Border(doubleBorder, true).
        SetString("content 2")

    fmt.Println(Flexbox(80, 40, style1, style2))
}

Botched implementation:


// Flexbox requires to have used Style.SetString beforehand.
func Flexbox(width, height int, styles ...Style) string {
    if len(styles) == 0 {
        return ""
    }
    if len(styles) == 1 {
        return styles[0].String()
    }

    // crazy flexbox voodoo, botched example with flex-grow:
    var totalGrow int
    for _, style := range styles {
        totalGrow += style.GetFlexGrow()
    }
    for _, style := range styles {
        style.Width(width * style.GetFlexGrow() / totalGrow)
    }

    var rendered []string
    for _, style := range styles {
        rendered = append(rendered, style.String())
    }

    // we would not actually use that:
    return JoinHorizontal(Bottom, rendered...)
}

const (
    flexGrowKey = iota + strikethroughSpacesKey + 1
)

func (s Style) FlexGrow(i int) Style {
    if i < 0 {
        panic("negative flex grow")
    }
    s.set(flexGrowKey, i)
    return s
}

func (s Style) GetFlexGrow() int {
    return s.getAsInt(flexGrowKey)
}

Rendered as:

β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•—
β–ˆcontent 1                                            β–ˆβ•‘content 2                 β•‘
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
MichaelMure commented 1 year ago

Flexbox algorithm is described in section 9: https://drafts.csswg.org/css-flexbox/#layout-algorithm

meowgorithm commented 1 year ago

Hi! Before we get too deep into this have you looked at stickers at all? It was built for the Bubble Tea ecosystem and contains both a generic flexbox implementation and a flexbox table.

MichaelMure commented 1 year ago

I've looked at it, but:

MichaelMure commented 1 year ago

FYI, I'm making some progress on this (see https://github.com/charmbracelet/lipgloss/compare/master...MichaelMure:lipgloss:flexbox for curiosity and advice), but the CSSWG spec is hardly digestible.

MichaelMure commented 1 year ago

FYI, I still have some issue with the box model (I ignored the border making the actual size of the box grow for now), but it's close to work!

container := NewStyle().
    Border(normalBorder).
    FlexWrap(FlexWrapWrap).
    FlexJustifyContent(FlexJustifyContentSpaceAround).
    Width(100)

style1 := NewStyle().
    Border(blockBorder, true).
    SetString("content 1\n\nfooo")

style2 := NewStyle().
    Border(doubleBorder, true).
    Padding(2).
    SetString("content 2")

style3 := NewStyle().
    Width(40).
    Border(doubleBorder, true).
    SetString("content 3\n\nfoobar\nbarfoo")

style4 := NewStyle().
    Width(60).
    Border(doubleBorder, true).
    SetString("content 4\n\nfoobar\nbarfoo")

fmt.Println(Flexbox(container, style1, style2, style3, style4))

image

MichaelMure commented 1 year ago

image