Introduce `UiStack` (#4588)

* Closes #4534

This PR:
- Introduces `Ui::stack()`, which returns the `UiStack` structure
providing information on the current `Ui` hierarchy.
- **BREAKING**: `Ui::new()` now takes a `UiStackInfo` argument, which is
used to populate some of this `Ui`'s `UiStack`'s fields.
- **BREAKING**: `Ui::child_ui()` and `Ui::child_ui_with_id_source()` now
take an `Option<UiStackInfo>` argument, which is used to populate some
of the children `Ui`'s `UiStack`'s fields.
- New `Area::kind()` builder function, to set the `UiStackKind` value of
the `Area`'s `Ui`.
- Adds a (minimalistic) demo to egui demo (in the "Misc Demos" window).
- Adds a more thorough `test_ui_stack` test/playground demo.

TODO:
- [x] benchmarks
- [x] add example to demo

Future work:
- Add `UiStackKind` and related support for more container (e.g.
`CollapsingHeader`, etc.)
- Add a tag/property system that would allow adding arbitrary data to a
stack node. This data could then be queried by nested `Ui`s. Probably
needed for #3284.
- Add support to track columnar layouts.

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Antoine Beyeler 2024-06-04 10:12:23 +02:00 committed by GitHub
parent c0a9800d05
commit a28792194d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 794 additions and 30 deletions

View File

@ -3636,6 +3636,15 @@ dependencies = [
"env_logger",
]
[[package]]
name = "test_ui_stack"
version = "0.1.0"
dependencies = [
"eframe",
"egui_extras",
"env_logger",
]
[[package]]
name = "test_viewports"
version = "0.1.0"

View File

@ -81,6 +81,7 @@ impl AreaState {
#[derive(Clone, Copy, Debug)]
pub struct Area {
pub(crate) id: Id,
kind: UiKind,
sense: Option<Sense>,
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)
}
@ -303,6 +314,7 @@ impl Area {
}
pub(crate) struct Prepared {
kind: UiKind,
layer_id: LayerId,
state: AreaState,
move_response: Response,
@ -336,6 +348,7 @@ impl Area {
pub(crate) fn begin(self, ctx: &Context) -> Prepared {
let Self {
id,
kind,
sense,
movable,
order,
@ -458,6 +471,7 @@ impl Area {
move_response.interact_rect = state.rect();
Prepared {
kind,
layer_id,
state,
move_response,
@ -498,6 +512,7 @@ impl Prepared {
self.layer_id.id,
max_rect,
clip_rect,
UiStackInfo::new(self.kind),
);
if self.fade_in {
@ -520,6 +535,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,

View File

@ -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);

View File

@ -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

View File

@ -257,7 +257,15 @@ 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(
panel_rect,
Layout::top_down(Align::Min),
id,
Some(UiStackInfo::new(match side {
Side::Left => UiKind::LeftPanel,
Side::Right => UiKind::RightPanel,
})),
);
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 +356,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 +741,15 @@ 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(
panel_rect,
Layout::top_down(Align::Min),
id,
Some(UiStackInfo::new(match side {
TopBottomSide::Top => UiKind::TopPanel,
TopBottomSide::Bottom => UiKind::BottomPanel,
})),
);
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 +842,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 +1081,11 @@ 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::new(UiKind::CentralPanel)),
);
let frame = frame.unwrap_or_else(|| Frame::central_panel(ui.style()));
frame.show(&mut panel_ui, |ui| {
@ -1074,7 +1114,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);

View File

@ -131,6 +131,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)
@ -311,6 +312,7 @@ pub fn popup_above_or_below_widget<R>(
let inner_width = widget_response.rect.width() - frame_margin.sum().x;
let inner = Area::new(popup_id)
.kind(UiKind::Popup)
.order(Order::Foreground)
.fixed_pos(pos)
.default_width(inner_width)

View File

@ -270,7 +270,11 @@ 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::new(UiKind::Resize)),
);
content_ui.set_clip_rect(content_clip_rect);
Prepared {

View File

@ -556,7 +556,11 @@ 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::new(UiKind::ScrollArea)),
);
{
// Clip the content, but only when we really need to:

View File

@ -49,7 +49,7 @@ 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<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(Id::new(title.text()));
let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
Self {
title,
open: None,

View File

@ -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},

View File

@ -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)
.interactable(true)

View File

@ -67,6 +67,9 @@ pub struct Ui {
/// Indicates whether this Ui belongs to a Menu.
menu_state: Option<Arc<RwLock<MenuState>>>,
/// The [`UiStack`] for this [`Ui`].
stack: Arc<UiStack>,
}
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,
ui_stack_info: UiStackInfo,
) -> Self {
let style = ctx.style();
let layout = Layout::default();
let placer = Placer::new(max_rect, layout);
let ui_stack = UiStack {
id,
layout_direction: layout.main_dir,
kind: ui_stack_info.kind,
frame: ui_stack_info.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(ui_stack),
};
// Register in the widget stack early, to ensure we are behind all widgets we contain:
@ -105,8 +127,16 @@ 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,
ui_stack_info: Option<UiStackInfo>,
) -> Self {
self.child_ui_with_id_source(max_rect, layout, "child", ui_stack_info)
}
/// Create a new [`Ui`] at a specific region with a specific id.
@ -115,6 +145,7 @@ impl Ui {
max_rect: Rect,
mut layout: Layout,
id_source: impl Hash,
ui_stack_info: Option<UiStackInfo>,
) -> 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 = ui_stack_info.unwrap_or_default();
let ui_stack = 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(ui_stack),
};
// Register in the widget stack early, to ensure we are behind all widgets we contain:
@ -258,6 +303,12 @@ impl Ui {
&mut self.style_mut().visuals
}
/// Get a reference to this [`Ui`]'s [`UiStack`].
#[inline]
pub fn stack(&self) -> &Arc<UiStack> {
&self.stack
}
/// Get a reference to the parent [`Context`].
#[inline]
pub fn ctx(&self) -> &Context {
@ -1021,7 +1072,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 +1093,7 @@ impl Ui {
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R> {
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 +1919,8 @@ impl Ui {
) -> InnerResponse<R> {
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(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 +1979,8 @@ 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(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 +2202,7 @@ impl Ui {
layout: Layout,
add_contents: Box<dyn FnOnce(&mut Self) -> R + 'c>,
) -> InnerResponse<R> {
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 +2286,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
})

166
crates/egui/src/ui_stack.rs Normal file
View File

@ -0,0 +1,166 @@
use std::iter::FusedIterator;
use std::sync::Arc;
use crate::{Direction, Frame, Id, Rect};
/// What kind is this [`crate::Ui`]?
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UiKind {
/// A [`crate::Window`].
Window,
/// A [`crate::CentralPanel`].
CentralPanel,
/// A left [`crate::SidePanel`].
LeftPanel,
/// A right [`crate::SidePanel`].
RightPanel,
/// A top [`crate::TopBottomPanel`].
TopPanel,
/// A bottom [`crate::TopBottomPanel`].
BottomPanel,
/// A [`crate::Frame`].
Frame,
/// A [`crate::ScrollArea`].
ScrollArea,
/// A [`crate::Resize`].
Resize,
/// The content of a regular menu.
Menu,
/// The content of a popup menu.
Popup,
/// A tooltip, as shown by e.g. [`crate::Response::on_hover_ui`].
Tooltip,
/// A picker, such as color picker.
Picker,
/// A table cell (from the `egui_extras` crate).
TableCell,
/// An [`crate::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 [`crate::Ui`] to be included in the corresponding [`UiStack`].
#[derive(Default, Copy, Clone, Debug)]
pub struct UiStackInfo {
pub kind: Option<UiKind>,
pub frame: Frame,
}
impl UiStackInfo {
/// Create a new [`UiStackInfo`] with the given kind and an empty frame.
pub fn new(kind: UiKind) -> Self {
Self {
kind: Some(kind),
frame: Default::default(),
}
}
}
// ----------------------------------------------------------------------------
/// Information about a [`crate::Ui`] and its parents.
///
/// [`UiStack`] serves to keep track of the current hierarchy of [`crate::Ui`]s, such
/// that nested widgets or user code may adapt to the surrounding context or obtain layout information
/// from a [`crate::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<UiKind>,
pub frame: Frame,
pub layout_direction: Direction,
pub min_rect: Rect,
pub max_rect: Rect,
pub parent: Option<Arc<UiStack>>,
}
// these methods act on this specific node
impl UiStack {
/// Is this [`crate::Ui`] a panel?
#[inline]
pub fn is_panel_ui(&self) -> bool {
self.kind.map_or(false, |kind| kind.is_panel())
}
/// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]?
#[inline]
pub fn is_root_ui(&self) -> bool {
self.parent.is_none()
}
/// This this [`crate::Ui`] a [`crate::Frame`] with a visible stroke?
#[inline]
pub fn has_visible_frame(&self) -> bool {
!self.frame.stroke.is_empty()
}
}
// 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(self) }
}
/// Check if this node is or is contained in a [`crate::Ui`] of a specific kind.
pub fn contained_in(&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<'a> {
next: Option<&'a UiStack>,
}
impl<'a> Iterator for UiStackIterator<'a> {
type Item = &'a UiStack;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let current = self.next;
self.next = current.and_then(|frame| frame.parent.as_deref());
current
}
}
impl<'a> FusedIterator for UiStackIterator<'a> {}

View File

@ -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)
.show(ui.ctx(), |ui| {

View File

@ -1,4 +1,5 @@
use super::*;
use egui::*;
/// Showcase some ui code
@ -154,6 +155,10 @@ impl View for MiscDemoWindow {
});
});
CollapsingHeader::new("Ui Stack")
.default_open(false)
.show(ui, ui_stack_demo);
CollapsingHeader::new("Misc")
.default_open(false)
.show(ui, |ui| {
@ -492,6 +497,72 @@ impl Tree {
// ----------------------------------------------------------------------------
fn ui_stack_demo(ui: &mut Ui) {
ui.horizontal_wrapped(|ui| {
ui.label("The");
ui.code("egui::Ui");
ui.label("core type is typically deeply nested in");
ui.code("egui");
ui.label(
"applications. To provide context to nested code, it maintains a stack \
with various information.\n\nThis is how the stack looks like here:",
);
});
let stack = ui.stack().clone();
Frame {
inner_margin: ui.spacing().menu_margin,
stroke: ui.visuals().widgets.noninteractive.bg_stroke,
..Default::default()
}
.show(ui, |ui| {
egui_extras::TableBuilder::new(ui)
.column(egui_extras::Column::auto())
.column(egui_extras::Column::auto())
.header(18.0, |mut header| {
header.col(|ui| {
ui.strong("id");
});
header.col(|ui| {
ui.strong("kind");
});
})
.body(|mut body| {
for node in stack.iter() {
body.row(18.0, |mut row| {
row.col(|ui| {
let response = ui.label(format!("{:?}", node.id));
if response.hovered() {
ui.ctx().debug_painter().debug_rect(
node.max_rect,
Color32::GREEN,
"max_rect",
);
ui.ctx().debug_painter().circle_filled(
node.min_rect.min,
2.0,
Color32::RED,
);
}
});
row.col(|ui| {
ui.label(if let Some(kind) = node.kind {
format!("{kind:?}")
} else {
"-".to_owned()
});
});
});
}
});
});
ui.small("Hover on UI's ids to display their origin and max rect.");
}
// ----------------------------------------------------------------------------
fn text_layout_demo(ui: &mut Ui) {
use egui::text::LayoutJob;

View File

@ -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)
.show(ui.ctx(), |ui| {

View File

@ -195,9 +195,12 @@ 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(
rect,
self.cell_layout,
child_ui_id_source,
Some(egui::UiStackInfo::new(egui::UiKind::TableCell)),
);
if flags.clip {
let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin);

View File

@ -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 {

View File

@ -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);

View File

@ -71,7 +71,7 @@ fn custom_window_frame(ctx: &egui::Context, title: &str, add_contents: impl FnOn
rect
}
.shrink(4.0);
let mut content_ui = ui.child_ui(content_rect, *ui.layout());
let mut content_ui = ui.child_ui(content_rect, *ui.layout(), None);
add_contents(&mut content_ui);
});
}

View File

@ -0,0 +1,27 @@
[package]
name = "test_ui_stack"
version = "0.1.0"
authors = ["Antoine Beyeler <abeyeler@gmail.com>"]
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",
] }

View File

@ -0,0 +1,347 @@
#![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::<MyApp>::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().clone());
});
});
});
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);
});
}
}
/// Demo of a widget that highlights its background all the way to the edge of its container when
/// hovered.
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 ui_stack = ui.stack();
let rect = egui::Rect::from_x_y_ranges(
full_span_horizontal_range(ui_stack),
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),
);
}
}
/// Find the horizontal range of the enclosing container.
fn full_span_horizontal_range(ui_stack: &egui::UiStack) -> Rangef {
for node in ui_stack.iter() {
if node.has_visible_frame()
|| node.is_panel_ui()
|| node.is_root_ui()
|| node.kind == Some(UiKind::TableCell)
{
return (node.max_rect + node.frame.inner_margin).x_range();
}
}
// should never happen
Rangef::EVERYTHING
}
fn stack_ui(ui: &mut egui::Ui) {
let ui_stack = ui.stack().clone();
ui.scope(|ui| {
stack_ui_impl(ui, &ui_stack);
});
}
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("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 node in stack.iter() {
body.row(20.0, |mut row| {
row.col(|ui| {
if ui.label(format!("{:?}", node.id)).hovered() {
ui.ctx().debug_painter().debug_rect(
node.max_rect,
egui::Color32::GREEN,
"max",
);
ui.ctx().debug_painter().circle_filled(
node.min_rect.min,
2.0,
egui::Color32::RED,
);
}
});
row.col(|ui| {
let s = if let Some(kind) = node.kind {
format!("{kind:?}")
} else {
"-".to_owned()
};
ui.label(s);
});
row.col(|ui| {
if node.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()),
node.frame.stroke.color,
),
);
layout_job.append(
format!("{}px", node.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(&node.frame.inner_margin));
});
row.col(|ui| {
ui.label(print_margin(&node.frame.outer_margin));
});
row.col(|ui| {
ui.label(format!("{:?}", node.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}")
}
}

View File

@ -457,7 +457,7 @@ fn drop_target<R>(
let available_rect = ui.available_rect_before_wrap();
let inner_rect = available_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);
let ret = body(&mut content_ui);
let outer_rect =