From eadf1038557dc0998cec422f2779bb94b0743229 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 14 Apr 2022 16:51:28 -0400 Subject: [PATCH] wip: add readme --- ui/common/common.go | 2 +- ui/common/component.go | 2 + ui/common/error.go | 2 + ui/components/code/code.go | 30 +++-- ui/components/footer/footer.go | 6 + ui/components/header/header.go | 6 + ui/components/selector/selector.go | 29 ++++- ui/components/viewport/viewport_patch.go | 5 + ui/keymap/keymap.go | 12 ++ .../selector => pages/selection}/item.go | 39 ++++--- ui/pages/selection/selection.go | 105 +++++++++++++++--- ui/styles/styles.go | 32 ++++-- ui/ui.go | 8 ++ 13 files changed, 224 insertions(+), 54 deletions(-) rename ui/{components/selector => pages/selection}/item.go (74%) diff --git a/ui/common/common.go b/ui/common/common.go index 90379ef82e605b28a51f9ba5d74a39c66f5df10e..fbfdf345b5de4b44755c4a0efef744f0979b5209 100644 --- a/ui/common/common.go +++ b/ui/common/common.go @@ -12,7 +12,7 @@ type Common struct { Height int } -func (c Common) SetSize(width, height int) { +func (c *Common) SetSize(width, height int) { c.Width = width c.Height = height } diff --git a/ui/common/component.go b/ui/common/component.go index ee6b6874160389c99ab242309451bf134a7c07c1..2880f30e41717ba661e5f09d6230e29843e6e661 100644 --- a/ui/common/component.go +++ b/ui/common/component.go @@ -5,11 +5,13 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +// Component represents a Bubble Tea model that implements a SetSize function. type Component interface { tea.Model SetSize(width, height int) } +// Page represents a component that implements help.KeyMap. type Page interface { Component help.KeyMap diff --git a/ui/common/error.go b/ui/common/error.go index bbf5bbe5f47e808acceb22c8a340bb2a67a8756e..fe972980562270ccfb0c8f854e3a3360b4389bc7 100644 --- a/ui/common/error.go +++ b/ui/common/error.go @@ -2,8 +2,10 @@ package common import tea "github.com/charmbracelet/bubbletea" +// ErrorMsg is a Bubble Tea message that represents an error. type ErrorMsg error +// ErrorCmd returns an ErrorMsg from error. func ErrorCmd(err error) tea.Cmd { return func() tea.Msg { return ErrorMsg(err) diff --git a/ui/components/code/code.go b/ui/components/code/code.go index 2ee4aec1b0f902ccf50bd91095f22ada64f03e3c..f0a800a60cc9481d5cd5acf0751474e95c8c295f 100644 --- a/ui/components/code/code.go +++ b/ui/components/code/code.go @@ -8,19 +8,23 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" gansi "github.com/charmbracelet/glamour/ansi" + "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/soft-serve/ui/common" vp "github.com/charmbracelet/soft-serve/ui/components/viewport" "github.com/muesli/reflow/wrap" "github.com/muesli/termenv" ) +// Code is a code snippet. type Code struct { - common common.Common - content string - extension string - viewport *vp.ViewportBubble + common common.Common + content string + extension string + viewport *vp.ViewportBubble + NoContentStyle lipgloss.Style } +// New returns a new Code. func New(c common.Common, content, extension string) *Code { r := &Code{ common: c, @@ -31,28 +35,36 @@ func New(c common.Common, content, extension string) *Code { MouseWheelEnabled: true, }, }, + NoContentStyle: c.Styles.CodeNoContent.Copy(), } + r.SetSize(c.Width, c.Height) return r } +// SetSize implements common.Component. func (r *Code) SetSize(width, height int) { - r.common.Width = width - r.common.Height = height + r.common.SetSize(width, height) r.viewport.SetSize(width, height) } +// SetContent sets the content of the Code. func (r *Code) SetContent(c, ext string) tea.Cmd { r.content = c r.extension = ext return r.Init() } +// GotoTop reset the viewport to the top. +func (r *Code) GotoTop() { + r.viewport.Viewport.GotoTop() +} + +// Init implements tea.Model. func (r *Code) Init() tea.Cmd { w := r.common.Width - s := r.common.Styles c := r.content if c == "" { - c = s.AboutNoReadme.Render("File is empty.") + c = r.NoContentStyle.String() } f, err := renderFile(r.extension, c, w) if err != nil { @@ -63,12 +75,14 @@ func (r *Code) Init() tea.Cmd { return nil } +// Update implements tea.Model. func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) { v, cmd := r.viewport.Update(msg) r.viewport = v.(*vp.ViewportBubble) return r, cmd } +// View implements tea.View. func (r *Code) View() string { return r.viewport.View() } diff --git a/ui/components/footer/footer.go b/ui/components/footer/footer.go index 4a43be97d6d4a105bb3a108fec257b46f8add174..70e87defa5d29f44b22fde452d1ccf0036ef7ae9 100644 --- a/ui/components/footer/footer.go +++ b/ui/components/footer/footer.go @@ -6,12 +6,14 @@ import ( "github.com/charmbracelet/soft-serve/ui/common" ) +// Footer is a Bubble Tea model that displays help and other info. type Footer struct { common common.Common help help.Model keymap help.KeyMap } +// New creates a new Footer. func New(c common.Common, keymap help.KeyMap) *Footer { h := help.New() h.Styles.ShortKey = c.Styles.HelpKey @@ -24,19 +26,23 @@ func New(c common.Common, keymap help.KeyMap) *Footer { return f } +// SetSize implements common.Component. func (f *Footer) SetSize(width, height int) { f.common.Width = width f.common.Height = height } +// Init implements tea.Model. func (f *Footer) Init() tea.Cmd { return nil } +// Update implements tea.Model. func (f *Footer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return f, nil } +// View implements tea.Model. func (f *Footer) View() string { if f.keymap == nil { return "" diff --git a/ui/components/header/header.go b/ui/components/header/header.go index 50fb715176ca945c59ff561def49b3c4b22c7653..075cf64ac3a46abe8f375c0c9d51103302e20d31 100644 --- a/ui/components/header/header.go +++ b/ui/components/header/header.go @@ -7,11 +7,13 @@ import ( "github.com/charmbracelet/soft-serve/ui/common" ) +// Header represents a header component. type Header struct { common common.Common text string } +// New creates a new header component. func New(c common.Common, text string) *Header { h := &Header{ common: c, @@ -20,19 +22,23 @@ func New(c common.Common, text string) *Header { return h } +// SetSize implements common.Component. func (h *Header) SetSize(width, height int) { h.common.Width = width h.common.Height = height } +// Init implements tea.Model. func (h *Header) Init() tea.Cmd { return nil } +// Update implements tea.Model. func (h *Header) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return h, nil } +// View implements tea.Model. func (h *Header) View() string { s := h.common.Styles.Header.Copy().Width(h.common.Width) return s.Render(strings.TrimSpace(h.text)) diff --git a/ui/components/selector/selector.go b/ui/components/selector/selector.go index d4fc300aead274491a7397a75d658223f92cc3da..9274f58acb225a9f407cf947a94e1cec14f196a8 100644 --- a/ui/components/selector/selector.go +++ b/ui/components/selector/selector.go @@ -7,18 +7,28 @@ import ( "github.com/charmbracelet/soft-serve/ui/common" ) +// Selector is a list of items that can be selected. type Selector struct { list list.Model common common.Common active int } +// IdentifiableItem is an item that can be identified by a string and extends list.Item. +type IdentifiableItem interface { + list.Item + ID() string +} + +// SelectMsg is a message that is sent when an item is selected. type SelectMsg string +// ActiveMsg is a message that is sent when an item is active but not selected. type ActiveMsg string -func New(common common.Common, items []list.Item) *Selector { - l := list.New(items, ItemDelegate{common.Styles}, common.Width, common.Height) +// New creates a new selector. +func New(common common.Common, items []list.Item, delegate list.ItemDelegate) *Selector { + l := list.New(items, delegate, common.Width, common.Height) l.SetShowTitle(false) l.SetShowHelp(false) l.SetShowStatusBar(false) @@ -31,27 +41,33 @@ func New(common common.Common, items []list.Item) *Selector { return s } +// KeyMap returns the underlying list's keymap. func (s *Selector) KeyMap() list.KeyMap { return s.list.KeyMap } +// SetSize implements common.Component. func (s *Selector) SetSize(width, height int) { s.common.SetSize(width, height) s.list.SetSize(width, height) } +// SetItems sets the items in the selector. func (s *Selector) SetItems(items []list.Item) tea.Cmd { return s.list.SetItems(items) } +// Index returns the index of the selected item. func (s *Selector) Index() int { return s.list.Index() } +// Init implements tea.Model. func (s *Selector) Init() tea.Cmd { return s.activeCmd } +// Update implements tea.Model. func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { @@ -74,18 +90,19 @@ func (s *Selector) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return s, tea.Batch(cmds...) } +// View implements tea.Model. func (s *Selector) View() string { return s.list.View() } func (s *Selector) selectCmd() tea.Msg { item := s.list.SelectedItem() - i := item.(Item) - return SelectMsg(i.Name) + i := item.(IdentifiableItem) + return SelectMsg(i.ID()) } func (s *Selector) activeCmd() tea.Msg { item := s.list.SelectedItem() - i := item.(Item) - return ActiveMsg(i.Name) + i := item.(IdentifiableItem) + return ActiveMsg(i.ID()) } diff --git a/ui/components/viewport/viewport_patch.go b/ui/components/viewport/viewport_patch.go index 16427a7d2fe47df09b05a15688b031f3c042e9f1..c60fec050174887fb4a276a3444b01b06df805df 100644 --- a/ui/components/viewport/viewport_patch.go +++ b/ui/components/viewport/viewport_patch.go @@ -5,25 +5,30 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +// ViewportBubble represents a viewport component. type ViewportBubble struct { Viewport *viewport.Model } +// SetSize implements common.Component. func (v *ViewportBubble) SetSize(width, height int) { v.Viewport.Width = width v.Viewport.Height = height } +// Init implements tea.Model. func (v *ViewportBubble) Init() tea.Cmd { return nil } +// Update implements tea.Model. func (v *ViewportBubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { vp, cmd := v.Viewport.Update(msg) v.Viewport = &vp return v, cmd } +// View implements tea.Model. func (v *ViewportBubble) View() string { return v.Viewport.View() } diff --git a/ui/keymap/keymap.go b/ui/keymap/keymap.go index 8b879a82c8f0ef2daa3a353083c0b1419c1fed85..e347a8bd5456d4927485c82bfa9afe724f4c14b6 100644 --- a/ui/keymap/keymap.go +++ b/ui/keymap/keymap.go @@ -11,6 +11,7 @@ type KeyMap struct { LeftRight key.Binding Arrows key.Binding Select key.Binding + Section key.Binding } // DefaultKeyMap returns the default key map. @@ -103,5 +104,16 @@ func DefaultKeyMap() *KeyMap { ), ) + km.Section = key.NewBinding( + key.WithKeys( + "tab", + "shift+tab", + ), + key.WithHelp( + "tab", + "section", + ), + ) + return km } diff --git a/ui/components/selector/item.go b/ui/pages/selection/item.go similarity index 74% rename from ui/components/selector/item.go rename to ui/pages/selection/item.go index 16a66458414ce9cd60386691e9e3f726fedc4b7a..6bf66247394437199542d1c0d1f5cab850316076 100644 --- a/ui/components/selector/item.go +++ b/ui/pages/selection/item.go @@ -1,4 +1,4 @@ -package selector +package selection import ( "fmt" @@ -14,6 +14,7 @@ import ( "github.com/dustin/go-humanize" ) +// Item represents a single item in the selector. type Item struct { Title string Name string @@ -22,35 +23,41 @@ type Item struct { URL *yankable.Yankable } -func (i *Item) Init() tea.Cmd { - return nil -} - -func (i *Item) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - return i, nil -} - -func (i *Item) View() string { - return "" +// ID implements selector.IdentifiableItem. +func (i Item) ID() string { + return i.Name } +// FilterValue implements list.Item. func (i Item) FilterValue() string { return i.Title } +// ItemDelegate is the delegate for the item. type ItemDelegate struct { - styles *styles.Styles + styles *styles.Styles + activeBox *box } +// Width returns the item width. func (d ItemDelegate) Width() int { width := d.styles.MenuItem.GetHorizontalFrameSize() + d.styles.MenuItem.GetWidth() return width } + +// Height returns the item height. Implements list.ItemDelegate. func (d ItemDelegate) Height() int { height := d.styles.MenuItem.GetVerticalFrameSize() + d.styles.MenuItem.GetHeight() return height } + +// Spacing returns the spacing between items. Implements list.ItemDelegate. func (d ItemDelegate) Spacing() int { return 1 } + +// Update implements list.ItemDelegate. func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { cmds := make([]tea.Cmd, 0) + if d.activeBox == nil || *d.activeBox != selectorBox { + return nil + } for i, item := range m.VisibleItems() { itm, ok := item.(Item) if !ok { @@ -78,12 +85,18 @@ func (d ItemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { } return tea.Batch(cmds...) } + +// Render implements list.ItemDelegate. func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { i := listItem.(Item) s := strings.Builder{} style := d.styles.MenuItem.Copy() if index == m.Index() { - style = d.styles.SelectedMenuItem.Copy() + style = style.BorderForeground(d.styles.ActiveBorderColor) + if d.activeBox != nil && *d.activeBox == readmeBox { + // TODO make this into its own color + style = style.BorderForeground(lipgloss.Color("15")) + } } titleStr := i.Title updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.LastUpdate)) diff --git a/ui/pages/selection/selection.go b/ui/pages/selection/selection.go index 349e46960f0caf2604fe5ab8f602047d017791d4..c1ce0195bc83fa7445c3b327e9a539c0429ceb93 100644 --- a/ui/pages/selection/selection.go +++ b/ui/pages/selection/selection.go @@ -16,39 +16,67 @@ import ( "github.com/charmbracelet/soft-serve/ui/session" ) +type box int + +const ( + readmeBox box = iota + selectorBox +) + +// Selection is the model for the selection screen/page. type Selection struct { - s session.Session - common common.Common - readme *code.Code - selector *selector.Selector + s session.Session + common common.Common + readme *code.Code + selector *selector.Selector + activeBox box } +// New creates a new selection model. func New(s session.Session, common common.Common) *Selection { sel := &Selection{ - s: s, - common: common, - readme: code.New(common, "", ""), - selector: selector.New(common, []list.Item{}), + s: s, + common: common, + activeBox: 1, } + readme := code.New(common, "", "") + readme.NoContentStyle = readme.NoContentStyle.SetString("No readme found.") + sel.readme = readme + sel.selector = selector.New(common, []list.Item{}, ItemDelegate{common.Styles, &sel.activeBox}) return sel } +// SetSize implements common.Component. func (s *Selection) SetSize(width, height int) { s.common.SetSize(width, height) - s.readme.SetSize(width, height) - s.selector.SetSize(width, height) + sw := s.common.Styles.SelectorBox.GetWidth() + wm := sw + + s.common.Styles.SelectorBox.GetHorizontalFrameSize() + + s.common.Styles.ReadmeBox.GetHorizontalFrameSize() + hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize() + s.readme.SetSize(width-wm, height-hm) + s.selector.SetSize(sw, height) } +// ShortHelp implements help.KeyMap. func (s *Selection) ShortHelp() []key.Binding { k := s.selector.KeyMap() - return []key.Binding{ + kb := make([]key.Binding, 0) + kb = append(kb, s.common.Keymap.UpDown, s.common.Keymap.Select, - k.Filter, - k.ClearFilter, + ) + if s.activeBox == selectorBox { + kb = append(kb, + k.Filter, + k.ClearFilter, + ) } + return kb } +// FullHelp implements help.KeyMap. +// TODO implement full help on ? func (s *Selection) FullHelp() [][]key.Binding { k := s.selector.KeyMap() return [][]key.Binding{ @@ -74,9 +102,11 @@ func (s *Selection) FullHelp() [][]key.Binding { } } +// Init implements tea.Model. func (s *Selection) Init() tea.Cmd { items := make([]list.Item, 0) cfg := s.s.Config() + // TODO fix yankable component yank := func(text string) *yankable.Yankable { return yankable.New( lipgloss.NewStyle().Foreground(lipgloss.Color("168")), @@ -86,7 +116,7 @@ func (s *Selection) Init() tea.Cmd { } // Put configured repos first for _, r := range cfg.Repos { - items = append(items, selector.Item{ + items = append(items, Item{ Title: r.Name, Name: r.Repo, Description: r.Note, @@ -97,14 +127,14 @@ func (s *Selection) Init() tea.Cmd { for _, r := range cfg.Source.AllRepos() { exists := false for _, item := range items { - item := item.(selector.Item) + item := item.(Item) if item.Name == r.Name() { exists = true break } } if !exists { - items = append(items, selector.Item{ + items = append(items, Item{ Title: r.Name(), Name: r.Name(), Description: "", @@ -119,12 +149,39 @@ func (s *Selection) Init() tea.Cmd { ) } +// Update implements tea.Model. func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg := msg.(type) { + case tea.WindowSizeMsg: + r, cmd := s.readme.Update(msg) + s.readme = r.(*code.Code) + if cmd != nil { + cmds = append(cmds, cmd) + } + m, cmd := s.selector.Update(msg) + s.selector = m.(*selector.Selector) + if cmd != nil { + cmds = append(cmds, cmd) + } case selector.ActiveMsg: cmds = append(cmds, s.changeActive(msg)) - default: + // reset readme position + s.readme.GotoTop() + case tea.KeyMsg: + switch { + case key.Matches(msg, s.common.Keymap.Section): + s.activeBox = (s.activeBox + 1) % 2 + } + } + switch s.activeBox { + case readmeBox: + r, cmd := s.readme.Update(msg) + s.readme = r.(*code.Code) + if cmd != nil { + cmds = append(cmds, cmd) + } + case selectorBox: m, cmd := s.selector.Update(msg) s.selector = m.(*selector.Selector) if cmd != nil { @@ -134,10 +191,22 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return s, tea.Batch(cmds...) } +// View implements tea.Model. func (s *Selection) View() string { + wm := s.common.Styles.SelectorBox.GetWidth() + + s.common.Styles.SelectorBox.GetHorizontalFrameSize() + + s.common.Styles.ReadmeBox.GetHorizontalFrameSize() + hm := s.common.Styles.ReadmeBox.GetVerticalFrameSize() + rs := s.common.Styles.ReadmeBox.Copy(). + Width(s.common.Width - wm). + Height(s.common.Height - hm) + if s.activeBox == readmeBox { + rs.BorderForeground(s.common.Styles.ActiveBorderColor) + } + readme := rs.Render(s.readme.View()) return lipgloss.JoinHorizontal( lipgloss.Top, - s.readme.View(), + readme, s.selector.View(), ) } diff --git a/ui/styles/styles.go b/ui/styles/styles.go index 58837ad807f225541acc2e4c5d16132ce980062f..89fed7d14ec22157e7a973691cb67a8823d12d43 100644 --- a/ui/styles/styles.go +++ b/ui/styles/styles.go @@ -15,11 +15,14 @@ type Styles struct { App lipgloss.Style Header lipgloss.Style - Menu lipgloss.Style - MenuCursor lipgloss.Style - MenuItem lipgloss.Style - MenuLastUpdate lipgloss.Style - SelectedMenuItem lipgloss.Style + Menu lipgloss.Style + MenuCursor lipgloss.Style + MenuItem lipgloss.Style + MenuLastUpdate lipgloss.Style + + // Selection page styles + SelectorBox lipgloss.Style + ReadmeBox lipgloss.Style RepoTitleBorder lipgloss.Border RepoNoteBorder lipgloss.Border @@ -74,6 +77,8 @@ type Styles struct { TreeNoItems lipgloss.Style Spinner lipgloss.Style + + CodeNoContent lipgloss.Style } // DefaultStyles returns default styles for the UI. @@ -81,7 +86,7 @@ func DefaultStyles() *Styles { s := new(Styles) s.ActiveBorderColor = lipgloss.Color("62") - s.InactiveBorderColor = lipgloss.Color("236") + s.InactiveBorderColor = lipgloss.Color("241") s.App = lipgloss.NewStyle(). Margin(1, 2) @@ -113,8 +118,13 @@ func DefaultStyles() *Styles { Foreground(lipgloss.Color("241")). Align(lipgloss.Right) - s.SelectedMenuItem = s.MenuItem.Copy(). - BorderForeground(s.ActiveBorderColor) + s.SelectorBox = lipgloss.NewStyle(). + Width(64) + + s.ReadmeBox = lipgloss.NewStyle(). + BorderForeground(s.InactiveBorderColor). + Padding(1). + MarginRight(1) s.RepoTitleBorder = lipgloss.Border{ Top: "─", @@ -288,5 +298,11 @@ func DefaultStyles() *Styles { MarginLeft(2). Foreground(lipgloss.Color("205")) + s.CodeNoContent = lipgloss.NewStyle(). + SetString("No Content."). + MarginTop(1). + MarginLeft(2). + Foreground(lipgloss.Color("#626262")) + return s } diff --git a/ui/ui.go b/ui/ui.go index 4a3cf1f09edec657b94f539b764eb92ca24408a9..21c4ab3e00bd1b53e5836fbc8e6d506055c7d7dd 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -21,6 +21,7 @@ const ( loadedState ) +// UI is the main UI model. type UI struct { s session.Session common common.Common @@ -32,6 +33,7 @@ type UI struct { error error } +// New returns a new UI model. func New(s session.Session, c common.Common, initialRepo string) *UI { h := header.New(c, s.Config().Name) ui := &UI{ @@ -55,6 +57,7 @@ func (ui *UI) getMargins() (wm, hm int) { return } +// ShortHelp implements help.KeyMap. func (ui *UI) ShortHelp() []key.Binding { b := make([]key.Binding, 0) b = append(b, ui.pages[ui.activePage].ShortHelp()...) @@ -62,6 +65,7 @@ func (ui *UI) ShortHelp() []key.Binding { return b } +// FullHelp implements help.KeyMap. func (ui *UI) FullHelp() [][]key.Binding { b := make([][]key.Binding, 0) b = append(b, ui.pages[ui.activePage].FullHelp()...) @@ -69,6 +73,7 @@ func (ui *UI) FullHelp() [][]key.Binding { return b } +// SetSize implements common.Component. func (ui *UI) SetSize(width, height int) { ui.common.SetSize(width, height) wm, hm := ui.getMargins() @@ -81,6 +86,7 @@ func (ui *UI) SetSize(width, height int) { } } +// Init implements tea.Model. func (ui *UI) Init() tea.Cmd { ui.pages[0] = selection.New(ui.s, ui.common) ui.pages[1] = selection.New(ui.s, ui.common) @@ -89,6 +95,7 @@ func (ui *UI) Init() tea.Cmd { return ui.pages[ui.activePage].Init() } +// Update implements tea.Model. // TODO update help when page change. func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) @@ -130,6 +137,7 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return ui, tea.Batch(cmds...) } +// View implements tea.Model. func (ui *UI) View() string { s := strings.Builder{} switch ui.state {