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 { rows: cell, ..self.state },
138 ..self
139 }
140 }
141 pub fn rows_provider(self, p: RowsProvider) -> Self {
146 *self.state.rows_provider.borrow_mut() = Some(p);
147 self
148 }
149
150 pub fn overline_pred(self, pred: RowPredicate) -> Self {
151 *self.state.overline_pred.borrow_mut() = Some(pred);
152 self
153 }
154
155 pub fn selection(self, sel: Rc<RefCell<HashSet<usize>>>) -> Self {
160 let pred: RowPredicate = Box::new(move |i| sel.borrow().contains(&i));
161 *self.state.selection_pred.borrow_mut() = Some(pred);
162 self
163 }
164
165 pub fn selection_pred(self, pred: RowPredicate) -> Self {
170 *self.state.selection_pred.borrow_mut() = Some(pred);
171 self
172 }
173
174 pub fn resizable(self, on: bool) -> Self {
175 self.state.resizable.set(on);
176 self
177 }
178 pub fn resizable_cell(self, cell: Rc<Cell<bool>>) -> Self {
179 Self {
180 state: TableState {
181 resizable: cell,
182 ..self.state
183 },
184 ..self
185 }
186 }
187
188 pub fn column_overrides_cell(self, cell: Rc<RefCell<Vec<Option<f64>>>>) -> Self {
192 Self {
193 state: TableState {
194 column_overrides: cell,
195 ..self.state
196 },
197 ..self
198 }
199 }
200
201 pub fn scroll_to_row_cell(self, cell: Rc<Cell<Option<usize>>>) -> Self {
202 Self {
203 state: TableState {
204 scroll_to_row: cell,
205 ..self.state
206 },
207 ..self
208 }
209 }
210
211 pub fn scroll_offset_cell(self, cell: Rc<Cell<f64>>) -> Self {
212 Self {
213 state: TableState {
214 scroll_offset: cell,
215 ..self.state
216 },
217 ..self
218 }
219 }
220
221 pub fn header_height(mut self, h: f64) -> Self {
222 self.header_height = h;
223 self
224 }
225 pub fn header_painter(mut self, p: HeaderPainter) -> Self {
226 self.header_painter = Some(p);
227 self
228 }
229 pub fn header_click(mut self, p: HeaderClick) -> Self {
230 self.header_click = Some(p);
231 self
232 }
233
234 pub fn cell_painter(self, p: CellPainter) -> Self {
235 *self.state.cell_painter.borrow_mut() = Some(p);
236 self
237 }
238
239 pub fn on_row_click(self, f: Box<dyn FnMut(usize, usize)>) -> Self {
240 *self.state.on_row_click.borrow_mut() = Some(f);
241 self
242 }
243
244 pub fn build(self, font: Arc<Font>) -> Table {
246 let body = TableBody {
247 bounds: Rect::default(),
248 children: Vec::new(),
249 font: Arc::clone(&font),
250 state: self.state.clone(),
251 };
252 let mut scroll = ScrollView::new(Box::new(body))
253 .vertical(true)
254 .horizontal(true)
255 .with_offset_cell(Rc::clone(&self.state.scroll_offset))
256 .with_h_offset_cell(Rc::clone(&self.state.h_offset))
257 .with_viewport_cell(Rc::clone(&self.state.viewport_cell));
258 if let Some(c) = self.fade_color {
259 scroll = scroll.with_fade_color(c);
260 }
261
262 let n = self.columns.len();
263 self.state.column_overrides.borrow_mut().resize(n, None);
264 Table {
265 bounds: Rect::default(),
266 children: vec![Box::new(scroll)],
267 base: WidgetBase::new(),
268 font,
269 columns: self.columns,
270 state: self.state,
271 header_height: self.header_height,
272 header_painter: RefCell::new(self.header_painter),
273 header_click: RefCell::new(self.header_click),
274 drag_resize: Cell::new(None),
275 }
276 }
277}
278
279pub struct Table {
282 bounds: Rect,
283 children: Vec<Box<dyn Widget>>, base: WidgetBase,
285 font: Arc<Font>,
286 columns: Vec<TableColumn>,
287 state: TableState,
288 header_height: f64,
289 header_painter: RefCell<Option<HeaderPainter>>,
290 header_click: RefCell<Option<HeaderClick>>,
291 drag_resize: Cell<Option<(usize, f64, f64)>>,
293}
294
295impl Table {
296 pub fn builder() -> TableBuilder {
298 TableBuilder::new()
299 }
300
301 pub fn reset_column_widths(&self) {
304 self.state.column_overrides.borrow_mut().clear();
305 }
306
307 pub fn column_overrides(&self) -> Rc<RefCell<Vec<Option<f64>>>> {
311 Rc::clone(&self.state.column_overrides)
312 }
313
314 pub fn set_rows(&self, rows: TableRows) {
317 *self.state.rows.borrow_mut() = rows;
318 }
319
320 pub fn rows_handle(&self) -> Rc<RefCell<TableRows>> {
322 Rc::clone(&self.state.rows)
323 }
324
325 pub fn margin(mut self, m: crate::layout_props::Insets) -> Self {
326 self.base.margin = m;
327 self
328 }
329}
330
331impl Widget for Table {
332 fn type_name(&self) -> &'static str {
333 "Table"
334 }
335 fn bounds(&self) -> Rect {
336 self.bounds
337 }
338 fn set_bounds(&mut self, b: Rect) {
339 self.bounds = b;
340 }
341 fn children(&self) -> &[Box<dyn Widget>] {
342 &self.children
343 }
344 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
345 &mut self.children
346 }
347
348 fn margin(&self) -> crate::layout_props::Insets {
349 self.base.margin
350 }
351
352 fn layout(&mut self, available: Size) -> Size {
353 let w = available.width.max(40.0);
354 let h = available.height.max(40.0);
355 self.bounds = Rect::new(0.0, 0.0, w, h);
356
357 if let Some(p) = self.state.rows_provider.borrow().as_ref() {
360 let new_rows = p();
361 *self.state.rows.borrow_mut() = new_rows;
362 }
363
364 if let Some(target) = self.state.scroll_to_row.get() {
366 self.state.scroll_to_row.set(None);
367 let rows = self.state.rows.borrow();
368 let n = rows.count();
369 if n > 0 {
370 let target = target.min(n - 1);
371 self.state.scroll_offset.set(rows.top_down_y_at(target));
372 }
373 }
374
375 let overrides = self.state.column_overrides.borrow().clone();
380 let widths = distribute_widths(&self.columns, w, &overrides);
381 let content_w: f64 = widths.iter().sum();
382 *self.state.widths.borrow_mut() = widths;
383 self.state.content_w.set(content_w);
384
385 let body_h = (h - self.header_height).max(0.0);
387 let scroll = &mut self.children[0];
388 scroll.layout(Size::new(w, body_h));
389 scroll.set_bounds(Rect::new(0.0, 0.0, w, body_h));
390
391 Size::new(w, h)
392 }
393
394 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
395 let v = ctx.visuals();
396 let widths = self.state.widths.borrow().clone();
397 let header_y = self.bounds.height - self.header_height;
398 let h = self.header_height;
399 let viewport_w = self.bounds.width;
400 let h_offset = self.state.h_offset.get();
401
402 ctx.set_fill_color(Color::rgba(0.5, 0.5, 0.5, 0.10));
406 ctx.begin_path();
407 ctx.rect(0.0, header_y, viewport_w, h);
408 ctx.fill();
409
410 ctx.set_stroke_color(v.separator);
412 ctx.set_line_width(1.0);
413 ctx.begin_path();
414 ctx.move_to(0.0, header_y);
415 ctx.line_to(viewport_w, header_y);
416 ctx.stroke();
417
418 ctx.save();
422 ctx.clip_rect(0.0, header_y, viewport_w, h);
423 ctx.translate(-h_offset, 0.0);
424
425 if let Some(painter) = self.header_painter.borrow_mut().as_mut() {
427 let mut x = 0.0;
428 for (col, &cw) in widths.iter().enumerate() {
429 let info = HeaderInfo {
430 col,
431 rect: Rect::new(x, header_y, cw, h),
432 visuals: &v,
433 font: &self.font,
434 };
435 ctx.save();
436 ctx.clip_rect(x, header_y, cw, h);
437 painter(&info, ctx);
438 ctx.restore();
439 x += cw;
440 }
441 }
442
443 let mut sx = 0.0;
446 let dragging = self.drag_resize.get().map(|(c, _, _)| c);
447 for (i, &cw) in widths.iter().enumerate() {
448 sx += cw;
449 if i + 1 < widths.len() {
450 let is_resizable = self.columns.get(i).map(|c| c.resizable).unwrap_or(false)
451 && self.state.resizable.get();
452 let is_active = dragging == Some(i);
453 let color = if is_active {
454 v.accent
455 } else if is_resizable {
456 Color::rgba(v.separator.r, v.separator.g, v.separator.b, 0.9)
457 } else {
458 v.separator
459 };
460 ctx.set_stroke_color(color);
461 ctx.set_line_width(if is_active { 2.0 } else { 1.0 });
462 ctx.begin_path();
463 ctx.move_to(sx, header_y);
464 ctx.line_to(sx, header_y + h);
465 ctx.stroke();
466 }
467 }
468 ctx.restore();
469 }
470
471 fn on_event(&mut self, event: &Event) -> EventResult {
472 let header_y = self.bounds.height - self.header_height;
473 let in_header = |y: f64| y >= header_y && y <= header_y + self.header_height;
474 let h_offset = self.state.h_offset.get();
475
476 let resize_target_at = |content_x: f64, y: f64| -> Option<(usize, f64)> {
481 if !in_header(y) || !self.state.resizable.get() {
482 return None;
483 }
484 let widths = self.state.widths.borrow().clone();
485 let mut acc = 0.0;
486 for (col, &cw) in widths.iter().enumerate() {
487 let edge = acc + cw;
488 let last = col + 1 == widths.len();
489 let resizable = self
490 .columns
491 .get(col)
492 .map(|c| c.resizable)
493 .unwrap_or(false);
494 if !last && resizable && (content_x - edge).abs() <= RESIZE_HIT_HALF {
495 return Some((col, cw));
496 }
497 acc += cw;
498 }
499 None
500 };
501
502 if let Some((col, content_x0, w0)) = self.drag_resize.get() {
505 match event {
506 Event::MouseMove { pos } => {
507 set_cursor_icon(CursorIcon::ResizeHorizontal);
508 let content_x = pos.x + h_offset;
509 let dx = content_x - content_x0;
510 let new_w = (w0 + dx).max(MIN_COL_W);
511 let mut overs = self.state.column_overrides.borrow_mut();
512 if overs.len() <= col {
513 overs.resize(col + 1, None);
514 }
515 overs[col] = Some(new_w);
516 crate::animation::request_draw();
517 return EventResult::Consumed;
518 }
519 Event::MouseUp {
520 button: MouseButton::Left,
521 ..
522 } => {
523 self.drag_resize.set(None);
524 crate::animation::request_draw();
525 return EventResult::Consumed;
526 }
527 _ => {}
528 }
529 }
530
531 if let Event::MouseMove { pos } = event {
534 let content_x = pos.x + h_offset;
535 if resize_target_at(content_x, pos.y).is_some() {
536 set_cursor_icon(CursorIcon::ResizeHorizontal);
537 return EventResult::Consumed;
538 }
539 }
540
541 if let Event::MouseDown {
542 pos,
543 button: MouseButton::Left,
544 ..
545 } = event
546 {
547 let content_x = pos.x + h_offset;
548 if let Some((col, cw)) = resize_target_at(content_x, pos.y) {
549 {
558 let widths = self.state.widths.borrow().clone();
559 let mut overs = self.state.column_overrides.borrow_mut();
560 overs.resize(widths.len(), None);
561 for (j, &w) in widths.iter().enumerate() {
562 if overs[j].is_none() {
563 overs[j] = Some(w);
564 }
565 }
566 }
567 self.drag_resize.set(Some((col, content_x, cw)));
568 set_cursor_icon(CursorIcon::ResizeHorizontal);
569 crate::animation::request_draw();
570 return EventResult::Consumed;
571 }
572 }
573
574 if let Event::MouseUp {
575 pos,
576 button: MouseButton::Left,
577 ..
578 } = event
579 {
580 if in_header(pos.y) {
581 let widths = self.state.widths.borrow().clone();
582 let content_x = pos.x + h_offset;
583 let mut x = 0.0;
584 for (col, cw) in widths.iter().enumerate() {
585 if content_x >= x && content_x < x + cw {
586 let local_x = content_x - x;
587 let local_y = pos.y - header_y;
588 if let Some(cb) = self.header_click.borrow_mut().as_mut() {
589 let r = cb(col, local_x, local_y);
590 if r == EventResult::Consumed {
591 crate::animation::request_draw();
592 }
593 return r;
594 }
595 return EventResult::Ignored;
596 }
597 x += cw;
598 }
599 }
600 }
601 EventResult::Ignored
602 }
603}
604
605pub fn clip_text_to_width(ctx: &dyn DrawCtx, text: &str, max_w: f64) -> String {
610 if let Some(m) = ctx.measure_text(text) {
611 if m.width <= max_w {
612 return text.to_string();
613 }
614 }
615 let mut out = text.to_string();
616 let ell = "…";
617 while !out.is_empty() {
618 out.pop();
619 let candidate = format!("{out}{ell}");
620 if let Some(m) = ctx.measure_text(&candidate) {
621 if m.width <= max_w {
622 return candidate;
623 }
624 }
625 }
626 String::new()
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632
633 #[test]
634 fn distribute_widths_splits_remainders_equally() {
635 let cols = vec![
636 TableColumn::auto(50.0),
637 TableColumn::remainder().at_least(40.0),
638 TableColumn::auto(60.0),
639 TableColumn::remainder(),
640 TableColumn::remainder(),
641 ];
642 let widths = distribute_widths(&cols, 410.0, &[]);
643 assert_eq!(widths[0], 50.0);
644 assert_eq!(widths[2], 60.0);
645 assert!((widths[1] - 100.0).abs() < 0.001);
646 assert!((widths[3] - 100.0).abs() < 0.001);
647 assert!((widths[4] - 100.0).abs() < 0.001);
648 }
649
650 #[test]
651 fn distribute_widths_respects_at_least() {
652 let cols = vec![
653 TableColumn::auto(200.0),
654 TableColumn::remainder().at_least(40.0),
655 ];
656 let widths = distribute_widths(&cols, 100.0, &[]);
657 assert!(widths[1] >= 40.0);
658 }
659
660 #[test]
661 fn distribute_widths_pins_overrides_and_redistributes_remainders() {
662 let cols = vec![
663 TableColumn::auto(50.0),
664 TableColumn::remainder().at_least(20.0),
665 TableColumn::remainder().at_least(20.0),
666 TableColumn::remainder().at_least(20.0),
667 ];
668 let overrides = vec![None, Some(200.0), None, None];
670 let widths = distribute_widths(&cols, 500.0, &overrides);
671 assert_eq!(widths[0], 50.0);
672 assert!((widths[1] - 200.0).abs() < 0.001);
673 assert!((widths[2] - 125.0).abs() < 0.001);
675 assert!((widths[3] - 125.0).abs() < 0.001);
676 }
677
678 #[test]
679 fn distribute_widths_clamps_override_min() {
680 let cols = vec![
681 TableColumn::auto(100.0),
682 TableColumn::remainder().at_least(20.0),
683 ];
684 let widths = distribute_widths(&cols, 200.0, &[Some(2.0), None]);
685 assert!(widths[0] >= MIN_COL_W);
686 }
687
688 #[test]
689 fn rows_homogeneous_total() {
690 let r = TableRows::Homogeneous {
691 count: 5,
692 height: 10.0,
693 };
694 assert_eq!(r.total_height(), 50.0);
695 assert_eq!(r.height_at(3), 10.0);
696 assert_eq!(r.top_down_y_at(2), 20.0);
697 }
698
699 #[test]
700 fn rows_heterogeneous_total() {
701 let r = TableRows::Heterogeneous {
702 heights: vec![10.0, 20.0, 30.0],
703 };
704 assert_eq!(r.total_height(), 60.0);
705 assert_eq!(r.top_down_y_at(2), 30.0);
706 }
707}