Skip to content

Commit 080a4fd

Browse files
authored
Select "paragraphs" rather than words for triple click (#381)
See discussion around [#parley > Sharing selection/editing code between Blitz and Masonry @ πŸ’¬](https://xi.zulipchat.com/#narrow/channel/205635-parley/topic/Sharing.20selection.2Fediting.20code.20between.20Blitz.20and.20Masonry/near/525065337)
1 parent 63ec13a commit 080a4fd

File tree

12 files changed

+199
-1
lines changed

12 files changed

+199
-1
lines changed

β€Žexamples/vello_editor/src/text.rsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ impl Editor {
265265
);
266266
match state.count {
267267
2 => drv.select_word_at_point(cursor_pos.0, cursor_pos.1),
268-
3 => drv.select_line_at_point(cursor_pos.0, cursor_pos.1),
268+
3 => drv.select_hard_line_at_point(cursor_pos.0, cursor_pos.1),
269269
_ => drv.move_to_point(cursor_pos.0, cursor_pos.1),
270270
}
271271
}

β€Žparley/src/layout/cursor.rsβ€Ž

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,21 @@ impl Selection {
487487
}
488488
}
489489

490+
/// Creates a new selection bounding the "logical" line at the given coordinates.
491+
///
492+
/// That is, the line as defined by line break characters, rather than due to soft-wrapping.
493+
pub fn hard_line_from_point<B: Brush>(layout: &Layout<B>, x: f32, y: f32) -> Self {
494+
let Self { anchor, focus, .. } = Self::from_point(layout, x, y)
495+
.hard_line_start(layout, false)
496+
.hard_line_end(layout, true);
497+
Self {
498+
anchor,
499+
focus,
500+
anchor_base: AnchorBase::Line(anchor, focus),
501+
h_pos: None,
502+
}
503+
}
504+
490505
#[cfg(feature = "accesskit")]
491506
pub fn from_access_selection<B: Brush>(
492507
selection: &accesskit::TextSelection,
@@ -719,6 +734,42 @@ impl Selection {
719734
}
720735
}
721736

737+
/// Returns a new selection with the focus moved to just after the previous hard line break.
738+
///
739+
/// If `extend` is `true` then the current anchor will be retained,
740+
/// otherwise the new selection will be collapsed.
741+
#[must_use]
742+
pub fn hard_line_start<B: Brush>(&self, layout: &Layout<B>, extend: bool) -> Self {
743+
if let Some((mut hard_line_start_index, line)) = self.focus.line(layout) {
744+
let mut result_byte_index = line.text_range().start;
745+
loop {
746+
if hard_line_start_index == 0 {
747+
break;
748+
}
749+
let prev_index = hard_line_start_index - 1;
750+
let Some(line) = layout.get(prev_index) else {
751+
unreachable!(
752+
"{hard_line_start_index} is a valid line in the layout, but {prev_index} isn't, despite the latter being smaller.\n\
753+
The layout has {} lines.",
754+
layout.len()
755+
);
756+
};
757+
if matches!(line.break_reason(), BreakReason::Explicit) {
758+
// The start of the line 'hard_line_start_index' is the target point.
759+
break;
760+
}
761+
result_byte_index = line.text_range().start;
762+
hard_line_start_index = prev_index;
763+
}
764+
self.maybe_extend(
765+
Cursor::from_byte_index(layout, result_byte_index, Affinity::Downstream),
766+
extend,
767+
)
768+
} else {
769+
*self
770+
}
771+
}
772+
722773
/// Returns a new selection with the focus moved to the end of the
723774
/// current line.
724775
///
@@ -740,6 +791,46 @@ impl Selection {
740791
}
741792
}
742793

794+
/// Returns a new selection with the focus moved to just before the next hard line break.
795+
///
796+
/// If `extend` is `true` then the current anchor will be retained,
797+
/// otherwise the new selection will be collapsed.
798+
#[must_use]
799+
pub fn hard_line_end<B: Brush>(&self, layout: &Layout<B>, extend: bool) -> Self {
800+
if let Some((mut hard_line_end_index, line)) = self.focus.line(layout) {
801+
let mut result_byte_index = line.text_range().end;
802+
// If we're already on the last line of the hard line, use that.
803+
if !matches!(line.break_reason(), BreakReason::Explicit) {
804+
// Otherwise, check if any of the following lines are the last line of the hard line.
805+
loop {
806+
let next_index = hard_line_end_index + 1;
807+
if let Some(line) = layout.get(next_index) {
808+
result_byte_index = line.text_range().end;
809+
hard_line_end_index = next_index;
810+
if matches!(line.break_reason(), BreakReason::Explicit) {
811+
// result_byte_index is the last byte of the previous line, so is the value we need
812+
break;
813+
}
814+
} else {
815+
// We hit the end of text. Select to the end of the "final" line, which was not an EOF.
816+
return self.maybe_extend(
817+
Cursor::from_byte_index(layout, result_byte_index, Affinity::Upstream),
818+
extend,
819+
);
820+
}
821+
}
822+
}
823+
824+
// We want to select to "before" the newline character in the hard line, so we have downstream affinity on the boundary before it.
825+
self.maybe_extend(
826+
Cursor::from_byte_index(layout, result_byte_index - 1, Affinity::Downstream),
827+
extend,
828+
)
829+
} else {
830+
*self
831+
}
832+
}
833+
743834
/// Returns a new selection with the focus extended to the given point.
744835
///
745836
/// If the initial selection was created from a word or line, then the new

β€Žparley/src/layout/editor.rsβ€Ž

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,16 @@ where
464464
));
465465
}
466466

467+
/// Move the cursor to just after the previous hard line break (such as `\n`).
468+
pub fn move_to_hard_line_start(&mut self) {
469+
self.refresh_layout();
470+
self.editor.set_selection(
471+
self.editor
472+
.selection
473+
.hard_line_start(&self.editor.layout, false),
474+
);
475+
}
476+
467477
/// Move the cursor to the start of the physical line.
468478
pub fn move_to_line_start(&mut self) {
469479
self.refresh_layout();
@@ -481,6 +491,16 @@ where
481491
));
482492
}
483493

494+
/// Move the cursor to just before the next hard line break (such as `\n`).
495+
pub fn move_to_hard_line_end(&mut self) {
496+
self.refresh_layout();
497+
self.editor.set_selection(
498+
self.editor
499+
.selection
500+
.hard_line_end(&self.editor.layout, false),
501+
);
502+
}
503+
484504
/// Move the cursor to the end of the physical line.
485505
pub fn move_to_line_end(&mut self) {
486506
self.refresh_layout();
@@ -569,6 +589,16 @@ where
569589
));
570590
}
571591

592+
/// Move the selection focus point to just after the previous hard line break (such as `\n`).
593+
pub fn select_to_hard_line_start(&mut self) {
594+
self.refresh_layout();
595+
self.editor.set_selection(
596+
self.editor
597+
.selection
598+
.hard_line_start(&self.editor.layout, true),
599+
);
600+
}
601+
572602
/// Move the selection focus point to the start of the physical line.
573603
pub fn select_to_line_start(&mut self) {
574604
self.refresh_layout();
@@ -586,6 +616,16 @@ where
586616
));
587617
}
588618

619+
/// Move the selection focus point to just before the next hard line break (such as `\n`).
620+
pub fn select_to_hard_line_end(&mut self) {
621+
self.refresh_layout();
622+
self.editor.set_selection(
623+
self.editor
624+
.selection
625+
.hard_line_end(&self.editor.layout, true),
626+
);
627+
}
628+
589629
/// Move the selection focus point to the end of the physical line.
590630
pub fn select_to_line_end(&mut self) {
591631
self.refresh_layout();
@@ -655,12 +695,25 @@ where
655695
}
656696

657697
/// Select the physical line at the point.
698+
///
699+
/// Note that this metehod determines line breaks for any reason, including due to word wrapping.
700+
/// To select the text between explicit newlines, use [`select_hard_line_at_point`](Self::select_hard_line_at_point).
701+
/// In most text editing cases, this is the preferred behaviour.
658702
pub fn select_line_at_point(&mut self, x: f32, y: f32) {
659703
self.refresh_layout();
660704
let line = Selection::line_from_point(&self.editor.layout, x, y);
661705
self.editor.set_selection(line);
662706
}
663707

708+
/// Select the "logical" line at the point.
709+
///
710+
/// The logical line is defined by line break characters, such as `\n`, rather than due to soft-wrapping.
711+
pub fn select_hard_line_at_point(&mut self, x: f32, y: f32) {
712+
self.refresh_layout();
713+
let hard_line = Selection::hard_line_from_point(&self.editor.layout, x, y);
714+
self.editor.set_selection(hard_line);
715+
}
716+
664717
/// Move the selection focus point to the cluster boundary closest to point.
665718
pub fn extend_selection_to_point(&mut self, x: f32, y: f32) {
666719
self.refresh_layout();

β€Žparley/src/layout/mod.rsβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ impl<B: Brush> Layout<B> {
121121
}
122122

123123
/// Returns the line at the specified index.
124+
///
125+
/// Returns `None` if the index is out of bounds, i.e. if it's
126+
/// not less than [`self.len()`](Self::len).
124127
pub fn get(&self, index: usize) -> Option<Line<'_, B>> {
125128
Some(Line {
126129
index: index as u32,

β€Žparley/src/tests/test_editor.rsβ€Ž

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,36 @@ fn editor_select_all() {
3434
env.check_editor_snapshot(&mut editor);
3535
}
3636

37+
#[test]
38+
fn editor_select_hard_line() {
39+
let mut env = TestEnv::new(test_name!(), None);
40+
let mut editor = env.editor("First\nNew Hard Line with soft break!\nLast");
41+
editor.set_width(Some(40.));
42+
env.driver(&mut editor).move_right();
43+
// We can select the first line.
44+
env.driver(&mut editor).select_to_hard_line_end();
45+
env.check_editor_snapshot(&mut editor);
46+
env.driver(&mut editor).move_to_hard_line_start();
47+
env.check_editor_snapshot(&mut editor);
48+
env.driver(&mut editor).move_down();
49+
env.driver(&mut editor).move_to_hard_line_end();
50+
env.check_editor_snapshot(&mut editor);
51+
env.driver(&mut editor).select_to_hard_line_start();
52+
env.check_editor_snapshot(&mut editor);
53+
env.driver(&mut editor).move_right();
54+
// Cursor is logically after the newline; there's not really any great answer here.
55+
env.driver(&mut editor).select_to_hard_line_start();
56+
env.check_editor_snapshot(&mut editor);
57+
58+
// We can select the last line.
59+
env.driver(&mut editor).move_right();
60+
env.driver(&mut editor).move_right();
61+
env.driver(&mut editor).move_to_hard_line_end();
62+
env.check_editor_snapshot(&mut editor);
63+
env.driver(&mut editor).select_to_hard_line_start();
64+
env.check_editor_snapshot(&mut editor);
65+
}
66+
3767
#[test]
3868
fn editor_double_newline() {
3969
let mut env = TestEnv::new(test_name!(), None);
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
Β (0)