1use serde::{Deserialize, Serialize};
4
5use crate::extras::icon::Icon;
6use crate::node::{ActionId, Common, IntoNode, Key, Node};
7use crate::style::Style;
8use crate::text::{Line, Text};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub enum ListDirection {
13 #[default]
15 TopToBottom,
16 BottomToTop,
18}
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub enum HighlightSpacing {
23 #[default]
25 Always,
26 WhenSelected,
28 Never,
30}
31
32#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct ListState {
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub selected: Option<Selection>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub offset: Option<u32>,
42}
43
44impl ListState {
45 #[must_use]
47 pub fn with_selected(key: impl Into<Key>) -> Self {
48 Self {
49 selected: Some(Selection::One(key.into())),
50 offset: None,
51 }
52 }
53
54 #[must_use]
56 pub fn with_selected_many(keys: Vec<Key>) -> Self {
57 Self {
58 selected: Some(Selection::Many(keys)),
59 offset: None,
60 }
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
66#[serde(untagged)]
67pub enum Selection {
68 One(Key),
70 Many(Vec<Key>),
72}
73
74#[allow(
77 clippy::large_enum_variant,
78 reason = "list items are built once per list; boxing the content adds noise"
79)]
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81#[serde(untagged)]
82pub enum ListItem {
83 Text(Text),
85 Full {
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 key: Option<Key>,
90 content: Text,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 style: Option<Style>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 detail: Option<Line>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 badge: Option<Line>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 icon: Option<Icon>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 disabled: Option<bool>,
107 },
108}
109
110impl ListItem {
111 #[must_use]
113 pub fn new(content: impl Into<Text>) -> Self {
114 Self::Full {
115 key: None,
116 content: content.into(),
117 style: None,
118 detail: None,
119 badge: None,
120 icon: None,
121 disabled: None,
122 }
123 }
124
125 #[must_use]
127 pub fn key(mut self, key: impl AsRef<str>) -> Self {
128 if let Self::Full { key: slot, .. } = &mut self {
129 *slot = Some(key.as_ref().to_owned());
130 }
131 self
132 }
133
134 #[must_use]
136 pub fn detail(mut self, detail: impl Into<Line>) -> Self {
137 if let Self::Full { detail: slot, .. } = &mut self {
138 *slot = Some(detail.into());
139 }
140 self
141 }
142
143 #[must_use]
145 pub fn badge(mut self, badge: impl Into<Line>) -> Self {
146 if let Self::Full { badge: slot, .. } = &mut self {
147 *slot = Some(badge.into());
148 }
149 self
150 }
151
152 #[must_use]
154 pub fn icon(mut self, icon: Icon) -> Self {
155 if let Self::Full { icon: slot, .. } = &mut self {
156 *slot = Some(icon);
157 }
158 self
159 }
160
161 #[must_use]
163 pub fn disabled(mut self, disabled: bool) -> Self {
164 if let Self::Full { disabled: slot, .. } = &mut self {
165 *slot = Some(disabled);
166 }
167 self
168 }
169
170 #[must_use]
172 pub fn style(mut self, style: Style) -> Self {
173 if let Self::Full { style: slot, .. } = &mut self {
174 *slot = Some(style);
175 }
176 self
177 }
178
179 #[must_use]
181 pub fn content(&self) -> &Text {
182 match self {
183 Self::Text(text) => text,
184 Self::Full { content, .. } => content,
185 }
186 }
187}
188
189impl From<Text> for ListItem {
190 fn from(text: Text) -> Self {
191 Self::Text(text)
192 }
193}
194
195impl From<&str> for ListItem {
196 fn from(s: &str) -> Self {
197 Self::Text(Text::from(s))
198 }
199}
200
201impl From<String> for ListItem {
202 fn from(s: String) -> Self {
203 Self::Text(Text::from(s))
204 }
205}
206
207#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
210#[serde(rename_all = "camelCase")]
211pub struct List {
212 #[serde(flatten)]
214 pub common: Common,
215 pub items: Vec<ListItem>,
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub state: Option<ListState>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub highlight_style: Option<Style>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub highlight_symbol: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub repeat_highlight_symbol: Option<bool>,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub highlight_spacing: Option<HighlightSpacing>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub direction: Option<ListDirection>,
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub scroll_padding: Option<u16>,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub multi: Option<bool>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub scrollbar: Option<bool>,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub on_select: Option<ActionId>,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub on_activate: Option<ActionId>,
250}
251
252impl List {
253 #[must_use]
255 pub fn new(items: impl IntoIterator<Item = impl Into<ListItem>>) -> Self {
256 Self {
257 items: items.into_iter().map(Into::into).collect(),
258 ..Self::default()
259 }
260 }
261
262 #[must_use]
264 pub fn items(mut self, items: impl IntoIterator<Item = impl Into<ListItem>>) -> Self {
265 self.items = items.into_iter().map(Into::into).collect();
266 self
267 }
268
269 #[must_use]
271 pub fn state(mut self, state: ListState) -> Self {
272 self.state = Some(state);
273 self
274 }
275
276 #[must_use]
278 pub fn highlight_style(mut self, style: Style) -> Self {
279 self.highlight_style = Some(style);
280 self
281 }
282
283 #[must_use]
285 pub fn highlight_symbol(mut self, symbol: impl Into<String>) -> Self {
286 self.highlight_symbol = Some(symbol.into());
287 self
288 }
289
290 #[must_use]
292 pub fn repeat_highlight_symbol(mut self, repeat: bool) -> Self {
293 self.repeat_highlight_symbol = Some(repeat);
294 self
295 }
296
297 #[must_use]
299 pub fn highlight_spacing(mut self, spacing: HighlightSpacing) -> Self {
300 self.highlight_spacing = Some(spacing);
301 self
302 }
303
304 #[must_use]
306 pub fn direction(mut self, direction: ListDirection) -> Self {
307 self.direction = Some(direction);
308 self
309 }
310
311 #[must_use]
313 pub fn scroll_padding(mut self, padding: u16) -> Self {
314 self.scroll_padding = Some(padding);
315 self
316 }
317
318 #[must_use]
320 pub fn multi(mut self, multi: bool) -> Self {
321 self.multi = Some(multi);
322 self
323 }
324
325 #[must_use]
327 pub fn scrollbar(mut self, scrollbar: bool) -> Self {
328 self.scrollbar = Some(scrollbar);
329 self
330 }
331
332 #[must_use]
334 pub fn on_select(mut self, action: impl Into<ActionId>) -> Self {
335 self.on_select = Some(action.into());
336 self
337 }
338
339 #[must_use]
341 pub fn on_activate(mut self, action: impl Into<ActionId>) -> Self {
342 self.on_activate = Some(action.into());
343 self
344 }
345}
346
347impl IntoNode for List {}
348impl From<List> for Node {
349 fn from(l: List) -> Self {
350 Node::List(l)
351 }
352}
353
354pub type TableState = ListState;