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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
use crate::style::Style;
use crate::terminal::{Rectangle, Terminal, TerminalConst, UpdateInfo, UpdateResult};
use crate::widgets::{BoundingBox, Widget};
use crate::Error;
/// A widget that displays a list of buttons, left-to-right.
#[derive(Eq, PartialEq, Copy, Clone, Hash, Debug, Default)]
pub struct Buttons<'a, T> {
/// The buttons to display.
pub buttons: &'a [T],
/// The style to use for the selected button.
pub selected_button_style: Style,
/// The style to use for the unselected buttons.
pub unselected_button_style: Style,
/// The index of the currently hovered button.
pub hovered_button: Option<usize>,
}
impl<'a, T: AsRef<str>> Buttons<'a, T> {
/// Create a new [`Buttons`] widget.
#[must_use]
pub const fn new(buttons: &'a [T]) -> Self {
Self {
buttons,
selected_button_style: Style::new(),
unselected_button_style: Style::new(),
hovered_button: None,
}
}
#[must_use]
/// Returns the currently selected button. Will be `None` if no button is selected.
pub const fn selected(&self) -> Option<usize> {
self.hovered_button
}
/// Select a button based on its order from left-to-right.
#[must_use]
pub const fn select(mut self, selection: usize) -> Self {
if selection < self.buttons.len() {
self.hovered_button = Some(selection);
}
self
}
/// Deselects button.
#[must_use]
pub const fn select_none(mut self) -> Self {
self.hovered_button = None;
self
}
/// Selects the rightmost available button, or `None` if there are no buttons.
#[must_use]
pub const fn select_rightmost(self) -> Self {
let Some(len) = self.buttons.len().checked_sub(1) else {
return self.select_none();
};
self.select(len)
}
/// Selects the rightmost available button, or `None` if there are no buttons.
///
/// This is an alias for [`Buttons::select_rightmost`]. Right is not last in all languages.
#[must_use]
pub const fn select_last(self) -> Self {
self.select_rightmost()
}
/// Selects the leftmost button, or `None` if there are no buttons.
#[must_use]
pub const fn select_leftmost(self) -> Self {
self.select(0)
}
/// Selects the leftmost button, or `None` if there are no buttons.
///
/// This is an alias for [`Buttons::select_leftmost`]. Left is not first in all languages.
#[must_use]
pub const fn select_first(self) -> Self {
self.select_leftmost()
}
/// Selects the button to the right of the cursor, or `None` if there are no buttons.
///
/// If the cursor position is unset, it will select the rightmost item.
#[must_use]
pub const fn move_right(self) -> Self {
let Some(mut selected) = self.hovered_button else {
return self.select_rightmost();
};
selected += 1;
self.select(selected)
}
/// Selects the button to the right of the cursor, or `None` if there are no buttons.
///
/// If the cursor position is unset, it will select the leftmost item.
#[must_use]
pub const fn move_left(self) -> Self {
let Some(selected) = self.hovered_button else {
return self.select_leftmost();
};
let Some(selected) = selected.checked_sub(1) else {
return self.select_leftmost();
};
self.select(selected)
}
}
impl<T: AsRef<str>> Widget for Buttons<'_, T> {
fn update(
&mut self,
_update_info: UpdateInfo,
_terminal: impl TerminalConst,
) -> crate::Result<UpdateResult> {
Err(Error::Todo)
}
fn draw(
&self,
_update_info: UpdateInfo,
mut terminal: impl Terminal,
) -> crate::Result<UpdateResult> {
let term_bounding_box = terminal.bounding_box();
let mut terminal_cells = terminal.cells_mut().enumerate().peekable();
for (button_idx, button) in self.buttons.iter().enumerate() {
let selected = Some(button_idx) == self.hovered_button;
let base_style = if selected {
self.selected_button_style
} else {
self.unselected_button_style
};
let max_len = button.as_ref().len().min(term_bounding_box.width());
let (next_idx, _next_cell) = terminal_cells
.peek()
.ok_or(Error::OutOfBoundsCoordinate { x: None, y: None })?;
let (cursor_x, cursor_y) = term_bounding_box
.index_into(*next_idx)
.ok_or(Error::OutOfBoundsIndex(*next_idx))?;
let button_chars = button.as_ref()[..max_len].chars();
if button.as_ref().len() + cursor_x > term_bounding_box.width() {
// Skips until next line, only if the button fits on one line.
if button.as_ref().len() <= term_bounding_box.width() {
while let Some((idx, _cell)) = terminal_cells.peek() {
let (_x, y) = term_bounding_box
.index_into(*idx)
.ok_or(Error::OutOfBoundsIndex(*idx))?;
if y != cursor_y {
break;
}
terminal_cells.next();
}
}
// If it does not fit on one line, then we need to truncate the button.
// We already did this above, so we don't need to do it again.
}
for current_character in button_chars {
let (_idx, current_cell) = terminal_cells
.next()
.ok_or(Error::OutOfBoundsCoordinate { x: None, y: None })?;
current_cell.character = current_character;
current_cell.style = base_style.inherits(current_cell.style);
}
}
Ok(UpdateResult::NoEvent)
}
}
impl<T: AsRef<str>> BoundingBox for Buttons<'_, T> {
fn bounding_box(&self, rect: Rectangle) -> crate::Result<Rectangle> {
let term_bounding_box = rect;
let (mut width, mut height) = (0, 0);
let mut idx = 0;
height += 1; // Account for the first line.
// FIXME: Optimize this so that it doesn't have to do a big ugly for loop.
// We literally just copy the code from the draw method, but change it so that it logs
// the furthest out x and y coordinates. This is probably not the most efficient way to do
// this, but it's the only way I can think of right now.
for button in self.buttons {
let max_len = button.as_ref().len().min(term_bounding_box.width());
let next_idx = idx + 1;
let (cursor_x, cursor_y) = rect
.index_into(next_idx)
.ok_or(Error::OutOfBoundsIndex(next_idx))?;
let button_chars = button.as_ref()[..max_len].chars().enumerate();
if button.as_ref().len() + cursor_x > rect.width() {
// Skips until next line, only if the button fits on one line.
if button.as_ref().len() <= rect.width() {
while let Some((_x, y)) = rect.index_into(idx) {
if y != cursor_y {
break;
}
idx += 1;
}
}
// If it does not fit on one line, then we need to truncate the button.
// We already did this above, so we don't need to do it again.
}
for _current_character in button_chars {
idx += 1;
let (x, y) = rect
.index_into(idx)
.ok_or(Error::OutOfBoundsIndex(idx))?;
width = width.max(x + 1);
height = height.max(y);
}
let (x, y) = rect
.index_into(idx)
.ok_or(Error::OutOfBoundsIndex(idx))?;
width = width.max(x);
height = height.max(y);
}
Ok(Rectangle::of_size((width, height)))
}
fn completely_covers(&self, rectangle: Rectangle) -> bool {
let term_bounding_box = rectangle;
let mut idx = 0;
// FIXME: Optimize this so that it doesn't have to do a big ugly for loop.
// We literally just copy the code from the draw method, but change it so that diverges
// early if any Cell has been skipped.
for button in self.buttons {
let max_len = button.as_ref().len().min(term_bounding_box.width());
let mut button_chars = button.as_ref()[..max_len].chars().enumerate().peekable();
while let Some((chr_dep, _character)) = button_chars.peek() {
let chr_dep = *chr_dep;
idx += 1;
let Some((x, _y)) = term_bounding_box.index_into(idx) else {
return true;
};
if button.as_ref().len() + x - chr_dep >= term_bounding_box.width() {
if button.as_ref().len() < term_bounding_box.width() {
return false;
}
}
button_chars.next().expect("This should always be Some");
}
}
true
}
}