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 }