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 }