use std::cell::RefCell;
use std::rc::Rc;
use crate::base::{Point, Rect, Rgba};
use crate::input::{Event, KeyCode, KeyEventKind, Mods, MouseButton, MouseKind};
use crate::reactive::request_frame;
use crate::render::{Cell, Surface};
use crate::ui::PressProbe;
thread_local! {
static SELECTION: Rc<RefCell<SelectionState>> =
Rc::new(RefCell::new(SelectionState::default()));
static CAPTURE: Rc<RefCell<CaptureState>> =
Rc::new(RefCell::new(CaptureState::default()));
}
pub fn selection() -> Selection {
Selection {
state: SELECTION.with(Rc::clone),
}
}
pub fn mouse_capture() -> MouseCapture {
MouseCapture {
state: CAPTURE.with(Rc::clone),
}
}
pub fn copy_to_clipboard(text: impl Into<String>) {
let text = text.into();
if text.trim().is_empty() {
return;
}
SELECTION.with(|s| s.borrow_mut().pending_copies.push(text));
request_frame();
}
fn publish_freeze(st: &SelectionState) {
crate::widgets::scroll::freeze_follow_tail(st.region.is_some());
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct Region {
anchor: Point,
head: Point,
clamp: Rect,
}
impl Region {
fn ordered(&self) -> (Point, Point) {
if (self.anchor.y, self.anchor.x) <= (self.head.y, self.head.x) {
(self.anchor, self.head)
} else {
(self.head, self.anchor)
}
}
pub(crate) fn row_spans(&self, out: &mut Vec<Rect>) {
let (s, e) = self.ordered();
for y in s.y..=e.y {
let x0 = if y == s.y { s.x } else { self.clamp.x };
let x1 = if y == e.y {
e.x + 1
} else {
self.clamp.right()
};
let rect = Rect::new(x0, y, x1 - x0, 1).intersect(self.clamp);
if !rect.is_empty() {
out.push(rect);
}
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum SelectionAct {
Pass,
Consumed,
Claim,
Copy(Region),
}
#[derive(Copy, Clone, Debug)]
struct DragArm {
anchor: Point,
clamp: Rect,
press_routed: bool,
}
#[derive(Default)]
struct SelectionState {
enabled: bool,
drag: Option<DragArm>,
region: Option<Region>,
painted: Vec<Rect>,
dirty: bool,
pending_copies: Vec<String>,
}
#[derive(Clone)]
pub struct Selection {
state: Rc<RefCell<SelectionState>>,
}
impl Selection {
pub fn set_enabled(&self, on: bool) {
let mut st = self.state.borrow_mut();
if st.enabled == on {
return;
}
st.enabled = on;
if !on {
Self::clear_locked(&mut st);
}
}
pub fn enabled(&self) -> bool {
self.state.borrow().enabled
}
pub fn toggle(&self) -> bool {
let on = !self.enabled();
self.set_enabled(on);
on
}
pub fn is_active(&self) -> bool {
self.state.borrow().region.is_some()
}
pub fn clear(&self) {
Self::clear_locked(&mut self.state.borrow_mut());
}
fn clear_locked(st: &mut SelectionState) {
st.drag = None;
if st.region.take().is_some() || !st.painted.is_empty() {
st.dirty = true;
request_frame();
}
publish_freeze(st);
}
pub(crate) fn on_input(
&self,
ev: &Event,
probe_at: &mut dyn FnMut(Point) -> PressProbe,
) -> SelectionAct {
let mut st = self.state.borrow_mut();
match ev {
Event::Mouse(m) => {
if !st.enabled {
return SelectionAct::Pass;
}
match (m.kind, m.button) {
(MouseKind::Down, MouseButton::Left) => {
st.drag = None;
let dismissed = st.region.take().is_some();
if dismissed {
st.dirty = true;
request_frame();
publish_freeze(&st); }
let probe = probe_at(m.pos);
if probe.drag_owner {
return SelectionAct::Pass;
}
let Some(clamp) = probe.pane else {
return SelectionAct::Pass;
};
st.drag = Some(DragArm {
anchor: clamp_point(m.pos, clamp),
clamp,
press_routed: !dismissed,
});
if dismissed {
SelectionAct::Consumed
} else {
SelectionAct::Pass
}
}
(MouseKind::Drag, MouseButton::Left) => {
let Some(arm) = st.drag else {
return SelectionAct::Pass;
};
let head = clamp_point(m.pos, arm.clamp);
if st.region.is_none() && head == arm.anchor {
return if arm.press_routed {
SelectionAct::Pass
} else {
SelectionAct::Consumed
};
}
let claiming = st.region.is_none();
let next = Region {
anchor: arm.anchor,
head,
clamp: arm.clamp,
};
if st.region != Some(next) {
st.region = Some(next);
st.dirty = true;
request_frame();
publish_freeze(&st);
}
if claiming && arm.press_routed {
SelectionAct::Claim
} else {
SelectionAct::Consumed
}
}
(MouseKind::Up, MouseButton::Left) => {
let Some(arm) = st.drag.take() else {
return SelectionAct::Pass; };
match st.region.take() {
Some(region) => {
Self::clear_locked(&mut st);
SelectionAct::Copy(region)
}
None => {
if arm.press_routed {
SelectionAct::Pass
} else {
SelectionAct::Consumed
}
}
}
}
_ => SelectionAct::Pass, }
}
Event::Key(k) => {
let Some(region) = st.region else {
return SelectionAct::Pass;
};
if k.kind == KeyEventKind::Release {
return SelectionAct::Pass;
}
let mods = k.mods.without_locks();
match k.code {
KeyCode::Esc if mods == Mods::NONE => {
Self::clear_locked(&mut st);
SelectionAct::Consumed
}
KeyCode::Enter if mods == Mods::NONE => {
Self::clear_locked(&mut st);
SelectionAct::Copy(region)
}
KeyCode::Char('c') if mods == Mods::NONE || mods == Mods::CTRL => {
Self::clear_locked(&mut st);
SelectionAct::Copy(region)
}
_ => SelectionAct::Pass,
}
}
_ => SelectionAct::Pass,
}
}
pub(crate) fn add_flatten_damage(&self, root: &mut Surface) {
let mut st = self.state.borrow_mut();
if !st.dirty {
return;
}
st.dirty = false;
for &r in &st.painted {
root.add_damage(r);
}
let mut spans = Vec::new();
if let Some(region) = st.region {
region.row_spans(&mut spans);
}
for r in spans {
root.add_damage(r);
}
}
pub(crate) fn paint_into(&self, frame: &mut Surface, fg: Rgba, bg: Rgba) {
let mut st = self.state.borrow_mut();
let Some(region) = st.region else {
st.painted.clear();
return;
};
st.painted.clear();
let mut spans = Vec::new();
region.row_spans(&mut spans);
for span in spans {
let Some((x0, x1)) = expand_to_glyphs(frame, span) else {
continue;
};
for x in x0..x1 {
let Some(&cell) = frame.get(x, span.y) else {
continue;
};
if cell.is_continuation() {
continue; }
frame.put_composed(x, span.y, Cell { fg, bg, ..cell });
}
frame.repair_wide_pairs(span.y, x0, x1);
st.painted.push(Rect::new(x0, span.y, x1 - x0, 1));
}
}
pub(crate) fn take_pending_copies(&self) -> Vec<String> {
std::mem::take(&mut self.state.borrow_mut().pending_copies)
}
pub(crate) fn reset_session(&self) {
let mut st = self.state.borrow_mut();
st.drag = None;
st.region = None;
st.painted.clear();
st.dirty = false;
publish_freeze(&st);
}
pub(crate) fn on_resize(&self) {
let mut st = self.state.borrow_mut();
st.drag = None;
st.region = None;
st.painted.clear();
st.dirty = false;
publish_freeze(&st);
}
}
fn clamp_point(p: Point, rect: Rect) -> Point {
Point::new(
p.x.clamp(rect.x, (rect.right() - 1).max(rect.x)),
p.y.clamp(rect.y, (rect.bottom() - 1).max(rect.y)),
)
}
fn expand_to_glyphs(frame: &Surface, span: Rect) -> Option<(i32, i32)> {
if span.y < 0 || span.y >= frame.height() {
return None;
}
let mut x0 = span.x.max(0);
let mut x1 = span.right().min(frame.width());
if x1 <= x0 {
return None;
}
if frame.get(x0, span.y).is_some_and(Cell::is_continuation) {
x0 = (x0 - 1).max(0);
}
if x1 < frame.width() && frame.get(x1, span.y).is_some_and(Cell::is_continuation) {
x1 += 1;
}
Some((x0, x1))
}
pub(crate) fn extract_text(frame: &Surface, region: &Region) -> String {
let mut spans = Vec::new();
region.row_spans(&mut spans);
let mut out = String::new();
for (i, span) in spans.iter().enumerate() {
if i > 0 {
out.push('\n');
}
let row_start = out.len();
let Some((x0, x1)) = expand_to_glyphs(frame, *span) else {
continue;
};
for x in x0..x1 {
let Some(cell) = frame.get(x, span.y) else {
continue;
};
if cell.is_continuation() {
continue; }
let g = frame.glyph_str(cell);
out.push_str(if g.is_empty() { " " } else { g });
}
let kept = out[row_start..].trim_end().len();
out.truncate(row_start + kept);
}
out
}
pub(super) fn layout_for_anchor(app: &mut super::App, overlays: &super::overlays::Overlays) {
use super::overlays::{OverlayContent, ROOT_LAYER_ID};
app.tree().layout();
let mut trees: Vec<crate::ui::UiTree> = {
let store = overlays.store().borrow();
store
.meta
.iter()
.zip(&store.layers)
.filter(|(m, l)| m.id != ROOT_LAYER_ID && l.visible())
.filter_map(|(m, _)| match &m.content {
OverlayContent::Tree { tree, .. } => Some(tree.handle()),
_ => None,
})
.collect()
};
for tree in &mut trees {
tree.layout();
}
}
pub(super) fn selection_anchor(
app: &mut super::App,
overlays: &super::overlays::Overlays,
viewport: crate::base::Size,
p: Point,
) -> PressProbe {
use super::overlays::{OverlayContent, ROOT_LAYER_ID};
let vp = Rect::from_size(viewport);
let mut candidates: Vec<(crate::ui::UiTree, Rect, i32)> = {
let store = overlays.store().borrow();
store
.meta
.iter()
.zip(&store.layers)
.filter(|(m, l)| m.id != ROOT_LAYER_ID && l.visible())
.filter_map(|(m, l)| match &m.content {
OverlayContent::Tree { tree, .. } => Some((tree.handle(), l.bounds(), l.z())),
_ => None,
})
.collect()
};
candidates.sort_by_key(|(_, _, z)| std::cmp::Reverse(*z));
for (tree, bounds, _) in candidates {
if bounds.contains(p) {
let local = Point::new(p.x - bounds.x, p.y - bounds.y);
let probe = tree.press_probe_at(local);
let pane = probe
.pane
.map(|r| r.translate(bounds.x, bounds.y))
.unwrap_or(bounds)
.intersect(vp);
return PressProbe {
pane: Some(if pane.is_empty() { vp } else { pane }),
drag_owner: probe.drag_owner,
};
}
}
let probe = app.tree().press_probe_at(p);
let pane = probe.pane.unwrap_or(vp).intersect(vp);
PressProbe {
pane: Some(if pane.is_empty() { vp } else { pane }),
drag_owner: probe.drag_owner,
}
}
#[derive(Default)]
struct CaptureState {
requested: Option<bool>,
suspended: bool,
}
#[derive(Clone)]
pub struct MouseCapture {
state: Rc<RefCell<CaptureState>>,
}
impl MouseCapture {
pub fn set_reporting(&self, on: bool) {
let mut st = self.state.borrow_mut();
st.requested = Some(on);
st.suspended = !on;
drop(st);
request_frame();
}
pub fn suspend(&self) {
self.set_reporting(false);
}
pub fn resume(&self) {
self.set_reporting(true);
}
pub fn is_suspended(&self) -> bool {
self.state.borrow().suspended
}
pub(crate) fn take_request(&self) -> Option<bool> {
self.state.borrow_mut().requested.take()
}
}
#[cfg(test)]
#[path = "selection_tests.rs"]
mod tests;