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 }