github.com/grahambrereton-form3/tilt@v0.10.18/internal/rty/layouts.go (about) 1 package rty 2 3 import ( 4 "fmt" 5 6 "github.com/rivo/tview" 7 8 "github.com/gdamore/tcell" 9 ) 10 11 // Layouts implement Component 12 13 type Dir int 14 15 const ( 16 DirHor Dir = iota 17 DirVert 18 ) 19 20 var EmptyLayout = NewConcatLayout(DirVert) 21 22 func IsEmpty(l Component) bool { 23 return l == nil || l == EmptyLayout 24 } 25 26 type Align int 27 28 const ( 29 AlignStart Align = iota 30 AlignEnd 31 ) 32 33 // FlexLayout lays out its sub-components. 34 type FlexLayout struct { 35 dir Dir 36 cs []Component 37 } 38 39 var _ Component = &FlexLayout{} 40 41 func NewFlexLayout(dir Dir) *FlexLayout { 42 return &FlexLayout{ 43 dir: dir, 44 } 45 } 46 47 func (l *FlexLayout) Add(c Component) *FlexLayout { 48 l.cs = append(l.cs, c) 49 return l 50 } 51 52 func whToLd(width int, height int, dir Dir) (length int, depth int) { 53 if dir == DirVert { 54 return height, width 55 } 56 return width, height 57 } 58 59 func ldToWh(length int, depth int, dir Dir) (width int, height int) { 60 if dir == DirVert { 61 return depth, length 62 } 63 return length, depth 64 } 65 66 func (l *FlexLayout) Size(width int, height int) (int, int, error) { 67 return width, height, nil 68 } 69 70 func (l *FlexLayout) Render(w Writer, width, height int) error { 71 length, _ := whToLd(width, height, l.dir) 72 73 allocations := make([]int, len(l.cs)) 74 allocated := 0 75 var flexIdxs []int 76 77 for i, c := range l.cs { 78 reqWidth, reqHeight, err := c.Size(width, height) 79 if err != nil { 80 return err 81 } 82 reqLen, _ := whToLd(reqWidth, reqHeight, l.dir) 83 if allocated+reqLen >= length { 84 flexIdxs = append(flexIdxs, i) 85 } else { 86 allocations[i] = reqLen 87 allocated += reqLen 88 } 89 } 90 91 flexTotal := length - allocated 92 if flexTotal < 0 { 93 noun := "lines" 94 if l.dir == DirHor { 95 noun = "columns" 96 } 97 98 return fmt.Errorf("FlexLayout can't render in %v %s; need at least %v", length, noun, allocated) 99 } 100 numFlex := len(flexIdxs) 101 for _, i := range flexIdxs { 102 elemLength := flexTotal / numFlex 103 allocations[i] = elemLength 104 numFlex-- 105 flexTotal -= elemLength 106 } 107 108 offset := 0 109 for i, c := range l.cs { 110 elemLength := allocations[i] 111 112 var subW Writer 113 114 if l.dir == DirHor { 115 var err error 116 subW, err = w.Divide(offset, 0, allocations[i], height) 117 if err != nil { 118 return err 119 } 120 } else { 121 var err error 122 subW, err = w.Divide(0, offset, width, allocations[i]) 123 if err != nil { 124 return err 125 } 126 } 127 128 offset += elemLength 129 130 subW.RenderChild(c) 131 } 132 return nil 133 } 134 135 type concatLayoutComponent struct { 136 c Component 137 fixed bool 138 } 139 140 type ConcatLayout struct { 141 dir Dir 142 cs []concatLayoutComponent 143 } 144 145 var _ Component = &ConcatLayout{} 146 147 func NewConcatLayout(dir Dir) *ConcatLayout { 148 return &ConcatLayout{dir: dir} 149 } 150 151 func (l *ConcatLayout) Add(c Component) *ConcatLayout { 152 l.cs = append(l.cs, concatLayoutComponent{c, true}) 153 return l 154 } 155 156 // A ConcatLayout element can be either fixed or dynamic. Fixed components are all given a chance at the full 157 // canvas. If they ask for too much in sum, things will break. 158 // Dynamic components get equal shares of whatever is left after the fixed components get theirs. 159 // NB: There is currently a bit of a murky line between ConcatLayout and FlexLayout. 160 func (l *ConcatLayout) AddDynamic(c Component) *ConcatLayout { 161 l.cs = append(l.cs, concatLayoutComponent{c, false}) 162 return l 163 } 164 165 func (l *ConcatLayout) allocate(width, height int) (widths []int, heights []int, allocatedLen int, maxDepth int, err error) { 166 length, depth := whToLd(width, height, l.dir) 167 168 type componentAndIndex struct { 169 c Component 170 index int 171 } 172 173 var fixedComponents, unfixedComponents []componentAndIndex 174 for i, clc := range l.cs { 175 if clc.fixed { 176 fixedComponents = append(fixedComponents, componentAndIndex{clc.c, i}) 177 } else { 178 unfixedComponents = append(unfixedComponents, componentAndIndex{clc.c, i}) 179 } 180 } 181 182 alloc := func(c Component, w, h int) (int, int, error) { 183 reqWidth, reqHeight, err := c.Size(w, h) 184 if err != nil { 185 return 0, 0, err 186 } 187 reqLen, reqDepth := whToLd(reqWidth, reqHeight, l.dir) 188 if reqLen == GROW { 189 allocatedLen = GROW 190 } else { 191 allocatedLen += reqLen 192 } 193 if reqDepth > maxDepth { 194 maxDepth = reqDepth 195 } 196 197 return reqWidth, reqHeight, nil 198 } 199 200 widths = make([]int, len(l.cs)) 201 heights = make([]int, len(l.cs)) 202 203 for _, c := range fixedComponents { 204 len, dep := whToLd(width, height, l.dir) 205 len -= allocatedLen 206 if len <= 0 { 207 widths[c.index], heights[c.index] = 0, 0 208 continue 209 } 210 211 widthRemainder, heightRemainder := ldToWh(len, dep, l.dir) 212 w, h, err := alloc(c.c, widthRemainder, heightRemainder) 213 if err != nil { 214 return nil, nil, 0, 0, err 215 } 216 widths[c.index], heights[c.index] = w, h 217 } 218 219 if len(unfixedComponents) > 0 { 220 lenPerUnfixed := (length - allocatedLen) / len(unfixedComponents) 221 for _, c := range unfixedComponents { 222 if lenPerUnfixed <= 0 { 223 widths[c.index], heights[c.index] = 0, 0 224 continue 225 } 226 227 w, h := ldToWh(lenPerUnfixed, depth, l.dir) 228 reqW, reqH, err := alloc(c.c, w, h) 229 if err != nil { 230 return nil, nil, 0, 0, err 231 } 232 widths[c.index], heights[c.index] = reqW, reqH 233 } 234 } 235 236 return widths, heights, allocatedLen, maxDepth, nil 237 } 238 239 func (l *ConcatLayout) Size(width, height int) (int, int, error) { 240 _, _, allocatedLen, maxDepth, err := l.allocate(width, height) 241 if err != nil { 242 return 0, 0, err 243 } 244 len, dep := ldToWh(allocatedLen, maxDepth, l.dir) 245 return len, dep, err 246 } 247 248 func (l *ConcatLayout) Render(w Writer, width int, height int) error { 249 if width <= 0 && height <= 0 { 250 return nil 251 } 252 253 widths, heights, _, _, err := l.allocate(width, height) 254 if err != nil { 255 return err 256 } 257 258 offset := 0 259 for i, c := range l.cs { 260 reqWidth, reqHeight, err := c.c.Size(widths[i], heights[i]) 261 if err != nil { 262 return err 263 } 264 265 if reqWidth <= 0 && reqHeight <= 0 { 266 continue 267 } 268 269 var subW Writer 270 if l.dir == DirHor { 271 var err error 272 subW, err = w.Divide(offset, 0, reqWidth, reqHeight) 273 if err != nil { 274 return err 275 } 276 offset += reqWidth 277 } else { 278 var err error 279 subW, err = w.Divide(0, offset, reqWidth, reqHeight) 280 if err != nil { 281 return err 282 } 283 offset += reqHeight 284 } 285 286 subW.RenderChild(c.c) 287 } 288 return nil 289 } 290 291 func NewLines() *ConcatLayout { 292 return NewConcatLayout(DirVert) 293 } 294 295 type Line struct { 296 del *FlexLayout 297 } 298 299 var _ Component = &Line{} 300 301 func NewLine() *Line { 302 return &Line{del: NewFlexLayout(DirHor)} 303 } 304 305 func OneLine(c Component) *Line { 306 l := NewLine() 307 l.Add(c) 308 return l 309 } 310 311 func (l *Line) Add(c Component) { 312 l.del.Add(c) 313 } 314 315 func (l *Line) Size(width int, height int) (int, int, error) { 316 return width, 1, nil 317 } 318 319 func (l *Line) Render(w Writer, width int, height int) error { 320 if height == 0 { 321 return nil 322 } 323 w.SetContent(0, 0, 0, nil) // set at least one to take up our line 324 w, err := w.Divide(0, 0, width, height) 325 if err != nil { 326 return err 327 } 328 w.RenderChild(l.del) 329 return nil 330 } 331 332 // Fills a space by repeating a string 333 type FillerString struct { 334 ch rune 335 } 336 337 var _ Component = &FillerString{} 338 339 func NewFillerString(ch rune) *FillerString { 340 return &FillerString{ch: ch} 341 } 342 343 func (f *FillerString) Size(width int, height int) (int, int, error) { 344 return width, 1, nil 345 } 346 347 func (f *FillerString) Render(w Writer, width int, height int) error { 348 for i := 0; i < width; i++ { 349 w.SetContent(i, 0, f.ch, nil) 350 } 351 return nil 352 } 353 354 type ColorLayout struct { 355 del Component 356 color tcell.Color 357 foreground bool 358 } 359 360 var _ Component = &ColorLayout{} 361 362 func Fg(del Component, color tcell.Color) Component { 363 return &ColorLayout{ 364 del: del, 365 color: color, 366 foreground: true, 367 } 368 } 369 370 func Bg(del Component, color tcell.Color) Component { 371 return &ColorLayout{ 372 del: del, 373 color: color, 374 foreground: false, 375 } 376 } 377 378 func (l *ColorLayout) Size(width int, height int) (int, int, error) { 379 return l.del.Size(width, height) 380 } 381 382 func (l *ColorLayout) Render(w Writer, width int, height int) error { 383 if l.foreground { 384 w = w.Foreground(l.color) 385 } else { 386 w = w.Background(l.color) 387 } 388 w, err := w.Fill() 389 if err != nil { 390 return err 391 } 392 w.RenderChild(l.del) 393 return nil 394 } 395 396 type Box struct { 397 title string 398 inner Component 399 grow bool 400 401 tl, t, tr, l, r, bl, b, br rune 402 invertTitle bool 403 } 404 405 var _ Component = &Box{} 406 407 func newBox() *Box { 408 return &Box{ 409 tl: tview.BoxDrawingsLightDownAndRight, 410 t: tview.BoxDrawingsLightHorizontal, 411 tr: tview.BoxDrawingsLightDownAndLeft, 412 l: tview.BoxDrawingsLightVertical, 413 r: tview.BoxDrawingsLightVertical, 414 bl: tview.BoxDrawingsLightUpAndRight, 415 b: tview.BoxDrawingsLightHorizontal, 416 br: tview.BoxDrawingsLightUpAndLeft, 417 } 418 } 419 420 // makes a box that will grow to fill its canvas 421 func NewGrowingBox() *Box { 422 ret := newBox() 423 ret.grow = true 424 return ret 425 } 426 427 // makes a new box that tightly wraps its inner component 428 func NewBox(inner Component) *Box { 429 ret := newBox() 430 ret.inner = inner 431 return ret 432 } 433 434 func newWindow() *Box { 435 return &Box{ 436 tl: '█', 437 t: '█', 438 tr: '█', 439 l: '▏', 440 r: '▕', 441 bl: '▔', 442 b: '▔', 443 br: '▔', 444 invertTitle: true, 445 } 446 } 447 448 func NewWindow(inner Component) *Box { 449 ret := newWindow() 450 ret.inner = inner 451 return ret 452 } 453 454 func NewGrowingWindow() *Box { 455 ret := newWindow() 456 ret.grow = true 457 return ret 458 } 459 460 func (b *Box) SetInner(c Component) { 461 b.inner = c 462 } 463 464 func (b *Box) SetTitle(title string) { 465 b.title = title 466 } 467 468 func (b *Box) Size(width int, height int) (int, int, error) { 469 if b.grow { 470 return width, height, nil 471 } else { 472 // +/-2 to account for the box chars themselves 473 w, h, err := b.inner.Size(width-2, height-2) 474 if err != nil { 475 return 0, 0, err 476 } 477 return w + 2, h + 2, nil 478 } 479 } 480 481 func (b *Box) Render(w Writer, width int, height int) error { 482 if height == GROW && b.inner == nil { 483 return fmt.Errorf("box must have either fixed height or a child") 484 } 485 486 width, height, err := b.Size(width, height) 487 if err != nil { 488 return err 489 } 490 491 if b.inner != nil { 492 innerHeight := height - 2 493 if height == GROW { 494 innerHeight = GROW 495 } 496 497 w, err := w.Divide(1, 1, width-2, innerHeight) 498 if err != nil { 499 return err 500 } 501 502 childHeight := w.RenderChild(b.inner) 503 height = childHeight + 2 504 } 505 506 for i := 1; i < width-1; i++ { 507 w.SetContent(i, 0, b.t, nil) 508 } 509 510 for i := 1; i < width-1; i++ { 511 w.SetContent(i, height-1, b.b, nil) 512 } 513 514 if len(b.title) > 0 { 515 middle := width / 2 516 titleMargin := 3 517 maxLength := width - (titleMargin * 2) 518 renderedTitle := b.title 519 if maxLength <= 0 { 520 renderedTitle = "" 521 } else if len(b.title) > maxLength { 522 renderedTitle = renderedTitle[0:maxLength] 523 } 524 525 // don't add spaces if we can't fit any of the actual title in 526 if len(renderedTitle) > 0 { 527 renderedTitle = fmt.Sprintf(" %s ", renderedTitle) 528 } 529 530 titleWriter := w 531 if b.invertTitle { 532 titleWriter = titleWriter.Invert() 533 } 534 535 start := middle - len(renderedTitle)/2 536 for i, c := range renderedTitle { 537 titleWriter.SetContent(start+i, 0, c, nil) 538 } 539 } 540 541 for i := 1; i < height-1; i++ { 542 w.SetContent(0, i, b.l, nil) 543 w.SetContent(width-1, i, b.r, nil) 544 } 545 546 w.SetContent(0, 0, b.tl, nil) 547 w.SetContent(width-1, 0, b.tr, nil) 548 w.SetContent(0, height-1, b.bl, nil) 549 w.SetContent(width-1, height-1, b.br, nil) 550 551 return nil 552 } 553 554 // FixedSizeLayout fixes a component to a size 555 type FixedSizeLayout struct { 556 del Component 557 width int 558 height int 559 } 560 561 var _ Component = &FixedSizeLayout{} 562 563 func NewFixedSize(del Component, width int, height int) *FixedSizeLayout { 564 return &FixedSizeLayout{del: del, width: width, height: height} 565 } 566 567 func (l *FixedSizeLayout) Size(width int, height int) (int, int, error) { 568 if l.width != GROW && l.height != GROW { 569 return l.width, l.height, nil 570 } 571 rWidth, rHeight := l.width, l.height 572 delWidth, delHeight, err := l.del.Size(width, height) 573 if err != nil { 574 return 0, 0, err 575 } 576 if rWidth == GROW { 577 rWidth = delWidth 578 } 579 if rHeight == GROW { 580 rHeight = delHeight 581 } 582 583 return rWidth, rHeight, nil 584 } 585 586 func (l *FixedSizeLayout) Render(w Writer, width int, height int) error { 587 w.RenderChild(l.del) 588 return nil 589 } 590 591 type ModalLayout struct { 592 bg Component 593 fg Component 594 fraction float64 595 fixed bool 596 } 597 598 var _ Component = &ModalLayout{} 599 600 // fg will be rendered on top of bg 601 // if fixed is true, it will use using fraction/1 of the height and width of the screen 602 // if fixed is false, it will use whatever `fg` asks for, up to fraction/1 of width and height 603 func NewModalLayout(bg Component, fg Component, fraction float64, fixed bool) *ModalLayout { 604 return &ModalLayout{fg: fg, bg: bg, fraction: fraction, fixed: fixed} 605 } 606 607 func (l *ModalLayout) Size(width int, height int) (int, int, error) { 608 w, h, err := l.bg.Size(width, height) 609 if err != nil { 610 return 0, 0, err 611 } 612 if l.fraction > 1 { 613 w = int(l.fraction * float64(w)) 614 h = int(l.fraction * float64(h)) 615 } 616 617 return w, h, nil 618 } 619 620 func (l *ModalLayout) Render(w Writer, width int, height int) error { 621 w.RenderChild(l.bg) 622 623 var mw, mh int 624 if !l.fixed { 625 var err error 626 mw, mh, err = l.fg.Size(int(l.fraction*float64(width)), int(l.fraction*float64(height))) 627 if err != nil { 628 return err 629 } 630 } else { 631 f := (1 - l.fraction) / 2 632 mw = int((1 - 2*f) * float64(width)) 633 mh = int((1 - 2*f) * float64(height)) 634 } 635 636 mx := width/2 - mw/2 637 my := height/2 - mh/2 638 w, err := w.Divide(mx, my, mw, mh) 639 if err != nil { 640 return err 641 } 642 w.RenderChild(l.fg) 643 return nil 644 } 645 646 type MinLengthLayout struct { 647 inner *ConcatLayout 648 minLength int 649 align Align 650 } 651 652 func NewMinLengthLayout(len int, dir Dir) *MinLengthLayout { 653 return &MinLengthLayout{ 654 inner: NewConcatLayout(dir), 655 minLength: len, 656 } 657 } 658 659 func (l *MinLengthLayout) SetAlign(val Align) *MinLengthLayout { 660 l.align = val 661 return l 662 } 663 664 func (l *MinLengthLayout) Add(c Component) *MinLengthLayout { 665 l.inner.Add(c) 666 return l 667 } 668 669 func (ml *MinLengthLayout) Size(width int, height int) (int, int, error) { 670 sizedWidth, sizedHeight, err := ml.inner.Size(width, height) 671 if err != nil { 672 return 0, 0, err 673 } 674 675 // If the inner container requested a length smaller than the minLength, 676 // increase the length to the minLength 677 sizedLen, sizedDep := whToLd(sizedWidth, sizedHeight, ml.inner.dir) 678 if sizedLen < ml.minLength { 679 sizedLen = ml.minLength 680 } 681 sizedWidth, sizedHeight = ldToWh(sizedLen, sizedDep, ml.inner.dir) 682 683 // It should be impossible to make the container bigger than the size alloted, 684 // even with minLength. 685 if sizedWidth > width { 686 sizedWidth = width 687 } 688 if sizedHeight > height { 689 sizedHeight = height 690 } 691 return sizedWidth, sizedHeight, err 692 } 693 694 func (ml *MinLengthLayout) Render(writer Writer, width int, height int) error { 695 if ml.align == AlignEnd { 696 w, h, err := ml.inner.Size(width, height) 697 if err != nil { 698 return err 699 } 700 701 indentW := 0 702 indentH := 0 703 if ml.inner.dir == DirHor { 704 indentW = width - w 705 } else { 706 indentH = height - h 707 } 708 if indentW != 0 || indentH != 0 { 709 subW, err := writer.Divide(indentW, indentH, w, h) 710 if err != nil { 711 return err 712 } 713 return ml.inner.Render(subW, w, h) 714 } 715 } 716 717 return ml.inner.Render(writer, width, height) 718 } 719 720 type MaxLengthLayout struct { 721 del Component 722 dir Dir 723 max int 724 } 725 726 func NewMaxLengthLayout(del Component, dir Dir, max int) MaxLengthLayout { 727 return MaxLengthLayout{ 728 del: del, 729 dir: dir, 730 max: max, 731 } 732 } 733 734 func (l MaxLengthLayout) Size(width int, height int) (int, int, error) { 735 width, height, err := l.del.Size(width, height) 736 if err != nil { 737 return 0, 0, err 738 } 739 740 if l.dir == DirHor && width > l.max { 741 width = l.max 742 } else if l.dir == DirVert && height > l.max { 743 height = l.max 744 } 745 return width, height, nil 746 } 747 748 func (l MaxLengthLayout) Render(w Writer, width int, height int) error { 749 return l.del.Render(w, width, height) 750 } 751 752 // A tail layout renders the "end" of its contents rather than the beginning 753 // when the contents overflow the box. 754 type TailLayout struct { 755 del Component 756 } 757 758 var _ Component = TailLayout{} 759 760 func NewTailLayout(del Component) TailLayout { 761 return TailLayout{del: del} 762 } 763 764 func (l TailLayout) Size(width int, height int) (int, int, error) { 765 return l.del.Size(width, height) 766 } 767 768 func (l TailLayout) Render(w Writer, width, height int) error { 769 // Measure the inner element. 770 canvas := w.RenderChildInTemp(l.del) 771 _, childHeight := canvas.Size() 772 773 // Truncate it if it's bigger than the available width. 774 lines := childHeight 775 diff := 0 776 if lines > height { 777 diff = lines - height 778 lines = height 779 } 780 return w.Embed(canvas, diff, lines) 781 }