on main help sign in
feat: render scrollbar for model list (dialog and onboarding) (#2978)
ce39583Andrey Nering
internal/ui/common/scrollbar.go+1−1
@@ -23,7 +23,7 @@ func Scrollbar(s *styles.Styles, height, contentSize, viewportSize, offset int)
23 }
23 }
24
24
25 // Calculate where the thumb starts.
25 // Calculate where the thumb starts.
26− trackSpace := height - thumbSize
26+ trackSpace := height - thumbSize + 1
27 thumbPos := 0
27 thumbPos := 0
28 if trackSpace > 0 && maxOffset > 0 {
28 if trackSpace > 0 && maxOffset > 0 {
29 thumbPos = min(trackSpace, offset*trackSpace/maxOffset)
29 thumbPos = min(trackSpace, offset*trackSpace/maxOffset)
internal/ui/dialog/models.go+11−1
@@ -10,6 +10,7 @@ import (
10 "charm.land/bubbles/v2/textinput"
10 "charm.land/bubbles/v2/textinput"
11 tea "charm.land/bubbletea/v2"
11 tea "charm.land/bubbletea/v2"
12 "charm.land/catwalk/pkg/catwalk"
12 "charm.land/catwalk/pkg/catwalk"
13+ "charm.land/lipgloss/v2"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/ui/common"
15 "github.com/charmbracelet/crush/internal/ui/common"
15 "github.com/charmbracelet/crush/internal/ui/util"
16 "github.com/charmbracelet/crush/internal/ui/util"
@@ -265,9 +266,14 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
265 t.Dialog.View.GetVerticalFrameSize()
266 t.Dialog.View.GetVerticalFrameSize()
266
267
267 m.input.SetWidth(max(0, innerWidth-t.Dialog.InputPrompt.GetHorizontalFrameSize()-1)) // (1) cursor padding
268 m.input.SetWidth(max(0, innerWidth-t.Dialog.InputPrompt.GetHorizontalFrameSize()-1)) // (1) cursor padding
268− m.list.SetSize(innerWidth, height-heightOffset)
269 m.help.SetWidth(innerWidth)
269 m.help.SetWidth(innerWidth)
270
270
271+ listHeight := height - heightOffset
272+ m.list.SetSize(innerWidth, listHeight)
273+ listTotalHeight := m.list.TotalHeight()
274+ listWidth := max(0, innerWidth-3) // Reserve space for scrollbar.
275+ m.list.SetSize(listWidth, listHeight)
276+
271 rc := NewRenderContext(t, width)
277 rc := NewRenderContext(t, width)
272 rc.Title = "Switch Model"
278 rc.Title = "Switch Model"
273 rc.TitleInfo = m.modelTypeRadioView()
279 rc.TitleInfo = m.modelTypeRadioView()
@@ -281,6 +287,10 @@ func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
281 rc.AddPart(inputView)
287 rc.AddPart(inputView)
282
288
283 listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render())
289 listView := t.Dialog.List.Height(m.list.Height()).Render(m.list.Render())
290+ scrollbar := common.Scrollbar(t, listHeight, listTotalHeight, listHeight, m.list.Offset())
291+ if scrollbar != "" {
292+ listView = lipgloss.JoinHorizontal(lipgloss.Top, listView, scrollbar)
293+ }
284 rc.AddPart(listView)
294 rc.AddPart(listView)
285
295
286 rc.Help = m.help.View(m)
296 rc.Help = m.help.View(m)
internal/ui/list/list.go+27
@@ -157,6 +157,33 @@ func (l *List) Len() int {
157 return len(l.items)
157 return len(l.items)
158}
158}
159
159
160+// TotalHeight returns the total height of all items in the list.
161+func (l *List) TotalHeight() int {
162+ total := 0
163+ for idx := range l.items {
164+ item := l.getItem(idx)
165+ total += item.height
166+ if l.gap > 0 && idx < len(l.items)-1 {
167+ total += l.gap
168+ }
169+ }
170+ return total
171+}
172+
173+// Offset returns the current scroll offset in lines from the top.
174+func (l *List) Offset() int {
175+ offset := 0
176+ for idx := 0; idx < l.offsetIdx; idx++ {
177+ item := l.getItem(idx)
178+ offset += item.height
179+ if l.gap > 0 && idx < len(l.items)-1 {
180+ offset += l.gap
181+ }
182+ }
183+ offset += l.offsetLine
184+ return offset
185+}
186+
160// lastOffsetItem returns the index and line offsets of the last item that can
187// lastOffsetItem returns the index and line offsets of the last item that can
161// be partially visible in the viewport.
188// be partially visible in the viewport.
162func (l *List) lastOffsetItem() (int, int, int) {
189func (l *List) lastOffsetItem() (int, int, int) {