Skip to content

Commit 0f910f0

Browse files
committed
Unify druid & druid-shell selection types
This lets us delete druid/src/text/selection.rs
1 parent fa156d7 commit 0f910f0

File tree

9 files changed

+109
-177
lines changed

9 files changed

+109
-177
lines changed

druid-shell/examples/edit_text.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ impl WinHandler for AppState {
102102
}
103103
}
104104
if !doc.selection.is_caret() {
105-
for rect in layout.rects_for_range(doc.selection.to_range()) {
105+
for rect in layout.rects_for_range(doc.selection.range()) {
106106
piet.fill(rect, &SELECTION_BG_COLOR);
107107
}
108108
}
@@ -132,7 +132,7 @@ impl WinHandler for AppState {
132132
println!("user pressed c! wow! setting selection to 0");
133133

134134
// update internal selection state
135-
self.document.borrow_mut().selection = Selection::new_caret(0);
135+
self.document.borrow_mut().selection = Selection::caret(0);
136136

137137
// notify the OS that we've updated the selection
138138
self.handle
@@ -189,7 +189,7 @@ struct AppInputHandler {
189189

190190
impl InputHandler for AppInputHandler {
191191
fn selection(&self) -> Selection {
192-
self.state.borrow().selection.clone()
192+
self.state.borrow().selection
193193
}
194194
fn composition_range(&self) -> Option<Range<usize>> {
195195
self.state.borrow().composition.clone()
@@ -282,7 +282,7 @@ fn apply_default_behavior(handler: &mut AppInputHandler, action: Action) -> bool
282282
None => return false,
283283
}
284284
};
285-
handler.set_selection(Selection::new_caret(updated_index));
285+
handler.set_selection(Selection::caret(updated_index));
286286
}
287287
Action::MoveSelecting(movement) => {
288288
let mut selection = handler.selection();
@@ -294,24 +294,21 @@ fn apply_default_behavior(handler: &mut AppInputHandler, action: Action) -> bool
294294
}
295295
Action::SelectAll => {
296296
let len = handler.len();
297-
let selection = Selection {
298-
anchor: 0,
299-
active: len,
300-
};
297+
let selection = Selection::new(0, len);
301298
handler.set_selection(selection);
302299
}
303300
Action::Delete(_) if !is_caret => {
304301
// movement is ignored for non-caret selections
305302
let selection = handler.selection();
306-
handler.replace_range(selection.to_range(), "");
303+
handler.replace_range(selection.range(), "");
307304
}
308305
Action::Delete(movement) => {
309306
let mut selection = handler.selection();
310307
selection.active = match apply_movement(handler, movement, selection.active) {
311308
Some(v) => v,
312309
None => return false,
313310
};
314-
handler.replace_range(selection.to_range(), "");
311+
handler.replace_range(selection.range(), "");
315312
}
316313
_ => return false,
317314
}

druid-shell/src/platform/mac/text_input.rs

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub extern "C" fn marked_range(this: &mut Object, _: Sel) -> NSRange {
8585

8686
pub extern "C" fn selected_range(this: &mut Object, _: Sel) -> NSRange {
8787
with_edit_lock_from_window(this, false, |mut edit_lock| {
88-
let range = edit_lock.selection().to_range();
88+
let range = edit_lock.selection().range();
8989
encode_nsrange(&mut edit_lock, range)
9090
})
9191
.unwrap_or(NSRange::NONE)
@@ -105,7 +105,7 @@ pub extern "C" fn set_marked_text(
105105
// https://github.com/yvt/Stella2/blob/076fb6ee2294fcd1c56ed04dd2f4644bf456e947/tcw3/pal/src/macos/window.rs#L1144-L1146
106106
decode_nsrange(&*edit_lock, &replacement_range, 0).unwrap_or_else(|| {
107107
// no replacement range either? apparently we default to the selection in this case
108-
edit_lock.selection().to_range()
108+
edit_lock.selection().range()
109109
})
110110
});
111111

@@ -141,15 +141,9 @@ pub extern "C" fn set_marked_text(
141141
// preserve ordering of anchor and active
142142
let existing_selection = edit_lock.selection();
143143
let new_selection = if existing_selection.anchor < existing_selection.active {
144-
Selection {
145-
anchor: selection_range.start,
146-
active: selection_range.end,
147-
}
144+
Selection::new(selection_range.start, selection_range.end)
148145
} else {
149-
Selection {
150-
active: selection_range.start,
151-
anchor: selection_range.end,
152-
}
146+
Selection::new(selection_range.end, selection_range.start)
153147
};
154148
edit_lock.set_selection(new_selection);
155149
}
@@ -206,13 +200,13 @@ pub extern "C" fn insert_text(this: &mut Object, _: Sel, text: id, replacement_r
206200
// https://github.com/yvt/Stella2/blob/076fb6ee2294fcd1c56ed04dd2f4644bf456e947/tcw3/pal/src/macos/window.rs#L1041-L1043
207201
let converted_range = decode_nsrange(&*edit_lock, &replacement_range, 0)
208202
.or_else(|| edit_lock.composition_range())
209-
.unwrap_or_else(|| edit_lock.selection().to_range());
203+
.unwrap_or_else(|| edit_lock.selection().range());
210204

211205
edit_lock.replace_range(converted_range.clone(), text_string);
212206
edit_lock.set_composition_range(None);
213207
// move the caret next to the inserted text
214208
let caret_index = converted_range.start + text_string.len();
215-
edit_lock.set_selection(Selection::new_caret(caret_index));
209+
edit_lock.set_selection(Selection::caret(caret_index));
216210
});
217211
}
218212

@@ -385,7 +379,7 @@ fn do_command_by_selector_impl(mut edit_lock: Box<dyn InputHandler>, cmd: Sel) {
385379
// textedit testing showed that this operation never fully inverts a selection; if this action
386380
// would cause a selection's active and anchor to swap order, it makes a caret instead. applying
387381
// the operation a second time (on the selection that is now a caret) is required to invert.
388-
edit_lock.set_selection(Selection::new_caret(selection.anchor));
382+
edit_lock.set_selection(Selection::caret(selection.anchor));
389383
}
390384
}
391385
"moveParagraphForwardAndModifySelection:" => {
@@ -399,7 +393,7 @@ fn do_command_by_selector_impl(mut edit_lock: Box<dyn InputHandler>, cmd: Sel) {
399393
// textedit testing showed that this operation never fully inverts a selection; if this action
400394
// would cause a selection's active and anchor to swap order, it makes a caret instead. applying
401395
// the operation a second time (on the selection that is now a caret) is required to invert.
402-
edit_lock.set_selection(Selection::new_caret(selection.anchor));
396+
edit_lock.set_selection(Selection::caret(selection.anchor));
403397
}
404398
}
405399
"moveRight:" => edit_lock.handle_action(Action::Move(Movement::Grapheme(Direction::Right))),
@@ -531,7 +525,7 @@ fn do_command_by_selector_impl(mut edit_lock: Box<dyn InputHandler>, cmd: Sel) {
531525
edit_lock.handle_action(Action::MoveSelecting(Movement::Grapheme(
532526
Direction::Downstream,
533527
)));
534-
let new_selection = edit_lock.selection().to_range();
528+
let new_selection = edit_lock.selection().range();
535529
let next_grapheme = edit_lock.slice(new_selection.clone());
536530
let next_char = next_grapheme.chars().next();
537531

@@ -545,7 +539,7 @@ fn do_command_by_selector_impl(mut edit_lock: Box<dyn InputHandler>, cmd: Sel) {
545539
edit_lock.set_selection(old_selection);
546540
} else {
547541
// normally, end of transpose range will be next grapheme
548-
edit_lock.set_selection(Selection::new_caret(new_selection.end));
542+
edit_lock.set_selection(Selection::caret(new_selection.end));
549543
}
550544
}
551545

@@ -562,7 +556,7 @@ fn do_command_by_selector_impl(mut edit_lock: Box<dyn InputHandler>, cmd: Sel) {
562556
let second_grapheme = edit_lock.slice(middle_idx..selection.max());
563557
let new_string = format!("{}{}", second_grapheme, first_grapheme);
564558
// replace_range should automatically set selection to end of inserted range
565-
edit_lock.replace_range(selection.to_range(), &new_string);
559+
edit_lock.replace_range(selection.range(), &new_string);
566560
}
567561
"capitalizeWord:" => {
568562
// this command expands the selection to words, and then applies titlecase to that selection

druid-shell/src/text.rs

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ pub enum Event {
143143
/// holds shift and presses the right arrow key five times, we would expect the
144144
/// word `hello` to be selected, the `anchor` to still be `0`, and the `active`
145145
/// to now be `5`.
146-
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
146+
#[derive(Clone, Copy, Debug, Default, PartialEq)]
147+
#[non_exhaustive]
147148
pub struct Selection {
148149
/// The 'anchor' end of the selection.
149150
///
@@ -155,18 +156,72 @@ pub struct Selection {
155156
/// This is the end of the selection that moves while holding shift and
156157
/// pressing the arrow keys.
157158
pub active: usize,
159+
/// The saved horizontal position, during vertical movement.
160+
///
161+
/// This should not be set by the IME; it will be tracked and handled by
162+
/// the text field.
163+
pub h_pos: Option<f64>,
158164
}
159165

160166
#[allow(clippy::len_without_is_empty)]
161167
impl Selection {
162-
/// Create a new caret (zero-length selection ) at the provided UTF-8 byte index.
168+
/// Create a new `Selection` with the provided `anchor` and `active` positions.
169+
///
170+
/// Both positions refer to UTF-8 byte indices in some text.
171+
///
172+
/// If your selection is a caret, you can use [`Selection::caret`] instead.
173+
pub fn new(anchor: usize, active: usize) -> Selection {
174+
Selection {
175+
anchor,
176+
active,
177+
h_pos: None,
178+
}
179+
}
180+
181+
/// Create a new caret (zero-length selection) at the provided UTF-8 byte index.
163182
///
164183
/// `index` must be a grapheme cluster boundary.
165-
pub fn new_caret(index: usize) -> Selection {
184+
pub fn caret(index: usize) -> Selection {
166185
Selection {
167186
anchor: index,
168187
active: index,
188+
h_pos: None,
189+
}
190+
}
191+
192+
/// Construct a new selection from this selection, with the provided h_pos.
193+
///
194+
/// # Note
195+
///
196+
/// `h_pos` is used to track the *pixel* location of the cursor when moving
197+
/// vertically; lines may have available cursor positions at different
198+
/// positions, and arrowing down and then back up should always result
199+
/// in a cursor at the original starting location; doing this correctly
200+
/// requires tracking this state.
201+
///
202+
/// You *probably* don't need to use this, unless you are implementing a new
203+
/// text field, or otherwise implementing vertical cursor motion, in which
204+
/// case you will want to set this during vertical motion if it is not
205+
/// already set.
206+
pub fn with_h_pos(mut self, h_pos: Option<f64>) -> Self {
207+
self.h_pos = h_pos;
208+
self
209+
}
210+
211+
/// Create a new selection that is guaranteed to be valid for the provided
212+
/// text.
213+
#[must_use = "constrained constructs a new Selection"]
214+
pub fn constrained(mut self, s: &str) -> Self {
215+
let s_len = s.len();
216+
self.anchor = self.anchor.min(s_len);
217+
self.active = self.active.min(s_len);
218+
while !s.is_char_boundary(self.anchor) {
219+
self.anchor += 1;
169220
}
221+
while !s.is_char_boundary(self.active) {
222+
self.active += 1;
223+
}
224+
self
170225
}
171226

172227
/// Return the position of the upstream end of the selection.
@@ -191,7 +246,7 @@ impl Selection {
191246
///
192247
/// This is the range that would be replaced if text were inserted at this
193248
/// selection.
194-
pub fn to_range(&self) -> Range<usize> {
249+
pub fn range(&self) -> Range<usize> {
195250
self.min()..self.max()
196251
}
197252

@@ -241,6 +296,9 @@ pub trait InputHandler {
241296
/// Both `selection.anchor` and `selection.active` must be less
242297
/// than or equal to the value returned from `InputHandler::len()`.
243298
///
299+
/// Properties of the `Selection` *other* than `anchor` and `active` may
300+
/// be ignored by the handler.
301+
///
244302
/// The `set_selection` implementation should round up (downstream) both
245303
/// `selection.anchor` and `selection.active` to the nearest extended
246304
/// grapheme cluster boundary.
@@ -414,9 +472,9 @@ pub fn simulate_input<H: WinHandler + ?Sized>(
414472
match event.key {
415473
KbKey::Character(c) if !event.mods.ctrl() && !event.mods.meta() && !event.mods.alt() => {
416474
let selection = input_handler.selection();
417-
input_handler.replace_range(selection.to_range(), &c);
475+
input_handler.replace_range(selection.range(), &c);
418476
let new_caret_index = selection.min() + c.len();
419-
input_handler.set_selection(Selection::new_caret(new_caret_index));
477+
input_handler.set_selection(Selection::caret(new_caret_index));
420478
}
421479
KbKey::ArrowLeft => {
422480
let movement = Movement::Grapheme(Direction::Left);

druid/src/text/backspace.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,6 @@ pub fn offset_for_delete_backwards(region: &Selection, text: &impl EditableText)
198198
if !region.is_caret() {
199199
region.min()
200200
} else {
201-
backspace_offset(text, region.end)
201+
backspace_offset(text, region.active)
202202
}
203203
}

druid/src/text/input_component.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ impl<T: TextStorage + EditableText> Widget<T> for TextComponent<T> {
415415
let sel_rects = self.borrow().layout.rects_for_range(selection.range());
416416
if let Some(composition) = composition {
417417
// I believe selection should always be contained in composition range while composing?
418-
assert!(composition.start <= selection.start && composition.end >= selection.end);
418+
assert!(composition.start <= selection.anchor && composition.end >= selection.active);
419419
let comp_rects = self.borrow().layout.rects_for_range(composition);
420420
for region in comp_rects {
421421
let y = region.max_y().floor();
@@ -647,7 +647,7 @@ impl<T: TextStorage + EditableText> EditSession<T> {
647647
fn backspace(&mut self, buffer: &mut T) {
648648
let to_del = if self.selection.is_caret() {
649649
let del_start = crate::text::offset_for_delete_backwards(&self.selection, buffer);
650-
del_start..self.selection.start
650+
del_start..self.selection.anchor
651651
} else {
652652
self.selection.range()
653653
};
@@ -660,19 +660,19 @@ impl<T: TextStorage + EditableText> EditSession<T> {
660660
let point = point + Vec2::new(self.alignment_offset, 0.0);
661661
let pos = self.layout.text_position_for_point(point);
662662
if mods.shift() {
663-
self.selection.end = pos;
663+
self.selection.active = pos;
664664
} else {
665665
let sel = self.sel_region_for_pos(pos, count);
666-
self.selection.start = sel.start;
667-
self.selection.end = sel.end;
666+
self.selection.anchor = sel.start;
667+
self.selection.active = sel.end;
668668
}
669669
}
670670

671671
fn do_drag(&mut self, point: Point) {
672672
let point = point + Vec2::new(self.alignment_offset, 0.0);
673673
//FIXME: this should behave differently if we were double or triple clicked
674674
let pos = self.layout.text_position_for_point(point);
675-
self.selection.end = pos;
675+
self.selection.active = pos;
676676
self.scroll_to_selection_end(false);
677677
}
678678

@@ -717,7 +717,7 @@ impl<T: TextStorage + EditableText> EditSession<T> {
717717
if self.layout.needs_rebuild_after_update(ctx) {
718718
ctx.request_layout();
719719
}
720-
let new_sel = self.selection.constrained(new_data);
720+
let new_sel = self.selection.constrained(new_data.as_str());
721721
if new_sel != self.selection {
722722
self.selection = new_sel;
723723
self.update_pending_invalidation(ImeUpdate::SelectionChanged);
@@ -734,12 +734,12 @@ impl<T: TextStorage> EditSessionHandle<T> {
734734
}
735735

736736
impl<T: TextStorage + EditableText> InputHandler for EditSessionHandle<T> {
737-
fn selection(&self) -> crate::shell::text::Selection {
738-
self.inner.borrow().selection.into()
737+
fn selection(&self) -> Selection {
738+
self.inner.borrow().selection
739739
}
740740

741-
fn set_selection(&mut self, selection: crate::shell::text::Selection) {
742-
self.inner.borrow_mut().external_selection_change = Some(selection.into());
741+
fn set_selection(&mut self, selection: Selection) {
742+
self.inner.borrow_mut().external_selection_change = Some(selection);
743743
self.inner.borrow_mut().external_scroll_to = Some(true);
744744
}
745745

druid/src/text/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ mod input_methods;
2424
mod layout;
2525
pub mod movement;
2626
mod rich_text;
27-
pub mod selection;
2827
mod storage;
2928

29+
pub use druid_shell::text::Selection;
30+
3031
pub use self::attribute::{Attribute, AttributeSpans, Link};
3132
pub use self::backspace::offset_for_delete_backwards;
3233
pub use self::editable_text::{EditableText, EditableTextCursor, StringCursor};
3334
pub use self::font_descriptor::FontDescriptor;
3435
pub use self::layout::{LayoutMetrics, TextLayout};
3536
pub use self::movement::{movement, Movement};
36-
pub use self::selection::Selection;
3737
pub use input_component::{EditSession, TextComponent};
3838
pub use input_methods::ImeHandlerRef;
3939
pub use rich_text::{AttributesAdder, RichText, RichTextBuilder};

0 commit comments

Comments
 (0)