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  }