src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/tk/colview.go (about)

     1  package tk
     2  
     3  import (
     4  	"sync"
     5  
     6  	"src.elv.sh/pkg/cli/term"
     7  	"src.elv.sh/pkg/ui"
     8  )
     9  
    10  // ColView is a Widget that arranges several widgets in a column.
    11  type ColView interface {
    12  	Widget
    13  	// MutateState mutates the state.
    14  	MutateState(f func(*ColViewState))
    15  	// CopyState returns a copy of the state.
    16  	CopyState() ColViewState
    17  	// Left triggers the OnLeft callback.
    18  	Left()
    19  	// Right triggers the OnRight callback.
    20  	Right()
    21  }
    22  
    23  // ColViewSpec specifies the configuration and initial state for ColView.
    24  type ColViewSpec struct {
    25  	// Key bindings.
    26  	Bindings Bindings
    27  	// A function that takes the number of columns and return weights for the
    28  	// widths of the columns. The returned slice must have a size of n. If this
    29  	// function is nil, all the columns will have the same weight.
    30  	Weights func(n int) []int
    31  	// A function called when the Left method of Widget is called, or when Left
    32  	// is pressed and unhandled.
    33  	OnLeft func(w ColView)
    34  	// A function called when the Right method of Widget is called, or when
    35  	// Right is pressed and unhandled.
    36  	OnRight func(w ColView)
    37  
    38  	// State. Specifies the initial state when used in New.
    39  	State ColViewState
    40  }
    41  
    42  // ColViewState keeps the mutable state of the ColView widget.
    43  type ColViewState struct {
    44  	Columns     []Widget
    45  	FocusColumn int
    46  }
    47  
    48  type colView struct {
    49  	// Mutex for synchronizing access to State.
    50  	StateMutex sync.RWMutex
    51  	ColViewSpec
    52  }
    53  
    54  // NewColView creates a new ColView from the given spec.
    55  func NewColView(spec ColViewSpec) ColView {
    56  	if spec.Bindings == nil {
    57  		spec.Bindings = DummyBindings{}
    58  	}
    59  	if spec.Weights == nil {
    60  		spec.Weights = equalWeights
    61  	}
    62  	if spec.OnLeft == nil {
    63  		spec.OnLeft = func(ColView) {}
    64  	}
    65  	if spec.OnRight == nil {
    66  		spec.OnRight = func(ColView) {}
    67  	}
    68  	return &colView{ColViewSpec: spec}
    69  }
    70  
    71  func equalWeights(n int) []int {
    72  	weights := make([]int, n)
    73  	for i := 0; i < n; i++ {
    74  		weights[i] = 1
    75  	}
    76  	return weights
    77  }
    78  
    79  func (w *colView) MutateState(f func(*ColViewState)) {
    80  	w.StateMutex.Lock()
    81  	defer w.StateMutex.Unlock()
    82  	f(&w.State)
    83  }
    84  
    85  func (w *colView) CopyState() ColViewState {
    86  	w.StateMutex.RLock()
    87  	defer w.StateMutex.RUnlock()
    88  	copied := w.State
    89  	copied.Columns = append([]Widget(nil), w.State.Columns...)
    90  	return copied
    91  }
    92  
    93  const colViewColGap = 1
    94  
    95  // Render renders all the columns side by side, putting the dot in the focused
    96  // column.
    97  func (w *colView) Render(width, height int) *term.Buffer {
    98  	cols, widths := w.prepareRender(width)
    99  	if len(cols) == 0 {
   100  		return &term.Buffer{Width: width}
   101  	}
   102  	var buf term.Buffer
   103  	for i, col := range cols {
   104  		if i > 0 {
   105  			buf.Width += colViewColGap
   106  		}
   107  		bufCol := col.Render(widths[i], height)
   108  		buf.ExtendRight(bufCol)
   109  	}
   110  	return &buf
   111  }
   112  
   113  func (w *colView) MaxHeight(width, height int) int {
   114  	cols, widths := w.prepareRender(width)
   115  	max := 0
   116  	for i, col := range cols {
   117  		colMax := col.MaxHeight(widths[i], height)
   118  		if max < colMax {
   119  			max = colMax
   120  		}
   121  	}
   122  	return max
   123  }
   124  
   125  // Returns widgets in and widths of columns.
   126  func (w *colView) prepareRender(width int) ([]Widget, []int) {
   127  	state := w.CopyState()
   128  	ncols := len(state.Columns)
   129  	if ncols == 0 {
   130  		// No column.
   131  		return nil, nil
   132  	}
   133  	if width < ncols {
   134  		// To narrow; give up by rendering nothing.
   135  		return nil, nil
   136  	}
   137  	widths := distribute(width-(ncols-1)*colViewColGap, w.Weights(ncols))
   138  	return state.Columns, widths
   139  }
   140  
   141  // Handle handles the event first by consulting the overlay handler, and then
   142  // delegating the event to the currently focused column.
   143  func (w *colView) Handle(event term.Event) bool {
   144  	if w.Bindings.Handle(w, event) {
   145  		return true
   146  	}
   147  	state := w.CopyState()
   148  	if 0 <= state.FocusColumn && state.FocusColumn < len(state.Columns) {
   149  		if state.Columns[state.FocusColumn].Handle(event) {
   150  			return true
   151  		}
   152  	}
   153  
   154  	switch event {
   155  	case term.K(ui.Left):
   156  		w.Left()
   157  		return true
   158  	case term.K(ui.Right):
   159  		w.Right()
   160  		return true
   161  	default:
   162  		return false
   163  	}
   164  }
   165  
   166  func (w *colView) Left() {
   167  	w.OnLeft(w)
   168  }
   169  
   170  func (w *colView) Right() {
   171  	w.OnRight(w)
   172  }
   173  
   174  // Distributes fullWidth according to the weights, rounding to integers.
   175  //
   176  // This works iteratively each step by taking the sum of all remaining weights,
   177  // and using floor(remainedWidth * currentWeight / remainedAllWeights) for the
   178  // current column.
   179  //
   180  // A simpler alternative is to simply use floor(fullWidth * currentWeight /
   181  // allWeights) at each step, and also giving the remainder to the last column.
   182  // However, this means that the last column gets all the rounding errors from
   183  // flooring, which can be big. The more sophisticated algorithm distributes the
   184  // rounding errors among all the remaining elements and can result in a much
   185  // better distribution, and as a special upside, does not need to handle the
   186  // last column as a special case.
   187  //
   188  // As an extreme example, consider the case of fullWidth = 9, weights = {1, 1,
   189  // 1, 1, 1} (five 1's). Using the simplistic algorithm, the widths are {1, 1, 1,
   190  // 1, 5}. Using the more complex algorithm, the widths are {1, 2, 2, 2, 2}.
   191  func distribute(fullWidth int, weights []int) []int {
   192  	remainedWidth := fullWidth
   193  	remainedWeight := 0
   194  	for _, weight := range weights {
   195  		remainedWeight += weight
   196  	}
   197  
   198  	widths := make([]int, len(weights))
   199  	for i, weight := range weights {
   200  		widths[i] = remainedWidth * weight / remainedWeight
   201  		remainedWidth -= widths[i]
   202  		remainedWeight -= weight
   203  	}
   204  	return widths
   205  }