diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index 997307bd..5e3d03b8 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -180,6 +180,8 @@ impl CollapsingHeader { let id = ui.make_persistent_id(id_source); + let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open); + let available = ui.available_finite(); let text_pos = available.min + vec2(ui.style().spacing.indent, 0.0); let galley = label.layout_width(ui, available.right() - text_pos.x); @@ -192,53 +194,57 @@ impl CollapsingHeader { galley.size.y + 2.0 * ui.style().spacing.button_padding.y, ); desired_size = desired_size.at_least(ui.style().spacing.interact_size); - let rect = ui.allocate_space(desired_size); - let header_response = ui.interact(rect, id, Sense::click()); - let text_pos = pos2( - text_pos.x, - header_response.rect.center().y - galley.size.y / 2.0, - ); + let header_response; - let mut state = State::from_memory_with_default_open(ui.ctx(), id, default_open); - if header_response.clicked { - state.toggle(ui); + if let Some(rect) = ui.request_space(desired_size) { + header_response = ui.interact(rect, id, Sense::click()); + let text_pos = pos2( + text_pos.x, + header_response.rect.center().y - galley.size.y / 2.0, + ); + + if header_response.clicked { + state.toggle(ui); + } + + let bg_index = ui.painter().add(PaintCmd::Noop); + + { + let (mut icon_rect, _) = ui.style().spacing.icon_rectangles(header_response.rect); + icon_rect.set_center(pos2( + header_response.rect.left() + ui.style().spacing.indent / 2.0, + header_response.rect.center().y, + )); + let icon_response = Response { + rect: icon_rect, + ..header_response.clone() + }; + let openness = state.openness(ui.ctx(), id); + paint_icon(ui, openness, &icon_response); + } + + let painter = ui.painter(); + painter.galley( + text_pos, + galley, + label.text_style_or_default(ui.style()), + ui.style().interact(&header_response).text_color(), + ); + + painter.set( + bg_index, + PaintCmd::Rect { + rect: header_response.rect, + corner_radius: ui.style().interact(&header_response).corner_radius, + fill: ui.style().interact(&header_response).bg_fill, + stroke: Default::default(), + }, + ); + } else { + header_response = Default::default(); } - let bg_index = ui.painter().add(PaintCmd::Noop); - - { - let (mut icon_rect, _) = ui.style().spacing.icon_rectangles(header_response.rect); - icon_rect.set_center(pos2( - header_response.rect.left() + ui.style().spacing.indent / 2.0, - header_response.rect.center().y, - )); - let icon_response = Response { - rect: icon_rect, - ..header_response.clone() - }; - let openness = state.openness(ui.ctx(), id); - paint_icon(ui, openness, &icon_response); - } - - let painter = ui.painter(); - painter.galley( - text_pos, - galley, - label.text_style_or_default(ui.style()), - ui.style().interact(&header_response).text_color(), - ); - - painter.set( - bg_index, - PaintCmd::Rect { - rect: header_response.rect, - corner_radius: ui.style().interact(&header_response).corner_radius, - fill: ui.style().interact(&header_response).bg_fill, - stroke: Default::default(), - }, - ); - Prepared { id, header_response, diff --git a/egui/src/containers/combo_box.rs b/egui/src/containers/combo_box.rs index 8c894150..5d6f0327 100644 --- a/egui/src/containers/combo_box.rs +++ b/egui/src/containers/combo_box.rs @@ -38,7 +38,7 @@ pub fn combo_box( let advance = full_minimum_width - icon_width - ui.min_rect().width(); ui.advance_cursor(advance.at_least(0.0)); - let icon_rect = ui.allocate_space(Vec2::splat(icon_width)); + let icon_rect = unwrap_or_return_default!(ui.request_space(Vec2::splat(icon_width))); let button_rect = ui.min_rect().expand2(ui.style().spacing.button_padding); let mut response = ui.interact(button_rect, button_id, Sense::click()); response.active |= button_active; @@ -91,6 +91,7 @@ fn button_frame( add_contents(&mut content_ui); let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin); + ui.allocate_space(outer_rect.size()); let mut response = ui.interact(outer_rect, id, sense); response.active |= button_active; @@ -106,8 +107,6 @@ fn button_frame( }, ); - ui.allocate_space(outer_rect.size()); - response } diff --git a/egui/src/context.rs b/egui/src/context.rs index cbfd47c9..4591e122 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -436,7 +436,7 @@ impl Context { if id.is_none() || sense == Sense::nothing() || !layer_id.allow_interaction() { // Not interested or allowed input: return Response { - ctx: self.clone(), + ctx: Some(self.clone()), sense, rect, hovered, @@ -461,7 +461,7 @@ impl Context { if self.input.mouse.pressed { if hovered { let mut response = Response { - ctx: self.clone(), + ctx: Some(self.clone()), sense, rect, hovered: true, @@ -491,7 +491,7 @@ impl Context { } else { // miss Response { - ctx: self.clone(), + ctx: Some(self.clone()), sense, rect, hovered, @@ -504,7 +504,7 @@ impl Context { } else if self.input.mouse.released { let clicked = hovered && active && self.input.mouse.could_be_click; Response { - ctx: self.clone(), + ctx: Some(self.clone()), sense, rect, hovered, @@ -515,7 +515,7 @@ impl Context { } } else if self.input.mouse.down { Response { - ctx: self.clone(), + ctx: Some(self.clone()), sense, rect, hovered: hovered && active, @@ -526,7 +526,7 @@ impl Context { } } else { Response { - ctx: self.clone(), + ctx: Some(self.clone()), sense, rect, hovered, diff --git a/egui/src/demos/app.rs b/egui/src/demos/app.rs index ad810bad..814a72d5 100644 --- a/egui/src/demos/app.rs +++ b/egui/src/demos/app.rs @@ -105,7 +105,9 @@ impl FrameHistory { // TODO: we should not use `slider_width` as default graph width. let height = ui.style().spacing.slider_width; - let rect = ui.allocate_space(vec2(ui.available_finite().width(), height)); + let rect = unwrap_or_return_default!( + ui.request_space(vec2(ui.available_finite().width(), height)) + ); let style = ui.style().noninteractive(); let mut cmds = vec![PaintCmd::Rect { diff --git a/egui/src/demos/color_test.rs b/egui/src/demos/color_test.rs index d7fc71ff..e0e1d1d9 100644 --- a/egui/src/demos/color_test.rs +++ b/egui/src/demos/color_test.rs @@ -267,7 +267,7 @@ impl ColorTest { fn vertex_gradient(ui: &mut Ui, bg_fill: Srgba, gradient: &Gradient) -> Response { use crate::paint::*; - let rect = ui.allocate_space(GRADIENT_SIZE); + let rect = unwrap_or_return_default!(ui.request_space(GRADIENT_SIZE)); if bg_fill != Default::default() { let mut triangles = Triangles::default(); triangles.add_colored_rect(rect, bg_fill); diff --git a/egui/src/demos/demo_window.rs b/egui/src/demos/demo_window.rs index 4bbb01de..1cb8eee5 100644 --- a/egui/src/demos/demo_window.rs +++ b/egui/src/demos/demo_window.rs @@ -93,16 +93,17 @@ impl DemoWindow { .show(ui, |ui| { ui.horizontal(|ui| { ui.label("You can pretty easily paint your own small icons:"); - let rect = ui.allocate_space(Vec2::splat(16.0)); - let painter = ui.painter(); - let c = rect.center(); - let r = rect.width() / 2.0 - 1.0; - let color = Srgba::gray(128); - let stroke = Stroke::new(1.0, color); - painter.circle_stroke(c, r, stroke); - painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); - painter.line_segment([c, c + r * Vec2::angled(TAU * 1.0 / 8.0)], stroke); - painter.line_segment([c, c + r * Vec2::angled(TAU * 3.0 / 8.0)], stroke); + if let Some(rect) = ui.request_space(Vec2::splat(16.0)) { + let painter = ui.painter(); + let c = rect.center(); + let r = rect.width() / 2.0 - 1.0; + let color = Srgba::gray(128); + let stroke = Stroke::new(1.0, color); + painter.circle_stroke(c, r, stroke); + painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke); + painter.line_segment([c, c + r * Vec2::angled(TAU * 1.0 / 8.0)], stroke); + painter.line_segment([c, c + r * Vec2::angled(TAU * 3.0 / 8.0)], stroke); + } }); }); } @@ -210,9 +211,9 @@ impl BoxPainting { ui.add(Slider::f32(&mut self.stroke_width, 0.0..=10.0).text("stroke_width")); ui.add(Slider::usize(&mut self.num_boxes, 0..=5).text("num_boxes")); - let pos = ui - .allocate_space(vec2(self.size.x * (self.num_boxes as f32), self.size.y)) - .min; + let rect = ui.request_space(vec2(self.size.x * (self.num_boxes as f32), self.size.y)); + let rect = unwrap_or_return_default!(rect); + let pos = rect.min; let mut cmds = vec![]; for i in 0..self.num_boxes { @@ -265,7 +266,7 @@ impl Painting { } fn content(&mut self, ui: &mut Ui) { - let rect = ui.allocate_space(ui.available_finite().size()); + let rect = unwrap_or_return_default!(ui.request_space(ui.available_finite().size())); let response = ui.interact(rect, ui.id(), Sense::drag()); let rect = response.rect; let clip_rect = ui.clip_rect().intersect(rect); // Make sure we don't paint out of bounds diff --git a/egui/src/demos/toggle_switch.rs b/egui/src/demos/toggle_switch.rs index b55766d1..e9a2f393 100644 --- a/egui/src/demos/toggle_switch.rs +++ b/egui/src/demos/toggle_switch.rs @@ -13,7 +13,7 @@ use crate::*; pub fn toggle(ui: &mut Ui, on: &mut bool) -> Response { // Widget code can be broken up in four steps: // 1. Decide a size for the widget - // 2. Allocate space for it + // 2. Request space for it // 3. Handle interactions with the widget (if any) // 4. Paint the widget @@ -22,9 +22,14 @@ pub fn toggle(ui: &mut Ui, on: &mut bool) -> Response { // but in this example we have a fixed size widget of the default size for a button: let desired_size = ui.style().spacing.interact_size; - // 2. Allocating space: + // 2. Requesting space: // This is where we get a region (`Rect`) of the screen assigned. - let rect = ui.allocate_space(desired_size); + let rect = ui.request_space(desired_size); + + // If we get `None` back from `request_space`, it means this widgets isn't visible. + // In this case we shouldn't do anything else and just return early. + // Egui has a helper macro for this: + let rect = unwrap_or_return_default!(rect); // 3. Interact: Time to check for clicks! // To do that we need an `Id` for the button. diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index 7e303afb..462da09a 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -19,7 +19,7 @@ impl Texture { if size.x > ui.available().width() { size *= ui.available().width() / size.x; } - let rect = ui.allocate_space(size); + let rect = unwrap_or_return_default!(ui.request_space(size)); let mut triangles = Triangles::default(); triangles.add_rect_with_uv(rect, [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), WHITE); ui.painter().add(PaintCmd::triangles(triangles)); @@ -34,7 +34,7 @@ impl Texture { .mouse .pos .unwrap_or_else(|| ui.min_rect().left_top()); - let zoom_rect = ui.allocate_space(vec2(128.0, 128.0)); + let zoom_rect = unwrap_or_return_default!(ui.request_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/lib.rs b/egui/src/lib.rs index f31d1a12..d3dbd148 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -120,3 +120,15 @@ pub fn text_egui_e2e() { assert!(!paint_jobs.is_empty()); } } + +#[macro_export] +macro_rules! unwrap_or_return_default { + ($option:expr) => { + match $option { + Some(value) => value, + None => { + return Default::default(); + } + } + }; +} diff --git a/egui/src/style.rs b/egui/src/style.rs index 99fa3ada..3d758394 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -1,11 +1,6 @@ #![allow(clippy::if_same_then_else)] -use crate::{ - color::*, - math::*, - paint::{Stroke, TextStyle}, - types::*, -}; +use crate::{color::*, *}; /// Specifies the look and feel of a `Ui`. #[derive(Clone, Debug, PartialEq)] @@ -474,7 +469,8 @@ impl Stroke { ui.label(text); // stroke preview: - let stroke_rect = ui.allocate_space(ui.style().spacing.interact_size); + let stroke_rect = + unwrap_or_return_default!(ui.request_space(ui.style().spacing.interact_size)); let left = stroke_rect.left_center(); let right = stroke_rect.right_center(); ui.painter().line_segment([left, right], (*width, *color)); diff --git a/egui/src/types.rs b/egui/src/types.rs index 47390aab..b627f803 100644 --- a/egui/src/types.rs +++ b/egui/src/types.rs @@ -56,8 +56,10 @@ impl Default for CursorIcon { #[derive(Clone)] pub struct Response { // CONTEXT: - /// Used for optionally showing a tooltip - pub ctx: Arc, + /// Used for optionally showing a tooltip. + /// If `None`, we likely come from a cull widgets and shouldn't show any + /// tooltip. + pub ctx: Option>, // IN: /// The area of the screen we are talking about @@ -83,17 +85,18 @@ pub struct Response { pub has_kb_focus: bool, } -impl std::fmt::Debug for Response { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Response") - .field("rect", &self.rect) - .field("sense", &self.sense) - .field("hovered", &self.hovered) - .field("clicked", &self.clicked) - .field("double_clicked", &self.double_clicked) - .field("active", &self.active) - .field("has_kb_focus", &self.has_kb_focus) - .finish() +impl Default for Response { + fn default() -> Self { + Self { + ctx: None, + rect: Rect::nothing(), + sense: Sense::nothing(), + hovered: false, + clicked: false, + double_clicked: false, + active: false, + has_kb_focus: false, + } } } @@ -101,7 +104,11 @@ impl Response { /// Show this UI if the item was hovered (i.e. a tooltip) pub fn on_hover_ui(self, add_contents: impl FnOnce(&mut Ui)) -> Self { if self.hovered { - crate::containers::show_tooltip(&self.ctx, add_contents); + if let Some(ctx) = &self.ctx { + crate::containers::show_tooltip(ctx, add_contents); + } else { + panic!("We shouldn't be able to hover something without a Context"); + } } self } @@ -123,9 +130,12 @@ impl Response { /// A logical "or" operation. /// For instance `a.union(b).hovered` means "was either a or b hovered?". pub fn union(&self, other: Self) -> Self { - assert!(Arc::ptr_eq(&self.ctx, &other.ctx)); + if let (Some(lc), Some(rc)) = (&self.ctx, &other.ctx) { + debug_assert!(Arc::ptr_eq(lc, rc)); + } + Self { - ctx: other.ctx, + ctx: other.ctx.or_else(|| self.ctx.clone()), rect: self.rect.union(other.rect), sense: self.sense.union(other.sense), hovered: self.hovered || other.hovered, @@ -137,6 +147,23 @@ impl Response { } } +impl std::fmt::Debug for Response { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Response") + .field("ctx", &self.ctx.is_some()) + .field("rect", &self.rect) + .field("sense", &self.sense) + .field("hovered", &self.hovered) + .field("clicked", &self.clicked) + .field("double_clicked", &self.double_clicked) + .field("active", &self.active) + .field("has_kb_focus", &self.has_kb_focus) + .finish() + } +} + +// ---------------------------------------------------------------------------- + /// To summarize the response from many widgets you can use this pattern: /// /// ``` diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 28137223..d77089b4 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -462,6 +462,26 @@ impl Ui { rect } + /// Potential future of `allocate_space`. + /// Returns `None` if the allocated rectangle is outside the clip rect and is thus invisible. + /// Works well together with the `unwrap_or_return_default!` macro: + /// ``` + /// # use egui::*; + /// # let mut ui = Ui::test(); + /// let desired_size = vec2(100.0, 200.0); + /// let rect = unwrap_or_return_default!(ui.request_space(desired_size)); + /// assert_eq!(rect, Rect::from_min_size(pos2(0.0, 0.0), desired_size)); + /// ``` + /// + pub fn request_space(&mut self, desired_size: Vec2) -> Option { + let rect = self.allocate_space(desired_size); + if self.clip_rect().intersects(rect) { + Some(rect) + } else { + None + } + } + /// Reserve this much space and move the cursor. /// Returns where to put the widget. fn reserve_space_impl(&mut self, child_size: Vec2) -> Rect { diff --git a/egui/src/widgets/color_picker.rs b/egui/src/widgets/color_picker.rs index 698ce17f..ac89665e 100644 --- a/egui/src/widgets/color_picker.rs +++ b/egui/src/widgets/color_picker.rs @@ -43,7 +43,7 @@ pub fn show_color(ui: &mut Ui, color: impl Into, desired_size: Vec2) -> R } fn show_srgba(ui: &mut Ui, srgba: Srgba, desired_size: Vec2) -> Response { - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); background_checkers(ui.painter(), rect); ui.painter().add(PaintCmd::Rect { rect, @@ -56,7 +56,7 @@ fn show_srgba(ui: &mut Ui, srgba: Srgba, desired_size: Vec2) -> Response { fn color_button(ui: &mut Ui, color: Srgba) -> Response { let desired_size = ui.style().spacing.interact_size; - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); let id = ui.make_position_id(); let response = ui.interact(rect, id, Sense::click()); let visuals = ui.style().interact(&response); @@ -77,7 +77,7 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Srgba ui.style().spacing.slider_width, ui.style().spacing.interact_size.y * 2.0, ); - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); let id = ui.make_position_id(); let response = ui.interact(rect, id, Sense::click_and_drag()); @@ -136,7 +136,7 @@ fn color_slider_2d( color_at: impl Fn(f32, f32) -> Srgba, ) -> Response { let desired_size = Vec2::splat(ui.style().spacing.slider_width); - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); let id = ui.make_position_id(); let response = ui.interact(rect, id, Sense::click_and_drag()); diff --git a/egui/src/widgets/image.rs b/egui/src/widgets/image.rs index cd7a9826..37836589 100644 --- a/egui/src/widgets/image.rs +++ b/egui/src/widgets/image.rs @@ -49,7 +49,7 @@ impl Widget for Image { bg_fill, tint, } = self; - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); if bg_fill != Default::default() { let mut triangles = Triangles::default(); triangles.add_colored_rect(rect, bg_fill); diff --git a/egui/src/widgets/mod.rs b/egui/src/widgets/mod.rs index 0bac8eb2..ab94f8be 100644 --- a/egui/src/widgets/mod.rs +++ b/egui/src/widgets/mod.rs @@ -135,7 +135,7 @@ macro_rules! label { impl Widget for Label { fn ui(self, ui: &mut Ui) -> Response { let galley = self.layout(ui); - let rect = ui.allocate_space(galley.size); + let rect = unwrap_or_return_default!(ui.request_space(galley.size)); self.paint_galley(ui, rect.min, galley); ui.interact_hover(rect) } @@ -207,7 +207,7 @@ impl Widget for Hyperlink { let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); let font = &ui.fonts()[text_style]; let galley = font.layout_multiline(text, ui.available().width()); - let rect = ui.allocate_space(galley.size); + let rect = unwrap_or_return_default!(ui.request_space(galley.size)); let id = ui.make_position_id(); let response = ui.interact(rect, id, Sense::click()); @@ -318,7 +318,7 @@ impl Widget for Button { let galley = font.layout_multiline(text, ui.available().width()); let mut desired_size = galley.size + 2.0 * button_padding; desired_size = desired_size.at_least(ui.style().spacing.interact_size); - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); let id = ui.make_position_id(); let response = ui.interact(rect, id, sense); @@ -390,7 +390,7 @@ impl<'a> Widget for Checkbox<'a> { button_padding + vec2(icon_width + icon_spacing, 0.0) + galley.size + button_padding; desired_size = desired_size.at_least(spacing.interact_size); desired_size.y = desired_size.y.max(icon_width); - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); let id = ui.make_position_id(); let response = ui.interact(rect, id, Sense::click()); @@ -475,7 +475,7 @@ impl Widget for RadioButton { button_padding + vec2(icon_width + icon_spacing, 0.0) + galley.size + button_padding; desired_size = desired_size.at_least(ui.style().spacing.interact_size); desired_size.y = desired_size.y.max(icon_width); - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); let id = ui.make_position_id(); let response = ui.interact(rect, id, Sense::click()); @@ -544,7 +544,8 @@ impl Widget for Separator { let (points, rect) = match ui.layout().dir() { Direction::Horizontal => { - let rect = ui.allocate_space(vec2(spacing, available_space.y)); + let rect = + unwrap_or_return_default!(ui.request_space(vec2(spacing, available_space.y))); ( [ pos2(rect.center().x, rect.top()), @@ -554,7 +555,8 @@ impl Widget for Separator { ) } Direction::Vertical => { - let rect = ui.allocate_space(vec2(available_space.x, spacing)); + let rect = + unwrap_or_return_default!(ui.request_space(vec2(available_space.x, spacing))); ( [ pos2(rect.left(), rect.center().y), diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index a4d50fda..ede870d2 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -122,7 +122,7 @@ impl<'t> Widget for TextEdit<'t> { galley.size.x.max(desired_width.min(available_width)), galley.size.y.max(line_spacing), ); - let rect = ui.allocate_space(desired_size); + let rect = unwrap_or_return_default!(ui.request_space(desired_size)); let sense = if enabled { Sense::click_and_drag() } else {