Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

151 lines
3.8 KiB
Go

package model
import (
"strings"
"charm.land/lipgloss/v2"
"github.com/bjarneo/cliamp/ui"
)
// clampScroll keeps cursor inside [0, count) and adjusts scroll so that
// the cursor sits within the visible window of `visible` rows.
func clampScroll(cursor, scroll *int, count, visible int) {
if visible <= 0 {
return
}
if *cursor < 0 {
*cursor = 0
}
if *cursor >= count && count > 0 {
*cursor = count - 1
}
if *cursor < *scroll {
*scroll = *cursor
} else if *cursor >= *scroll+visible {
*scroll = *cursor - visible + 1
}
if *scroll+visible > count && count > 0 {
*scroll = max(0, count-visible)
}
if *scroll < 0 {
*scroll = 0
}
}
// measurePlVisible calculates playlist lines available for a given upper limit.
func (m *Model) measurePlVisible(limit int) int {
saved := m.plVisible
m.plVisible = 3 // temporary minimal value for measurement
defer func() { m.plVisible = saved }()
// Use mainSections to get all fixed chrome plus any active transient messages.
probe := strings.Join(m.mainSections("x", true), "\n")
fixedLines := lipgloss.Height(ui.FrameStyle.Render(probe)) - 1
return max(3, min(limit, m.height-fixedLines))
}
// collapsedPlVisible returns the natural (non-expanded) playlist height.
func (m *Model) collapsedPlVisible() int {
return m.measurePlVisible(maxPlVisible)
}
// expandedPlVisible returns the expanded playlist height with no cap.
func (m *Model) expandedPlVisible() int {
return m.measurePlVisible(m.height)
}
// applyHeightMode sets plVisible based on the current heightExpanded state.
func (m *Model) applyHeightMode() {
if m.playlist == nil {
return
}
if m.heightExpanded {
m.plVisible = m.expandedPlVisible()
} else {
m.plVisible = m.collapsedPlVisible()
}
}
// adjustScroll ensures plCursor is visible in the playlist view.
// It accounts for album separator lines that reduce the number of
// tracks that fit in the visible window.
func (m *Model) adjustScroll() {
if m.playlist == nil {
return
}
tracks := m.playlist.Tracks()
if len(tracks) == 0 {
return
}
visible := m.effectivePlaylistVisible()
if visible <= 0 {
return
}
m.plScroll = m.playlistScroll(visible)
}
func (m Model) playlistScroll(visible int) int {
tracks := m.playlist.Tracks()
scroll := max(0, m.plScroll)
if scroll >= len(tracks) {
scroll = max(0, len(tracks)-1)
}
if m.plCursor < scroll {
return m.plCursor
}
for scroll < m.plCursor && m.albumSeparatorRows(tracks, scroll, m.plCursor, m.showAlbumHeaders) > visible {
scroll++
}
return scroll
}
func (m Model) mainFrameFixedLines(includeTransient bool) int {
if m.chromeOK {
if !includeTransient {
return m.chromeHeight
}
transientLines := len(m.footerMessages())
if m.err != nil {
transientLines++
}
return m.chromeHeight + transientLines
}
// Fallback: render and measure (only needed until first WindowSizeMsg)
content := strings.Join(m.mainSections("", includeTransient), "\n")
return lipgloss.Height(ui.FrameStyle.Render(content))
}
func (m Model) effectivePlaylistVisible() int {
available := m.height - m.mainFrameFixedLines(true)
if available <= 0 {
return 0
}
if m.plVisible <= 0 {
return 0
}
return min(m.plVisible, available)
}
// recomputeChrome renders the fixed chrome (without playlist or transients)
// and caches its height. Called when terminal width or compact mode changes.
func (m *Model) recomputeChrome() {
content := strings.Join(m.mainSections("", false), "\n")
m.chromeHeight = lipgloss.Height(ui.FrameStyle.Render(content))
m.chromeOK = true
}
// invalidateChrome marks the chrome height dirty. Until the next recompute,
// mainFrameFixedLines falls back to direct measurement.
func (m *Model) invalidateChrome() {
m.chromeOK = false
}
func (m *Model) refreshChrome() {
if m.width > 0 {
m.recomputeChrome()
return
}
m.invalidateChrome()
}