aboutsummaryrefslogblamecommitdiffstats
path: root/lib/ui/table.go
blob: 3f5fc4b381c6e8560445dec7ddb5a65cb112769e (plain) (tree)
1
2
3
4
5
6
7
8




                

                                       
                                     





                                       




                                                                                
                                                       



















                                                              
                   

                                                         
                                                  




                                                                              

                                                             















                                                                            





















                                                               
                                          



                                                       
                                                                 

                                                                       




                                 
                         















                                                                         
                                                                                   












                                                     
                       

                                    
                                                                          




                                                     




                                                        













                                                                 
                                 
                          



                                                  

                                                
                                             

                                                


                                                    
                                                


                                                       
                                             

                                                


                                                   

                                               
                                             

                                                    







                                    
                                            






                                               
                                            




                                                                
 


                                                 



                                                                                     
package ui

import (
	"math"
	"regexp"

	"git.sr.ht/~rjarry/aerc/config"
	"git.sr.ht/~rockorager/vaxis"
	"github.com/mattn/go-runewidth"
)

type Table struct {
	Columns []Column
	Rows    []Row
	Height  int
	// Optional callback that allows customizing the default drawing routine
	// of table rows. If true is returned, the default routine is skipped.
	CustomDraw func(t *Table, row int, c *Context) bool
	// Optional callback that allows returning a custom style for the row.
	GetRowStyle func(t *Table, row int) vaxis.Style

	// true if at least one column has WIDTH_FIT
	autoFitWidths bool
	// if false, widths need to be computed before drawing
	widthsComputed bool
}

type Column struct {
	Offset    int
	Width     int
	Def       *config.ColumnDef
	Separator string
}

type Row struct {
	Cells []string
	Priv  interface{}
}

func NewTable(
	height int,
	columnDefs []*config.ColumnDef, separator string,
	customDraw func(*Table, int, *Context) bool,
	getRowStyle func(*Table, int) vaxis.Style,
) Table {
	if customDraw == nil {
		customDraw = func(*Table, int, *Context) bool { return false }
	}
	if getRowStyle == nil {
		getRowStyle = func(*Table, int) vaxis.Style {
			return vaxis.Style{}
		}
	}
	columns := make([]Column, len(columnDefs))
	autoFitWidths := false
	for c, col := range columnDefs {
		if col.Flags.Has(config.WIDTH_FIT) {
			autoFitWidths = true
		}
		columns[c] = Column{Def: col}
		if c != len(columns)-1 {
			// set separator for all columns except the last one
			columns[c].Separator = separator
		}
	}
	return Table{
		Columns:       columns,
		Height:        height,
		CustomDraw:    customDraw,
		GetRowStyle:   getRowStyle,
		autoFitWidths: autoFitWidths,
	}
}

// add a row to the table, returns true when the table is full
func (t *Table) AddRow(cells []string, priv interface{}) bool {
	if len(cells) != len(t.Columns) {
		panic("invalid number of cells")
	}
	if len(t.Rows) >= t.Height {
		return true
	}
	t.Rows = append(t.Rows, Row{Cells: cells, Priv: priv})
	if t.autoFitWidths {
		t.widthsComputed = false
	}
	return len(t.Rows) >= t.Height
}

func (t *Table) computeWidths(width int) {
	contentMaxWidths := make([]int, len(t.Columns))
	if t.autoFitWidths {
		for _, row := range t.Rows {
			for c := range t.Columns {
				buf := StyledString(row.Cells[c])
				if buf.Len() > contentMaxWidths[c] {
					contentMaxWidths[c] = buf.Len()
				}
			}
		}
	}

	nonFixed := width
	autoWidthCount := 0
	for c := range t.Columns {
		col := &t.Columns[c]
		switch {
		case col.Def.Flags.Has(config.WIDTH_FIT):
			col.Width = contentMaxWidths[c]
			// compensate for exact width columns
			col.Width += runewidth.StringWidth(col.Separator)
		case col.Def.Flags.Has(config.WIDTH_EXACT):
			col.Width = int(math.Round(col.Def.Width))
			// compensate for exact width columns
			col.Width += runewidth.StringWidth(col.Separator)
		case col.Def.Flags.Has(config.WIDTH_AUTO):
			col.Width = 0
			autoWidthCount += 1
		case col.Def.Flags.Has(config.WIDTH_FRACTION):
			col.Width = int(math.Round(float64(width) * col.Def.Width))
		}
		nonFixed -= col.Width
	}

	autoWidth := 0
	if autoWidthCount > 0 && nonFixed > 0 {
		autoWidth = nonFixed / autoWidthCount
		if autoWidth == 0 {
			autoWidth = 1
		}
	}

	offset := 0
	remain := width
	for c := range t.Columns {
		col := &t.Columns[c]
		if col.Def.Flags.Has(config.WIDTH_AUTO) && autoWidth > 0 {
			col.Width = autoWidth
			if nonFixed >= 2*autoWidth {
				nonFixed -= autoWidth
			}
		}
		if remain == 0 {
			// column is outside of screen
			col.Width = -1
		} else if col.Width > remain {
			// limit width to avoid overflow
			col.Width = remain
		}
		remain -= col.Width
		col.Offset = offset
		offset += col.Width
		// reserve room for separator
		col.Width -= runewidth.StringWidth(col.Separator)
	}
}

var metaCharsRegexp = regexp.MustCompile(`[\t\r\f\n\v]`)

func (col *Column) alignCell(cell string) string {
	cell = metaCharsRegexp.ReplaceAllString(cell, " ")
	buf := StyledString(cell)
	width := buf.Len()

	switch {
	case col.Def.Flags.Has(config.ALIGN_LEFT):
		if width < col.Width {
			PadRight(buf, col.Width)
			cell = buf.Encode()
		} else if width > col.Width {
			Truncate(buf, col.Width)
			cell = buf.Encode()
		}
	case col.Def.Flags.Has(config.ALIGN_CENTER):
		if width < col.Width {
			pad := col.Width - width
			PadLeft(buf, col.Width-(pad/2))
			PadRight(buf, col.Width)
			cell = buf.Encode()
		} else if width > col.Width {
			Truncate(buf, col.Width)
			cell = buf.Encode()
		}
	case col.Def.Flags.Has(config.ALIGN_RIGHT):
		if width < col.Width {
			PadLeft(buf, col.Width)
			cell = buf.Encode()
		} else if width > col.Width {
			TruncateHead(buf, col.Width)
			cell = buf.Encode()
		}
	}

	return cell
}

func (t *Table) Draw(ctx *Context) {
	if !t.widthsComputed {
		t.computeWidths(ctx.Width())
		t.widthsComputed = true
	}
	for r, row := range t.Rows {
		if t.CustomDraw(t, r, ctx) {
			continue
		}
		for c, col := range t.Columns {
			if col.Width == -1 {
				// column overflows screen width
				continue
			}
			cell := col.alignCell(row.Cells[c])
			style := t.GetRowStyle(t, r)

			buf := StyledString(cell)
			ApplyAttrs(buf, style)
			cell = buf.Encode()
			ctx.Printf(col.Offset, r, style, "%s%s", cell, col.Separator)
		}
	}
}