Skip to main content

iced_shadcn/
table.rs

1use iced::border::Border;
2use iced::widget::{column, container, row, rule, text};
3use iced::{Alignment, Background, Color, Element, Length};
4use std::hash::Hash;
5
6use crate::theme::Theme;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
9pub enum TableSize {
10    Size1,
11    #[default]
12    Size2,
13    Size3,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
17pub enum TableVariant {
18    #[default]
19    Default,
20    Muted,
21}
22
23#[derive(Clone, Debug)]
24pub struct TableProps {
25    pub size: TableSize,
26    pub variant: TableVariant,
27}
28
29impl Default for TableProps {
30    fn default() -> Self {
31        Self {
32            size: TableSize::Size2,
33            variant: TableVariant::Default,
34        }
35    }
36}
37
38impl TableProps {
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    pub fn size(mut self, size: TableSize) -> Self {
44        self.size = size;
45        self
46    }
47
48    pub fn variant(mut self, variant: TableVariant) -> Self {
49        self.variant = variant;
50        self
51    }
52}
53
54#[derive(Clone, Copy, Debug)]
55pub struct TableRowProps<IdSource> {
56    pub id_source: IdSource,
57    pub selected: bool,
58    pub hoverable: bool,
59}
60
61impl<IdSource> TableRowProps<IdSource> {
62    pub fn new(id_source: IdSource) -> Self {
63        Self {
64            id_source,
65            selected: false,
66            hoverable: true,
67        }
68    }
69
70    pub fn selected(mut self, selected: bool) -> Self {
71        self.selected = selected;
72        self
73    }
74
75    pub fn hoverable(mut self, hoverable: bool) -> Self {
76        self.hoverable = hoverable;
77        self
78    }
79}
80
81#[derive(Clone, Copy, Debug, Default)]
82pub struct TableCellProps {
83    pub checkbox: bool,
84    pub fill: bool,
85}
86
87impl TableCellProps {
88    pub fn new() -> Self {
89        Self::default()
90    }
91
92    pub fn checkbox(mut self, checkbox: bool) -> Self {
93        self.checkbox = checkbox;
94        self
95    }
96
97    pub fn fill(mut self, fill: bool) -> Self {
98        self.fill = fill;
99        self
100    }
101}
102
103#[derive(Clone, Copy, Debug)]
104pub struct TableContext {
105    pub size: TableSize,
106    pub variant: TableVariant,
107    tokens: TableTokens,
108    metrics: TableMetrics,
109}
110
111#[derive(Clone, Copy, Debug)]
112struct TableTokens {
113    border: Color,
114    text: Color,
115    text_muted: Color,
116    selected_bg: Color,
117    footer_bg: Color,
118    container_bg: Color,
119}
120
121#[derive(Clone, Copy, Debug)]
122struct TableMetrics {
123    row_height: f32,
124    cell_padding: [f32; 2],
125    checkbox_padding: [f32; 2],
126    caption_gap: f32,
127}
128
129fn table_tokens(theme: &Theme, variant: TableVariant) -> TableTokens {
130    let palette = theme.palette;
131    let container_bg = match variant {
132        TableVariant::Default => Color::TRANSPARENT,
133        TableVariant::Muted => apply_opacity(palette.muted, 0.2),
134    };
135    TableTokens {
136        border: palette.border,
137        text: palette.foreground,
138        text_muted: palette.muted_foreground,
139        selected_bg: apply_opacity(palette.muted, 0.7),
140        footer_bg: apply_opacity(palette.muted, 0.5),
141        container_bg,
142    }
143}
144
145fn table_metrics(size: TableSize) -> TableMetrics {
146    match size {
147        TableSize::Size1 => TableMetrics {
148            row_height: 32.0,
149            cell_padding: [6.0, 4.0],
150            checkbox_padding: [6.0, 4.0],
151            caption_gap: 12.0,
152        },
153        TableSize::Size2 => TableMetrics {
154            row_height: 40.0,
155            cell_padding: [8.0, 6.0],
156            checkbox_padding: [8.0, 6.0],
157            caption_gap: 16.0,
158        },
159        TableSize::Size3 => TableMetrics {
160            row_height: 48.0,
161            cell_padding: [10.0, 8.0],
162            checkbox_padding: [10.0, 8.0],
163            caption_gap: 20.0,
164        },
165    }
166}
167
168pub fn table<'a, Message: Clone + 'a>(
169    props: TableProps,
170    theme: &Theme,
171    add_contents: impl FnOnce(&TableContext) -> Element<'a, Message>,
172) -> Element<'a, Message> {
173    let tokens = table_tokens(theme, props.variant);
174    let metrics = table_metrics(props.size);
175    let ctx = TableContext {
176        size: props.size,
177        variant: props.variant,
178        tokens,
179        metrics,
180    };
181
182    container(add_contents(&ctx))
183        .style(move |_t| iced::widget::container::Style {
184            background: Some(Background::Color(tokens.container_bg)),
185            text_color: Some(tokens.text),
186            ..Default::default()
187        })
188        .into()
189}
190
191pub fn table_header<'a, Message: Clone + 'a>(
192    _ctx: &TableContext,
193    content: impl Into<Element<'a, Message>>,
194) -> Element<'a, Message> {
195    container(content).into()
196}
197
198pub fn table_body<'a, Message: Clone + 'a>(
199    _ctx: &TableContext,
200    content: impl Into<Element<'a, Message>>,
201) -> Element<'a, Message> {
202    container(content).into()
203}
204
205pub fn table_footer<'a, Message: Clone + 'a>(
206    ctx: &TableContext,
207    content: impl Into<Element<'a, Message>>,
208) -> Element<'a, Message> {
209    let footer_bg = ctx.tokens.footer_bg;
210    container(content)
211        .style(move |_t| iced::widget::container::Style {
212            background: Some(Background::Color(footer_bg)),
213            ..Default::default()
214        })
215        .into()
216}
217
218pub fn table_row<'a, Message: Clone + 'a, IdSource: Hash>(
219    ctx: &TableContext,
220    props: TableRowProps<IdSource>,
221    cells: Vec<Element<'a, Message>>,
222) -> Element<'a, Message> {
223    let background = if props.selected {
224        ctx.tokens.selected_bg
225    } else {
226        Color::TRANSPARENT
227    };
228    let row_height = ctx.metrics.row_height;
229    let border_color = ctx.tokens.border;
230
231    container(row(cells).spacing(0).align_y(Alignment::Center))
232        .height(Length::Fixed(row_height))
233        .style(move |_t| iced::widget::container::Style {
234            background: Some(Background::Color(background)),
235            border: Border {
236                radius: 0.0.into(),
237                width: 1.0,
238                color: border_color,
239            },
240            ..Default::default()
241        })
242        .into()
243}
244
245pub fn table_head<'a, Message: Clone + 'a>(
246    ctx: &TableContext,
247    props: TableCellProps,
248    content: impl Into<Element<'a, Message>>,
249) -> Element<'a, Message> {
250    let padding = if props.checkbox {
251        ctx.metrics.checkbox_padding
252    } else {
253        ctx.metrics.cell_padding
254    };
255    let text_muted = ctx.tokens.text_muted;
256    let element =
257        container(content)
258            .padding(padding)
259            .style(move |_t| iced::widget::container::Style {
260                text_color: Some(text_muted),
261                ..Default::default()
262            });
263
264    if props.fill {
265        element.width(Length::Fill).into()
266    } else {
267        element.into()
268    }
269}
270
271pub fn table_cell<'a, Message: Clone + 'a>(
272    ctx: &TableContext,
273    props: TableCellProps,
274    content: impl Into<Element<'a, Message>>,
275) -> Element<'a, Message> {
276    let padding = if props.checkbox {
277        ctx.metrics.checkbox_padding
278    } else {
279        ctx.metrics.cell_padding
280    };
281    let element = container(content).padding(padding);
282    if props.fill {
283        element.width(Length::Fill).into()
284    } else {
285        element.into()
286    }
287}
288
289pub fn table_caption<'a, Message: Clone + 'a>(
290    ctx: &TableContext,
291    text_value: &'a str,
292) -> Element<'a, Message> {
293    let text_muted = ctx.tokens.text_muted;
294    let caption_gap = ctx.metrics.caption_gap;
295    column![
296        rule::horizontal(1),
297        text(text_value)
298            .size(12)
299            .style(move |_t| iced::widget::text::Style {
300                color: Some(text_muted),
301            })
302    ]
303    .spacing(caption_gap)
304    .into()
305}
306
307fn apply_opacity(color: Color, opacity: f32) -> Color {
308    Color {
309        a: color.a * opacity,
310        ..color
311    }
312}