use log::warn;
use skia_safe::textlayout::Paragraph;
use super::{EditableText, Selection};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Direction {
Left,
Right,
Upstream,
Downstream,
}
impl Direction {
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),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Movement {
Grapheme(Direction),
Word(Direction),
Line(Direction),
Page(Direction),
Body(Direction),
LineStart,
LineEnd,
Vertical(VerticalMovement),
ParagraphStart,
ParagraphEnd,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VerticalMovement {
LineUp,
LineDown,
PageUp,
PageDown,
DocumentStart,
DocumentEnd,
}
#[derive(Debug, Clone, Copy)]
pub enum WritingDirection {
LeftToRight,
RightToLeft,
Natural,
}
pub fn apply_movement<T: EditableText>(
m: Movement,
s: Selection,
text: &T,
paragraph: &Paragraph,
modify: bool,
) -> Selection {
let writing_direction = WritingDirection::LeftToRight;
let (offset, h_pos) = match m {
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)).unwrap_or((0, s.h_pos))
} else {
(s.min(), None)
}
}
Movement::Grapheme(_) => {
if s.is_caret() || modify {
text.next_grapheme_offset(s.active)
.map(|off| (off, None))
.unwrap_or((s.active, s.h_pos))
} else {
(s.max(), None)
}
}
Movement::Vertical(VerticalMovement::LineUp) => {
let cluster = paragraph.get_glyph_cluster_at(s.active).unwrap();
let glyph_bounds = cluster.bounds;
let line = paragraph.get_line_number_at(s.active).unwrap();
let h_pos = s.h_pos.unwrap_or(glyph_bounds.x());
if line == 0 {
(0, Some(h_pos))
} else {
let lm = paragraph.get_line_metrics_at(line).unwrap();
let up_pos = paragraph
.get_closest_glyph_cluster_at((h_pos, glyph_bounds.y() - lm.height as f32))
.unwrap();
let s = if h_pos < up_pos.bounds.center_x() {
up_pos.text_range.start
} else {
up_pos.text_range.end
};
(s, Some(h_pos))
}
}
Movement::Vertical(VerticalMovement::LineDown) => {
let cluster = paragraph.get_glyph_cluster_at(s.active).unwrap();
let h_pos = s.h_pos.unwrap_or(cluster.bounds.x());
let line = paragraph.get_line_number_at(s.active).unwrap();
if line == paragraph.line_number() - 1 {
(text.len(), Some(h_pos))
} else {
let lm = paragraph.get_line_metrics_at(line).unwrap();
let y_below = lm.baseline - lm.ascent + lm.height + 1.0;
let down_pos =
paragraph.get_closest_glyph_cluster_at((h_pos, y_below as f32)).unwrap();
let s = if h_pos < down_pos.bounds.center_x() {
down_pos.text_range.start
} else {
down_pos.text_range.end
};
(s.min(text.len()), Some(h_pos))
}
}
Movement::Vertical(VerticalMovement::DocumentStart) => (0, None),
Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None),
Movement::ParagraphStart => (text.preceding_line_break(s.active), None),
Movement::ParagraphEnd => (text.next_line_break(s.active), None),
Movement::Line(_) => {
todo!()
}
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::Word(_) => {
let offset = if s.is_caret() || modify {
text.next_word_offset(s.active).unwrap_or(s.active)
} else {
s.max()
};
(offset, None)
}
Movement::Vertical(VerticalMovement::PageDown)
| Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos),
Movement::LineStart => {
let line = paragraph.get_line_number_at(s.active).unwrap();
let lm = paragraph.get_line_metrics_at(line).unwrap();
(lm.start_index, None)
}
Movement::LineEnd => {
let line = paragraph.get_line_number_at(s.active).unwrap();
let lm = paragraph.get_line_metrics_at(line).unwrap();
(lm.end_index - 1, None)
}
other => {
warn!("unhandled movement {:?}", other);
(s.anchor, s.h_pos)
}
};
let start = if modify { s.anchor } else { offset };
Selection::new(start, offset).with_h_pos(h_pos)
}