Skip to content

Commit

Permalink
Fix word selection logic
Browse files Browse the repository at this point in the history
This is particularly relevant for when the user double-clicks a word.
Previously if the click fell on a word boundary we would not
recognize that; now if the click falls on a word boundary we will
treat that as the start of the new selection range.

In addition, word select now selects *anything* between two word
boundaries; we do not care if it is actually a word. As an example:
if the user double clicks in whitespcae, we will select any
contiguous whitespace.

progress on #1652
  • Loading branch information
cmyr committed Mar 18, 2021
1 parent fa156d7 commit 076bb04
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 15 deletions.
59 changes: 44 additions & 15 deletions druid/src/text/input_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::kurbo::{Line, Point, Rect, Vec2};
use crate::piet::TextLayout as _;
use crate::shell::text::{Action as ImeAction, Event as ImeUpdate, InputHandler};
use crate::widget::prelude::*;
use crate::{theme, Cursor, Env, Modifiers, Selector, TextAlignment, UpdateCtx};
use crate::{text, theme, Cursor, Env, Modifiers, Selector, TextAlignment, UpdateCtx};

/// A widget that accepts text input.
///
Expand Down Expand Up @@ -576,12 +576,30 @@ impl<T: TextStorage + EditableText> EditSession<T> {
self.scroll_to_selection_end(false);
}
ImeAction::SelectAll => {
let len = self.layout.text().as_ref().map(|t| t.len()).unwrap_or(0);
let len = buffer.len();
self.external_selection_change = Some(Selection::new(0, len));
}
//ImeAction::SelectLine | ImeAction::SelectParagraph | ImeAction::SelectWord => {
//tracing::warn!("Line/Word selection actions are not implemented");
//}
ImeAction::SelectWord => {
if self.selection.is_caret() {
let range =
text::movement::word_range_for_pos(buffer.as_str(), self.selection.start);
self.external_selection_change = Some(Selection::new(range.start, range.end));
}

// it is unclear what the behaviour should be if the selection
// is not a caret (and may span multiple words)
}
// This requires us to have access to the layout, which might be stale?
ImeAction::SelectLine => (),
// this assumes our internal selection is consistent with the buffer?
ImeAction::SelectParagraph => {
if !self.selection.is_caret() || buffer.len() < self.selection.start {
return;
}
let prev = buffer.preceding_line_break(self.selection.start);
let next = buffer.next_line_break(self.selection.start);
self.external_selection_change = Some(Selection::new(prev, next));
}
ImeAction::Delete(movement) if self.selection.is_caret() => {
let movement: Movement = movement.into();
if movement == Movement::Left {
Expand Down Expand Up @@ -683,26 +701,37 @@ impl<T: TextStorage + EditableText> EditSession<T> {
}

fn sel_region_for_pos(&mut self, pos: usize, click_count: u8) -> Range<usize> {
let text = match self.layout.text() {
Some(text) => text,
None => return pos..pos,
};
match click_count {
1 => pos..pos,
2 => {
//FIXME: this doesn't handle whitespace correctly
let word_min = text.prev_word_offset(pos).unwrap_or(0);
let word_max = text.next_word_offset(pos).unwrap_or_else(|| text.len());
word_min..word_max
}
2 => self.word_for_pos(pos),
_ => {
let text = match self.layout.text() {
Some(text) => text,
None => return pos..pos,
};
let line_min = text.preceding_line_break(pos);
let line_max = text.next_line_break(pos);
line_min..line_max
}
}
}

fn word_for_pos(&self, pos: usize) -> Range<usize> {
let layout = match self.layout.layout() {
Some(layout) => layout,
None => return pos..pos,
};

let line_n = layout.hit_test_text_position(pos).line;
let lm = layout.line_metric(line_n).unwrap();
let text = layout.line_text(line_n).unwrap();
let rel_pos = pos - lm.start_offset;
let mut range = text::movement::word_range_for_pos(text, rel_pos);
range.start += lm.start_offset;
range.end += lm.start_offset;
range
}

fn update(&mut self, ctx: &mut UpdateCtx, new_data: &T, env: &Env) {
if self
.layout
Expand Down
28 changes: 28 additions & 0 deletions druid/src/text/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

//! Text editing movements.

use std::ops::Range;

use unicode_segmentation::UnicodeSegmentation;

use crate::kurbo::Point;
use crate::piet::TextLayout as _;
use crate::text::{EditableText, Selection, TextLayout, TextStorage};
Expand Down Expand Up @@ -165,3 +169,27 @@ pub fn movement<T: EditableText + TextStorage>(
let start = if modify { s.start } else { offset };
Selection::new(start, offset).with_h_pos(h_pos)
}

/// Given a position in some text, return the containing word boundaries.
///
/// The returned range may not necessary be a 'word'; for instance it could be
/// the sequence of whitespace between two words.
///
/// If the position is on a word boundary, that will be considered the start
/// of the range.
///
/// This uses Unicode word boundaries, as defined in [UAX#29].
///
/// [UAX#29]: http://www.unicode.org/reports/tr29/
pub fn word_range_for_pos(text: &str, pos: usize) -> Range<usize> {
let mut word_iter = text.split_word_bound_indices().peekable();
let mut word_start = pos;
while let Some((ix, _)) = word_iter.next() {
if word_iter.peek().map(|(ix, _)| *ix > pos).unwrap_or(false) {
word_start = ix;
break;
}
}
let word_end = word_iter.next().map(|(ix, _)| ix).unwrap_or(pos);
word_start..word_end
}

0 comments on commit 076bb04

Please sign in to comment.