Files
figcraft/crates/engine/src/canvas/tools/pointer.rs
T

296 lines
15 KiB
Rust

use super::handles::{handle_at, label_at, locked_in, on_corner_square, resize_cursor, selected_end_at};
use super::{
Cursor, Document, Gesture, NodeId, NodeKind, Overlay, PenCursor, Point, PointerEvent, PointerKind, Rect, Result, Session, ToolId, at_45_degrees,
frame_at, json, select_target,
};
impl Session {
/// The pointer left the canvas: what hovering there showed goes (the distances Alt or Dev Mode
/// measured and the box measured to, a vector region's highlight), unless a gesture holds it.
pub fn pointer_left(&mut self) {
if !self.tool.busy() {
self.tool.overlay.guides.clear();
self.tool.overlay.measure_box = None;
}
self.vector_pointer_left();
}
/// What a marquee starting at page point `p` selects inside: the top-level frame it starts in
/// (its children), or the focus view's frame.
pub(crate) fn marquee_within(&self, p: Point) -> Result<Option<NodeId>> {
let (doc, page) = (self.doc()?, self.page()?);
let within = frame_at(doc, page, p, &[]).filter(|f| doc.node(*f).is_some_and(|n| n.parent == Some(page)));
let within = within.filter(|f| doc.world_bounds(*f).is_some_and(|b| b.contains(p)));
Ok(self.ds()?.focused().or(within))
}
/// The selection a marquee over page rect `r` makes from `base` (what was selected before):
/// the layers it touches (`deep`: nested ones too); `additive` (Shift) toggles them.
pub(crate) fn marquee_selection(&self, r: Rect, within: Option<NodeId>, deep: bool, base: &[NodeId], additive: bool) -> Result<Vec<NodeId>> {
let (doc, page) = (self.doc()?, self.page()?);
let mut hits =
if deep { figcraft_doc::geometry::marquee_deep(doc, page, r) } else { figcraft_doc::geometry::marquee_select(doc, page, r, within) };
if let Some(f) = self.ds()?.focused() {
hits.retain(|h| *h != f && doc.is_ancestor(f, *h));
}
// A marquee over a frame from outside selects the frame only when it covers it fully.
if within.is_none() && !deep {
hits.retain(|h| {
let full = doc.world_bounds(*h).is_some_and(|b| r.contains(b.origin()) && r.contains(Point::new(b.x1, b.y1)));
full || !doc.node(*h).is_some_and(|n| n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }))
});
}
// Shift toggles: what was selected and is touched again comes out.
let mut sel: Vec<NodeId> = base.iter().copied().filter(|b| !(additive && hits.contains(b))).collect();
for h in hits {
if !base.contains(&h) && !sel.contains(&h) {
sel.push(h);
}
}
Ok(sel)
}
pub fn set_tool(&mut self, t: ToolId) {
if self.tool.tool == ToolId::Pen && t != ToolId::Pen {
let _ = self.pen_finish();
}
if !matches!(t, ToolId::Move | ToolId::Scale | ToolId::Hand) && self.ds().is_ok_and(|d| d.vector_edit.is_some()) {
let _ = crate::vectoredit::end(self);
}
self.tool.tool = t;
self.tool.gesture = Gesture::None;
self.tool.pick_only = false;
// The tool's cursor shows before the pointer moves.
self.tool.overlay =
Overlay { builder_remove: self.tool.builder_remove && t == ToolId::ShapeBuilder, cursor: t.cursor(), ..Overlay::default() };
self.tool.shape_builder = None;
if t == ToolId::ShapeBuilder {
// Regions show straight away, before the pointer moves.
let sel = self.selection();
let _ = crate::shapebuilder::arrangement(self, &sel);
}
if let Ok(ds) = self.ds_mut() {
if !matches!(t, ToolId::Text | ToolId::TextOnPath | ToolId::Move) {
ds.text_edit = None;
}
ds.revision += 1;
}
}
/// The tool pointer events go to: in Dev Mode every tool but the hand and comments selects.
fn acting_tool(&self) -> ToolId {
match self.tool.tool {
t if self.tool.dev_mode && !matches!(t, ToolId::Hand | ToolId::Comment) => ToolId::Move,
t => t,
}
}
/// Drop the gesture under way (a drag, a resize, a shape being drawn), restoring the document
/// as Escape does; the UI calls it when Dev Mode starts, where nothing may move.
pub fn drop_gesture(&mut self) -> Result<()> {
self.tool.gesture = Gesture::None;
self.cancel_interaction()
}
/// Feed one pointer event to the current tool.
pub fn pointer(&mut self, ev: &PointerEvent) -> Result<()> {
// A held button sends the same move every frame: running the gesture again would only
// rebuild the same preview (a new document, and a canvas render, every frame).
if ev.kind != PointerKind::Move {
self.tool.last_move = None;
} else if self.tool.busy() {
if self.tool.last_move.as_ref() == Some(ev) {
return Ok(());
}
self.tool.last_move = Some(ev.clone());
}
let tool = self.acting_tool();
let edits = matches!(tool, ToolId::Move | ToolId::Scale) && !self.tool.dev_mode;
if edits && self.origin_pointer(ev)? {
return Ok(());
}
if edits && self.ds().is_ok_and(|d| d.vector_edit.is_some()) && self.vector_pointer(ev)? {
return Ok(());
}
if tool == ToolId::ShapeBuilder {
return self.shape_builder_pointer(ev);
}
match ev.kind {
PointerKind::Hover => self.hover(ev),
PointerKind::Down => match tool {
ToolId::Move | ToolId::Scale => {
// While a board's vote runs, a click on an object votes.
if tool == ToolId::Move && !self.tool.dev_mode && self.vote_down(ev)? {
return Ok(());
}
// A click on the canvas deselects a ruler guide (the UI selects guides itself).
self.ds_mut()?.guide = None;
self.move_down(ev)
}
ToolId::Hand => {
let v = &self.ds()?.view;
self.tool.gesture = Gesture::Pan { sx: ev.sx, sy: ev.sy, cx: v.cx, cy: v.cy };
self.tool.overlay.cursor = Cursor::Grabbing;
Ok(())
}
ToolId::Text => self.text_down(ev),
ToolId::TextOnPath => self.text_on_path_down(ev),
ToolId::Pen => self.pen_down(ev),
ToolId::Pencil => {
self.tool.gesture = Gesture::Pencil { points: vec![ev.p()], pressure: vec![ev.pressure] };
Ok(())
}
ToolId::Comment => {
self.request_ui("comment.new", json!({"x": ev.x, "y": ev.y}));
Ok(())
}
ToolId::Eyedropper => self.eyedrop(ev),
t if t.on_boards() => self.board_down(ev),
t if t.draws_shape() => self.draw_down(ev),
_ => Ok(()),
},
PointerKind::Move => self.drag(ev),
PointerKind::Up => self.up(ev),
PointerKind::DoubleClick => self.double_click(ev),
}
}
/// The rotation origin handle while it shows: a press drags it, a double-click puts it back
/// at the centre. True when the event was the handle's.
fn origin_pointer(&mut self, ev: &PointerEvent) -> Result<bool> {
let dragging = matches!(self.tool.gesture, Gesture::Origin);
if !dragging && !self.ds()?.on_origin_handle(ev.p(), ev.zoom) {
return Ok(false);
}
match ev.kind {
PointerKind::Down if !dragging => self.tool.gesture = Gesture::Origin,
PointerKind::Move if dragging => self.drag_origin_to(ev.p(), ev.zoom)?,
PointerKind::Up if dragging => self.tool.gesture = Gesture::None,
PointerKind::DoubleClick => self.set_rotation_origin(None)?,
PointerKind::Hover => {}
_ => return Ok(false),
}
self.tool.overlay.cursor = Cursor::Move;
Ok(true)
}
pub(super) fn hover(&mut self, ev: &PointerEvent) -> Result<()> {
let tool = self.acting_tool();
let ds = self.ds()?;
let (doc, sel, page) = (ds.doc.clone(), ds.selection.clone(), ds.page);
let mut cursor = tool.cursor();
// The text being edited, and with the Text tool any text, takes the I-beam: a click there
// puts the caret.
let on_edited = ds
.text_edit
.as_ref()
.is_some_and(|te| doc.world_bounds(te.id).is_some_and(|b| b.inflate(ev.tol() * 4.0, ev.tol() * 4.0).contains(ev.p())));
let on_text =
|s: &Self| s.hit(&doc, page, ev.p(), ev.tol() * 2.0).and_then(|l| doc.node(l)).is_some_and(|n| matches!(n.kind, NodeKind::Text(_)));
if tool == ToolId::Text && (on_edited || on_text(self)) {
cursor = Cursor::Text;
}
if tool == ToolId::TextOnPath && (on_edited || self.path_at(&doc, page, ev).is_some()) {
cursor = Cursor::Text;
}
let mut hover = None;
let mut guides = Vec::new();
let mut measure_box = None;
let arc_hover = if self.arc_handles_shown() { crate::arc_handles::hit(&doc, &sel, ev.p(), ev.zoom).map(|(_, h)| h) } else { None };
self.tool.overlay.arc_hover = arc_hover;
let shape_hover = if self.arc_handles_shown() && !on_corner_square(&doc, &sel, ev.p(), ev.zoom) {
crate::shape_handles::hit(&doc, &sel, ev.p(), ev.zoom).map(|(_, h)| h)
} else {
None
};
self.tool.overlay.shape_hover = shape_hover;
if matches!(tool, ToolId::Move | ToolId::Scale) && on_edited {
cursor = Cursor::Text;
} else if matches!(tool, ToolId::Move | ToolId::Scale) && arc_hover.is_none() && shape_hover.is_none() {
if let Some(h) = handle_at(&doc, &sel, ev.p(), ev.zoom).filter(|_| !self.tool.dev_mode) {
cursor = resize_cursor(&doc, &sel, h);
} else {
let leaf = self.hit(&doc, page, ev.p(), ev.tol() * 2.0);
if leaf.and_then(|l| crate::widgets::target_at(&doc, l, ev.p())).is_some() {
cursor = Cursor::Pointer;
}
hover = label_at(&doc, page, &self.tool.labels, ev.p()).or_else(|| leaf.map(|leaf| select_target(&doc, leaf, &sel, ev.cmd)));
// Alt over a selected layer: a drag from here duplicates it.
if ev.alt && !self.tool.dev_mode && tool == ToolId::Move && hover.is_some_and(|h| sel.contains(&h) && !locked_in(&doc, h)) {
cursor = Cursor::Copy;
}
if ev.alt || self.tool.dev_mode {
use crate::distance::{Hover, guide_at, hover_lines, padding_frame, parent_of};
// Shift+Alt over an auto layout frame: its padding and gaps. Otherwise from the
// selection to a ruler guide, the hovered layer or (over empty canvas) the parent.
let around: Vec<NodeId> = sel.iter().copied().chain(hover).collect();
let what = hover.filter(|_| ev.alt && ev.shift).and_then(|t| padding_frame(&doc, t)).map(Hover::Padding).or_else(|| {
if sel.is_empty() {
return None;
}
let guide = guide_at(&doc, page, &around, ev.p(), ev.tol() * 4.0).map(|(vertical, at)| Hover::Guide { vertical, at });
guide.or_else(|| hover.or_else(|| parent_of(&doc, page, &sel)).map(Hover::Layer))
});
if let Some(what) = what {
let (g, b) = hover_lines(&doc, page, &sel, what);
guides = g;
measure_box = b.map(|b| [b.x0, b.y0, b.x1, b.y1]);
}
}
}
}
if tool == ToolId::Move && !self.tool.dev_mode && crate::board::end_handle_at(&doc, &sel, ev.p(), ev.zoom).is_some() {
cursor = Cursor::Grab;
}
self.tool.overlay.measure_box = measure_box;
self.tool.overlay.cursor = cursor;
if tool == ToolId::Pen {
// Where a click would land (snapped), with its guides.
let (to, g) = if ev.shift { (ev.p(), Vec::new()) } else { self.snap_pen_tool(ev)? };
guides = g;
self.tool.overlay.cursor = Cursor::Pen(self.pen_cursor(&doc, &sel, to, ev.zoom));
if let Some((last, _, out)) = self.tool.pen.as_ref().and_then(|pen| pen.points.last()) {
// With Shift the band ends where a click puts the point (#970).
let to = if ev.shift { at_45_degrees(*last, to) } else { to };
self.tool.overlay.rubber = Some([last.x, last.y, to.x, to.y]);
self.tool.overlay.rubber_handle = (out.hypot() > 0.0).then_some([last.x + out.x, last.y + out.y]);
}
}
self.tool.overlay.guides = guides;
let ds = self.ds_mut()?;
if ds.hover != hover {
ds.hover = hover;
ds.revision += 1;
}
Ok(())
}
/// What a click with the Pen tool at `p` (where it snaps) would do: close the path on its first
/// point, connect to another of its points, go on from a selected open path's end, or add a
/// new point (the same tests as [`Self::pen_down`]).
fn pen_cursor(&self, doc: &Document, sel: &[NodeId], p: Point, zoom: f64) -> PenCursor {
let near = |q: &Point| (*q - p).hypot() * zoom < 8.0;
let on_point = match &self.tool.pen {
None => selected_end_at(doc, sel, p, 8.0 / zoom.max(1e-6)).is_some(),
Some(pen) => {
let n = pen.points.len();
(n > 2 && pen.points.first().is_some_and(|(q, _, _)| near(q)))
|| (1..n.saturating_sub(1)).any(|k| pen.points.get(k).is_some_and(|(q, _, _)| near(q)))
}
};
if on_point { PenCursor::Close } else { PenCursor::New }
}
/// Whether what the gesture did to `ids` so far is an edit only a main component can make
/// (`moved`: their place among the layers counts too): it is undone when the gesture ends.
pub(super) fn refused(&self, ids: &[NodeId], moved: bool) -> bool {
let Ok(ds) = self.ds() else { return false };
ds.interaction.as_ref().is_some_and(|it| {
ids.iter().any(|m| {
(moved && !crate::components::move_allowed(&it.doc, &ds.doc, *m))
|| crate::components::refused_override(&it.doc, &ds.doc, *m).is_some()
})
})
}
}