1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

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:
<img width="154" height="46" alt="image"
src="https://github.com/user-attachments/assets/2666361d-d7e2-4d50-8e4d-2fcc128f1a81"
/>


It also means the color of selected text in labels matches that of the
text color of selected buttons.
This commit is contained in:
Emil Ernerfeldt
2025-11-07 15:34:36 +01:00
parent 93fb652f79
commit 3657e4239c
13 changed files with 70 additions and 8 deletions

View File

@@ -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,
}

View File

@@ -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);
@@ -52,6 +54,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:
@@ -59,7 +86,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(),

View File

@@ -1,4 +1,5 @@
use egui_kittest::Harness;
use egui::{Color32, accesskit::Role};
use egui_kittest::{Harness, kittest::Queryable as _};
#[test]
fn test_kerning() {
@@ -45,7 +46,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",
@@ -55,3 +56,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");
}

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:14f253fedc94985ff1431f1016d901d747e1f9948531cc6350f6615649f29056
size 4862

View File

@@ -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<Vec2>) -> 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),

View File

@@ -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;
@@ -532,6 +533,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;
}
@@ -749,7 +751,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();
@@ -844,8 +846,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;

View File

@@ -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 {