1use std::ops::Range;
28
29use egui::accesskit::{Role as NodeRole, SortDirection};
30use egui::{Event, Id, Key, Modifiers, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, pos2, vec2};
31
32use ashlar_motion::gesture::{VelocityTracker, decay_k, deceleration, project, rubber_band};
33use ashlar_motion::{Class, Curve, tokens as mtokens};
34
35use crate::automation::{self, Details, Role};
36use crate::motion;
37use crate::selection::Selection;
38use crate::tokens::{self, Tokens};
39
40pub const MAX_ROWS: usize = 10_000_000;
42
43#[derive(Clone, Copy)]
45pub enum RowHeights<'a> {
46 Uniform(f32),
47 Variable {
51 revision: u64,
52 height: &'a dyn Fn(usize) -> f32,
53 },
54}
55
56fn sane_height(h: f32) -> f64 {
57 if h.is_finite() { h.clamp(0.0, 10_000.0) as f64 } else { 0.0 }
58}
59
60#[derive(Clone, Default)]
63pub(crate) struct Offsets {
64 heights: Vec<f32>,
65 tree: Vec<f64>,
67 total: f64,
68 revision: u64,
69}
70
71fn lowbit(k: usize) -> usize {
72 k & k.wrapping_neg()
73}
74
75impl Offsets {
76 fn update(&mut self, rows: usize, revision: u64, height: &dyn Fn(usize) -> f32, changed: &[usize]) -> bool {
79 if self.heights.len() == rows && self.revision == revision && self.tree.len() == rows + 1 {
80 let mut any = false;
81 for &row in changed {
82 if let Some(old) = self.heights.get(row).copied() {
83 let new = sane_height(height(row)) as f32;
84 if new != old {
85 self.set(row, old, new);
86 any = true;
87 }
88 }
89 }
90 return any;
91 }
92 self.heights.clear();
95 self.heights.reserve(rows);
96 self.tree.clear();
97 self.tree.reserve(rows + 1);
98 self.tree.push(0.0);
99 let mut sum = 0.0f64;
100 for i in 0..rows {
101 let h = sane_height(height(i));
102 self.heights.push(h as f32);
103 sum += h;
104 self.tree.push(sum);
105 }
106 for k in (1..=rows).rev() {
107 let before = self.tree.get(k - lowbit(k)).copied().unwrap_or(0.0);
108 if let Some(node) = self.tree.get_mut(k) {
109 *node -= before;
110 }
111 }
112 self.total = sum;
113 self.revision = revision;
114 true
115 }
116
117 fn set(&mut self, row: usize, old: f32, new: f32) {
118 if let Some(h) = self.heights.get_mut(row) {
119 *h = new;
120 }
121 let delta = new as f64 - old as f64;
122 let mut k = row + 1;
123 while let Some(node) = self.tree.get_mut(k) {
124 *node += delta;
125 k += lowbit(k);
126 }
127 self.total = self.prefix(self.heights.len());
129 }
130
131 fn rows(&self) -> usize {
132 self.heights.len()
133 }
134
135 fn prefix(&self, i: usize) -> f64 {
137 let mut k = i.min(self.heights.len());
138 let mut sum = 0.0;
139 while k > 0 {
140 sum += self.tree.get(k).copied().unwrap_or(0.0);
141 k -= lowbit(k);
142 }
143 sum.max(0.0)
144 }
145
146 fn height(&self, i: usize) -> f64 {
147 self.heights.get(i).copied().unwrap_or(0.0) as f64
148 }
149
150 fn row_at(&self, y: f64) -> usize {
152 let n = self.heights.len();
153 let mut pos = 0usize;
154 let mut rest = y;
155 let mut step = if n == 0 { 0 } else { 1usize << (usize::BITS - 1 - n.leading_zeros()) };
156 while step > 0 {
157 if let Some(&sum) = self.tree.get(pos + step)
158 && sum <= rest
159 {
160 pos += step;
161 rest -= sum;
162 }
163 step >>= 1;
164 }
165 pos
166 }
167}
168
169pub(crate) enum Layout<'o> {
171 Uniform { rows: usize, height: f64 },
172 Variable(&'o Offsets),
173}
174
175impl Layout<'_> {
176 fn rows(&self) -> usize {
177 match self {
178 Layout::Uniform { rows, .. } => *rows,
179 Layout::Variable(o) => o.rows(),
180 }
181 }
182 fn total(&self) -> f64 {
183 match self {
184 Layout::Uniform { rows, height } => *rows as f64 * height,
185 Layout::Variable(o) => o.total,
186 }
187 }
188 fn span(&self, i: usize) -> (f64, f64) {
190 match self {
191 Layout::Uniform { height, .. } => (i as f64 * height, *height),
192 Layout::Variable(o) => (o.prefix(i), o.height(i)),
193 }
194 }
195 fn row_at(&self, y: f64) -> usize {
197 let rows = self.rows();
198 if rows == 0 {
199 return 0;
200 }
201 let i = match self {
202 Layout::Uniform { height, .. } => {
203 if *height > 0.0 {
204 (y.max(0.0) / height).floor() as usize
205 } else {
206 0
207 }
208 }
209 Layout::Variable(o) => o.row_at(y),
210 };
211 i.min(rows - 1)
212 }
213 fn visible(&self, top: f64, bottom: f64, overscan: usize) -> Range<usize> {
215 let rows = self.rows();
216 if rows == 0 || bottom <= top {
217 return 0..0;
218 }
219 let first = self.row_at(top).saturating_sub(overscan);
220 let last = (self.row_at(bottom) + 1 + overscan).min(rows);
221 first..last
222 }
223}
224
225#[derive(Clone, Copy, Debug)]
227pub struct Row {
228 pub index: usize,
229 pub rect: Rect,
231 pub selected: bool,
232 pub hovered: bool,
233}
234
235pub struct ListResponse {
237 pub response: egui::Response,
239 pub clicked: Option<usize>,
241 pub double_clicked: Option<usize>,
242 pub secondary_clicked: Option<usize>,
243 pub select: Option<usize>,
245 pub activated: Option<usize>,
247 pub visible: Range<usize>,
249}
250
251pub struct VirtualList<'a> {
253 id: &'a str,
254 rows: usize,
255 heights: RowHeights<'a>,
256 selected: Option<usize>,
257 is_selected: Option<&'a dyn Fn(usize) -> bool>,
258 overscan: usize,
259 stripes: bool,
260 scroll_to: Option<usize>,
261 max_height: Option<f32>,
262 row_label: Option<&'a dyn Fn(usize) -> &'a str>,
263 type_ahead: Option<&'a dyn Fn(usize) -> &'a str>,
264 selection: Option<&'a mut Selection>,
265 changed_rows: &'a [usize],
266 fling: bool,
267 label: Option<&'a str>,
268}
269
270pub const TYPE_AHEAD_SECS: f64 = 1.0;
272
273impl<'a> VirtualList<'a> {
274 pub fn new(id: &'a str, rows: usize, heights: RowHeights<'a>) -> Self {
275 VirtualList {
276 id,
277 rows: rows.min(MAX_ROWS),
278 heights,
279 selected: None,
280 is_selected: None,
281 overscan: 2,
282 stripes: false,
283 scroll_to: None,
284 max_height: None,
285 row_label: None,
286 type_ahead: None,
287 selection: None,
288 changed_rows: &[],
289 fling: false,
290 label: None,
291 }
292 }
293 pub fn fling(mut self, on: bool) -> Self {
297 self.fling = on;
298 self
299 }
300 pub fn label(mut self, text: &'a str) -> Self {
302 self.label = Some(text).filter(|t| !t.is_empty());
303 self
304 }
305 pub fn changed_rows(mut self, rows: &'a [usize]) -> Self {
308 self.changed_rows = rows;
309 self
310 }
311 pub fn selected(mut self, row: Option<usize>) -> Self {
313 self.selected = row;
314 self
315 }
316 pub fn is_selected(mut self, f: &'a dyn Fn(usize) -> bool) -> Self {
318 self.is_selected = Some(f);
319 self
320 }
321 pub fn overscan(mut self, rows: usize) -> Self {
323 self.overscan = rows.min(64);
324 self
325 }
326 pub fn stripes(mut self, yes: bool) -> Self {
327 self.stripes = yes;
328 self
329 }
330 pub fn scroll_to(mut self, row: Option<usize>) -> Self {
332 self.scroll_to = row;
333 self
334 }
335 pub fn max_height(mut self, h: f32) -> Self {
336 self.max_height = Some(h);
337 self
338 }
339 pub fn row_label(mut self, f: &'a dyn Fn(usize) -> &'a str) -> Self {
341 self.row_label = Some(f);
342 self
343 }
344 pub fn type_ahead(mut self, text: &'a dyn Fn(usize) -> &'a str) -> Self {
348 self.type_ahead = Some(text);
349 self
350 }
351 pub fn selection(mut self, selection: &'a mut Selection) -> Self {
355 self.selection = Some(selection);
356 self
357 }
358
359 pub fn show(mut self, ui: &mut Ui, row: impl FnMut(&mut Ui, Row)) -> ListResponse {
360 let base = ui.make_persistent_id(self.id);
361 show_rows(ui, base, &mut self, LIST, None, row)
362 }
363}
364
365pub(crate) struct Header<'h> {
366 pub height: f32,
367 pub paint: &'h mut dyn FnMut(&mut Ui, Rect),
368}
369
370#[derive(Clone, Copy)]
374pub(crate) struct Kind {
375 pub role: Role,
376 pub rows: bool,
377 pub node: NodeRole,
379 pub item: NodeRole,
380}
381
382pub(crate) const LIST: Kind = Kind { role: Role::List, rows: true, node: NodeRole::List, item: NodeRole::ListItem };
383
384pub(crate) fn row_id(base: Id, i: usize) -> Id {
386 base.with(("row", i))
387}
388
389fn starts_with_ci(s: &str, prefix: &str) -> bool {
391 let mut s = s.chars().flat_map(char::to_lowercase);
392 prefix.chars().flat_map(char::to_lowercase).all(|p| s.next() == Some(p))
393}
394
395pub(crate) fn type_ahead<'t>(ui: &Ui, base: Id, rows: usize, cursor: Option<usize>, text: &dyn Fn(usize) -> &'t str) -> Option<usize> {
397 let typed: String = ui.input(|i| {
398 if i.modifiers.command || i.modifiers.ctrl || i.modifiers.alt {
399 return String::new();
400 }
401 i.events.iter().filter_map(|e| if let Event::Text(t) = e { Some(t.as_str()) } else { None }).collect()
402 });
403 if typed.is_empty() || rows == 0 {
404 return None;
405 }
406 let key = base.with("type_ahead");
407 let now = ui.input(|i| i.time);
408 let (mut buf, last): (String, f64) = ui.data_mut(|d| d.get_temp(key)).unwrap_or_default();
409 if !(now - last).is_finite() || now - last > TYPE_AHEAD_SECS || now < last {
410 buf.clear();
411 }
412 let fresh = buf.is_empty();
413 for c in typed.chars().filter(|c| !c.is_control()) {
414 if (c != ' ' || !buf.is_empty()) && buf.len() < 64 {
415 buf.push(c);
416 }
417 }
418 ui.data_mut(|d| d.insert_temp(key, (buf.clone(), now)));
419 if buf.is_empty() {
420 return None;
421 }
422 let find = |prefix: &str, from: usize| (0..rows).map(|k| from.saturating_add(k) % rows).find(|&r| starts_with_ci(text(r), prefix));
423 let from = cursor.map_or(0, |c| if fresh { c.saturating_add(1) } else { c });
425 find(&buf, from).or_else(|| {
426 let mut chars = buf.chars();
428 let first = chars.next()?;
429 chars.all(|c| c == first).then(|| find(first.encode_utf8(&mut [0; 4]), cursor.map_or(0, |c| c.saturating_add(1)))).flatten()
430 })
431}
432
433pub(crate) fn show_rows(
434 ui: &mut Ui,
435 base: Id,
436 list: &mut VirtualList<'_>,
437 kind: Kind,
438 header: Option<Header<'_>>,
439 mut row_fn: impl FnMut(&mut Ui, Row),
440) -> ListResponse {
441 let t = tokens::current(ui.ctx());
442 let offsets_id = base.with("offsets");
443 let mut offsets = None;
444 let layout = match list.heights {
445 RowHeights::Uniform(h) => Layout::Uniform { rows: list.rows, height: sane_height(h).max(1.0) },
446 RowHeights::Variable { revision, height } => {
447 let mut o: Offsets = ui.data_mut(|d| d.remove_temp::<Offsets>(offsets_id)).unwrap_or_default();
448 o.update(list.rows, revision, height, list.changed_rows);
449 offsets = Some(o);
450 match &offsets {
451 Some(o) => Layout::Variable(o),
452 None => Layout::Uniform { rows: 0, height: 1.0 },
453 }
454 }
455 };
456
457 let avail = ui.available_rect_before_wrap();
459 let height = list.max_height.filter(|h| h.is_finite() && *h > 0.0).map_or(avail.height(), |h| h.min(avail.height().max(h.min(40.0))));
460 let outer = Rect::from_min_size(avail.min, vec2(avail.width(), height.max(t.control_height)));
461 let response = ui.interact(outer, base, Sense::focusable_noninteractive());
462 let mut sel = list.selection.take();
463 let accesskit = ui
465 .ctx()
466 .accesskit_node_builder(base, |node| {
467 node.set_role(kind.node);
468 if let Some(label) = list.label {
469 node.set_label(label);
470 }
471 if sel.is_some() {
472 node.set_multiselectable();
473 }
474 })
475 .is_some();
476 let focused = crate::edit::has_keyboard(&response);
477 if focused {
478 let filter = egui::EventFilter { tab: false, horizontal_arrows: true, vertical_arrows: true, escape: false };
480 ui.memory_mut(|m| m.set_focus_lock_filter(base, filter));
481 }
482
483 let mut select = None;
484 let mut activated = None;
485 let rows = layout.rows();
486 let cursor = sel.as_ref().and_then(|s| s.cursor()).or(list.selected);
487 let page = ((outer.height() as f64 / layout.span(cursor.unwrap_or(0)).1.max(1.0)) as usize).max(1);
488 if focused && rows > 0 {
489 let cur = cursor.map(|s| s.min(rows - 1));
490 let (up, down, home, end, pgup, pgdn, enter) = ui.input(|i| {
491 (
492 i.key_pressed(Key::ArrowUp),
493 i.key_pressed(Key::ArrowDown),
494 i.key_pressed(Key::Home),
495 i.key_pressed(Key::End),
496 i.key_pressed(Key::PageUp),
497 i.key_pressed(Key::PageDown),
498 i.key_pressed(Key::Enter),
499 )
500 });
501 let next = if up {
502 Some(cur.map_or(0, |c| c.saturating_sub(1)))
503 } else if down {
504 Some(cur.map_or(0, |c| (c + 1).min(rows - 1)))
505 } else if home {
506 Some(0)
507 } else if end {
508 Some(rows - 1)
509 } else if pgup {
510 Some(cur.map_or(0, |c| c.saturating_sub(page)))
511 } else if pgdn {
512 Some(cur.map_or(0, |c| (c + page).min(rows - 1)))
513 } else {
514 list.type_ahead.and_then(|text| type_ahead(ui, base, rows, cur, text))
515 };
516 if let Some(s) = sel.as_deref_mut() {
517 let (shift, all) = ui.input_mut(|i| (i.modifiers.shift, i.consume_key(Modifiers::COMMAND, Key::A)));
518 if let Some(n) = next {
519 if shift { s.extend_to(n) } else { s.select_only(n) }
520 }
521 if all {
522 s.select_all(rows);
523 }
524 }
525 if next.is_some() && next != cur {
526 select = next;
527 }
528 if enter {
529 activated = cur;
530 }
531 }
532 let scroll_target = select.or(list.scroll_to.filter(|r| *r < rows));
533
534 let mut body_rect = outer;
536 if let Some(h) = header {
537 let header_rect = Rect::from_min_size(outer.min, vec2(outer.width(), h.height));
538 let mut hui = ui.new_child(UiBuilder::new().max_rect(header_rect).id_salt("ashlar_list_header").accessibility_parent(base));
539 ui.ctx().accesskit_node_builder(hui.unique_id(), |n| n.set_role(NodeRole::Row));
541 hui.set_clip_rect(header_rect.intersect(ui.clip_rect()));
542 (h.paint)(&mut hui, header_rect);
543 body_rect.min.y = header_rect.max.y;
544 }
545
546 let view_h_id = base.with("view_h");
547 let mut scroll = egui::ScrollArea::vertical().id_salt(base.with("scroll")).auto_shrink([false, false]).max_height(body_rect.height());
548 if let Some(target) = scroll_target {
549 let state = egui::scroll_area::State::load(ui.ctx(), scroll_area_id(ui, base));
550 let offset = state.map_or(0.0, |s| s.offset.y as f64);
551 let view_h: f32 = ui.data(|d| d.get_temp(view_h_id)).unwrap_or(body_rect.height());
552 let (top, h) = layout.span(target);
553 if top < offset {
554 scroll = scroll.vertical_scroll_offset(top as f32);
555 } else if top + h > offset + view_h as f64 {
556 scroll = scroll.vertical_scroll_offset((top + h - view_h as f64).max(0.0) as f32);
557 }
558 }
559
560 let over = if list.fling { drive_fling(ui, base, &layout, body_rect, scroll_target.is_some(), &mut scroll) } else { 0.0 };
561
562 let mut clicked = None;
563 let mut double_clicked = None;
564 let mut secondary_clicked = None;
565 let mut visible = 0..0;
566 let mut body = ui.new_child(UiBuilder::new().max_rect(body_rect).id_salt("ashlar_list_body").accessibility_parent(base));
567 let out = scroll.show_viewport(&mut body, |ui, viewport| {
568 let total = layout.total().min(f32::MAX as f64 / 4.0) as f32;
569 ui.set_height(total);
570 ui.set_width(ui.available_width());
571 let origin = ui.max_rect().min - vec2(0.0, over);
573 let width = ui.max_rect().width();
574 visible = layout.visible(viewport.min.y as f64, viewport.max.y as f64, list.overscan);
575 let label_fn = list.row_label;
576 let list_id = list.id;
577 for i in visible.clone() {
578 let (top, h) = layout.span(i);
579 let rect = Rect::from_min_size(pos2(origin.x, origin.y + top as f32), vec2(width, h as f32));
580 if h <= 0.0 {
581 continue;
582 }
583 let rid = row_id(base, i);
584 let resp = ui.interact(rect, rid, Sense::click());
585 let selected = sel.as_ref().is_some_and(|s| s.contains(i)) || list.selected == Some(i) || list.is_selected.is_some_and(|f| f(i));
586 let hovered = resp.hovered();
587 if resp.clicked() || resp.secondary_clicked() {
588 ui.memory_mut(|m| m.request_focus(base));
589 }
590 if resp.clicked() {
591 clicked = Some(i);
592 if let Some(s) = sel.as_deref_mut() {
593 let m = ui.input(|inp| inp.modifiers);
594 if m.shift {
595 s.extend_to(i);
596 } else if m.command {
597 s.toggle(i);
598 } else {
599 s.select_only(i);
600 }
601 }
602 if cursor != Some(i) {
603 select = Some(i);
604 }
605 }
606 if accesskit {
607 ui.ctx().accesskit_node_builder(rid, |node| {
608 node.set_role(kind.item);
609 if let Some(f) = label_fn {
610 node.set_label(f(i));
611 }
612 node.set_selected(selected);
613 node.set_position_in_set(i.saturating_add(1));
614 node.set_size_of_set(rows);
615 });
616 }
617 if resp.double_clicked() {
618 double_clicked = Some(i);
619 activated = Some(i);
620 }
621 if resp.secondary_clicked() {
622 secondary_clicked = Some(i);
623 }
624 paint_row_background(ui, &t, rect, i, selected, hovered, list.stripes, focused);
625 let mut child =
626 ui.new_child(UiBuilder::new().max_rect(rect.shrink2(vec2(t.space.sm, 0.0))).id_salt(("row", i)).accessibility_parent(rid));
627 child.set_clip_rect(rect.intersect(ui.clip_rect()));
628 if selected {
629 child.visuals_mut().override_text_color = Some(t.selection_text);
630 }
631 row_fn(&mut child, Row { index: i, rect, selected, hovered });
632 if kind.rows {
633 let label = label_fn.map_or("", |f| f(i));
634 automation::register_fmt(
635 ui,
636 rect,
637 format_args!("{list_id}.row.{i}"),
638 label,
639 Role::Row,
640 true,
641 Details { value: None, selected: Some(selected) },
642 );
643 }
644 }
645 });
646 let view_h = out.inner_rect.height();
647 ui.data_mut(|d| d.insert_temp(view_h_id, view_h));
648 ui.data_mut(|d| d.insert_temp(base.with("scroll_id"), out.id));
650 if let Some(o) = offsets {
651 ui.data_mut(|d| d.insert_temp(offsets_id, o));
652 }
653 ui.advance_cursor_after_rect(outer);
654 if focused {
655 ui.painter().rect_stroke(outer, t.radius.sm, Stroke::new(1.0, t.focus.gamma_multiply(0.6)), StrokeKind::Inside);
656 }
657 automation::register_with(ui, &response, list.id, "", kind.role, Details::default());
658 ListResponse { response, clicked, double_clicked, secondary_clicked, select, activated, visible }
659}
660
661fn scroll_area_id(ui: &Ui, base: Id) -> Id {
664 ui.data(|d| d.get_temp::<Id>(base.with("scroll_id"))).unwrap_or_else(|| ui.make_persistent_id(base.with("scroll")))
665}
666
667#[derive(Clone, Copy, Debug, Default)]
669struct ListFling {
670 tracker: VelocityTracker<f32>,
671 raw: f32,
673 tracked: f32,
675 dragging: bool,
676 running: bool,
678 target: f32,
679 last: f32,
681}
682
683fn drive_fling(ui: &Ui, base: Id, layout: &Layout<'_>, body: Rect, scrolling_to: bool, scroll: &mut egui::ScrollArea) -> f32 {
687 let ctx = ui.ctx().clone();
688 let key = base.with("fling");
689 let anim = key.with("offset");
690 let view_h: f32 = ui.data(|d| d.get_temp(base.with("view_h"))).unwrap_or(body.height()).max(1.0);
691 let max = (layout.total().min(f32::MAX as f64 / 4.0) as f32 - view_h).max(0.0);
692 let scroll_id = scroll_area_id(ui, base);
693 let current = egui::scroll_area::State::load(&ctx, scroll_id).map_or(0.0, |s| s.offset.y);
694 *scroll = scroll.clone().scroll_source(egui::scroll_area::ScrollSource { drag: egui::scroll_area::DragScroll::Never, ..Default::default() });
696 let drag = ui.interact(body, key, Sense::drag());
697 ui.ctx().accesskit_node_builder(drag.id, |n| n.set_role(NodeRole::GenericContainer));
699 let now = ui.input(|i| i.time);
700 let mut f: ListFling = ui.data(|d| d.get_temp(key)).unwrap_or_default();
701 let band = |raw: f32| {
702 if raw < 0.0 {
703 rubber_band(raw, view_h)
704 } else if raw > max {
705 max + rubber_band(raw - max, view_h)
706 } else {
707 raw
708 }
709 };
710 let mut offset = None;
711 if drag.drag_started() {
712 f = ListFling { raw: current, tracked: current, dragging: true, last: current, ..ListFling::default() };
713 f.tracker.add(now, current);
714 }
715 if f.dragging {
716 f.raw -= drag.drag_delta().y;
717 let shown = band(f.raw);
718 if shown != f.tracked {
720 f.tracker.add(now, shown);
721 f.tracked = shown;
722 }
723 motion::set(&ctx, anim, shown);
724 offset = Some(shown);
725 if drag.drag_stopped() || !drag.dragged() {
726 f.dragging = false;
727 let reduced = motion::prefs(&ctx).reduced;
728 let v = if reduced { 0.0 } else { f.tracker.velocity_at(now) };
729 let target = project(shown, v, deceleration::NORMAL).clamp(0.0, max);
730 if target != shown {
731 let reach = (target - shown) * decay_k(deceleration::NORMAL);
734 let v = if v.signum() == reach.signum() { v.abs().min(reach.abs()).copysign(v) } else { 0.0 };
735 let curve = if reduced && v == 0.0 && (0.0..=max).contains(&shown) { Curve::Instant } else { Curve::Spring(mtokens::SETTLE.enter) };
736 motion::release(&ctx, anim, v, target, curve);
737 f.running = true;
738 f.target = target;
739 }
740 }
741 } else if f.running {
742 if scrolling_to || (current - f.last).abs() > 0.5 {
743 f.running = false;
745 } else {
746 let o = motion::value(&ctx, anim, f.target, Class::Gesture, mtokens::SETTLE.enter);
747 if o == f.target {
748 f.running = false;
749 }
750 offset = Some(o);
751 }
752 }
753 let mut over = 0.0;
754 if let Some(o) = offset.filter(|o| o.is_finite()) {
755 let inside = o.clamp(0.0, max);
756 *scroll = scroll.clone().vertical_scroll_offset(inside);
757 over = o - inside;
758 f.last = inside;
759 }
760 ui.data_mut(|d| d.insert_temp(key, f));
761 over
762}
763
764fn paint_row_background(ui: &Ui, t: &Tokens, rect: Rect, i: usize, selected: bool, hovered: bool, stripes: bool, focused: bool) {
765 let fill = if selected {
766 if focused { t.selection } else { t.selection.gamma_multiply(0.7) }
767 } else if hovered {
768 t.surface.hover
769 } else if stripes && i % 2 == 1 {
770 t.surface.row_alt
771 } else {
772 return;
773 };
774 ui.painter().rect_filled(rect, 0, fill);
775}
776
777#[derive(Clone, Copy, Debug)]
783pub struct Column<'a> {
784 pub title: &'a str,
785 pub width: f32,
787 pub min_width: f32,
788 pub resizable: bool,
789}
790
791impl<'a> Column<'a> {
792 pub fn new(title: &'a str, width: f32) -> Self {
793 Column { title, width, min_width: 24.0, resizable: true }
794 }
795}
796
797#[derive(Clone, Copy, Debug, PartialEq, Eq)]
799pub struct Sort {
800 pub column: usize,
801 pub ascending: bool,
802}
803
804pub struct TableResponse {
806 pub list: ListResponse,
807 pub header_clicked: Option<usize>,
809 pub widths: Vec<f32>,
811}
812
813pub struct VirtualTable<'a> {
815 list: VirtualList<'a>,
816 columns: &'a [Column<'a>],
817 sort: Option<Sort>,
818}
819
820pub const MAX_COLUMNS: usize = 256;
822
823impl<'a> VirtualTable<'a> {
824 pub fn new(id: &'a str, columns: &'a [Column<'a>], rows: usize, heights: RowHeights<'a>) -> Self {
825 VirtualTable {
826 list: VirtualList::new(id, rows, heights).stripes(true),
827 columns: columns.get(..columns.len().min(MAX_COLUMNS)).unwrap_or(&[]),
828 sort: None,
829 }
830 }
831 pub fn selected(mut self, row: Option<usize>) -> Self {
832 self.list = self.list.selected(row);
833 self
834 }
835 pub fn is_selected(mut self, f: &'a dyn Fn(usize) -> bool) -> Self {
836 self.list = self.list.is_selected(f);
837 self
838 }
839 pub fn sort(mut self, sort: Option<Sort>) -> Self {
840 self.sort = sort;
841 self
842 }
843 pub fn scroll_to(mut self, row: Option<usize>) -> Self {
844 self.list = self.list.scroll_to(row);
845 self
846 }
847 pub fn max_height(mut self, h: f32) -> Self {
848 self.list = self.list.max_height(h);
849 self
850 }
851 pub fn label(mut self, text: &'a str) -> Self {
853 self.list = self.list.label(text);
854 self
855 }
856 pub fn row_label(mut self, f: &'a dyn Fn(usize) -> &'a str) -> Self {
857 self.list = self.list.row_label(f);
858 self
859 }
860 pub fn changed_rows(mut self, rows: &'a [usize]) -> Self {
862 self.list = self.list.changed_rows(rows);
863 self
864 }
865
866 pub fn type_ahead(mut self, text: &'a dyn Fn(usize) -> &'a str) -> Self {
868 self.list = self.list.type_ahead(text);
869 self
870 }
871 pub fn selection(mut self, selection: &'a mut Selection) -> Self {
873 self.list = self.list.selection(selection);
874 self
875 }
876
877 pub fn show(mut self, ui: &mut Ui, mut cell: impl FnMut(&mut Ui, usize, usize)) -> TableResponse {
879 let t = tokens::current(ui.ctx());
880 let base = ui.make_persistent_id(self.list.id);
881 let widths_id = base.with("widths");
882 let mut stored: Vec<f32> = ui.data_mut(|d| d.remove_temp::<Vec<f32>>(widths_id)).unwrap_or_default();
883 if stored.len() != self.columns.len() {
884 stored = self.columns.iter().map(|c| sane_width(c.width, c.min_width)).collect();
885 }
886 let widths: Vec<std::cell::Cell<f32>> = stored.iter().map(|w| std::cell::Cell::new(*w)).collect();
888 let mut header_clicked = None;
889 let columns = self.columns;
890 let sort = self.sort;
891 let table_id = self.list.id;
892 let header_h = t.control_height;
893 let mut paint_header = |ui: &mut Ui, rect: Rect| {
894 ui.painter().rect_filled(rect, 0, t.surface.panel);
895 ui.painter().hline(rect.x_range(), rect.bottom() - 0.5, Stroke::new(1.0, t.separator));
896 let mut x = rect.left();
897 for (c, col) in columns.iter().enumerate() {
898 let w = widths.get(c).map_or(col.width, std::cell::Cell::get);
899 let cell = Rect::from_min_size(pos2(x, rect.top()), vec2(w, rect.height()));
900 let resp = ui.interact(cell, base.with(("header", c)), Sense::click());
901 if resp.hovered() {
902 ui.painter().rect_filled(cell, 0, t.surface.hover);
903 }
904 if resp.clicked() {
905 header_clicked = Some(c);
906 }
907 let font = Tokens::font(t.type_scale.small);
908 let galley =
909 crate::cache::galley(ui, (base, "header", c), crate::cache::hash_of(&(col.title, font.size.to_bits(), t.text.secondary)), || {
910 egui::text::LayoutJob::simple_singleline(col.title.to_owned(), font, t.text.secondary)
911 });
912 let text_pos = pos2(cell.left() + t.space.sm, cell.center().y - galley.size().y / 2.0);
913 let painter = ui.painter().with_clip_rect(cell.intersect(ui.clip_rect()));
914 painter.galley(text_pos, galley.clone(), t.text.secondary);
915 if let Some(s) = sort.filter(|s| s.column == c) {
916 let ax = (text_pos.x + galley.size().x + t.space.sm).min(cell.right() - 8.0);
917 let ay = cell.center().y;
918 let (a, b) = if s.ascending { (-3.0, 3.0) } else { (3.0, -3.0) };
919 painter.line_segment([pos2(ax - 3.5, ay + b * 0.5), pos2(ax, ay + a * 0.5 - 0.5)], Stroke::new(1.5, t.text.secondary));
920 painter.line_segment([pos2(ax, ay + a * 0.5 - 0.5), pos2(ax + 3.5, ay + b * 0.5)], Stroke::new(1.5, t.text.secondary));
921 }
922 resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, col.title));
923 ui.ctx().accesskit_node_builder(resp.id, |n| {
924 n.set_role(NodeRole::ColumnHeader);
925 n.set_column_index(c);
926 if let Some(s) = sort.filter(|s| s.column == c) {
927 n.set_sort_direction(if s.ascending { SortDirection::Ascending } else { SortDirection::Descending });
928 }
929 });
930 let sorted = sort.map(|s| s.column == c);
931 automation::register_fmt(
932 ui,
933 cell,
934 format_args!("{table_id}.header.{c}"),
935 col.title,
936 Role::Header,
937 true,
938 Details { value: None, selected: sorted },
939 );
940 if col.resizable {
942 let handle = Rect::from_center_size(pos2(cell.right(), cell.center().y), vec2(6.0, cell.height()));
943 let hr = ui.interact(handle, base.with(("resize", c)), Sense::drag());
944 ui.ctx().accesskit_node_builder(hr.id, |n| n.set_role(NodeRole::GenericContainer));
946 if hr.hovered() || hr.dragged() {
947 ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeColumn);
948 }
949 if hr.dragged()
950 && let Some(w) = widths.get(c)
951 {
952 w.set(sane_width(w.get() + hr.drag_delta().x, col.min_width));
953 }
954 }
955 ui.painter().vline(cell.right() - 0.5, rect.y_range().shrink(4.0), Stroke::new(1.0, t.separator));
956 x += w;
957 }
958 };
959 let list = show_rows(
960 ui,
961 base,
962 &mut self.list,
963 Kind { role: Role::Table, rows: true, node: NodeRole::Table, item: NodeRole::Row },
964 Some(Header { height: header_h, paint: &mut paint_header }),
965 |ui, row| {
966 let mut x = row.rect.left();
967 for (c, col) in columns.iter().enumerate() {
968 let w = widths.get(c).map_or(col.width, std::cell::Cell::get);
969 let rect = Rect::from_min_size(pos2(x, row.rect.top()), vec2(w, row.rect.height()));
970 let inner = rect.shrink2(vec2(t.space.sm, 0.0));
971 if inner.width() > 0.0 {
972 let mut cui =
973 ui.new_child(UiBuilder::new().max_rect(inner).layout(egui::Layout::left_to_right(egui::Align::Center)).id_salt(c));
974 cui.set_clip_rect(inner.intersect(ui.clip_rect()));
975 cui.ctx().accesskit_node_builder(cui.unique_id(), |n| {
976 n.set_role(NodeRole::Cell);
977 n.set_column_index(c);
978 n.set_row_index(row.index);
979 });
980 cell(&mut cui, row.index, c);
981 }
982 x += w;
983 }
984 },
985 );
986 for (dst, w) in stored.iter_mut().zip(&widths) {
987 *dst = w.get();
988 }
989 let out = stored.clone();
990 ui.data_mut(|d| d.insert_temp(widths_id, stored));
991 TableResponse { list, header_clicked, widths: out }
992 }
993}
994
995fn sane_width(w: f32, min: f32) -> f32 {
996 let min = if min.is_finite() { min.clamp(4.0, 2000.0) } else { 24.0 };
997 if w.is_finite() { w.clamp(min, 4000.0) } else { min }
998}
999
1000#[cfg(test)]
1001mod tests;