Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify druid & druid-shell Movement types #1655

Merged
merged 1 commit into from
Mar 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions druid-shell/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,25 @@ pub enum Direction {
Downstream,
}

impl Direction {
/// Returns `true` if this direction is byte-wise backwards for
/// the provided [`WritingDirection`].
///
/// The provided direction *must not be* `WritingDirection::Natural`.
pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool {
assert!(
!matches!(direction, WritingDirection::Natural),
"writing direction must be resolved"
);
match self {
Direction::Upstream => true,
Direction::Downstream => false,
Direction::Left => matches!(direction, WritingDirection::LeftToRight),
Direction::Right => matches!(direction, WritingDirection::RightToLeft),
}
}
}

/// Distinguishes between two visually distinct locations with the same byte
/// index.
///
Expand Down
9 changes: 3 additions & 6 deletions druid/src/text/input_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,14 +564,12 @@ impl<T: TextStorage + EditableText> EditSession<T> {
fn do_action(&mut self, buffer: &mut T, action: ImeAction) {
match action {
ImeAction::Move(movement) => {
let sel =
crate::text::movement(movement.into(), self.selection, &self.layout, false);
let sel = crate::text::movement(movement, self.selection, &self.layout, false);
self.external_selection_change = Some(sel);
self.scroll_to_selection_end(false);
}
ImeAction::MoveSelecting(movement) => {
let sel =
crate::text::movement(movement.into(), self.selection, &self.layout, true);
let sel = crate::text::movement(movement, self.selection, &self.layout, true);
self.external_selection_change = Some(sel);
self.scroll_to_selection_end(false);
}
Expand All @@ -583,8 +581,7 @@ impl<T: TextStorage + EditableText> EditSession<T> {
//tracing::warn!("Line/Word selection actions are not implemented");
//}
ImeAction::Delete(movement) if self.selection.is_caret() => {
let movement: Movement = movement.into();
if movement == Movement::Left {
if movement == Movement::Grapheme(druid_shell::text::Direction::Upstream) {
self.backspace(buffer);
} else {
let to_delete =
Expand Down
31 changes: 21 additions & 10 deletions druid/src/text/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub struct TextLayout<T> {
wrap_width: f64,
alignment: TextAlignment,
links: Rc<[(Rect, usize)]>,
text_is_rtl: bool,
}

/// Metrics describing the layout text.
Expand Down Expand Up @@ -87,16 +88,7 @@ impl<T> TextLayout<T> {
wrap_width: f64::INFINITY,
alignment: Default::default(),
links: Rc::new([]),
}
}

/// Create a new `TextLayout` with the provided text.
///
/// This is useful when the text is not died to application data.
pub fn from_text(text: impl Into<T>) -> Self {
TextLayout {
text: Some(text.into()),
..TextLayout::new()
text_is_rtl: false,
}
}

Expand Down Expand Up @@ -162,9 +154,27 @@ impl<T> TextLayout<T> {
self.layout = None;
}
}

/// Returns `true` if this layout's text appears to be right-to-left.
///
/// See [`piet::util::first_strong_rtl`] for more information.
///
/// [`piet::util::first_strong_rtl`]: crate::piet::util::first_strong_rtl
pub fn text_is_rtl(&self) -> bool {
self.text_is_rtl
}
}

impl<T: TextStorage> TextLayout<T> {
/// Create a new `TextLayout` with the provided text.
///
/// This is useful when the text is not tied to application data.
pub fn from_text(text: impl Into<T>) -> Self {
let mut this = TextLayout::new();
this.set_text(text.into());
this
}

/// Returns `true` if this layout needs to be rebuilt.
///
/// This happens (for instance) after style attributes are modified.
Expand All @@ -178,6 +188,7 @@ impl<T: TextStorage> TextLayout<T> {
/// Set the text to display.
pub fn set_text(&mut self, text: T) {
if self.text.is_none() || !self.text.as_ref().unwrap().same(&text) {
self.text_is_rtl = crate::piet::util::first_strong_rtl(text.as_str());
self.text = Some(text);
self.layout = None;
}
Expand Down
100 changes: 36 additions & 64 deletions druid/src/text/movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,9 @@

use crate::kurbo::Point;
use crate::piet::TextLayout as _;
pub use crate::shell::text::{Direction, Movement, VerticalMovement, WritingDirection};
use crate::text::{EditableText, Selection, TextLayout, TextStorage};

/// The specification of a movement.
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Movement {
/// Move to the left by one grapheme cluster.
Left,
/// Move to the right by one grapheme cluster.
Right,
/// Move up one visible line.
Up,
/// Move down one visible line.
Down,
/// Move to the left by one word.
LeftWord,
/// Move to the right by one word.
RightWord,
/// Move to left end of visible line.
PrecedingLineBreak,
/// Move to right end of visible line.
NextLineBreak,
/// Move to the beginning of the document
StartOfDocument,
/// Move to the end of the document
EndOfDocument,
}

//FIXME: we should remove this whole file, and use the Movement type defined in druid-shell?
impl From<crate::shell::text::Movement> for Movement {
fn from(src: crate::shell::text::Movement) -> Movement {
use crate::shell::text::{Direction, Movement as SMovemement, VerticalMovement};
match src {
SMovemement::Grapheme(Direction::Left) | SMovemement::Grapheme(Direction::Upstream) => {
Movement::Left
}
SMovemement::Grapheme(_) => Movement::Right,
SMovemement::Word(Direction::Left) => Movement::LeftWord,
SMovemement::Word(_) => Movement::RightWord,
SMovemement::Line(Direction::Left) | SMovemement::ParagraphStart => {
Movement::PrecedingLineBreak
}
SMovemement::Line(_) | SMovemement::ParagraphEnd => Movement::NextLineBreak,
SMovemement::Vertical(VerticalMovement::LineUp)
| SMovemement::Vertical(VerticalMovement::PageUp) => Movement::Up,
SMovemement::Vertical(VerticalMovement::LineDown)
| SMovemement::Vertical(VerticalMovement::PageDown) => Movement::Down,
SMovemement::Vertical(VerticalMovement::DocumentStart) => Movement::StartOfDocument,
SMovemement::Vertical(VerticalMovement::DocumentEnd) => Movement::EndOfDocument,
// the source enum is non_exhaustive
_ => panic!("unhandled movement {:?}", src),
}
}
}

/// Compute the result of movement on a selection.
///
/// returns a new selection representing the state after the movement.
Expand All @@ -91,8 +40,14 @@ pub fn movement<T: EditableText + TextStorage>(
}
};

let writing_direction = if crate::piet::util::first_strong_rtl(text.as_str()) {
WritingDirection::RightToLeft
} else {
WritingDirection::LeftToRight
};

let (offset, h_pos) = match m {
Movement::Left => {
Movement::Grapheme(d) if d.is_upstream_for_direction(writing_direction) => {
if s.is_caret() || modify {
text.prev_grapheme_offset(s.active)
.map(|off| (off, None))
Expand All @@ -101,7 +56,7 @@ pub fn movement<T: EditableText + TextStorage>(
(s.min(), None)
}
}
Movement::Right => {
Movement::Grapheme(_) => {
if s.is_caret() || modify {
text.next_grapheme_offset(s.active)
.map(|off| (off, None))
Expand All @@ -110,8 +65,7 @@ pub fn movement<T: EditableText + TextStorage>(
(s.max(), None)
}
}

Movement::Up => {
Movement::Vertical(VerticalMovement::LineUp) => {
let cur_pos = layout.hit_test_text_position(s.active);
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
if cur_pos.line == 0 {
Expand All @@ -123,7 +77,7 @@ pub fn movement<T: EditableText + TextStorage>(
(up_pos.idx, Some(point_above.x))
}
}
Movement::Down => {
Movement::Vertical(VerticalMovement::LineDown) => {
let cur_pos = layout.hit_test_text_position(s.active);
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
if cur_pos.line == layout.line_count() - 1 {
Expand All @@ -137,29 +91,47 @@ pub fn movement<T: EditableText + TextStorage>(
(up_pos.idx, Some(point_below.x))
}
}
Movement::Vertical(VerticalMovement::DocumentStart) => (0, None),
Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None),

Movement::PrecedingLineBreak => (text.preceding_line_break(s.active), None),
Movement::NextLineBreak => (text.next_line_break(s.active), None),

Movement::StartOfDocument => (0, None),
Movement::EndOfDocument => (text.len(), None),
Movement::ParagraphStart => (text.preceding_line_break(s.active), None),
Movement::ParagraphEnd => (text.next_line_break(s.active), None),

Movement::LeftWord => {
Movement::Line(d) => {
let hit = layout.hit_test_text_position(s.active);
let lm = layout.line_metric(hit.line).unwrap();
let offset = if d.is_upstream_for_direction(writing_direction) {
lm.start_offset
} else {
lm.end_offset
};
(offset, None)
}
Movement::Word(d) if d.is_upstream_for_direction(writing_direction) => {
let offset = if s.is_caret() || modify {
text.prev_word_offset(s.active).unwrap_or(0)
} else {
s.min()
};
(offset, None)
}
Movement::RightWord => {
Movement::Word(_) => {
let offset = if s.is_caret() || modify {
text.next_word_offset(s.active).unwrap_or(s.active)
} else {
s.max()
};
(offset, None)
}

// These two are not handled; they require knowledge of the size
// of the viewport.
Movement::Vertical(VerticalMovement::PageDown)
| Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos),
other => {
tracing::warn!("unhandled movement {:?}", other);
(s.anchor, s.h_pos)
}
};

let start = if modify { s.anchor } else { offset };
Expand Down