From 4e1567cecb7290f27adaa35fa6d20e7141344e50 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 7 Apr 2026 13:29:57 +0200 Subject: [PATCH] Fix text selection of centered and right-aligned text (#8076) * Fixes https://github.com/emilk/egui/issues/8049 * Bug introduced in https://github.com/emilk/egui/pull/7831 --- crates/egui_demo_lib/src/demo/text_layout.rs | 32 ++++++++++++++++--- crates/egui_demo_lib/tests/misc.rs | 20 +++++++----- .../tests/snapshots/demos/Text Layout.png | 4 +-- .../tests/snapshots/text_selection_0.png | 4 +-- .../tests/snapshots/text_selection_1.png | 4 +-- crates/epaint/src/text/text_layout_types.rs | 4 +-- 6 files changed, 48 insertions(+), 20 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/text_layout.rs b/crates/egui_demo_lib/src/demo/text_layout.rs index ef84fb326..656d5a6d2 100644 --- a/crates/egui_demo_lib/src/demo/text_layout.rs +++ b/crates/egui_demo_lib/src/demo/text_layout.rs @@ -7,17 +7,21 @@ pub struct TextLayoutDemo { overflow_character: Option, extra_letter_spacing: f32, line_height_pixels: u32, + halign: egui::Align, + justify: bool, lorem_ipsum: bool, } impl Default for TextLayoutDemo { fn default() -> Self { Self { - max_rows: 6, - break_anywhere: true, + max_rows: 1000, + break_anywhere: false, overflow_character: Some('…'), extra_letter_spacing: 0.0, line_height_pixels: 0, + halign: egui::Align::LEFT, + justify: false, lorem_ipsum: true, } } @@ -48,6 +52,8 @@ impl crate::View for TextLayoutDemo { overflow_character, extra_letter_spacing, line_height_pixels, + halign, + justify, lorem_ipsum, } = self; @@ -109,6 +115,18 @@ impl crate::View for TextLayoutDemo { }); ui.end_row(); + ui.label("Horizontal align:"); + ui.horizontal(|ui| { + ui.selectable_value(halign, egui::Align::LEFT, "Left"); + ui.selectable_value(halign, egui::Align::Center, "Center"); + ui.selectable_value(halign, egui::Align::RIGHT, "Right"); + }); + ui.end_row(); + + ui.label("Justify:"); + ui.checkbox(justify, "Fill row width"); + ui.end_row(); + ui.label("Text:"); ui.horizontal(|ui| { ui.selectable_value(lorem_ipsum, true, "Lorem Ipsum"); @@ -145,8 +163,14 @@ impl crate::View for TextLayoutDemo { ..Default::default() }; - // NOTE: `Label` overrides some of the wrapping settings, e.g. wrap width - ui.label(job); + // NOTE: `Label` overrides some of the wrapping settings, + // e.g. wrap width, halign, and justify. + ui.with_layout( + egui::Layout::top_down(*halign).with_cross_justify(*justify), + |ui| { + ui.label(job); + }, + ); }); } } diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index d5f6a3a3c..946975aa0 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -61,21 +61,25 @@ fn test_italics() { fn test_text_selection() { let mut results = egui_kittest::SnapshotResults::new(); - for (test_idx, drag_start_x) in [0.2_f32, 0.9].into_iter().enumerate() { - let mut harness = Harness::builder().build_ui(|ui| { - let visuals = ui.visuals_mut(); - visuals.selection.bg_fill = Color32::LIGHT_GREEN; - visuals.selection.stroke.color = Color32::RED; + for (test_idx, drag_start_x) in [0.2_f32, 0.95].into_iter().enumerate() { + let mut harness = Harness::builder() + .with_pixels_per_point(1.0) // TODO(emilk): why does this test fail with 2.0? + .build_ui(|ui| { + let visuals = ui.visuals_mut(); + visuals.selection.bg_fill = Color32::LIGHT_GREEN; + visuals.selection.stroke.color = Color32::RED; - ui.label("Some varied ☺ text :)\nAnd it has a second line!"); - }); + ui.vertical_centered(|ui| { + ui.label("Some varied ☺ text :)\nAnd it has a second line!\nAnd a third!"); + }); + }); harness.run(); harness.fit_contents(); // Drag to select text: let label = harness.get_by_role(Role::Label); harness.drag_at(label.rect().lerp_inside([drag_start_x, 0.25])); - harness.drop_at(label.rect().lerp_inside([0.6, 0.75])); + harness.drop_at(label.rect().lerp_inside([0.5, 0.75])); harness.run(); harness.snapshot(format!("text_selection_{test_idx}")); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index e9b302746..0bd5b9ceb 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c964d07a39ad286a562b53cdfe514d568d91955e6c1ca06a0cb5e45dbe3977e -size 60947 +oid sha256:661570ed4bdf24d52ab049e7d3cf22ef4d50542ee5486d133e0a618a6146da42 +size 98582 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png index a7aac34bf..dae75cfc3 100644 --- a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce1e16fa09588ec5351d031408657137f42ff40eb7cf6dd00b8c65a4f51ec680 -size 5332 +oid sha256:d7d8578b55ed8fdaf2062cb53d7320555e490289ff9efb30f39ebb697d2e252e +size 6529 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png index 8691211cb..4be490adc 100644 --- a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60449af267336663304e44e254d0984e037bebfa2d1efdf32234cab4374e8c79 -size 5301 +oid sha256:5e09ec77bd473ad6695557b78d4967ade09b12100f04bc66d0e29779dfb0d551 +size 6495 diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 22fb03c57..481e3ebbf 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1047,7 +1047,7 @@ impl Galley { return self.end_pos(); }; - let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left(); + let x = row.x_offset(layout_cursor.column) + row.pos.x; Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } @@ -1092,7 +1092,7 @@ impl Galley { if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos. - let column = row.char_at(pos.x - row.pos.x + self.rect.left()); + let column = row.char_at(pos.x - row.pos.x); let prefer_next_row = column < row.char_count_excluding_newline(); cursor = CCursor { index: ccursor_index + column,