1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

Implement wrapping atom layout

This commit is contained in:
lucasmerlin
2026-06-04 15:31:45 +02:00
parent cd9e351221
commit 3a85b165aa
20 changed files with 931 additions and 212 deletions

View 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;
}
}
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7506fa98769b5d387dac906206859824b8cf8c86665c383e3721cbe74e839379
size 13160

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f56cb05b59acf3550f73ad9609922f1b49da83ba7671148f5f5067b7e45c8918
size 13219

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:133af05ea2a6c387547cbba17b27d5e93a3f13df7d7ade5c134a39b057d48d03
size 13241

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca11ca5fbb4d1efa83929decfbea0fe723ed8da32277bcffbe509f444b5e8fa5
size 572550

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:860b8f749bfbbc27f037ac0f643a19873d0e8c879baa919d76878ad658efa846
size 22237

View File

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

View File

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