Browse Source

Add tags to `UiStack` (#4617)

You can now set custom tags on the `UiStack`. This allows you to write
code that is situationally aware at runtime. For instance, you could
decide wether or not a label should truncate its text depending on what
part of your ui it is in, without having to pass that info down via the
callstack.
pull/4623/head
Emil Ernerfeldt 5 months ago
committed by GitHub
parent
commit
bb8400853f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      crates/egui/src/containers/frame.rs
  2. 15
      crates/egui/src/containers/panel.rs
  3. 33
      crates/egui/src/ui.rs
  4. 130
      crates/egui/src/ui_stack.rs
  5. 2
      crates/egui_demo_lib/src/demo/misc_demo_window.rs
  6. 17
      tests/test_ui_stack/src/main.rs

5
crates/egui/src/containers/frame.rs

@ -253,10 +253,7 @@ impl Frame {
let content_ui = ui.child_ui(
inner_rect,
*ui.layout(),
Some(UiStackInfo {
frame: self,
kind: Some(UiKind::Frame),
}),
Some(UiStackInfo::new(UiKind::Frame).with_frame(self)),
);
// 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

15
crates/egui/src/containers/panel.rs

@ -362,10 +362,7 @@ impl SidePanel {
self.id,
available_rect,
clip_rect,
UiStackInfo {
kind: None, // set by show_inside_dyn
frame: Frame::default(),
},
UiStackInfo::default(),
);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
@ -848,10 +845,7 @@ impl TopBottomPanel {
self.id,
available_rect,
clip_rect,
UiStackInfo {
kind: None, // set by show_inside_dyn
frame: Frame::default(),
},
UiStackInfo::default(), // set by show_inside_dyn
);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
@ -1120,10 +1114,7 @@ impl CentralPanel {
id,
available_rect,
clip_rect,
UiStackInfo {
kind: None, // set by show_inside_dyn
frame: Frame::default(),
},
UiStackInfo::default(), // set by show_inside_dyn
);
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);

33
crates/egui/src/ui.rs

@ -94,8 +94,7 @@ impl Ui {
let ui_stack = UiStack {
id,
layout_direction: layout.main_dir,
kind: ui_stack_info.kind,
frame: ui_stack_info.frame,
info: ui_stack_info,
parent: None,
min_rect: placer.min_rect(),
max_rect: placer.max_rect(),
@ -130,6 +129,8 @@ impl Ui {
///
/// Note: calling this function twice from the same [`Ui`] will create a conflict of id. Use
/// [`Self::scope`] if needed.
///
/// When in doubt, use `None` for the `UiStackInfo` argument.
pub fn child_ui(
&mut self,
max_rect: Rect,
@ -140,6 +141,8 @@ impl Ui {
}
/// Create a new [`Ui`] at a specific region with a specific id.
///
/// When in doubt, use `None` for the `UiStackInfo` argument.
pub fn child_ui_with_id_source(
&mut self,
max_rect: Rect,
@ -162,12 +165,10 @@ impl Ui {
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,
info: ui_stack_info.unwrap_or_default(),
parent: Some(self.stack.clone()),
min_rect: placer.min_rect(),
max_rect: placer.max_rect(),
@ -1956,7 +1957,22 @@ impl Ui {
id_source: impl Hash,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.scope_dyn(Box::new(add_contents), Id::new(id_source))
self.scope_dyn(Box::new(add_contents), Id::new(id_source), None)
}
/// Push another level onto the [`UiStack`].
///
/// You can use this, for instance, to tag a group of widgets.
pub fn push_stack_info<R>(
&mut self,
ui_stack_info: UiStackInfo,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.scope_dyn(
Box::new(add_contents),
Id::new("child"),
Some(ui_stack_info),
)
}
/// Create a scoped child ui.
@ -1972,18 +1988,19 @@ impl Ui {
/// # });
/// ```
pub fn scope<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
self.scope_dyn(Box::new(add_contents), Id::new("child"))
self.scope_dyn(Box::new(add_contents), Id::new("child"), None)
}
fn scope_dyn<'c, R>(
&mut self,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
id_source: Id,
ui_stack_info: Option<UiStackInfo>,
) -> 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, None);
self.child_ui_with_id_source(child_rect, *self.layout(), id_source, ui_stack_info);
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());

130
crates/egui/src/ui_stack.rs

@ -1,5 +1,5 @@
use std::iter::FusedIterator;
use std::sync::Arc;
use std::{any::Any, iter::FusedIterator};
use crate::{Direction, Frame, Id, Rect};
@ -54,6 +54,7 @@ pub enum UiKind {
impl UiKind {
/// Is this any kind of panel?
#[inline]
pub fn is_panel(&self) -> bool {
matches!(
self,
@ -64,25 +65,121 @@ impl UiKind {
| Self::BottomPanel
)
}
/// Is this any kind of [`crate::Area`]?
#[inline]
pub fn is_area(&self) -> bool {
match self {
Self::CentralPanel
| Self::LeftPanel
| Self::RightPanel
| Self::TopPanel
| Self::BottomPanel
| Self::Frame
| Self::ScrollArea
| Self::Resize
| Self::TableCell => false,
Self::Window
| Self::Menu
| Self::Popup
| Self::Tooltip
| Self::Picker
| Self::GenericArea => true,
}
}
}
// ----------------------------------------------------------------------------
/// Information about a [`crate::Ui`] to be included in the corresponding [`UiStack`].
#[derive(Default, Copy, Clone, Debug)]
#[derive(Clone, Default, Debug)]
pub struct UiStackInfo {
pub kind: Option<UiKind>,
pub frame: Frame,
pub tags: UiTags,
}
impl UiStackInfo {
/// Create a new [`UiStackInfo`] with the given kind and an empty frame.
#[inline]
pub fn new(kind: UiKind) -> Self {
Self {
kind: Some(kind),
frame: Default::default(),
..Default::default()
}
}
#[inline]
pub fn with_frame(mut self, frame: Frame) -> Self {
self.frame = frame;
self
}
/// Insert a tag with no value.
#[inline]
pub fn with_tag(mut self, key: impl Into<String>) -> Self {
self.tags.insert(key, None);
self
}
/// Insert a tag with some value.
#[inline]
pub fn with_tag_value(
mut self,
key: impl Into<String>,
value: impl Any + Send + Sync + 'static,
) -> Self {
self.tags.insert(key, Some(Arc::new(value)));
self
}
}
// ----------------------------------------------------------------------------
/// User-chosen tags.
///
/// You can use this in any way you want,
/// i.e. to set some tag on a [`crate::Ui`] and then in your own widget check
/// for the existence of this tag up the [`UiStack`].
///
/// Note that egui never sets any tags itself, so this is purely for user code.
///
/// All tagging is transient, and will only live as long as the parent [`crate::Ui`], i.e. within a single render frame.
#[derive(Clone, Default, Debug)]
pub struct UiTags(pub ahash::HashMap<String, Option<Arc<dyn Any + Send + Sync + 'static>>>);
impl UiTags {
#[inline]
pub fn insert(
&mut self,
key: impl Into<String>,
value: Option<Arc<dyn Any + Send + Sync + 'static>>,
) {
self.0.insert(key.into(), value);
}
#[inline]
pub fn contains(&self, key: &str) -> bool {
self.0.contains_key(key)
}
/// Get the value of a tag.
///
/// Note that `None` is returned both if the key is set to the value `None`,
/// and if the key is not set at all.
#[inline]
pub fn get_any(&self, key: &str) -> Option<&Arc<dyn Any + Send + Sync + 'static>> {
self.0.get(key)?.as_ref()
}
/// Get the value of a tag.
///
/// Note that `None` is returned both if the key is set to the value `None`,
/// and if the key is not set at all.
pub fn get_downcast<T: Any + Send + Sync + 'static>(&self, key: &str) -> Option<&T> {
self.0.get(key)?.as_ref().and_then(|any| any.downcast_ref())
}
}
// ----------------------------------------------------------------------------
@ -96,12 +193,11 @@ impl UiStackInfo {
/// 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)]
#[derive(Debug)]
pub struct UiStack {
// stuff that `Ui::child_ui` can deal with directly
pub id: Id,
pub kind: Option<UiKind>,
pub frame: Frame,
pub info: UiStackInfo,
pub layout_direction: Direction,
pub min_rect: Rect,
pub max_rect: Rect,
@ -110,10 +206,26 @@ pub struct UiStack {
// these methods act on this specific node
impl UiStack {
#[inline]
pub fn kind(&self) -> Option<UiKind> {
self.info.kind
}
#[inline]
pub fn frame(&self) -> &Frame {
&self.info.frame
}
/// User tags.
#[inline]
pub fn tags(&self) -> &UiTags {
&self.info.tags
}
/// Is this [`crate::Ui`] a panel?
#[inline]
pub fn is_panel_ui(&self) -> bool {
self.kind.map_or(false, |kind| kind.is_panel())
self.kind().map_or(false, |kind| kind.is_panel())
}
/// Is this a root [`crate::Ui`], i.e. created with [`crate::Ui::new()`]?
@ -125,7 +237,7 @@ impl UiStack {
/// This this [`crate::Ui`] a [`crate::Frame`] with a visible stroke?
#[inline]
pub fn has_visible_frame(&self) -> bool {
!self.frame.stroke.is_empty()
!self.info.frame.stroke.is_empty()
}
}
@ -139,7 +251,7 @@ impl UiStack {
/// 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))
self.iter().any(|frame| frame.kind() == Some(kind))
}
}

2
crates/egui_demo_lib/src/demo/misc_demo_window.rs

@ -547,7 +547,7 @@ fn ui_stack_demo(ui: &mut Ui) {
});
row.col(|ui| {
ui.label(if let Some(kind) = node.kind {
ui.label(if let Some(kind) = node.kind() {
format!("{kind:?}")
} else {
"-".to_owned()

17
tests/test_ui_stack/src/main.rs

@ -209,9 +209,9 @@ fn full_span_horizontal_range(ui_stack: &egui::UiStack) -> Rangef {
if node.has_visible_frame()
|| node.is_panel_ui()
|| node.is_root_ui()
|| node.kind == Some(UiKind::TableCell)
|| node.kind() == Some(UiKind::TableCell)
{
return (node.max_rect + node.frame.inner_margin).x_range();
return (node.max_rect + node.frame().inner_margin).x_range();
}
}
@ -280,7 +280,7 @@ fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) {
}
});
row.col(|ui| {
let s = if let Some(kind) = node.kind {
let s = if let Some(kind) = node.kind() {
format!("{kind:?}")
} else {
"-".to_owned()
@ -289,7 +289,8 @@ fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) {
ui.label(s);
});
row.col(|ui| {
if node.frame.stroke == egui::Stroke::NONE {
let frame = node.frame();
if frame.stroke == egui::Stroke::NONE {
ui.label("-");
} else {
let mut layout_job = egui::text::LayoutJob::default();
@ -298,11 +299,11 @@ fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) {
0.0,
egui::TextFormat::simple(
egui::TextStyle::Body.resolve(ui.style()),
node.frame.stroke.color,
frame.stroke.color,
),
);
layout_job.append(
format!("{}px", node.frame.stroke.width).as_str(),
format!("{}px", frame.stroke.width).as_str(),
0.0,
egui::TextFormat::simple(
egui::TextStyle::Body.resolve(ui.style()),
@ -314,10 +315,10 @@ fn stack_ui_impl(ui: &mut egui::Ui, stack: &egui::UiStack) {
}
});
row.col(|ui| {
ui.label(print_margin(&node.frame.inner_margin));
ui.label(print_margin(&node.frame().inner_margin));
});
row.col(|ui| {
ui.label(print_margin(&node.frame.outer_margin));
ui.label(print_margin(&node.frame().outer_margin));
});
row.col(|ui| {
ui.label(format!("{:?}", node.layout_direction));

Loading…
Cancel
Save