1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 22:00:03 -04:00

Table resize (#1438)

* Let 1D strips fill up parent width/height
* Add Strip + Table + DatePicker to egui_extras changelog
* Expose some dragging- and pointer related context/memory methods
* Make tables resizable
This commit is contained in:
Emil Ernerfeldt
2022-04-01 12:01:00 +02:00
committed by GitHub
parent a52bbade45
commit 21c32a18d8
12 changed files with 323 additions and 159 deletions

View File

@@ -56,7 +56,7 @@ impl<'a> DatePickerPopup<'a> {
ui.spacing_mut().item_spacing = Vec2::splat(spacing);
StripBuilder::new(ui)
.sizes(
Size::Absolute(height),
Size::exact(height),
match (self.combo_boxes, self.arrows) {
(true, true) => 2,
(true, false) | (false, true) => 1,
@@ -64,14 +64,14 @@ impl<'a> DatePickerPopup<'a> {
},
)
.sizes(
Size::Absolute((spacing + height) * (weeks.len() + 1) as f32),
Size::exact((spacing + height) * (weeks.len() + 1) as f32),
if self.calendar { 1 } else { 0 },
)
.size(Size::Absolute(height))
.size(Size::exact(height))
.vertical(|mut strip| {
if self.combo_boxes {
strip.strip_clip(|builder| {
builder.sizes(Size::Remainder, 3).horizontal(|mut strip| {
builder.sizes(Size::remainder(), 3).horizontal(|mut strip| {
strip.cell(|ui| {
ComboBox::from_id_source("date_picker_year")
.selected_text(popup_state.year.to_string())
@@ -138,7 +138,7 @@ impl<'a> DatePickerPopup<'a> {
if self.arrows {
strip.strip(|builder| {
builder.sizes(Size::Remainder, 6).horizontal(|mut strip| {
builder.sizes(Size::remainder(), 6).horizontal(|mut strip| {
strip.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {
if ui
@@ -236,7 +236,7 @@ impl<'a> DatePickerPopup<'a> {
ui.spacing_mut().item_spacing = Vec2::new(1.0, 2.0);
TableBuilder::new(ui)
.scroll(false)
.columns(Size::Remainder, if self.calendar_week { 8 } else { 7 })
.columns(Size::remainder(), if self.calendar_week { 8 } else { 7 })
.header(height, |mut header| {
if self.calendar_week {
header.col(|ui| {
@@ -322,7 +322,7 @@ impl<'a> DatePickerPopup<'a> {
}
strip.strip(|builder| {
builder.sizes(Size::Remainder, 3).horizontal(|mut strip| {
builder.sizes(Size::remainder(), 3).horizontal(|mut strip| {
strip.empty();
strip.cell(|ui| {
ui.with_layout(Layout::top_down_justified(Align::Center), |ui| {

View File

@@ -99,10 +99,9 @@ impl<'l> StripLayout<'l> {
add_contents: impl FnOnce(&mut Ui),
) -> Response {
let rect = self.cell_rect(&width, &height);
self.cell(rect, clip, add_contents);
let used_rect = self.cell(rect, clip, add_contents);
self.set_pos(rect);
self.ui.allocate_rect(rect, Sense::click())
self.ui.allocate_rect(rect.union(used_rect), Sense::hover())
}
pub(crate) fn add_striped(
@@ -138,7 +137,7 @@ impl<'l> StripLayout<'l> {
}
}
fn cell(&mut self, rect: Rect, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
fn cell(&mut self, rect: Rect, clip: bool, add_contents: impl FnOnce(&mut Ui)) -> Rect {
let mut child_ui = self.ui.child_ui(rect, *self.ui.layout());
if clip {
@@ -149,6 +148,7 @@ impl<'l> StripLayout<'l> {
}
add_contents(&mut child_ui);
child_ui.min_rect()
}
/// Allocate the rect in [`Self::ui`] so that the scrollview knows about our size

View File

@@ -1,26 +1,84 @@
/// Size hint for table column/strip cell
/// Size hint for table column/strip cell.
#[derive(Clone, Debug, Copy)]
pub enum Size {
/// Absolute size in points
Absolute(f32),
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
Relative(f32),
/// [`Size::Relative`] with a minimum size in points
RelativeMinimum {
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`
relative: f32,
/// Absolute minimum size in points
minimum: f32,
},
/// Multiple remainders each get the same space
Remainder,
/// [`Size::Remainder`] with a minimum size in points
RemainderMinimum(f32),
/// Absolute size in points, with a given range of allowed sizes to resize within.
Absolute { initial: f32, range: (f32, f32) },
/// Relative size relative to all available space.
Relative { fraction: f32, range: (f32, f32) },
/// Multiple remainders each get the same space.
Remainder { range: (f32, f32) },
}
impl Size {
/// Exactly this big, with no room for resize.
pub fn exact(points: f32) -> Self {
Self::Absolute {
initial: points,
range: (points, points),
}
}
/// Initially this big, but can resize.
pub fn initial(points: f32) -> Self {
Self::Absolute {
initial: points,
range: (0.0, f32::INFINITY),
}
}
/// Relative size relative to all available space. Values must be in range `0.0..=1.0`.
pub fn relative(fraction: f32) -> Self {
egui::egui_assert!(0.0 <= fraction && fraction <= 1.0);
Self::Relative {
fraction,
range: (0.0, f32::INFINITY),
}
}
/// Multiple remainders each get the same space.
pub fn remainder() -> Self {
Self::Remainder {
range: (0.0, f32::INFINITY),
}
}
/// Won't shrink below this size (in points).
pub fn at_least(mut self, minimum: f32) -> Self {
match &mut self {
Self::Absolute { range, .. }
| Self::Relative { range, .. }
| Self::Remainder { range, .. } => {
range.0 = minimum;
}
}
self
}
/// Won't grow above this size (in points).
pub fn at_most(mut self, maximum: f32) -> Self {
match &mut self {
Self::Absolute { range, .. }
| Self::Relative { range, .. }
| Self::Remainder { range, .. } => {
range.1 = maximum;
}
}
self
}
/// Allowed range of movement (in points), if in a resizable [`Table`].
pub fn range(self) -> (f32, f32) {
match self {
Self::Absolute { range, .. }
| Self::Relative { range, .. }
| Self::Remainder { range, .. } => range,
}
}
}
#[derive(Clone)]
pub struct Sizing {
sizes: Vec<Size>,
pub(crate) sizes: Vec<Size>,
}
impl Sizing {
@@ -32,24 +90,21 @@ impl Sizing {
self.sizes.push(size);
}
pub fn into_lengths(self, length: f32, spacing: f32) -> Vec<f32> {
pub fn to_lengths(&self, length: f32, spacing: f32) -> Vec<f32> {
let mut remainders = 0;
let sum_non_remainder = self
.sizes
.iter()
.map(|size| match size {
Size::Absolute(absolute) => *absolute,
Size::Relative(relative) => {
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
length * relative
.map(|&size| match size {
Size::Absolute { initial, .. } => initial,
Size::Relative {
fraction,
range: (min, max),
} => {
assert!(0.0 <= fraction && fraction <= 1.0);
(length * fraction).clamp(min, max)
}
Size::RelativeMinimum { relative, minimum } => {
assert!(*relative > 0.0, "Below 0.0 is not allowed.");
assert!(*relative <= 1.0, "Above 1.0 is not allowed.");
minimum.max(length * relative)
}
Size::Remainder | Size::RemainderMinimum(..) => {
Size::Remainder { .. } => {
remainders += 1;
0.0
}
@@ -62,10 +117,10 @@ impl Sizing {
} else {
let mut remainder_length = length - sum_non_remainder;
let avg_remainder_length = 0.0f32.max(remainder_length / remainders as f32).floor();
self.sizes.iter().for_each(|size| {
if let Size::RemainderMinimum(minimum) = size {
if *minimum > avg_remainder_length {
remainder_length -= minimum;
self.sizes.iter().for_each(|&size| {
if let Size::Remainder { range: (min, _max) } = size {
if avg_remainder_length < min {
remainder_length -= min;
remainders -= 1;
}
}
@@ -78,13 +133,14 @@ impl Sizing {
};
self.sizes
.into_iter()
.map(|size| match size {
Size::Absolute(absolute) => absolute,
Size::Relative(relative) => length * relative,
Size::RelativeMinimum { relative, minimum } => minimum.max(length * relative),
Size::Remainder => avg_remainder_length,
Size::RemainderMinimum(minimum) => minimum.max(avg_remainder_length),
.iter()
.map(|&size| match size {
Size::Absolute { initial, .. } => initial,
Size::Relative {
fraction,
range: (min, max),
} => (length * fraction).clamp(min, max),
Size::Remainder { range: (min, max) } => avg_remainder_length.clamp(min, max),
})
.collect()
}
@@ -98,23 +154,16 @@ impl From<Vec<Size>> for Sizing {
#[test]
fn test_sizing() {
let sizing: Sizing = vec![Size::RemainderMinimum(20.0), Size::Remainder].into();
assert_eq!(sizing.clone().into_lengths(50.0, 0.0), vec![25.0, 25.0]);
assert_eq!(sizing.clone().into_lengths(30.0, 0.0), vec![20.0, 10.0]);
assert_eq!(sizing.clone().into_lengths(20.0, 0.0), vec![20.0, 0.0]);
assert_eq!(sizing.clone().into_lengths(10.0, 0.0), vec![20.0, 0.0]);
assert_eq!(sizing.into_lengths(20.0, 10.0), vec![20.0, 0.0]);
let sizing: Sizing = vec![Size::remainder().at_least(20.0), Size::remainder()].into();
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 25.0]);
assert_eq!(sizing.to_lengths(30.0, 0.0), vec![20.0, 10.0]);
assert_eq!(sizing.to_lengths(20.0, 0.0), vec![20.0, 0.0]);
assert_eq!(sizing.to_lengths(10.0, 0.0), vec![20.0, 0.0]);
assert_eq!(sizing.to_lengths(20.0, 10.0), vec![20.0, 0.0]);
let sizing: Sizing = vec![
Size::RelativeMinimum {
relative: 0.5,
minimum: 10.0,
},
Size::Absolute(10.0),
]
.into();
assert_eq!(sizing.clone().into_lengths(50.0, 0.0), vec![25.0, 10.0]);
assert_eq!(sizing.clone().into_lengths(30.0, 0.0), vec![15.0, 10.0]);
assert_eq!(sizing.clone().into_lengths(20.0, 0.0), vec![10.0, 10.0]);
assert_eq!(sizing.into_lengths(10.0, 0.0), vec![10.0, 10.0]);
let sizing: Sizing = vec![Size::relative(0.5).at_least(10.0), Size::exact(10.0)].into();
assert_eq!(sizing.to_lengths(50.0, 0.0), vec![25.0, 10.0]);
assert_eq!(sizing.to_lengths(30.0, 0.0), vec![15.0, 10.0]);
assert_eq!(sizing.to_lengths(20.0, 0.0), vec![10.0, 10.0]);
assert_eq!(sizing.to_lengths(10.0, 0.0), vec![10.0, 10.0]);
}

View File

@@ -18,11 +18,11 @@ use egui::{Response, Ui};
/// # egui::__run_test_ui(|ui| {
/// use egui_extras::{StripBuilder, Size};
/// StripBuilder::new(ui)
/// .size(Size::RemainderMinimum(100.0))
/// .size(Size::Absolute(40.0))
/// .size(Size::remainder().at_least(100.0))
/// .size(Size::exact(40.0))
/// .vertical(|mut strip| {
/// strip.strip(|builder| {
/// builder.sizes(Size::Remainder, 2).horizontal(|mut strip| {
/// builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
/// strip.cell(|ui| {
/// ui.label("Top Left");
/// });
@@ -72,7 +72,7 @@ impl<'a> StripBuilder<'a> {
where
F: for<'b> FnOnce(Strip<'a, 'b>),
{
let widths = self.sizing.into_lengths(
let widths = self.sizing.to_lengths(
self.ui.available_rect_before_wrap().width() - self.ui.spacing().item_spacing.x,
self.ui.spacing().item_spacing.x,
);
@@ -93,7 +93,7 @@ impl<'a> StripBuilder<'a> {
where
F: for<'b> FnOnce(Strip<'a, 'b>),
{
let heights = self.sizing.into_lengths(
let heights = self.sizing.to_lengths(
self.ui.available_rect_before_wrap().height() - self.ui.spacing().item_spacing.y,
self.ui.spacing().item_spacing.y,
);
@@ -117,6 +117,10 @@ pub struct Strip<'a, 'b> {
impl<'a, 'b> Strip<'a, 'b> {
fn next_cell_size(&mut self) -> (CellSize, CellSize) {
assert!(
!self.sizes.is_empty(),
"Tried using more strip cells than available."
);
let size = self.sizes[0];
self.sizes = &self.sizes[1..];
@@ -128,21 +132,11 @@ impl<'a, 'b> Strip<'a, 'b> {
/// Add empty cell
pub fn empty(&mut self) {
assert!(
!self.sizes.is_empty(),
"Tried using more strip cells than available."
);
let (width, height) = self.next_cell_size();
self.layout.empty(width, height);
}
fn cell_impl(&mut self, clip: bool, add_contents: impl FnOnce(&mut Ui)) {
assert!(
!self.sizes.is_empty(),
"Tried using more strip cells than available."
);
let (width, height) = self.next_cell_size();
self.layout.add(width, height, clip, add_contents);
}

View File

@@ -10,7 +10,6 @@ use crate::{
};
use egui::{Response, Ui};
use std::cmp;
/// Builder for a [`Table`] with (optional) fixed header and scrolling body.
///
@@ -26,8 +25,8 @@ use std::cmp;
/// # egui::__run_test_ui(|ui| {
/// use egui_extras::{TableBuilder, Size};
/// TableBuilder::new(ui)
/// .column(Size::RemainderMinimum(100.0))
/// .column(Size::Absolute(40.0))
/// .column(Size::remainder().at_least(100.0))
/// .column(Size::exact(40.0))
/// .header(20.0, |mut header| {
/// header.col(|ui| {
/// ui.heading("Growing");
@@ -53,6 +52,7 @@ pub struct TableBuilder<'a> {
sizing: Sizing,
scroll: bool,
striped: bool,
resizable: bool,
}
impl<'a> TableBuilder<'a> {
@@ -64,6 +64,7 @@ impl<'a> TableBuilder<'a> {
sizing,
scroll: true,
striped: false,
resizable: false,
}
}
@@ -79,6 +80,17 @@ impl<'a> TableBuilder<'a> {
self
}
/// Make the columns resizable by dragging.
///
/// Default is `false`.
///
/// If you have multiple [`Table`]:s in the same [`Ui`]
/// you will need to give them unique id:s with [`Ui::push_id`].
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
/// Add size hint for column
pub fn column(mut self, width: Size) -> Self {
self.sizing.add(width);
@@ -106,10 +118,26 @@ impl<'a> TableBuilder<'a> {
/// Create a header row which always stays visible and at the top
pub fn header(self, height: f32, header: impl FnOnce(TableRow<'_, '_>)) -> Table<'a> {
let available_width = self.available_width();
let widths = self
.sizing
.into_lengths(available_width, self.ui.spacing().item_spacing.x);
let ui = self.ui;
let Self {
ui,
sizing,
scroll,
striped,
resizable,
} = self;
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
let widths = if let Some(resize_id) = resize_id {
ui.data().get_persisted(resize_id)
} else {
None
};
let widths = widths
.unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x));
let table_top = ui.min_rect().bottom();
{
let mut layout = StripLayout::new(ui, CellDirection::Horizontal);
header(TableRow {
@@ -124,9 +152,12 @@ impl<'a> TableBuilder<'a> {
Table {
ui,
table_top,
resize_id,
sizing,
widths,
scroll: self.scroll,
striped: self.striped,
scroll,
striped,
}
}
@@ -136,15 +167,34 @@ impl<'a> TableBuilder<'a> {
F: for<'b> FnOnce(TableBody<'b>),
{
let available_width = self.available_width();
let widths = self
.sizing
.into_lengths(available_width, self.ui.spacing().item_spacing.x);
let Self {
ui,
sizing,
scroll,
striped,
resizable,
} = self;
let resize_id = resizable.then(|| ui.id().with("__table_resize"));
let widths = if let Some(resize_id) = resize_id {
ui.data().get_persisted(resize_id)
} else {
None
};
let widths = widths
.unwrap_or_else(|| sizing.to_lengths(available_width, ui.spacing().item_spacing.x));
let table_top = ui.min_rect().bottom();
Table {
ui: self.ui,
ui,
table_top,
resize_id,
sizing,
widths,
scroll: self.scroll,
striped: self.striped,
scroll,
striped,
}
.body(body);
}
@@ -155,6 +205,9 @@ impl<'a> TableBuilder<'a> {
/// Is created by [`TableBuilder`] by either calling [`TableBuilder::body`] or after creating a header row with [`TableBuilder::header`].
pub struct Table<'a> {
ui: &'a mut Ui,
table_top: f32,
resize_id: Option<egui::Id>,
sizing: Sizing,
widths: Vec<f32>,
scroll: bool,
striped: bool,
@@ -168,26 +221,91 @@ impl<'a> Table<'a> {
{
let Table {
ui,
table_top,
resize_id,
sizing,
widths,
scroll,
striped,
} = self;
let start_y = ui.available_rect_before_wrap().top();
let end_y = ui.available_rect_before_wrap().bottom();
let avail_rect = ui.available_rect_before_wrap();
egui::ScrollArea::new([false, scroll]).show(ui, move |ui| {
let layout = StripLayout::new(ui, CellDirection::Horizontal);
let mut new_widths = widths.clone();
body(TableBody {
layout,
widths,
striped,
row_nr: 0,
start_y,
end_y,
egui::ScrollArea::new([false, scroll])
.auto_shrink([true; 2])
.show(ui, move |ui| {
let layout = StripLayout::new(ui, CellDirection::Horizontal);
body(TableBody {
layout,
widths,
striped,
row_nr: 0,
start_y: avail_rect.top(),
end_y: avail_rect.bottom(),
});
});
});
let bottom = ui.min_rect().bottom();
// TODO: fix frame-delay by interacting before laying out (but painting later).
if let Some(resize_id) = resize_id {
let spacing_x = ui.spacing().item_spacing.x;
let mut x = avail_rect.left() - spacing_x * 0.5;
for (i, width) in new_widths.iter_mut().enumerate() {
x += *width + spacing_x;
let resize_id = ui.id().with("__panel_resize").with(i);
let mut p0 = egui::pos2(x, table_top);
let mut p1 = egui::pos2(x, bottom);
let line_rect = egui::Rect::from_min_max(p0, p1)
.expand(ui.style().interaction.resize_grab_radius_side);
let mouse_over_resize_line = ui.rect_contains_pointer(line_rect);
if ui.input().pointer.any_pressed()
&& ui.input().pointer.any_down()
&& mouse_over_resize_line
{
ui.memory().set_dragged_id(resize_id);
}
let is_resizing = ui.memory().is_being_dragged(resize_id);
if is_resizing {
if let Some(pointer) = ui.ctx().pointer_latest_pos() {
let new_width = *width + pointer.x - x;
let (min, max) = sizing.sizes[i].range();
let new_width = new_width.clamp(min, max);
let x = x - *width + new_width;
p0.x = x;
p1.x = x;
*width = new_width;
}
}
let dragging_something_else =
ui.input().pointer.any_down() || ui.input().pointer.any_pressed();
let resize_hover = mouse_over_resize_line && !dragging_something_else;
if resize_hover || is_resizing {
ui.output().cursor_icon = egui::CursorIcon::ResizeHorizontal;
}
let stroke = if is_resizing {
ui.style().visuals.widgets.active.bg_stroke
} else if resize_hover {
ui.style().visuals.widgets.hovered.bg_stroke
} else {
// ui.visuals().widgets.inactive.bg_stroke
ui.visuals().widgets.noninteractive.bg_stroke
};
ui.painter().line_segment([p0, p1], stroke);
}
ui.data().insert_persisted(resize_id, new_widths);
}
}
}
@@ -226,7 +344,7 @@ impl<'a> TableBody<'a> {
let max_height = self.end_y - self.start_y;
let count = (max_height / height).ceil() as usize;
let end = cmp::min(start + count, rows);
let end = rows.min(start + count);
for idx in start..end {
row(