fix(text_input): font color in selected mode (#1409)

This commit is contained in:
Alex Klepov 2026-09-10 18:33:57 +03:00 • committed by GitHub
parent 521f3c2baf
commit 7cc116803b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,6 +7,7 @@
//! A [`TextInput`] has some local [`State`]. //! A [`TextInput`] has some local [`State`].
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::{Cell, LazyCell}; use std::cell::{Cell, LazyCell};
use unicode_segmentation::UnicodeSegmentation;
use crate::ext::ColorExt; use crate::ext::ColorExt;
use crate::theme::THEME; use crate::theme::THEME;
@ -2561,6 +2562,63 @@ fn input_method<'b>(
} }
} }
#[allow(clippy::too_many_arguments)]
fn render_graphemes(
value: &Value,
state: &State,
left: usize,
right: usize,
text: &str,
text_color: Color,
bounds: Rectangle,
size: f32,
renderer: &mut crate::Renderer,
font: iced_core::Font,
) {
let lo_byte = value.byte_index_at_grapheme(left);
let hi_byte = value.byte_index_at_grapheme(right);
let rects = state.value.raw().highlight(
0,
(lo_byte, text::Affinity::After),
(hi_byte, text::Affinity::Before),
);
if !rects.is_empty() {
let grapheme_range = text[lo_byte..hi_byte].to_string();
let origin = bounds.position();
let start_pos = origin + (rects.first().unwrap().position() - Point::ORIGIN);
for rect in rects {
let absolute_rect = Rectangle {
x: rect.x + origin.x,
y: rect.y + origin.y,
width: rect.width,
height: rect.height,
};
renderer.fill_text(
Text {
content: grapheme_range.clone(),
font,
bounds: bounds.size(),
size: iced::Pixels(size),
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::None,
ellipsize: text::Ellipsize::None,
},
start_pos,
text_color,
absolute_rect,
);
}
}
}
/// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// Draws the [`TextInput`] with the given [`Renderer`], overriding its
/// [`Value`] if provided. /// [`Value`] if provided.
/// ///
@ -2769,7 +2827,7 @@ pub fn draw<'a, Message>(
let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None);
#[cfg(not(wayland_platform))] #[cfg(not(wayland_platform))]
let handling_dnd_offer = false; let handling_dnd_offer = false;
let (cursors, offset, is_selecting) = if let Some(focus) = let (cursors, offset, _) = if let Some(focus) =
state.is_focused.filter(|f| f.focused).or_else(|| { state.is_focused.filter(|f| f.focused).or_else(|| {
let now = Instant::now(); let now = Instant::now();
handling_dnd_offer.then_some(Focus { handling_dnd_offer.then_some(Focus {
@ -2843,7 +2901,6 @@ pub fn draw<'a, Message>(
(lo_byte, text::Affinity::After), (lo_byte, text::Affinity::After),
(hi_byte, text::Affinity::Before), (hi_byte, text::Affinity::Before),
); );
let cursors: Vec<(renderer::Quad, Color)> = rects let cursors: Vec<(renderer::Quad, Color)> = rects
.into_iter() .into_iter()
.map(|r| { .map(|r| {
@ -2897,7 +2954,6 @@ pub fn draw<'a, Message>(
state.value.raw().min_width(), state.value.raw().min_width(),
effective_alignment(state.value.raw()), effective_alignment(state.value.raw()),
); );
if cursors.is_empty() { if cursors.is_empty() {
renderer.with_translation(Vector::ZERO, |_| {}); renderer.with_translation(Vector::ZERO, |_| {});
} else { } else {
@ -2914,33 +2970,77 @@ pub fn draw<'a, Message>(
width: actual_width, width: actual_width,
..text_bounds ..text_bounds
}; };
let color = if text.is_empty() { let tcolor = if text.is_empty() {
appearance.placeholder_color appearance.placeholder_color
} else { } else {
text_color text_color
}; };
renderer.fill_text( if let cursor::State::Selection { start, end } = state.cursor.state(value)
Text { && state.is_focused()
content: if text.is_empty() { {
placeholder.to_string() let left = start.min(end);
} else { let right = end.max(start);
text.clone() let grapheme_len = text.graphemes(true).count();
if left > 0 {
render_graphemes(
value, state, 0, left, &text, tcolor, bounds, size, renderer, font,
);
}
if grapheme_len >= right {
render_graphemes(
value,
state,
left,
right,
&text,
appearance.selected_text_color,
bounds,
size,
renderer,
font,
);
}
if left < grapheme_len {
render_graphemes(
value,
state,
right,
grapheme_len,
&text,
tcolor,
bounds,
size,
renderer,
font,
);
}
} else {
renderer.fill_text(
Text {
content: if text.is_empty() {
placeholder.to_string()
} else {
text.clone()
},
font,
bounds: bounds.size(),
size: iced::Pixels(size),
align_x: text::Alignment::Default,
align_y: alignment::Vertical::Center,
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::None,
ellipsize: text::Ellipsize::None,
}, },
font, bounds.position(),
bounds: bounds.size(), tcolor,
size: iced::Pixels(size), text_bounds,
align_x: text::Alignment::Default, );
align_y: alignment::Vertical::Center, }
line_height: text::LineHeight::default(),
shaping: text::Shaping::Advanced,
wrapping: text::Wrapping::None,
ellipsize: text::Ellipsize::None,
},
bounds.position(),
color,
text_bounds,
);
}; };
// FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue.