diff --git a/Cargo.lock b/Cargo.lock index c536538b39f..9bab19057bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3636,6 +3636,15 @@ dependencies = [ "env_logger", ] +[[package]] +name = "test_stack_frame" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_extras", + "env_logger", +] + [[package]] name = "test_viewports" version = "0.1.0" diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 33e3fae9783..12fea39b7e5 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -81,6 +81,7 @@ impl AreaState { #[derive(Clone, Copy, Debug)] pub struct Area { pub(crate) id: Id, + kind: UiKind, sense: Option, movable: bool, interactable: bool, @@ -105,6 +106,7 @@ impl Area { pub fn new(id: Id) -> Self { Self { id, + kind: UiKind::GenericArea, sense: None, movable: true, interactable: true, @@ -130,6 +132,15 @@ impl Area { self } + /// Change the [`UiKind`] of the arena. + /// + /// Default to [`UiKind::GenericArea`]. + #[inline] + pub fn kind(mut self, kind: UiKind) -> Self { + self.kind = kind; + self + } + pub fn layer(&self) -> LayerId { LayerId::new(self.order, self.id) } @@ -301,6 +312,7 @@ impl Area { } pub(crate) struct Prepared { + kind: UiKind, layer_id: LayerId, state: AreaState, move_response: Response, @@ -334,6 +346,7 @@ impl Area { pub(crate) fn begin(self, ctx: &Context) -> Prepared { let Self { id, + kind, sense, movable, order, @@ -453,6 +466,7 @@ impl Area { move_response.interact_rect = state.rect(); Prepared { + kind, layer_id, state, move_response, @@ -508,6 +522,10 @@ impl Prepared { self.layer_id.id, max_rect, clip_rect, + UiStackInfo { + kind: Some(self.kind), + frame: Default::default(), + }, ); if self.fade_in { @@ -530,6 +548,7 @@ impl Prepared { #[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`. pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response { let Self { + kind: _, layer_id, mut state, move_response, diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index b4c7bbca7d3..f4c18b51b92 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -418,7 +418,7 @@ fn button_frame( outer_rect.set_height(outer_rect.height().at_least(interact_size.y)); let inner_rect = outer_rect.shrink2(margin); - let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); + let mut content_ui = ui.child_ui(inner_rect, *ui.layout(), None); add_contents(&mut content_ui); let mut outer_rect = content_ui.min_rect().expand2(margin); diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index 8568a8f0b9b..04c2d246d29 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -250,7 +250,14 @@ impl Frame { inner_rect.max.x = inner_rect.max.x.max(inner_rect.min.x); inner_rect.max.y = inner_rect.max.y.max(inner_rect.min.y); - let content_ui = ui.child_ui(inner_rect, *ui.layout()); + let content_ui = ui.child_ui( + inner_rect, + *ui.layout(), + Some(UiStackInfo { + frame: self, + kind: Some(UiKind::Frame), + }), + ); // content_ui.set_clip_rect(outer_rect_bounds.shrink(self.stroke.width * 0.5)); // Can't do this since we don't know final size yet diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 324e17bf91f..5aa70cd4e1b 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -257,7 +257,18 @@ impl SidePanel { } } - let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id); + let mut panel_ui = ui.child_ui_with_id_source_and_frame_data( + panel_rect, + Layout::top_down(Align::Min), + id, + Some(UiStackInfo { + kind: Some(match side { + Side::Left => UiKind::LeftPanel, + Side::Right => UiKind::RightPanel, + }), + frame: Frame::default(), + }), + ); panel_ui.expand_to_include_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { @@ -348,7 +359,17 @@ impl SidePanel { let side = self.side; let available_rect = ctx.available_rect(); let clip_rect = ctx.screen_rect(); - let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect); + let mut panel_ui = Ui::new( + ctx.clone(), + layer_id, + self.id, + available_rect, + clip_rect, + UiStackInfo { + kind: None, // set by show_inside_dyn + frame: Frame::default(), + }, + ); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; @@ -723,7 +744,18 @@ impl TopBottomPanel { } } - let mut panel_ui = ui.child_ui_with_id_source(panel_rect, Layout::top_down(Align::Min), id); + let mut panel_ui = ui.child_ui_with_id_source_and_frame_data( + panel_rect, + Layout::top_down(Align::Min), + id, + Some(UiStackInfo { + kind: Some(match side { + TopBottomSide::Top => UiKind::TopPanel, + TopBottomSide::Bottom => UiKind::BottomPanel, + }), + frame: Frame::default(), + }), + ); panel_ui.expand_to_include_rect(panel_rect); let frame = frame.unwrap_or_else(|| Frame::side_top_panel(ui.style())); let inner_response = frame.show(&mut panel_ui, |ui| { @@ -816,7 +848,17 @@ impl TopBottomPanel { let side = self.side; let clip_rect = ctx.screen_rect(); - let mut panel_ui = Ui::new(ctx.clone(), layer_id, self.id, available_rect, clip_rect); + let mut panel_ui = Ui::new( + ctx.clone(), + layer_id, + self.id, + available_rect, + clip_rect, + UiStackInfo { + kind: None, // set by show_inside_dyn + frame: Frame::default(), + }, + ); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); let rect = inner_response.response.rect; @@ -1045,7 +1087,14 @@ impl CentralPanel { let Self { frame } = self; let panel_rect = ui.available_rect_before_wrap(); - let mut panel_ui = ui.child_ui(panel_rect, Layout::top_down(Align::Min)); + let mut panel_ui = ui.child_ui( + panel_rect, + Layout::top_down(Align::Min), + Some(UiStackInfo { + kind: Some(UiKind::CentralPanel), + frame: Frame::default(), + }), + ); let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style())); frame.show(&mut panel_ui, |ui| { @@ -1074,7 +1123,17 @@ impl CentralPanel { let id = Id::new((ctx.viewport_id(), "central_panel")); let clip_rect = ctx.screen_rect(); - let mut panel_ui = Ui::new(ctx.clone(), layer_id, id, available_rect, clip_rect); + let mut panel_ui = Ui::new( + ctx.clone(), + layer_id, + id, + available_rect, + clip_rect, + UiStackInfo { + kind: None, // set by show_inside_dyn + frame: Frame::default(), + }, + ); let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index ed2a15989ac..64623f25dfd 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -124,6 +124,7 @@ fn show_tooltip_at_avoid_dyn<'c, R>( ); let InnerResponse { inner, response } = Area::new(tooltip_area_id) + .kind(UiKind::Popup) .order(Order::Tooltip) .pivot(pivot) .fixed_pos(anchor) @@ -287,6 +288,7 @@ pub fn popup_above_or_below_widget( let inner_width = widget_response.rect.width() - frame_margin.sum().x; let inner = Area::new(popup_id) + .kind(UiKind::Popup) .order(Order::Foreground) .constrain(true) .fixed_pos(pos) diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index e2654ca5a6c..30536eee711 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -270,7 +270,14 @@ impl Resize { content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region - let mut content_ui = ui.child_ui(inner_rect, *ui.layout()); + let mut content_ui = ui.child_ui( + inner_rect, + *ui.layout(), + Some(UiStackInfo { + kind: Some(UiKind::Resize), + frame: Frame::default(), + }), + ); content_ui.set_clip_rect(content_clip_rect); Prepared { diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 229870db9d4..736703a6fb9 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -556,7 +556,14 @@ impl ScrollArea { } let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size); - let mut content_ui = ui.child_ui(content_max_rect, *ui.layout()); + let mut content_ui = ui.child_ui( + content_max_rect, + *ui.layout(), + Some(UiStackInfo { + kind: Some(UiKind::ScrollArea), + frame: Frame::default(), + }), + ); { // Clip the content, but only when we really need to: diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index cc6cea3f9e3..fc8c30c7367 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -49,7 +49,9 @@ impl<'open> Window<'open> { /// If you need a changing title, you must call `window.id(…)` with a fixed id. pub fn new(title: impl Into) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading); - let area = Area::new(Id::new(title.text())).constrain(true); + let area = Area::new(Id::new(title.text())) + .kind(UiKind::Window) + .constrain(true); Self { title, open: None, diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 517ee4c8ca5..3358e09b18b 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -400,6 +400,7 @@ mod sense; pub mod style; pub mod text_selection; mod ui; +mod ui_stack; pub mod util; pub mod viewport; mod widget_rect; @@ -466,6 +467,7 @@ pub use { style::{FontSelection, Style, TextStyle, Visuals}, text::{Galley, TextFormat}, ui::Ui, + ui_stack::*, viewport::*, widget_rect::{WidgetRect, WidgetRects}, widget_text::{RichText, WidgetText}, diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index d23d5d20f31..d4840fd0092 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -146,6 +146,7 @@ fn menu_popup<'c, R>( }; let area = Area::new(menu_id.with("__menu")) + .kind(UiKind::Menu) .order(Order::Foreground) .fixed_pos(pos) .constrain_to(ctx.screen_rect()) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 94cfb638b01..ea46e33b43a 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -67,6 +67,9 @@ pub struct Ui { /// Indicates whether this Ui belongs to a Menu. menu_state: Option>>, + + /// The [`UiStack`] for this [`Ui`]. + stack: Arc, } impl Ui { @@ -77,17 +80,36 @@ impl Ui { /// /// Normally you would not use this directly, but instead use /// [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`]. - pub fn new(ctx: Context, layer_id: LayerId, id: Id, max_rect: Rect, clip_rect: Rect) -> Self { + pub fn new( + ctx: Context, + layer_id: LayerId, + id: Id, + max_rect: Rect, + clip_rect: Rect, + stack_frame_data: UiStackInfo, + ) -> Self { let style = ctx.style(); + let layout = Layout::default(); + let placer = Placer::new(max_rect, layout); + let stack_frame = UiStack { + id, + layout_direction: layout.main_dir, + kind: stack_frame_data.kind, + frame: stack_frame_data.frame, + parent: None, + min_rect: placer.min_rect(), + max_rect: placer.max_rect(), + }; let ui = Ui { id, next_auto_id_source: id.with("auto").value(), painter: Painter::new(ctx, layer_id, clip_rect), style, - placer: Placer::new(max_rect, Layout::default()), + placer, enabled: true, sizing_pass: false, menu_state: None, + stack: Arc::new(stack_frame), }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -105,16 +127,25 @@ impl Ui { } /// Create a new [`Ui`] at a specific region. - pub fn child_ui(&mut self, max_rect: Rect, layout: Layout) -> Self { - self.child_ui_with_id_source(max_rect, layout, "child") + /// + /// Note: calling this function twice from the same [`Ui`] will create a conflict of id. Use + /// [`Self::scope`] if needed. + pub fn child_ui( + &mut self, + max_rect: Rect, + layout: Layout, + stack_frame_data: Option, + ) -> Self { + self.child_ui_with_id_source_and_frame_data(max_rect, layout, "child", stack_frame_data) } /// Create a new [`Ui`] at a specific region with a specific id. - pub fn child_ui_with_id_source( + pub fn child_ui_with_id_source_and_frame_data( &mut self, max_rect: Rect, mut layout: Layout, id_source: impl Hash, + stack_frame_data: Option, ) -> Self { if self.sizing_pass { // During the sizing pass we want widgets to use up as little space as possible, @@ -128,15 +159,29 @@ impl Ui { debug_assert!(!max_rect.any_nan()); let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value(); self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1); + + let new_id = self.id.with(id_source); + let placer = Placer::new(max_rect, layout); + let ui_stack_info = stack_frame_data.unwrap_or_default(); + let stack_frame = UiStack { + id: new_id, + layout_direction: layout.main_dir, + kind: ui_stack_info.kind, + frame: ui_stack_info.frame, + parent: Some(self.stack.clone()), + min_rect: placer.min_rect(), + max_rect: placer.max_rect(), + }; let child_ui = Ui { - id: self.id.with(id_source), + id: new_id, next_auto_id_source, painter: self.painter.clone(), style: self.style.clone(), - placer: Placer::new(max_rect, layout), + placer, enabled: self.enabled, sizing_pass: self.sizing_pass, menu_state: self.menu_state.clone(), + stack: Arc::new(stack_frame), }; // Register in the widget stack early, to ensure we are behind all widgets we contain: @@ -258,6 +303,11 @@ impl Ui { &mut self.style_mut().visuals } + /// Get a reference to this [`Ui`]'s [`UiStack`]. + pub fn stack(&self) -> Arc { + self.stack.clone() + } + /// Get a reference to the parent [`Context`]. #[inline] pub fn ctx(&self) -> &Context { @@ -1021,7 +1071,7 @@ impl Ui { let frame_rect = self.placer.next_space(desired_size, item_spacing); let child_rect = self.placer.justify_and_align(frame_rect, desired_size); - let mut child_ui = self.child_ui(child_rect, layout); + let mut child_ui = self.child_ui(child_rect, layout, None); let ret = add_contents(&mut child_ui); let final_child_rect = child_ui.min_rect(); @@ -1042,7 +1092,7 @@ impl Ui { add_contents: impl FnOnce(&mut Self) -> R, ) -> InnerResponse { debug_assert!(max_rect.is_finite()); - let mut child_ui = self.child_ui(max_rect, *self.layout()); + let mut child_ui = self.child_ui(max_rect, *self.layout(), None); let ret = add_contents(&mut child_ui); let final_child_rect = child_ui.min_rect(); @@ -1868,7 +1918,12 @@ impl Ui { ) -> InnerResponse { let child_rect = self.available_rect_before_wrap(); let next_auto_id_source = self.next_auto_id_source; - let mut child_ui = self.child_ui_with_id_source(child_rect, *self.layout(), id_source); + let mut child_ui = self.child_ui_with_id_source_and_frame_data( + child_rect, + *self.layout(), + id_source, + None, + ); self.next_auto_id_source = next_auto_id_source; // 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 = self.allocate_rect(child_ui.min_rect(), Sense::hover()); @@ -1927,7 +1982,12 @@ impl Ui { let mut child_rect = self.placer.available_rect_before_wrap(); child_rect.min.x += indent; - let mut child_ui = self.child_ui_with_id_source(child_rect, *self.layout(), id_source); + let mut child_ui = self.child_ui_with_id_source_and_frame_data( + child_rect, + *self.layout(), + id_source, + None, + ); let ret = add_contents(&mut child_ui); let left_vline = self.visuals().indent_has_left_vline; @@ -2149,7 +2209,7 @@ impl Ui { layout: Layout, add_contents: Box R + 'c>, ) -> InnerResponse { - let mut child_ui = self.child_ui(self.available_rect_before_wrap(), layout); + let mut child_ui = self.child_ui(self.available_rect_before_wrap(), layout, None); let inner = add_contents(&mut child_ui); let rect = child_ui.min_rect(); let item_spacing = self.spacing().item_spacing; @@ -2233,7 +2293,7 @@ impl Ui { pos2(pos.x + column_width, self.max_rect().right_bottom().y), ); let mut column_ui = - self.child_ui(child_rect, Layout::top_down_justified(Align::LEFT)); + self.child_ui(child_rect, Layout::top_down_justified(Align::LEFT), None); column_ui.set_width(column_width); column_ui }) diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs new file mode 100644 index 00000000000..78ae3caaafe --- /dev/null +++ b/crates/egui/src/ui_stack.rs @@ -0,0 +1,157 @@ +use std::iter::FusedIterator; +use std::sync::Arc; + +use crate::{Color32, Direction, Frame, Id, Rect}; + +/// What kind is this [`Ui`]? +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UiKind { + /// A [`egui::Window`]. + Window, + + /// A [`egui::CentralPanel`]. + CentralPanel, + + /// A left [`egui::SidePanel`]. + LeftPanel, + + /// A right [`egui::SidePanel`]. + RightPanel, + + /// A top [`egui::TopBottomPanel`]. + TopPanel, + + /// A bottom [`egui::TopBottomPanel`]. + BottomPanel, + + /// A [`egui::Frame`]. + Frame, + + /// A [`egui::ScrollArea`]. + ScrollArea, + + /// A [`egui::Resize`]. + Resize, + + /// The content of a regular menu. + Menu, + + /// The content of a popup menu. + Popup, + + /// A tooltip, as shown by e.g. [`egui::Response::on_hover_ui`]. + Tooltip, + + /// A picker, such as color picker. + Picker, + + /// A table cell (from the `egui_extras` crate). + TableCell, + + /// An [`egui::Area`] that is not of any other kind. + GenericArea, +} + +impl UiKind { + /// Is this any kind of panel? + pub fn is_panel(&self) -> bool { + matches!( + self, + Self::CentralPanel + | Self::LeftPanel + | Self::RightPanel + | Self::TopPanel + | Self::BottomPanel + ) + } +} + +// ---------------------------------------------------------------------------- + +/// Information about a [`egui::Ui`] to be included in the corresponding [`UiStack`]. +#[derive(Default, Copy, Clone, Debug)] +pub struct UiStackInfo { + pub kind: Option, + pub frame: Frame, +} + +// ---------------------------------------------------------------------------- + +/// Information about a [`egui::Ui`] and its parents. +/// +/// [`UiStack`] serves to keep track of the current hierarchy of [`egui::Ui`]s, such +/// that nested widgets or user code may adapt to the surrounding context or obtain layout information +/// from a [`egui::Ui`] that might be several steps higher in the hierarchy. +/// +/// Note: since [`UiStack`] contains a reference to its parent, it is both a stack, and a node within +/// that stack. Most of its methods are about the specific node, but some methods walk up the +/// hierarchy to provide information about the entire stack. +#[derive(Clone, Debug)] +pub struct UiStack { + // stuff that `Ui::child_ui` can deal with directly + pub id: Id, + pub kind: Option, + pub frame: Frame, + pub layout_direction: Direction, + pub min_rect: Rect, + pub max_rect: Rect, + pub parent: Option>, +} + +// these methods act on this specific node +impl UiStack { + /// Is this [`egui::Ui`] a panel? + #[inline] + pub fn is_panel_ui(&self) -> bool { + self.kind.map_or(false, |kind| kind.is_panel()) + } + + /// Is this a root [`egui::Ui`], i.e. created with [`Ui::new()`]? + #[inline] + pub fn is_root_ui(&self) -> bool { + self.parent.is_none() + } + + /// This this [`egui::Ui`] a [`egui::Frame`] with a visible stroke? + #[inline] + pub fn has_visible_frame(&self) -> bool { + self.frame.stroke.width > 0.0 && self.frame.stroke.color != Color32::TRANSPARENT + } +} + +// these methods act on the entire stack +impl UiStack { + /// Return an iterator that walks the stack from this node to the root. + #[allow(clippy::iter_without_into_iter)] + pub fn iter(&self) -> UiStackIterator { + UiStackIterator { + next: Some(Arc::new(self.clone())), + } + } + + /// Check if this node is or is contained in a [`egui::Ui`] of a specific kind. + pub fn contained_id(&self, kind: UiKind) -> bool { + self.iter().any(|frame| frame.kind == Some(kind)) + } +} + +// ---------------------------------------------------------------------------- + +/// Iterator that walks up a stack of `StackFrame`s. +/// +/// See [`UiStack::iter`]. +pub struct UiStackIterator { + next: Option>, +} + +impl Iterator for UiStackIterator { + type Item = Arc; + + fn next(&mut self) -> Option { + let current = self.next.clone(); + self.next = current.as_ref().and_then(|frame| frame.parent.clone()); + current + } +} + +impl FusedIterator for UiStackIterator {} diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index 6935de4536d..2a6c18152c2 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -489,6 +489,7 @@ pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Res // TODO(emilk): make it easier to show a temporary popup that closes when you click outside it if ui.memory(|mem| mem.is_popup_open(popup_id)) { let area_response = Area::new(popup_id) + .kind(UiKind::Picker) .order(Order::Foreground) .fixed_pos(button_response.rect.max) .constrain(true) diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 839adebfc47..ce9b168f914 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -140,6 +140,7 @@ impl<'a> Widget for DatePickerButton<'a> { inner: saved, response: area_response, } = Area::new(ui.make_persistent_id(self.id_source)) + .kind(egui::UiKind::Picker) .order(Order::Foreground) .fixed_pos(pos) .constrain_to(ui.ctx().screen_rect()) diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index a8ae5998daa..e133956ea6c 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -195,9 +195,15 @@ impl<'l> StripLayout<'l> { child_ui_id_source: egui::Id, add_cell_contents: impl FnOnce(&mut Ui), ) -> Ui { - let mut child_ui = - self.ui - .child_ui_with_id_source(rect, self.cell_layout, child_ui_id_source); + let mut child_ui = self.ui.child_ui_with_id_source_and_frame_data( + rect, + self.cell_layout, + child_ui_id_source, + Some(egui::UiStackInfo { + kind: Some(egui::UiKind::TableCell), + frame: egui::Frame::default(), + }), + ); if flags.clip { let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); diff --git a/crates/egui_plot/src/legend.rs b/crates/egui_plot/src/legend.rs index 538f2baf623..26e820f64e0 100644 --- a/crates/egui_plot/src/legend.rs +++ b/crates/egui_plot/src/legend.rs @@ -250,7 +250,7 @@ impl Widget for &mut LegendWidget { let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); let legend_pad = 4.0; let legend_rect = rect.shrink(legend_pad); - let mut legend_ui = ui.child_ui(legend_rect, layout); + let mut legend_ui = ui.child_ui(legend_rect, layout, None); legend_ui .scope(|ui| { let background_frame = Frame { diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 6dec9d7276d..c145e134730 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -1488,7 +1488,7 @@ impl<'a> PreparedPlot<'a> { let transform = &self.transform; - let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default()); + let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default(), None); plot_ui.set_clip_rect(transform.frame().intersect(ui.clip_rect())); for item in &self.items { item.shapes(&plot_ui, transform, &mut shapes); diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index 2780d0ce56e..c84b5d177b3 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -53,6 +53,12 @@ impl eframe::App for MyApp { ui.image(egui::include_image!( "../../../crates/egui/assets/ferris.png" )); + + ui.scope(|ui| { + ui.scope(|ui| { + ui.label(format!("{:?}", ui.stack())); + }); + }); }); } } diff --git a/tests/test_stack_frame/Cargo.toml b/tests/test_stack_frame/Cargo.toml new file mode 100644 index 00000000000..384cb177c39 --- /dev/null +++ b/tests/test_stack_frame/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "test_stack_frame" +version = "0.1.0" +authors = ["Antoine Beyeler "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +[lints] +workspace = true + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "persistence", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/tests/test_stack_frame/src/main.rs b/tests/test_stack_frame/src/main.rs new file mode 100644 index 00000000000..6dfd6ec6377 --- /dev/null +++ b/tests/test_stack_frame/src/main.rs @@ -0,0 +1,344 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use eframe::egui; +use eframe::egui::{Rangef, Shape, UiKind}; +use egui_extras::Column; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]), + ..Default::default() + }; + eframe::run_native( + "Stack Frame Demo", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ) +} + +#[derive(Default)] +struct MyApp {} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + ctx.style_mut(|style| style.interaction.tooltip_delay = 0.0); + egui::SidePanel::right("side_panel").show(ctx, |ui| { + ui.heading("Information"); + ui.label( + "This is a demo/test environment of the `UiStack` feature. The tables display \ + the UI stack in various contexts. You can hover on the IDs to display the \ + corresponding origin/`max_rect`.\n\n\ + The \"Full span test\" labels showcase an implementation of full-span \ + highlighting. Hover to see them in action!", + ); + ui.add_space(10.0); + if ui.button("Reset egui memory").clicked() { + ctx.memory_mut(|mem| *mem = Default::default()); + } + ui.add_space(20.0); + + egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| { + stack_ui(ui); + + // full span test + ui.add_space(20.0); + full_span_widget(ui, false); + + // nested frames test + ui.add_space(20.0); + egui::Frame { + stroke: ui.visuals().noninteractive().bg_stroke, + inner_margin: egui::Margin::same(4.0), + outer_margin: egui::Margin::same(4.0), + ..Default::default() + } + .show(ui, |ui| { + full_span_widget(ui, false); + stack_ui(ui); + + egui::Frame { + stroke: ui.visuals().noninteractive().bg_stroke, + inner_margin: egui::Margin::same(8.0), + outer_margin: egui::Margin::same(6.0), + ..Default::default() + } + .show(ui, |ui| { + full_span_widget(ui, false); + stack_ui(ui); + }); + }); + }); + }); + + egui::TopBottomPanel::bottom("bottom_panel") + .resizable(true) + .show(ctx, |ui| { + egui::ScrollArea::vertical() + .auto_shrink(false) + .show(ui, |ui| { + stack_ui(ui); + + // full span test + ui.add_space(20.0); + full_span_widget(ui, false); + }); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + egui::ScrollArea::vertical() + .auto_shrink(false) + .show(ui, |ui| { + ui.label("stack here:"); + stack_ui(ui); + + // full span test + ui.add_space(20.0); + full_span_widget(ui, false); + + // tooltip test + ui.add_space(20.0); + ui.label("Hover me").on_hover_ui(|ui| { + full_span_widget(ui, true); + ui.add_space(20.0); + stack_ui(ui); + }); + + // combobox test + ui.add_space(20.0); + egui::ComboBox::from_id_source("combo_box") + .selected_text("click me") + .show_ui(ui, |ui| { + full_span_widget(ui, true); + ui.add_space(20.0); + stack_ui(ui); + }); + + // Ui nesting test + ui.add_space(20.0); + ui.label("UI nesting test:"); + egui::Frame { + stroke: ui.visuals().noninteractive().bg_stroke, + inner_margin: egui::Margin::same(4.0), + ..Default::default() + } + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.scope(stack_ui); + }); + }); + }); + + // table test + let mut cell_stack = None; + ui.add_space(20.0); + ui.label("Table test:"); + + egui_extras::TableBuilder::new(ui) + .vscroll(false) + .column(Column::auto()) + .column(Column::auto()) + .header(20.0, |mut header| { + header.col(|ui| { + ui.strong("column 1"); + }); + header.col(|ui| { + ui.strong("column 2"); + }); + }) + .body(|mut body| { + body.row(20.0, |mut row| { + row.col(|ui| { + full_span_widget(ui, false); + }); + row.col(|ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + ui.label("See stack below"); + cell_stack = Some(ui.stack()); + }); + }); + }); + + if let Some(cell_stack) = cell_stack { + ui.label("Cell's stack:"); + stack_ui_impl(ui, &cell_stack); + } + }); + }); + + egui::Window::new("Window") + .pivot(egui::Align2::RIGHT_TOP) + .show(ctx, |ui| { + full_span_widget(ui, false); + ui.add_space(20.0); + stack_ui(ui); + }); + } +} + +fn full_span_widget(ui: &mut egui::Ui, permanent: bool) { + let bg_shape_idx = ui.painter().add(Shape::Noop); + let response = ui.label("Full span test"); + let stack_frame = ui.stack(); + + let rect = egui::Rect::from_x_y_ranges( + full_span_horizontal_range(&stack_frame), + response.rect.y_range(), + ); + + if permanent || response.hovered() { + ui.painter().set( + bg_shape_idx, + Shape::rect_filled(rect, 0.0, ui.visuals().selection.bg_fill), + ); + } +} + +fn full_span_horizontal_range(stack: &egui::UiStack) -> Rangef { + for stack_frame in stack.iter() { + if stack_frame.has_visible_frame() + || stack_frame.is_panel_ui() + || stack_frame.is_root_ui() + || stack_frame.kind == Some(UiKind::TableCell) + { + return (stack_frame.max_rect + stack_frame.frame.inner_margin).x_range(); + } + } + + // should never happen + Rangef::EVERYTHING +} + +fn stack_ui(ui: &mut egui::Ui) { + let stack_frame = &ui.stack(); + ui.scope(|ui| { + stack_ui_impl(ui, stack_frame); + }); +} + +fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) { + egui::Frame { + stroke: ui.style().noninteractive().fg_stroke, + inner_margin: egui::Margin::same(4.0), + ..Default::default() + } + .show(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + egui_extras::TableBuilder::new(ui) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .column(Column::auto()) + .header(20.0, |mut header| { + header.col(|ui| { + ui.strong("frame_id"); + }); + header.col(|ui| { + ui.strong("kind"); + }); + header.col(|ui| { + ui.strong("stroke"); + }); + header.col(|ui| { + ui.strong("inner"); + }); + header.col(|ui| { + ui.strong("outer"); + }); + header.col(|ui| { + ui.strong("direction"); + }); + }) + .body(|mut body| { + for stack_frame in stack.iter() { + body.row(20.0, |mut row| { + row.col(|ui| { + if ui.label(format!("{:?}", stack_frame.id)).hovered() { + ui.ctx().debug_painter().debug_rect( + stack_frame.max_rect, + egui::Color32::GREEN, + "max", + ); + ui.ctx().debug_painter().circle_filled( + stack_frame.min_rect.min, + 2.0, + egui::Color32::RED, + ); + } + }); + row.col(|ui| { + let s = if let Some(kind) = stack_frame.kind { + format!("{:?}", kind) + } else { + "-".to_owned() + }; + + ui.label(s); + }); + row.col(|ui| { + if stack_frame.frame.stroke == egui::Stroke::NONE { + ui.label("-"); + } else { + let mut layout_job = egui::text::LayoutJob::default(); + layout_job.append( + "⬛ ", + 0.0, + egui::TextFormat::simple( + egui::TextStyle::Body.resolve(ui.style()), + stack_frame.frame.stroke.color, + ), + ); + layout_job.append( + format!("{}px", stack_frame.frame.stroke.width).as_str(), + 0.0, + egui::TextFormat::simple( + egui::TextStyle::Body.resolve(ui.style()), + ui.style().visuals.text_color(), + ), + ); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + ui.label(layout_job); + } + }); + row.col(|ui| { + ui.label(print_margin(&stack_frame.frame.inner_margin)); + }); + row.col(|ui| { + ui.label(print_margin(&stack_frame.frame.outer_margin)); + }); + row.col(|ui| { + ui.label(format!("{:?}", stack_frame.layout_direction)); + }); + }); + } + }); + }); +} + +fn print_margin(margin: &egui::Margin) -> String { + if margin.is_same() { + format!("{}px", margin.left) + } else { + let s1 = if margin.left == margin.right { + format!("H: {}px", margin.left) + } else { + format!("L: {}px R: {}px", margin.left, margin.right) + }; + let s2 = if margin.top == margin.bottom { + format!("V: {}px", margin.top) + } else { + format!("T: {}px B: {}px", margin.top, margin.bottom) + }; + format!("{s1} / {s2}") + } +}