feat: select until char and double click select delimiter

adds a feature to select from the start of the sentence until the last
occurrence of a character. This can be used to select until the
extension in cosmic-files save dialog or rename pop up.

Also, it adds a feature to select until the last occurrence of a
character on double-click.
This commit is contained in:
Hojjat 2026-04-06 22:56:18 -06:00
parent 9aa87cd66b
commit 3e37e3b843
2 changed files with 49 additions and 4 deletions

View file

@ -188,6 +188,7 @@ pub struct TextInput<'a, Message> {
is_editable_variant: bool,
is_read_only: bool,
select_on_focus: bool,
double_click_select_delimiter: Option<char>,
font: Option<<crate::Renderer as iced_core::text::Renderer>::Font>,
width: Length,
padding: Padding,
@ -238,6 +239,7 @@ where
is_editable_variant: false,
is_read_only: false,
select_on_focus: false,
double_click_select_delimiter: None,
font: None,
width: Length::Fill,
padding: spacing.into(),
@ -343,6 +345,17 @@ where
self
}
/// Sets a delimiter character for double-click selection behavior.
///
/// When set, double-clicking before the last occurrence of this character
/// selects from the start to that character. Double-clicking after the
/// delimiter uses normal word selection.
#[inline]
pub const fn double_click_select_delimiter(mut self, delimiter: char) -> Self {
self.double_click_select_delimiter = Some(delimiter);
self
}
/// Emits a message when an unfocused text input has been focused by click.
///
/// This will not trigger if the input was focused externally by the application.
@ -598,6 +611,7 @@ where
self.value = state.tracked_value.clone();
// std::mem::swap(&mut state.tracked_value, &mut self.value);
}
state.double_click_select_delimiter = self.double_click_select_delimiter;
// Unfocus text input if it becomes disabled
if self.on_input.is_none() && !self.manage_value {
state.last_click = None;
@ -1180,6 +1194,14 @@ pub fn select_range<Message: 'static>(id: Id, start: usize, end: usize) -> Task<
)))
}
/// Produces a [`Task`] that selects from the front to the last occurrence of the given character
/// in the [`TextInput`] with the given [`Id`], or selects all if not found.
pub fn select_until_last<Message: 'static>(id: Id, value: &str, ch: char) -> Task<Message> {
let v = Value::new(value);
let end = v.rfind_char(ch).unwrap_or(v.len());
select_range(id, 0, end)
}
/// Computes the layout of a [`TextInput`].
#[allow(clippy::cast_precision_loss)]
#[allow(clippy::too_many_arguments)]
@ -1600,10 +1622,23 @@ pub fn update<'a, Message: Clone + 'static>(
.unwrap_or((0, text::Affinity::Before));
state.cursor.set_affinity(affinity);
state.cursor.select_range(
value.previous_start_of_word(position),
value.next_end_of_word(position),
);
if let Some(delimiter) = state.double_click_select_delimiter {
if let Some(delim_pos) = value.rfind_char(delimiter) {
if position <= delim_pos {
state.cursor.select_range(0, delim_pos);
} else {
state.cursor.select_range(delim_pos + 1, value.len());
}
} else {
state.cursor.select_all(value);
}
} else {
state.cursor.select_range(
value.previous_start_of_word(position),
value.next_end_of_word(position),
);
}
}
state.dragging_state = Some(DraggingState::Selection);
}
@ -2882,6 +2917,7 @@ pub struct State {
pub is_read_only: bool,
pub emit_unfocus: bool,
select_on_focus: bool,
double_click_select_delimiter: Option<char>,
is_focused: Option<Focus>,
dragging_state: Option<DraggingState>,
dnd_offer: DndOfferState,
@ -2963,6 +2999,7 @@ impl State {
emit_unfocus: false,
is_focused: None,
select_on_focus: false,
double_click_select_delimiter: None,
dragging_state: None,
dnd_offer: DndOfferState::default(),
is_pasting: None,

View file

@ -142,6 +142,14 @@ impl Value {
.sum()
}
/// Returns the grapheme index of the last occurrence of the given character,
/// searching from the end.
#[must_use]
pub fn rfind_char(&self, ch: char) -> Option<usize> {
let needle = ch.to_string();
self.graphemes.iter().rposition(|g| g == &needle)
}
/// Converts a byte index to a grapheme index.
#[must_use]
pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize {