diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index 2e5903239d6..0f05b29bd5c 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -10,6 +10,7 @@ pub mod panel; pub mod popup; pub(crate) mod resize; pub mod scroll_area; +mod sides; pub(crate) mod window; pub use { @@ -21,5 +22,6 @@ pub use { popup::*, resize::Resize, scroll_area::ScrollArea, + sides::Sides, window::Window, }; diff --git a/crates/egui/src/containers/sides.rs b/crates/egui/src/containers/sides.rs new file mode 100644 index 00000000000..4dd517f8751 --- /dev/null +++ b/crates/egui/src/containers/sides.rs @@ -0,0 +1,120 @@ +use emath::Align; + +use crate::{Layout, Ui, UiBuilder}; + +/// Put some widgets on the left and right sides of a ui. +/// +/// The result will look like this: +/// ```text +/// parent Ui +/// ______________________________________________________ +/// | | | | ^ +/// | -> left widgets -> | gap | <- right widgets <- | | height +/// |____________________| |_____________________| v +/// | | +/// | | +/// ``` +/// +/// The width of the gap is dynamic, based on the max width of the parent [`Ui`]. +/// When the parent is being auto-sized ([`Ui::is_sizing_pass`]) the gap will be as small as possible. +/// +/// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right. +/// +/// The left widgets are first added to the ui, left-to-right. +/// Then the right widgets are added, right-to-left. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// egui::containers::Sides::new().show(ui, +/// |ui| { +/// ui.label("Left"); +/// }, +/// |ui| { +/// ui.label("Right"); +/// } +/// ); +/// # }); +/// ``` +#[must_use = "You should call sides.show()"] +#[derive(Clone, Copy, Debug, Default)] +pub struct Sides { + height: Option, + spacing: Option, +} + +impl Sides { + #[inline] + pub fn new() -> Self { + Default::default() + } + + /// The minimum height of the sides. + /// + /// The content will be centered vertically within this height. + /// The default height is [`crate::Spacing::interact_size`]`.y`. + #[inline] + pub fn height(mut self, height: f32) -> Self { + self.height = Some(height); + self + } + + /// The horizontal spacing between the left and right UIs. + /// + /// This is the minimum gap. + /// The default is [`crate::Spacing::item_spacing`]`.x`. + #[inline] + pub fn spacing(mut self, spacing: f32) -> Self { + self.spacing = Some(spacing); + self + } + + pub fn show( + self, + ui: &mut Ui, + add_left: impl FnOnce(&mut Ui), + add_right: impl FnOnce(&mut Ui), + ) { + let Self { height, spacing } = self; + let height = height.unwrap_or_else(|| ui.spacing().interact_size.y); + let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x); + + let mut top_rect = ui.max_rect(); + top_rect.max.y = top_rect.min.y + height; + + let left_rect = { + let left_max_rect = top_rect; + let mut left_ui = ui.new_child( + UiBuilder::new() + .max_rect(left_max_rect) + .layout(Layout::left_to_right(Align::Center)), + ); + add_left(&mut left_ui); + left_ui.min_rect() + }; + + let right_rect = { + let right_max_rect = top_rect.with_min_x(left_rect.max.x); + let mut right_ui = ui.new_child( + UiBuilder::new() + .max_rect(right_max_rect) + .layout(Layout::right_to_left(Align::Center)), + ); + add_right(&mut right_ui); + right_ui.min_rect() + }; + + let mut final_rect = left_rect.union(right_rect); + let min_width = left_rect.width() + spacing + right_rect.width(); + + if ui.is_sizing_pass() { + // Make as small as possible: + final_rect.max.x = left_rect.min.x + min_width; + } else { + // If the rects overlap, make sure we expand the allocated rect so that the parent + // ui knows we overflowed, and resizes: + final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width); + } + + ui.advance_cursor_after_rect(final_rect); + } +} diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state/mod.rs similarity index 100% rename from crates/egui/src/input_state.rs rename to crates/egui/src/input_state/mod.rs diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index eb04e47799b..9a93a56fa90 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -466,7 +466,7 @@ pub use self::{ painter::Painter, response::{InnerResponse, Response}, sense::Sense, - style::{FontSelection, Style, TextStyle, Visuals}, + style::{FontSelection, Spacing, Style, TextStyle, Visuals}, text::{Galley, TextFormat}, ui::Ui, ui_builder::UiBuilder, diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory/mod.rs similarity index 100% rename from crates/egui/src/memory.rs rename to crates/egui/src/memory/mod.rs diff --git a/crates/egui_demo_lib/src/demo/table_demo.rs b/crates/egui_demo_lib/src/demo/table_demo.rs index a52d07e64dd..5c6f42d9649 100644 --- a/crates/egui_demo_lib/src/demo/table_demo.rs +++ b/crates/egui_demo_lib/src/demo/table_demo.rs @@ -20,6 +20,7 @@ pub struct TableDemo { scroll_to_row: Option, selection: std::collections::HashSet, checked: bool, + reversed: bool, } impl Default for TableDemo { @@ -34,6 +35,7 @@ impl Default for TableDemo { scroll_to_row: None, selection: Default::default(), checked: false, + reversed: false, } } } @@ -173,7 +175,16 @@ impl TableDemo { table .header(20.0, |mut header| { header.col(|ui| { - ui.strong("Row"); + egui::Sides::new().show( + ui, + |ui| { + ui.strong("Row"); + }, + |ui| { + self.reversed ^= + ui.button(if self.reversed { "⬆" } else { "⬇" }).clicked(); + }, + ); }); header.col(|ui| { ui.strong("Clipped text"); @@ -191,6 +202,12 @@ impl TableDemo { .body(|mut body| match self.demo { DemoType::Manual => { for row_index in 0..NUM_MANUAL_ROWS { + let row_index = if self.reversed { + NUM_MANUAL_ROWS - 1 - row_index + } else { + row_index + }; + let is_thick = thick_row(row_index); let row_height = if is_thick { 30.0 } else { 18.0 }; body.row(row_height, |mut row| { @@ -223,7 +240,12 @@ impl TableDemo { } DemoType::ManyHomogeneous => { body.rows(text_height, self.num_rows, |mut row| { - let row_index = row.index(); + let row_index = if self.reversed { + self.num_rows - 1 - row.index() + } else { + row.index() + }; + row.set_selected(self.selection.contains(&row_index)); row.col(|ui| { @@ -251,7 +273,12 @@ impl TableDemo { DemoType::ManyHeterogenous => { 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(); + let row_index = if self.reversed { + self.num_rows - 1 - row.index() + } else { + row.index() + }; + row.set_selected(self.selection.contains(&row_index)); row.col(|ui| { diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index d5cf5dfb1a1..9fff73b2573 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -451,7 +451,7 @@ impl<'a> TableBuilder<'a> { let Self { ui, id_salt, - columns, + mut columns, striped, resizable, cell_layout, @@ -459,6 +459,15 @@ impl<'a> TableBuilder<'a> { sense, } = self; + for (i, column) in columns.iter_mut().enumerate() { + let column_resize_id = ui.id().with("resize_column").with(i); + if let Some(response) = ui.ctx().read_response(column_resize_id) { + if response.double_clicked() { + column.auto_size_this_frame = true; + } + } + } + let striped = striped.unwrap_or(ui.visuals().striped); let state_id = ui.id().with(id_salt); @@ -695,7 +704,7 @@ impl<'a> Table<'a> { ui, table_top, state_id, - mut columns, + columns, resizable, mut available_width, mut state, @@ -719,15 +728,6 @@ impl<'a> Table<'a> { scroll_bar_visibility, } = scroll_options; - for (i, column) in columns.iter_mut().enumerate() { - let column_resize_id = ui.id().with("resize_column").with(i); - if let Some(response) = ui.ctx().read_response(column_resize_id) { - if response.double_clicked() { - column.auto_size_this_frame = true; - } - } - } - let cursor_position = ui.cursor().min; let mut scroll_area = ScrollArea::new([false, vscroll]) diff --git a/scripts/check.sh b/scripts/check.sh index 072c2ea542d..6f4aee69ec3 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -21,7 +21,7 @@ cargo fmt --all -- --check cargo doc --quiet --lib --no-deps --all-features cargo doc --quiet --document-private-items --no-deps --all-features cargo clippy --quiet --all-targets --all-features -- -D warnings -cargo clippy --all-targets --all-features --release -- -D warnings # we need to check release mode too +cargo clippy --quiet --all-targets --all-features --release -- -D warnings # we need to check release mode too ./scripts/clippy_wasm.sh