github.com/elves/elvish@v0.15.0/pkg/cli/colview.go (about)

     1  package cli
     2  
     3  import (
     4  	"sync"
     5  
     6  	"github.com/elves/elvish/pkg/cli/term"
     7  	"github.com/elves/elvish/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  	// An overlay handler.
    26  	OverlayHandler Handler
    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.OverlayHandler == nil {
    57  		spec.OverlayHandler = DummyHandler{}
    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  	state := w.CopyState()
    99  	ncols := len(state.Columns)
   100  	if ncols == 0 {
   101  		// No column.
   102  		return &term.Buffer{Width: width}
   103  	}
   104  	if width < ncols {
   105  		// To narrow; give up by rendering nothing.
   106  		return &term.Buffer{Width: width}
   107  	}
   108  	colWidths := distribute(width-(ncols-1)*colViewColGap, w.Weights(ncols))
   109  	var buf term.Buffer
   110  	for i, col := range state.Columns {
   111  		if i > 0 {
   112  			buf.Width += colViewColGap
   113  		}
   114  		bufCol := col.Render(colWidths[i], height)
   115  		buf.ExtendRight(bufCol)
   116  	}
   117  	return &buf
   118  }
   119  
   120  // Handle handles the event first by consulting the overlay handler, and then
   121  // delegating the event to the currently focused column.
   122  func (w *colView) Handle(event term.Event) bool {
   123  	if w.OverlayHandler.Handle(event) {
   124  		return true
   125  	}
   126  	state := w.CopyState()
   127  	if 0 <= state.FocusColumn && state.FocusColumn < len(state.Columns) {
   128  		if state.Columns[state.FocusColumn].Handle(event) {
   129  			return true
   130  		}
   131  	}
   132  
   133  	switch event {
   134  	case term.K(ui.Left):
   135  		w.Left()
   136  		return true
   137  	case term.K(ui.Right):
   138  		w.Right()
   139  		return true
   140  	default:
   141  		return false
   142  	}
   143  }
   144  
   145  func (w *colView) Left() {
   146  	w.OnLeft(w)
   147  }
   148  
   149  func (w *colView) Right() {
   150  	w.OnRight(w)
   151  }
   152  
   153  // Distributes fullWidth according to the weights, rounding to integers.
   154  //
   155  // This works iteratively each step by taking the sum of all remaining weights,
   156  // and using floor(remainedWidth * currentWeight / remainedAllWeights) for the
   157  // current column.
   158  //
   159  // A simpler alternative is to simply use floor(fullWidth * currentWeight /
   160  // allWeights) at each step, and also giving the remainder to the last column.
   161  // However, this means that the last column gets all the rounding errors from
   162  // flooring, which can be big. The more sophisticated algorithm distributes the
   163  // rounding errors among all the remaining elements and can result in a much
   164  // better distribution, and as a special upside, does not need to handle the
   165  // last column as a special case.
   166  //
   167  // As an extreme example, consider the case of fullWidth = 9, weights = {1, 1,
   168  // 1, 1, 1} (five 1's). Using the simplistic algorithm, the widths are {1, 1, 1,
   169  // 1, 5}. Using the more complex algorithm, the widths are {1, 2, 2, 2, 2}.
   170  func distribute(fullWidth int, weights []int) []int {
   171  	remainedWidth := fullWidth
   172  	remainedWeight := 0
   173  	for _, weight := range weights {
   174  		remainedWeight += weight
   175  	}
   176  
   177  	widths := make([]int, len(weights))
   178  	for i, weight := range weights {
   179  		widths[i] = remainedWidth * weight / remainedWeight
   180  		remainedWidth -= widths[i]
   181  		remainedWeight -= weight
   182  	}
   183  	return widths
   184  }