diff --git a/egui/src/containers/panel.rs b/egui/src/containers/panel.rs index ea17aa17..a015863f 100644 --- a/egui/src/containers/panel.rs +++ b/egui/src/containers/panel.rs @@ -201,7 +201,7 @@ impl SidePanel { let mut is_resizing = false; if resizable { let resize_id = id.with("__resize"); - if let Some(pointer) = ui.ctx().latest_pointer_pos() { + if let Some(pointer) = ui.ctx().pointer_latest_pos() { let we_are_on_top = ui .ctx() .layer_id_at(pointer) @@ -217,9 +217,9 @@ impl SidePanel { && ui.input().pointer.any_down() && mouse_over_resize_line { - ui.memory().interaction.drag_id = Some(resize_id); + ui.memory().set_dragged_id(resize_id); } - is_resizing = ui.memory().interaction.drag_id == Some(resize_id); + is_resizing = ui.memory().is_being_dragged(resize_id); if is_resizing { let width = (pointer.x - side.side_x(panel_rect)).abs(); let width = diff --git a/egui/src/context.rs b/egui/src/context.rs index bb4fe880..f996ae15 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -885,7 +885,7 @@ impl Context { /// Latest reported pointer position. /// When tapping a touch screen, this will be `None`. #[inline(always)] - pub(crate) fn latest_pointer_pos(&self) -> Option { + pub fn pointer_latest_pos(&self) -> Option { self.input().pointer.latest_pos() } diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 4956918c..91a1a8a4 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -52,7 +52,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo response .on_hover_cursor(CursorIcon::ZoomIn) .on_hover_ui_at_pointer(|ui| { - if let Some(pos) = ui.ctx().latest_pointer_pos() { + if let Some(pos) = ui.ctx().pointer_latest_pos() { let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0)); let u = remap_clamp(pos.x, rect.x_range(), 0.0..=tex_w); let v = remap_clamp(pos.y, rect.y_range(), 0.0..=tex_h); diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 0ccc36ae..a1a3358a 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -418,6 +418,11 @@ impl Memory { self.interaction.drag_id == Some(id) } + #[inline(always)] + pub fn set_dragged_id(&mut self, id: Id) { + self.interaction.drag_id = Some(id); + } + /// Forget window positions, sizes etc. /// Can be used to auto-layout windows. pub fn reset_areas(&mut self) { diff --git a/egui_demo_lib/src/apps/demo/strip_demo.rs b/egui_demo_lib/src/apps/demo/strip_demo.rs index 5bdd0731..5d1d4c25 100644 --- a/egui_demo_lib/src/apps/demo/strip_demo.rs +++ b/egui_demo_lib/src/apps/demo/strip_demo.rs @@ -26,13 +26,10 @@ impl super::Demo for StripDemo { impl super::View for StripDemo { fn ui(&mut self, ui: &mut egui::Ui) { StripBuilder::new(ui) - .size(Size::Absolute(50.0)) - .size(Size::Remainder) - .size(Size::RelativeMinimum { - relative: 0.5, - minimum: 60.0, - }) - .size(Size::Absolute(10.0)) + .size(Size::exact(50.0)) + .size(Size::remainder()) + .size(Size::relative(0.5).at_least(60.0)) + .size(Size::exact(10.0)) .vertical(|mut strip| { strip.cell_clip(|ui| { ui.painter() @@ -40,7 +37,7 @@ impl super::View for StripDemo { ui.label("Full width and 50px height"); }); strip.strip(|builder| { - builder.sizes(Size::Remainder, 2).horizontal(|mut strip| { + builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { strip.cell_clip(|ui| { ui.painter().rect_filled( ui.available_rect_before_wrap(), @@ -50,7 +47,7 @@ impl super::View for StripDemo { ui.label("remaining height and 50% of the width"); }); strip.strip(|builder| { - builder.sizes(Size::Remainder, 3).vertical(|mut strip| { + builder.sizes(Size::remainder(), 3).vertical(|mut strip| { strip.empty(); strip.cell_clip(|ui| { ui.painter().rect_filled( @@ -66,17 +63,17 @@ impl super::View for StripDemo { }); strip.strip(|builder| { builder - .size(Size::Remainder) - .size(Size::Absolute(60.0)) - .size(Size::Remainder) - .size(Size::Absolute(70.0)) + .size(Size::remainder()) + .size(Size::exact(60.0)) + .size(Size::remainder()) + .size(Size::exact(70.0)) .horizontal(|mut strip| { strip.empty(); strip.strip(|builder| { builder - .size(Size::Remainder) - .size(Size::Absolute(60.0)) - .size(Size::Remainder) + .size(Size::remainder()) + .size(Size::exact(60.0)) + .size(Size::remainder()) .vertical(|mut strip| { strip.empty(); strip.cell_clip(|ui| { diff --git a/egui_demo_lib/src/apps/demo/table_demo.rs b/egui_demo_lib/src/apps/demo/table_demo.rs index 4374e51a..2e235d02 100644 --- a/egui_demo_lib/src/apps/demo/table_demo.rs +++ b/egui_demo_lib/src/apps/demo/table_demo.rs @@ -5,6 +5,7 @@ use egui_extras::{Size, StripBuilder, TableBuilder}; #[derive(Default)] pub struct TableDemo { virtual_scroll: bool, + resizable: bool, } impl super::Demo for TableDemo { @@ -26,12 +27,13 @@ impl super::Demo for TableDemo { impl super::View for TableDemo { fn ui(&mut self, ui: &mut egui::Ui) { - ui.checkbox(&mut self.virtual_scroll, "Virtual scroll demo"); + ui.checkbox(&mut self.virtual_scroll, "Virtual scroll"); + ui.checkbox(&mut self.resizable, "Resizable columns"); // Leave room for the source code link after the table demo: StripBuilder::new(ui) - .size(Size::Remainder) // for the table - .size(Size::Absolute(10.0)) // for the source code link + .size(Size::remainder()) // for the table + .size(Size::exact(10.0)) // for the source code link .vertical(|mut strip| { strip.cell_clip(|ui| { self.table_ui(ui); @@ -49,24 +51,25 @@ impl TableDemo { fn table_ui(&mut self, ui: &mut egui::Ui) { TableBuilder::new(ui) .striped(true) - .column(Size::Absolute(120.0)) - .column(Size::RemainderMinimum(180.0)) - .column(Size::Absolute(100.0)) + .column(Size::initial(60.0).at_least(40.0)) + .column(Size::remainder().at_least(60.0)) + .column(Size::initial(60.0).at_least(40.0)) + .resizable(self.resizable) .header(20.0, |mut header| { - header.col(|ui| { + header.col_clip(|ui| { ui.heading("Left"); }); - header.col(|ui| { + header.col_clip(|ui| { ui.heading("Middle"); }); - header.col(|ui| { + header.col_clip(|ui| { ui.heading("Right"); }); }) .body(|mut body| { if self.virtual_scroll { body.rows(20.0, 100_000, |index, mut row| { - row.col(|ui| { + row.col_clip(|ui| { ui.label(index.to_string()); }); row.col_clip(|ui| { @@ -75,30 +78,27 @@ impl TableDemo { .wrap(false), ); }); - row.col(|ui| { + row.col_clip(|ui| { ui.label(index.to_string()); }); }); } else { - for i in 0..100 { - let height = match i % 8 { - 0 => 25.0, - 4 => 30.0, - _ => 20.0, - }; + for i in 0..20 { + let thick = i % 4 == 0; + let height = if thick { 25.0 } else { 15.0 }; body.row(height, |mut row| { - row.col(|ui| { + row.col_clip(|ui| { ui.label(i.to_string()); }); row.col_clip(|ui| { - ui.add( - egui::Label::new( - format!("Normal scroll, each row can have a different height. Height: {}", height), - ) - .wrap(false), - ); + ui.style_mut().wrap = Some(false); + if thick { + ui.heading("Extra thick row"); + } else { + ui.label("Normal row"); + } }); - row.col(|ui| { + row.col_clip(|ui| { ui.label(i.to_string()); }); }); diff --git a/egui_extras/CHANGELOG.md b/egui_extras/CHANGELOG.md index 23fb3ddd..986f1f90 100644 --- a/egui_extras/CHANGELOG.md +++ b/egui_extras/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to the `egui_extras` integration will be noted in this file. ## Unreleased +* Added `Strip`, `Table` and `DatePicker` ([#963](https://github.com/emilk/egui/pull/963)). ## 0.17.0 - 2022-02-22 diff --git a/egui_extras/src/datepicker/popup.rs b/egui_extras/src/datepicker/popup.rs index 74cb8b8e..2c552919 100644 --- a/egui_extras/src/datepicker/popup.rs +++ b/egui_extras/src/datepicker/popup.rs @@ -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| { diff --git a/egui_extras/src/layout.rs b/egui_extras/src/layout.rs index 47952aa7..ce78b197 100644 --- a/egui_extras/src/layout.rs +++ b/egui_extras/src/layout.rs @@ -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 diff --git a/egui_extras/src/sizing.rs b/egui_extras/src/sizing.rs index cd057d9c..96322c0b 100644 --- a/egui_extras/src/sizing.rs +++ b/egui_extras/src/sizing.rs @@ -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, + pub(crate) sizes: Vec, } impl Sizing { @@ -32,24 +90,21 @@ impl Sizing { self.sizes.push(size); } - pub fn into_lengths(self, length: f32, spacing: f32) -> Vec { + pub fn to_lengths(&self, length: f32, spacing: f32) -> Vec { 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> 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]); } diff --git a/egui_extras/src/strip.rs b/egui_extras/src/strip.rs index 131c55cb..62061d99 100644 --- a/egui_extras/src/strip.rs +++ b/egui_extras/src/strip.rs @@ -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); } diff --git a/egui_extras/src/table.rs b/egui_extras/src/table.rs index b105a886..6242eba2 100644 --- a/egui_extras/src/table.rs +++ b/egui_extras/src/table.rs @@ -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, + sizing: Sizing, widths: Vec, 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(