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  }