Browse Source

`egui_extras::Table` improvements (#2369)

* Use simple `ui.interact` for the resize line

* Introduce TableReizeState

* Simplify some code

* Add striped options to table demo

* Auto-size table columns by double-clicking the resize line

* Table: add option to auto-size the columns

* Table: don't let column width gets too small, unless clipping is on

* egui_extras: always use serde

Otherwise using `get_persisted` etc is impossible,
and working around that tedious.

* Avoid clipping last column in a resizable table

* Some better naming

* Table: Use new `Column` for setting column sizes and properties

Also make `clip` a per-column property

* All Table:s store state for auto-sizing purposes

* Customize each column wether or not it is resizable

* fix some auto-sizing bugs

* Fix shrinkage of adaptive column content

* Rename `scroll` to `vscroll` for clarity

* Add Table::scroll_to_row

* scroll_to_row takes alignment

* Fix bug in table sizing

* Strip: turn clipping OFF by default, because it is dangerous and sucks

* Add TableBody::mac_rect helper

* Table: add options to control the scroll area height.

* Docstring fixes

* Cleanup
pull/2379/head
Emil Ernerfeldt 2 years ago
committed by GitHub
parent
commit
2dc2a5540d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      crates/egui/src/containers/scroll_area.rs
  2. 4
      crates/egui_demo_app/Cargo.toml
  3. 125
      crates/egui_demo_lib/src/demo/table_demo.rs
  4. 8
      crates/egui_extras/Cargo.toml
  5. 3
      crates/egui_extras/src/datepicker/button.rs
  6. 15
      crates/egui_extras/src/datepicker/popup.rs
  7. 66
      crates/egui_extras/src/layout.rs
  8. 27
      crates/egui_extras/src/strip.rs
  9. 791
      crates/egui_extras/src/table.rs

18
crates/egui/src/containers/scroll_area.rs

@ -66,6 +66,10 @@ pub struct ScrollAreaOutput<R> {
/// The current state of the scroll area.
pub state: State,
/// The size of the content. If this is larger than [`Self::inner_rect`],
/// then there was need for scrolling.
pub content_size: Vec2,
/// Where on the screen the content is (excludes scroll bars).
pub inner_rect: Rect,
}
@ -198,6 +202,8 @@ impl ScrollArea {
/// Set the horizontal and vertical scroll offset position.
///
/// Positive offset means scrolling down/right.
///
/// See also: [`Self::vertical_scroll_offset`], [`Self::horizontal_scroll_offset`],
/// [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
@ -209,6 +215,8 @@ impl ScrollArea {
/// Set the vertical scroll offset position.
///
/// Positive offset means scrolling down.
///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
pub fn vertical_scroll_offset(mut self, offset: f32) -> Self {
@ -218,6 +226,8 @@ impl ScrollArea {
/// Set the horizontal scroll offset position.
///
/// Positive offset means scrolling right.
///
/// See also: [`Self::scroll_offset`], [`Ui::scroll_to_cursor`](crate::ui::Ui::scroll_to_cursor) and
/// [`Response::scroll_to_me`](crate::Response::scroll_to_me)
pub fn horizontal_scroll_offset(mut self, offset: f32) -> Self {
@ -541,18 +551,20 @@ impl ScrollArea {
let id = prepared.id;
let inner_rect = prepared.inner_rect;
let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
let state = prepared.end(ui);
let (content_size, state) = prepared.end(ui);
ScrollAreaOutput {
inner,
id,
state,
content_size,
inner_rect,
}
}
}
impl Prepared {
fn end(self, ui: &mut Ui) -> State {
/// Returns content size and state
fn end(self, ui: &mut Ui) -> (Vec2, State) {
let Prepared {
id,
mut state,
@ -847,7 +859,7 @@ impl Prepared {
state.store(ui.ctx(), id);
state
(content_size, state)
}
}

4
crates/egui_demo_app/Cargo.toml

@ -20,8 +20,8 @@ default = ["glow", "persistence"]
http = ["ehttp", "image", "poll-promise", "egui_extras/image"]
persistence = ["eframe/persistence", "egui/persistence", "serde"]
screen_reader = ["eframe/screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui_extras/serde", "egui/serde"]
screen_reader = ["eframe/screen_reader"] # experimental
serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntax_highlighting = ["egui_demo_lib/syntax_highlighting"]
glow = ["eframe/glow"]

125
crates/egui_demo_lib/src/demo/table_demo.rs

@ -10,20 +10,22 @@ enum DemoType {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct TableDemo {
demo: DemoType,
striped: bool,
resizable: bool,
num_rows: usize,
row_to_scroll_to: i32,
vertical_scroll_offset: Option<f32>,
scroll_to_row_slider: usize,
scroll_to_row: Option<usize>,
}
impl Default for TableDemo {
fn default() -> Self {
Self {
demo: DemoType::Manual,
striped: true,
resizable: true,
num_rows: 10_000,
row_to_scroll_to: 0,
vertical_scroll_offset: None,
scroll_to_row_slider: 0,
scroll_to_row: None,
}
}
}
@ -45,16 +47,15 @@ impl super::Demo for TableDemo {
}
}
fn scroll_offset_for_row(ui: &egui::Ui, row: i32) -> f32 {
let text_height = egui::TextStyle::Body.resolve(ui.style()).size;
let row_item_spacing = ui.spacing().item_spacing.y;
row as f32 * (text_height + row_item_spacing)
}
const NUM_MANUAL_ROWS: usize = 32;
impl super::View for TableDemo {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.vertical(|ui| {
ui.checkbox(&mut self.resizable, "Resizable columns");
ui.horizontal(|ui| {
ui.checkbox(&mut self.striped, "Striped");
ui.checkbox(&mut self.resizable, "Resizable columns");
});
ui.label("Table type:");
ui.radio_value(&mut self.demo, DemoType::Manual, "Few, manual rows");
@ -77,16 +78,20 @@ impl super::View for TableDemo {
);
}
if self.demo == DemoType::ManyHomogenous {
ui.add(
egui::Slider::new(&mut self.row_to_scroll_to, 0..=self.num_rows as i32)
{
let max_rows = if self.demo == DemoType::Manual {
NUM_MANUAL_ROWS
} else {
self.num_rows
};
let slider_response = ui.add(
egui::Slider::new(&mut self.scroll_to_row_slider, 0..=max_rows)
.logarithmic(true)
.text("Row to scroll to"),
);
if ui.button("Scroll to row").clicked() {
self.vertical_scroll_offset
.replace(scroll_offset_for_row(ui, self.row_to_scroll_to));
if slider_response.changed() {
self.scroll_to_row = Some(self.scroll_to_row_slider);
}
}
});
@ -113,37 +118,45 @@ impl super::View for TableDemo {
impl TableDemo {
fn table_ui(&mut self, ui: &mut egui::Ui) {
use egui_extras::{Size, TableBuilder};
use egui_extras::{Column, TableBuilder};
let text_height = egui::TextStyle::Body.resolve(ui.style()).size;
let mut table = TableBuilder::new(ui)
.striped(true)
.striped(self.striped)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Size::initial(60.0).at_least(40.0))
.column(Size::initial(60.0).at_least(40.0))
.column(Size::remainder().at_least(60.0))
.resizable(self.resizable);
if let Some(y_scroll) = self.vertical_scroll_offset.take() {
table = table.vertical_scroll_offset(y_scroll);
.column(Column::auto())
.column(Column::initial(100.0).range(40.0..=300.0).resizable(true))
.column(
Column::initial(100.0)
.at_least(40.0)
.resizable(true)
.clip(true),
)
.column(Column::remainder());
if let Some(row_nr) = self.scroll_to_row.take() {
table = table.scroll_to_row(row_nr, None);
}
table
.header(20.0, |mut header| {
header.col(|ui| {
ui.heading("Row");
ui.strong("Row");
});
header.col(|ui| {
ui.heading("Clock");
ui.strong("Expanding content");
});
header.col(|ui| {
ui.heading("Content");
ui.strong("Clipped text");
});
header.col(|ui| {
ui.strong("Content");
});
})
.body(|mut body| match self.demo {
DemoType::Manual => {
for row_index in 0..20 {
for row_index in 0..NUM_MANUAL_ROWS {
let is_thick = thick_row(row_index);
let row_height = if is_thick { 30.0 } else { 18.0 };
body.row(row_height, |mut row| {
@ -151,7 +164,10 @@ impl TableDemo {
ui.label(row_index.to_string());
});
row.col(|ui| {
ui.label(clock_emoji(row_index));
expanding_content(ui);
});
row.col(|ui| {
ui.label(long_text(row_index));
});
row.col(|ui| {
ui.style_mut().wrap = Some(false);
@ -170,7 +186,10 @@ impl TableDemo {
ui.label(row_index.to_string());
});
row.col(|ui| {
ui.label(clock_emoji(row_index));
expanding_content(ui);
});
row.col(|ui| {
ui.label(long_text(row_index));
});
row.col(|ui| {
ui.add(
@ -191,24 +210,21 @@ impl TableDemo {
(0..self.num_rows).into_iter().map(row_thickness),
|row_index, mut row| {
row.col(|ui| {
ui.centered_and_justified(|ui| {
ui.label(row_index.to_string());
});
ui.label(row_index.to_string());
});
row.col(|ui| {
ui.centered_and_justified(|ui| {
ui.label(clock_emoji(row_index));
});
expanding_content(ui);
});
row.col(|ui| {
ui.centered_and_justified(|ui| {
ui.style_mut().wrap = Some(false);
if thick_row(row_index) {
ui.heading("Extra thick row");
} else {
ui.label("Normal row");
}
});
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");
}
});
},
);
@ -217,10 +233,19 @@ impl TableDemo {
}
}
fn clock_emoji(row_index: usize) -> String {
char::from_u32(0x1f550 + row_index as u32 % 24)
.unwrap()
.to_string()
fn expanding_content(ui: &mut egui::Ui) {
let width = ui.available_width().clamp(20.0, 200.0);
let height = ui.available_height();
let (rect, _response) = ui.allocate_exact_size(egui::vec2(width, height), egui::Sense::hover());
ui.painter().hline(
rect.x_range(),
rect.center().y,
(1.0, ui.visuals().text_color()),
);
}
fn long_text(row_index: usize) -> String {
format!("Row {row_index} has some long text that you may want to clip, or it will take up too much horizontal space!")
}
fn thick_row(row_index: usize) -> bool {

8
crates/egui_extras/Cargo.toml

@ -29,9 +29,6 @@ default = []
## Enable [`DatePickerButton`] widget.
datepicker = ["chrono"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde"]
## Support loading svg images.
svg = ["resvg", "tiny-skia", "usvg"]
@ -42,6 +39,8 @@ tracing = ["dep:tracing", "egui/tracing"]
[dependencies]
egui = { version = "0.19.0", path = "../egui", default-features = false }
serde = { version = "1", features = ["derive"] }
#! ### Optional dependencies
# Date operations needed for datepicker widget
@ -63,9 +62,6 @@ resvg = { version = "0.23", optional = true }
tiny-skia = { version = "0.6", optional = true } # must be updated in lock-step with resvg
usvg = { version = "0.23", optional = true }
# feature "serde":
serde = { version = "1", features = ["derive"], optional = true }
# feature "tracing"
tracing = { version = "0.1", optional = true, default-features = false, features = [
"std",

3
crates/egui_extras/src/datepicker/button.rs

@ -2,8 +2,7 @@ use super::popup::DatePickerPopup;
use chrono::{Date, Utc};
use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget};
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
pub(crate) struct DatePickerButtonState {
pub picker_visible: bool,
}

15
crates/egui_extras/src/datepicker/popup.rs

@ -1,10 +1,12 @@
use super::{button::DatePickerButtonState, month_data};
use crate::{Size, StripBuilder, TableBuilder};
use chrono::{Date, Datelike, NaiveDate, Utc, Weekday};
use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2};
#[derive(Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
use super::{button::DatePickerButtonState, month_data};
use crate::{Column, Size, StripBuilder, TableBuilder};
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
struct DatePickerPopupState {
year: i32,
month: u32,
@ -243,9 +245,8 @@ impl<'a> DatePickerPopup<'a> {
strip.cell(|ui| {
ui.spacing_mut().item_spacing = Vec2::new(1.0, 2.0);
TableBuilder::new(ui)
.scroll(false)
.clip(false)
.columns(Size::remainder(), if self.calendar_week { 8 } else { 7 })
.vscroll(false)
.columns(Column::remainder(), if self.calendar_week { 8 } else { 7 })
.header(height, |mut header| {
if self.calendar_week {
header.col(|ui| {

66
crates/egui_extras/src/layout.rs

@ -31,19 +31,15 @@ pub struct StripLayout<'l> {
pub(crate) ui: &'l mut Ui,
direction: CellDirection,
pub(crate) rect: Rect,
cursor: Pos2,
pub(crate) cursor: Pos2,
/// Keeps track of the max used position,
/// so we know how much space we used.
max: Pos2,
pub(crate) clip: bool,
cell_layout: egui::Layout,
}
impl<'l> StripLayout<'l> {
pub(crate) fn new(
ui: &'l mut Ui,
direction: CellDirection,
clip: bool,
cell_layout: egui::Layout,
) -> Self {
pub(crate) fn new(ui: &'l mut Ui, direction: CellDirection, cell_layout: egui::Layout) -> Self {
let rect = ui.available_rect_before_wrap();
let pos = rect.left_top();
@ -53,7 +49,6 @@ impl<'l> StripLayout<'l> {
rect,
cursor: pos,
max: pos,
clip,
cell_layout,
}
}
@ -92,34 +87,41 @@ impl<'l> StripLayout<'l> {
self.set_pos(self.cell_rect(&width, &height));
}
/// This is the innermost part of [`crate::Table`] and [`crate::Strip`].
///
/// Return the used space (`min_rect`) plus the [`Response`] of the whole cell.
pub(crate) fn add(
&mut self,
clip: bool,
striped: bool,
width: CellSize,
height: CellSize,
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let rect = self.cell_rect(&width, &height);
let used_rect = self.cell(rect, add_contents);
self.set_pos(rect);
self.ui.allocate_rect(rect.union(used_rect), Sense::hover())
}
add_cell_contents: impl FnOnce(&mut Ui),
) -> (Rect, Response) {
let max_rect = self.cell_rect(&width, &height);
pub(crate) fn add_striped(
&mut self,
width: CellSize,
height: CellSize,
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let 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);
self.ui
.painter()
.rect_filled(stripe_rect, 0.0, self.ui.visuals().faint_bg_color);
}
let used_rect = self.cell(clip, max_rect, add_cell_contents);
self.set_pos(max_rect);
// Make sure we don't have a gap in the stripe background:
let rect = rect.expand2(0.5 * self.ui.spacing().item_spacing);
let allocation_rect = if clip {
max_rect
} else {
max_rect.union(used_rect)
};
self.ui
.painter()
.rect_filled(rect, 0.0, self.ui.visuals().faint_bg_color);
let response = self.ui.allocate_rect(allocation_rect, Sense::hover());
self.add(width, height, add_contents)
(used_rect, response)
}
/// only needed for layouts with multiple lines, like [`Table`](crate::Table).
@ -144,17 +146,17 @@ impl<'l> StripLayout<'l> {
self.ui.allocate_rect(rect, Sense::hover());
}
fn cell(&mut self, rect: Rect, add_contents: impl FnOnce(&mut Ui)) -> Rect {
fn cell(&mut self, clip: bool, rect: Rect, add_cell_contents: impl FnOnce(&mut Ui)) -> Rect {
let mut child_ui = self.ui.child_ui(rect, self.cell_layout);
if self.clip {
if 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);
child_ui.set_clip_rect(clip_rect.intersect(child_ui.clip_rect()));
}
add_contents(&mut child_ui);
add_cell_contents(&mut child_ui);
child_ui.min_rect()
}

27
crates/egui_extras/src/strip.rs

@ -56,11 +56,11 @@ impl<'a> StripBuilder<'a> {
ui,
sizing: Default::default(),
cell_layout,
clip: true,
clip: false,
}
}
/// Should we clip the contents of each cell? Default: `true`.
/// Should we clip the contents of each cell? Default: `false`.
pub fn clip(mut self, clip: bool) -> Self {
self.clip = clip;
self
@ -98,15 +98,11 @@ 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.clip,
self.cell_layout,
);
let mut layout = StripLayout::new(self.ui, CellDirection::Horizontal, self.cell_layout);
strip(Strip {
layout: &mut layout,
direction: CellDirection::Horizontal,
clip: self.clip,
sizes: widths,
size_index: 0,
});
@ -125,15 +121,11 @@ 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.clip,
self.cell_layout,
);
let mut layout = StripLayout::new(self.ui, CellDirection::Vertical, self.cell_layout);
strip(Strip {
layout: &mut layout,
direction: CellDirection::Vertical,
clip: self.clip,
sizes: heights,
size_index: 0,
});
@ -146,6 +138,7 @@ impl<'a> StripBuilder<'a> {
pub struct Strip<'a, 'b> {
layout: &'b mut StripLayout<'a>,
direction: CellDirection,
clip: bool,
sizes: Vec<f32>,
size_index: usize,
}
@ -172,7 +165,9 @@ impl<'a, 'b> Strip<'a, 'b> {
/// Add cell contents.
pub fn cell(&mut self, add_contents: impl FnOnce(&mut Ui)) {
let (width, height) = self.next_cell_size();
self.layout.add(width, height, add_contents);
let striped = false;
self.layout
.add(self.clip, striped, width, height, add_contents);
}
/// Add an empty cell.
@ -183,7 +178,7 @@ impl<'a, 'b> Strip<'a, 'b> {
/// Add a strip as cell.
pub fn strip(&mut self, strip_builder: impl FnOnce(StripBuilder<'_>)) {
let clip = self.layout.clip;
let clip = self.clip;
self.cell(|ui| {
strip_builder(StripBuilder::new(ui).clip(clip));
});

791
crates/egui_extras/src/table.rs

File diff suppressed because it is too large
Loading…
Cancel
Save