mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 21:30:03 -04:00
Fix text layout bugs in wrapped texts (#8137)
Fixes some bugs that happen randomly when resizing horizontal_wrapped texts: https://github.com/user-attachments/assets/141392d2-0239-465a-ba7b-c864f7823319 Adds regression tests (I enjoy using claude to fix these bugs, first have it create a minimal repro test case, then fix the bug by iterating until it figures out a fix).
This commit is contained in:
committed by
lucasmerlin
parent
27aa63a520
commit
e06edc127d
@@ -1,5 +1,5 @@
|
|||||||
use egui::accesskit::{self, Role};
|
use egui::accesskit::{self, Role};
|
||||||
use egui::{Button, ComboBox, Image, Modifiers, Popup, Vec2, Widget as _};
|
use egui::{Button, ComboBox, Image, Modifiers, Popup, Rect, Vec2, Widget as _};
|
||||||
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
#[cfg(all(feature = "wgpu", feature = "snapshot"))]
|
||||||
use egui_kittest::SnapshotResults;
|
use egui_kittest::SnapshotResults;
|
||||||
use egui_kittest::{Harness, kittest::Queryable as _};
|
use egui_kittest::{Harness, kittest::Queryable as _};
|
||||||
@@ -337,6 +337,100 @@ pub fn keyboard_should_close_nested_submenu_with_second_enter() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Regression test for a bug in `horizontal_wrapped` layouts where text wraps but does not
|
||||||
|
/// move to the next line, causing overlapping text.
|
||||||
|
///
|
||||||
|
/// Sweeps the available width from 200 down to 50 (one frame per width) and asserts that no
|
||||||
|
/// two `TextRun` accesskit nodes (one per laid-out row) have overlapping bounds, and that
|
||||||
|
/// all accesskit text runs and painted text shapes stay within the `horizontal_wrapped` rect.
|
||||||
|
#[test]
|
||||||
|
pub fn horizontal_wrapped_text_should_not_overlap() {
|
||||||
|
struct State {
|
||||||
|
width: f32,
|
||||||
|
rect: egui::Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut harness = Harness::builder()
|
||||||
|
.with_size(Vec2::new(300.0, 400.0))
|
||||||
|
.build_ui_state(
|
||||||
|
|ui, state: &mut State| {
|
||||||
|
ui.set_width(state.width);
|
||||||
|
state.rect = egui::Frame::popup(ui.style())
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.set_width(ui.available_width());
|
||||||
|
for i in 0..20 {
|
||||||
|
ui.label(format!("Hello{i}"));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.response
|
||||||
|
.rect
|
||||||
|
})
|
||||||
|
.inner;
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
width: 200.0,
|
||||||
|
rect: Rect::NAN,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let min_width = 50.0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let width = harness.state().width - 1.0;
|
||||||
|
if width < min_width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
harness.state_mut().width = width;
|
||||||
|
harness.step();
|
||||||
|
|
||||||
|
let container_rect = harness.state().rect.expand(1.0);
|
||||||
|
|
||||||
|
let runs: Vec<_> = harness
|
||||||
|
.query_all_by_role(accesskit::Role::TextRun)
|
||||||
|
.map(|node| (node.rect(), node.value().unwrap_or_default()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (rect, text) in &runs {
|
||||||
|
assert!(
|
||||||
|
container_rect.contains_rect(*rect),
|
||||||
|
"TextRun rect at available width = {width} is outside horizontal_wrapped rect: \
|
||||||
|
{text:?} {rect:?} outside {container_rect:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for clipped in &harness.output().shapes {
|
||||||
|
if let egui::epaint::Shape::Text(text_shape) = &clipped.shape {
|
||||||
|
let shape_rect = text_shape.visual_bounding_rect();
|
||||||
|
assert!(
|
||||||
|
container_rect.contains_rect(shape_rect),
|
||||||
|
"TextShape rect at available width = {width} is outside horizontal_wrapped rect: \
|
||||||
|
{:?} {shape_rect:?} outside {container_rect:?}",
|
||||||
|
text_shape.galley.text()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..runs.len() {
|
||||||
|
for j in (i + 1)..runs.len() {
|
||||||
|
let (a, ta) = &runs[i];
|
||||||
|
let (b, tb) = &runs[j];
|
||||||
|
let inter = a.intersect(*b);
|
||||||
|
// Allow tiny floating-point slop for rects that just touch.
|
||||||
|
let overlaps = inter.width() > 0.5 && inter.height() > 0.5;
|
||||||
|
assert!(
|
||||||
|
!overlaps,
|
||||||
|
"TextRun rects overlap at available width = {width}: \
|
||||||
|
{ta:?} {a:?} vs {tb:?} {b:?} \
|
||||||
|
(overlap = {}x{})",
|
||||||
|
inter.width(),
|
||||||
|
inter.height()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn pointer_click_on_open_submenu_button_should_not_close_it() {
|
pub fn pointer_click_on_open_submenu_button_should_not_close_it() {
|
||||||
let mut harness = keyboard_submenu_harness();
|
let mut harness = keyboard_submenu_harness();
|
||||||
|
|||||||
@@ -420,18 +420,19 @@ fn line_break(
|
|||||||
if job.wrap.max_rows <= out_rows.len() {
|
if job.wrap.max_rows <= out_rows.len() {
|
||||||
*elided = true; // can't fit another row
|
*elided = true; // can't fit another row
|
||||||
} else {
|
} else {
|
||||||
|
let paragraph_min_x = paragraph.glyphs[row_start_idx].pos.x - row_start_x;
|
||||||
|
let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x() - row_start_x;
|
||||||
|
|
||||||
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
|
let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.map(|mut glyph| {
|
.map(|mut glyph| {
|
||||||
glyph.pos.x -= row_start_x;
|
glyph.pos.x -= row_start_x + paragraph_min_x;
|
||||||
glyph
|
glyph
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let section_index_at_start = glyphs[0].section_index;
|
let section_index_at_start = glyphs[0].section_index;
|
||||||
let paragraph_min_x = glyphs[0].pos.x;
|
|
||||||
let paragraph_max_x = glyphs.last().unwrap().max_x();
|
|
||||||
|
|
||||||
out_rows.push(PlacedRow {
|
out_rows.push(PlacedRow {
|
||||||
pos: pos2(paragraph_min_x, 0.0),
|
pos: pos2(paragraph_min_x, 0.0),
|
||||||
|
|||||||
@@ -696,9 +696,12 @@ impl PlacedRow {
|
|||||||
|
|
||||||
/// Same as [`Self::rect`] but excluding the `LayoutSection::leading_space`.
|
/// Same as [`Self::rect`] but excluding the `LayoutSection::leading_space`.
|
||||||
pub fn rect_without_leading_space(&self) -> Rect {
|
pub fn rect_without_leading_space(&self) -> Rect {
|
||||||
let x = self.glyphs.first().map_or(self.pos.x, |g| g.pos.x);
|
let x = self.pos.x + self.glyphs.first().map_or(0.0, |g| g.pos.x);
|
||||||
let size_x = self.size.x - x;
|
let right = self.pos.x + self.size.x;
|
||||||
Rect::from_min_size(Pos2::new(x, self.pos.y), Vec2::new(size_x, self.size.y))
|
Rect::from_min_max(
|
||||||
|
Pos2::new(x, self.pos.y),
|
||||||
|
Pos2::new(right, self.pos.y + self.size.y),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user