use std::rc::Rc;
use inset_embedder::{TextPosition, TextRange};
use inset_foundation::App;
use inset_painting::InlineSpanRef;
use inset_services::{TextBoundary, UntilPredicate};
use unicode_general_category::{GeneralCategory, get_general_category};
use crate::editable::RenderEditable;
use crate::object::RenderHandle;
#[derive(Clone, Copy, Debug)]
pub struct LineBoundary {
text_layout: RenderHandle<RenderEditable>,
}
impl LineBoundary {
pub fn new(text_layout: RenderHandle<RenderEditable>) -> LineBoundary {
LineBoundary { text_layout }
}
}
impl TextBoundary for LineBoundary {
fn get_text_boundary_at(&self, app: &mut App, position: i32) -> TextRange {
self.text_layout
.get_line_at_offset(app, TextPosition::new(position.max(0)))
.range()
}
}
#[derive(Clone)]
pub struct WordBoundary {
text: InlineSpanRef,
paragraph: RenderHandle<RenderEditable>,
}
impl WordBoundary {
pub(crate) fn new(
text: InlineSpanRef,
paragraph: RenderHandle<RenderEditable>,
) -> WordBoundary {
WordBoundary { text, paragraph }
}
pub fn move_by_word_boundary(&self) -> Box<dyn TextBoundary> {
let text = Rc::clone(&self.text);
Box::new(UntilTextBoundary {
text_boundary: Box::new(self.clone()),
predicate: Rc::new(move |offset, forward| {
skip_spaces_and_punctuations(&text, offset, forward)
}),
})
}
}
impl TextBoundary for WordBoundary {
fn get_text_boundary_at(&self, app: &mut App, position: i32) -> TextRange {
self.paragraph
.get_word_boundary(app, TextPosition::new(position.max(0)))
}
}
fn code_point_from_surrogates(high_surrogate: i32, low_surrogate: i32) -> i32 {
const BASE: i32 = 0x010000 - (0xD800 << 10) - 0xDC00;
(high_surrogate << 10) + low_surrogate + BASE
}
fn code_point_at(text: &InlineSpanRef, index: i32) -> Option<i32> {
let code_unit_at_index = i32::from(text.clone().code_unit_at(index)?);
Some(match code_unit_at_index & 0xFC00 {
0xD800 => code_point_from_surrogates(
code_unit_at_index,
i32::from(text.clone().code_unit_at(index + 1)?),
),
0xDC00 => code_point_from_surrogates(
i32::from(text.clone().code_unit_at(index - 1)?),
code_unit_at_index,
),
_ => code_unit_at_index,
})
}
fn is_newline(code_point: i32) -> bool {
matches!(
code_point,
0x000A | 0x0085 | 0x000B | 0x000C | 0x2028 | 0x2029 )
}
fn is_space_separator_or_punctuation(code_point: i32) -> bool {
let Some(character) = u32::try_from(code_point).ok().and_then(char::from_u32) else {
return false;
};
matches!(
get_general_category(character),
GeneralCategory::SpaceSeparator
| GeneralCategory::ConnectorPunctuation
| GeneralCategory::DashPunctuation
| GeneralCategory::OpenPunctuation
| GeneralCategory::ClosePunctuation
| GeneralCategory::InitialPunctuation
| GeneralCategory::FinalPunctuation
| GeneralCategory::OtherPunctuation
)
}
fn skip_spaces_and_punctuations(text: &InlineSpanRef, offset: i32, forward: bool) -> bool {
let inner_code_point = code_point_at(text, if forward { offset - 1 } else { offset });
let outer_code_unit = text
.clone()
.code_unit_at(if forward { offset } else { offset - 1 });
let Some(inner_code_point) = inner_code_point else {
return true;
};
let Some(outer_code_unit) = outer_code_unit else {
return true;
};
let hard_break_rules_apply =
is_newline(inner_code_point) || is_newline(i32::from(outer_code_unit));
hard_break_rules_apply || !is_space_separator_or_punctuation(inner_code_point)
}
struct UntilTextBoundary {
text_boundary: Box<dyn TextBoundary>,
predicate: Rc<UntilPredicate>,
}
impl TextBoundary for UntilTextBoundary {
fn get_leading_text_boundary_at(&self, app: &mut App, position: i32) -> Option<i32> {
if position < 0 {
return None;
}
let offset = self
.text_boundary
.get_leading_text_boundary_at(app, position)?;
if (self.predicate)(offset, false) {
Some(offset)
} else {
self.get_leading_text_boundary_at(app, offset - 1)
}
}
fn get_trailing_text_boundary_at(&self, app: &mut App, position: i32) -> Option<i32> {
let offset = self
.text_boundary
.get_trailing_text_boundary_at(app, position.max(0))?;
if (self.predicate)(offset, true) {
Some(offset)
} else {
self.get_trailing_text_boundary_at(app, offset)
}
}
}