1use serde::{Deserialize, Serialize};
4
5use crate::layout::Constraint;
6use crate::node::{ActionId, Common, IntoNode, Key, Node};
7use crate::style::Style;
8use crate::text::Text;
9use crate::widgets::list::{HighlightSpacing, ListState};
10
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum Cell {
15 Text(Text),
17 Full {
19 content: Text,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 style: Option<Style>,
24 },
25}
26
27impl Cell {
28 #[must_use]
30 pub fn new(content: impl Into<Text>) -> Self {
31 Self::Full {
32 content: content.into(),
33 style: None,
34 }
35 }
36
37 #[must_use]
39 pub fn style(mut self, style: Style) -> Self {
40 if let Self::Full { style: slot, .. } = &mut self {
41 *slot = Some(style);
42 }
43 self
44 }
45
46 #[must_use]
48 pub fn content(&self) -> &Text {
49 match self {
50 Self::Text(text) => text,
51 Self::Full { content, .. } => content,
52 }
53 }
54}
55
56impl From<Text> for Cell {
57 fn from(text: Text) -> Self {
58 Self::Text(text)
59 }
60}
61
62impl From<&str> for Cell {
63 fn from(s: &str) -> Self {
64 Self::Text(Text::from(s))
65 }
66}
67
68impl From<String> for Cell {
69 fn from(s: String) -> Self {
70 Self::Text(Text::from(s))
71 }
72}
73
74#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct Row {
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub key: Option<Key>,
81 pub cells: Vec<Cell>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub height: Option<u16>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub bottom_margin: Option<u16>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub style: Option<Style>,
92}
93
94impl Row {
95 #[must_use]
97 pub fn new(cells: impl IntoIterator<Item = impl Into<Cell>>) -> Self {
98 Self {
99 cells: cells.into_iter().map(Into::into).collect(),
100 ..Self::default()
101 }
102 }
103
104 #[must_use]
106 pub fn key(mut self, key: impl AsRef<str>) -> Self {
107 self.key = Some(key.as_ref().to_owned());
108 self
109 }
110
111 #[must_use]
113 pub fn height(mut self, height: u16) -> Self {
114 self.height = Some(height);
115 self
116 }
117
118 #[must_use]
120 pub fn bottom_margin(mut self, margin: u16) -> Self {
121 self.bottom_margin = Some(margin);
122 self
123 }
124
125 #[must_use]
127 pub fn style(mut self, style: Style) -> Self {
128 self.style = Some(style);
129 self
130 }
131}
132
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
135pub enum Sticky {
136 #[default]
138 FirstColumn,
139}
140
141#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct Table {
145 #[serde(flatten)]
147 pub common: Common,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub header: Option<Row>,
151 pub rows: Vec<Row>,
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub footer: Option<Row>,
156 pub widths: Vec<Constraint>,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub column_spacing: Option<u16>,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub row_highlight_style: Option<Style>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub highlight_symbol: Option<String>,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub highlight_spacing: Option<HighlightSpacing>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub state: Option<ListState>,
173 #[serde(skip_serializing_if = "Option::is_none")]
175 pub sticky: Option<Sticky>,
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub scrollbar: Option<bool>,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub on_select: Option<ActionId>,
182 #[serde(skip_serializing_if = "Option::is_none")]
184 pub on_activate: Option<ActionId>,
185}
186
187impl Table {
188 #[must_use]
190 pub fn new(
191 rows: impl IntoIterator<Item = impl Into<Row>>,
192 widths: impl IntoIterator<Item = Constraint>,
193 ) -> Self {
194 Self {
195 rows: rows.into_iter().map(Into::into).collect(),
196 widths: widths.into_iter().collect(),
197 ..Self::default()
198 }
199 }
200
201 #[must_use]
203 pub fn header(mut self, header: impl Into<Row>) -> Self {
204 self.header = Some(header.into());
205 self
206 }
207
208 #[must_use]
210 pub fn footer(mut self, footer: impl Into<Row>) -> Self {
211 self.footer = Some(footer.into());
212 self
213 }
214
215 #[must_use]
217 pub fn widths(mut self, widths: impl IntoIterator<Item = Constraint>) -> Self {
218 self.widths = widths.into_iter().collect();
219 self
220 }
221
222 #[must_use]
224 pub fn column_spacing(mut self, spacing: u16) -> Self {
225 self.column_spacing = Some(spacing);
226 self
227 }
228
229 #[must_use]
231 pub fn row_highlight_style(mut self, style: Style) -> Self {
232 self.row_highlight_style = Some(style);
233 self
234 }
235
236 #[must_use]
238 pub fn highlight_symbol(mut self, symbol: impl Into<String>) -> Self {
239 self.highlight_symbol = Some(symbol.into());
240 self
241 }
242
243 #[must_use]
245 pub fn highlight_spacing(mut self, spacing: HighlightSpacing) -> Self {
246 self.highlight_spacing = Some(spacing);
247 self
248 }
249
250 #[must_use]
252 pub fn state(mut self, state: ListState) -> Self {
253 self.state = Some(state);
254 self
255 }
256
257 #[must_use]
259 pub fn sticky(mut self, sticky: Sticky) -> Self {
260 self.sticky = Some(sticky);
261 self
262 }
263
264 #[must_use]
266 pub fn scrollbar(mut self, scrollbar: bool) -> Self {
267 self.scrollbar = Some(scrollbar);
268 self
269 }
270
271 #[must_use]
273 pub fn on_select(mut self, action: impl Into<ActionId>) -> Self {
274 self.on_select = Some(action.into());
275 self
276 }
277
278 #[must_use]
280 pub fn on_activate(mut self, action: impl Into<ActionId>) -> Self {
281 self.on_activate = Some(action.into());
282 self
283 }
284}
285
286impl IntoNode for Table {}
287impl From<Table> for Node {
288 fn from(t: Table) -> Self {
289 Node::Table(t)
290 }
291}