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  }