github.com/elves/elvish@v0.15.0/pkg/cli/listbox_window.go (about) 1 package cli 2 3 import "github.com/elves/elvish/pkg/wcwidth" 4 5 // The number of lines the listing mode keeps between the current selected item 6 // and the top and bottom edges of the window, unless the available height is 7 // too small or if the selected item is near the top or bottom of the list. 8 var respectDistance = 2 9 10 // Determines the index of the first item to show in vertical mode. 11 // 12 // This function does not return the full window, but just the first item to 13 // show, and how many initial lines to crop. The window determined by this 14 // algorithm has the following properties: 15 // 16 // * It always includes the selected item. 17 // 18 // * The combined height of all the entries in the window is equal to 19 // min(height, combined height of all entries). 20 // 21 // * There are at least respectDistance rows above the first row of the selected 22 // item, as well as that many rows below the last row of the selected item, 23 // unless the height is too small. 24 // 25 // * Among all values satisfying the above conditions, the value of first is 26 // the one closest to lastFirst. 27 func getVerticalWindow(state ListBoxState, height int) (first, crop int) { 28 items, selected, lastFirst := state.Items, state.Selected, state.First 29 n := items.Len() 30 if selected < 0 { 31 selected = 0 32 } else if selected >= n { 33 selected = n - 1 34 } 35 selectedHeight := items.Show(selected).CountLines() 36 37 if height <= selectedHeight { 38 // The height is not big enough (or just big enough) to fit the selected 39 // item. Fit as much as the selected item as we can. 40 return selected, 0 41 } 42 43 // Determine the minimum amount of space required for the downward direction. 44 budget := height - selectedHeight 45 var needDown int 46 if budget >= 2*respectDistance { 47 // If we can afford maintaining the respect distance on both sides, then 48 // the minimum amount of space required is the respect distance. 49 needDown = respectDistance 50 } else { 51 // Otherwise we split the available space by half. The downward (no pun 52 // intended) rounding here is an arbitrary choice. 53 needDown = budget / 2 54 } 55 // Calculate how much of the budget the downward direction can use. This is 56 // used to 1) potentially shrink needDown 2) decide how much to expand 57 // upward later. 58 useDown := 0 59 for i := selected + 1; i < n; i++ { 60 useDown += items.Show(i).CountLines() 61 if useDown >= budget { 62 break 63 } 64 } 65 if needDown > useDown { 66 // We reached the last item without using all of needDown. That means we 67 // don't need so much in the downward direction. 68 needDown = useDown 69 } 70 71 // The maximum amount of space we can use in the upward direction is the 72 // entire budget minus the minimum amount of space we need in the downward 73 // direction. 74 budgetUp := budget - needDown 75 76 useUp := 0 77 // Extend upwards until any of the following becomes true: 78 // 79 // * We have exhausted budgetUp; 80 // 81 // * We have reached item 0; 82 // 83 // * We have reached or passed lastFirst, satisfied the upward respect 84 // distance, and will be able to use up the entire budget when expanding 85 // downwards later. 86 for i := selected - 1; i >= 0; i-- { 87 useUp += items.Show(i).CountLines() 88 if useUp >= budgetUp { 89 return i, useUp - budgetUp 90 } 91 if i <= lastFirst && useUp >= respectDistance && useUp+useDown >= budget { 92 return i, 0 93 } 94 } 95 return 0, 0 96 } 97 98 // Determines the window to show in horizontal It returns the first item 99 // to show and the amount of height required. 100 func getHorizontalWindow(state ListBoxState, padding, width, height int) (int, int) { 101 items := state.Items 102 n := items.Len() 103 // Lower bound of number of items that can fit in a row. 104 perRow := (width + listBoxColGap) / (maxWidth(items, padding, 0, n) + listBoxColGap) 105 if perRow == 0 { 106 // We trim items that are too wide, so there is at least one item per row. 107 perRow = 1 108 } 109 if height*perRow >= n { 110 // All items can fit. 111 return 0, (n + perRow - 1) / perRow 112 } 113 // Reduce the amount of available height by one because the last row will be 114 // reserved for the scrollbar. 115 height-- 116 selected, lastFirst := state.Selected, state.First 117 // Start with the column containing the selected item, move left until 118 // either the width is exhausted, or lastFirst has been reached. 119 first := selected / height * height 120 usedWidth := maxWidth(items, padding, first, first+height) 121 for ; first > lastFirst; first -= height { 122 usedWidth += maxWidth(items, padding, first-height, first) + listBoxColGap 123 if usedWidth > width { 124 break 125 } 126 } 127 return first, height 128 } 129 130 func maxWidth(items Items, padding, low, high int) int { 131 n := items.Len() 132 width := 0 133 for i := low; i < high && i < n; i++ { 134 w := 0 135 for _, seg := range items.Show(i) { 136 w += wcwidth.Of(seg.Text) 137 } 138 if width < w { 139 width = w 140 } 141 } 142 return width + 2*padding 143 }