Skip to main content

oino_ui/widgets/
table.rs

1//! ratatui `Table` (§4.5.1): [`Table`], [`Row`] and [`Cell`].
2
3use 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/// ratatui `Cell`: a `Text` shorthand or `{content, style}`.
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13#[serde(untagged)]
14pub enum Cell {
15    /// Shorthand: cell text.
16    Text(Text),
17    /// Full form.
18    Full {
19        /// Cell text.
20        content: Text,
21        /// Cell style.
22        #[serde(default, skip_serializing_if = "Option::is_none")]
23        style: Option<Style>,
24    },
25}
26
27impl Cell {
28    /// Cell from any text-like value.
29    #[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    /// Sets the cell style.
38    #[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    /// The cell's text.
47    #[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/// ratatui `Row`.
75#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase")]
77pub struct Row {
78    /// Row identity among siblings (defaults to the positional key, §4.1).
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub key: Option<Key>,
81    /// Row cells.
82    pub cells: Vec<Cell>,
83    /// Row height in rows (default 1).
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub height: Option<u16>,
86    /// Blank rows below the row.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub bottom_margin: Option<u16>,
89    /// Row style.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub style: Option<Style>,
92}
93
94impl Row {
95    /// A row from cells.
96    #[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    /// Sets the row key.
105    #[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    /// Sets the row height.
112    #[must_use]
113    pub fn height(mut self, height: u16) -> Self {
114        self.height = Some(height);
115        self
116    }
117
118    /// Sets the bottom margin.
119    #[must_use]
120    pub fn bottom_margin(mut self, margin: u16) -> Self {
121        self.bottom_margin = Some(margin);
122        self
123    }
124
125    /// Sets the row style.
126    #[must_use]
127    pub fn style(mut self, style: Style) -> Self {
128        self.style = Some(style);
129        self
130    }
131}
132
133/// Sticky column mode (GUI extra).
134#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
135pub enum Sticky {
136    /// First column stays visible during horizontal scroll.
137    #[default]
138    FirstColumn,
139}
140
141/// ratatui `Table` (§4.5.1). Column widths are solved horizontally like a Layout.
142#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct Table {
145    /// Common node props (`key`, `style`, `block`, `fallback`).
146    #[serde(flatten)]
147    pub common: Common,
148    /// Header row.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub header: Option<Row>,
151    /// Body rows (≤ 5 000, §4.8).
152    pub rows: Vec<Row>,
153    /// Footer row.
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub footer: Option<Row>,
156    /// One width constraint per column.
157    pub widths: Vec<Constraint>,
158    /// Gap between columns (default 1).
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub column_spacing: Option<u16>,
161    /// Selected-row style.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub row_highlight_style: Option<Style>,
164    /// Prefix drawn before the selected row.
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub highlight_symbol: Option<String>,
167    /// Space reserved for the highlight symbol.
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub highlight_spacing: Option<HighlightSpacing>,
170    /// Selection / offset state (wins when it changes, §4.6).
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub state: Option<ListState>,
173    /// GUI extra: sticky first column.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub sticky: Option<Sticky>,
176    /// GUI extra: native scrollbar.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub scrollbar: Option<bool>,
179    /// Action on selection change (row key).
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub on_select: Option<ActionId>,
182    /// Action on Enter (row key).
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub on_activate: Option<ActionId>,
185}
186
187impl Table {
188    /// A table from rows and column widths.
189    #[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    /// Sets the header row.
202    #[must_use]
203    pub fn header(mut self, header: impl Into<Row>) -> Self {
204        self.header = Some(header.into());
205        self
206    }
207
208    /// Sets the footer row.
209    #[must_use]
210    pub fn footer(mut self, footer: impl Into<Row>) -> Self {
211        self.footer = Some(footer.into());
212        self
213    }
214
215    /// Sets the column width constraints.
216    #[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    /// Sets the column gap.
223    #[must_use]
224    pub fn column_spacing(mut self, spacing: u16) -> Self {
225        self.column_spacing = Some(spacing);
226        self
227    }
228
229    /// Sets the selected-row style.
230    #[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    /// Sets the prefix drawn before the selected row.
237    #[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    /// Sets the highlight-symbol spacing policy.
244    #[must_use]
245    pub fn highlight_spacing(mut self, spacing: HighlightSpacing) -> Self {
246        self.highlight_spacing = Some(spacing);
247        self
248    }
249
250    /// Sets the selection / offset state.
251    #[must_use]
252    pub fn state(mut self, state: ListState) -> Self {
253        self.state = Some(state);
254        self
255    }
256
257    /// GUI extra: sticky first column.
258    #[must_use]
259    pub fn sticky(mut self, sticky: Sticky) -> Self {
260        self.sticky = Some(sticky);
261        self
262    }
263
264    /// GUI extra: native scrollbar.
265    #[must_use]
266    pub fn scrollbar(mut self, scrollbar: bool) -> Self {
267        self.scrollbar = Some(scrollbar);
268        self
269    }
270
271    /// Sets the selection action.
272    #[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    /// Sets the activate action.
279    #[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}