From 80d218cf7186c8abbbbed822d81dc42c12525bd0 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Sun, 2 Mar 2025 11:48:37 +0100 Subject: [PATCH 1/9] Experiment with calculating an Ui's "intrinsic size" by remembering the widget's size --- crates/egui/src/containers/area.rs | 4 +- crates/egui/src/containers/popup.rs | 3 +- crates/egui/src/layout.rs | 5 +++ crates/egui/src/placer.rs | 7 ++++ crates/egui/src/ui.rs | 21 +++++++++-- crates/egui/src/widgets/checkbox.rs | 1 + crates/egui_demo_lib/src/demo/popups.rs | 12 ++++++ examples/hello_world_simple/src/main.rs | 50 +++++++++++++++++++------ 8 files changed, 87 insertions(+), 16 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 55cdb075597..f0cc8cab771 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -613,7 +613,9 @@ impl Prepared { .. } = self; - state.size = Some(content_ui.min_size()); + state.size = Some(content_ui.placer().min_item_size()); + + dbg!(state.size); // Make sure we report back the correct size. // Very important after the initial sizing pass, when the initial estimate of the size is way off. diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 4a457bf03d6..9011b9f366a 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -568,7 +568,8 @@ impl<'a> Popup<'a> { let mut response = area.show(&ctx, |ui| { style.apply(ui.style_mut()); - frame.show(ui, content).inner + // frame.show(ui, content).inner + content(ui) }); let closed_by_click = match close_behavior { diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index e967aaa6d57..d18a342dafa 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -45,6 +45,8 @@ pub(crate) struct Region { /// If something has already been added, this will point to `style.spacing.item_spacing` beyond the latest child. /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the `min_rect`. pub(crate) cursor: Rect, + + pub min_item_size: Vec2, } impl Region { @@ -426,6 +428,7 @@ impl Layout { min_rect: Rect::NOTHING, // temporary max_rect, cursor: self.initial_cursor(max_rect), + min_item_size: Vec2::default(), }; let seed = self.next_widget_position(®ion); region.min_rect = Rect::from_center_size(seed, Vec2::ZERO); @@ -529,6 +532,7 @@ impl Layout { mut cursor, mut max_rect, min_rect, + min_item_size, } = *region; match self.main_dir { @@ -590,6 +594,7 @@ impl Layout { min_rect, max_rect, cursor, + min_item_size, }; self.next_frame_ignore_wrap(®ion, child_size) diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index 2de822c0315..f05ee9588c1 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -163,6 +163,7 @@ impl Placer { frame_rect: Rect, widget_rect: Rect, item_spacing: Vec2, + desired_size: Vec2, ) { debug_assert!(!frame_rect.any_nan()); debug_assert!(!widget_rect.any_nan()); @@ -179,11 +180,17 @@ impl Placer { ); } + self.region.min_item_size = self.region.min_item_size.max(desired_size); + self.expand_to_include_rect(frame_rect); // e.g. for centered layouts: pretend we used whole frame self.region.sanity_check(); } + pub(crate) fn min_item_size(&self) -> Vec2 { + self.region.min_item_size + } + /// Move to the next row in a grid layout or wrapping layout. /// Otherwise does nothing. pub(crate) fn end_row(&mut self, item_spacing: Vec2, painter: &Painter) { diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 6da01d9beda..94c9d0411e1 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1403,7 +1403,7 @@ impl Ui { let widget_rect = self.placer.justify_and_align(frame_rect, desired_size); self.placer - .advance_after_rects(frame_rect, widget_rect, item_spacing); + .advance_after_rects(frame_rect, widget_rect, item_spacing, desired_size); register_rect(self, widget_rect); @@ -1426,7 +1426,8 @@ impl Ui { let rect = rect.round_ui(); let item_spacing = self.spacing().item_spacing; - self.placer.advance_after_rects(rect, rect, item_spacing); + self.placer + .advance_after_rects(rect, rect, item_spacing, rect.size()); register_rect(self, rect); let id = Id::new(self.next_auto_id_salt); @@ -2412,8 +2413,22 @@ impl Ui { let mut child_ui = self.new_child(ui_builder); self.next_auto_id_salt = next_auto_id_salt; // HACK: we want `scope` to only increment this once, so that `ui.scope` is equivalent to `ui.allocate_space`. let ret = add_contents(&mut child_ui); - let response = child_ui.remember_min_rect(); + let mut response = child_ui.remember_min_rect(); self.advance_cursor_after_rect(child_ui.min_rect()); + match self.layout().is_horizontal() { + true => { + response.intrinsic_size = Some(Vec2::new( + response.rect.width(), + self.placer.min_item_size().y, + )); + } + false => { + response.intrinsic_size = Some(Vec2::new( + self.placer.min_item_size().x, + response.rect.height(), + )); + } + } InnerResponse::new(ret, response) } diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 7bdb6c86fe9..0b468478e93 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -75,6 +75,7 @@ impl Widget for Checkbox<'_> { desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); desired_size.y = desired_size.y.max(icon_width); + dbg!(desired_size); let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); if response.clicked() { diff --git a/crates/egui_demo_lib/src/demo/popups.rs b/crates/egui_demo_lib/src/demo/popups.rs index 4cf689d3cc7..7f723158af7 100644 --- a/crates/egui_demo_lib/src/demo/popups.rs +++ b/crates/egui_demo_lib/src/demo/popups.rs @@ -70,6 +70,18 @@ fn nested_menus(ui: &mut egui::Ui, checked: &mut bool) { } let _ = ui.button("Item"); ui.menu_button("Recursive", |ui| nested_menus(ui, checked)); + + // if ui.button(if *checked { "short" } else { "Very long text for this item that should be wrapped" }).clicked() { + // *checked = !*checked; + // } + ui.checkbox( + checked, + if *checked { + "short" + } else { + "Very long text for this item that should be wrapped" + }, + ); }); ui.menu_button("SubMenu", |ui| { if ui.button("Open…").clicked() { diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 4fe49a89d68..2a3006c225d 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -2,6 +2,7 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; +use eframe::egui::{Align, Button, Layout, Popup, TextWrapMode, Widget}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -15,19 +16,46 @@ fn main() -> eframe::Result { let mut name = "Arthur".to_owned(); let mut age = 42; + let mut checked = true; + eframe::run_simple_native("My egui App", options, move |ctx, _frame| { egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("My egui Application"); - ui.horizontal(|ui| { - let name_label = ui.label("Your name: "); - ui.text_edit_singleline(&mut name) - .labelled_by(name_label.id); - }); - ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); - if ui.button("Increment").clicked() { - age += 1; - } - ui.label(format!("Hello '{name}', age {age}")); + // ui.heading("My egui Application"); + // ui.horizontal(|ui| { + // let name_label = ui.label("Your name: "); + // ui.text_edit_singleline(&mut name) + // .labelled_by(name_label.id); + // }); + // ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); + // if ui.button("Increment").clicked() { + // age += 1; + // } + // ui.label(format!("Hello '{name}', age {age}")); + + let response = ui.button("Hiiii"); + + let text = if checked { + "short" + } else { + "Very long text for this item that should be wrapped" + }; + Popup::from_response(&response) + .layout(Layout::top_down_justified(Align::Min)) + .show(|ui| { + // ui.checkbox(&mut checked, text); + + ui.button("Hiiii"); + + if Button::new(text) + .wrap_mode(TextWrapMode::Extend) + .ui(ui) + .clicked() + { + checked = !checked; + } + + ui.button("Some other button"); + }); }); }) } From 166e8e2092f7d3137e17678418a34429069db688 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 4 Mar 2025 22:51:55 +0100 Subject: [PATCH 2/9] Calculate preferred size from Galley; Pass it back up the Uis, Frames, etc.. --- crates/egui/src/containers/area.rs | 2 +- .../egui/src/containers/collapsing_header.rs | 8 +- crates/egui/src/containers/combo_box.rs | 4 +- crates/egui/src/containers/frame.rs | 6 +- crates/egui/src/containers/popup.rs | 3 +- crates/egui/src/containers/resize.rs | 5 +- crates/egui/src/containers/scroll_area.rs | 2 +- crates/egui/src/containers/sides.rs | 17 ++-- crates/egui/src/introspection.rs | 6 +- crates/egui/src/layout.rs | 8 +- crates/egui/src/menu.rs | 2 +- crates/egui/src/placer.rs | 22 +++-- crates/egui/src/style.rs | 2 +- crates/egui/src/ui.rs | 84 ++++++++++++------- crates/egui/src/widgets/button.rs | 20 ++++- crates/egui/src/widgets/color_picker.rs | 8 +- crates/egui/src/widgets/label.rs | 4 +- crates/egui/src/widgets/selected_label.rs | 3 +- crates/egui/src/widgets/separator.rs | 2 +- crates/egui/src/widgets/slider.rs | 2 +- crates/egui/src/widgets/text_edit/builder.rs | 3 +- crates/egui_demo_app/src/frame_history.rs | 3 +- .../egui_demo_lib/src/demo/dancing_strings.rs | 4 +- .../src/demo/misc_demo_window.rs | 5 +- crates/egui_demo_lib/src/demo/scrolling.rs | 2 +- .../src/demo/tests/window_resize_test.rs | 4 +- crates/egui_demo_lib/src/rendering_test.rs | 2 +- crates/egui_extras/src/layout.rs | 9 +- crates/epaint/src/text/text_layout_types.rs | 25 ++++++ examples/hello_world_simple/src/main.rs | 23 +++-- 30 files changed, 198 insertions(+), 92 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index f0cc8cab771..e7f787f40a5 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -613,7 +613,7 @@ impl Prepared { .. } = self; - state.size = Some(content_ui.placer().min_item_size()); + state.size = Some(content_ui.intrinsic_size()); dbg!(state.size); diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index c34e5629464..5be77f3d069 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -18,7 +18,7 @@ pub(crate) struct InnerState { open_height: Option, } -/// This is a a building block for building collapsing regions. +/// This is a building block for building collapsing regions. /// /// It is used by [`CollapsingHeader`] and [`crate::Window`], but can also be used on its own. /// @@ -87,7 +87,7 @@ impl CollapsingState { ui: &mut Ui, button_size: Vec2, ) -> Response { - let (_id, rect) = ui.allocate_space(button_size); + let (_id, rect) = ui.allocate_space(button_size, Vec2::ZERO); // TODO let response = ui.interact(rect, self.id, Sense::click()); response.widget_info(|| { WidgetInfo::labeled( @@ -117,7 +117,7 @@ impl CollapsingState { icon_fn: impl FnOnce(&mut Ui, f32, &Response) + 'static, ) -> Response { let size = vec2(ui.spacing().indent, ui.spacing().icon_width); - let (_id, rect) = ui.allocate_space(size); + let (_id, rect) = ui.allocate_space(size, Vec2::ZERO); // TODO let response = ui.interact(rect, self.id, Sense::click()); if response.clicked() { self.toggle(ui); @@ -554,7 +554,7 @@ impl CollapsingHeader { let mut desired_size = vec2(desired_width, galley.size().y + 2.0 * button_padding.y); desired_size = desired_size.at_least(ui.spacing().interact_size); - let (_, rect) = ui.allocate_space(desired_size); + let (_, rect) = ui.allocate_space(desired_size, desired_size); // TODO let mut header_response = ui.interact(rect, id, Sense::click()); let text_pos = pos2( diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index 7a5a160f96c..86ec7d298bc 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -340,7 +340,7 @@ fn combo_box_dyn<'c, R>( let actual_width = (galley.size().x + icon_spacing + icon_size.x).at_least(minimum_width); let actual_height = galley.size().y.max(icon_size.y); - let (_, rect) = ui.allocate_space(Vec2::new(actual_width, actual_height)); + let (_, rect) = ui.allocate_space(Vec2::new(actual_width, actual_height), Vec2::ZERO); // TODO let button_rect = ui.min_rect().expand2(ui.spacing().button_padding); let response = ui.interact(button_rect, button_id, Sense::click()); // response.active |= is_popup_open; @@ -444,7 +444,7 @@ fn button_frame( ); } - ui.advance_cursor_after_rect(outer_rect); + ui.advance_cursor_after_rect(outer_rect, Vec2::ZERO); // TODO response } diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 1fdbbca922d..2f8e7cbe8fe 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -473,7 +473,11 @@ impl Prepared { /// /// This can be called before or after [`Self::paint`]. pub fn allocate_space(&self, ui: &mut Ui) -> Response { - ui.allocate_rect(self.outer_rect(), Sense::hover()) + ui.allocate_rect( + self.outer_rect(), + Sense::hover(), + self.content_ui.intrinsic_size() + self.frame.total_margin().sum(), + ) } /// Paint the frame. diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 9011b9f366a..4a457bf03d6 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -568,8 +568,7 @@ impl<'a> Popup<'a> { let mut response = area.show(&ctx, |ui| { style.apply(ui.style_mut()); - // frame.show(ui, content).inner - content(ui) + frame.show(ui, content).inner }); let closed_by_click = match close_behavior { diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 0df9a113607..35c4d42c085 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -331,7 +331,10 @@ impl Resize { size[d] = state.last_content_size[d]; } } - ui.advance_cursor_after_rect(Rect::from_min_size(content_ui.min_rect().min, size)); + ui.advance_cursor_after_rect( + Rect::from_min_size(content_ui.min_rect().min, size), + Vec2::ZERO, + ); // TODO // ------------------------------ diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index c430b6ea9b5..d88c9c2f34d 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1245,7 +1245,7 @@ impl Prepared { } } - ui.advance_cursor_after_rect(outer_rect); + ui.advance_cursor_after_rect(outer_rect, Vec2::ZERO); // TODO if show_scroll_this_frame != state.show_scroll { ui.ctx().request_repaint(); diff --git a/crates/egui/src/containers/sides.rs b/crates/egui/src/containers/sides.rs index e34ae70eb8c..a7275ad54f3 100644 --- a/crates/egui/src/containers/sides.rs +++ b/crates/egui/src/containers/sides.rs @@ -1,4 +1,4 @@ -use emath::Align; +use emath::{Align, Vec2}; use crate::{Layout, Ui, UiBuilder}; @@ -84,7 +84,7 @@ impl Sides { let result_left; let result_right; - let left_rect = { + let (left_rect, left_preferred_size) = { let left_max_rect = top_rect; let mut left_ui = ui.new_child( UiBuilder::new() @@ -92,10 +92,10 @@ impl Sides { .layout(Layout::left_to_right(Align::Center)), ); result_left = add_left(&mut left_ui); - left_ui.min_rect() + (left_ui.min_rect(), left_ui.intrinsic_size()) }; - let right_rect = { + let (right_rect, right_preferred_size) = { let right_max_rect = top_rect.with_min_x(left_rect.max.x); let mut right_ui = ui.new_child( UiBuilder::new() @@ -103,7 +103,7 @@ impl Sides { .layout(Layout::right_to_left(Align::Center)), ); result_right = add_right(&mut right_ui); - right_ui.min_rect() + (right_ui.min_rect(), right_ui.intrinsic_size()) }; let mut final_rect = left_rect.union(right_rect); @@ -118,7 +118,12 @@ impl Sides { final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width); } - ui.advance_cursor_after_rect(final_rect); + let preferred_size = Vec2::new( + left_preferred_size.x + spacing + right_preferred_size.x, + left_preferred_size.y.max(right_preferred_size.y), + ); + + ui.advance_cursor_after_rect(final_rect, preferred_size); (result_left, result_right) } diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index e21ddb3a5c7..8d0a2474564 100644 --- a/crates/egui/src/introspection.rs +++ b/crates/egui/src/introspection.rs @@ -1,8 +1,10 @@ //! Showing UI:s for egui/epaint types. + use crate::{ epaint, memory, pos2, remap_clamp, vec2, Color32, CursorIcon, FontFamily, FontId, Label, Mesh, NumExt, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget, }; +use emath::Vec2; pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) { let families = ui.fonts(|f| f.families()); @@ -42,7 +44,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo if size.x > ui.available_width() { size *= ui.available_width() / size.x; } - let (rect, response) = ui.allocate_at_least(size, Sense::hover()); + let (rect, response) = ui.allocate_at_least(size, Sense::hover(), Vec2::ZERO); // TODO let mut mesh = Mesh::default(); mesh.add_rect_with_uv(rect, [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), color); ui.painter().add(Shape::mesh(mesh)); @@ -53,7 +55,7 @@ pub(crate) fn font_texture_ui(ui: &mut Ui, [width, height]: [usize; 2]) -> Respo .on_hover_cursor(CursorIcon::ZoomIn) .on_hover_ui_at_pointer(|ui| { if let Some(pos) = ui.ctx().pointer_latest_pos() { - let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0)); + let (_id, zoom_rect) = ui.allocate_space(vec2(128.0, 128.0), Vec2::ZERO); // TODO 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/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index d18a342dafa..cf165c28110 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -46,7 +46,7 @@ pub(crate) struct Region { /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the `min_rect`. pub(crate) cursor: Rect, - pub min_item_size: Vec2, + pub intrinsic_size: Vec2, } impl Region { @@ -428,7 +428,7 @@ impl Layout { min_rect: Rect::NOTHING, // temporary max_rect, cursor: self.initial_cursor(max_rect), - min_item_size: Vec2::default(), + intrinsic_size: Vec2::default(), }; let seed = self.next_widget_position(®ion); region.min_rect = Rect::from_center_size(seed, Vec2::ZERO); @@ -532,7 +532,7 @@ impl Layout { mut cursor, mut max_rect, min_rect, - min_item_size, + intrinsic_size: min_item_size, } = *region; match self.main_dir { @@ -594,7 +594,7 @@ impl Layout { min_rect, max_rect, cursor, - min_item_size, + intrinsic_size: min_item_size, }; self.next_frame_ignore_wrap(®ion, child_size) diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index fa99cebbd2d..c44cfbed2d7 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -563,7 +563,7 @@ impl SubMenuButton { let mut desired_size = text_and_icon_size + 2.0 * button_padding; desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); - let (rect, response) = ui.allocate_at_least(desired_size, sense); + let (rect, response) = ui.allocate_at_least(desired_size, sense, Vec2::ZERO); // TODO response.widget_info(|| { crate::WidgetInfo::labeled( crate::WidgetType::Button, diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index f05ee9588c1..26e2365f9c8 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -1,4 +1,4 @@ -use crate::{grid, vec2, Layout, Painter, Pos2, Rect, Region, Vec2}; +use crate::{grid, vec2, Direction, Layout, Painter, Pos2, Rect, Region, Vec2}; #[cfg(debug_assertions)] use crate::{Align2, Color32, Stroke}; @@ -163,7 +163,7 @@ impl Placer { frame_rect: Rect, widget_rect: Rect, item_spacing: Vec2, - desired_size: Vec2, + intrinsic_size: Vec2, ) { debug_assert!(!frame_rect.any_nan()); debug_assert!(!widget_rect.any_nan()); @@ -180,15 +180,27 @@ impl Placer { ); } - self.region.min_item_size = self.region.min_item_size.max(desired_size); + if self.layout.is_horizontal() { + if self.region.intrinsic_size.x != 0.0 { + self.region.intrinsic_size.x += item_spacing.x; + } + self.region.intrinsic_size.x += intrinsic_size.x; + self.region.intrinsic_size.y = self.region.intrinsic_size.y.max(intrinsic_size.y); + } else { + if self.region.intrinsic_size.y != 0.0 { + self.region.intrinsic_size.y += item_spacing.y; + } + self.region.intrinsic_size.x = self.region.intrinsic_size.x.max(intrinsic_size.x); + self.region.intrinsic_size.y += intrinsic_size.y; + } self.expand_to_include_rect(frame_rect); // e.g. for centered layouts: pretend we used whole frame self.region.sanity_check(); } - pub(crate) fn min_item_size(&self) -> Vec2 { - self.region.min_item_size + pub(crate) fn intrinsic_size(&self) -> Vec2 { + self.region.intrinsic_size } /// Move to the next row in a grid layout or wrapping layout. diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 4d227f63bf8..159916b68cb 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2548,7 +2548,7 @@ impl Widget for &mut Stroke { ui.color_edit_button_srgba(color); // stroke preview: - let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size); + let (_id, stroke_rect) = ui.allocate_space(ui.spacing().interact_size, Vec2::ZERO); // TODO let left = stroke_rect.left_center(); let right = stroke_rect.right_center(); ui.painter().line_segment([left, right], (*width, *color)); diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 94c9d0411e1..99067eb317b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1140,6 +1140,12 @@ impl Ui { if self.should_close() { response.set_close(); } + response.intrinsic_size = Some(self.intrinsic_size()); + // self.ctx().debug_painter().debug_rect( + // Rect::from_min_size(self.min_rect().min, self.intrinsic_size()), + // Color32::GREEN, + // "", + // ); response } @@ -1296,8 +1302,13 @@ impl Ui { /// ui.painter().rect_stroke(response.rect, 0.0, (1.0, egui::Color32::WHITE), egui::StrokeKind::Inside); /// # }); /// ``` - pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response { - let (id, rect) = self.allocate_space(desired_size); + pub fn allocate_response( + &mut self, + desired_size: Vec2, + sense: Sense, + desired_size_2: Vec2, + ) -> Response { + let (id, rect) = self.allocate_space(desired_size, desired_size_2); let mut response = self.interact(rect, id, sense); response.intrinsic_size = Some(desired_size); response @@ -1309,7 +1320,7 @@ impl Ui { /// This means that if this is a narrow widget in a wide justified layout, then /// the widget will react to interactions outside the returned [`Rect`]. pub fn allocate_exact_size(&mut self, desired_size: Vec2, sense: Sense) -> (Rect, Response) { - let response = self.allocate_response(desired_size, sense); + let response = self.allocate_response(desired_size, sense, desired_size); // TODO? let rect = self .placer .align_size_within_rect(desired_size, response.rect); @@ -1319,8 +1330,15 @@ impl Ui { /// Allocate at least as much space as needed, and interact with that rect. /// /// The returned [`Rect`] will be the same size as `Response::rect`. - pub fn allocate_at_least(&mut self, desired_size: Vec2, sense: Sense) -> (Rect, Response) { - let response = self.allocate_response(desired_size, sense); + // TODO: Is this still needed? + // TODO: Awful naming conflict + pub fn allocate_at_least( + &mut self, + desired_size: Vec2, + sense: Sense, + desired_size_2: Vec2, + ) -> (Rect, Response) { + let response = self.allocate_response(desired_size, sense, desired_size_2); (response.rect, response) } @@ -1345,11 +1363,11 @@ impl Ui { /// let response = ui.interact(rect, id, egui::Sense::click()); /// # }); /// ``` - pub fn allocate_space(&mut self, desired_size: Vec2) -> (Id, Rect) { + pub fn allocate_space(&mut self, desired_size: Vec2, desired_size_2: Vec2) -> (Id, Rect) { #[cfg(debug_assertions)] let original_available = self.available_size_before_wrap(); - let rect = self.allocate_space_impl(desired_size); + let rect = self.allocate_space_impl(desired_size, desired_size_2); #[cfg(debug_assertions)] { @@ -1396,14 +1414,19 @@ impl Ui { /// Reserve this much space and move the cursor. /// Returns where to put the widget. - fn allocate_space_impl(&mut self, desired_size: Vec2) -> Rect { + fn allocate_space_impl(&mut self, desired_size: Vec2, desired_size_2: Vec2) -> Rect { + // self.ctx().debug_painter().debug_rect( + // Rect::from_min_size(self.cursor().min, desired_size_2), + // Color32::YELLOW, + // "", + // ); let item_spacing = self.spacing().item_spacing; let frame_rect = self.placer.next_space(desired_size, item_spacing); debug_assert!(!frame_rect.any_nan()); let widget_rect = self.placer.justify_and_align(frame_rect, desired_size); self.placer - .advance_after_rects(frame_rect, widget_rect, item_spacing, desired_size); + .advance_after_rects(frame_rect, widget_rect, item_spacing, desired_size_2); register_rect(self, widget_rect); @@ -1414,20 +1437,20 @@ impl Ui { /// /// Ignore the layout of the [`Ui`]: just put my widget here! /// The layout cursor will advance to past this `rect`. - pub fn allocate_rect(&mut self, rect: Rect, sense: Sense) -> Response { + pub fn allocate_rect(&mut self, rect: Rect, sense: Sense, intrinsic_size: Vec2) -> Response { let rect = rect.round_ui(); - let id = self.advance_cursor_after_rect(rect); + let id = self.advance_cursor_after_rect(rect, intrinsic_size); self.interact(rect, id, sense) } /// Allocate a rect without interacting with it. - pub fn advance_cursor_after_rect(&mut self, rect: Rect) -> Id { + pub fn advance_cursor_after_rect(&mut self, rect: Rect, intrinsic_size: Vec2) -> Id { debug_assert!(!rect.any_nan()); let rect = rect.round_ui(); let item_spacing = self.spacing().item_spacing; self.placer - .advance_after_rects(rect, rect, item_spacing, rect.size()); + .advance_after_rects(rect, rect, item_spacing, intrinsic_size); register_rect(self, rect); let id = Id::new(self.next_auto_id_salt); @@ -1555,7 +1578,7 @@ impl Ui { /// # }); /// ``` pub fn allocate_painter(&mut self, desired_size: Vec2, sense: Sense) -> (Response, Painter) { - let response = self.allocate_response(desired_size, sense); + let response = self.allocate_response(desired_size, sense, desired_size); // TODO? let clip_rect = self.clip_rect().intersect(response.rect); // Make sure we don't paint out of bounds let painter = self.painter().with_clip_rect(clip_rect); (response, painter) @@ -2414,24 +2437,17 @@ impl Ui { self.next_auto_id_salt = next_auto_id_salt; // HACK: we want `scope` to only increment this once, so that `ui.scope` is equivalent to `ui.allocate_space`. let ret = add_contents(&mut child_ui); let mut response = child_ui.remember_min_rect(); - self.advance_cursor_after_rect(child_ui.min_rect()); - match self.layout().is_horizontal() { - true => { - response.intrinsic_size = Some(Vec2::new( - response.rect.width(), - self.placer.min_item_size().y, - )); - } - false => { - response.intrinsic_size = Some(Vec2::new( - self.placer.min_item_size().x, - response.rect.height(), - )); - } - } + self.advance_cursor_after_rect( + child_ui.min_rect(), + response.intrinsic_size.unwrap_or(response.rect.size()), + ); InnerResponse::new(ret, response) } + pub(crate) fn intrinsic_size(&self) -> Vec2 { + self.placer.intrinsic_size() + } + /// Redirect shapes to another paint layer. /// /// ``` @@ -2520,7 +2536,11 @@ impl Ui { } } - let response = self.allocate_rect(child_ui.min_rect(), Sense::hover()); + let response = self.allocate_rect( + child_ui.min_rect(), + Sense::hover(), + child_ui.intrinsic_size(), + ); // TODO InnerResponse::new(ret, response) } @@ -2810,7 +2830,7 @@ impl Ui { let total_required_width = total_spacing + max_column_width * (num_columns as f32); let size = vec2(self.available_width().max(total_required_width), max_height); - self.advance_cursor_after_rect(Rect::from_min_size(top_left, size)); + self.advance_cursor_after_rect(Rect::from_min_size(top_left, size), Vec2::ZERO); // TODO result } @@ -2865,7 +2885,7 @@ impl Ui { let total_required_width = total_spacing + max_column_width * (NUM_COL as f32); let size = vec2(self.available_width().max(total_required_width), max_height); - self.advance_cursor_after_rect(Rect::from_min_size(top_left, size)); + self.advance_cursor_after_rect(Rect::from_min_size(top_left, size), Vec2::ZERO); // TODO result } diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index dcf37d20355..bd12d2458c2 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -295,7 +295,25 @@ impl Widget for Button<'_> { } desired_size = desired_size.at_least(min_size); - let (rect, mut response) = ui.allocate_at_least(desired_size, sense); + // TODO: Missing a lot + let mut desired_size_2 = galley + .as_ref() + .map_or(Vec2::ZERO, |galley| galley.desired_size()); + let mut desired_right_galley_size = right_galley.as_ref().map_or(Vec2::ZERO, |galley| { + galley.desired_size() + Vec2::new(gap_before_right_text, 0.0) + }); + desired_size_2.x += desired_right_galley_size.x; + desired_size_2.y = desired_size_2.y.max(desired_right_galley_size.y); + desired_size_2 += 2.0 * button_padding; + + // dbg!(desired_size_2); + // ui.ctx().debug_painter().debug_rect( + // Rect::from_min_size(ui.cursor().min, desired_size_2), + // Color32::from_rgb(100, 0, 0), + // "", + // ); + + let (rect, mut response) = ui.allocate_at_least(desired_size, sense, desired_size_2); response.widget_info(|| { if let Some(galley) = &galley { WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), galley.text()) diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index a9906cef118..41ce8df978b 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -58,7 +58,7 @@ pub fn show_color(ui: &mut Ui, color: impl Into, desired_size: Vec2) -> } fn show_color32(ui: &mut Ui, color: Color32, desired_size: Vec2) -> Response { - let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover()); + let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover(), desired_size); if ui.is_rect_visible(rect) { show_color_at(ui.painter(), color, rect); } @@ -116,7 +116,8 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color #![allow(clippy::identity_op)] let desired_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y); - let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag()); + let (rect, response) = + ui.allocate_at_least(desired_size, Sense::click_and_drag(), desired_size); if let Some(mpos) = response.interact_pointer_pos() { *value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0); @@ -183,7 +184,8 @@ fn color_slider_2d( color_at: impl Fn(f32, f32) -> Color32, ) -> Response { let desired_size = Vec2::splat(ui.spacing().slider_width); - let (rect, response) = ui.allocate_at_least(desired_size, Sense::click_and_drag()); + let (rect, response) = + ui.allocate_at_least(desired_size, Sense::click_and_drag(), desired_size); if let Some(mpos) = response.interact_pointer_pos() { *x_value = remap_clamp(mpos.x, rect.left()..=rect.right(), 0.0..=1.0); diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 34b684df637..1a4c49c7803 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -214,10 +214,10 @@ impl Label { assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y)); - let mut response = ui.allocate_rect(rect, sense); + let mut response = ui.allocate_rect(rect, sense, galley.desired_size()); for row in galley.rows.iter().skip(1) { let rect = row.rect.translate(vec2(pos.x, pos.y)); - response |= ui.allocate_rect(rect, sense); + response |= ui.allocate_rect(rect, sense, galley.desired_size()); } (pos, galley, response) } else { diff --git a/crates/egui/src/widgets/selected_label.rs b/crates/egui/src/widgets/selected_label.rs index dfed4d2badd..2b7a8f16e1a 100644 --- a/crates/egui/src/widgets/selected_label.rs +++ b/crates/egui/src/widgets/selected_label.rs @@ -48,7 +48,8 @@ impl Widget for SelectableLabel { let mut desired_size = total_extra + galley.size(); desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y); - let (rect, response) = ui.allocate_at_least(desired_size, Sense::click()); + let (rect, response) = + ui.allocate_at_least(desired_size, Sense::click(), galley.desired_size()); response.widget_info(|| { WidgetInfo::selected( WidgetType::SelectableLabel, diff --git a/crates/egui/src/widgets/separator.rs b/crates/egui/src/widgets/separator.rs index d018cfa4d0d..eabd98d3e45 100644 --- a/crates/egui/src/widgets/separator.rs +++ b/crates/egui/src/widgets/separator.rs @@ -108,7 +108,7 @@ impl Widget for Separator { vec2(spacing, available_space.y) }; - let (rect, response) = ui.allocate_at_least(size, Sense::hover()); + let (rect, response) = ui.allocate_at_least(size, Sense::hover(), size); if ui.is_rect_visible(response.rect) { let stroke = ui.visuals().widgets.noninteractive.bg_stroke; diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 5902820742b..1df7a0653dc 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -650,7 +650,7 @@ impl Slider<'_> { SliderOrientation::Horizontal => vec2(ui.spacing().slider_width, thickness), SliderOrientation::Vertical => vec2(thickness, ui.spacing().slider_width), }; - ui.allocate_response(desired_size, Sense::drag()) + ui.allocate_response(desired_size, Sense::drag(), desired_size) } /// Just the slider, no text diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index b3ac4cd8b39..a9e212b2e9b 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -541,7 +541,7 @@ impl TextEdit<'_> { let desired_height = (desired_height_rows.at_least(1) as f32) * row_height; let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height)); let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size); - let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size); + let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size, desired_outer_size); let rect = outer_rect - margin; // inner rect (excluding frame/margin). let id = id.unwrap_or_else(|| { @@ -739,6 +739,7 @@ impl TextEdit<'_> { ui.allocate_rect( Rect::from_min_size(outer_rect.max, extra_size), Sense::hover(), + response.intrinsic_size.unwrap(), ); } } diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index 231ba4a4f44..30b3458ab4d 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -1,3 +1,4 @@ +use eframe::emath::Vec2; use egui::util::History; pub struct FrameHistory { @@ -62,7 +63,7 @@ impl FrameHistory { // TODO(emilk): we should not use `slider_width` as default graph width. let height = ui.spacing().slider_width; let size = vec2(ui.available_size_before_wrap().x, height); - let (rect, response) = ui.allocate_at_least(size, Sense::hover()); + let (rect, response) = ui.allocate_at_least(size, Sense::hover(), Vec2::ZERO); // TODO let style = ui.style().noninteractive(); let graph_top_cpu_usage = 0.010; diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index dd480181dc2..c385f10e6ea 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -2,7 +2,7 @@ use egui::{ containers::{Frame, Window}, emath, epaint, epaint::PathStroke, - hex_color, lerp, pos2, remap, vec2, Color32, Context, Pos2, Rect, Ui, + hex_color, lerp, pos2, remap, vec2, Color32, Context, Pos2, Rect, Ui, Vec2, }; #[derive(Default)] @@ -43,7 +43,7 @@ impl crate::View for DancingStrings { let time = ui.input(|i| i.time); let desired_size = ui.available_width() * vec2(1.0, 0.35); - let (_id, rect) = ui.allocate_space(desired_size); + let (_id, rect) = ui.allocate_space(desired_size, Vec2::ZERO); // TODO let to_screen = emath::RectTransform::from_to(Rect::from_x_y_ranges(0.0..=1.0, -1.0..=1.0), rect); diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index edb19c3eaa7..454737abaa1 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -189,7 +189,8 @@ impl View for MiscDemoWindow { for i in 0..100 { let r = i as f32 * 0.5; let size = Vec2::splat(2.0 * r + 5.0); - let (rect, _response) = ui.allocate_at_least(size, Sense::hover()); + let (rect, _response) = + ui.allocate_at_least(size, Sense::hover(), Vec2::ZERO); // TODO ui.painter() .circle_filled(rect.center(), r, ui.visuals().text_color()); } @@ -384,7 +385,7 @@ impl BoxPainting { ui.horizontal_wrapped(|ui| { for _ in 0..self.num_boxes { - let (rect, _response) = ui.allocate_at_least(self.size, Sense::hover()); + let (rect, _response) = ui.allocate_at_least(self.size, Sense::hover(), Vec2::ZERO); // TODO ui.painter().rect( rect, self.corner_radius, diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index 7a2ca4d7c9f..2ffcdbb0dcf 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -225,7 +225,7 @@ fn huge_content_painter(ui: &mut egui::Ui) { used_rect = used_rect.union(text_rect); } - ui.allocate_rect(used_rect, Sense::hover()); // make sure it is visible! + ui.allocate_rect(used_rect, Sense::hover(), Vec2::ZERO); // TODO // make sure it is visible! }); } diff --git a/crates/egui_demo_lib/src/demo/tests/window_resize_test.rs b/crates/egui_demo_lib/src/demo/tests/window_resize_test.rs index 39a189a39fd..2d550fb11d9 100644 --- a/crates/egui_demo_lib/src/demo/tests/window_resize_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/window_resize_test.rs @@ -1,3 +1,5 @@ +use egui::Vec2; + pub struct WindowResizeTest { text: String, } @@ -91,7 +93,7 @@ impl crate::Demo for WindowResizeTest { ui.vertical_centered(|ui| { ui.add(crate::egui_github_link_file!()); }); - ui.allocate_space(ui.available_size()); + ui.allocate_space(ui.available_size(), Vec2::ZERO); // TODO }); } } diff --git a/crates/egui_demo_lib/src/rendering_test.rs b/crates/egui_demo_lib/src/rendering_test.rs index e83e643bb83..5e48ae996c1 100644 --- a/crates/egui_demo_lib/src/rendering_test.rs +++ b/crates/egui_demo_lib/src/rendering_test.rs @@ -269,7 +269,7 @@ impl ColorTest { } fn vertex_gradient(ui: &mut Ui, bg_fill: Color32, gradient: &Gradient) -> Response { - let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover()); + let (rect, response) = ui.allocate_at_least(GRADIENT_SIZE, Sense::hover(), Vec2::ZERO); // TODO let rect = rect.round_to_pixels(ui.pixels_per_point()); if bg_fill != Default::default() { let mut mesh = Mesh::default(); diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index fb84169f314..860c9e0716c 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -1,4 +1,4 @@ -use egui::{emath::GuiRounding, Id, Pos2, Rect, Response, Sense, Ui, UiBuilder}; +use egui::{emath::GuiRounding, Id, Pos2, Rect, Response, Sense, Ui, UiBuilder, Vec2}; #[derive(Clone, Copy)] pub(crate) enum CellSize { @@ -166,7 +166,8 @@ impl<'l> StripLayout<'l> { self.set_pos(allocation_rect); - self.ui.advance_cursor_after_rect(allocation_rect); + self.ui + .advance_cursor_after_rect(allocation_rect, Vec2::ZERO); // TODO let response = child_ui.response(); @@ -192,7 +193,7 @@ impl<'l> StripLayout<'l> { let before = self.cursor; self.cursor += delta; let rect = Rect::from_two_pos(before, self.cursor); - self.ui.allocate_rect(rect, Sense::hover()); + self.ui.allocate_rect(rect, Sense::hover(), Vec2::ZERO); // TODO } /// Return the Ui to which the contents where added @@ -243,6 +244,6 @@ impl<'l> StripLayout<'l> { rect.set_right(self.max.x); rect.set_bottom(self.max.y); - self.ui.allocate_rect(rect, Sense::hover()) + self.ui.allocate_rect(rect, Sense::hover(), Vec2::ZERO) // TODO } } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index b228d023e66..1b07d0ebbbc 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -726,6 +726,31 @@ impl Galley { pub fn size(&self) -> Vec2 { self.rect.size() } + + // TODO: Instead return Option? + pub fn desired_size(&self) -> Vec2 { + let mut current_width: f32 = 0.0; + let mut widest_width: f32 = 0.0; + let mut height = self.rows.first().map_or(0.0, |row| row.height()); + for row in &self.rows { + if current_width != 0.0 { + let space = row.glyphs.last(); + if let Some(space) = space { + if space.chr.is_whitespace() { + // TODO: Needed or not? Doesn't seem like it's needed + // current_width += space.advance_width; + } + } + } + current_width += row.rect.width(); + widest_width = widest_width.max(current_width); + if row.ends_with_newline { + height += row.height(); + current_width = 0.0; + } + } + vec2(widest_width, height) + } } impl AsRef for Galley { diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 2a3006c225d..4e2fcd8c37d 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -19,6 +19,7 @@ fn main() -> eframe::Result { let mut checked = true; eframe::run_simple_native("My egui App", options, move |ctx, _frame| { + ctx.options_mut(|o| o.max_passes = 1.try_into().unwrap()); egui::CentralPanel::default().show(ctx, |ui| { // ui.heading("My egui Application"); // ui.horizontal(|ui| { @@ -32,6 +33,7 @@ fn main() -> eframe::Result { // } // ui.label(format!("Hello '{name}', age {age}")); + std::thread::sleep(std::time::Duration::from_millis(100)); let response = ui.button("Hiiii"); let text = if checked { @@ -46,13 +48,20 @@ fn main() -> eframe::Result { ui.button("Hiiii"); - if Button::new(text) - .wrap_mode(TextWrapMode::Extend) - .ui(ui) - .clicked() - { - checked = !checked; - } + ui.horizontal(|ui| { + ui.vertical(|ui| { + if Button::new(text) + // .wrap_mode(TextWrapMode::Extend) + .ui(ui) + .clicked() + { + checked = !checked; + } + ui.button("Button1"); + }); + + ui.button("Button2") + }); ui.button("Some other button"); }); From 4675c4d9a7b2edf410dd94a84a25c8d0ef5fb3ca Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Tue, 18 Mar 2025 15:58:15 +0100 Subject: [PATCH 3/9] Fix labels, propagate preferred size in scrollareas and add more examples from the issue --- crates/egui/src/containers/scroll_area.rs | 2 +- crates/egui/src/widgets/label.rs | 4 +- examples/hello_world_simple/src/main.rs | 114 ++++++++++++++-------- 3 files changed, 80 insertions(+), 40 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index d88c9c2f34d..dd6c6944701 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1245,7 +1245,7 @@ impl Prepared { } } - ui.advance_cursor_after_rect(outer_rect, Vec2::ZERO); // TODO + ui.advance_cursor_after_rect(outer_rect, content_ui.placer().intrinsic_size()); if show_scroll_this_frame != state.show_scroll { ui.ctx().request_repaint(); diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 1a4c49c7803..8eaf9d944a6 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -214,6 +214,7 @@ impl Label { assert!(!galley.rows.is_empty(), "Galleys are never empty"); // collect a response from many rows: let rect = galley.rows[0].rect.translate(vec2(pos.x, pos.y)); + dbg!(galley.desired_size()); let mut response = ui.allocate_rect(rect, sense, galley.desired_size()); for row in galley.rows.iter().skip(1) { let rect = row.rect.translate(vec2(pos.x, pos.y)); @@ -247,7 +248,8 @@ impl Label { }; let galley = ui.fonts(|fonts| fonts.layout_job(layout_job)); - let (rect, response) = ui.allocate_exact_size(galley.size(), sense); + let (rect, response) = + ui.allocate_at_least(galley.size(), sense, galley.desired_size()); // TODO: Change back to allocate_exact_size let galley_pos = match galley.job.halign { Align::LEFT => rect.left_top(), Align::Center => rect.center_top(), diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index 4e2fcd8c37d..db2f2ac40e4 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -2,7 +2,8 @@ #![allow(rustdoc::missing_crate_level_docs)] // it's an example use eframe::egui; -use eframe::egui::{Align, Button, Layout, Popup, TextWrapMode, Widget}; +use eframe::egui::containers::menu::{MenuButton, MenuConfig}; +use eframe::egui::{Align, Button, Layout, Popup, PopupCloseBehavior, TextWrapMode, Widget}; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -18,52 +19,89 @@ fn main() -> eframe::Result { let mut checked = true; + let mut flag = false; + let mut row_count = 5; + eframe::run_simple_native("My egui App", options, move |ctx, _frame| { ctx.options_mut(|o| o.max_passes = 1.try_into().unwrap()); egui::CentralPanel::default().show(ctx, |ui| { - // ui.heading("My egui Application"); - // ui.horizontal(|ui| { - // let name_label = ui.label("Your name: "); - // ui.text_edit_singleline(&mut name) - // .labelled_by(name_label.id); - // }); - // ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); - // if ui.button("Increment").clicked() { - // age += 1; - // } - // ui.label(format!("Hello '{name}', age {age}")); - - std::thread::sleep(std::time::Duration::from_millis(100)); - let response = ui.button("Hiiii"); + // std::thread::sleep(std::time::Duration::from_millis(100)); + // let response = ui.button("Hiiii"); + // + // let text = if checked { + // "short" + // } else { + // "Very long text for this item that should be wrapped" + // }; + // Popup::from_response(&response) + // .layout(Layout::top_down_justified(Align::Min)) + // .show(|ui| { + // // ui.checkbox(&mut checked, text); + // + // ui.button("Hiiii"); + // + // ui.horizontal(|ui| { + // ui.vertical(|ui| { + // if Button::new(text) + // // .wrap_mode(TextWrapMode::Extend) + // .ui(ui) + // .clicked() + // { + // checked = !checked; + // } + // ui.button("Button1"); + // }); + // + // ui.button("Button2") + // }); + // + // ui.button("Some other button"); + // }); - let text = if checked { - "short" - } else { - "Very long text for this item that should be wrapped" - }; - Popup::from_response(&response) - .layout(Layout::top_down_justified(Align::Min)) - .show(|ui| { - // ui.checkbox(&mut checked, text); + // Antoines example - ui.button("Hiiii"); + // ui.label(format!("Showing {} rows", row_count)); + // if MenuButton::new("BTN") + // .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) + // .ui(ui, |ui| { + // ui.radio_value(&mut flag, false, "False"); + // ui.radio_value(&mut flag, true, "True"); + // + // if flag { + // egui::ScrollArea::vertical().show(ui, |ui| { + // for _ in 0..row_count { + // ui.add_space(30.0); + // ui.label("Veeeeeeeeeeeery long text."); + // } + // }); + // } + // }) + // .0 + // .clicked() + // { + // if row_count % 2 == 1 { + // row_count -= 3; + // } else { + // row_count += 5; + // } + // } - ui.horizontal(|ui| { - ui.vertical(|ui| { - if Button::new(text) - // .wrap_mode(TextWrapMode::Extend) - .ui(ui) - .clicked() - { - checked = !checked; + MenuButton::new("Menu") + .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) + .ui(ui, |ui| { + if ui.button("Close menu").clicked() { + ui.close_menu(); + } + ui.collapsing("Collapsing", |ui| { + egui::ScrollArea::both().show(ui, |ui| { + for _ in 0..10 { + ui.label( + "This is a long text label containing \ + a lot of words and spans many lines", + ); } - ui.button("Button1"); }); - - ui.button("Button2") }); - - ui.button("Some other button"); }); }); }) From 2ef859e6a780bcd9ee63c76fe7b38c1581a47025 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 19 Mar 2025 09:20:08 +0100 Subject: [PATCH 4/9] Use correct intrinsic_size in allocate_response --- crates/egui/src/ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 99067eb317b..e4953c4a678 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1310,7 +1310,7 @@ impl Ui { ) -> Response { let (id, rect) = self.allocate_space(desired_size, desired_size_2); let mut response = self.interact(rect, id, sense); - response.intrinsic_size = Some(desired_size); + response.intrinsic_size = Some(desired_size_2); response } From ceb78780dea1a39b0c379620dd910e78b00b8701 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 19 Mar 2025 11:21:29 +0100 Subject: [PATCH 5/9] Cleanup --- crates/egui/src/widgets/checkbox.rs | 1 - examples/hello_world_simple/src/main.rs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/checkbox.rs b/crates/egui/src/widgets/checkbox.rs index 0b468478e93..7bdb6c86fe9 100644 --- a/crates/egui/src/widgets/checkbox.rs +++ b/crates/egui/src/widgets/checkbox.rs @@ -75,7 +75,6 @@ impl Widget for Checkbox<'_> { desired_size = desired_size.at_least(Vec2::splat(spacing.interact_size.y)); desired_size.y = desired_size.y.max(icon_width); - dbg!(desired_size); let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click()); if response.clicked() { diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index db2f2ac40e4..a25881a9c53 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -89,6 +89,7 @@ fn main() -> eframe::Result { MenuButton::new("Menu") .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) .ui(ui, |ui| { + // ui.set_max_width(180.0); if ui.button("Close menu").clicked() { ui.close_menu(); } From d72e85b6b4ed817899a77e204ccdf25bb6caf0fb Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 19 Mar 2025 11:51:03 +0100 Subject: [PATCH 6/9] Add workaround for max_width set_max_width and set_max_height will limit intrinsic size but not max_rect itself --- crates/egui/src/layout.rs | 10 +++++++++- crates/egui/src/placer.rs | 14 +++++++++++++- examples/hello_world_simple/src/main.rs | 3 ++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index cf165c28110..8a9c1e8705f 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -46,7 +46,9 @@ pub(crate) struct Region { /// The cursor can thus be `style.spacing.item_spacing` pixels outside of the `min_rect`. pub(crate) cursor: Rect, - pub intrinsic_size: Vec2, + pub(crate) intrinsic_size: Vec2, + pub max_intrinsic_width: Option, + pub max_intrinsic_height: Option, } impl Region { @@ -429,6 +431,8 @@ impl Layout { max_rect, cursor: self.initial_cursor(max_rect), intrinsic_size: Vec2::default(), + max_intrinsic_width: None, + max_intrinsic_height: None, }; let seed = self.next_widget_position(®ion); region.min_rect = Rect::from_center_size(seed, Vec2::ZERO); @@ -533,6 +537,8 @@ impl Layout { mut max_rect, min_rect, intrinsic_size: min_item_size, + max_intrinsic_width, + max_intrinsic_height, } = *region; match self.main_dir { @@ -595,6 +601,8 @@ impl Layout { max_rect, cursor, intrinsic_size: min_item_size, + max_intrinsic_width, + max_intrinsic_height, }; self.next_frame_ignore_wrap(®ion, child_size) diff --git a/crates/egui/src/placer.rs b/crates/egui/src/placer.rs index 26e2365f9c8..244a7558401 100644 --- a/crates/egui/src/placer.rs +++ b/crates/egui/src/placer.rs @@ -200,7 +200,15 @@ impl Placer { } pub(crate) fn intrinsic_size(&self) -> Vec2 { - self.region.intrinsic_size + //self.region.intrinsic_size.min(self.max_rect().size()) + let mut intrinsic = self.region.intrinsic_size; + if let Some(max_x) = self.region.max_intrinsic_width { + intrinsic.x = intrinsic.x.min(max_x); + } + if let Some(max_y) = self.region.max_intrinsic_height { + intrinsic.y = intrinsic.y.min(max_y); + } + intrinsic } /// Move to the next row in a grid layout or wrapping layout. @@ -253,6 +261,8 @@ impl Placer { region.cursor.max.x = region.max_rect.max.x; region.sanity_check(); + + region.max_intrinsic_width = Some(width); } /// Set the maximum height of the ui. @@ -268,6 +278,8 @@ impl Placer { region.cursor.max.y = region.max_rect.max.y; region.sanity_check(); + + region.max_intrinsic_height = Some(height); } /// Set the minimum width of the ui. diff --git a/examples/hello_world_simple/src/main.rs b/examples/hello_world_simple/src/main.rs index a25881a9c53..01503747957 100644 --- a/examples/hello_world_simple/src/main.rs +++ b/examples/hello_world_simple/src/main.rs @@ -89,12 +89,13 @@ fn main() -> eframe::Result { MenuButton::new("Menu") .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) .ui(ui, |ui| { - // ui.set_max_width(180.0); + ui.set_max_width(180.0); if ui.button("Close menu").clicked() { ui.close_menu(); } ui.collapsing("Collapsing", |ui| { egui::ScrollArea::both().show(ui, |ui| { + // ui.set_width(ui.available_width()); for _ in 0..10 { ui.label( "This is a long text label containing \ From 3e07862b7255883db131627aad16eaaebed57af4 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 17 Apr 2025 12:06:03 +0200 Subject: [PATCH 7/9] Correctly calculate preferred/intrinsic size in AtomLayout --- crates/egui/src/atomics/atom_kind.rs | 16 +++++++++++-- crates/epaint/src/text/text_layout_types.rs | 25 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index 2672e646b41..75b03c04a90 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -81,9 +81,21 @@ impl<'a> AtomKind<'a> { ) -> (Vec2, SizedAtomKind<'a>) { match self { AtomKind::Text(text) => { - let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button); + let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode()); + let desired_size = matches!(wrap_mode, TextWrapMode::Truncate).then(|| { + text.clone() + .into_galley( + ui, + Some(TextWrapMode::Extend), + available_size.x, + TextStyle::Button, + ) + .desired_size() + }); + let galley = + text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button); ( - galley.size(), // TODO(#5762): calculate the preferred size + desired_size.unwrap_or_else(|| galley.desired_size()), SizedAtomKind::Text(galley), ) } diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index f7e11911b73..f6a182dcb79 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -795,6 +795,31 @@ impl Galley { self.rect.size() } + // TODO: Instead return Option? + pub fn desired_size(&self) -> Vec2 { + let mut current_width: f32 = 0.0; + let mut widest_width: f32 = 0.0; + let mut height = self.rows.first().map_or(0.0, |row| row.height()); + for row in &self.rows { + if current_width != 0.0 { + let space = row.glyphs.last(); + if let Some(space) = space { + if space.chr.is_whitespace() { + // TODO: Needed or not? Doesn't seem like it's needed + // current_width += space.advance_width; + } + } + } + current_width += row.rect().width(); + widest_width = widest_width.max(current_width); + if row.ends_with_newline { + height += row.height(); + current_width = 0.0; + } + } + vec2(widest_width, height) + } + pub(crate) fn round_output_to_gui(&mut self) { for placed_row in &mut self.rows { // Optimization: only call `make_mut` if necessary (can cause a deep clone) From cd3ac65c0994ffd82e2b8f3b2fe3e8a860ac9ba1 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Sat, 14 Jun 2025 13:04:36 +0200 Subject: [PATCH 8/9] At least size --- crates/egui/src/atomics/atom.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 4f4b5b75032..b3336b263f4 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -89,7 +89,7 @@ impl<'a> Atom<'a> { SizedAtom { size, - preferred_size: preferred, + preferred_size: preferred.at_least(size), grow: self.grow, kind, } From 3c39dee7f0efc4f1527ec05e604693b5ede95a61 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 16 Jun 2025 10:05:10 +0200 Subject: [PATCH 9/9] Fixes after update --- crates/egui/src/atomics/atom_layout.rs | 7 ++- crates/egui_demo_lib/src/demo/popups.rs | 75 +++++++++++++------------ 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index a25a4b7c6bd..008b9cce84e 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -255,11 +255,12 @@ impl<'a> AtomLayout<'a> { let desired_size = Vec2::new(desired_width, height); let frame_size = (desired_size + margin.sum()).at_least(min_size); - let (_, rect) = ui.allocate_space(frame_size); + let intrinsic_size = + (Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size); + let (_, rect) = ui.allocate_space(frame_size, intrinsic_size); let mut response = ui.interact(rect, id, sense); - response.intrinsic_size = - Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size)); + response.intrinsic_size = Some(intrinsic_size); AllocatedAtomLayout { sized_atoms: sized_items, diff --git a/crates/egui_demo_lib/src/demo/popups.rs b/crates/egui_demo_lib/src/demo/popups.rs index a49fa3e76c9..2da624411ca 100644 --- a/crates/egui_demo_lib/src/demo/popups.rs +++ b/crates/egui_demo_lib/src/demo/popups.rs @@ -42,47 +42,48 @@ impl PopupsDemo { let _ = ui.button("Item"); ui.menu_button("Recursive", |ui| self.nested_menus(ui)); - // if ui.button(if *checked { "short" } else { "Very long text for this item that should be wrapped" }).clicked() { - // *checked = !*checked; - // } - ui.checkbox( - checked, - if *checked { - "short" - } else { - "Very long text for this item that should be wrapped" - }, - ); - }); - ui.menu_button("SubMenu", |ui| { - if ui.button("Open…").clicked() { - ui.close(); - } + // if ui.button(if self.checked { "short" } else { "Very long text for this item that should be wrapped" }).clicked() { + // self.checked = !self.checked; + // } + let checked = self.checked; + ui.checkbox( + &mut self.checked, + if checked { + "short" + } else { + "Very long text for this item that should be wrapped" + }, + ); + }); + ui.menu_button("SubMenu", |ui| { + if ui.button("Open…").clicked() { + ui.close(); + } + let _ = ui.button("Item"); + }); let _ = ui.button("Item"); - }); - let _ = ui.button("Item"); - if ui.button("Open…").clicked() { - ui.close(); - } - }); - ui.menu_image_text_button( - include_image!("../../data/icon.png"), - "I have an icon!", - |ui| { - let _ = ui.button("Item1"); - let _ = ui.button("Item2"); - let _ = ui.button("Item3"); - let _ = ui.button("Item4"); if ui.button("Open…").clicked() { ui.close(); } - }, - ); - let _ = ui.button("Very long text for this item that should be wrapped"); - SubMenuButton::new("Always CloseOnClickOutside") - .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) - .ui(ui, |ui| { - ui.checkbox(&mut self.checked, "Checkbox"); + }); + ui.menu_image_text_button( + include_image!("../../data/icon.png"), + "I have an icon!", + |ui| { + let _ = ui.button("Item1"); + let _ = ui.button("Item2"); + let _ = ui.button("Item3"); + let _ = ui.button("Item4"); + if ui.button("Open…").clicked() { + ui.close(); + } + }, + ); + let _ = ui.button("Very long text for this item that should be wrapped"); + SubMenuButton::new("Always CloseOnClickOutside") + .config(MenuConfig::new().close_behavior(PopupCloseBehavior::CloseOnClickOutside)) + .ui(ui, |ui| { + ui.checkbox(&mut self.checked, "Checkbox"); // Customized color SubMenuButton let is_bright = self.color.intensity() > 0.5;