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  }