diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 34e0a21ce..b9bb0bcde 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -1,3 +1,4 @@ +use super::MeasureCache; use crate::{ AtomKind, AtomLayout, FontSelection, Id, IntoSizedArgs, IntoSizedResult, SizedAtom, Ui, }; @@ -124,6 +125,7 @@ impl<'a> Atom<'a> { mut available_size: Vec2, mut wrap_mode: Option, fallback_font: FontSelection, + cache: &mut MeasureCache<'a>, ) -> SizedAtom<'a> { if !self.shrink && self.max_size.x.is_infinite() { wrap_mode = Some(TextWrapMode::Extend); @@ -149,6 +151,7 @@ impl<'a> Atom<'a> { wrap_mode, fallback_font, }, + cache, ); let size = self diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index ceec9083d..526086332 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -102,6 +102,7 @@ impl<'a> AtomKind<'a> { wrap_mode, fallback_font, }: IntoSizedArgs, + cache: &mut super::MeasureCache<'a>, ) -> IntoSizedResult<'a> { match self { AtomKind::Text(text) => { @@ -131,8 +132,10 @@ impl<'a> AtomKind<'a> { AtomKind::Layout(layout) => { // Measure at the natural size for the parent's sizing, but keep a shared handle to // the original layout so a grown atom can be re-measured at its painted size in - // `paint_at` (cheap `Arc` clone, no deep copy). - let sized = layout.measure(ui, available_size); + // `paint_at` (cheap `Rc` clone, no deep copy). `measure_rc` shares the `cache` + // (keyed by the `Rc`'s identity) so a deep tree of `grow` layouts doesn't + // re-measure its descendants exponentially. + let sized = AtomLayout::measure_rc(layout, ui, available_size, cache); IntoSizedResult { intrinsic_size: sized.intrinsic_size, sized: SizedAtomKind::Layout { diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index 4b137c582..f8ff88676 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -6,9 +6,19 @@ use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::{Color32, Galley}; use smallvec::SmallVec; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; +use std::rc::Rc; use std::sync::Arc; +/// Frame-pass-local memoization cache for [`AtomLayout::measure_rc`]. +/// +/// Keyed by an [`Rc::as_ptr`] identity plus the available-size bits. Both are stable within a +/// single top-level measure pass (nested layouts are held alive via `Rc`), so repeatedly measuring +/// the same nested layout at the same size — which a deep tree of `grow` layouts does `O(2^depth)` +/// times — becomes a cache hit instead of a full re-measure. +pub(crate) type MeasureCache<'a> = HashMap<(usize, u64), SizedAtomLayout<'a>>; + /// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y). #[inline] fn main_cross_axis(direction: Direction) -> (usize, usize) { @@ -341,6 +351,44 @@ impl<'a> AtomLayout<'a> { /// clamped by `max_size`/`min_size`, exactly like [`Self::allocate`] does with /// [`Ui::available_size`]. pub fn measure(&self, ui: &Ui, available_size: Vec2) -> SizedAtomLayout<'a> { + self.measure_impl(ui, available_size, &mut MeasureCache::default()) + } + + /// Measure a nested layout held by an [`Rc`], memoizing the result in `cache`. + /// + /// A grown nested `Layout` atom is re-measured (the cross-after-main reflow) at its grown + /// size, recursively. Without memoization a deep tree of `grow` layouts re-measures its + /// descendants `O(2^depth)` times. Keyed by the layout's [`Rc::as_ptr`] identity and the + /// available size — both stable within a pass — repeated `(layout, size)` measures become + /// cache hits. The `Rc` is held by the caller (the `Layout` atom / reflow source), which is + /// why the identity lives here rather than in [`Self::measure_impl`]. + pub(crate) fn measure_rc( + layout: &Rc, + ui: &Ui, + available_size: Vec2, + cache: &mut MeasureCache<'a>, + ) -> SizedAtomLayout<'a> { + let key = ( + Rc::as_ptr(layout) as usize, + (u64::from(available_size.x.to_bits()) << 32) | u64::from(available_size.y.to_bits()), + ); + if let Some(cached) = cache.get(&key) { + return cached.clone(); + } + let result = layout.measure_impl(ui, available_size, cache); + cache.insert(key, result.clone()); + result + } + + /// The measure body. Threads `cache` so nested [`Rc`] layouts are memoized via + /// [`Self::measure_rc`]; it does not memoize its own result (a top-level layout is measured + /// once, and a nested one is keyed by its `Rc` at the call site). + pub(crate) fn measure_impl( + &self, + ui: &Ui, + available_size: Vec2, + cache: &mut MeasureCache<'a>, + ) -> SizedAtomLayout<'a> { let atoms = &self.atoms; let frame = self.frame; let sense = self.sense; @@ -444,6 +492,7 @@ impl<'a> AtomLayout<'a> { available_inner_size, Some(wrap_mode), fallback_font.clone(), + cache, ); let size = sized.size; @@ -473,6 +522,7 @@ impl<'a> AtomLayout<'a> { available_size_for_shrink_item, Some(wrap_mode), fallback_font, + cache, ) } else { let mut item = item.clone(); @@ -482,6 +532,7 @@ impl<'a> AtomLayout<'a> { available_size_for_shrink_item, Some(wrap_mode), fallback_font, + cache, ) }; let size = sized.size; @@ -553,7 +604,7 @@ impl<'a> AtomLayout<'a> { sized.size[main_axis] + grow_main, available_inner_size[cross_axis], ); - let remeasured = source.measure(ui, grown); + let remeasured = AtomLayout::measure_rc(source, ui, grown, cache); sized.size[cross_axis] = remeasured.outer_size[cross_axis]; **inner = remeasured; } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index b015a0f60..740423006 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -594,6 +594,7 @@ impl TextEdit<'_> { Vec2::new(available_inner_width, f32::INFINITY), Some(TextWrapMode::Extend), FontSelection::default(), + &mut Default::default(), ) .size .x