Files
deckcraft/crates/engine/src/tools/click.rs
T
Dylan M. Taylor 505a6f28ac Update prompt shows the app icon and accent color instead of a letter (#1561)
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#f07a1e, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/engine/src/tools/click.rs
- crates/engine/src/tools/pointer.rs
- crates/metafile/src/emfplus.rs
- crates/model/src/trendline.rs
- crates/pptx/src/read/dml.rs
- xtask/src/layers.rs
2026-10-30 01:28:38 -04:00

220 lines
10 KiB
Rust

use super::hit::{EditZone, edit_zone, edited_box, shape_in_front, text_member_at, text_pos};
use super::{Drag, Hit, Point, Result, Session, ShapeId, ShapeKind, TextSel, ToolKind, Value, cmd, json, xfrm_of};
impl Session {
pub(super) fn double_click(&mut self, p: Point, tol: f64) -> Result<Value> {
if let Some(Drag::Freeform { mode, mut points, .. }) = self.tool.drag.clone() {
// The double-click's own click added a duplicate vertex.
while points.len() > 2 && points.last().zip(points.get(points.len() - 2)).is_some_and(|(a, b)| (*a - *b).hypot() <= tol) {
points.pop();
}
self.tool.drag = Some(Drag::Freeform { mode, points, pressed: false });
return self.finish_freeform(false);
}
if !matches!(self.tool.kind, ToolKind::Select) {
// Double-click while drawing: lock drawing mode off.
self.tool.sticky = false;
return Ok(Value::Null);
}
// The double-click's second press started a click (caret on release): this replaces it.
if matches!(self.tool.drag, Some(Drag::Move { moved: false, .. })) {
self.tool.drag = None;
}
// In the text being edited: select the word double-clicked.
if self.place_caret(p, tol, false)?.is_some() {
return self.execute("text.selectWord", &json!({}));
}
match self.hit_test(p, tol) {
Some(Hit::Shape { id, .. }) => {
let st = self.doc()?;
let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) };
match &sh.kind {
ShapeKind::Table(_) | ShapeKind::Shape | ShapeKind::Connector { .. } if !sh.is_line() => {
// Caret at the double-click (in a table, in the clicked cell), then select
// the word there.
if !self.edit_text_at(id, p)? {
return Ok(Value::Null);
}
self.execute("text.selectWord", &json!({}))
}
ShapeKind::Group { .. } => {
// A group is a single selection unit, but its text stays editable:
// double-click reaches the text-holding member under the cursor.
match text_member_at(st, &sh, p, tol).and_then(|mid| st.shape(mid).cloned().map(|m| (mid, m))) {
Some((mid, msh)) if !msh.is_line() => {
let pos = text_pos(st, &msh, p);
self.execute("text.edit", &json!({"id": mid, "at": [pos.0, pos.1]}))?;
self.execute("text.selectWord", &json!({}))
}
_ => Ok(Value::Null),
}
}
_ => {
self.ui_requests.push(crate::UiRequest::Pane { id: "format".into() });
Ok(Value::Null)
}
}
}
_ => Ok(Value::Null),
}
}
/// Right-button press: the selection the context menu will act on, as in PowerPoint. In the
/// box being edited, the caret and text selection stay (the menu then acts on the text). On a
/// selected shape (or its handles) the selection stays, multiple selection included; on
/// another shape, that shape alone is selected; on empty slide, nothing is. Never edits text,
/// starts a drag or adds an undo step.
pub(super) fn context_click(&mut self, p: Point, tol: f64) -> Result<Value> {
if !matches!(self.tool.kind, ToolKind::Select) || self.tool.drag.is_some() {
return Ok(Value::Null);
}
let st = self.doc()?;
// The same zone in which a left press places the caret (`down`).
if let Some(t) = &st.selection.text
&& !t.notes
&& t.cell.is_none()
&& let Some(sh) = st.shape(t.shape)
&& xfrm_of(&st.doc, &st.selection, sh).bounds().inflate(tol, tol).contains(p)
{
return Ok(json!({"hit": {"kind": "text", "id": t.shape}, "selected": [t.shape], "editing": true}));
}
let hit = self.hit_test(p, tol);
let id = match &hit {
Some(
Hit::Shape { id, .. }
| Hit::Resize { id, .. }
| Hit::Rotate { id }
| Hit::Adjust { id, .. }
| Hit::LineEnd { id, .. }
| Hit::Handle { id, .. }
| Hit::Vertex { id, .. }
| Hit::Frame { id }
| Hit::TableColumn { id, .. }
| Hit::TableRow { id, .. }
| Hit::Warp { id, .. },
) => Some(*id),
None => None,
};
// Edit Points: the point or else the segment under the pointer picks the menu.
let picked = self.tool.editing_points(st).and_then(|eid| st.shape(eid)).map(|sh| match &hit {
Some(Hit::Vertex { path, index, .. }) => (Some((*path, *index)), None),
Some(Hit::Handle { .. }) => (None, None),
_ => (None, cmd::points::segment_at(&st.doc, &st.selection, sh, p, tol * 1.6)),
});
if !id.is_some_and(|id| st.selection.shapes.contains(&id)) {
self.select(|_, sel| {
sel.text = None;
sel.cells = None;
sel.shapes = id.into_iter().collect();
})?;
}
if let Some((vertex, segment)) = picked {
// A control handle belongs to the picked point: its menu stays.
if !matches!(hit, Some(Hit::Handle { .. })) {
self.tool.edit_vertex = vertex;
}
self.tool.edit_segment = segment.map(|(path, seg, _)| (path, seg));
self.tool.edit_segment_t = segment.map(|(_, _, t)| t);
}
let st = self.doc()?;
Ok(json!({
"hit": serde_json::to_value(&hit).unwrap_or_default(),
"selected": st.selection.shapes,
"editing": st.selection.text.is_some(),
}))
}
/// A click at `p` inside the text being edited: the caret goes there (`extend`: the selection
/// grows to it). In a table being edited, a click in another cell moves the caret into that
/// cell. Returns the edited shape and the caret, or `None` when `p` isn't in the edited text
/// (or is on a handle or a table border).
pub(super) fn place_caret(&mut self, p: Point, tol: f64, extend: bool) -> Result<Option<(ShapeId, (usize, usize))>> {
let st = self.doc()?;
let Some(mut t) = st.selection.text.clone().filter(|t| !t.notes) else { return Ok(None) };
let Some(sh) = st.shape(t.shape) else { return Ok(None) };
// A text box: in its own (rotated) frame, short of the band that moves it (`Hit::Frame`).
// A table cell: anywhere over the table.
let inside = match t.cell {
None => edited_box(st).is_some_and(|e| edit_zone(&e.xfrm, p, tol) != EditZone::Outside),
Some(_) => xfrm_of(&st.doc, &st.selection, sh).bounds().inflate(tol, tol).contains(p),
};
// A shape in front of the edited text takes the click, as in PowerPoint (#931).
let in_front = shape_in_front(st, st.shapes(), t.shape, p, tol, 0);
if !inside
|| in_front
|| matches!(
self.hit_test(p, tol),
Some(
Hit::Resize { .. }
| Hit::Rotate { .. }
| Hit::Adjust { .. }
| Hit::Warp { .. }
| Hit::Frame { .. }
| Hit::TableColumn { .. }
| Hit::TableRow { .. }
)
)
{
return Ok(None);
}
let old_cell = t.cell;
if old_cell.is_some()
&& let Some(f) = cmd::table::table_frame(st, sh)
{
t.cell = f.cell_at(f.local(p)).or(old_cell);
}
let pos = cmd::text::text_pos_at(st, &t, p);
// A selection can't span cells: a click in another cell starts afresh.
let extend = extend && t.cell == old_cell;
let cell = t.cell;
self.select(|_, sel| {
if let Some(tt) = sel.text.as_mut() {
tt.cell = cell;
tt.caret = pos;
if !extend {
tt.anchor = pos;
}
}
})?;
Ok(Some((t.shape, pos)))
}
/// Start editing shape `id`'s text with the caret at slide point `p`; for a table, in the cell
/// at `p`. Returns whether editing started.
pub(super) fn edit_text_at(&mut self, id: ShapeId, p: Point) -> Result<bool> {
let st = self.doc()?;
let Some(sh) = st.shape(id) else { return Ok(false) };
let params = match cmd::table::table_frame(st, sh) {
Some(f) => {
let Some((r, c)) = f.cell_at(f.local(p)) else { return Ok(false) };
let pos = cmd::text::text_pos_at(st, &TextSel { shape: id, cell: Some((r, c)), ..Default::default() }, p);
json!({"id": id, "cell": [r, c], "at": [pos.0, pos.1]})
}
None => {
let pos = text_pos(st, sh, p);
json!({"id": id, "at": [pos.0, pos.1]})
}
};
self.execute("text.edit", &params)?;
Ok(true)
}
/// Press on a table border: dragging it resizes the column or row. Editing a cell of the
/// table goes on; otherwise the table becomes the selection.
pub(super) fn begin_table_border(&mut self, id: ShapeId, column: bool, index: usize, p: Point) -> Result<()> {
let st = self.doc()?;
let Some(f) = st.shape(id).and_then(|sh| cmd::table::table_frame(st, sh)) else { return Ok(()) };
let size = if column { f.cols.get(index) } else { f.heights.get(index) }.copied().unwrap_or(0.0);
let (start, frame) = (f.local(p), f.affine);
if st.selection.text.as_ref().is_none_or(|t| t.shape != id) {
self.select(|_, sel| {
sel.shapes = vec![id];
sel.text = None;
sel.cells = None;
})?;
}
self.tool.drag = Some(Drag::TableBorder { id, column, index, start, size, frame, moved: false });
Ok(())
}
}