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}