github.com/elves/elvish@v0.15.0/pkg/cli/listbox.go (about) 1 package cli 2 3 import ( 4 "strings" 5 "sync" 6 7 "github.com/elves/elvish/pkg/cli/term" 8 "github.com/elves/elvish/pkg/ui" 9 ) 10 11 // ListBox is a list for displaying and selecting from a list of items. 12 type ListBox interface { 13 Widget 14 // CopyState returns a copy of the state. 15 CopyState() ListBoxState 16 // Reset resets the state of the widget with the given items and index of 17 // the selected item. It triggers the OnSelect callback if the index is 18 // valid. 19 Reset(it Items, selected int) 20 // Select changes the selection by calling f with the current state, and 21 // using the return value as the new selection index. It triggers the 22 // OnSelect callback if the selected index has changed and is valid. 23 Select(f func(ListBoxState) int) 24 // Accept accepts the currently selected item. 25 Accept() 26 } 27 28 // ListBoxSpec specifies the configuration and initial state for ListBox. 29 type ListBoxSpec struct { 30 // A Handler that takes precedence over the default handling of events. 31 OverlayHandler Handler 32 // A placeholder to show when there are no items. 33 Placeholder ui.Text 34 // A function to call when the selected item has changed. 35 OnSelect func(it Items, i int) 36 // A function called on the accept event. 37 OnAccept func(it Items, i int) 38 // Whether the listbox should be rendered in a horizontal Note that 39 // in the horizontal layout, items must have only one line. 40 Horizontal bool 41 // The minimal amount of space to reserve for left and right sides of each 42 // entry. 43 Padding int 44 // If true, the left padding of each item will be styled the same as the 45 // first segment of the item, and the right spacing and padding will be 46 // styled the same as the last segment of the item. 47 ExtendStyle bool 48 49 // State. When used in New, this field specifies the initial state. 50 State ListBoxState 51 } 52 53 type listBox struct { 54 // Mutex for synchronizing access to the state. 55 StateMutex sync.RWMutex 56 // Configuration and state. 57 ListBoxSpec 58 } 59 60 // NewListBox creates a new ListBox from the given spec. 61 func NewListBox(spec ListBoxSpec) ListBox { 62 if spec.OverlayHandler == nil { 63 spec.OverlayHandler = DummyHandler{} 64 } 65 if spec.OnAccept == nil { 66 spec.OnAccept = func(Items, int) {} 67 } 68 if spec.OnSelect == nil { 69 spec.OnSelect = func(Items, int) {} 70 } else { 71 s := spec.State 72 if s.Items != nil && 0 <= s.Selected && s.Selected < s.Items.Len() { 73 spec.OnSelect(s.Items, s.Selected) 74 } 75 } 76 return &listBox{ListBoxSpec: spec} 77 } 78 79 var stylingForSelected = ui.Inverse 80 81 func (w *listBox) Render(width, height int) *term.Buffer { 82 if w.Horizontal { 83 return w.renderHorizontal(width, height) 84 } 85 return w.renderVertical(width, height) 86 } 87 88 const listBoxColGap = 2 89 90 func (w *listBox) renderHorizontal(width, height int) *term.Buffer { 91 var state ListBoxState 92 w.mutate(func(s *ListBoxState) { 93 if s.Items == nil || s.Items.Len() == 0 { 94 s.First = 0 95 } else { 96 s.First, s.Height = getHorizontalWindow(*s, w.Padding, width, height) 97 // Override height to the height required; we don't need the 98 // original height later. 99 height = s.Height 100 } 101 state = *s 102 }) 103 104 if state.Items == nil || state.Items.Len() == 0 { 105 return Label{Content: w.Placeholder}.Render(width, height) 106 } 107 108 items, selected, first := state.Items, state.Selected, state.First 109 n := items.Len() 110 111 buf := term.NewBuffer(0) 112 remainedWidth := width 113 hasCropped := false 114 last := first 115 for i := first; i < n; i += height { 116 selectedRow := -1 117 // Render the column starting from i. 118 col := make([]ui.Text, 0, height) 119 for j := i; j < i+height && j < n; j++ { 120 last = j 121 item := items.Show(j) 122 if j == selected { 123 selectedRow = j - i 124 } 125 col = append(col, item) 126 } 127 128 colWidth := maxWidth(items, w.Padding, i, i+height) 129 if colWidth > remainedWidth { 130 colWidth = remainedWidth 131 hasCropped = true 132 } 133 134 colBuf := croppedLines{ 135 lines: col, padding: w.Padding, 136 selectFrom: selectedRow, selectTo: selectedRow + 1, 137 extendStyle: w.ExtendStyle}.Render(colWidth, height) 138 buf.ExtendRight(colBuf) 139 140 remainedWidth -= colWidth 141 if remainedWidth <= listBoxColGap { 142 break 143 } 144 remainedWidth -= listBoxColGap 145 buf.Width += listBoxColGap 146 } 147 // We may not have used all the width required; force buffer width. 148 buf.Width = width 149 if first != 0 || last != n-1 || hasCropped { 150 scrollbar := HScrollbar{Total: n, Low: first, High: last + 1} 151 buf.Extend(scrollbar.Render(width, 1), false) 152 } 153 return buf 154 } 155 156 func (w *listBox) renderVertical(width, height int) *term.Buffer { 157 var state ListBoxState 158 var firstCrop int 159 w.mutate(func(s *ListBoxState) { 160 if s.Items == nil || s.Items.Len() == 0 { 161 s.First = 0 162 } else { 163 s.First, firstCrop = getVerticalWindow(*s, height) 164 } 165 s.Height = height 166 state = *s 167 }) 168 169 if state.Items == nil || state.Items.Len() == 0 { 170 return Label{Content: w.Placeholder}.Render(width, height) 171 } 172 173 items, selected, first := state.Items, state.Selected, state.First 174 n := items.Len() 175 allLines := []ui.Text{} 176 hasCropped := firstCrop > 0 177 178 var i, selectFrom, selectTo int 179 for i = first; i < n && len(allLines) < height; i++ { 180 item := items.Show(i) 181 lines := item.SplitByRune('\n') 182 if i == first { 183 lines = lines[firstCrop:] 184 } 185 if i == selected { 186 selectFrom, selectTo = len(allLines), len(allLines)+len(lines) 187 } 188 // TODO: Optionally, add underlines to the last line as a visual 189 // separator between adjacent entries. 190 191 if len(allLines)+len(lines) > height { 192 lines = lines[:len(allLines)+len(lines)-height] 193 hasCropped = true 194 } 195 allLines = append(allLines, lines...) 196 } 197 198 var rd Renderer = croppedLines{ 199 lines: allLines, padding: w.Padding, 200 selectFrom: selectFrom, selectTo: selectTo, extendStyle: w.ExtendStyle} 201 if first > 0 || i < n || hasCropped { 202 rd = VScrollbarContainer{ 203 Content: rd, 204 Scrollbar: VScrollbar{Total: n, Low: first, High: i}, 205 } 206 } 207 return rd.Render(width, height) 208 } 209 210 type croppedLines struct { 211 lines []ui.Text 212 padding int 213 selectFrom int 214 selectTo int 215 extendStyle bool 216 } 217 218 func (c croppedLines) Render(width, height int) *term.Buffer { 219 bb := term.NewBufferBuilder(width) 220 leftSpacing := ui.T(strings.Repeat(" ", c.padding)) 221 rightSpacing := ui.T(strings.Repeat(" ", width-c.padding)) 222 for i, line := range c.lines { 223 if i > 0 { 224 bb.Newline() 225 } 226 227 selected := c.selectFrom <= i && i < c.selectTo 228 extendStyle := c.extendStyle && len(line) > 0 229 230 left := leftSpacing.Clone() 231 if extendStyle { 232 left[0].Style = line[0].Style 233 } 234 acc := ui.Concat(left, line.TrimWcwidth(width-2*c.padding)) 235 if extendStyle || selected { 236 right := rightSpacing.Clone() 237 if extendStyle { 238 right[0].Style = line[len(line)-1].Style 239 } 240 acc = ui.Concat(acc, right).TrimWcwidth(width) 241 } 242 if selected { 243 acc = ui.StyleText(acc, stylingForSelected) 244 } 245 246 bb.WriteStyled(acc) 247 } 248 return bb.Buffer() 249 } 250 251 func (w *listBox) Handle(event term.Event) bool { 252 if w.OverlayHandler.Handle(event) { 253 return true 254 } 255 256 switch event { 257 case term.K(ui.Up): 258 w.Select(Prev) 259 return true 260 case term.K(ui.Down): 261 w.Select(Next) 262 return true 263 case term.K(ui.Enter): 264 w.Accept() 265 return true 266 } 267 return false 268 } 269 270 func (w *listBox) CopyState() ListBoxState { 271 w.StateMutex.RLock() 272 defer w.StateMutex.RUnlock() 273 return w.State 274 } 275 276 func (w *listBox) Reset(it Items, selected int) { 277 w.mutate(func(s *ListBoxState) { *s = ListBoxState{Items: it, Selected: selected} }) 278 if 0 <= selected && selected < it.Len() { 279 w.OnSelect(it, selected) 280 } 281 } 282 283 func (w *listBox) Select(f func(ListBoxState) int) { 284 var it Items 285 var oldSelected, selected int 286 w.mutate(func(s *ListBoxState) { 287 oldSelected, it = s.Selected, s.Items 288 selected = f(*s) 289 s.Selected = selected 290 }) 291 if selected != oldSelected && 0 <= selected && selected < it.Len() { 292 w.OnSelect(it, selected) 293 } 294 } 295 296 // Prev moves the selection to the previous item, or does nothing if the 297 // first item is currently selected. It is a suitable as an argument to 298 // Widget.Select. 299 func Prev(s ListBoxState) int { 300 return fixIndex(s.Selected-1, s.Items.Len()) 301 } 302 303 // PrevPage moves the selection to the item one page before. It is only 304 // meaningful in vertical layout and suitable as an argument to Widget.Select. 305 // 306 // TODO(xiaq): This does not correctly with multi-line items. 307 func PrevPage(s ListBoxState) int { 308 return fixIndex(s.Selected-s.Height, s.Items.Len()) 309 } 310 311 // Next moves the selection to the previous item, or does nothing if the 312 // last item is currently selected. It is a suitable as an argument to 313 // Widget.Select. 314 func Next(s ListBoxState) int { 315 return fixIndex(s.Selected+1, s.Items.Len()) 316 } 317 318 // NextPage moves the selection to the item one page after. It is only 319 // meaningful in vertical layout and suitable as an argument to Widget.Select. 320 // 321 // TODO(xiaq): This does not correctly with multi-line items. 322 func NextPage(s ListBoxState) int { 323 return fixIndex(s.Selected+s.Height, s.Items.Len()) 324 } 325 326 // PrevWrap moves the selection to the previous item, or to the last item if 327 // the first item is currently selected. It is a suitable as an argument to 328 // Widget.Select. 329 func PrevWrap(s ListBoxState) int { 330 selected, n := s.Selected, s.Items.Len() 331 switch { 332 case selected >= n: 333 return n - 1 334 case selected <= 0: 335 return n - 1 336 default: 337 return selected - 1 338 } 339 } 340 341 // NextWrap moves the selection to the previous item, or to the first item 342 // if the last item is currently selected. It is a suitable as an argument to 343 // Widget.Select. 344 func NextWrap(s ListBoxState) int { 345 selected, n := s.Selected, s.Items.Len() 346 switch { 347 case selected >= n-1: 348 return 0 349 case selected < 0: 350 return 0 351 default: 352 return selected + 1 353 } 354 } 355 356 // Left moves the selection to the item to the left. It is only meaningful in 357 // horizontal layout and suitable as an argument to Widget.Select. 358 func Left(s ListBoxState) int { 359 return horizontal(s.Selected, s.Items.Len(), -s.Height) 360 } 361 362 // Right moves the selection to the item to the right. It is only meaningful in 363 // horizontal layout and suitable as an argument to Widget.Select. 364 func Right(s ListBoxState) int { 365 return horizontal(s.Selected, s.Items.Len(), s.Height) 366 } 367 368 func horizontal(selected, n, d int) int { 369 selected = fixIndex(selected, n) 370 newSelected := selected + d 371 if newSelected < 0 || newSelected >= n { 372 return selected 373 } 374 return newSelected 375 } 376 377 func fixIndex(i, n int) int { 378 switch { 379 case i < 0: 380 return 0 381 case i >= n: 382 return n - 1 383 default: 384 return i 385 } 386 } 387 388 func (w *listBox) Accept() { 389 state := w.CopyState() 390 if 0 <= state.Selected && state.Selected < state.Items.Len() { 391 w.OnAccept(state.Items, state.Selected) 392 } 393 } 394 395 func (w *listBox) mutate(f func(s *ListBoxState)) { 396 w.StateMutex.Lock() 397 defer w.StateMutex.Unlock() 398 f(&w.State) 399 }