mirror of
https://github.com/emilk/egui.git
synced 2026-06-26 22:53:14 -04:00
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:
347
tests/test_ui_stack/src/main.rs
Normal file
347
tests/test_ui_stack/src/main.rs
Normal 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}")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user