use std::{any::Any, borrow::Cow, fmt, ops, sync::Arc};
use parking_lot::Mutex;
use zng_ext_font::*;
use zng_ext_l10n::l10n;
use zng_ext_undo::*;
use zng_wgt::prelude::*;
use super::{node::TEXT, *};
command! {
pub static EDIT_CMD;
pub static SELECT_CMD;
pub static SELECT_ALL_CMD = {
l10n!: true,
name: "Select All",
shortcut: shortcut!(CTRL+'A'),
shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
};
pub static PARSE_CMD;
}
struct SharedTextEditOp {
data: Box<dyn Any + Send>,
op: Box<dyn FnMut(&mut dyn Any, UndoFullOp) + Send>,
}
#[derive(Clone)]
pub struct TextEditOp(Arc<Mutex<SharedTextEditOp>>);
impl fmt::Debug for TextEditOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TextEditOp").finish_non_exhaustive()
}
}
impl TextEditOp {
pub fn new<D>(data: D, mut op: impl FnMut(&mut D, UndoFullOp) + Send + 'static) -> Self
where
D: Send + Any + 'static,
{
Self(Arc::new(Mutex::new(SharedTextEditOp {
data: Box::new(data),
op: Box::new(move |data, o| op(data.downcast_mut().unwrap(), o)),
})))
}
pub fn insert(insert: impl Into<Txt>) -> Self {
struct InsertData {
insert: Txt,
selection_state: SelectionState,
removed: Txt,
}
let data = InsertData {
insert: insert.into(),
selection_state: SelectionState::PreInit,
removed: Txt::from_static(""),
};
Self::new(data, move |data, op| match op {
UndoFullOp::Init { redo } => {
let ctx = TEXT.resolved();
let caret = &ctx.caret;
let mut rmv_range = 0..0;
if let Some(range) = caret.selection_range() {
rmv_range = range.start.index..range.end.index;
ctx.txt.with(|t| {
let r = &t[rmv_range.clone()];
if r != data.removed {
data.removed = Txt::from_str(r);
}
});
if range.start.index == caret.index.unwrap_or(CaretIndex::ZERO).index {
data.selection_state = SelectionState::CaretSelection(range.start, range.end);
} else {
data.selection_state = SelectionState::SelectionCaret(range.start, range.end);
}
} else {
data.selection_state = SelectionState::Caret(caret.index.unwrap_or(CaretIndex::ZERO));
}
Self::apply_max_count(redo, &ctx.txt, rmv_range, &mut data.insert)
}
UndoFullOp::Op(UndoOp::Redo) => {
let insert = &data.insert;
match data.selection_state {
SelectionState::PreInit => unreachable!(),
SelectionState::Caret(insert_idx) => {
let i = insert_idx.index;
TEXT.resolved()
.txt
.modify(clmv!(insert, |args| {
args.to_mut().to_mut().insert_str(i, insert.as_str());
}))
.unwrap();
let mut i = insert_idx;
i.index += insert.len();
let mut caret = TEXT.resolve_caret();
caret.set_index(i);
caret.clear_selection();
}
SelectionState::CaretSelection(start, end) | SelectionState::SelectionCaret(start, end) => {
let char_range = start.index..end.index;
TEXT.resolved()
.txt
.modify(clmv!(insert, |args| {
args.to_mut().to_mut().replace_range(char_range, insert.as_str());
}))
.unwrap();
let mut caret = TEXT.resolve_caret();
caret.set_char_index(start.index + insert.len());
caret.clear_selection();
}
}
}
UndoFullOp::Op(UndoOp::Undo) => {
let len = data.insert.len();
let (insert_idx, selection_idx, caret_idx) = match data.selection_state {
SelectionState::Caret(c) => (c, None, c),
SelectionState::CaretSelection(start, end) => (start, Some(end), start),
SelectionState::SelectionCaret(start, end) => (start, Some(start), end),
SelectionState::PreInit => unreachable!(),
};
let i = insert_idx.index;
let removed = &data.removed;
TEXT.resolved()
.txt
.modify(clmv!(removed, |args| {
args.to_mut().to_mut().replace_range(i..i + len, removed.as_str());
}))
.unwrap();
let mut caret = TEXT.resolve_caret();
caret.set_index(caret_idx);
caret.selection_index = selection_idx;
}
UndoFullOp::Info { info } => {
let mut label = Txt::from_static("\"");
for (i, mut c) in data.insert.chars().take(21).enumerate() {
if i == 20 {
c = '…';
} else if c == '\n' {
c = '↵';
} else if c == '\t' {
c = '→';
} else if c == '\r' {
continue;
}
label.push(c);
}
label.push('"');
*info = Some(Arc::new(label));
}
UndoFullOp::Merge {
next_data,
within_undo_interval,
merged,
..
} => {
if within_undo_interval {
if let Some(next_data) = next_data.downcast_mut::<InsertData>() {
if let (SelectionState::Caret(mut after_idx), SelectionState::Caret(caret)) =
(data.selection_state, next_data.selection_state)
{
after_idx.index += data.insert.len();
if after_idx.index == caret.index {
data.insert.push_str(&next_data.insert);
*merged = true;
}
}
}
}
}
})
}
pub fn backspace() -> Self {
Self::backspace_impl(SegmentedText::backspace_range)
}
pub fn backspace_word() -> Self {
Self::backspace_impl(SegmentedText::backspace_word_range)
}
fn backspace_impl(backspace_range: fn(&SegmentedText, usize, u32) -> std::ops::Range<usize>) -> Self {
struct BackspaceData {
selection_state: SelectionState,
count: u32,
removed: Txt,
}
let data = BackspaceData {
selection_state: SelectionState::PreInit,
count: 1,
removed: Txt::from_static(""),
};
Self::new(data, move |data, op| match op {
UndoFullOp::Init { .. } => {
let ctx = TEXT.resolved();
let caret = &ctx.caret;
if let Some(range) = caret.selection_range() {
if range.start.index == caret.index.unwrap_or(CaretIndex::ZERO).index {
data.selection_state = SelectionState::CaretSelection(range.start, range.end);
} else {
data.selection_state = SelectionState::SelectionCaret(range.start, range.end);
}
} else {
data.selection_state = SelectionState::Caret(caret.index.unwrap_or(CaretIndex::ZERO));
}
}
UndoFullOp::Op(UndoOp::Redo) => {
let rmv = match data.selection_state {
SelectionState::Caret(c) => backspace_range(&TEXT.resolved().segmented_text, c.index, data.count),
SelectionState::CaretSelection(s, e) | SelectionState::SelectionCaret(s, e) => s.index..e.index,
SelectionState::PreInit => unreachable!(),
};
if rmv.is_empty() {
data.removed = Txt::from_static("");
return;
}
{
let mut caret = TEXT.resolve_caret();
caret.set_char_index(rmv.start);
caret.clear_selection();
}
let ctx = TEXT.resolved();
ctx.txt.with(|t| {
let r = &t[rmv.clone()];
if r != data.removed {
data.removed = Txt::from_str(r);
}
});
ctx.txt
.modify(move |args| {
args.to_mut().to_mut().replace_range(rmv, "");
})
.unwrap();
}
UndoFullOp::Op(UndoOp::Undo) => {
if data.removed.is_empty() {
return;
}
let (insert_idx, selection_idx, caret_idx) = match data.selection_state {
SelectionState::Caret(c) => (c.index - data.removed.len(), None, c),
SelectionState::CaretSelection(s, e) => (s.index, Some(e), s),
SelectionState::SelectionCaret(s, e) => (s.index, Some(s), e),
SelectionState::PreInit => unreachable!(),
};
let removed = &data.removed;
TEXT.resolved()
.txt
.modify(clmv!(removed, |args| {
args.to_mut().to_mut().insert_str(insert_idx, removed.as_str());
}))
.unwrap();
let mut caret = TEXT.resolve_caret();
caret.set_index(caret_idx);
caret.selection_index = selection_idx;
}
UndoFullOp::Info { info } => {
*info = Some(if data.count == 1 {
Arc::new("⌫")
} else {
Arc::new(formatx!("⌫ (x{})", data.count))
})
}
UndoFullOp::Merge {
next_data,
within_undo_interval,
merged,
..
} => {
if within_undo_interval {
if let Some(next_data) = next_data.downcast_mut::<BackspaceData>() {
if let (SelectionState::Caret(mut after_idx), SelectionState::Caret(caret)) =
(data.selection_state, next_data.selection_state)
{
after_idx.index -= data.removed.len();
if after_idx.index == caret.index {
data.count += next_data.count;
next_data.removed.push_str(&data.removed);
data.removed = std::mem::take(&mut next_data.removed);
*merged = true;
}
}
}
}
}
})
}
pub fn delete() -> Self {
Self::delete_impl(SegmentedText::delete_range)
}
pub fn delete_word() -> Self {
Self::delete_impl(SegmentedText::delete_word_range)
}
fn delete_impl(delete_range: fn(&SegmentedText, usize, u32) -> std::ops::Range<usize>) -> Self {
struct DeleteData {
selection_state: SelectionState,
count: u32,
removed: Txt,
}
let data = DeleteData {
selection_state: SelectionState::PreInit,
count: 1,
removed: Txt::from_static(""),
};
Self::new(data, move |data, op| match op {
UndoFullOp::Init { .. } => {
let ctx = TEXT.resolved();
let caret = &ctx.caret;
if let Some(range) = caret.selection_range() {
if range.start.index == caret.index.unwrap_or(CaretIndex::ZERO).index {
data.selection_state = SelectionState::CaretSelection(range.start, range.end);
} else {
data.selection_state = SelectionState::SelectionCaret(range.start, range.end);
}
} else {
data.selection_state = SelectionState::Caret(caret.index.unwrap_or(CaretIndex::ZERO));
}
}
UndoFullOp::Op(UndoOp::Redo) => {
let rmv = match data.selection_state {
SelectionState::CaretSelection(s, e) | SelectionState::SelectionCaret(s, e) => s.index..e.index,
SelectionState::Caret(c) => delete_range(&TEXT.resolved().segmented_text, c.index, data.count),
SelectionState::PreInit => unreachable!(),
};
if rmv.is_empty() {
data.removed = Txt::from_static("");
return;
}
{
let mut caret = TEXT.resolve_caret();
caret.set_char_index(rmv.start); caret.clear_selection();
}
let ctx = TEXT.resolved();
ctx.txt.with(|t| {
let r = &t[rmv.clone()];
if r != data.removed {
data.removed = Txt::from_str(r);
}
});
ctx.txt
.modify(move |args| {
args.to_mut().to_mut().replace_range(rmv, "");
})
.unwrap();
}
UndoFullOp::Op(UndoOp::Undo) => {
let removed = &data.removed;
if data.removed.is_empty() {
return;
}
let (insert_idx, selection_idx, caret_idx) = match data.selection_state {
SelectionState::Caret(c) => (c.index, None, c),
SelectionState::CaretSelection(s, e) => (s.index, Some(e), s),
SelectionState::SelectionCaret(s, e) => (s.index, Some(s), e),
SelectionState::PreInit => unreachable!(),
};
TEXT.resolved()
.txt
.modify(clmv!(removed, |args| {
args.to_mut().to_mut().insert_str(insert_idx, removed.as_str());
}))
.unwrap();
let mut caret = TEXT.resolve_caret();
caret.set_index(caret_idx); caret.selection_index = selection_idx;
}
UndoFullOp::Info { info } => {
*info = Some(if data.count == 1 {
Arc::new("⌦")
} else {
Arc::new(formatx!("⌦ (x{})", data.count))
})
}
UndoFullOp::Merge {
next_data,
within_undo_interval,
merged,
..
} => {
if within_undo_interval {
if let Some(next_data) = next_data.downcast_ref::<DeleteData>() {
if let (SelectionState::Caret(after_idx), SelectionState::Caret(caret)) =
(data.selection_state, next_data.selection_state)
{
if after_idx.index == caret.index {
data.count += next_data.count;
data.removed.push_str(&next_data.removed);
*merged = true;
}
}
}
}
}
})
}
fn apply_max_count(redo: &mut bool, txt: &BoxedVar<Txt>, rmv_range: ops::Range<usize>, insert: &mut Txt) {
let max_count = MAX_CHARS_COUNT_VAR.get();
if max_count > 0 {
let (txt_count, rmv_count) = txt.with(|t| (t.chars().count(), t[rmv_range].chars().count()));
let ins_count = insert.chars().count();
let final_count = txt_count - rmv_count + ins_count;
if final_count > max_count {
let ins_rmv = final_count - max_count;
if ins_rmv < ins_count {
let i = insert.char_indices().nth(ins_count - ins_rmv).unwrap().0;
insert.truncate(i);
} else {
debug_assert!(txt_count >= max_count);
*redo = false;
}
}
}
}
pub fn clear() -> Self {
#[derive(Default, Clone)]
struct Cleared {
txt: Txt,
selection: SelectionState,
}
Self::new(Cleared::default(), |data, op| match op {
UndoFullOp::Init { .. } => {
let ctx = TEXT.resolved();
data.txt = ctx.txt.get();
if let Some(range) = ctx.caret.selection_range() {
if range.start.index == ctx.caret.index.unwrap_or(CaretIndex::ZERO).index {
data.selection = SelectionState::CaretSelection(range.start, range.end);
} else {
data.selection = SelectionState::SelectionCaret(range.start, range.end);
}
} else {
data.selection = SelectionState::Caret(ctx.caret.index.unwrap_or(CaretIndex::ZERO));
};
}
UndoFullOp::Op(UndoOp::Redo) => {
let _ = TEXT.resolved().txt.set("");
}
UndoFullOp::Op(UndoOp::Undo) => {
let _ = TEXT.resolved().txt.set(data.txt.clone());
let (selection_idx, caret_idx) = match data.selection {
SelectionState::Caret(c) => (None, c),
SelectionState::CaretSelection(s, e) => (Some(e), s),
SelectionState::SelectionCaret(s, e) => (Some(s), e),
SelectionState::PreInit => unreachable!(),
};
let mut caret = TEXT.resolve_caret();
caret.set_index(caret_idx); caret.selection_index = selection_idx;
}
UndoFullOp::Info { info } => *info = Some(Arc::new(l10n!("text-edit-op.clear", "clear").get())),
UndoFullOp::Merge {
next_data,
within_undo_interval,
merged,
..
} => *merged = within_undo_interval && next_data.is::<Cleared>(),
})
}
pub fn replace(mut select_before: ops::Range<usize>, insert: impl Into<Txt>, mut select_after: ops::Range<usize>) -> Self {
let mut insert = insert.into();
let mut removed = Txt::from_static("");
Self::new((), move |_, op| match op {
UndoFullOp::Init { redo } => {
let ctx = TEXT.resolved();
select_before.start = ctx.segmented_text.snap_grapheme_boundary(select_before.start);
select_before.end = ctx.segmented_text.snap_grapheme_boundary(select_before.end);
ctx.txt.with(|t| {
removed = Txt::from_str(&t[select_before.clone()]);
});
Self::apply_max_count(redo, &ctx.txt, select_before.clone(), &mut insert);
}
UndoFullOp::Op(UndoOp::Redo) => {
TEXT.resolved()
.txt
.modify(clmv!(select_before, insert, |args| {
args.to_mut().to_mut().replace_range(select_before, insert.as_str());
}))
.unwrap();
TEXT.resolve_caret().set_char_selection(select_after.start, select_after.end);
}
UndoFullOp::Op(UndoOp::Undo) => {
let ctx = TEXT.resolved();
select_after.start = ctx.segmented_text.snap_grapheme_boundary(select_after.start);
select_after.end = ctx.segmented_text.snap_grapheme_boundary(select_after.end);
ctx.txt
.modify(clmv!(select_after, removed, |args| {
args.to_mut().to_mut().replace_range(select_after, removed.as_str());
}))
.unwrap();
drop(ctx);
TEXT.resolve_caret().set_char_selection(select_before.start, select_before.end);
}
UndoFullOp::Info { info } => *info = Some(Arc::new(l10n!("text-edit-op.replace", "replace").get())),
UndoFullOp::Merge { .. } => {}
})
}
pub fn apply_transforms() -> Self {
let mut prev = Txt::from_static("");
let mut transform = None::<(TextTransformFn, WhiteSpace)>;
Self::new((), move |_, op| match op {
UndoFullOp::Init { .. } => {}
UndoFullOp::Op(UndoOp::Redo) => {
let (t, w) = transform.get_or_insert_with(|| (TEXT_TRANSFORM_VAR.get(), WHITE_SPACE_VAR.get()));
let ctx = TEXT.resolved();
let new_txt = ctx.txt.with(|txt| {
let transformed = t.transform(txt);
let white_spaced = w.transform(transformed.as_ref());
if let Cow::Owned(w) = white_spaced {
Some(w)
} else if let Cow::Owned(t) = transformed {
Some(t)
} else {
None
}
});
if let Some(t) = new_txt {
if ctx.txt.with(|t| t != prev.as_str()) {
prev = ctx.txt.get();
}
let _ = ctx.txt.set(t);
}
}
UndoFullOp::Op(UndoOp::Undo) => {
let ctx = TEXT.resolved();
if ctx.txt.with(|t| t != prev.as_str()) {
let _ = ctx.txt.set(prev.clone());
}
}
UndoFullOp::Info { info } => *info = Some(Arc::new(l10n!("text-edit-op.transform", "transform").get())),
UndoFullOp::Merge { .. } => {}
})
}
fn call(self) -> bool {
{
let mut op = self.0.lock();
let op = &mut *op;
let mut redo = true;
(op.op)(&mut *op.data, UndoFullOp::Init { redo: &mut redo });
if !redo {
return false;
}
(op.op)(&mut *op.data, UndoFullOp::Op(UndoOp::Redo));
}
if !OBSCURE_TXT_VAR.get() {
UNDO.register(UndoTextEditOp::new(self));
}
true
}
pub(super) fn call_edit_op(self) {
let registered = self.call();
if registered && !TEXT.resolved().pending_edit {
TEXT.resolve().pending_edit = true;
WIDGET.update(); }
}
}
#[derive(Clone, Copy, Default)]
enum SelectionState {
#[default]
PreInit,
Caret(CaretIndex),
CaretSelection(CaretIndex, CaretIndex),
SelectionCaret(CaretIndex, CaretIndex),
}
#[derive(Debug, Clone)]
pub(super) struct UndoTextEditOp {
pub target: WidgetId,
edit_op: TextEditOp,
exec_op: UndoOp,
}
impl UndoTextEditOp {
fn new(edit_op: TextEditOp) -> Self {
Self {
target: WIDGET.id(),
edit_op,
exec_op: UndoOp::Undo,
}
}
pub(super) fn call(&self) {
let mut op = self.edit_op.0.lock();
let op = &mut *op;
(op.op)(&mut *op.data, UndoFullOp::Op(self.exec_op))
}
}
impl UndoAction for UndoTextEditOp {
fn undo(self: Box<Self>) -> Box<dyn RedoAction> {
EDIT_CMD.scoped(self.target).notify_param(Self {
target: self.target,
edit_op: self.edit_op.clone(),
exec_op: UndoOp::Undo,
});
self
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
let mut op = self.edit_op.0.lock();
let op = &mut *op;
let mut info = None;
(op.op)(&mut *op.data, UndoFullOp::Info { info: &mut info });
info.unwrap_or_else(|| Arc::new(l10n!("text-edit-op.generic", "text edit").get()))
}
fn as_any(&mut self) -> &mut dyn std::any::Any {
self
}
fn merge(self: Box<Self>, mut args: UndoActionMergeArgs) -> Result<Box<dyn UndoAction>, (Box<dyn UndoAction>, Box<dyn UndoAction>)> {
if let Some(next) = args.next.as_any().downcast_mut::<Self>() {
let mut merged = false;
{
let mut op = self.edit_op.0.lock();
let op = &mut *op;
let mut next_op = next.edit_op.0.lock();
(op.op)(
&mut *op.data,
UndoFullOp::Merge {
next_data: &mut *next_op.data,
prev_timestamp: args.prev_timestamp,
within_undo_interval: args.within_undo_interval,
merged: &mut merged,
},
);
}
if merged {
return Ok(self);
}
}
Err((self, args.next))
}
}
impl RedoAction for UndoTextEditOp {
fn redo(self: Box<Self>) -> Box<dyn UndoAction> {
EDIT_CMD.scoped(self.target).notify_param(Self {
target: self.target,
edit_op: self.edit_op.clone(),
exec_op: UndoOp::Redo,
});
self
}
fn info(&mut self) -> Arc<dyn UndoInfo> {
let mut op = self.edit_op.0.lock();
let op = &mut *op;
let mut info = None;
(op.op)(&mut *op.data, UndoFullOp::Info { info: &mut info });
info.unwrap_or_else(|| Arc::new(l10n!("text-edit-op.generic", "text edit").get()))
}
}
#[derive(Clone)]
pub struct TextSelectOp {
op: Arc<Mutex<dyn FnMut() + Send>>,
}
impl fmt::Debug for TextSelectOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TextSelectOp").finish_non_exhaustive()
}
}
impl TextSelectOp {
pub fn new(op: impl FnMut() + Send + 'static) -> Self {
Self {
op: Arc::new(Mutex::new(op)),
}
}
pub fn next() -> Self {
Self::new(|| next_prev(true, SegmentedText::next_insert_index, |_, s| s.end.index))
}
pub fn select_next() -> Self {
Self::new(|| next_prev(false, SegmentedText::next_insert_index, |_, _| unreachable!()))
}
pub fn prev() -> Self {
Self::new(|| next_prev(true, SegmentedText::prev_insert_index, |_, s| s.start.index))
}
pub fn select_prev() -> Self {
Self::new(|| next_prev(false, SegmentedText::prev_insert_index, |_, _| unreachable!()))
}
pub fn next_word() -> Self {
Self::new(|| next_prev(true, SegmentedText::next_word_index, |t, s| t.next_word_index(s.end.index)))
}
pub fn select_next_word() -> Self {
Self::new(|| next_prev(false, SegmentedText::next_word_index, |_, _| unreachable!()))
}
pub fn prev_word() -> Self {
Self::new(|| next_prev(true, SegmentedText::prev_word_index, |t, s| t.prev_word_index(s.start.index)))
}
pub fn select_prev_word() -> Self {
Self::new(|| next_prev(false, SegmentedText::prev_word_index, |_, _| unreachable!()))
}
pub fn line_up() -> Self {
Self::new(|| line_up_down(true, -1))
}
pub fn select_line_up() -> Self {
Self::new(|| line_up_down(false, -1))
}
pub fn line_down() -> Self {
Self::new(|| line_up_down(true, 1))
}
pub fn select_line_down() -> Self {
Self::new(|| line_up_down(false, 1))
}
pub fn page_up() -> Self {
Self::new(|| page_up_down(true, -1))
}
pub fn select_page_up() -> Self {
Self::new(|| page_up_down(false, -1))
}
pub fn page_down() -> Self {
Self::new(|| page_up_down(true, 1))
}
pub fn select_page_down() -> Self {
Self::new(|| page_up_down(false, 1))
}
pub fn line_start() -> Self {
Self::new(|| line_start_end(true, |li| li.text_range().start))
}
pub fn select_line_start() -> Self {
Self::new(|| line_start_end(false, |li| li.text_range().start))
}
pub fn line_end() -> Self {
Self::new(|| line_start_end(true, |li| li.text_caret_range().end))
}
pub fn select_line_end() -> Self {
Self::new(|| line_start_end(false, |li| li.text_caret_range().end))
}
pub fn text_start() -> Self {
Self::new(|| text_start_end(true, |_| 0))
}
pub fn select_text_start() -> Self {
Self::new(|| text_start_end(false, |_| 0))
}
pub fn text_end() -> Self {
Self::new(|| text_start_end(true, |s| s.len()))
}
pub fn select_text_end() -> Self {
Self::new(|| text_start_end(false, |s| s.len()))
}
pub fn nearest_to(window_point: DipPoint) -> Self {
Self::new(move || {
nearest_to(true, window_point);
})
}
pub fn select_nearest_to(window_point: DipPoint) -> Self {
Self::new(move || {
nearest_to(false, window_point);
})
}
pub fn select_index_nearest_to(window_point: DipPoint, move_selection_index: bool) -> Self {
Self::new(move || {
index_nearest_to(window_point, move_selection_index);
})
}
pub fn select_word_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
Self::new(move || select_line_word_nearest_to(replace_selection, true, window_point))
}
pub fn select_line_nearest_to(replace_selection: bool, window_point: DipPoint) -> Self {
Self::new(move || select_line_word_nearest_to(replace_selection, false, window_point))
}
pub fn select_all() -> Self {
Self::new(|| {
let len = TEXT.resolved().segmented_text.text().len();
let mut caret = TEXT.resolve_caret();
caret.set_char_selection(0, len);
caret.skip_next_scroll = true;
})
}
pub(super) fn call(self) {
(self.op.lock())();
}
}
fn next_prev(
clear_selection: bool,
insert_index_fn: fn(&SegmentedText, usize) -> usize,
selection_index: fn(&SegmentedText, ops::Range<CaretIndex>) -> usize,
) {
let resolved = TEXT.resolved();
let mut i = resolved.caret.index.unwrap_or(CaretIndex::ZERO);
if clear_selection {
i.index = if let Some(s) = resolved.caret.selection_range() {
selection_index(&resolved.segmented_text, s)
} else {
insert_index_fn(&resolved.segmented_text, i.index)
};
} else {
i.index = insert_index_fn(&resolved.segmented_text, i.index);
}
drop(resolved);
let mut c = TEXT.resolve_caret();
if clear_selection {
c.clear_selection();
} else if c.selection_index.is_none() {
c.selection_index = Some(i);
}
c.set_index(i);
c.used_retained_x = false;
}
fn line_up_down(clear_selection: bool, diff: i8) {
let diff = diff as isize;
let mut caret = TEXT.resolve_caret();
let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
if clear_selection {
caret.clear_selection();
} else if caret.selection_index.is_none() {
caret.selection_index = Some(i);
}
caret.used_retained_x = true;
let laidout = TEXT.laidout();
if laidout.caret_origin.is_some() {
let last_line = laidout.shaped_text.lines_len().saturating_sub(1);
let li = i.line;
let next_li = li.saturating_add_signed(diff).min(last_line);
if li != next_li {
drop(caret);
let resolved = TEXT.resolved();
match laidout.shaped_text.line(next_li) {
Some(l) => {
i.line = next_li;
i.index = match l.nearest_seg(laidout.caret_retained_x) {
Some(s) => s.nearest_char_index(laidout.caret_retained_x, resolved.segmented_text.text()),
None => l.text_range().end,
}
}
None => i = CaretIndex::ZERO,
};
i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
drop(resolved);
caret = TEXT.resolve_caret();
caret.set_index(i);
} else if diff == -1 {
caret.set_char_index(0);
} else if diff == 1 {
drop(caret);
let len = TEXT.resolved().segmented_text.text().len();
caret = TEXT.resolve_caret();
caret.set_char_index(len);
}
}
if caret.index.is_none() {
caret.set_index(CaretIndex::ZERO);
caret.clear_selection();
}
}
fn page_up_down(clear_selection: bool, diff: i8) {
let diff = diff as i32;
let mut caret = TEXT.resolve_caret();
let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
if clear_selection {
caret.clear_selection();
} else if caret.selection_index.is_none() {
caret.selection_index = Some(i);
}
let laidout = TEXT.laidout();
let page_y = laidout.viewport.height * Px(diff);
caret.used_retained_x = true;
if laidout.caret_origin.is_some() {
let li = i.line;
if diff == -1 && li == 0 {
caret.set_char_index(0);
} else if diff == 1 && li == laidout.shaped_text.lines_len() - 1 {
drop(caret);
let len = TEXT.resolved().segmented_text.text().len();
caret = TEXT.resolve_caret();
caret.set_char_index(len);
} else if let Some(li) = laidout.shaped_text.line(li) {
drop(caret);
let resolved = TEXT.resolved();
let target_line_y = li.rect().origin.y + page_y;
match laidout.shaped_text.nearest_line(target_line_y) {
Some(l) => {
i.line = l.index();
i.index = match l.nearest_seg(laidout.caret_retained_x) {
Some(s) => s.nearest_char_index(laidout.caret_retained_x, resolved.segmented_text.text()),
None => l.text_range().end,
}
}
None => i = CaretIndex::ZERO,
};
i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
drop(resolved);
caret = TEXT.resolve_caret();
caret.set_index(i);
}
}
if caret.index.is_none() {
caret.set_index(CaretIndex::ZERO);
caret.clear_selection();
}
}
fn line_start_end(clear_selection: bool, index: impl FnOnce(ShapedLine) -> usize) {
let mut caret = TEXT.resolve_caret();
let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
if clear_selection {
caret.clear_selection();
} else if caret.selection_index.is_none() {
caret.selection_index = Some(i);
}
if let Some(li) = TEXT.laidout().shaped_text.line(i.line) {
i.index = index(li);
caret.set_index(i);
caret.used_retained_x = false;
}
}
fn text_start_end(clear_selection: bool, index: impl FnOnce(&str) -> usize) {
let idx = index(TEXT.resolved().segmented_text.text());
let mut caret = TEXT.resolve_caret();
let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
if clear_selection {
caret.clear_selection();
} else if caret.selection_index.is_none() {
caret.selection_index = Some(i);
}
i.index = idx;
caret.set_index(i);
caret.used_retained_x = false;
}
fn nearest_to(clear_selection: bool, window_point: DipPoint) {
let mut caret = TEXT.resolve_caret();
let mut i = caret.index.unwrap_or(CaretIndex::ZERO);
if clear_selection {
caret.clear_selection();
} else if caret.selection_index.is_none() {
caret.selection_index = Some(i);
} else if let Some((_, is_word)) = caret.initial_selection.clone() {
drop(caret);
return select_line_word_nearest_to(false, is_word, window_point);
}
caret.used_retained_x = false;
let laidout = TEXT.laidout();
if let Some(pos) = laidout
.render_info
.transform
.inverse()
.and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
{
drop(caret);
let resolved = TEXT.resolved();
i = match laidout.shaped_text.nearest_line(pos.y) {
Some(l) => CaretIndex {
line: l.index(),
index: match l.nearest_seg(pos.x) {
Some(s) => s.nearest_char_index(pos.x, resolved.segmented_text.text()),
None => l.text_range().end,
},
},
None => CaretIndex::ZERO,
};
i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
drop(resolved);
caret = TEXT.resolve_caret();
caret.set_index(i);
}
if caret.index.is_none() {
caret.set_index(CaretIndex::ZERO);
caret.clear_selection();
}
}
fn index_nearest_to(window_point: DipPoint, move_selection_index: bool) {
let mut caret = TEXT.resolve_caret();
if caret.index.is_none() {
caret.index = Some(CaretIndex::ZERO);
}
if caret.selection_index.is_none() {
caret.selection_index = Some(caret.index.unwrap());
}
caret.used_retained_x = false;
caret.index_version += 1;
let laidout = TEXT.laidout();
if let Some(pos) = laidout
.render_info
.transform
.inverse()
.and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
{
drop(caret);
let resolved = TEXT.resolved();
let mut i = match laidout.shaped_text.nearest_line(pos.y) {
Some(l) => CaretIndex {
line: l.index(),
index: match l.nearest_seg(pos.x) {
Some(s) => s.nearest_char_index(pos.x, resolved.segmented_text.text()),
None => l.text_range().end,
},
},
None => CaretIndex::ZERO,
};
i.index = resolved.segmented_text.snap_grapheme_boundary(i.index);
drop(resolved);
caret = TEXT.resolve_caret();
if move_selection_index {
caret.selection_index = Some(i);
} else {
caret.index = Some(i);
}
}
}
fn select_line_word_nearest_to(replace_selection: bool, select_word: bool, window_point: DipPoint) {
let mut caret = TEXT.resolve_caret();
let laidout = TEXT.laidout();
if let Some(pos) = laidout
.render_info
.transform
.inverse()
.and_then(|t| t.project_point(window_point.to_px(laidout.render_info.scale_factor)))
{
if let Some(l) = laidout.shaped_text.nearest_line(pos.y) {
let range = if select_word {
let max_char = l.actual_text_caret_range().end;
let mut r = l.nearest_seg(pos.x).map(|seg| seg.text_range()).unwrap_or_else(|| l.text_range());
r.start = r.start.min(max_char);
r.end = r.end.min(max_char);
r
} else {
l.actual_text_caret_range()
};
let merge_with_selection = if replace_selection {
None
} else {
caret.initial_selection.clone().map(|(s, _)| s).or_else(|| caret.selection_range())
};
if let Some(mut s) = merge_with_selection {
let caret_at_start = range.start < s.start.index;
s.start.index = s.start.index.min(range.start);
s.end.index = s.end.index.max(range.end);
if caret_at_start {
caret.selection_index = Some(s.end);
caret.set_index(s.start);
} else {
caret.selection_index = Some(s.start);
caret.set_index(s.end);
}
} else {
let start = CaretIndex {
line: l.index(),
index: range.start,
};
let end = CaretIndex {
line: l.index(),
index: range.end,
};
caret.selection_index = Some(start);
caret.set_index(end);
caret.initial_selection = Some((start..end, select_word));
}
return;
};
}
if caret.index.is_none() {
caret.set_index(CaretIndex::ZERO);
caret.clear_selection();
}
}