Skip to content

Commit ac936c2

Browse files
committed
Implement vertical movement (for text editing)
1 parent 694af59 commit ac936c2

File tree

6 files changed

+119
-22
lines changed

6 files changed

+119
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ You can find its changes [documented below](#060---2020-06-01).
4646
- 'Tabs' widget allowing static and dynamic tabbed layouts. ([#1160] by [@rjwittams])
4747
- `RichText` and `Attribute` types for creating rich text ([#1255] by [@cmyr])
4848
- `request_timer` can now be called from `LayoutCtx` ([#1278] by [@Majora320])
49+
- TextBox supports vertical movement ([#1280] by [@cmyr])
4950

5051
### Changed
5152

@@ -490,6 +491,7 @@ Last release without a changelog :(
490491
[#1255]: https://github.com/linebender/druid/pull/1255
491492
[#1276]: https://github.com/linebender/druid/pull/1276
492493
[#1278]: https://github.com/linebender/druid/pull/1278
494+
[#1280]: https://github.com/linebender/druid/pull/1280
493495

494496
[Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master
495497
[0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0

druid/src/text/editor.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,18 @@ impl<T: TextStorage + EditableText> Editor<T> {
173173
EditAction::Delete => self.delete_forward(data),
174174
EditAction::JumpDelete(mvmt) | EditAction::JumpBackspace(mvmt) => {
175175
let to_delete = if self.selection.is_caret() {
176-
movement(mvmt, self.selection, data, true)
176+
movement(mvmt, self.selection, &self.layout, true)
177177
} else {
178178
self.selection
179179
};
180180
data.edit(to_delete.range(), "");
181181
self.selection = Selection::caret(to_delete.min());
182182
}
183-
EditAction::Move(mvmt) => self.selection = movement(mvmt, self.selection, data, false),
183+
EditAction::Move(mvmt) => {
184+
self.selection = movement(mvmt, self.selection, &self.layout, false)
185+
}
184186
EditAction::ModifySelection(mvmt) => {
185-
self.selection = movement(mvmt, self.selection, data, true)
187+
self.selection = movement(mvmt, self.selection, &self.layout, true)
186188
}
187189
EditAction::Click(action) => {
188190
if action.mods.shift() {
@@ -240,7 +242,7 @@ impl<T: TextStorage + EditableText> Editor<T> {
240242

241243
fn delete_forward(&mut self, data: &mut T) {
242244
let to_delete = if self.selection.is_caret() {
243-
movement(Movement::Right, self.selection, data, true)
245+
movement(Movement::Right, self.selection, &self.layout, true)
244246
} else {
245247
self.selection
246248
};

druid/src/text/layout.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ impl<T: TextStorage> TextLayout<T> {
174174
self.text.as_ref()
175175
}
176176

177+
/// Returns the inner Piet [`TextLayout`] type.
178+
///
179+
/// [`TextLayout`]: ./piet/trait.TextLayout.html
180+
pub fn layout(&self) -> Option<&PietTextLayout> {
181+
self.layout.as_ref()
182+
}
183+
177184
/// The size of the laid-out text.
178185
///
179186
/// This is not meaningful until [`rebuild_if_needed`] has been called.

druid/src/text/movement.rs

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414

1515
//! Text editing movements.
1616
17-
use crate::text::{EditableText, Selection};
17+
use crate::kurbo::Point;
18+
use crate::piet::TextLayout as _;
19+
use crate::text::{EditableText, Selection, TextLayout, TextStorage};
1820

1921
/// The specification of a movement.
2022
#[derive(Debug, PartialEq, Clone, Copy)]
@@ -23,6 +25,10 @@ pub enum Movement {
2325
Left,
2426
/// Move to the right by one grapheme cluster.
2527
Right,
28+
/// Move up one visible line.
29+
Up,
30+
/// Move down one visible line.
31+
Down,
2632
/// Move to the left by one word.
2733
LeftWord,
2834
/// Move to the right by one word.
@@ -37,44 +43,98 @@ pub enum Movement {
3743
EndOfDocument,
3844
}
3945

40-
/// Compute the result of movement on a selection .
41-
pub fn movement(m: Movement, s: Selection, text: &impl EditableText, modify: bool) -> Selection {
42-
let offset = match m {
46+
/// Compute the result of movement on a selection.
47+
///
48+
/// returns a new selection representing the state after the movement.
49+
///
50+
/// If `modify` is true, only the 'active' edge (the `end`) of the selection
51+
/// should be changed; this is the case when the user moves with the shift
52+
/// key pressed.
53+
pub fn movement<T: EditableText + TextStorage>(
54+
m: Movement,
55+
s: Selection,
56+
layout: &TextLayout<T>,
57+
modify: bool,
58+
) -> Selection {
59+
let (text, layout) = match (layout.text(), layout.layout()) {
60+
(Some(text), Some(layout)) => (text, layout),
61+
_ => {
62+
debug_assert!(false, "movement() called before layout rebuild");
63+
return s;
64+
}
65+
};
66+
67+
let (offset, h_pos) = match m {
4368
Movement::Left => {
4469
if s.is_caret() || modify {
45-
text.prev_grapheme_offset(s.end).unwrap_or(0)
70+
text.prev_grapheme_offset(s.end)
71+
.map(|off| (off, None))
72+
.unwrap_or((0, s.h_pos))
4673
} else {
47-
s.min()
74+
(s.min(), None)
4875
}
4976
}
5077
Movement::Right => {
5178
if s.is_caret() || modify {
52-
text.next_grapheme_offset(s.end).unwrap_or(s.end)
79+
text.next_grapheme_offset(s.end)
80+
.map(|off| (off, None))
81+
.unwrap_or((s.end, s.h_pos))
5382
} else {
54-
s.max()
83+
(s.max(), None)
5584
}
5685
}
5786

58-
Movement::PrecedingLineBreak => text.preceding_line_break(s.end),
59-
Movement::NextLineBreak => text.next_line_break(s.end),
87+
Movement::Up => {
88+
let cur_pos = layout.hit_test_text_position(s.end);
89+
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
90+
if cur_pos.line == 0 {
91+
(0, Some(h_pos))
92+
} else {
93+
let lm = layout.line_metric(cur_pos.line).unwrap();
94+
let point_above = Point::new(h_pos, cur_pos.point.y - lm.height);
95+
let up_pos = layout.hit_test_point(point_above);
96+
(up_pos.idx, Some(point_above.x))
97+
}
98+
}
99+
Movement::Down => {
100+
let cur_pos = layout.hit_test_text_position(s.end);
101+
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
102+
if cur_pos.line == layout.line_count() - 1 {
103+
(text.len(), Some(h_pos))
104+
} else {
105+
let lm = layout.line_metric(cur_pos.line).unwrap();
106+
// may not work correctly for point sizes below 1.0
107+
let y_below = lm.y_offset + lm.height + 1.0;
108+
let point_below = Point::new(h_pos, y_below);
109+
let up_pos = layout.hit_test_point(point_below);
110+
(up_pos.idx, Some(point_below.x))
111+
}
112+
}
60113

61-
Movement::StartOfDocument => 0,
62-
Movement::EndOfDocument => text.len(),
114+
Movement::PrecedingLineBreak => (text.preceding_line_break(s.end), None),
115+
Movement::NextLineBreak => (text.next_line_break(s.end), None),
116+
117+
Movement::StartOfDocument => (0, None),
118+
Movement::EndOfDocument => (text.len(), None),
63119

64120
Movement::LeftWord => {
65-
if s.is_caret() || modify {
121+
let offset = if s.is_caret() || modify {
66122
text.prev_word_offset(s.end).unwrap_or(0)
67123
} else {
68124
s.min()
69-
}
125+
};
126+
(offset, None)
70127
}
71128
Movement::RightWord => {
72-
if s.is_caret() || modify {
129+
let offset = if s.is_caret() || modify {
73130
text.next_word_offset(s.end).unwrap_or(s.end)
74131
} else {
75132
s.max()
76-
}
133+
};
134+
(offset, None)
77135
}
78136
};
79-
Selection::new(if modify { s.start } else { offset }, offset)
137+
138+
let start = if modify { s.start } else { offset };
139+
Selection::new(start, offset).with_h_pos(h_pos)
80140
}

druid/src/text/selection.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,20 @@ pub struct Selection {
2828

2929
/// The active edge of a selection, as a byte offset.
3030
pub end: usize,
31+
32+
/// The saved horizontal position, during vertical movement.
33+
pub h_pos: Option<f64>,
3134
}
3235

3336
impl Selection {
3437
/// Create a selection that begins at start and goes to end.
3538
/// Like dragging a mouse from start to end.
3639
pub fn new(start: usize, end: usize) -> Self {
37-
Selection { start, end }
40+
Selection {
41+
start,
42+
end,
43+
h_pos: None,
44+
}
3845
}
3946

4047
/// Create a selection that starts at the beginning and ends at text length.
@@ -49,9 +56,16 @@ impl Selection {
4956
Selection {
5057
start: pos,
5158
end: pos,
59+
h_pos: None,
5260
}
5361
}
5462

63+
/// Construct a new selection from this selection, with the provided h_pos.
64+
pub fn with_h_pos(mut self, h_pos: Option<f64>) -> Self {
65+
self.h_pos = h_pos;
66+
self
67+
}
68+
5569
/// If start == end, it's a caret
5670
pub fn is_caret(self) -> bool {
5771
self.start == self.end

druid/src/text/text_input.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ impl TextInput for BasicTextInput {
112112
k_e if (HotKey::new(None, KbKey::ArrowRight)).matches(k_e) => {
113113
EditAction::Move(Movement::Right)
114114
}
115+
k_e if (HotKey::new(None, KbKey::ArrowUp)).matches(k_e) => {
116+
EditAction::Move(Movement::Up)
117+
}
118+
k_e if (HotKey::new(None, KbKey::ArrowDown)).matches(k_e) => {
119+
EditAction::Move(Movement::Down)
120+
}
121+
k_e if (HotKey::new(SysMods::Shift, KbKey::ArrowUp)).matches(k_e) => {
122+
EditAction::ModifySelection(Movement::Up)
123+
}
124+
k_e if (HotKey::new(SysMods::Shift, KbKey::ArrowDown)).matches(k_e) => {
125+
EditAction::ModifySelection(Movement::Down)
126+
}
115127
// Delete left word
116128
k_e if (HotKey::new(SysMods::Cmd, KbKey::Backspace)).matches(k_e) => {
117129
EditAction::JumpBackspace(Movement::LeftWord)

0 commit comments

Comments
 (0)