layout.go (2604B)
1 package tui 2 3 import "github.com/charmbracelet/lipgloss" 4 5 // detailViewKey identifies what the detail pane is showing. Scroll offsets 6 // survive re-renders of the same view (async readme/markdown results, window 7 // resizes, ticks) and only reset when the view itself changes. 8 type detailViewKey struct { 9 tab, project, experience int 10 } 11 12 func (m portfolioModel) currentDetailViewKey() detailViewKey { 13 return detailViewKey{tab: m.activeTab, project: m.projectIndex, experience: m.experienceIndex} 14 } 15 16 // syncViewport re-renders the detail pane, keeping the scroll position when 17 // the same tab/item is still selected. 18 func (m *portfolioModel) syncViewport() { 19 key := m.currentDetailViewKey() 20 same := m.detailViewSet && key == m.detailView 21 m.detailView, m.detailViewSet = key, true 22 23 m.ensureSelectedProjectMarkdown() 24 content := m.detailContent() 25 m.viewport.SetContent(content) 26 m.detailLineWidths = lineWidths(content) 27 w, h := m.detailSize() 28 m.viewport.Width = max(w, 1) 29 m.viewport.Height = max(h, 1) 30 31 if !same { 32 m.detailXOffset = 0 33 } 34 m.detailXOffset = min(max(m.detailXOffset, 0), m.detailMaxXOffset()) 35 m.viewport.SetXOffset(m.detailXOffset) 36 37 if m.activeTab == tabAbout && (m.shell.cleared || len(m.shell.history) > 0 || m.shell.input != "") { 38 m.viewport.GotoBottom() 39 return 40 } 41 if same { 42 m.viewport.SetYOffset(m.viewport.YOffset) // clamp to the new content 43 return 44 } 45 m.viewport.GotoTop() 46 } 47 48 // resetViewport re-renders the detail pane scrolled back to the top-left. 49 func (m *portfolioModel) resetViewport() { 50 m.detailViewSet = false 51 m.syncViewport() 52 } 53 54 func (m portfolioModel) detailSize() (int, int) { 55 bodyW, bodyH := m.bodySize() 56 57 if m.activeTab == tabProjects { 58 if bodyW < 72 { 59 stackTop := max(bodyH/2, 3) 60 stackBottom := max(bodyH-stackTop, 1) 61 return max(bodyW-4, 10), max(stackBottom-2, 3) 62 } 63 64 sideW := m.sidebarWidth(bodyW) 65 detailPanelW := bodyW - sideW 66 return max(detailPanelW-4, 10), max(bodyH-2, 3) 67 } 68 69 return max(bodyW-4, 10), max(bodyH-2, 3) 70 } 71 72 func (m portfolioModel) bodySize() (int, int) { 73 w := max(m.width, 40) 74 h := max(m.height, 12) 75 headerH := lipgloss.Height(m.renderHeader(w)) 76 footerH := lipgloss.Height(m.renderFooter(w)) 77 bodyH := max(h-headerH-footerH, 3) 78 return max(w, 20), bodyH 79 } 80 81 func (m portfolioModel) sidebarWidth(bodyWidth int) int { 82 if bodyWidth < 80 { 83 return bodyWidth 84 } 85 return min(max(bodyWidth/3, 24), 40) 86 } 87 88 func (m portfolioModel) detailContent() string { 89 switch m.activeTab { 90 case tabExperience: 91 return m.experiencePage() 92 case tabProjects: 93 return m.projectDetail() 94 default: 95 return m.aboutDetail() 96 } 97 }