1mod body;
28mod config;
29mod state;
30
31pub use config::{
32 distribute_widths, CellInfo, CellPainter, ColumnSize, HeaderClick, HeaderInfo, HeaderPainter,
33 RowPredicate, RowsProvider, TableColumn, TableRows, MIN_COL_W, RESIZE_HIT_HALF,
34};
35
36use std::cell::{Cell, RefCell};
37use std::collections::HashSet;
38use std::rc::Rc;
39use std::sync::Arc;
40
41use crate::color::Color;
42use crate::cursor::{set_cursor_icon, CursorIcon};
43use crate::draw_ctx::DrawCtx;
44use crate::event::{Event, EventResult, MouseButton};
45use crate::geometry::{Rect, Size};
46use crate::layout_props::WidgetBase;
47use crate::text::Font;
48use crate::widget::Widget;
49use crate::widgets::scroll_view::ScrollView;
50
51use body::TableBody;
52use state::TableState;
53
54pub struct TableBuilder {
57 state: TableState,
58 columns: Vec<TableColumn>,
59 header_height: f64,
60 header_painter: Option<HeaderPainter>,
61 header_click: Option<HeaderClick>,
62 fade_color: Option<Color>,
68}
69
70impl Default for TableBuilder {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl TableBuilder {
77 pub fn new() -> Self {
78 Self {
79 state: TableState::defaults(),
80 columns: Vec::new(),
81 header_height: 22.0,
82 header_painter: None,
83 header_click: None,
84 fade_color: None,
85 }
86 }
87
88 pub fn fade_color(mut self, c: Color) -> Self {
94 self.fade_color = Some(c);
95 self
96 }
97
98 pub fn columns(mut self, cols: Vec<TableColumn>) -> Self {
99 self.columns = cols;
100 self
101 }
102
103 pub fn striped(self, on: bool) -> Self {
104 self.state.striped.set(on);
105 self
106 }
107 pub fn striped_cell(self, cell: Rc<Cell<bool>>) -> Self {
108 Self {
109 state: TableState {
110 striped: cell,
111 ..self.state
112 },
113 ..self
114 }
115 }
116
117 pub fn sense_click(self, on: bool) -> Self {
118 self.state.sense_click.set(on);
119 self
120 }
121 pub fn sense_click_cell(self, cell: Rc<Cell<bool>>) -> Self {
122 Self {
123 state: TableState {
124 sense_click: cell,
125 ..self.state
126 },
127 ..self
128 }
129 }
130
131 pub fn rows(self, spec: TableRows) -> Self {
132 *self.state.rows.borrow_mut() = spec;
133 self
134 }
135 pub fn rows_cell(self, cell: Rc<RefCell<TableRows>>) -> Self {
136 Self {
137 state: TableState {
138 rows: cell,
139 ..self.state
140 },
141 ..self
142 }
143 }
144 pub fn rows_provider(self, p: RowsProvider) -> Self {
149 *self.state.rows_provider.borrow_mut() = Some(p);
150 self
151 }
152
153 pub fn overline_pred(self, pred: RowPredicate) -> Self {
154 *self.state.overline_pred.borrow_mut() = Some(pred);
155 self
156 }
157
158 pub fn selection(self, sel: Rc<RefCell<HashSet<usize>>>) -> Self {
163 let pred: RowPredicate = Box::new(move |i| sel.borrow().contains(&i));
164 *self.state.selection_pred.borrow_mut() = Some(pred);
165 self
166 }
167
168 pub fn selection_pred(self, pred: RowPredicate) -> Self {
173 *self.state.selection_pred.borrow_mut() = Some(pred);
174 self
175 }
176
177 pub fn resizable(self, on: bool) -> Self {
178 self.state.resizable.set(on);
179 self
180 }
181 pub fn resizable_cell(self, cell: Rc<Cell<bool>>) -> Self {
182 Self {
183 state: TableState {
184 resizable: cell,
185 ..self.state
186 },
187 ..self
188 }
189 }
190
191 pub fn column_overrides_cell(self, cell: Rc<RefCell<Vec<Option<f64>>>>) -> Self {
195 Self {
196 state: TableState {
197 column_overrides: cell,
198 ..self.state
199 },
200 ..self
201 }
202 }
203
204 pub fn scroll_to_row_cell(self, cell: Rc<Cell<Option<usize>>>) -> Self {
205 Self {
206 state: TableState {
207 scroll_to_row: cell,
208 ..self.state
209 },
210 ..self
211 }
212 }
213
214 pub fn scroll_offset_cell(self, cell: Rc<Cell<f64>>) -> Self {
215 Self {
216 state: TableState {
217 scroll_offset: cell,
218 ..self.state
219 },
220 ..self
221 }
222 }
223
224 pub fn header_height(mut self, h: f64) -> Self {
225 self.header_height = h;
226 self
227 }
228 pub fn header_painter(mut self, p: HeaderPainter) -> Self {
229 self.header_painter = Some(p);
230 self
231 }
232 pub fn header_click(mut self, p: HeaderClick) -> Self {
233 self.header_click = Some(p);
234 self
235 }
236
237 pub fn cell_painter(self, p: CellPainter) -> Self {
238 *self.state.cell_painter.borrow_mut() = Some(p);
239 self
240 }
241
242 pub fn on_row_click(self, f: Box<dyn FnMut(usize, usize)>) -> Self {
243 *self.state.on_row_click.borrow_mut() = Some(f);
244 self
245 }
246
247 pub fn build(self, font: Arc<Font>) -> Table {
249 let body = TableBody {
250 bounds: Rect::default(),
251 children: Vec::new(),
252 font: Arc::clone(&font),
253 state: self.state.clone(),
254 };
255 let mut scroll = ScrollView::new(Box::new(body))
256 .vertical(true)
257 .horizontal(true)
258 .with_offset_cell(Rc::clone(&self.state.scroll_offset))
259 .with_h_offset_cell(Rc::clone(&self.state.h_offset))
260 .with_viewport_cell(Rc::clone(&self.state.viewport_cell));
261 if let Some(c) = self.fade_color {
262 scroll = scroll.with_fade_color(c);
263 }
264
265 let n = self.columns.len();
266 self.state.column_overrides.borrow_mut().resize(n, None);
267 Table {
268 bounds: Rect::default(),
269 children: vec![Box::new(scroll)],
270 base: WidgetBase::new(),
271 font,
272 columns: self.columns,
273 state: self.state,
274 header_height: self.header_height,
275 header_painter: RefCell::new(self.header_painter),
276 header_click: RefCell::new(self.header_click),
277 drag_resize: Cell::new(None),
278 }
279 }
280}
281
282pub struct Table {
285 bounds: Rect,
286 children: Vec<Box<dyn Widget>>, base: WidgetBase,
288 font: Arc<Font>,
289 columns: Vec<TableColumn>,
290 state: TableState,
291 header_height: f64,
292 header_painter: RefCell<Option<HeaderPainter>>,
293 header_click: RefCell<Option<HeaderClick>>,
294 drag_resize: Cell<Option<(usize, f64, f64)>>,
296}
297
298impl Table {
299 pub fn builder() -> TableBuilder {
301 TableBuilder::new()
302 }
303
304 pub fn reset_column_widths(&self) {
307 self.state.column_overrides.borrow_mut().clear();
308 }
309
310 pub fn column_overrides(&self) -> Rc<RefCell<Vec<Option<f64>>>> {
314 Rc::clone(&self.state.column_overrides)
315 }
316
317 pub fn set_rows(&self, rows: TableRows) {
320 *self.state.rows.borrow_mut() = rows;
321 }
322
323 pub fn rows_handle(&self) -> Rc<RefCell<TableRows>> {
325 Rc::clone(&self.state.rows)
326 }
327
328 pub fn margin(mut self, m: crate::layout_props::Insets) -> Self {
329 self.base.margin = m;
330 self
331 }
332}
333
334impl Widget for Table {
335 fn type_name(&self) -> &'static str {
336 "Table"
337 }
338 fn bounds(&self) -> Rect {
339 self.bounds
340 }
341 fn set_bounds(&mut self, b: Rect) {
342 self.bounds = b;
343 }
344 fn children(&self) -> &[Box<dyn Widget>] {
345 &self.children
346 }
347 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
348 &mut self.children
349 }
350
351 fn margin(&self) -> crate::layout_props::Insets {
352 self.base.margin
353 }
354
355 fn layout(&mut self, available: Size) -> Size {
356 let w = available.width.max(40.0);
357 let h = available.height.max(40.0);
358 self.bounds = Rect::new(0.0, 0.0, w, h);
359
360 if let Some(p) = self.state.rows_provider.borrow().as_ref() {
363 let new_rows = p();
364 *self.state.rows.borrow_mut() = new_rows;
365 }
366
367 if let Some(target) = self.state.scroll_to_row.get() {
369 self.state.scroll_to_row.set(None);
370 let rows = self.state.rows.borrow();
371 let n = rows.count();
372 if n > 0 {
373 let target = target.min(n - 1);
374 self.state.scroll_offset.set(rows.top_down_y_at(target));
375 }
376 }
377
378 let overrides = self.state.column_overrides.borrow().clone();
383 let widths = distribute_widths(&self.columns, w, &overrides);
384 let content_w: f64 = widths.iter().sum();
385 *self.state.widths.borrow_mut() = widths;
386 self.state.content_w.set(content_w);
387
388 let body_h = (h - self.header_height).max(0.0);
390 let scroll = &mut self.children[0];
391 scroll.layout(Size::new(w, body_h));
392 scroll.set_bounds(Rect::new(0.0, 0.0, w, body_h));
393
394 Size::new(w, h)
395 }
396
397 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
398 let v = ctx.visuals();
399 let widths = self.state.widths.borrow().clone();
400 let header_y = self.bounds.height - self.header_height;
401 let h = self.header_height;
402 let viewport_w = self.bounds.width;
403 let h_offset = self.state.h_offset.get();
404
405 ctx.set_fill_color(Color::rgba(0.5, 0.5, 0.5, 0.10));
409 ctx.begin_path();
410 ctx.rect(0.0, header_y, viewport_w, h);
411 ctx.fill();
412
413 ctx.set_stroke_color(v.separator);
415 ctx.set_line_width(1.0);
416 ctx.begin_path();
417 ctx.move_to(0.0, header_y);
418 ctx.line_to(viewport_w, header_y);
419 ctx.stroke();
420
421 ctx.save();
425 ctx.clip_rect(0.0, header_y, viewport_w, h);
426 ctx.translate(-h_offset, 0.0);
427
428 if let Some(painter) = self.header_painter.borrow_mut().as_mut() {
430 let mut x = 0.0;
431 for (col, &cw) in widths.iter().enumerate() {
432 let info = HeaderInfo {
433 col,
434 rect: Rect::new(x, header_y, cw, h),
435 visuals: &v,
436 font: &self.font,
437 };
438 ctx.save();
439 ctx.clip_rect(x, header_y, cw, h);
440 painter(&info, ctx);
441 ctx.restore();
442 x += cw;
443 }
444 }
445
446 let mut sx = 0.0;
449 let dragging = self.drag_resize.get().map(|(c, _, _)| c);
450 for (i, &cw) in widths.iter().enumerate() {
451 sx += cw;
452 if i + 1 < widths.len() {
453 let is_resizable = self.columns.get(i).map(|c| c.resizable).unwrap_or(false)
454 && self.state.resizable.get();
455 let is_active = dragging == Some(i);
456 let color = if is_active {
457 v.accent
458 } else if is_resizable {
459 Color::rgba(v.separator.r, v.separator.g, v.separator.b, 0.9)
460 } else {
461 v.separator
462 };
463 ctx.set_stroke_color(color);
464 ctx.set_line_width(if is_active { 2.0 } else { 1.0 });
465 ctx.begin_path();
466 ctx.move_to(sx, header_y);
467 ctx.line_to(sx, header_y + h);
468 ctx.stroke();
469 }
470 }
471 ctx.restore();
472 }
473
474 fn on_event(&mut self, event: &Event) -> EventResult {
475 let header_y = self.bounds.height - self.header_height;
476 let in_header = |y: f64| y >= header_y && y <= header_y + self.header_height;
477 let h_offset = self.state.h_offset.get();
478
479 let resize_target_at = |content_x: f64, y: f64| -> Option<(usize, f64)> {
484 if !in_header(y) || !self.state.resizable.get() {
485 return None;
486 }
487 let widths = self.state.widths.borrow().clone();
488 let mut acc = 0.0;
489 for (col, &cw) in widths.iter().enumerate() {
490 let edge = acc + cw;
491 let last = col + 1 == widths.len();
492 let resizable = self.columns.get(col).map(|c| c.resizable).unwrap_or(false);
493 if !last && resizable && (content_x - edge).abs() <= RESIZE_HIT_HALF {
494 return Some((col, cw));
495 }
496 acc += cw;
497 }
498 None
499 };
500
501 if let Some((col, content_x0, w0)) = self.drag_resize.get() {
504 match event {
505 Event::MouseMove { pos } => {
506 set_cursor_icon(CursorIcon::ResizeHorizontal);
507 let content_x = pos.x + h_offset;
508 let dx = content_x - content_x0;
509 let new_w = (w0 + dx).max(MIN_COL_W);
510 let mut overs = self.state.column_overrides.borrow_mut();
511 if overs.len() <= col {
512 overs.resize(col + 1, None);
513 }
514 overs[col] = Some(new_w);
515 crate::animation::request_draw();
516 return EventResult::Consumed;
517 }
518 Event::MouseUp {
519 button: MouseButton::Left,
520 ..
521 } => {
522 self.drag_resize.set(None);
523 crate::animation::request_draw();
524 return EventResult::Consumed;
525 }
526 _ => {}
527 }
528 }
529
530 if let Event::MouseMove { pos } = event {
533 let content_x = pos.x + h_offset;
534 if resize_target_at(content_x, pos.y).is_some() {
535 set_cursor_icon(CursorIcon::ResizeHorizontal);
536 return EventResult::Consumed;
537 }
538 }
539
540 if let Event::MouseDown {
541 pos,
542 button: MouseButton::Left,
543 ..
544 } = event
545 {
546 let content_x = pos.x + h_offset;
547 if let Some((col, cw)) = resize_target_at(content_x, pos.y) {
548 {
557 let widths = self.state.widths.borrow().clone();
558 let mut overs = self.state.column_overrides.borrow_mut();
559 overs.resize(widths.len(), None);
560 for (j, &w) in widths.iter().enumerate() {
561 if overs[j].is_none() {
562 overs[j] = Some(w);
563 }
564 }
565 }
566 self.drag_resize.set(Some((col, content_x, cw)));
567 set_cursor_icon(CursorIcon::ResizeHorizontal);
568 crate::animation::request_draw();
569 return EventResult::Consumed;
570 }
571 }
572
573 if let Event::MouseUp {
574 pos,
575 button: MouseButton::Left,
576 ..
577 } = event
578 {
579 if in_header(pos.y) {
580 let widths = self.state.widths.borrow().clone();
581 let content_x = pos.x + h_offset;
582 let mut x = 0.0;
583 for (col, cw) in widths.iter().enumerate() {
584 if content_x >= x && content_x < x + cw {
585 let local_x = content_x - x;
586 let local_y = pos.y - header_y;
587 if let Some(cb) = self.header_click.borrow_mut().as_mut() {
588 let r = cb(col, local_x, local_y);
589 if r == EventResult::Consumed {
590 crate::animation::request_draw();
591 }
592 return r;
593 }
594 return EventResult::Ignored;
595 }
596 x += cw;
597 }
598 }
599 }
600 EventResult::Ignored
601 }
602}
603
604pub fn clip_text_to_width(ctx: &dyn DrawCtx, text: &str, max_w: f64) -> String {
609 if let Some(m) = ctx.measure_text(text) {
610 if m.width <= max_w {
611 return text.to_string();
612 }
613 }
614 let mut out = text.to_string();
615 let ell = "…";
616 while !out.is_empty() {
617 out.pop();
618 let candidate = format!("{out}{ell}");
619 if let Some(m) = ctx.measure_text(&candidate) {
620 if m.width <= max_w {
621 return candidate;
622 }
623 }
624 }
625 String::new()
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn distribute_widths_splits_remainders_equally() {
634 let cols = vec![
635 TableColumn::auto(50.0),
636 TableColumn::remainder().at_least(40.0),
637 TableColumn::auto(60.0),
638 TableColumn::remainder(),
639 TableColumn::remainder(),
640 ];
641 let widths = distribute_widths(&cols, 410.0, &[]);
642 assert_eq!(widths[0], 50.0);
643 assert_eq!(widths[2], 60.0);
644 assert!((widths[1] - 100.0).abs() < 0.001);
645 assert!((widths[3] - 100.0).abs() < 0.001);
646 assert!((widths[4] - 100.0).abs() < 0.001);
647 }
648
649 #[test]
650 fn distribute_widths_respects_at_least() {
651 let cols = vec![
652 TableColumn::auto(200.0),
653 TableColumn::remainder().at_least(40.0),
654 ];
655 let widths = distribute_widths(&cols, 100.0, &[]);
656 assert!(widths[1] >= 40.0);
657 }
658
659 #[test]
660 fn distribute_widths_pins_overrides_and_redistributes_remainders() {
661 let cols = vec![
662 TableColumn::auto(50.0),
663 TableColumn::remainder().at_least(20.0),
664 TableColumn::remainder().at_least(20.0),
665 TableColumn::remainder().at_least(20.0),
666 ];
667 let overrides = vec![None, Some(200.0), None, None];
669 let widths = distribute_widths(&cols, 500.0, &overrides);
670 assert_eq!(widths[0], 50.0);
671 assert!((widths[1] - 200.0).abs() < 0.001);
672 assert!((widths[2] - 125.0).abs() < 0.001);
674 assert!((widths[3] - 125.0).abs() < 0.001);
675 }
676
677 #[test]
678 fn distribute_widths_clamps_override_min() {
679 let cols = vec![
680 TableColumn::auto(100.0),
681 TableColumn::remainder().at_least(20.0),
682 ];
683 let widths = distribute_widths(&cols, 200.0, &[Some(2.0), None]);
684 assert!(widths[0] >= MIN_COL_W);
685 }
686
687 #[test]
688 fn rows_homogeneous_total() {
689 let r = TableRows::Homogeneous {
690 count: 5,
691 height: 10.0,
692 };
693 assert_eq!(r.total_height(), 50.0);
694 assert_eq!(r.height_at(3), 10.0);
695 assert_eq!(r.top_down_y_at(2), 20.0);
696 }
697
698 #[test]
699 fn rows_heterogeneous_total() {
700 let r = TableRows::Heterogeneous {
701 heights: vec![10.0, 20.0, 30.0],
702 };
703 assert_eq!(r.total_height(), 60.0);
704 assert_eq!(r.top_down_y_at(2), 30.0);
705 }
706}