You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
postsack/ps-gui/src/widgets/table.rs

319 lines
11 KiB
Rust

//! This is a fork of `https://raw.githubusercontent.com/sagebind/smplinfo/master/src/ui/widgets/table.rs` with
//! some modifications.
use std::ops::Range;
use std::{fmt::Display, hash::Hash};
use eframe::egui::{vec2, Id, Response, ScrollArea, Sense, TextStyle, Ui, Vec2, Widget};
const DEFAULT_COLUMN_WIDTH: f32 = 200.0;
/// An ordinary table. Feature set is currently pretty limited.
///
/// - `R`: The data type of a single row displayed.
/// - `C`: The type of collection holding the rows to display. Any collection
/// implementing `AsRef<[R]>` can be used.
pub struct Table<
'selection,
R,
C: AsRef<[Option<R>]>,
RowMaker: FnMut(Range<usize>) -> C,
RowSelection: Fn(&Option<R>),
> {
id_source: Id,
columns: Vec<Column<R>>,
selected_row: Option<&'selection mut Option<usize>>,
header_height: f32,
row_height: f32,
cell_padding: Vec2,
row_maker: RowMaker,
num_rows: usize,
pub row_action: Option<RowSelection>,
}
/// Table column definition.
struct Column<R> {
name: String,
value_mapper: Box<dyn FnMut(&Option<R>) -> String>,
max_width: Option<f32>,
}
impl<
R,
C: AsRef<[Option<R>]>,
RowMaker: FnMut(Range<usize>) -> C,
RowSelection: Fn(&Option<R>),
> Table<'static, R, C, RowMaker, RowSelection>
{
#[allow(dead_code)]
pub fn new(id_source: impl Hash, num_rows: usize, row_maker: RowMaker) -> Self {
Self {
id_source: Id::new(id_source),
columns: Vec::new(),
selected_row: None,
header_height: 28.0,
row_height: 24.0,
cell_padding: vec2(8.0, 4.0),
row_maker,
num_rows,
row_action: None,
}
}
}
impl<
's,
R,
C: AsRef<[Option<R>]>,
RowMaker: FnMut(Range<usize>) -> C,
RowSelection: Fn(&Option<R>),
> Table<'s, R, C, RowMaker, RowSelection>
{
pub fn new_selectable(
id_source: impl Hash,
selected_row: &'s mut Option<usize>,
num_rows: usize,
selection: RowSelection,
row_maker: RowMaker,
) -> Self {
Self {
id_source: Id::new(id_source),
columns: Vec::new(),
selected_row: Some(selected_row),
header_height: 28.0,
row_height: 24.0,
cell_padding: vec2(8.0, 4.0),
row_maker,
num_rows,
row_action: Some(selection),
}
}
pub fn column(
mut self,
name: impl Display,
width: f32,
value_mapper: impl FnMut(&Option<R>) -> String + 'static,
) -> Self {
self.columns.push(Column {
name: name.to_string(),
value_mapper: Box::new(value_mapper),
max_width: Some(width),
});
self
}
#[allow(dead_code)]
fn supports_selection(&self) -> bool {
self.selected_row.is_some()
}
fn header_ui(&mut self, ui: &mut Ui, state: &mut State) {
let header_text_style = TextStyle::Body;
// Table always grows as wide as available, so the header should too.
let (_, rect) = ui.allocate_space(vec2(ui.available_width(), self.header_height));
// Header background
let painter = ui.painter_at(rect);
painter.rect_filled(
rect,
ui.visuals().widgets.inactive.corner_radius,
ui.visuals().widgets.inactive.bg_fill,
);
let mut column_offset = 0.0;
let column_len = self.columns.len();
for (i, column) in self.columns.iter().enumerate() {
let column_id = self.id_source.with("_column_").with(i);
let desired_column_width = state.column_width(i);
let mut column_rect = rect;
column_rect.min.x += column_offset;
let is_last = i == (column_len - 1);
if !is_last && column_rect.width() > desired_column_width {
column_rect.set_width(desired_column_width);
}
let response = ui.interact(column_rect, column_id, Sense::hover());
let color = if response.hovered() {
ui.style().visuals.widgets.hovered.fg_stroke.color
} else {
ui.style().visuals.widgets.inactive.fg_stroke.color
};
let galley = ui
.fonts()
.layout_no_wrap(column.name.clone(), header_text_style, color);
if response.hovered() {
ui.painter().rect_stroke(
column_rect,
ui.visuals().widgets.hovered.corner_radius,
ui.visuals().widgets.hovered.bg_stroke,
);
}
let mut text_pos = column_rect.left_center();
text_pos.x += self.cell_padding.x;
text_pos.y -= galley.size().y / 2.0;
ui.painter_at(column_rect).galley(text_pos, galley);
column_offset += column_rect.width();
}
}
}
impl<
's,
R: std::fmt::Debug,
C: AsRef<[Option<R>]>,
RowMaker: FnMut(Range<usize>) -> C,
RowAction: Fn(&Option<R>),
> Widget for Table<'s, R, C, RowMaker, RowAction>
{
fn ui(mut self, ui: &mut Ui) -> Response {
if self.columns.is_empty() {
panic!("uh, what do I do if no columns are defined?");
}
let mut state = ui
.memory()
.id_data_temp
.get_or_default::<State>(self.id_source)
.clone();
// set the sizes on the state
state.column_widths = self
.columns
.iter()
.map(|e| e.max_width.unwrap_or(DEFAULT_COLUMN_WIDTH))
.collect();
// First step: compute some sizes used during rendering. Since this is a
// homogenous table, we can figure out its exact sizes based on the
// number of rows and columns.
let table_rect = ui.available_rect_before_wrap();
let response = ui.interact(table_rect, self.id_source, Sense::hover());
self.header_ui(ui, &mut state);
// Now render the table body, which is inside an independently
// scrollable area.
ScrollArea::vertical().show_rows(ui, self.row_height, self.num_rows, |ui, row_range| {
ui.scope(|ui| {
let maker = &mut self.row_maker;
let rows = maker(row_range);
// When laying out the table, don't allocate any spacing between the
// rows.
ui.spacing_mut().item_spacing.y = 0.0;
// TODO: Decide row height more intelligently...
let row_size = vec2(ui.available_width(), self.row_height);
let cell_text_style = ui.style().body_text_style;
let column_len = self.columns.len();
for (row_idx, row) in rows.as_ref().iter().enumerate() {
let (row_id, row_rect) = ui.allocate_space(row_size);
let mut row_response = ui.interact(row_rect, row_id, Sense::click());
let mut cell_text_color = ui.style().visuals.text_color();
// If this row is currently selected, make it look like it is.
if self.selected_row == Some(&mut Some(row_idx)) {
cell_text_color = ui.visuals().strong_text_color();
ui.painter().rect_filled(
row_rect,
0.0,
ui.style().visuals.selection.bg_fill,
);
} else if row_response.hovered() {
ui.painter().rect_filled(
row_rect,
0.0,
ui.visuals().widgets.hovered.bg_fill,
);
} else if row_idx % 2 > 0 {
ui.painter()
.rect_filled(row_rect, 0.0, ui.visuals().faint_bg_color);
}
// Give the hovered mails a tooltip
if row_response.hovered() {
//let data = self.columns[row_id];
let mut hover_data = Vec::new();
for (_, column) in self.columns.iter_mut().enumerate() {
let cell_text = (column.value_mapper)(row);
hover_data.push(cell_text);
}
let hover_string = hover_data.join("\n");
row_response = row_response.on_hover_text(hover_string);
}
let mut column_offset = 0.0;
for (col_idx, column) in self.columns.iter_mut().enumerate() {
let desired_column_width = state.column_width(col_idx);
let cell_text = (column.value_mapper)(row);
let mut column_rect = row_rect;
column_rect.min.x += column_offset;
// Auto-expand the last column
let is_last = col_idx == (column_len - 1);
if !is_last && column_rect.width() > desired_column_width {
column_rect.set_width(desired_column_width);
}
let painter = ui.painter_at(column_rect);
let galley =
ui.fonts()
.layout_no_wrap(cell_text, cell_text_style, cell_text_color);
let mut text_pos = column_rect.left_center();
text_pos.x += self.cell_padding.x;
text_pos.y -= galley.size().y / 2.0;
painter.galley(text_pos, galley);
column_offset += column_rect.width();
}
if let Some(selected_row) = self.selected_row.as_mut() {
if row_response.clicked() {
if let Some(ref n) = self.row_action {
if let Some(a) = rows.as_ref().get(row_idx) {
n(a)
}
}
**selected_row = Some(row_idx);
}
}
}
});
});
response
}
}
/// Persistent table UI state.
#[derive(Clone, Default)]
struct State {
/// Current width of each column. This is updated when a column is resized.
column_widths: Vec<f32>,
}
impl State {
fn column_width(&self, column: usize) -> f32 {
self.column_widths
.get(column)
.cloned()
.unwrap_or(DEFAULT_COLUMN_WIDTH)
}
}