Implement table row selection and hover highlighting (#3347)
* Based on #3105 by @vvv. ## Additions and Changes - Add `TableBuilder::sense()` and `StripBuilder::sense()` to enable detecting clicks or drags on table and strip cells. - Add `TableRow::select()` which takes a boolean that sets the highlight state for all cells added after a call to it. This allows highlighting an entire row or specific cells. - Add `TableRow::response()` which returns the union of the `Response` of all cells added to the row up to that point. This makes it easy to detect interactions with an entire row. See below for an alternative design. - Add `TableRow::index()` and `TableRow::col_index()` helpers. - Remove explicit `row_index` from callback passed to `TableBody::rows()` and `TableBody::heterogeneous_rows()`, possible due to the above. This is a breaking change but makes the callback compatible with `TableBody::row()`. - Update Table example to demonstrate all of the above. ## Design Decisions An alternative design to `TableRow::response()` would be to return the row response from `TableBody`s `row()`, `rows()` and `heterogeneous_rows()` functions. `row()` could just return the response. `rows()` and `heterogeneous_rows()` could return a tuple of the hovered row index and that rows response. I feel like this might be the cleaner soluction if only returning the hovered rows response isn't too limiting. I didn't implement `TableBuilder::select_rows()` as described [here](https://github.com/emilk/egui/pull/3105#issuecomment-1618062533) because it requires an immutable borrow of the selection state for the lifetime of the `TableBuilder`. This makes updating the selection state from within the body unnecessarily complicated. Additionally the current design allows for selecting specific cells, though that could be possible by modifying `TableBuilder::select_rows()` to provide row and column indices like below. ```rust pub fn select_cells(is_selected: impl Fn(usize, usize) -> bool) -> Self ``` ## Hover Highlighting EDIT: Thanks to @samitbasu we now have hover highlighting too. ~This is not implemented yet. Ideally we'd have an api that allows to choose between highlighting the hovered cell, column or row. Should cells containing interactive widgets, be highlighted when hovering over the widget or only when hovering over the cell itself? I'd like to implement that before this gets merged though.~ Feedback is more than welcome. I'd be happy to make any changes necessary to get this merged. * Closes #1519 * Closes #1553 * Closes #3069 --------- Co-authored-by: Samit Basu <basu.samit@gmail.com> Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
37762f72ff
commit
5a6d1cbd58
|
|
@ -12,9 +12,12 @@ pub struct TableDemo {
|
|||
demo: DemoType,
|
||||
striped: bool,
|
||||
resizable: bool,
|
||||
clickable: bool,
|
||||
num_rows: usize,
|
||||
scroll_to_row_slider: usize,
|
||||
scroll_to_row: Option<usize>,
|
||||
selection: std::collections::HashSet<usize>,
|
||||
checked: bool,
|
||||
}
|
||||
|
||||
impl Default for TableDemo {
|
||||
|
|
@ -23,9 +26,12 @@ impl Default for TableDemo {
|
|||
demo: DemoType::Manual,
|
||||
striped: true,
|
||||
resizable: true,
|
||||
clickable: true,
|
||||
num_rows: 10_000,
|
||||
scroll_to_row_slider: 0,
|
||||
scroll_to_row: None,
|
||||
selection: Default::default(),
|
||||
checked: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +60,7 @@ impl super::View for TableDemo {
|
|||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut self.striped, "Striped");
|
||||
ui.checkbox(&mut self.resizable, "Resizable columns");
|
||||
ui.checkbox(&mut self.clickable, "Clickable rows");
|
||||
});
|
||||
|
||||
ui.label("Table type:");
|
||||
|
|
@ -121,20 +128,28 @@ impl TableDemo {
|
|||
fn table_ui(&mut self, ui: &mut egui::Ui) {
|
||||
use egui_extras::{Column, TableBuilder};
|
||||
|
||||
let text_height = egui::TextStyle::Body.resolve(ui.style()).size;
|
||||
let text_height = egui::TextStyle::Body
|
||||
.resolve(ui.style())
|
||||
.size
|
||||
.max(ui.spacing().interact_size.y);
|
||||
|
||||
let mut table = TableBuilder::new(ui)
|
||||
.striped(self.striped)
|
||||
.resizable(self.resizable)
|
||||
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
|
||||
.column(Column::auto())
|
||||
.column(Column::auto())
|
||||
.column(Column::initial(100.0).range(40.0..=300.0))
|
||||
.column(Column::initial(100.0).at_least(40.0).clip(true))
|
||||
.column(Column::remainder())
|
||||
.min_scrolled_height(0.0);
|
||||
|
||||
if let Some(row_nr) = self.scroll_to_row.take() {
|
||||
table = table.scroll_to_row(row_nr, None);
|
||||
if self.clickable {
|
||||
table = table.sense(egui::Sense::click());
|
||||
}
|
||||
|
||||
if let Some(row_index) = self.scroll_to_row.take() {
|
||||
table = table.scroll_to_row(row_index, None);
|
||||
}
|
||||
|
||||
table
|
||||
|
|
@ -142,6 +157,9 @@ impl TableDemo {
|
|||
header.col(|ui| {
|
||||
ui.strong("Row");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Interaction");
|
||||
});
|
||||
header.col(|ui| {
|
||||
ui.strong("Expanding content");
|
||||
});
|
||||
|
|
@ -158,9 +176,14 @@ impl TableDemo {
|
|||
let is_thick = thick_row(row_index);
|
||||
let row_height = if is_thick { 30.0 } else { 18.0 };
|
||||
body.row(row_height, |mut row| {
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
|
|
@ -175,14 +198,22 @@ impl TableDemo {
|
|||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
|
||||
self.toggle_row_selection(row_index, &row.response());
|
||||
});
|
||||
}
|
||||
}
|
||||
DemoType::ManyHomogeneous => {
|
||||
body.rows(text_height, self.num_rows, |row_index, mut row| {
|
||||
body.rows(text_height, self.num_rows, |mut row| {
|
||||
let row_index = row.index();
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
|
|
@ -194,41 +225,52 @@ impl TableDemo {
|
|||
egui::Label::new("Thousands of rows of even height").wrap(false),
|
||||
);
|
||||
});
|
||||
|
||||
self.toggle_row_selection(row_index, &row.response());
|
||||
});
|
||||
}
|
||||
DemoType::ManyHeterogenous => {
|
||||
fn row_thickness(row_index: usize) -> f32 {
|
||||
if thick_row(row_index) {
|
||||
30.0
|
||||
} else {
|
||||
18.0
|
||||
}
|
||||
}
|
||||
body.heterogeneous_rows(
|
||||
(0..self.num_rows).map(row_thickness),
|
||||
|row_index, mut row| {
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if thick_row(row_index) {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
let row_height = |i: usize| if thick_row(i) { 30.0 } else { 18.0 };
|
||||
body.heterogeneous_rows((0..self.num_rows).map(row_height), |mut row| {
|
||||
let row_index = row.index();
|
||||
row.set_selected(self.selection.contains(&row_index));
|
||||
|
||||
row.col(|ui| {
|
||||
ui.label(row_index.to_string());
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.checkbox(&mut self.checked, "Click me");
|
||||
});
|
||||
row.col(|ui| {
|
||||
expanding_content(ui);
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.label(long_text(row_index));
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.style_mut().wrap = Some(false);
|
||||
if thick_row(row_index) {
|
||||
ui.heading("Extra thick row");
|
||||
} else {
|
||||
ui.label("Normal row");
|
||||
}
|
||||
});
|
||||
|
||||
self.toggle_row_selection(row_index, &row.response());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_row_selection(&mut self, row_index: usize, row_response: &egui::Response) {
|
||||
if row_response.clicked() {
|
||||
if self.selection.contains(&row_index) {
|
||||
self.selection.remove(&row_index);
|
||||
} else {
|
||||
self.selection.insert(row_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expanding_content(ui: &mut egui::Ui) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@ pub(crate) enum CellDirection {
|
|||
Vertical,
|
||||
}
|
||||
|
||||
/// Flags used by [`StripLayout::add`].
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub(crate) struct StripLayoutFlags {
|
||||
pub(crate) clip: bool,
|
||||
pub(crate) striped: bool,
|
||||
pub(crate) hovered: bool,
|
||||
pub(crate) selected: bool,
|
||||
}
|
||||
|
||||
/// Positions cells in [`CellDirection`] and starts a new line on [`StripLayout::end_line`]
|
||||
pub struct StripLayout<'l> {
|
||||
pub(crate) ui: &'l mut Ui,
|
||||
|
|
@ -38,10 +47,16 @@ pub struct StripLayout<'l> {
|
|||
max: Pos2,
|
||||
|
||||
cell_layout: egui::Layout,
|
||||
sense: Sense,
|
||||
}
|
||||
|
||||
impl<'l> StripLayout<'l> {
|
||||
pub(crate) fn new(ui: &'l mut Ui, direction: CellDirection, cell_layout: egui::Layout) -> Self {
|
||||
pub(crate) fn new(
|
||||
ui: &'l mut Ui,
|
||||
direction: CellDirection,
|
||||
cell_layout: egui::Layout,
|
||||
sense: Sense,
|
||||
) -> Self {
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let pos = rect.left_top();
|
||||
|
||||
|
|
@ -52,6 +67,7 @@ impl<'l> StripLayout<'l> {
|
|||
cursor: pos,
|
||||
max: pos,
|
||||
cell_layout,
|
||||
sense,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,34 +110,53 @@ impl<'l> StripLayout<'l> {
|
|||
/// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
|
||||
pub(crate) fn add(
|
||||
&mut self,
|
||||
clip: bool,
|
||||
striped: bool,
|
||||
flags: StripLayoutFlags,
|
||||
width: CellSize,
|
||||
height: CellSize,
|
||||
add_cell_contents: impl FnOnce(&mut Ui),
|
||||
) -> (Rect, Response) {
|
||||
let max_rect = self.cell_rect(&width, &height);
|
||||
|
||||
if striped {
|
||||
// Make sure we don't have a gap in the stripe background:
|
||||
let stripe_rect = max_rect.expand2(0.5 * self.ui.spacing().item_spacing);
|
||||
// Make sure we don't have a gap in the stripe/frame/selection background:
|
||||
let item_spacing = self.ui.spacing().item_spacing;
|
||||
let gapless_rect = max_rect.expand2(0.5 * item_spacing);
|
||||
|
||||
self.ui
|
||||
.painter()
|
||||
.rect_filled(stripe_rect, 0.0, self.ui.visuals().faint_bg_color);
|
||||
if flags.striped {
|
||||
self.ui.painter().rect_filled(
|
||||
gapless_rect,
|
||||
egui::Rounding::ZERO,
|
||||
self.ui.visuals().faint_bg_color,
|
||||
);
|
||||
}
|
||||
|
||||
let used_rect = self.cell(clip, max_rect, add_cell_contents);
|
||||
if flags.selected {
|
||||
self.ui.painter().rect_filled(
|
||||
gapless_rect,
|
||||
egui::Rounding::ZERO,
|
||||
self.ui.visuals().selection.bg_fill,
|
||||
);
|
||||
}
|
||||
|
||||
if flags.hovered && !flags.selected && self.sense.interactive() {
|
||||
self.ui.painter().rect_filled(
|
||||
gapless_rect,
|
||||
egui::Rounding::ZERO,
|
||||
self.ui.visuals().widgets.hovered.bg_fill,
|
||||
);
|
||||
}
|
||||
|
||||
let response = self.ui.allocate_rect(max_rect, self.sense);
|
||||
let used_rect = self.cell(flags, max_rect, add_cell_contents);
|
||||
|
||||
self.set_pos(max_rect);
|
||||
|
||||
let allocation_rect = if clip {
|
||||
let allocation_rect = if flags.clip {
|
||||
max_rect
|
||||
} else {
|
||||
max_rect.union(used_rect)
|
||||
};
|
||||
|
||||
let response = self.ui.allocate_rect(allocation_rect, Sense::hover());
|
||||
let response = response.with_new_rect(allocation_rect);
|
||||
|
||||
(used_rect, response)
|
||||
}
|
||||
|
|
@ -148,10 +183,15 @@ impl<'l> StripLayout<'l> {
|
|||
self.ui.allocate_rect(rect, Sense::hover());
|
||||
}
|
||||
|
||||
fn cell(&mut self, clip: bool, rect: Rect, add_cell_contents: impl FnOnce(&mut Ui)) -> Rect {
|
||||
fn cell(
|
||||
&mut self,
|
||||
flags: StripLayoutFlags,
|
||||
rect: Rect,
|
||||
add_cell_contents: impl FnOnce(&mut Ui),
|
||||
) -> Rect {
|
||||
let mut child_ui = self.ui.child_ui(rect, self.cell_layout);
|
||||
|
||||
if clip {
|
||||
if flags.clip {
|
||||
let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);
|
||||
let margin = margin.min(0.5 * self.ui.spacing().item_spacing);
|
||||
let clip_rect = rect.expand2(margin);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
layout::{CellDirection, CellSize, StripLayout},
|
||||
layout::{CellDirection, CellSize, StripLayout, StripLayoutFlags},
|
||||
sizing::Sizing,
|
||||
Size,
|
||||
};
|
||||
|
|
@ -46,6 +46,7 @@ pub struct StripBuilder<'a> {
|
|||
sizing: Sizing,
|
||||
clip: bool,
|
||||
cell_layout: egui::Layout,
|
||||
sense: egui::Sense,
|
||||
}
|
||||
|
||||
impl<'a> StripBuilder<'a> {
|
||||
|
|
@ -55,8 +56,9 @@ impl<'a> StripBuilder<'a> {
|
|||
Self {
|
||||
ui,
|
||||
sizing: Default::default(),
|
||||
cell_layout,
|
||||
clip: false,
|
||||
cell_layout,
|
||||
sense: egui::Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +76,13 @@ impl<'a> StripBuilder<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// What should strip cells sense for? Default: [`egui::Sense::hover()`].
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: egui::Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
|
||||
/// Allocate space for one column/row.
|
||||
#[inline]
|
||||
pub fn size(mut self, size: Size) -> Self {
|
||||
|
|
@ -102,7 +111,12 @@ impl<'a> StripBuilder<'a> {
|
|||
self.ui.available_rect_before_wrap().width(),
|
||||
self.ui.spacing().item_spacing.x,
|
||||
);
|
||||
let mut layout = StripLayout::new(self.ui, CellDirection::Horizontal, self.cell_layout);
|
||||
let mut layout = StripLayout::new(
|
||||
self.ui,
|
||||
CellDirection::Horizontal,
|
||||
self.cell_layout,
|
||||
self.sense,
|
||||
);
|
||||
strip(Strip {
|
||||
layout: &mut layout,
|
||||
direction: CellDirection::Horizontal,
|
||||
|
|
@ -125,7 +139,12 @@ impl<'a> StripBuilder<'a> {
|
|||
self.ui.available_rect_before_wrap().height(),
|
||||
self.ui.spacing().item_spacing.y,
|
||||
);
|
||||
let mut layout = StripLayout::new(self.ui, CellDirection::Vertical, self.cell_layout);
|
||||
let mut layout = StripLayout::new(
|
||||
self.ui,
|
||||
CellDirection::Vertical,
|
||||
self.cell_layout,
|
||||
self.sense,
|
||||
);
|
||||
strip(Strip {
|
||||
layout: &mut layout,
|
||||
direction: CellDirection::Vertical,
|
||||
|
|
@ -171,9 +190,11 @@ impl<'a, 'b> Strip<'a, 'b> {
|
|||
#[cfg_attr(debug_assertions, track_caller)]
|
||||
pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) {
|
||||
let (width, height) = self.next_cell_size();
|
||||
let striped = false;
|
||||
self.layout
|
||||
.add(self.clip, striped, width, height, add_contents);
|
||||
let flags = StripLayoutFlags {
|
||||
clip: self.clip,
|
||||
..Default::default()
|
||||
};
|
||||
self.layout.add(flags, width, height, add_contents);
|
||||
}
|
||||
|
||||
/// Add an empty cell.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
use egui::{Align, NumExt as _, Rangef, Rect, Response, ScrollArea, Ui, Vec2, Vec2b};
|
||||
|
||||
use crate::{
|
||||
layout::{CellDirection, CellSize},
|
||||
layout::{CellDirection, CellSize, StripLayoutFlags},
|
||||
StripLayout,
|
||||
};
|
||||
|
||||
|
|
@ -231,6 +231,7 @@ pub struct TableBuilder<'a> {
|
|||
resizable: bool,
|
||||
cell_layout: egui::Layout,
|
||||
scroll_options: TableScrollOptions,
|
||||
sense: egui::Sense,
|
||||
}
|
||||
|
||||
impl<'a> TableBuilder<'a> {
|
||||
|
|
@ -243,6 +244,7 @@ impl<'a> TableBuilder<'a> {
|
|||
resizable: false,
|
||||
cell_layout,
|
||||
scroll_options: Default::default(),
|
||||
sense: egui::Sense::hover(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +257,13 @@ impl<'a> TableBuilder<'a> {
|
|||
self
|
||||
}
|
||||
|
||||
/// What should table cells sense for? (default: [`egui::Sense::hover()`]).
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: egui::Sense) -> Self {
|
||||
self.sense = sense;
|
||||
self
|
||||
}
|
||||
|
||||
/// Make the columns resizable by dragging.
|
||||
///
|
||||
/// You can set this for individual columns with [`Column::resizable`].
|
||||
|
|
@ -398,6 +407,7 @@ impl<'a> TableBuilder<'a> {
|
|||
resizable,
|
||||
cell_layout,
|
||||
scroll_options,
|
||||
sense,
|
||||
} = self;
|
||||
|
||||
let striped = striped.unwrap_or(ui.visuals().striped);
|
||||
|
|
@ -415,15 +425,20 @@ impl<'a> TableBuilder<'a> {
|
|||
|
||||
// Hide first-frame-jitters when auto-sizing.
|
||||
ui.add_visible_ui(!first_frame_auto_size_columns, |ui| {
|
||||
let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout);
|
||||
let mut layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense);
|
||||
let mut response: Option<Response> = None;
|
||||
add_header_row(TableRow {
|
||||
layout: &mut layout,
|
||||
columns: &columns,
|
||||
widths: &state.column_widths,
|
||||
max_used_widths: &mut max_used_widths,
|
||||
row_index: 0,
|
||||
col_index: 0,
|
||||
striped: false,
|
||||
height,
|
||||
striped: false,
|
||||
hovered: false,
|
||||
selected: false,
|
||||
response: &mut response,
|
||||
});
|
||||
layout.allocate_rect();
|
||||
});
|
||||
|
|
@ -441,6 +456,7 @@ impl<'a> TableBuilder<'a> {
|
|||
striped,
|
||||
cell_layout,
|
||||
scroll_options,
|
||||
sense,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -458,6 +474,7 @@ impl<'a> TableBuilder<'a> {
|
|||
resizable,
|
||||
cell_layout,
|
||||
scroll_options,
|
||||
sense,
|
||||
} = self;
|
||||
|
||||
let striped = striped.unwrap_or(ui.visuals().striped);
|
||||
|
|
@ -486,6 +503,7 @@ impl<'a> TableBuilder<'a> {
|
|||
striped,
|
||||
cell_layout,
|
||||
scroll_options,
|
||||
sense,
|
||||
}
|
||||
.body(add_body_contents);
|
||||
}
|
||||
|
|
@ -546,6 +564,8 @@ pub struct Table<'a> {
|
|||
cell_layout: egui::Layout,
|
||||
|
||||
scroll_options: TableScrollOptions,
|
||||
|
||||
sense: egui::Sense,
|
||||
}
|
||||
|
||||
impl<'a> Table<'a> {
|
||||
|
|
@ -574,6 +594,7 @@ impl<'a> Table<'a> {
|
|||
striped,
|
||||
cell_layout,
|
||||
scroll_options,
|
||||
sense,
|
||||
} = self;
|
||||
|
||||
let TableScrollOptions {
|
||||
|
|
@ -612,7 +633,14 @@ impl<'a> Table<'a> {
|
|||
|
||||
// Hide first-frame-jitters when auto-sizing.
|
||||
ui.add_visible_ui(!first_frame_auto_size_columns, |ui| {
|
||||
let layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout);
|
||||
let hovered_row_index_id = self.state_id.with("__table_hovered_row");
|
||||
let hovered_row_index = ui.memory_mut(|w| {
|
||||
let hovered_row = w.data.get_temp(hovered_row_index_id);
|
||||
w.data.remove::<usize>(hovered_row_index_id);
|
||||
hovered_row
|
||||
});
|
||||
|
||||
let layout = StripLayout::new(ui, CellDirection::Horizontal, cell_layout, sense);
|
||||
|
||||
add_body_contents(TableBody {
|
||||
layout,
|
||||
|
|
@ -620,11 +648,13 @@ impl<'a> Table<'a> {
|
|||
widths: widths_ref,
|
||||
max_used_widths: max_used_widths_ref,
|
||||
striped,
|
||||
row_nr: 0,
|
||||
row_index: 0,
|
||||
start_y: clip_rect.top(),
|
||||
end_y: clip_rect.bottom(),
|
||||
scroll_to_row: scroll_to_row.map(|(r, _)| r),
|
||||
scroll_to_y_range: &mut scroll_to_y_range,
|
||||
hovered_row_index,
|
||||
hovered_row_index_id,
|
||||
});
|
||||
|
||||
if scroll_to_row.is_some() && scroll_to_y_range.is_none() {
|
||||
|
|
@ -755,7 +785,7 @@ pub struct TableBody<'a> {
|
|||
max_used_widths: &'a mut [f32],
|
||||
|
||||
striped: bool,
|
||||
row_nr: usize,
|
||||
row_index: usize,
|
||||
start_y: f32,
|
||||
end_y: f32,
|
||||
|
||||
|
|
@ -765,6 +795,11 @@ pub struct TableBody<'a> {
|
|||
/// If we find the correct row to scroll to,
|
||||
/// this is set to the y-range of the row.
|
||||
scroll_to_y_range: &'a mut Option<Rangef>,
|
||||
|
||||
hovered_row_index: Option<usize>,
|
||||
|
||||
/// Used to store the hovered row index between frames.
|
||||
hovered_row_index_id: egui::Id,
|
||||
}
|
||||
|
||||
impl<'a> TableBody<'a> {
|
||||
|
|
@ -800,23 +835,29 @@ impl<'a> TableBody<'a> {
|
|||
/// ⚠️ It is much more performant to use [`Self::rows`] or [`Self::heterogeneous_rows`],
|
||||
/// as those functions will only render the visible rows.
|
||||
pub fn row(&mut self, height: f32, add_row_content: impl FnOnce(TableRow<'a, '_>)) {
|
||||
let mut response: Option<Response> = None;
|
||||
let top_y = self.layout.cursor.y;
|
||||
add_row_content(TableRow {
|
||||
layout: &mut self.layout,
|
||||
columns: self.columns,
|
||||
widths: self.widths,
|
||||
max_used_widths: self.max_used_widths,
|
||||
row_index: self.row_index,
|
||||
col_index: 0,
|
||||
striped: self.striped && self.row_nr % 2 == 0,
|
||||
height,
|
||||
striped: self.striped && self.row_index % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(self.row_index),
|
||||
selected: false,
|
||||
response: &mut response,
|
||||
});
|
||||
self.capture_hover_state(&response, self.row_index);
|
||||
let bottom_y = self.layout.cursor.y;
|
||||
|
||||
if Some(self.row_nr) == self.scroll_to_row {
|
||||
if Some(self.row_index) == self.scroll_to_row {
|
||||
*self.scroll_to_y_range = Some(Rangef::new(top_y, bottom_y));
|
||||
}
|
||||
|
||||
self.row_nr += 1;
|
||||
self.row_index += 1;
|
||||
}
|
||||
|
||||
/// Add many rows with same height.
|
||||
|
|
@ -834,9 +875,10 @@ impl<'a> TableBody<'a> {
|
|||
/// .body(|mut body| {
|
||||
/// let row_height = 18.0;
|
||||
/// let num_rows = 10_000;
|
||||
/// body.rows(row_height, num_rows, |row_index, mut row| {
|
||||
/// body.rows(row_height, num_rows, |mut row| {
|
||||
/// let row_index = row.index();
|
||||
/// row.col(|ui| {
|
||||
/// ui.label("First column");
|
||||
/// ui.label(format!("First column of row {row_index}"));
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
|
|
@ -846,7 +888,7 @@ impl<'a> TableBody<'a> {
|
|||
mut self,
|
||||
row_height_sans_spacing: f32,
|
||||
total_rows: usize,
|
||||
mut add_row_content: impl FnMut(usize, TableRow<'_, '_>),
|
||||
mut add_row_content: impl FnMut(TableRow<'_, '_>),
|
||||
) {
|
||||
let spacing = self.layout.ui.spacing().item_spacing;
|
||||
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
|
||||
|
|
@ -874,19 +916,22 @@ impl<'a> TableBody<'a> {
|
|||
((scroll_offset_y + max_height) / row_height_with_spacing).ceil() as usize + 1;
|
||||
let max_row = max_row.min(total_rows);
|
||||
|
||||
for idx in min_row..max_row {
|
||||
add_row_content(
|
||||
idx,
|
||||
TableRow {
|
||||
layout: &mut self.layout,
|
||||
columns: self.columns,
|
||||
widths: self.widths,
|
||||
max_used_widths: self.max_used_widths,
|
||||
col_index: 0,
|
||||
striped: self.striped && (idx + self.row_nr) % 2 == 0,
|
||||
height: row_height_sans_spacing,
|
||||
},
|
||||
);
|
||||
for row_index in min_row..max_row {
|
||||
let mut response: Option<Response> = None;
|
||||
add_row_content(TableRow {
|
||||
layout: &mut self.layout,
|
||||
columns: self.columns,
|
||||
widths: self.widths,
|
||||
max_used_widths: self.max_used_widths,
|
||||
row_index,
|
||||
col_index: 0,
|
||||
height: row_height_sans_spacing,
|
||||
striped: self.striped && (row_index + self.row_index) % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(row_index),
|
||||
selected: false,
|
||||
response: &mut response,
|
||||
});
|
||||
self.capture_hover_state(&response, row_index);
|
||||
}
|
||||
|
||||
if total_rows - max_row > 0 {
|
||||
|
|
@ -911,7 +956,8 @@ impl<'a> TableBody<'a> {
|
|||
/// .column(Column::remainder().at_least(100.0))
|
||||
/// .body(|mut body| {
|
||||
/// let row_heights: Vec<f32> = vec![60.0, 18.0, 31.0, 240.0];
|
||||
/// body.heterogeneous_rows(row_heights.into_iter(), |row_index, mut row| {
|
||||
/// body.heterogeneous_rows(row_heights.into_iter(), |mut row| {
|
||||
/// let row_index = row.index();
|
||||
/// let thick = row_index % 6 == 0;
|
||||
/// row.col(|ui| {
|
||||
/// ui.centered_and_justified(|ui| {
|
||||
|
|
@ -925,7 +971,7 @@ impl<'a> TableBody<'a> {
|
|||
pub fn heterogeneous_rows(
|
||||
mut self,
|
||||
heights: impl Iterator<Item = f32>,
|
||||
mut add_row_content: impl FnMut(usize, TableRow<'_, '_>),
|
||||
mut add_row_content: impl FnMut(TableRow<'_, '_>),
|
||||
) {
|
||||
let spacing = self.layout.ui.spacing().item_spacing;
|
||||
let mut enumerated_heights = heights.enumerate();
|
||||
|
|
@ -952,19 +998,21 @@ impl<'a> TableBody<'a> {
|
|||
if cursor_y >= scroll_offset_y {
|
||||
// This row is visible:
|
||||
self.add_buffer(old_cursor_y as f32); // skip all the invisible rows
|
||||
|
||||
add_row_content(
|
||||
let mut response: Option<Response> = None;
|
||||
add_row_content(TableRow {
|
||||
layout: &mut self.layout,
|
||||
columns: self.columns,
|
||||
widths: self.widths,
|
||||
max_used_widths: self.max_used_widths,
|
||||
row_index,
|
||||
TableRow {
|
||||
layout: &mut self.layout,
|
||||
columns: self.columns,
|
||||
widths: self.widths,
|
||||
max_used_widths: self.max_used_widths,
|
||||
col_index: 0,
|
||||
striped: self.striped && (row_index + self.row_nr) % 2 == 0,
|
||||
height: row_height,
|
||||
},
|
||||
);
|
||||
col_index: 0,
|
||||
height: row_height,
|
||||
striped: self.striped && (row_index + self.row_index) % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(row_index),
|
||||
selected: false,
|
||||
response: &mut response,
|
||||
});
|
||||
self.capture_hover_state(&response, row_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -972,18 +1020,21 @@ impl<'a> TableBody<'a> {
|
|||
// populate visible rows:
|
||||
for (row_index, row_height) in &mut enumerated_heights {
|
||||
let top_y = cursor_y;
|
||||
add_row_content(
|
||||
let mut response: Option<Response> = None;
|
||||
add_row_content(TableRow {
|
||||
layout: &mut self.layout,
|
||||
columns: self.columns,
|
||||
widths: self.widths,
|
||||
max_used_widths: self.max_used_widths,
|
||||
row_index,
|
||||
TableRow {
|
||||
layout: &mut self.layout,
|
||||
columns: self.columns,
|
||||
widths: self.widths,
|
||||
max_used_widths: self.max_used_widths,
|
||||
col_index: 0,
|
||||
striped: self.striped && (row_index + self.row_nr) % 2 == 0,
|
||||
height: row_height,
|
||||
},
|
||||
);
|
||||
col_index: 0,
|
||||
height: row_height,
|
||||
striped: self.striped && (row_index + self.row_index) % 2 == 0,
|
||||
hovered: self.hovered_row_index == Some(row_index),
|
||||
selected: false,
|
||||
response: &mut response,
|
||||
});
|
||||
self.capture_hover_state(&response, row_index);
|
||||
cursor_y += (row_height + spacing.y) as f64;
|
||||
|
||||
if Some(row_index) == self.scroll_to_row {
|
||||
|
|
@ -1031,6 +1082,17 @@ impl<'a> TableBody<'a> {
|
|||
fn add_buffer(&mut self, height: f32) {
|
||||
self.layout.skip_space(egui::vec2(0.0, height));
|
||||
}
|
||||
|
||||
// Capture the hover information for the just created row. This is used in the next render
|
||||
// to ensure that the entire row is highlighted.
|
||||
fn capture_hover_state(&mut self, response: &Option<Response>, row_index: usize) {
|
||||
let is_row_hovered = response.as_ref().map_or(false, |r| r.hovered());
|
||||
if is_row_hovered {
|
||||
self.layout
|
||||
.ui
|
||||
.memory_mut(|w| w.data.insert_temp(self.hovered_row_index_id, row_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for TableBody<'a> {
|
||||
|
|
@ -1049,15 +1111,21 @@ pub struct TableRow<'a, 'b> {
|
|||
/// grows during building with the maximum widths
|
||||
max_used_widths: &'b mut [f32],
|
||||
|
||||
row_index: usize,
|
||||
col_index: usize,
|
||||
striped: bool,
|
||||
height: f32,
|
||||
|
||||
striped: bool,
|
||||
hovered: bool,
|
||||
selected: bool,
|
||||
|
||||
response: &'b mut Option<Response>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> TableRow<'a, 'b> {
|
||||
/// Add the contents of a column.
|
||||
///
|
||||
/// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
|
||||
/// Returns the used space (`min_rect`) plus the [`Response`] of the whole cell.
|
||||
#[cfg_attr(debug_assertions, track_caller)]
|
||||
pub fn col(&mut self, add_cell_contents: impl FnOnce(&mut Ui)) -> (Rect, Response) {
|
||||
let col_index = self.col_index;
|
||||
|
|
@ -1078,19 +1146,58 @@ impl<'a, 'b> TableRow<'a, 'b> {
|
|||
let width = CellSize::Absolute(width);
|
||||
let height = CellSize::Absolute(self.height);
|
||||
|
||||
let (used_rect, response) =
|
||||
self.layout
|
||||
.add(clip, self.striped, width, height, add_cell_contents);
|
||||
let flags = StripLayoutFlags {
|
||||
clip,
|
||||
striped: self.striped,
|
||||
hovered: self.hovered,
|
||||
selected: self.selected,
|
||||
};
|
||||
|
||||
let (used_rect, response) = self.layout.add(flags, width, height, add_cell_contents);
|
||||
|
||||
if let Some(max_w) = self.max_used_widths.get_mut(col_index) {
|
||||
*max_w = max_w.max(used_rect.width());
|
||||
}
|
||||
|
||||
*self.response = Some(
|
||||
self.response
|
||||
.as_ref()
|
||||
.map_or(response.clone(), |r| r.union(response.clone())),
|
||||
);
|
||||
|
||||
(used_rect, response)
|
||||
}
|
||||
|
||||
/// Set the selection highlight state for cells added after a call to this function.
|
||||
#[inline]
|
||||
pub fn set_selected(&mut self, selected: bool) {
|
||||
self.selected = selected;
|
||||
}
|
||||
|
||||
/// Returns a union of the [`Response`]s of the cells added to the row up to this point.
|
||||
///
|
||||
/// You need to add at least one row to the table before calling this function.
|
||||
pub fn response(&self) -> Response {
|
||||
self.response
|
||||
.clone()
|
||||
.expect("Should only be called after `col`")
|
||||
}
|
||||
|
||||
/// Returns the index of the row.
|
||||
#[inline]
|
||||
pub fn index(&self) -> usize {
|
||||
self.row_index
|
||||
}
|
||||
|
||||
/// Returns the index of the column. Incremented after a column is added.
|
||||
#[inline]
|
||||
pub fn col_index(&self) -> usize {
|
||||
self.col_index
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Drop for TableRow<'a, 'b> {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
self.layout.end_line();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue