mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-06 00:28:23 +02:00
108 lines
3.4 KiB
Go
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("│") + " "
|
|
}
|
|
}
|