src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/tk/textview.go (about) 1 package tk 2 3 import ( 4 "sync" 5 6 "src.elv.sh/pkg/cli/term" 7 "src.elv.sh/pkg/ui" 8 "src.elv.sh/pkg/wcwidth" 9 ) 10 11 // TextView is a Widget for displaying text, with support for vertical 12 // scrolling. 13 // 14 // NOTE: This widget now always crops long lines. In future it should support 15 // wrapping and horizontal scrolling. 16 type TextView interface { 17 Widget 18 // ScrollBy scrolls the widget by the given delta. Positive values scroll 19 // down, and negative values scroll up. 20 ScrollBy(delta int) 21 // MutateState mutates the state. 22 MutateState(f func(*TextViewState)) 23 // CopyState returns a copy of the State. 24 CopyState() TextViewState 25 } 26 27 // TextViewSpec specifies the configuration and initial state for a Widget. 28 type TextViewSpec struct { 29 // Key bindings. 30 Bindings Bindings 31 // If true, a vertical scrollbar will be shown when there are more lines 32 // that can be displayed, and the widget responds to Up and Down keys. 33 Scrollable bool 34 // State. Specifies the initial state if used in New. 35 State TextViewState 36 } 37 38 // TextViewState keeps mutable state of TextView. 39 type TextViewState struct { 40 Lines []string 41 First int 42 } 43 44 type textView struct { 45 // Mutex for synchronizing access to the state. 46 StateMutex sync.RWMutex 47 TextViewSpec 48 } 49 50 // NewTextView builds a TextView from the given spec. 51 func NewTextView(spec TextViewSpec) TextView { 52 if spec.Bindings == nil { 53 spec.Bindings = DummyBindings{} 54 } 55 return &textView{TextViewSpec: spec} 56 } 57 58 func (w *textView) Render(width, height int) *term.Buffer { 59 lines, first := w.getStateForRender(height) 60 needScrollbar := w.Scrollable && (first > 0 || first+height < len(lines)) 61 textWidth := width 62 if needScrollbar { 63 textWidth-- 64 } 65 66 bb := term.NewBufferBuilder(textWidth) 67 for i := first; i < first+height && i < len(lines); i++ { 68 if i > first { 69 bb.Newline() 70 } 71 bb.Write(wcwidth.Trim(lines[i], textWidth)) 72 } 73 buf := bb.Buffer() 74 75 if needScrollbar { 76 scrollbar := VScrollbar{ 77 Total: len(lines), Low: first, High: first + height} 78 buf.ExtendRight(scrollbar.Render(1, height)) 79 } 80 return buf 81 } 82 83 func (w *textView) MaxHeight(width, height int) int { 84 return len(w.CopyState().Lines) 85 } 86 87 func (w *textView) getStateForRender(height int) (lines []string, first int) { 88 w.MutateState(func(s *TextViewState) { 89 if s.First > len(s.Lines)-height && len(s.Lines)-height >= 0 { 90 s.First = len(s.Lines) - height 91 } 92 lines, first = s.Lines, s.First 93 }) 94 return 95 } 96 97 func (w *textView) Handle(event term.Event) bool { 98 if w.Bindings.Handle(w, event) { 99 return true 100 } 101 102 if w.Scrollable { 103 switch event { 104 case term.K(ui.Up): 105 w.ScrollBy(-1) 106 return true 107 case term.K(ui.Down): 108 w.ScrollBy(1) 109 return true 110 } 111 } 112 return false 113 } 114 115 func (w *textView) ScrollBy(delta int) { 116 w.MutateState(func(s *TextViewState) { 117 s.First += delta 118 if s.First < 0 { 119 s.First = 0 120 } 121 if s.First >= len(s.Lines) { 122 s.First = len(s.Lines) - 1 123 } 124 }) 125 } 126 127 func (w *textView) MutateState(f func(*TextViewState)) { 128 w.StateMutex.Lock() 129 defer w.StateMutex.Unlock() 130 f(&w.State) 131 } 132 133 // CopyState returns a copy of the State while r-locking the StateMutex. 134 func (w *textView) CopyState() TextViewState { 135 w.StateMutex.RLock() 136 defer w.StateMutex.RUnlock() 137 return w.State 138 }