github.com/grahambrereton-form3/tilt@v0.10.18/internal/rty/scroll.go (about)

     1  package rty
     2  
     3  import (
     4  	"strings"
     5  )
     6  
     7  type StatefulComponent interface {
     8  	RenderStateful(w Writer, prevState interface{}, width, height int) (state interface{}, err error)
     9  }
    10  
    11  type TextScrollLayout struct {
    12  	name string
    13  	cs   []Component
    14  }
    15  
    16  var _ Component = &TextScrollLayout{}
    17  
    18  func NewTextScrollLayout(name string) *TextScrollLayout {
    19  	return &TextScrollLayout{name: name}
    20  }
    21  
    22  func (l *TextScrollLayout) Add(c Component) {
    23  	l.cs = append(l.cs, c)
    24  }
    25  
    26  func (l *TextScrollLayout) Size(width int, height int) (int, int, error) {
    27  	return width, height, nil
    28  }
    29  
    30  type TextScrollState struct {
    31  	width  int
    32  	height int
    33  
    34  	canvasIdx     int
    35  	lineIdx       int // line within canvas
    36  	canvasLengths []int
    37  
    38  	following bool
    39  }
    40  
    41  func defaultTextScrollState() *TextScrollState {
    42  	return &TextScrollState{following: true}
    43  }
    44  func (l *TextScrollLayout) Render(w Writer, width, height int) error {
    45  	w.RenderStateful(l, l.name)
    46  	return nil
    47  }
    48  
    49  func (l *TextScrollLayout) RenderStateful(w Writer, prevState interface{}, width, height int) (state interface{}, err error) {
    50  	prev, ok := prevState.(*TextScrollState)
    51  	if !ok {
    52  		prev = defaultTextScrollState()
    53  	}
    54  	next := &TextScrollState{
    55  		width:     width,
    56  		height:    height,
    57  		following: prev.following,
    58  	}
    59  
    60  	if len(l.cs) == 0 {
    61  		return next, nil
    62  	}
    63  
    64  	scrollbarWriter, err := w.Divide(width-1, 0, 1, height)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	w, err = w.Divide(0, 0, width-1, height)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	next.canvasLengths = make([]int, len(l.cs))
    74  	canvases := make([]Canvas, len(l.cs))
    75  
    76  	for i, c := range l.cs {
    77  		childCanvas := w.RenderChildInTemp(c)
    78  		canvases[i] = childCanvas
    79  		_, childHeight := childCanvas.Size()
    80  		next.canvasLengths[i] = childHeight
    81  	}
    82  
    83  	l.adjustCursor(prev, next, canvases)
    84  
    85  	y := 0
    86  	canvases = canvases[next.canvasIdx:]
    87  
    88  	if next.lineIdx != 0 {
    89  		firstCanvas := canvases[0]
    90  		canvases = canvases[1:]
    91  		_, firstHeight := firstCanvas.Size()
    92  		numLines := firstHeight - prev.lineIdx
    93  		if numLines > height {
    94  			numLines = height
    95  		}
    96  
    97  		w, err := w.Divide(0, 0, width-1, numLines)
    98  		if err != nil {
    99  			return nil, err
   100  		}
   101  
   102  		err = w.Embed(firstCanvas, next.lineIdx, numLines)
   103  		if err != nil {
   104  			return nil, err
   105  		}
   106  		y += numLines
   107  	}
   108  
   109  	for _, canvas := range canvases {
   110  		_, canvasHeight := canvas.Size()
   111  		numLines := canvasHeight
   112  		if numLines > height-y {
   113  			numLines = height - y
   114  		}
   115  		w, err := w.Divide(0, y, width-1, numLines)
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  
   120  		err = w.Embed(canvas, 0, numLines)
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  		y += numLines
   125  	}
   126  
   127  	if height >= 2 {
   128  		if next.lineIdx > 0 || next.canvasIdx > 0 {
   129  			scrollbarWriter.SetContent(0, 0, '↑', nil)
   130  		}
   131  
   132  		if y >= height && !next.following {
   133  			scrollbarWriter.SetContent(0, height-1, '↓', nil)
   134  		}
   135  	}
   136  
   137  	return next, nil
   138  }
   139  
   140  func (l *TextScrollLayout) adjustCursor(prev *TextScrollState, next *TextScrollState, canvases []Canvas) {
   141  	if next.following {
   142  		next.jumpToBottom(canvases)
   143  		return
   144  	}
   145  
   146  	if prev.canvasIdx >= len(canvases) {
   147  		return
   148  	}
   149  
   150  	next.canvasIdx = prev.canvasIdx
   151  	_, canvasHeight := canvases[next.canvasIdx].Size()
   152  	if prev.lineIdx >= canvasHeight {
   153  		return
   154  	}
   155  	next.lineIdx = prev.lineIdx
   156  }
   157  
   158  func (s *TextScrollState) jumpToBottom(canvases []Canvas) {
   159  	totalHeight := totalHeight(canvases)
   160  	if totalHeight <= s.height {
   161  		// all content fits on the screen
   162  		s.canvasIdx = 0
   163  		s.lineIdx = 0
   164  		return
   165  	}
   166  
   167  	heightLeft := s.height
   168  	for i := range canvases {
   169  		// we actually want to iterate from the end
   170  		iEnd := len(canvases) - i - 1
   171  		c := canvases[iEnd]
   172  
   173  		_, cHeight := c.Size()
   174  		if cHeight < heightLeft {
   175  			heightLeft -= cHeight
   176  		} else if cHeight == heightLeft {
   177  			// start at the beginning of this canvas
   178  			s.canvasIdx = iEnd
   179  			s.lineIdx = 0
   180  			return
   181  		} else {
   182  			// start some number of lines into this canvas.
   183  			s.canvasIdx = iEnd
   184  			s.lineIdx = cHeight - heightLeft
   185  			return
   186  		}
   187  	}
   188  }
   189  
   190  type TextScrollController struct {
   191  	state *TextScrollState
   192  }
   193  
   194  func (s *TextScrollController) Top() {
   195  	st := s.state
   196  	if st.canvasIdx != 0 || st.lineIdx != 0 {
   197  		s.SetFollow(false)
   198  	}
   199  	st.canvasIdx = 0
   200  	st.lineIdx = 0
   201  }
   202  
   203  func (s *TextScrollController) Bottom() {
   204  	s.SetFollow(true)
   205  }
   206  
   207  func (s *TextScrollController) Up() {
   208  	st := s.state
   209  	if st.lineIdx != 0 {
   210  		s.SetFollow(false)
   211  		st.lineIdx--
   212  		return
   213  	}
   214  
   215  	if st.canvasIdx == 0 {
   216  		return
   217  	}
   218  	s.SetFollow(false)
   219  	st.canvasIdx--
   220  	st.lineIdx = st.canvasLengths[st.canvasIdx] - 1
   221  }
   222  
   223  func (s *TextScrollController) Down() {
   224  	st := s.state
   225  
   226  	if st.following {
   227  		return
   228  	}
   229  
   230  	if len(st.canvasLengths) == 0 {
   231  		return
   232  	}
   233  
   234  	canvasLength := st.canvasLengths[st.canvasIdx]
   235  	if st.lineIdx+st.height < canvasLength-1 {
   236  		// we can just go down in this canvas
   237  		st.lineIdx++
   238  		return
   239  	}
   240  	if st.canvasIdx == len(st.canvasLengths)-1 {
   241  		// we're at the end of the last canvas
   242  		s.SetFollow(true)
   243  		return
   244  	}
   245  	st.canvasIdx++
   246  	st.lineIdx = 0
   247  }
   248  
   249  func (s *TextScrollController) ToggleFollow() {
   250  	s.state.following = !s.state.following
   251  }
   252  
   253  func (s *TextScrollController) SetFollow(follow bool) {
   254  	s.state.following = follow
   255  }
   256  
   257  func NewScrollingWrappingTextArea(name string, text string) Component {
   258  	l := NewTextScrollLayout(name)
   259  	lines := strings.Split(text, "\n")
   260  	for _, line := range lines {
   261  		l.Add(TextString(line + "\n"))
   262  	}
   263  	return l
   264  }
   265  
   266  type ElementScrollLayout struct {
   267  	name     string
   268  	children []Component
   269  }
   270  
   271  var _ Component = &ElementScrollLayout{}
   272  
   273  func NewElementScrollLayout(name string) *ElementScrollLayout {
   274  	return &ElementScrollLayout{name: name}
   275  }
   276  
   277  func (l *ElementScrollLayout) Add(c Component) {
   278  	l.children = append(l.children, c)
   279  }
   280  
   281  func (l *ElementScrollLayout) Size(width int, height int) (int, int, error) {
   282  	return width, height, nil
   283  }
   284  
   285  type ElementScrollState struct {
   286  	width  int
   287  	height int
   288  
   289  	firstVisibleElement int
   290  
   291  	children []string
   292  
   293  	elementIdx int
   294  }
   295  
   296  func (l *ElementScrollLayout) Render(w Writer, width, height int) error {
   297  	w.RenderStateful(l, l.name)
   298  	return nil
   299  }
   300  
   301  func (l *ElementScrollLayout) RenderStateful(w Writer, prevState interface{}, width, height int) (state interface{}, err error) {
   302  	prev, ok := prevState.(*ElementScrollState)
   303  	if !ok {
   304  		prev = &ElementScrollState{}
   305  	}
   306  
   307  	next := *prev
   308  	next.width = width
   309  	next.height = height
   310  
   311  	if len(l.children) == 0 {
   312  		return &next, nil
   313  	}
   314  
   315  	scrollbarWriter, err := w.Divide(width-1, 0, 1, height)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  	w, err = w.Divide(0, 0, width-1, height)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  
   324  	var canvases []Canvas
   325  	var heights []int
   326  	for _, c := range l.children {
   327  		canvas := w.RenderChildInTemp(c)
   328  		canvases = append(canvases, canvas)
   329  		_, childHeight := canvas.Size()
   330  		heights = append(heights, childHeight)
   331  	}
   332  
   333  	next.firstVisibleElement = calculateFirstVisibleElement(next, heights, height)
   334  
   335  	y := 0
   336  	showDownArrow := false
   337  	for i, h := range heights {
   338  		if i >= next.firstVisibleElement {
   339  			if h > height-y {
   340  				h = height - y
   341  				showDownArrow = true
   342  			}
   343  			w, err := w.Divide(0, y, width-1, h)
   344  			if err != nil {
   345  				return nil, err
   346  			}
   347  
   348  			err = w.Embed(canvases[i], 0, h)
   349  			if err != nil {
   350  				return nil, err
   351  			}
   352  			y += h
   353  		}
   354  	}
   355  
   356  	if next.firstVisibleElement != 0 {
   357  		scrollbarWriter.SetContent(0, 0, '↑', nil)
   358  	}
   359  
   360  	if showDownArrow {
   361  		scrollbarWriter.SetContent(0, height-1, '↓', nil)
   362  	}
   363  
   364  	return &next, nil
   365  }
   366  
   367  func calculateFirstVisibleElement(state ElementScrollState, heights []int, height int) int {
   368  	if state.elementIdx < state.firstVisibleElement {
   369  		// if we've scrolled back above the old first visible element, just make the selected element the first visible
   370  		return state.elementIdx
   371  	} else if state.elementIdx > state.firstVisibleElement {
   372  		var lastLineOfSelectedElement int
   373  		for _, h := range heights[state.firstVisibleElement : state.elementIdx+1] {
   374  			lastLineOfSelectedElement += h
   375  		}
   376  
   377  		if lastLineOfSelectedElement > height {
   378  			// the selected element isn't fully visible, so start from that element and work backwards, adding previous elements
   379  			// as long as they'll fit on the screen
   380  			if lastLineOfSelectedElement > state.height {
   381  				firstVisibleElement := state.elementIdx
   382  				heightUsed := heights[firstVisibleElement]
   383  				for firstVisibleElement > 0 {
   384  					prevHeight := heights[firstVisibleElement-1]
   385  					if heightUsed+prevHeight > state.height {
   386  						break
   387  					}
   388  					firstVisibleElement--
   389  					heightUsed += prevHeight
   390  				}
   391  				return firstVisibleElement
   392  			}
   393  		}
   394  	}
   395  
   396  	return state.firstVisibleElement
   397  }
   398  
   399  type ElementScrollController struct {
   400  	state *ElementScrollState
   401  }
   402  
   403  func adjustElementScroll(prevInt interface{}, newChildren []string) (*ElementScrollState, string) {
   404  	prev, ok := prevInt.(*ElementScrollState)
   405  	if !ok {
   406  		prev = &ElementScrollState{}
   407  	}
   408  
   409  	clone := *prev
   410  	next := &clone
   411  	next.children = newChildren
   412  
   413  	if len(newChildren) == 0 {
   414  		next.elementIdx = 0
   415  		return next, ""
   416  	}
   417  	if len(prev.children) == 0 {
   418  		sel := ""
   419  		if len(next.children) > 0 {
   420  			sel = next.children[0]
   421  		}
   422  		return next, sel
   423  	}
   424  	if prev.elementIdx >= len(prev.children) {
   425  		// NB(dbentley): this should be impossible, but we were hitting it and it was crashing
   426  		next.elementIdx = 0
   427  		return next, ""
   428  	}
   429  	prevChild := prev.children[prev.elementIdx]
   430  	for i, child := range newChildren {
   431  		if child == prevChild {
   432  			next.elementIdx = i
   433  			return next, child
   434  		}
   435  	}
   436  	return next, next.children[0]
   437  }
   438  
   439  func (s *ElementScrollController) GetSelectedIndex() int {
   440  	return s.state.elementIdx
   441  }
   442  
   443  func (s *ElementScrollController) GetSelectedChild() string {
   444  	if len(s.state.children) == 0 {
   445  		return ""
   446  	}
   447  	return s.state.children[s.state.elementIdx]
   448  }
   449  
   450  func (s *ElementScrollController) Up() {
   451  	if s.state.elementIdx == 0 {
   452  		return
   453  	}
   454  
   455  	s.state.elementIdx--
   456  }
   457  
   458  func (s *ElementScrollController) Down() {
   459  	if s.state.elementIdx == len(s.state.children)-1 {
   460  		return
   461  	}
   462  	s.state.elementIdx++
   463  }
   464  
   465  func (s *ElementScrollController) Top() {
   466  	s.state.elementIdx = 0
   467  }
   468  
   469  func (s *ElementScrollController) Bottom() {
   470  	s.state.elementIdx = len(s.state.children) - 1
   471  }