Add `Galley::intrinsic_size` and use it in `AtomLayout` (#7146)
- part of https://github.com/emilk/egui/issues/5762 - also allows me to simplify sizing logic in egui_flex
This commit is contained in:
parent
f46926aaf1
commit
508c60b2e2
|
|
@ -81,7 +81,7 @@ impl<'a> Atom<'a> {
|
||||||
wrap_mode = Some(TextWrapMode::Truncate);
|
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
|
let size = self
|
||||||
.size
|
.size
|
||||||
|
|
@ -89,7 +89,7 @@ impl<'a> Atom<'a> {
|
||||||
|
|
||||||
SizedAtom {
|
SizedAtom {
|
||||||
size,
|
size,
|
||||||
preferred_size: preferred,
|
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
|
||||||
grow: self.grow,
|
grow: self.grow,
|
||||||
kind,
|
kind,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,11 +81,10 @@ impl<'a> AtomKind<'a> {
|
||||||
) -> (Vec2, SizedAtomKind<'a>) {
|
) -> (Vec2, SizedAtomKind<'a>) {
|
||||||
match self {
|
match self {
|
||||||
AtomKind::Text(text) => {
|
AtomKind::Text(text) => {
|
||||||
let galley = text.into_galley(ui, wrap_mode, available_size.x, TextStyle::Button);
|
let wrap_mode = wrap_mode.unwrap_or(ui.wrap_mode());
|
||||||
(
|
let galley =
|
||||||
galley.size(), // TODO(#5762): calculate the preferred size
|
text.into_galley(ui, Some(wrap_mode), available_size.x, TextStyle::Button);
|
||||||
SizedAtomKind::Text(galley),
|
(galley.intrinsic_size, SizedAtomKind::Text(galley))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
AtomKind::Image(image) => {
|
AtomKind::Image(image) => {
|
||||||
let size = image.load_and_calc_size(ui, available_size);
|
let size = image.load_and_calc_size(ui, available_size);
|
||||||
|
|
|
||||||
|
|
@ -183,10 +183,10 @@ impl<'a> AtomLayout<'a> {
|
||||||
|
|
||||||
let mut desired_width = 0.0;
|
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.
|
// text is not wrapped. Used to set Response::intrinsic_size.
|
||||||
let mut preferred_width = 0.0;
|
let mut intrinsic_width = 0.0;
|
||||||
let mut preferred_height = 0.0;
|
let mut intrinsic_height = 0.0;
|
||||||
|
|
||||||
let mut height: f32 = 0.0;
|
let mut height: f32 = 0.0;
|
||||||
|
|
||||||
|
|
@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
|
||||||
if atoms.len() > 1 {
|
if atoms.len() > 1 {
|
||||||
let gap_space = gap * (atoms.len() as f32 - 1.0);
|
let gap_space = gap * (atoms.len() as f32 - 1.0);
|
||||||
desired_width += gap_space;
|
desired_width += gap_space;
|
||||||
preferred_width += gap_space;
|
intrinsic_width += gap_space;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (idx, item) in atoms.into_iter().enumerate() {
|
for (idx, item) in atoms.into_iter().enumerate() {
|
||||||
|
|
@ -224,10 +224,10 @@ impl<'a> AtomLayout<'a> {
|
||||||
let size = sized.size;
|
let size = sized.size;
|
||||||
|
|
||||||
desired_width += size.x;
|
desired_width += size.x;
|
||||||
preferred_width += sized.preferred_size.x;
|
intrinsic_width += sized.intrinsic_size.x;
|
||||||
|
|
||||||
height = height.at_least(size.y);
|
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);
|
sized_items.push(sized);
|
||||||
}
|
}
|
||||||
|
|
@ -243,10 +243,10 @@ impl<'a> AtomLayout<'a> {
|
||||||
let size = sized.size;
|
let size = sized.size;
|
||||||
|
|
||||||
desired_width += size.x;
|
desired_width += size.x;
|
||||||
preferred_width += sized.preferred_size.x;
|
intrinsic_width += sized.intrinsic_size.x;
|
||||||
|
|
||||||
height = height.at_least(size.y);
|
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);
|
sized_items.insert(index, sized);
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +259,7 @@ impl<'a> AtomLayout<'a> {
|
||||||
let mut response = ui.interact(rect, id, sense);
|
let mut response = ui.interact(rect, id, sense);
|
||||||
|
|
||||||
response.intrinsic_size =
|
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 {
|
AllocatedAtomLayout {
|
||||||
sized_atoms: sized_items,
|
sized_atoms: sized_items,
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ pub struct SizedAtom<'a> {
|
||||||
/// size.x + gap.
|
/// size.x + gap.
|
||||||
pub size: Vec2,
|
pub size: Vec2,
|
||||||
|
|
||||||
/// Preferred size of the atom. This is used to calculate `Response::intrinsic_size`.
|
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
|
||||||
pub preferred_size: Vec2,
|
pub intrinsic_size: Vec2,
|
||||||
|
|
||||||
pub kind: SizedAtomKind<'a>,
|
pub kind: SizedAtomKind<'a>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,10 +130,12 @@ impl TextShape {
|
||||||
num_vertices: _,
|
num_vertices: _,
|
||||||
num_indices: _,
|
num_indices: _,
|
||||||
pixels_per_point: _,
|
pixels_per_point: _,
|
||||||
|
intrinsic_size,
|
||||||
} = Arc::make_mut(galley);
|
} = Arc::make_mut(galley);
|
||||||
|
|
||||||
*rect = transform.scaling * *rect;
|
*rect = transform.scaling * *rect;
|
||||||
*mesh_bounds = transform.scaling * *mesh_bounds;
|
*mesh_bounds = transform.scaling * *mesh_bounds;
|
||||||
|
*intrinsic_size = transform.scaling * *intrinsic_size;
|
||||||
|
|
||||||
for text::PlacedRow { pos, row } in rows {
|
for text::PlacedRow { pos, row } in rows {
|
||||||
*pos *= transform.scaling;
|
*pos *= transform.scaling;
|
||||||
|
|
|
||||||
|
|
@ -1072,6 +1072,7 @@ mod tests {
|
||||||
use core::f32;
|
use core::f32;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::text::{TextWrapping, layout};
|
||||||
use crate::{Stroke, text::TextFormat};
|
use crate::{Stroke, text::TextFormat};
|
||||||
use ecolor::Color32;
|
use ecolor::Color32;
|
||||||
use emath::Align;
|
use emath::Align;
|
||||||
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||||
num_indices: 0,
|
num_indices: 0,
|
||||||
pixels_per_point: fonts.pixels_per_point(),
|
pixels_per_point: fonts.pixels_per_point(),
|
||||||
elided: true,
|
elided: true,
|
||||||
|
intrinsic_size: Vec2::ZERO,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +95,8 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||||
|
|
||||||
let point_scale = PointScale::new(fonts.pixels_per_point());
|
let point_scale = PointScale::new(fonts.pixels_per_point());
|
||||||
|
|
||||||
|
let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs);
|
||||||
|
|
||||||
let mut elided = false;
|
let mut elided = false;
|
||||||
let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
|
let mut rows = rows_from_paragraphs(paragraphs, &job, &mut elided);
|
||||||
if elided {
|
if elided {
|
||||||
|
|
@ -124,7 +127,7 @@ pub fn layout(fonts: &mut FontsImpl, job: Arc<LayoutJob>) -> Galley {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the Y positions and tessellate the text:
|
// 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.
|
// Ignores the Y coordinate.
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
intrinsic_size.y += point_scale.round_to_pixel(height);
|
||||||
|
}
|
||||||
|
intrinsic_size
|
||||||
|
}
|
||||||
|
|
||||||
// Ignores the Y coordinate.
|
// Ignores the Y coordinate.
|
||||||
fn rows_from_paragraphs(
|
fn rows_from_paragraphs(
|
||||||
paragraphs: Vec<Paragraph>,
|
paragraphs: Vec<Paragraph>,
|
||||||
|
|
@ -610,6 +653,7 @@ fn galley_from_rows(
|
||||||
job: Arc<LayoutJob>,
|
job: Arc<LayoutJob>,
|
||||||
mut rows: Vec<PlacedRow>,
|
mut rows: Vec<PlacedRow>,
|
||||||
elided: bool,
|
elided: bool,
|
||||||
|
intrinsic_size: Vec2,
|
||||||
) -> Galley {
|
) -> Galley {
|
||||||
let mut first_row_min_height = job.first_row_min_height;
|
let mut first_row_min_height = job.first_row_min_height;
|
||||||
let mut cursor_y = 0.0;
|
let mut cursor_y = 0.0;
|
||||||
|
|
@ -680,6 +724,7 @@ fn galley_from_rows(
|
||||||
num_vertices,
|
num_vertices,
|
||||||
num_indices,
|
num_indices,
|
||||||
pixels_per_point: point_scale.pixels_per_point,
|
pixels_per_point: point_scale.pixels_per_point,
|
||||||
|
intrinsic_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
if galley.job.round_output_to_gui {
|
if galley.job.round_output_to_gui {
|
||||||
|
|
|
||||||
|
|
@ -560,6 +560,12 @@ pub struct Galley {
|
||||||
/// so that we can warn if this has changed once we get to
|
/// so that we can warn if this has changed once we get to
|
||||||
/// tessellation.
|
/// tessellation.
|
||||||
pub pixels_per_point: f32,
|
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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
|
@ -821,6 +827,8 @@ impl Galley {
|
||||||
.at_most(rect.min.x + self.job.wrap.max_width)
|
.at_most(rect.min.x + self.job.wrap.max_width)
|
||||||
.floor_ui();
|
.floor_ui();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.intrinsic_size = self.intrinsic_size.round_ui();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append each galley under the previous one.
|
/// Append each galley under the previous one.
|
||||||
|
|
@ -836,6 +844,7 @@ impl Galley {
|
||||||
num_vertices: 0,
|
num_vertices: 0,
|
||||||
num_indices: 0,
|
num_indices: 0,
|
||||||
pixels_per_point,
|
pixels_per_point,
|
||||||
|
intrinsic_size: Vec2::ZERO,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (i, galley) in galleys.iter().enumerate() {
|
for (i, galley) in galleys.iter().enumerate() {
|
||||||
|
|
@ -872,6 +881,9 @@ impl Galley {
|
||||||
// Note that if `galley.elided` is true this will be the last `Galley` in
|
// Note that if `galley.elided` is true this will be the last `Galley` in
|
||||||
// the vector and the loop will end.
|
// the vector and the loop will end.
|
||||||
merged_galley.elided |= galley.elided;
|
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 {
|
if merged_galley.job.round_output_to_gui {
|
||||||
|
|
|
||||||
|
|
@ -69,3 +69,35 @@ fn single_test(name: &str, mut f: impl FnMut(&mut Ui)) -> SnapshotResult {
|
||||||
|
|
||||||
harness.try_snapshot(name)
|
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));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue