feat(tui): add scrollbar gutter to titledPanelH
Opt-in scrollbar track on the right border edge when totalItems > bodyH. Thin track (│) with muted thumb (┃) showing viewport position. Monitors panel passes len(sites) to enable it. Detail and fullscreen panels pass 0 to opt out. Any panel can opt in via the totalItems param.
This commit is contained in:
+52
-9
@@ -6,7 +6,13 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, focused bool) string {
|
type scrollbar struct {
|
||||||
|
pos int
|
||||||
|
total int
|
||||||
|
visible int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, sb scrollbar, focused bool) string {
|
||||||
if height <= 0 {
|
if height <= 0 {
|
||||||
return m.titledPanel(title, content, width, focused)
|
return m.titledPanel(title, content, width, focused)
|
||||||
}
|
}
|
||||||
@@ -64,21 +70,58 @@ func (m Model) titledPanelH(title, content, footer string, width, height, scroll
|
|||||||
}
|
}
|
||||||
visible := contentLines[scrollOffset:end]
|
visible := contentLines[scrollOffset:end]
|
||||||
|
|
||||||
borderLine := func(line string) string {
|
sbVisible := sb.visible
|
||||||
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + bc.Render("│")
|
if sbVisible <= 0 {
|
||||||
|
sbVisible = bodyH
|
||||||
|
}
|
||||||
|
showScrollbar := sb.total > 0 && sb.total > sbVisible
|
||||||
|
var thumbStart, thumbEnd int
|
||||||
|
if showScrollbar {
|
||||||
|
thumbSize := bodyH * sbVisible / sb.total
|
||||||
|
if thumbSize < 1 {
|
||||||
|
thumbSize = 1
|
||||||
|
}
|
||||||
|
scrollRange := sb.total - sbVisible
|
||||||
|
if scrollRange < 1 {
|
||||||
|
scrollRange = 1
|
||||||
|
}
|
||||||
|
trackSpace := bodyH - thumbSize
|
||||||
|
thumbStart = sb.pos * trackSpace / scrollRange
|
||||||
|
if thumbStart < 0 {
|
||||||
|
thumbStart = 0
|
||||||
|
}
|
||||||
|
thumbEnd = thumbStart + thumbSize
|
||||||
|
if thumbEnd > bodyH {
|
||||||
|
thumbEnd = bodyH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTrack := bc.Render("│")
|
||||||
|
scrollThumb := lipgloss.NewStyle().Foreground(m.theme.Muted).Render("┃")
|
||||||
|
|
||||||
|
borderLine := func(line string, idx int) string {
|
||||||
|
rightBorder := bc.Render("│")
|
||||||
|
if showScrollbar && idx >= thumbStart && idx < thumbEnd {
|
||||||
|
rightBorder = scrollThumb
|
||||||
|
} else if showScrollbar {
|
||||||
|
rightBorder = scrollTrack
|
||||||
|
}
|
||||||
|
return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + rightBorder
|
||||||
|
}
|
||||||
|
emptyLine := func(idx int) string {
|
||||||
|
return borderLine(strings.Repeat(" ", innerW), idx)
|
||||||
}
|
}
|
||||||
emptyLine := borderLine(strings.Repeat(" ", innerW))
|
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
lines = append(lines, top)
|
lines = append(lines, top)
|
||||||
for _, line := range visible {
|
for i, line := range visible {
|
||||||
lines = append(lines, borderLine(line))
|
lines = append(lines, borderLine(line, i))
|
||||||
}
|
}
|
||||||
for len(lines) < height-1-len(footerLines) {
|
for i := len(visible); len(lines) < height-1-len(footerLines); i++ {
|
||||||
lines = append(lines, emptyLine)
|
lines = append(lines, emptyLine(i))
|
||||||
}
|
}
|
||||||
for _, line := range footerLines {
|
for _, line := range footerLines {
|
||||||
lines = append(lines, borderLine(line))
|
lines = append(lines, borderLine(line, -1))
|
||||||
}
|
}
|
||||||
lines = append(lines, bottom)
|
lines = append(lines, bottom)
|
||||||
|
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
|
|
||||||
monTargetH := m.maxTableRows + 5
|
monTargetH := m.maxTableRows + 5
|
||||||
monitors := m.viewSitesTab()
|
monitors := m.viewSitesTab()
|
||||||
monPanel := m.zones.Mark("panel-monitors", m.titledPanelH("Monitors", monitors, "", monW, monTargetH, 0, m.focusedPanel == panelMonitors))
|
monPanel := m.zones.Mark("panel-monitors", m.titledPanelH("Monitors", monitors, "", monW, monTargetH, 0, scrollbar{pos: m.tableOffset, total: len(m.sites), visible: m.maxTableRows}, m.focusedPanel == panelMonitors))
|
||||||
|
|
||||||
var topParts []string
|
var topParts []string
|
||||||
topParts = append(topParts, monPanel)
|
topParts = append(topParts, monPanel)
|
||||||
@@ -181,7 +181,7 @@ func (m Model) viewMonitorsLayout() string {
|
|||||||
monHeight := lipgloss.Height(monPanel)
|
monHeight := lipgloss.Height(monPanel)
|
||||||
detail := m.viewDetailInline(detailW-2, monHeight)
|
detail := m.viewDetailInline(detailW-2, monHeight)
|
||||||
footer := m.detailFooter(detailW - 2)
|
footer := m.detailFooter(detailW - 2)
|
||||||
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail))
|
detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, scrollbar{}, m.focusedPanel == panelDetail))
|
||||||
topParts = append(topParts, detailPanel)
|
topParts = append(topParts, detailPanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (m Model) viewDetailFullscreen() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(
|
return lipgloss.NewStyle().Padding(1, 2).Render(
|
||||||
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, true))
|
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, scrollbar{}, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {
|
func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user