use std::time::Duration;
use ratatui_core::buffer::{Buffer, Cell};
use crate::geometry::{Rect, clamp_u16};
use crate::theme::State;
use crate::widget::{Frame, PaintCx, WidgetId};
const MULTI_PRESS: Duration = Duration::from_millis(400);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Unit {
Cell,
Word,
Line,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CopyKind {
Clean,
Raw,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Press {
at: (i32, i32),
time: Duration,
count: u8,
}
impl Press {
pub(crate) fn next(last: Option<Self>, at: (i32, i32), time: Duration) -> Self {
let count = match last {
Some(last) if last.at == at && time.saturating_sub(last.time) < MULTI_PRESS => last.count % 3 + 1,
_ => 1,
};
Self { at, time, count }
}
}
#[derive(Debug, Clone)]
pub(crate) struct Selection {
region: Rect,
owner: WidgetId,
inner: Option<WidgetId>,
tracked: Option<(WidgetId, Rect)>,
anchor: (i32, i32),
head: (i32, i32),
unit: Unit,
pub(crate) dragging: bool,
visible: bool,
pub(crate) copy_pending: Option<CopyKind>,
copied_at: Option<Duration>,
painted: Vec<Rect>,
}
impl Selection {
pub(crate) fn begin(frame: &Frame, press: Press, hit: Option<WidgetId>) -> Option<Self> {
let (x, y) = press.at;
if frame.unselectable.iter().any(|rect| rect.contains(x, y)) {
return None;
}
let owner = frame
.selectable
.iter()
.rev()
.filter(|(rect, id)| rect.contains(x, y) && frame.reachable(*id))
.find(|(_, id)| hit.is_none_or(|target| frame.is_within(target, *id) || frame.is_within(*id, target)))
.map(|(_, id)| *id)?;
let inner = if frame.focusable.contains(&owner) {
frame
.parents
.iter()
.find(|(child, parent)| **parent == owner && frame.rects.get(child).is_some_and(|r| r.contains(x, y)))
.map(|(child, _)| *child)
} else {
None
};
let region = Self::region_in(frame, owner, inner)?;
let tracked = frame
.rects
.iter()
.filter(|(id, rect)| rect.contains(x, y) && frame.is_within(**id, owner))
.min_by_key(|(_, rect)| u32::from(rect.width) * u32::from(rect.height))
.map(|(id, rect)| (*id, *rect));
let unit = match press.count {
1 => Unit::Cell,
2 => Unit::Word,
_ => Unit::Line,
};
Some(Self {
region,
owner,
inner,
tracked,
anchor: (x, y),
head: (x, y),
unit,
dragging: true,
visible: unit != Unit::Cell,
copy_pending: None,
copied_at: None,
painted: Vec::new(),
})
}
fn region_in(frame: &Frame, owner: WidgetId, inner: Option<WidgetId>) -> Option<Rect> {
let rect = frame.selectable.iter().rev().find(|(_, id)| *id == owner).map(|(rect, _)| *rect)?;
let region = inner.and_then(|inner| frame.rects.get(&inner)).map_or(rect, |child| rect.intersect(*child));
(!region.is_empty()).then_some(region)
}
pub(crate) fn drag_to(&mut self, x: i32, y: i32) {
let region = self.region;
let head = (x.clamp(region.x, region.right() - 1), y.clamp(region.y, region.bottom() - 1));
if head != self.anchor {
self.visible = true;
}
self.head = head;
}
pub(crate) fn release(&mut self) -> bool {
self.dragging = false;
self.visible
}
pub(crate) fn contains(&self, x: i32, y: i32) -> bool {
self.visible && self.painted.iter().any(|row| row.contains(x, y))
}
pub(crate) fn follow(&mut self, frame: &Frame) -> bool {
match Self::region_in(frame, self.owner, self.inner) {
Some(region) => self.region = region,
None => return false,
}
if let Some((id, old)) = self.tracked {
let Some(new) = frame.rects.get(&id).copied() else {
return false;
};
let (dx, dy) = (new.x - old.x, new.y - old.y);
self.anchor = (self.anchor.0 + dx, self.anchor.1 + dy);
self.head = (self.head.0 + dx, self.head.1 + dy);
self.tracked = Some((id, new));
}
true
}
fn span(&self, buf: &Buffer) -> ((i32, i32), (i32, i32)) {
let (mut start, mut end) = if (self.anchor.1, self.anchor.0) <= (self.head.1, self.head.0) {
(self.anchor, self.head)
} else {
(self.head, self.anchor)
};
match self.unit {
Unit::Cell => {}
Unit::Line => {
start.0 = self.region.x;
end.0 = self.region.right() - 1;
}
Unit::Word => {
if is_word(symbol(buf, start.0, start.1)) {
while start.0 > self.region.x && is_word(symbol(buf, start.0 - 1, start.1)) {
start.0 -= 1;
}
}
if is_word(symbol(buf, end.0, end.1)) {
while end.0 + 1 < self.region.right() && is_word(symbol(buf, end.0 + 1, end.1)) {
end.0 += 1;
}
}
}
}
(start, end)
}
fn row_columns(&self, span: ((i32, i32), (i32, i32)), y: i32) -> Option<(i32, i32)> {
let ((start_x, start_y), (end_x, end_y)) = span;
if y < start_y || y > end_y || y < self.region.y || y >= self.region.bottom() {
return None;
}
let from = if y == start_y { start_x } else { self.region.x };
let to = if y == end_y { end_x } else { self.region.right() - 1 };
let (from, to) = (from.max(self.region.x), to.min(self.region.right() - 1));
(from <= to).then_some((from, to))
}
pub(crate) fn text(&self, buf: &Buffer, decorations: &[Rect], kind: CopyKind) -> String {
let span = self.span(buf);
let mut lines = Vec::new();
for y in span.0.1..=span.1.1 {
let Some((from, to)) = self.row_columns(span, y) else { continue };
let content: Vec<i32> = (from..=to)
.filter(|x| kind == CopyKind::Raw || !decorations.iter().any(|rect| rect.contains(*x, y)))
.collect();
if content.is_empty() {
continue;
}
let line: String = content.into_iter().map(|x| symbol(buf, x, y)).collect();
lines.push(match kind {
CopyKind::Clean => line.trim_end().to_owned(),
CopyKind::Raw => line,
});
}
lines.join("\n")
}
pub(crate) fn paint(&mut self, cx: &mut PaintCx<'_>) {
self.painted.clear();
if !self.visible {
return;
}
let flash = cx.env().theme().motion().flash;
let flashing = self.copied_at.is_some_and(|at| cx.now() < at + flash);
if let Some(at) = self.copied_at.filter(|_| flashing) {
cx.request_frame_in(at + flash - cx.now());
}
let states = if flashing { vec![State::Pressed] } else { Vec::new() };
let style = cx.style("text-selection", None, &states).text();
let bg = style.bg.unwrap_or_else(|| cx.color("active"));
let readable = style.fg.unwrap_or_else(|| cx.color("text"));
let span = self.span(cx.buf);
for y in span.0.1..=span.1.1 {
let Some((from, to)) = self.row_columns(span, y) else { continue };
let row = Rect::new(from, y, clamp_u16(to - from + 1), 1);
cx.fill_keeping_text_readable(row, bg, readable);
self.painted.push(row);
}
}
pub(crate) fn copied(&mut self, now: Duration) {
self.copy_pending = None;
self.copied_at = Some(now);
}
}
fn symbol(buf: &Buffer, x: i32, y: i32) -> &str {
let (Ok(x), Ok(y)) = (u16::try_from(x), u16::try_from(y)) else { return "" };
buf.cell((x, y)).map_or("", Cell::symbol)
}
fn is_word(symbol: &str) -> bool {
!symbol.is_empty() && symbol.chars().all(|c| c.is_alphanumeric() || "_-./:@~#%+=".contains(c))
}