diff --git a/examples/layout/columns/main.go b/examples/layout/columns/main.go new file mode 100644 index 0000000..62217fa --- /dev/null +++ b/examples/layout/columns/main.go @@ -0,0 +1,25 @@ +package main + +import "github.com/charmbracelet/huh" + +func main() { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("First"), + huh.NewInput().Title("Second"), + huh.NewInput().Title("Third"), + ), + huh.NewGroup( + huh.NewInput().Title("Fourth"), + huh.NewInput().Title("Fifth"), + huh.NewInput().Title("Sixth"), + ), + huh.NewGroup( + huh.NewInput().Title("Seventh"), + huh.NewInput().Title("Eigth"), + huh.NewInput().Title("Nineth"), + huh.NewInput().Title("Tenth"), + ), + ).WithLayout(huh.LayoutColumns(2)) + form.Run() +} diff --git a/examples/layout/default/main.go b/examples/layout/default/main.go new file mode 100644 index 0000000..0ea2ba6 --- /dev/null +++ b/examples/layout/default/main.go @@ -0,0 +1,25 @@ +package main + +import "github.com/charmbracelet/huh" + +func main() { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("First"), + huh.NewInput().Title("Second"), + huh.NewInput().Title("Third"), + ), + huh.NewGroup( + huh.NewInput().Title("Fourth"), + huh.NewInput().Title("Fifth"), + huh.NewInput().Title("Sixth"), + ), + huh.NewGroup( + huh.NewInput().Title("Seventh"), + huh.NewInput().Title("Eigth"), + huh.NewInput().Title("Nineth"), + huh.NewInput().Title("Tenth"), + ), + ) + form.Run() +} diff --git a/examples/layout/grid/main.go b/examples/layout/grid/main.go new file mode 100644 index 0000000..55c5283 --- /dev/null +++ b/examples/layout/grid/main.go @@ -0,0 +1,30 @@ +package main + +import "github.com/charmbracelet/huh" + +func main() { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("First"), + huh.NewInput().Title("Second"), + huh.NewInput().Title("Third"), + ), + huh.NewGroup( + huh.NewInput().Title("Fourth"), + huh.NewInput().Title("Fifth"), + huh.NewInput().Title("Sixth"), + ), + huh.NewGroup( + huh.NewInput().Title("Seventh"), + huh.NewInput().Title("Eigth"), + huh.NewInput().Title("Nineth"), + huh.NewInput().Title("Tenth"), + ), + huh.NewGroup( + huh.NewInput().Title("Eleventh"), + huh.NewInput().Title("Twelveth"), + huh.NewInput().Title("Thirteenth"), + ), + ).WithLayout(huh.LayoutGrid(2, 2)) + form.Run() +} diff --git a/examples/layout/stack/main.go b/examples/layout/stack/main.go new file mode 100644 index 0000000..34b7a80 --- /dev/null +++ b/examples/layout/stack/main.go @@ -0,0 +1,25 @@ +package main + +import "github.com/charmbracelet/huh" + +func main() { + form := huh.NewForm( + huh.NewGroup( + huh.NewInput().Title("First"), + huh.NewInput().Title("Second"), + huh.NewInput().Title("Third"), + ), + huh.NewGroup( + huh.NewInput().Title("Fourth"), + huh.NewInput().Title("Fifth"), + huh.NewInput().Title("Sixth"), + ), + huh.NewGroup( + huh.NewInput().Title("Seventh"), + huh.NewInput().Title("Eigth"), + huh.NewInput().Title("Nineth"), + huh.NewInput().Title("Tenth"), + ), + ).WithLayout(huh.LayoutStack) + form.Run() +} diff --git a/form.go b/form.go index be74574..9f1aba6 100644 --- a/form.go +++ b/form.go @@ -62,6 +62,8 @@ type Form struct { height int keymap *KeyMap teaOptions []tea.ProgramOption + + layout Layout } // NewForm returns a form with the given groups and default themes and @@ -78,6 +80,7 @@ func NewForm(groups ...*Group) *Form { paginator: p, keymap: NewDefaultKeyMap(), results: make(map[string]any), + layout: LayoutDefault, teaOptions: []tea.ProgramOption{ tea.WithOutput(os.Stderr), }, @@ -266,6 +269,7 @@ func (f *Form) WithWidth(width int) *Form { } f.width = width for _, group := range f.groups { + width := f.layout.GroupWidth(f, group, width) group.WithWidth(width) } return f @@ -301,6 +305,14 @@ func (f *Form) WithProgramOptions(opts ...tea.ProgramOption) *Form { return f } +// WithLayout sets the layout on a form. +// +// This allows customization of the form group layout. +func (f *Form) WithLayout(layout Layout) *Form { + f.layout = layout + return f +} + // UpdateFieldPositions sets the position on all the fields. func (f *Form) UpdateFieldPositions() *Form { firstGroup := 0 @@ -431,6 +443,9 @@ func (f *Form) PrevField() tea.Cmd { func (f *Form) Init() tea.Cmd { cmds := make([]tea.Cmd, len(f.groups)) for i, group := range f.groups { + if i == 0 { + group.active = true + } cmds[i] = group.Init() } @@ -457,7 +472,8 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { break } for _, group := range f.groups { - group.WithWidth(msg.Width) + width := f.layout.GroupWidth(f, group, msg.Width) + group.WithWidth(width) } if f.height > 0 { break @@ -507,6 +523,7 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return submit() } } + f.groups[f.paginator.Page].active = true return f, f.groups[f.paginator.Page].Init() case prevGroupMsg: @@ -521,6 +538,7 @@ func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + f.groups[f.paginator.Page].active = true return f, f.groups[f.paginator.Page].Init() } @@ -551,7 +569,7 @@ func (f *Form) View() string { return "" } - return f.groups[f.paginator.Page].View() + return f.layout.View(f) } // Run runs the form. diff --git a/group.go b/group.go index ee97fa3..d9ef2c0 100644 --- a/group.go +++ b/group.go @@ -40,6 +40,7 @@ type Group struct { height int keymap *KeyMap hide func() bool + active bool } // NewGroup returns a new group with the given fields. @@ -53,6 +54,7 @@ func NewGroup(fields ...Field) *Group { help: help.New(), showHelp: true, showErrors: true, + active: false, } height := group.fullHeight() @@ -190,8 +192,10 @@ func (g *Group) Init() tea.Cmd { return tea.Batch(cmds...) } - cmd := g.fields[g.paginator.Page].Focus() - cmds = append(cmds, cmd) + if g.active { + cmd := g.fields[g.paginator.Page].Focus() + cmds = append(cmds, cmd) + } g.buildView() return tea.Batch(cmds...) } @@ -261,7 +265,7 @@ func (g *Group) fullHeight() int { return height } -func (g *Group) buildView() { +func (g *Group) getContent() (int, string) { var fields strings.Builder offset := 0 gap := "\n\n" @@ -282,7 +286,13 @@ func (g *Group) buildView() { } } - g.viewport.SetContent(fields.String() + "\n") + return offset, fields.String() + "\n" +} + +func (g *Group) buildView() { + offset, content := g.getContent() + + g.viewport.SetContent(content) g.viewport.SetYOffset(offset) } @@ -290,6 +300,19 @@ func (g *Group) buildView() { func (g *Group) View() string { var view strings.Builder view.WriteString(g.viewport.View()) + view.WriteString(g.Footer()) + return view.String() +} + +// Content renders the group's content only (no footer). +func (g *Group) Content() string { + _, content := g.getContent() + return content +} + +// Footer renders the group's footer only (no content). +func (g *Group) Footer() string { + var view strings.Builder view.WriteRune('\n') errors := g.Errors() if g.showHelp && len(errors) <= 0 { diff --git a/layout.go b/layout.go new file mode 100644 index 0000000..2a22f4f --- /dev/null +++ b/layout.go @@ -0,0 +1,149 @@ +package huh + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// A Layout is responsible for laying out groups in a form. +type Layout interface { + View(f *Form) string + GroupWidth(f *Form, g *Group, w int) int +} + +// Default layout shows a single group at a time. +var LayoutDefault Layout = &layoutDefault{} + +// Stack layout stacks all groups on top of each other. +var LayoutStack Layout = &layoutStack{} + +// Column layout distributes groups in even columns. +func LayoutColumns(columns int) Layout { + return &layoutColumns{columns: columns} +} + +// Grid layout distributes groups in a grid. +func LayoutGrid(rows int, columns int) Layout { + return &layoutGrid{rows: rows, columns: columns} +} + +type layoutDefault struct{} + +func (l *layoutDefault) View(f *Form) string { + return f.groups[f.paginator.Page].View() +} + +func (l *layoutDefault) GroupWidth(_ *Form, _ *Group, w int) int { + return w +} + +type layoutColumns struct { + columns int +} + +func (l *layoutColumns) visibleGroups(f *Form) []*Group { + segmentIndex := f.paginator.Page / l.columns + start := segmentIndex * l.columns + end := start + l.columns + + if end > len(f.groups) { + end = len(f.groups) + } + + return f.groups[start:end] +} + +func (l *layoutColumns) View(f *Form) string { + groups := l.visibleGroups(f) + if len(groups) == 0 { + return "" + } + + var columns []string + for _, group := range groups { + columns = append(columns, group.Content()) + } + footer := f.groups[f.paginator.Page].Footer() + + return lipgloss.JoinVertical(lipgloss.Left, + lipgloss.JoinHorizontal(lipgloss.Top, columns...), + footer, + ) +} + +func (l *layoutColumns) GroupWidth(_ *Form, _ *Group, w int) int { + return w / l.columns +} + +type layoutStack struct{} + +func (l *layoutStack) View(f *Form) string { + var columns []string + for _, group := range f.groups { + columns = append(columns, group.Content()) + } + footer := f.groups[f.paginator.Page].Footer() + + var view strings.Builder + view.WriteString(strings.Join(columns, "\n")) + view.WriteString(footer) + return view.String() +} + +func (l *layoutStack) GroupWidth(_ *Form, _ *Group, w int) int { + return w +} + +type layoutGrid struct { + rows, columns int +} + +func (l *layoutGrid) visibleGroups(f *Form) [][]*Group { + total := l.rows * l.columns + segmentIndex := f.paginator.Page / total + start := segmentIndex * total + end := start + total + + if end > len(f.groups) { + end = len(f.groups) + } + + visible := f.groups[start:end] + grid := make([][]*Group, l.rows) + for i := 0; i < l.rows; i++ { + startRow := i * l.columns + endRow := startRow + l.columns + if startRow >= len(visible) { + break + } + if endRow > len(visible) { + endRow = len(visible) + } + grid[i] = visible[startRow:endRow] + } + return grid +} + +func (l *layoutGrid) View(f *Form) string { + grid := l.visibleGroups(f) + if len(grid) == 0 { + return "" + } + + var rows []string + for _, row := range grid { + var columns []string + for _, group := range row { + columns = append(columns, group.Content()) + } + rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, columns...)) + } + footer := f.groups[f.paginator.Page].Footer() + + return lipgloss.JoinVertical(lipgloss.Left, strings.Join(rows, "\n"), footer) +} + +func (l *layoutGrid) GroupWidth(_ *Form, _ *Group, w int) int { + return w / l.columns +}