From fa4cfec777c9ea21b8643f77b6a26561da44aad0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 7 Nov 2025 15:34:36 +0100 Subject: [PATCH] Change text color of selected text (#7691) Selected text now gets the color of `visuals.selection.stroke.color`. This means you can have inverted colors for selected text, like in the new test: image It also means the color of selected text in labels matches that of the text color of selected buttons. --- crates/egui/src/style.rs | 3 ++ crates/egui/src/text_selection/visuals.rs | 31 +++++++++++++++++-- crates/egui_demo_lib/tests/misc.rs | 26 ++++++++++++++-- .../image_dark_x1.00.png | 0 .../image_dark_x1.41.png | 0 .../image_dark_x2.00.png | 0 .../image_light_x1.00.png | 0 .../image_light_x1.41.png | 0 .../image_light_x2.00.png | 0 .../tests/snapshots/text_selection.png | 3 ++ crates/emath/src/rect.rs | 3 +- crates/epaint/src/text/text_layout.rs | 9 ++++-- crates/epaint/src/text/text_layout_types.rs | 3 ++ 13 files changed, 70 insertions(+), 8 deletions(-) rename crates/egui_demo_lib/tests/snapshots/{image_blending => italics}/image_dark_x1.00.png (100%) rename crates/egui_demo_lib/tests/snapshots/{image_blending => italics}/image_dark_x1.41.png (100%) rename crates/egui_demo_lib/tests/snapshots/{image_blending => italics}/image_dark_x2.00.png (100%) rename crates/egui_demo_lib/tests/snapshots/{image_blending => italics}/image_light_x1.00.png (100%) rename crates/egui_demo_lib/tests/snapshots/{image_blending => italics}/image_light_x1.41.png (100%) rename crates/egui_demo_lib/tests/snapshots/{image_blending => italics}/image_light_x2.00.png (100%) create mode 100644 crates/egui_demo_lib/tests/snapshots/text_selection.png diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 454fc6d89..9982c05bb 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1129,7 +1129,10 @@ impl Visuals { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct Selection { + /// Background color behind selected text and other selectable buttons. pub bg_fill: Color32, + + /// Color of selected text. pub stroke: Stroke, } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 0f6d54abd..50bb1a34d 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -25,7 +25,9 @@ pub fn paint_text_selection( // and so we need to clone it if it is shared: let galley: &mut Galley = Arc::make_mut(galley); - let color = visuals.selection.bg_fill; + let background_color = visuals.selection.bg_fill; + let text_color = visuals.selection.stroke.color; + let [min, max] = cursor_range.sorted_cursors(); let min = galley.layout_from_cursor(min); let max = galley.layout_from_cursor(max); @@ -53,6 +55,31 @@ pub fn paint_text_selection( let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y)); let mesh = &mut row.visuals.mesh; + if !row.glyphs.is_empty() { + // Change color of the selected text: + let first_glyph_index = if ri == min.row { min.column } else { 0 }; + let last_glyph_index = if ri == max.row { + max.column + } else { + row.glyphs.len() - 1 + }; + + let first_vertex_index = row + .glyphs + .get(first_glyph_index) + .map_or(row.visuals.glyph_vertex_range.start, |g| { + g.first_vertex as _ + }); + let last_vertex_index = row + .glyphs + .get(last_glyph_index) + .map_or(row.visuals.glyph_vertex_range.end, |g| g.first_vertex as _); + + for vi in first_vertex_index..last_vertex_index { + mesh.vertices[vi].color = text_color; + } + } + // Time to insert the selection rectangle into the row mesh. // It should be on top (after) of any background in the galley, // but behind (before) any glyphs. The row visuals has this information: @@ -60,7 +87,7 @@ pub fn paint_text_selection( // Start by appending the selection rectangle to end of the mesh, as two triangles (= 6 indices): let num_indices_before = mesh.indices.len(); - mesh.add_colored_rect(rect, color); + mesh.add_colored_rect(rect, background_color); assert_eq!( num_indices_before + 6, mesh.indices.len(), diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index 395baceb6..af8858bca 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -1,4 +1,5 @@ -use egui_kittest::Harness; +use egui::{Color32, accesskit::Role}; +use egui_kittest::{Harness, kittest::Queryable as _}; #[test] fn test_kerning() { @@ -42,7 +43,7 @@ fn test_italics() { harness.run(); harness.fit_contents(); harness.snapshot(format!( - "image_blending/image_{theme}_x{pixels_per_point:.2}", + "italics/image_{theme}_x{pixels_per_point:.2}", theme = match theme { egui::Theme::Dark => "dark", egui::Theme::Light => "light", @@ -51,3 +52,24 @@ fn test_italics() { } } } + +#[test] +fn test_text_selection() { + 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::DARK_BLUE; + + ui.label("Some varied ☺ text :)\nAnd it has a second line!"); + }); + harness.run(); + harness.fit_contents(); + + // Drag to select text: + let label = harness.get_by_role(Role::Label); + harness.drag_at(label.rect().lerp_inside([0.2, 0.25])); + harness.drop_at(label.rect().lerp_inside([0.6, 0.75])); + harness.run(); + + harness.snapshot("text_selection"); +} diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_dark_x1.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.00.png similarity index 100% rename from crates/egui_demo_lib/tests/snapshots/image_blending/image_dark_x1.00.png rename to crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.00.png diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_dark_x1.41.png b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.41.png similarity index 100% rename from crates/egui_demo_lib/tests/snapshots/image_blending/image_dark_x1.41.png rename to crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.41.png diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_dark_x2.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x2.00.png similarity index 100% rename from crates/egui_demo_lib/tests/snapshots/image_blending/image_dark_x2.00.png rename to crates/egui_demo_lib/tests/snapshots/italics/image_dark_x2.00.png diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_light_x1.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.00.png similarity index 100% rename from crates/egui_demo_lib/tests/snapshots/image_blending/image_light_x1.00.png rename to crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.00.png diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_light_x1.41.png b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.41.png similarity index 100% rename from crates/egui_demo_lib/tests/snapshots/image_blending/image_light_x1.41.png rename to crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.41.png diff --git a/crates/egui_demo_lib/tests/snapshots/image_blending/image_light_x2.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x2.00.png similarity index 100% rename from crates/egui_demo_lib/tests/snapshots/image_blending/image_light_x2.00.png rename to crates/egui_demo_lib/tests/snapshots/italics/image_light_x2.00.png diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection.png b/crates/egui_demo_lib/tests/snapshots/text_selection.png new file mode 100644 index 000000000..78ebc0dbf --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/text_selection.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14f253fedc94985ff1431f1016d901d747e1f9948531cc6350f6615649f29056 +size 4862 diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index b46fc43ca..81729713b 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -449,7 +449,8 @@ impl Rect { /// Linearly interpolate so that `[0, 0]` is [`Self::min`] and /// `[1, 1]` is [`Self::max`]. #[inline] - pub fn lerp_inside(&self, t: Vec2) -> Pos2 { + pub fn lerp_inside(&self, t: impl Into) -> Pos2 { + let t = t.into(); Pos2 { x: lerp(self.min.x..=self.max.x, t.x), y: lerp(self.min.y..=self.max.y, t.y), diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index b1fe895da..1db56731d 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -230,6 +230,7 @@ fn layout_section( font_ascent: font_metrics.ascent, uv_rect: glyph_alloc.uv_rect, section_index, + first_vertex: 0, // filled in later }); paragraph.cursor_x_px += glyph_alloc.advance_width_px; @@ -531,6 +532,7 @@ fn replace_last_glyph_with_overflow_character( font_ascent: font_metrics.ascent, uv_rect: replacement_glyph_alloc.uv_rect, section_index, + first_vertex: 0, // filled in later }); return; } @@ -748,7 +750,7 @@ fn tessellate_row( point_scale: PointScale, job: &LayoutJob, format_summary: &FormatSummary, - row: &Row, + row: &mut Row, ) -> RowVisuals { if row.glyphs.is_empty() { return Default::default(); @@ -843,8 +845,9 @@ fn add_row_backgrounds(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh end_run(run_start.take(), last_rect.right()); } -fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh: &mut Mesh) { - for glyph in &row.glyphs { +fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &mut Row, mesh: &mut Mesh) { + for glyph in &mut row.glyphs { + glyph.first_vertex = mesh.vertices.len() as u32; let uv_rect = glyph.uv_rect; if !uv_rect.is_nothing() { let mut left_top = glyph.pos + uv_rect.offset; diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index d87f9a579..f3963394a 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -701,6 +701,9 @@ pub struct Glyph { /// enable the paragraph-concat optimization path without having to /// adjust `section_index` when concatting. pub(crate) section_index: u32, + + /// Which is our first vertex in [`RowVisuals::mesh`]. + pub first_vertex: u32, } impl Glyph {