File: listbox_window.go

package info (click to toggle)
elvish 0.21.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 6,372 kB
  • sloc: javascript: 236; sh: 130; python: 104; makefile: 88; xml: 9
file content (152 lines) | stat: -rw-r--r-- 5,132 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package tk

import "src.elv.sh/pkg/wcwidth"

// The number of lines the listing mode keeps between the current selected item
// and the top and bottom edges of the window, unless the available height is
// too small or if the selected item is near the top or bottom of the list.
var respectDistance = 2

// Determines the index of the first item to show in vertical mode.
//
// This function does not return the full window, but just the first item to
// show, and how many initial lines to crop. The window determined by this
// algorithm has the following properties:
//
//   - It always includes the selected item.
//
//   - The combined height of all the entries in the window is equal to
//     min(height, combined height of all entries).
//
//   - There are at least respectDistance rows above the first row of the selected
//     item, as well as that many rows below the last row of the selected item,
//     unless the height is too small.
//
//   - Among all values satisfying the above conditions, the value of first is
//     the one closest to lastFirst.
func getVerticalWindow(state ListBoxState, height int) (first, crop int) {
	items, selected, lastFirst := state.Items, state.Selected, state.First
	n := items.Len()
	if selected < 0 {
		selected = 0
	} else if selected >= n {
		selected = n - 1
	}
	selectedHeight := items.Show(selected).CountLines()

	if height <= selectedHeight {
		// The height is not big enough (or just big enough) to fit the selected
		// item. Fit as much as the selected item as we can.
		return selected, 0
	}

	// Determine the minimum amount of space required for the downward direction.
	budget := height - selectedHeight
	var needDown int
	if budget >= 2*respectDistance {
		// If we can afford maintaining the respect distance on both sides, then
		// the minimum amount of space required is the respect distance.
		needDown = respectDistance
	} else {
		// Otherwise we split the available space by half. The downward (no pun
		// intended) rounding here is an arbitrary choice.
		needDown = budget / 2
	}
	// Calculate how much of the budget the downward direction can use. This is
	// used to 1) potentially shrink needDown 2) decide how much to expand
	// upward later.
	useDown := 0
	for i := selected + 1; i < n; i++ {
		useDown += items.Show(i).CountLines()
		if useDown >= budget {
			break
		}
	}
	if needDown > useDown {
		// We reached the last item without using all of needDown. That means we
		// don't need so much in the downward direction.
		needDown = useDown
	}

	// The maximum amount of space we can use in the upward direction is the
	// entire budget minus the minimum amount of space we need in the downward
	// direction.
	budgetUp := budget - needDown

	useUp := 0
	// Extend upwards until any of the following becomes true:
	//
	// * We have exhausted budgetUp;
	//
	// * We have reached item 0;
	//
	// * We have reached or passed lastFirst, satisfied the upward respect
	//   distance, and will be able to use up the entire budget when expanding
	//   downwards later.
	for i := selected - 1; i >= 0; i-- {
		useUp += items.Show(i).CountLines()
		if useUp >= budgetUp {
			return i, useUp - budgetUp
		}
		if i <= lastFirst && useUp >= respectDistance && useUp+useDown >= budget {
			return i, 0
		}
	}
	return 0, 0
}

// Determines the window to show in horizontal. Returns the first item to show,
// the height of each column, and whether a scrollbar may be shown.
func getHorizontalWindow(state ListBoxState, padding, width, height int) (int, int, bool) {
	items := state.Items
	n := items.Len()
	// Lower bound of number of items that can fit in a row.
	perRow := (width + listBoxColGap) / (maxWidth(items, padding, 0, n) + listBoxColGap)
	if perRow == 0 {
		// We trim items that are too wide, so there is at least one item per row.
		perRow = 1
	}
	if height*perRow >= n {
		// All items can fit.
		return 0, (n + perRow - 1) / perRow, false
	}
	// At this point, assume that we'll have to use the entire available height
	// and show a scrollbar, unless height is 1, in which case we'd rather use the
	// one line to show some actual content and give up the scrollbar.
	//
	// This is rather pessimistic, but until an efficient
	// algorithm that generates a more optimal layout emerges we'll use this
	// simple one.
	scrollbar := false
	if height > 1 {
		scrollbar = true
		height--
	}
	selected, lastFirst := state.Selected, state.First
	// Start with the column containing the selected item, move left until
	// either the width is exhausted, or lastFirst has been reached.
	first := selected / height * height
	usedWidth := maxWidth(items, padding, first, first+height)
	for ; first > lastFirst; first -= height {
		usedWidth += maxWidth(items, padding, first-height, first) + listBoxColGap
		if usedWidth > width {
			break
		}
	}
	return first, height, scrollbar
}

func maxWidth(items Items, padding, low, high int) int {
	n := items.Len()
	width := 0
	for i := low; i < high && i < n; i++ {
		w := 0
		for _, seg := range items.Show(i) {
			w += wcwidth.Of(seg.Text)
		}
		if width < w {
			width = w
		}
	}
	return width + 2*padding
}