pub mod header;
pub use header::{MacSortDirection, MacTableColumn};
use crate::layout::subpixel_snap_rect;
use crate::theme::MacTheme;
use egui::{
CornerRadius, Id, Key, Layout, Modifiers, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui,
UiBuilder, Vec2,
};
use std::collections::BTreeSet;
use std::fmt::Debug;
use std::hash::Hash;
#[derive(Debug, Clone)]
pub struct MacTableViewResponse {
pub response: Response,
pub clicked_header: Option<usize>,
pub toggled_sort: Option<(usize, MacSortDirection)>,
pub clicked_row: Option<usize>,
pub double_clicked_row: Option<usize>,
pub selection_changed: bool,
}
pub type MacTableViewOutput = MacTableViewResponse;
pub struct MacTableView<'a> {
id: Id,
columns: &'a [MacTableColumn<'a>],
row_count: usize,
selection: &'a mut BTreeSet<usize>,
uses_alternating_row_backgrounds: bool,
row_height: Option<f32>,
enabled: bool,
theme: Option<&'a MacTheme>,
}
impl<'a> MacTableView<'a> {
pub fn new(
id_source: impl Hash + Debug,
columns: &'a [MacTableColumn<'a>],
row_count: usize,
selection: &'a mut BTreeSet<usize>,
) -> Self {
#[allow(deprecated)]
let id = Id::new(id_source);
Self {
id,
columns,
row_count,
selection,
uses_alternating_row_backgrounds: true,
row_height: None,
enabled: true,
theme: None,
}
}
pub fn uses_alternating_row_backgrounds(mut self, alternating: bool) -> Self {
self.uses_alternating_row_backgrounds = alternating;
self
}
pub fn row_height(mut self, height: f32) -> Self {
self.row_height = Some(height);
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn show(
self,
ui: &mut Ui,
mut cell_ui: impl FnMut(&mut Ui, usize, usize),
) -> MacTableViewResponse {
let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
let tm = theme.table_view();
let hdr_h = tm.header_height.points();
let row_h = self.row_height.unwrap_or_else(|| tm.row_height.points());
let pad_h = tm.cell_padding_horizontal.points();
let pad_v = tm.cell_padding_vertical.points();
let sep_w = tm.separator_width.points();
let outer_r = CornerRadius::same(tm.corner_radius.points() as u8);
let row_r = CornerRadius::same(tm.row_corner_radius.points() as u8);
let cols_w: f32 = self.columns.iter().map(|c| c.width).sum();
let total_w = cols_w.max(ui.available_width());
let total_h = hdr_h + (self.row_count as f32) * row_h;
let (raw_rect, _) = ui.allocate_exact_size(Vec2::new(total_w, total_h), Sense::hover());
let mut response = ui.interact(raw_rect, self.id, Sense::focusable_noninteractive());
let rect = subpixel_snap_rect(raw_rect, ui.ctx().pixels_per_point());
let painter = ui.painter_at(rect);
painter.rect_filled(rect, outer_r, theme.control_background());
let header_rect = Rect::from_min_size(rect.min, Vec2::new(rect.width(), hdr_h));
let (clicked_header, toggled_sort) = header::render_table_header(
ui,
self.id,
theme,
header_rect,
self.columns,
self.enabled,
);
let mut clicked_row = None;
let mut double_clicked_row = None;
let mut selection_changed = false;
let prev_sel_len = self.selection.len();
self.selection.retain(|&idx| idx < self.row_count);
if self.selection.len() != prev_sel_len {
selection_changed = true;
}
let anchor_id = self.id.with("selection_anchor");
let mut anchor = ui
.data(|d| d.get_temp::<usize>(anchor_id))
.filter(|&a| a < self.row_count)
.or_else(|| self.selection.iter().next().copied())
.unwrap_or(0);
let is_focused_or_hovered = response.has_focus()
|| ui.memory(|m| m.has_focus(self.id))
|| (ui.memory(|m| m.focused().is_none()) && ui.rect_contains_pointer(rect));
if self.enabled && self.row_count > 0 {
if is_focused_or_hovered
&& ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowDown))
{
let cur = self.selection.iter().next_back().copied().unwrap_or(0);
let next = (cur + 1).min(self.row_count - 1);
self.selection.clear();
self.selection.insert(next);
anchor = next;
selection_changed = true;
} else if is_focused_or_hovered
&& ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::ArrowUp))
{
let cur = self.selection.iter().next().copied().unwrap_or(0);
let prev = cur.saturating_sub(1);
self.selection.clear();
self.selection.insert(prev);
anchor = prev;
selection_changed = true;
} else if (is_focused_or_hovered || !self.selection.is_empty())
&& (ui.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Enter))
|| ui.input_mut(|i| i.consume_key(Modifiers::COMMAND, Key::ArrowDown))
|| ui.input_mut(|i| i.consume_key(Modifiers::CTRL, Key::ArrowDown)))
{
if let Some(&first_sel) = self.selection.iter().next() {
double_clicked_row = Some(first_sel);
}
}
}
let body_top = header_rect.bottom();
let clip_rect = ui.clip_rect();
let (first_row, last_row) = if row_h > 0.0 {
let start = ((clip_rect.top() - body_top) / row_h).floor().max(0.0) as usize;
let end = (((clip_rect.bottom() - body_top) / row_h).ceil().max(0.0) as usize)
.min(self.row_count);
let start = start.saturating_sub(1).min(self.row_count);
let end = (end + 1).clamp(start, self.row_count);
(start, end)
} else {
(0, self.row_count)
};
for row_idx in first_row..last_row {
let row_y = body_top + (row_idx as f32) * row_h;
let row_rect = Rect::from_min_size(
Pos2::new(rect.left(), row_y),
Vec2::new(rect.width(), row_h),
);
let row_id = self.id.with(("row", row_idx));
let row_sense = if self.enabled {
Sense::click()
} else {
Sense::hover()
};
let row_resp = ui.interact(row_rect, row_id, row_sense);
if row_resp.double_clicked() && self.enabled {
double_clicked_row = Some(row_idx);
}
if row_resp.clicked() && self.enabled {
clicked_row = Some(row_idx);
ui.memory_mut(|m| m.request_focus(response.id));
let mods = ui.input(|i| {
let mut m = i.modifiers;
for ev in &i.events {
if let egui::Event::PointerButton { modifiers, .. } = ev {
m |= *modifiers;
}
}
m
});
if mods.shift {
let lo = anchor.min(row_idx);
let hi = anchor.max(row_idx);
self.selection.clear();
for idx in lo..=hi {
self.selection.insert(idx);
}
} else if mods.command || mods.ctrl {
if !self.selection.remove(&row_idx) {
self.selection.insert(row_idx);
}
anchor = row_idx;
} else {
self.selection.clear();
self.selection.insert(row_idx);
anchor = row_idx;
}
selection_changed = true;
}
let is_selected = self.selection.contains(&row_idx);
let row_painter = ui.painter_at(row_rect);
if is_selected {
let sel_bg = if self.enabled {
theme.selected_control()
} else {
theme.selected_control_inactive()
};
row_painter.rect_filled(row_rect, row_r, sel_bg);
} else if row_resp.hovered() && self.enabled {
row_painter.rect_filled(row_rect, row_r, theme.control());
} else if self.uses_alternating_row_backgrounds {
let alt_bg = theme.alternating_content_background(row_idx);
row_painter.rect_filled(row_rect, 0, alt_bg);
}
let mut col_x = row_rect.left();
for (col_idx, col) in self.columns.iter().enumerate() {
let cell_rect = Rect::from_min_max(
Pos2::new(col_x + pad_h, row_rect.top() + pad_v),
Pos2::new(
(col_x + col.width - pad_h).max(col_x + pad_h),
row_rect.bottom() - pad_v,
),
);
let layout =
Layout::left_to_right(egui::Align::Center).with_main_align(col.alignment);
let mut child_ui = ui.new_child(
UiBuilder::new()
.max_rect(cell_rect)
.layout(layout)
.id_salt(("cell", row_idx, col_idx)),
);
child_ui.set_clip_rect(cell_rect.intersect(ui.clip_rect()));
if is_selected {
let fg = theme
.selected_control_background_style()
.primary_text_color(theme);
child_ui.visuals_mut().override_text_color = Some(fg);
}
cell_ui(&mut child_ui, row_idx, col_idx);
col_x += col.width;
}
}
let prev_anchor = ui.data(|d| d.get_temp::<usize>(anchor_id));
if prev_anchor != Some(anchor) {
ui.data_mut(|d| d.insert_temp(anchor_id, anchor));
}
ui.painter_at(rect).rect_stroke(
rect,
outer_r,
Stroke::new(sep_w, theme.separator()),
StrokeKind::Inside,
);
if selection_changed || clicked_header.is_some() {
response.mark_changed();
}
MacTableViewResponse {
response,
clicked_header,
toggled_sort,
clicked_row,
double_clicked_row,
selection_changed,
}
}
}