github.com/elves/elvish@v0.15.0/pkg/cli/textview.go (about) 1 package cli 2 3 import ( 4 "sync" 5 6 "github.com/elves/elvish/pkg/cli/term" 7 "github.com/elves/elvish/pkg/ui" 8 "github.com/elves/elvish/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 // A Handler that takes precedence over the default handling of events. 30 OverlayHandler Handler 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.OverlayHandler == nil { 53 spec.OverlayHandler = DummyHandler{} 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) getStateForRender(height int) (lines []string, first int) { 84 w.MutateState(func(s *TextViewState) { 85 if s.First > len(s.Lines)-height && len(s.Lines)-height >= 0 { 86 s.First = len(s.Lines) - height 87 } 88 lines, first = s.Lines, s.First 89 }) 90 return 91 } 92 93 func (w *textView) Handle(event term.Event) bool { 94 if w.OverlayHandler.Handle(event) { 95 return true 96 } 97 98 if w.Scrollable { 99 switch event { 100 case term.K(ui.Up): 101 w.ScrollBy(-1) 102 return true 103 case term.K(ui.Down): 104 w.ScrollBy(1) 105 return true 106 } 107 } 108 return false 109 } 110 111 func (w *textView) ScrollBy(delta int) { 112 w.MutateState(func(s *TextViewState) { 113 s.First += delta 114 if s.First < 0 { 115 s.First = 0 116 } 117 if s.First >= len(s.Lines) { 118 s.First = len(s.Lines) - 1 119 } 120 }) 121 } 122 123 func (w *textView) MutateState(f func(*TextViewState)) { 124 w.StateMutex.Lock() 125 defer w.StateMutex.Unlock() 126 f(&w.State) 127 } 128 129 // CopyState returns a copy of the State while r-locking the StateMutex. 130 func (w *textView) CopyState() TextViewState { 131 w.StateMutex.RLock() 132 defer w.StateMutex.RUnlock() 133 return w.State 134 }