src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/cli/tk/listbox_window.go (about) 1 package tk 2 3 import "src.elv.sh/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. Returns the first item to show, 99 // the height of each column, and whether a scrollbar may be shown. 100 func getHorizontalWindow(state ListBoxState, padding, width, height int) (int, int, bool) { 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, false 112 } 113 // At this point, assume that we'll have to use the entire available height 114 // and show a scrollbar, unless height is 1, in which case we'd rather use the 115 // one line to show some actual content and give up the scrollbar. 116 // 117 // This is rather pessimistic, but until an efficient 118 // algorithm that generates a more optimal layout emerges we'll use this 119 // simple one. 120 scrollbar := false 121 if height > 1 { 122 scrollbar = true 123 height-- 124 } 125 selected, lastFirst := state.Selected, state.First 126 // Start with the column containing the selected item, move left until 127 // either the width is exhausted, or lastFirst has been reached. 128 first := selected / height * height 129 usedWidth := maxWidth(items, padding, first, first+height) 130 for ; first > lastFirst; first -= height { 131 usedWidth += maxWidth(items, padding, first-height, first) + listBoxColGap 132 if usedWidth > width { 133 break 134 } 135 } 136 return first, height, scrollbar 137 } 138 139 func maxWidth(items Items, padding, low, high int) int { 140 n := items.Len() 141 width := 0 142 for i := low; i < high && i < n; i++ { 143 w := 0 144 for _, seg := range items.Show(i) { 145 w += wcwidth.Of(seg.Text) 146 } 147 if width < w { 148 width = w 149 } 150 } 151 return width + 2*padding 152 }