mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 05:40:03 -04:00
Implement wrapping atom layout
This commit is contained in:
56
tests/egui_tests/tests/atom_reflow_blowup.rs
Normal file
56
tests/egui_tests/tests/atom_reflow_blowup.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
//! Experiment (Taffy-style deep-tree blowup):
|
||||
//!
|
||||
//! egui's atom layout re-measures a grown nested `Layout` atom so its content can reflow at the
|
||||
//! resolved width (the cross-after-main pass in `AtomLayout::measure`, plus the re-measure in
|
||||
//! `paint_at`). Unlike Taffy, there is currently *no measurement cache*. So a chain of nested
|
||||
//! `grow` layouts where every level fills past its content should re-measure each child more than
|
||||
//! once per level — `O(2^depth)` — reproducing the kind of exponential blowup Taffy's PR #246
|
||||
//! cache fixed (a deep tree that went from ~17s to ~3ms).
|
||||
//!
|
||||
//! Run with:
|
||||
//! ```sh
|
||||
//! cargo nextest run -p egui_tests -E 'test(atom_reflow_blowup)' --run-ignored all --no-capture
|
||||
//! ```
|
||||
|
||||
use egui::{Atom, AtomExt as _, AtomLayout, Vec2};
|
||||
use egui_kittest::Harness;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// A chain of `depth` nested layouts. Each level is a single `grow` child inside a parent whose
|
||||
/// `min_size` is *strictly larger* than the child's own (the child's natural width = its own
|
||||
/// `min_size`). So every parent grows its child past its natural width — `grow_main > 0` — which
|
||||
/// triggers a re-measure of the child, recursively, all the way down.
|
||||
fn nested(depth: usize) -> AtomLayout<'static> {
|
||||
if depth == 0 {
|
||||
AtomLayout::new("leaf")
|
||||
} else {
|
||||
let child = Atom::layout(nested(depth - 1)).atom_grow(true);
|
||||
// Larger at the top, decreasing toward the leaves, so each level genuinely grows its child.
|
||||
AtomLayout::new(child).min_size(Vec2::new(50.0 * depth as f32, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "perf experiment, run manually with --run-ignored"]
|
||||
fn atom_reflow_blowup() {
|
||||
let mut prev: Option<Duration> = None;
|
||||
for depth in 1..=30 {
|
||||
let start = Instant::now();
|
||||
let mut harness = Harness::builder().build_ui(|ui| {
|
||||
nested(depth).show(ui);
|
||||
});
|
||||
harness.run_steps(1);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let ratio = prev.map_or(String::new(), |p| {
|
||||
format!("(x{:.2} vs prev)", elapsed.as_secs_f64() / p.as_secs_f64())
|
||||
});
|
||||
println!("depth {depth:2}: {elapsed:>12.3?} {ratio}");
|
||||
prev = Some(elapsed);
|
||||
|
||||
if elapsed > Duration::from_secs(3) {
|
||||
println!("... aborting: blowup confirmed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
tests/egui_tests/tests/snapshots/atom_wrap_justified.png
Normal file
3
tests/egui_tests/tests/snapshots/atom_wrap_justified.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7506fa98769b5d387dac906206859824b8cf8c86665c383e3721cbe74e839379
|
||||
size 13160
|
||||
3
tests/egui_tests/tests/snapshots/atom_wrap_ragged.png
Normal file
3
tests/egui_tests/tests/snapshots/atom_wrap_ragged.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f56cb05b59acf3550f73ad9609922f1b49da83ba7671148f5f5067b7e45c8918
|
||||
size 13219
|
||||
3
tests/egui_tests/tests/snapshots/atom_wrap_top_down.png
Normal file
3
tests/egui_tests/tests/snapshots/atom_wrap_top_down.png
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:133af05ea2a6c387547cbba17b27d5e93a3f13df7d7ade5c134a39b057d48d03
|
||||
size 13241
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ca11ca5fbb4d1efa83929decfbea0fe723ed8da32277bcffbe509f444b5e8fa5
|
||||
size 572550
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:860b8f749bfbbc27f037ac0f643a19873d0e8c879baa919d76878ad658efa846
|
||||
size 22237
|
||||
@@ -1,5 +1,6 @@
|
||||
use egui::{
|
||||
Align, Atom, AtomExt as _, AtomLayout, Button, Direction, Frame, Layout, TextWrapMode, Ui, Vec2,
|
||||
Align, Atom, AtomExt as _, AtomLayout, Atoms, Button, Direction, Frame, Layout, TextWrapMode,
|
||||
Ui, Vec2,
|
||||
};
|
||||
use egui_kittest::{HarnessBuilder, SnapshotResult, SnapshotResults};
|
||||
|
||||
@@ -186,3 +187,106 @@ fn test_atom_letter_spacing() {
|
||||
|
||||
harness.snapshot("atom_letter_spacing");
|
||||
}
|
||||
|
||||
/// A list of button-framed texts of varying widths, used by the wrapping tests.
|
||||
///
|
||||
/// When `grow` is set, each atom is marked `grow` so lines stretch to fill the available extent.
|
||||
fn fruit_atoms(button_frame: Frame, grow: bool) -> Atoms<'static> {
|
||||
let words = [
|
||||
"apple",
|
||||
"banana",
|
||||
"kiwi",
|
||||
"strawberry",
|
||||
"fig",
|
||||
"pomegranate",
|
||||
"pear",
|
||||
"plum",
|
||||
"blackberry",
|
||||
"lime",
|
||||
"cantaloupe",
|
||||
"date",
|
||||
"guava",
|
||||
"melon",
|
||||
];
|
||||
|
||||
let mut atoms = Atoms::default();
|
||||
for word in words {
|
||||
let atom = Atom::layout(AtomLayout::new(word).frame(button_frame.clone()));
|
||||
atoms.push_right(if grow { atom.atom_grow(true) } else { atom });
|
||||
}
|
||||
atoms
|
||||
}
|
||||
|
||||
fn button_frame(ui: &Ui) -> Frame {
|
||||
ui.style()
|
||||
.button_style(
|
||||
&egui::widget_style::Classes::default(),
|
||||
egui::widget_style::WidgetState::Inactive,
|
||||
)
|
||||
.frame
|
||||
}
|
||||
|
||||
/// Tests flex-like wrapping ([`AtomLayout::wrap`]) of a justified list.
|
||||
///
|
||||
/// Each entry is a button-framed text of a different width, marked `grow`. Inside a
|
||||
/// main-justified [`Layout`] the atoms wrap onto multiple lines, and every line stretches its
|
||||
/// atoms to fill the full width.
|
||||
#[test]
|
||||
fn test_atom_wrap_justified() {
|
||||
let mut harness = HarnessBuilder::default()
|
||||
.with_size(Vec2::new(320.0, 240.0))
|
||||
.build_ui(|ui| {
|
||||
let atoms = fruit_atoms(button_frame(ui), true);
|
||||
ui.with_layout(
|
||||
Layout::left_to_right(Align::Min).with_main_justify(true),
|
||||
|ui| {
|
||||
AtomLayout::new(atoms).wrap(true).show(ui);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
harness.run();
|
||||
harness.snapshot("atom_wrap_justified");
|
||||
}
|
||||
|
||||
/// Tests non-justified (left-aligned, ragged) flex-like wrapping.
|
||||
///
|
||||
/// The atoms are not marked `grow`, so each line keeps its natural width and is left-aligned;
|
||||
/// `max_width` forces wrapping onto multiple lines.
|
||||
#[test]
|
||||
fn test_atom_wrap_ragged() {
|
||||
let mut harness = HarnessBuilder::default()
|
||||
.with_size(Vec2::new(320.0, 240.0))
|
||||
.build_ui(|ui| {
|
||||
let atoms = fruit_atoms(button_frame(ui), false);
|
||||
AtomLayout::new(atoms)
|
||||
.wrap(true)
|
||||
.max_width(220.0)
|
||||
.align2(egui::Align2::LEFT_TOP)
|
||||
.show(ui);
|
||||
});
|
||||
|
||||
harness.run();
|
||||
harness.snapshot("atom_wrap_ragged");
|
||||
}
|
||||
|
||||
/// Tests flex-like wrapping along a vertical ([`Direction::TopDown`]) main axis.
|
||||
///
|
||||
/// Atoms flow downward and wrap into new columns to the right once they exceed `max_height`.
|
||||
#[test]
|
||||
fn test_atom_wrap_top_down() {
|
||||
let mut harness = HarnessBuilder::default()
|
||||
.with_size(Vec2::new(320.0, 240.0))
|
||||
.build_ui(|ui| {
|
||||
let atoms = fruit_atoms(button_frame(ui), false);
|
||||
AtomLayout::new(atoms)
|
||||
.wrap(true)
|
||||
.direction(Direction::TopDown)
|
||||
.max_height(140.0)
|
||||
.align2(egui::Align2::LEFT_TOP)
|
||||
.show(ui);
|
||||
});
|
||||
|
||||
harness.run();
|
||||
harness.snapshot("atom_wrap_top_down");
|
||||
}
|
||||
|
||||
@@ -134,6 +134,19 @@ fn widget_tests() {
|
||||
},
|
||||
&mut results,
|
||||
);
|
||||
test_widget(
|
||||
"text_edit_multiline_prefix_suffix",
|
||||
|ui| {
|
||||
ui.spacing_mut().text_edit_width = 80.0;
|
||||
// Multiline wraps at the editable width (the width left after prefix/suffix), so this
|
||||
// exercises that the editable width is derived correctly.
|
||||
TextEdit::multiline(&mut "Wrap this longer text".to_owned())
|
||||
.prefix("🔎")
|
||||
.suffix("!")
|
||||
.ui(ui)
|
||||
},
|
||||
&mut results,
|
||||
);
|
||||
|
||||
test_widget(
|
||||
"slider",
|
||||
|
||||
Reference in New Issue
Block a user