summaryrefslogtreecommitdiff
path: root/src/listwidget.cpp
blob: 4f12e6bb0403244dff729de87671be2a851f7390 (plain) (blame)
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#include "listwidget.h"

#include <algorithm>

#include "utils.h"

namespace newsboat {

ListWidget::ListWidget(const std::string& list_name, Stfl::Form& form,
	std::uint32_t scrolloff)
	: list_name(list_name)
	, form(form)
	, num_lines(0)
	, num_context_lines(scrolloff)
{
}

void ListWidget::stfl_replace_list(std::uint32_t number_of_lines,
	std::string stfl)
{
	num_lines = number_of_lines;
	form.modify(list_name, "replace", stfl);
}

void ListWidget::stfl_replace_lines(const ListFormatter& listfmt)
{
	num_lines = listfmt.get_lines_count();
	form.modify(list_name, "replace_inner", listfmt.format_list());
}

bool ListWidget::move_up(bool wrap_scroll)
{
	const std::uint32_t curpos = get_position();
	if (curpos > 0) {
		set_position(curpos - 1);
		return true;
	} else if (wrap_scroll) {
		move_to_last();
		return true;
	}
	return false;
}

bool ListWidget::move_down(bool wrap_scroll)
{
	if (num_lines == 0) {
		// Ignore if list is empty
		return false;
	}
	const std::uint32_t maxpos = num_lines - 1;
	const std::uint32_t curpos = get_position();
	if (curpos + 1 <= maxpos) {
		set_position(curpos + 1);
		return true;
	} else if (wrap_scroll) {
		move_to_first();
		return true;
	}
	return false;
}

void ListWidget::move_to_first()
{
	set_position(0);
}

void ListWidget::move_to_last()
{
	if (num_lines == 0) {
		// Ignore if list is empty
		return;
	}
	const std::uint32_t maxpos = num_lines - 1;
	set_position(maxpos);
}

void ListWidget::move_page_up(bool wrap_scroll)
{
	const std::uint32_t curpos = get_position();
	const std::uint32_t list_height = get_height();
	if (curpos > list_height) {
		set_position(curpos - list_height);
	} else if (wrap_scroll && curpos == 0) {
		move_to_last();
	} else {
		set_position(0);
	}
}

void ListWidget::move_page_down(bool wrap_scroll)
{
	if (num_lines == 0) {
		// Ignore if list is empty
		return;
	}
	const std::uint32_t maxpos = num_lines - 1;
	const std::uint32_t curpos = get_position();
	const std::uint32_t list_height = get_height();
	if (curpos + list_height < maxpos) {
		set_position(curpos + list_height);
	} else if (wrap_scroll && curpos == maxpos) {
		move_to_first();
	} else {
		set_position(maxpos);
	}
}

std::uint32_t ListWidget::get_position()
{
	const std::string pos = form.get(list_name + "_pos");
	if (!pos.empty()) {
		return std::max(0, std::stoi(pos));
	}
	return 0;
}

void ListWidget::set_position(std::uint32_t pos)
{
	form.set(list_name + "_pos", std::to_string(pos));
	update_scroll_offset(pos);
}

std::uint32_t ListWidget::get_width()
{
	return utils::to_u(form.get(list_name + ":w"));
}

std::uint32_t ListWidget::get_height()
{
	return utils::to_u(form.get(list_name + ":h"));
}

std::uint32_t ListWidget::get_scroll_offset()
{
	const std::string offset = form.get(list_name + "_offset");
	if (!offset.empty()) {
		return std::max(0, std::stoi(offset));
	}
	return 0;
}

void ListWidget::set_scroll_offset(std::uint32_t offset)
{
	form.set(list_name + "_offset", std::to_string(offset));
}

void ListWidget::update_scroll_offset(std::uint32_t pos)
{
	// In STFL, "offset" is how many items at the beginning of the list are
	// hidden off-screen. That's how scrolling is implemented: to scroll down,
	// you increase "offset", hiding items at the top and showing more at the
	// bottom. By manipulating "offset" here, we can keep the cursor within the
	// bounds we set.
	//
	// All the lines that are visible because of "scrolloff" setting are called
	// "context" here. They include the current line under cursor (which has
	// position "pos"), "cur_scroll_offset" lines above it, and
	// "cur_scroll_offset" lines below it.

	const auto h = get_height();
	const auto cur_scroll_offset = get_scroll_offset();
	// An offset at which the last item of the list is at the bottom of the
	// widget. We shouldn't set "offset" to more than this value, otherwise
	// we'll have an empty "gap" at the bottom of the list. That's only
	// acceptable if the list is shorter than the widget's height.
	const std::uint32_t max_offset = (num_lines >= h ? num_lines - h : 0);

	if (2 * num_context_lines < h) {
		// Check if items at the bottom of the "context" are visible. If not,
		// we'll have to scroll down.
		if (pos + num_context_lines >= cur_scroll_offset + h) {
			if (pos + num_context_lines >= h) {
				const std::uint32_t target_offset = pos + num_context_lines - h + 1;
				set_scroll_offset(std::min(target_offset, max_offset));
			} else { // "pos" is towards the beginning of the list; don't scroll
				set_scroll_offset(0);
			}
		}

		// Check if items at the top of the "context" are visible. If not,
		// we'll have to scroll up.
		if (pos < cur_scroll_offset + num_context_lines) {
			if (pos >= num_context_lines) {
				set_scroll_offset(pos - num_context_lines);
			} else { // "pos" is towards the beginning of the list; don't scroll
				set_scroll_offset(0);
			}
		}
	} else { // Keep selected item in the middle
		if (pos > h / 2) {
			const std::uint32_t target_offset = pos - h / 2;
			set_scroll_offset(std::min(target_offset, max_offset));
		} else { // "pos" is towards the beginning of the list; don't scroll
			set_scroll_offset(0);
		}
	}
}

} // namespace newsboat