use std::cell::RefCell;
use std::rc::Rc;
use repose_core::prelude::*;
use repose_core::{CursorIcon, FontStyle, PointerEvent, TextRange, TextSelection, View};
use web_time::{Duration, Instant};
use crate::textfield::{TextFieldMetrics, index_for_xy_bytes_with_metrics, word_range};
use crate::{Text, TextStyle};
const DOUBLE_TAP_MS: u64 = 300;
const TRIPLE_TAP_MS: u64 = 500;
const TAP_SLOP_PX: f32 = 12.0;
pub trait SelectableTextExt {
fn selectable(self, on_selection_change: impl Fn(Option<(usize, usize)>) + 'static) -> View;
}
impl SelectableTextExt for View {
fn selectable(self, on_selection_change: impl Fn(Option<(usize, usize)>) + 'static) -> View {
let (text, metrics) = match &self.kind {
ViewKind::Text {
text,
font_size,
font_family,
font_weight,
font_style,
letter_spacing,
line_height,
font_variation_settings,
..
} => {
let font_px = font_size.to_px().0;
let metrics = TextFieldMetrics {
font_px,
font_family: *font_family,
font_weight: font_weight.0,
font_style: if matches!(font_style, FontStyle::Italic) {
1
} else {
0
},
letter_spacing_px: letter_spacing.to_px().0,
font_variation_settings: font_variation_settings
.as_ref()
.map(|settings| settings.to_string()),
line_height_px: if line_height.0 > 0.0 {
line_height.to_px().0
} else {
font_px
},
};
(text.clone(), metrics)
}
_ => return self,
};
make_selectable(self, text, metrics, on_selection_change)
}
}
pub fn SelectableText(
text: impl Into<String>,
font_size_sp: Sp,
on_selection_change: impl Fn(Option<(usize, usize)>) + 'static,
) -> View {
let text: String = text.into();
let v = Text(text.clone()).size(font_size_sp);
let font_px = font_size_sp.to_px().0;
let metrics = TextFieldMetrics {
font_px,
line_height_px: font_px,
..TextFieldMetrics::default()
};
make_selectable(v, text, metrics, on_selection_change)
}
fn make_selectable(
mut v: View,
text: String,
metrics: TextFieldMetrics,
on_selection_change: impl Fn(Option<(usize, usize)>) + 'static,
) -> View {
let text_for_handlers = text.clone();
let selection = TextSelection::clone(&remember(TextSelection::default));
let anchor: Rc<RefCell<usize>> = remember(|| RefCell::new(0));
let dragging: Rc<RefCell<bool>> = remember(|| RefCell::new(false));
let last_tap_time: Rc<RefCell<Option<Instant>>> = remember(|| RefCell::new(None));
let last_tap_pos: Rc<RefCell<Option<(f32, f32)>>> = remember(|| RefCell::new(None));
let tap_count: Rc<RefCell<u8>> = remember(|| RefCell::new(0));
let callback = Rc::new(on_selection_change);
let metrics = Rc::new(metrics);
let set_sel = {
let selection = selection.clone();
let callback = callback.clone();
move |s: Option<(usize, usize)>| {
selection.set_range(s.map(|(a, b)| TextRange { start: a, end: b }));
callback(s);
}
};
let hit_test = {
let text = text_for_handlers.clone();
let selection = selection.clone();
let metrics = metrics.clone();
move |x: f32, y: f32| -> usize {
let geo = selection.geometry();
if geo.wrap_width_px <= 0.0 {
return 0;
}
let layout_metrics = TextFieldMetrics {
line_height_px: if geo.line_height_px > 0.0 {
geo.line_height_px
} else {
metrics.line_height_px
},
..(*metrics).clone()
};
index_for_xy_bytes_with_metrics(
&text,
geo.wrap_width_px,
(x - geo.origin.0).max(0.0),
(y - geo.origin.1).max(0.0),
&layout_metrics,
)
}
};
let on_down = {
let text = text_for_handlers.clone();
let selection = selection.clone();
let anchor = anchor.clone();
let dragging = dragging.clone();
let last_tap_time = last_tap_time.clone();
let last_tap_pos = last_tap_pos.clone();
let tap_count = tap_count.clone();
let set_sel = set_sel.clone();
let hit_test = hit_test.clone();
move |ev: PointerEvent| {
let byte = hit_test(ev.position.x, ev.position.y);
let now = Instant::now();
let pos = (ev.position.x, ev.position.y);
let mut count = *tap_count.borrow();
let mut is_multi = false;
if let (Some(t), Some(p)) = (*last_tap_time.borrow(), *last_tap_pos.borrow()) {
let dt = now.saturating_duration_since(t);
let dist = ((pos.0 - p.0).powi(2) + (pos.1 - p.1).powi(2)).sqrt();
if count == 0 {
count = 1;
} else if count == 1
&& dt < Duration::from_millis(DOUBLE_TAP_MS)
&& dist < TAP_SLOP_PX
{
count = 2;
is_multi = true;
} else if count >= 2
&& dt < Duration::from_millis(TRIPLE_TAP_MS)
&& dist < TAP_SLOP_PX
{
count = if count >= 3 {
1
} else {
count.saturating_add(1)
};
is_multi = true;
} else {
count = 1;
}
} else {
count = 1;
}
*tap_count.borrow_mut() = count;
*last_tap_time.borrow_mut() = Some(now);
*last_tap_pos.borrow_mut() = Some(pos);
let shift = ev.modifiers.shift;
if count >= 3 {
let sel = Some((0, text.len()));
*anchor.borrow_mut() = 0;
set_sel(sel);
*dragging.borrow_mut() = false;
if !text.is_empty() {
repose_core::clipboard::set_primary_selection(&text);
}
let _ = is_multi;
return;
}
if count == 2 {
let (s, e) = word_range(&text, byte);
let sel = Some((s, e));
*anchor.borrow_mut() = s;
set_sel(sel);
*dragging.borrow_mut() = true;
if e > s {
repose_core::clipboard::set_primary_selection(&text[s..e]);
}
let _ = is_multi;
return;
}
if shift {
let a = selection
.range()
.map(|range| {
let _ = range;
*anchor.borrow()
})
.unwrap_or(*anchor.borrow());
let sel = Some((a.min(byte), a.max(byte)));
set_sel(sel);
} else {
*anchor.borrow_mut() = byte;
set_sel(Some((byte, byte)));
}
*dragging.borrow_mut() = true;
let _ = is_multi;
}
};
let on_move = {
let text = text_for_handlers.clone();
let anchor = anchor.clone();
let dragging = dragging.clone();
let set_sel = set_sel.clone();
let hit_test = hit_test.clone();
move |ev: PointerEvent| {
if !*dragging.borrow() {
return;
}
let byte = hit_test(ev.position.x, ev.position.y);
let a = *anchor.borrow();
let sel = Some((a.min(byte), a.max(byte)));
set_sel(sel);
if let Some((s, e)) = sel
&& e > s
{
repose_core::clipboard::set_primary_selection(&text[s..e]);
}
}
};
let on_up = {
let text = text_for_handlers.clone();
let selection = selection.clone();
let dragging = dragging.clone();
let set_sel = set_sel.clone();
move |_ev: PointerEvent| {
*dragging.borrow_mut() = false;
let sel = selection.range().map(|r| (r.start, r.end));
if let Some((a, b)) = sel {
let s = a.min(b);
let e = a.max(b);
if e > s {
repose_core::clipboard::set_primary_selection(&text[s..e]);
}
}
set_sel(sel);
}
};
let on_cancel = {
let dragging = dragging.clone();
let tap_count = tap_count.clone();
let last_tap_time = last_tap_time.clone();
let last_tap_pos = last_tap_pos.clone();
move |_ev: PointerEvent| {
*dragging.borrow_mut() = false;
*tap_count.borrow_mut() = 0;
*last_tap_time.borrow_mut() = None;
*last_tap_pos.borrow_mut() = None;
}
};
v.modifier = v
.modifier
.on_pointer_down(on_down)
.on_pointer_move(on_move)
.on_pointer_up(on_up)
.on_pointer_cancel(on_cancel)
.text_selection(selection.clone())
.cursor(CursorIcon::Text)
.on_action({
let selection = selection.clone();
let text = text_for_handlers.clone();
move |action| match action {
repose_core::shortcuts::Action::Copy => {
if let Some(range) = selection.range() {
let s = range.min();
let e = range.max();
if e > s {
repose_core::clipboard::copy_to_clipboard(&text[s..e]);
return true;
}
}
false
}
repose_core::shortcuts::Action::SelectAll => {
let len = text.len();
selection.set_range(Some(TextRange { start: 0, end: len }));
if len > 0 {
repose_core::clipboard::set_primary_selection(&text);
}
true
}
_ => false,
}
});
v
}