Files
Hadi 59b31e5ca3 init
Signed-off-by: Hadi <[email protected]>
2026-08-13 00:58:37 +02:00

108 lines
3.4 KiB
Go

package util
import (
"strings"
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
ilovetui "github.com/anotherhadi/ilovetui"
)
// ScrollbarGutterWidth is the fixed width, in terminal columns, that
// RefreshScrollbar reserves for its gutter — whether or not a scrollbar is
// actually needed right now. Content that pre-wraps itself to a viewport's
// width (e.g. glamour markdown) must subtract this, otherwise it overflows
// the moment the scrollbar's thumb/track appear and the gutter eats into
// the width the content assumed it had.
const ScrollbarGutterWidth = 2
// SetYOffset sets vp's Y offset and refreshes its scrollbar gutter to match.
// Every scroll site should go through this (or [ScrollLines]/[ScrollViewport])
// instead of calling vp.SetYOffset directly, so the scrollbar never goes
// stale.
func SetYOffset(vp *viewport.Model, y int) {
vp.SetYOffset(y)
RefreshScrollbar(vp)
}
// ScrollLines scrolls vp by n lines (negative n scrolls up) and refreshes
// its scrollbar gutter.
func ScrollLines(vp *viewport.Model, n int) {
SetYOffset(vp, vp.YOffset()+n)
}
// ScrollViewport scrolls vp vertically by half its height, then refreshes
// its scrollbar gutter. delta should be -1 for up, +1 for down.
func ScrollViewport(vp *viewport.Model, delta int) {
step := vp.Height() / 2
if step < 1 {
step = 1
}
ScrollLines(vp, delta*step)
}
// HandleMouseWheel applies standard mouse wheel scrolling to vp, refreshing
// its scrollbar gutter on vertical scrolls.
// Vertical: one line at a time. Shift+vertical or horizontal: scroll 6 columns.
func HandleMouseWheel(msg tea.MouseWheelMsg, vp *viewport.Model) {
switch msg.Button {
case tea.MouseWheelUp:
if msg.Mod.Contains(tea.ModShift) {
vp.ScrollLeft(6)
} else {
ScrollLines(vp, -1)
}
case tea.MouseWheelDown:
if msg.Mod.Contains(tea.ModShift) {
vp.ScrollRight(6)
} else {
ScrollLines(vp, 1)
}
case tea.MouseWheelLeft:
vp.ScrollLeft(6)
case tea.MouseWheelRight:
vp.ScrollRight(6)
}
}
// RefreshScrollbar rebuilds vp's left-gutter scrollbar to reflect its
// current size, content and scroll position. Call it after anything that
// changes vp's width, height or content; YOffset changes are already
// covered if they go through [SetYOffset], [ScrollLines], [ScrollViewport]
// or [HandleMouseWheel].
//
// The gutter is always [ScrollbarGutterWidth] columns wide, even when
// there's nothing to scroll (it's just blank then). That keeps vp's usable
// content width constant regardless of scroll state, so content sized
// against vp.Width() doesn't need to know whether a scrollbar happens to be
// showing right now.
func RefreshScrollbar(vp *viewport.Model) {
height := vp.Height()
total := vp.TotalLineCount()
blank := strings.Repeat(" ", ScrollbarGutterWidth)
if height <= 0 || total <= height {
vp.LeftGutterFunc = func(viewport.GutterContext) string { return blank }
return
}
yOffset := vp.YOffset()
thumbSize := max(1, height*height/total)
thumbStart := 0
if maxOffset := total - height; maxOffset > 0 {
thumbStart = yOffset * (height - thumbSize) / maxOffset
}
trackStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle)
thumbStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
vp.LeftGutterFunc = func(ctx viewport.GutterContext) string {
pos := ctx.Index - yOffset
if pos >= thumbStart && pos < thumbStart+thumbSize {
return thumbStyle.Render("█") + " "
}
return trackStyle.Render("│") + " "
}
}