Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/egui/src/atomics/atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ impl<'a> Atom<'a> {
wrap_mode = Some(TextWrapMode::Truncate);
}

let (preferred, kind) = self.kind.into_sized(ui, available_size, wrap_mode);
let (intrinsic, kind) = self.kind.into_sized(ui, available_size, wrap_mode);

let size = self
.size
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));

SizedAtom {
size,
preferred_size: preferred,
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
grow: self.grow,
kind,
}
Expand Down
9 changes: 4 additions & 5 deletions crates/egui/src/atomics/atom_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,10 @@ impl<'a> AtomKind<'a> {
) -> (Vec2, SizedAtomKind<'a>) {
match self {
AtomKind::Text(text) => {
let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
(
galley.size(), // TODO(#5762): calculate the preferred size
SizedAtomKind::Text(galley),
)
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
let galley =
text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button);
(galley.intrinsic_size, SizedAtomKind::Text(galley))
}
AtomKind::Image(image) => {
let size = image.load_and_calc_size(ui, available_size);
Expand Down
18 changes: 9 additions & 9 deletions crates/egui/src/atomics/atom_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,10 @@ impl<'a> AtomLayout<'a> {

let mut desired_width = 0.0;

// Preferred width / height is the ideal size of the widget, e.g. the size where the
// intrinsic width / height is the ideal size of the widget, e.g. the size where the
// text is not wrapped. Used to set Response::intrinsic_size.
let mut preferred_width = 0.0;
let mut preferred_height = 0.0;
let mut intrinsic_width = 0.0;
let mut intrinsic_height = 0.0;

let mut height: f32 = 0.0;

Expand All @@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
desired_width += gap_space;
preferred_width += gap_space;
intrinsic_width += gap_space;
}

for (idx, item) in atoms.into_iter().enumerate() {
Expand All @@ -224,10 +224,10 @@ impl<'a> AtomLayout<'a> {
let size = sized.size;

desired_width += size.x;
preferred_width += sized.preferred_size.x;
intrinsic_width += sized.intrinsic_size.x;

height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);

sized_items.push(sized);
}
Expand All @@ -243,10 +243,10 @@ impl<'a> AtomLayout<'a> {
let size = sized.size;

desired_width += size.x;
preferred_width += sized.preferred_size.x;
intrinsic_width += sized.intrinsic_size.x;

height = height.at_least(size.y);
preferred_height = preferred_height.at_least(sized.preferred_size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);

sized_items.insert(index, sized);
}
Expand All @@ -259,7 +259,7 @@ impl<'a> AtomLayout<'a> {
let mut response = ui.interact(rect, id, sense);

response.intrinsic_size =
Some((Vec2::new(preferred_width, preferred_height) + margin.sum()).at_least(min_size));
Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));

AllocatedAtomLayout {
sized_atoms: sized_items,
Expand Down
4 changes: 2 additions & 2 deletions crates/egui/src/atomics/sized_atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ pub struct SizedAtom<'a> {
/// size.x + gap.
pub size: Vec2,

/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
pub preferred_size: Vec2,
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
pub intrinsic_size: Vec2,

pub kind: SizedAtomKind<'a>,
}
Expand Down
2 changes: 2 additions & 0 deletions crates/epaint/src/shapes/text_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,12 @@ impl TextShape {
num_vertices: _,
num_indices: _,
pixels_per_point: _,
intrinsic_size,
} = Arc::make_mut(galley);

*rect = transform.scaling * *rect;
*mesh_bounds = transform.scaling * *mesh_bounds;
*intrinsic_size = transform.scaling * *intrinsic_size;

for text::PlacedRow { pos, row } in rows {
*pos *= transform.scaling;
Expand Down
57 changes: 57 additions & 0 deletions crates/epaint/src/text/fonts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,7 @@ mod tests {
use core::f32;

use super::*;
use crate::text::{TextWrapping, layout};
use crate::{Stroke, text::TextFormat};
use ecolor::Color32;
use emath::Align;
Expand Down Expand Up @@ -1183,4 +1184,60 @@ mod tests {
}
}
}

#[test]
fn test_intrinsic_size() {
let pixels_per_point = [1.0, 1.3, 2.0, 0.867];
let max_widths = [40.0, 80.0, 133.0, 200.0];
let rounded_output_to_gui = [false, true];

for pixels_per_point in pixels_per_point {
let mut fonts = FontsImpl::new(
pixels_per_point,
1024,
AlphaFromCoverage::default(),
FontDefinitions::default(),
);

for &max_width in &max_widths {
for round_output_to_gui in rounded_output_to_gui {
for mut job in jobs() {
job.wrap = TextWrapping::wrap_at_width(max_width);

job.round_output_to_gui = round_output_to_gui;

let galley_wrapped = layout(&mut fonts, job.clone().into());

job.wrap = TextWrapping::no_max_width();

let text = job.text.clone();
let galley_unwrapped = layout(&mut fonts, job.into());

let intrinsic_size = galley_wrapped.intrinsic_size;
let unwrapped_size = galley_unwrapped.size();

let difference = (intrinsic_size - unwrapped_size).length().abs();
similar_asserts::assert_eq!(
format!("{intrinsic_size:.4?}"),
format!("{unwrapped_size:.4?}"),
"Wrapped intrinsic size should almost match unwrapped size. Intrinsic: {intrinsic_size:.8?} vs unwrapped: {unwrapped_size:.8?}
Difference: {difference:.8?}
wrapped rows: {}, unwrapped rows: {}
pixels_per_point: {pixels_per_point}, text: {text:?}, max_width: {max_width}, round_output_to_gui: {round_output_to_gui}",
galley_wrapped.rows.len(),
galley_unwrapped.rows.len()
);
similar_asserts::assert_eq!(
format!("{intrinsic_size:.4?}"),
format!("{unwrapped_size:.4?}"),
"Unwrapped galley intrinsic size should exactly match its size. \
{:.8?} vs {:8?}",
galley_unwrapped.intrinsic_size,
galley_unwrapped.size(),
);
}
}
}
}
}
}
47 changes: 46 additions & 1 deletion crates/epaint/src/text/text_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
num_indices: 0,
pixels_per_point: fonts.pixels_per_point(),
elided: true,
intrinsic_size: Vec2::ZERO,
};
}

Expand All @@ -94,6 +95,8 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {

let point_scale = PointScale::new(fonts.pixels_per_point());

let intrinsic_size = calculate_intrinsic_size(point_scale, &job, &paragraphs);

let mut elided = false;
let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
if elided {
Expand Down Expand Up @@ -124,7 +127,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
}

// Calculate the Y positions and tessellate the text:
galley_from_rows(point_scale, job, rows, elided)
galley_from_rows(point_scale, job, rows, elided, intrinsic_size)
}

// Ignores the Y coordinate.
Expand Down Expand Up @@ -190,6 +193,46 @@ fn layout_section(
}
}

/// Calculate the intrinsic size of the text.
///
/// The result is eventually passed to `Response::intrinsic_size`.
/// This works by calculating the size of each `Paragraph` (instead of each `Row`).
fn calculate_intrinsic_size(
point_scale: PointScale,
job: &LayoutJob,
paragraphs: &[Paragraph],
) -> Vec2 {
let mut intrinsic_size = Vec2::ZERO;
for (idx, paragraph) in paragraphs.iter().enumerate() {
if paragraph.glyphs.is_empty() {
if idx == 0 {
intrinsic_size.y += point_scale.round_to_pixel(paragraph.empty_paragraph_height);
}
continue;
}
Comment on lines +207 to +212
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this if-statement even needed? Seems like the logic below should be robust to handling this regardless? In fact, more robust, since the code below takes first_row_min_height into account

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, otherwise the test_split_paragraphs test fails. (I explained why in this comment in case you didn't see it #7146 (comment))

I'll try to fix this in a separate PR.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, thanks!

intrinsic_size.x = f32::max(
paragraph
.glyphs
.last()
.map(|l| l.max_x())
.unwrap_or_default(),
intrinsic_size.x,
);

let mut height = paragraph
.glyphs
.iter()
.map(|g| g.line_height)
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(paragraph.empty_paragraph_height);
if idx == 0 {
height = f32::max(height, job.first_row_min_height);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a test for this in

fn jobs() -> Vec<LayoutJob> {

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw your comment in #7146 (comment) - let's do that in a follow-up PR then :)

}
intrinsic_size.y += point_scale.round_to_pixel(height);
}
intrinsic_size
}

// Ignores the Y coordinate.
fn rows_from_paragraphs(
paragraphs: Vec<Paragraph>,
Expand Down Expand Up @@ -610,6 +653,7 @@ fn galley_from_rows(
job: Arc<LayoutJob>,
mut rows: Vec<PlacedRow>,
elided: bool,
intrinsic_size: Vec2,
) -> Galley {
let mut first_row_min_height = job.first_row_min_height;
let mut cursor_y = 0.0;
Expand Down Expand Up @@ -680,6 +724,7 @@ fn galley_from_rows(
num_vertices,
num_indices,
pixels_per_point: point_scale.pixels_per_point,
intrinsic_size,
};

if galley.job.round_output_to_gui {
Expand Down
12 changes: 12 additions & 0 deletions crates/epaint/src/text/text_layout_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ pub struct Galley {
/// so that we can warn if this has changed once we get to
/// tessellation.
pub pixels_per_point: f32,

/// This is the size that a non-wrapped, non-truncated, non-justified version of the text
/// would have.
///
/// Useful for advanced layouting.
pub intrinsic_size: Vec2,
}

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -821,6 +827,8 @@ impl Galley {
.at_most(rect.min.x + self.job.wrap.max_width)
.floor_ui();
}

self.intrinsic_size = self.intrinsic_size.round_ui();
}

/// Append each galley under the previous one.
Expand All @@ -836,6 +844,7 @@ impl Galley {
num_vertices: 0,
num_indices: 0,
pixels_per_point,
intrinsic_size: Vec2::ZERO,
};

for (i, galley) in galleys.iter().enumerate() {
Expand Down Expand Up @@ -872,6 +881,9 @@ impl Galley {
// Note that if `galley.elided` is true this will be the last `Galley` in
// the vector and the loop will end.
merged_galley.elided |= galley.elided;
merged_galley.intrinsic_size.x =
f32::max(merged_galley.intrinsic_size.x, galley.intrinsic_size.x);
merged_galley.intrinsic_size.y += galley.intrinsic_size.y;
}

if merged_galley.job.round_output_to_gui {
Expand Down
32 changes: 32 additions & 0 deletions tests/egui_tests/tests/test_atoms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,35 @@ fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult {

harness.try_snapshot(name)
}

#[test]
fn test_intrinsic_size() {
let mut intrinsic_size = None;
for wrapping in [
TextWrapMode::Extend,
TextWrapMode::Wrap,
TextWrapMode::Truncate,
] {
_ = HarnessBuilder::default()
.with_size(Vec2::new(100.0, 100.0))
.build_ui(|ui| {
ui.style_mut().wrap_mode = Some(wrapping);
let response = ui.add(Button::new(
"Hello world this is a long text that should be wrapped.",
));
if let Some(current_intrinsic_size) = intrinsic_size {
assert_eq!(
Some(current_intrinsic_size),
response.intrinsic_size,
"For wrapping: {wrapping:?}"
);
}
assert!(
response.intrinsic_size.is_some(),
"intrinsic_size should be set for `Button`"
);
intrinsic_size = response.intrinsic_size;
});
}
assert_eq!(intrinsic_size.unwrap().round(), Vec2::new(305.0, 18.0));
}