mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 04:16:46 -06:00
feat: 改进双击行为,编辑形状点时更符合平台习惯
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
scene: git_message
|
||||
---
|
||||
|
||||
在此处编写规则,自定义 AI 生成提交信息的风格。
|
||||
@@ -175,6 +175,30 @@ fn double_click_enters_one_level() {
|
||||
assert_eq!(s.ds().unwrap().vector_edit.as_ref().map(|v| v.id), Some(k.r), "double-click on a shape edits its points");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_double_click_anywhere_on_the_selected_shape_edits_its_points() {
|
||||
let mut s = Session::new();
|
||||
let r = id(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 100, "height": 100})));
|
||||
ex(&mut s, "node.set", json!({"ids": [r], "props": {"fills": []}}));
|
||||
ex(&mut s, "stroke.add", json!({"ids": [r]}));
|
||||
// Selected by its outline; the empty middle is no hit at all (the shape has no fill).
|
||||
click(&mut s, 0.0, 50.0, NONE);
|
||||
assert_eq!(s.selection(), vec![r]);
|
||||
s.pointer(&ev(PointerKind::DoubleClick, 50.0, 50.0, NONE)).unwrap();
|
||||
assert_eq!(s.ds().unwrap().vector_edit.as_ref().map(|v| v.id), Some(r), "a double-click on the shape edits its points");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_double_click_on_an_unselected_shape_edits_it_without_a_selecting_click() {
|
||||
let mut s = Session::new();
|
||||
let r = id(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 100, "height": 100})));
|
||||
ex(&mut s, "select.none", json!({}));
|
||||
// The press and release of the click that selects it, and the double-click, came in one frame:
|
||||
// only the double-click reached the tool, with nothing selected.
|
||||
s.pointer(&ev(PointerKind::DoubleClick, 50.0, 50.0, NONE)).unwrap();
|
||||
assert_eq!(s.ds().unwrap().vector_edit.as_ref().map(|v| v.id), Some(r));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marquee_selects_what_it_touches() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -1892,7 +1892,17 @@ impl Session {
|
||||
self.tool.overlay.marquee = None;
|
||||
self.cancel_interaction()?;
|
||||
}
|
||||
let Some(leaf) = self.hit(&doc, page, ev.p(), ev.tol() * 2.0) else { return Ok(()) };
|
||||
let hit_here = self.hit(&doc, page, ev.p(), ev.tol() * 2.0);
|
||||
// A double-click anywhere on the one selected shape edits its points, like Enter: also
|
||||
// inside a hollow (unfilled) shape, where the hit test finds nothing under the pointer.
|
||||
if let [id] = sel[..]
|
||||
&& doc.node(id).is_some_and(|n| n.kind.is_shape())
|
||||
&& doc.world_bounds(id).is_some_and(|b| b.contains(ev.p()))
|
||||
&& hit_here.is_none_or(|h| h == id || doc.is_ancestor(id, h))
|
||||
{
|
||||
return crate::vectoredit::begin_many(self, &[id]);
|
||||
}
|
||||
let Some(leaf) = hit_here else { return Ok(()) };
|
||||
let target = select_target(&doc, leaf, &sel, true);
|
||||
// Text: edit at the click.
|
||||
if doc.node(target).is_some_and(|n| matches!(n.kind, NodeKind::Text(_)))
|
||||
@@ -1910,8 +1920,11 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
// Shapes and vectors: enter vector edit mode (every selected shape, the clicked one active).
|
||||
if doc.node(target).is_some_and(|n| n.kind.is_shape()) && sel.contains(&target) {
|
||||
let mut ids = crate::vectoredit::enter_targets(&doc, &sel).unwrap_or_default();
|
||||
// A shape clicked on its own edits even when the click that selects it and the double-click
|
||||
// arrived in one frame (a press and a release the canvas took together), so no selection was
|
||||
// made yet; another layer's shapes are reached one level at a time, as below.
|
||||
if doc.node(target).is_some_and(|n| n.kind.is_shape()) && (sel.contains(&target) || select_target(&doc, leaf, &sel, false) == target) {
|
||||
let mut ids = if sel.contains(&target) { crate::vectoredit::enter_targets(&doc, &sel).unwrap_or_default() } else { vec![target] };
|
||||
ids.retain(|i| *i != target);
|
||||
ids.insert(0, target);
|
||||
return crate::vectoredit::begin_many(self, &ids);
|
||||
|
||||
@@ -604,7 +604,11 @@ fn input(app: &mut FigApp, ui: &mut Ui, resp: &egui::Response) -> bool {
|
||||
open_context_menu(app, pos, mods.command);
|
||||
return true;
|
||||
}
|
||||
if resp.double_clicked() {
|
||||
// A third quick click is counted as a triple, not a double (a click elsewhere in between still
|
||||
// leaves its time in the sequence): it means the same here — edit, enter, open — unless a
|
||||
// vector or text is being edited, where a triple already has its own meaning.
|
||||
let editing = app.session.ds().is_ok_and(|d| d.vector_edit.is_some() || d.text_edit.is_some());
|
||||
if resp.double_clicked() || (resp.triple_clicked() && !editing) {
|
||||
let ev = pointer_event(app, PointerKind::DoubleClick, pos, &mods);
|
||||
if let Err(e) = app.session.pointer(&ev) {
|
||||
app.toast(e.to_string());
|
||||
@@ -654,6 +658,13 @@ fn input(app: &mut FigApp, ui: &mut Ui, resp: &egui::Response) -> bool {
|
||||
}
|
||||
if let Some(k) = kind {
|
||||
send(app, k, pos, &mods);
|
||||
// The pointer also let go within this frame (a quick click, or a slow frame): the tools get
|
||||
// the release too, so the press's gesture ends — a quick click with a shape tool still
|
||||
// draws and returns to Move, and a double-click right after drawing finds the Move tool.
|
||||
if matches!(k, PointerKind::Down) && primary_released {
|
||||
app.canvas.dragging = false;
|
||||
send(app, PointerKind::Up, pos, &mods);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -2,11 +2,107 @@
|
||||
//! began is dropped, so the tool is idle once the button is up.
|
||||
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2};
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
|
||||
/// One press and release at `p`.
|
||||
fn click(u: &mut Ui, p: Pos2) {
|
||||
u.frame(vec![Event::PointerMoved(p)]);
|
||||
for pressed in [true, false] {
|
||||
u.frame(vec![Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A double-click at `p`.
|
||||
fn double_click_at(u: &mut Ui, p: Pos2) {
|
||||
u.frame(vec![Event::PointerMoved(p)]);
|
||||
for _ in 0..2 {
|
||||
for pressed in [true, false] {
|
||||
u.frame(vec![Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
|
||||
}
|
||||
}
|
||||
u.frame(vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_selected_shape_enters_vector_edit_like_enter() {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 300, "height": 200})).unwrap();
|
||||
let r: NodeId = figcraft_engine::params::id_of(&r["id"]).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
let p = u.app.to_screen(250.0, 200.0);
|
||||
// A click selects it; a double-click right after (three quick clicks) still edits its points.
|
||||
click(&mut u, p);
|
||||
assert_eq!(u.app.session.selection(), vec![r]);
|
||||
double_click_at(&mut u, p);
|
||||
assert!(u.app.session.ds().unwrap().vector_edit.is_some(), "a double-click enters vector edit");
|
||||
// Enter does the same.
|
||||
u.app.session.execute("vector.edit.end", &json!({})).unwrap();
|
||||
u.frame(vec![Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }]);
|
||||
assert!(u.app.session.ds().unwrap().vector_edit.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_slower_double_click_still_edits_the_points() {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 200})).unwrap();
|
||||
let r: NodeId = figcraft_engine::params::id_of(&r["id"]).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
let p = u.app.to_screen(200.0, 200.0);
|
||||
u.frame(vec![Event::PointerMoved(p)]);
|
||||
let press = |u: &mut Ui, pressed| {
|
||||
u.frame(vec![Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
|
||||
};
|
||||
// The first click, then 0.4 s: inside the platform's 0.5 s double-click window, past egui's
|
||||
// default 0.3 s.
|
||||
press(&mut u, true);
|
||||
press(&mut u, false);
|
||||
for _ in 0..24 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
press(&mut u, true);
|
||||
press(&mut u, false);
|
||||
assert!(u.app.session.ds().unwrap().vector_edit.is_some(), "a double-click the platform takes edits its points");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_an_unselected_shape_edits_its_points() {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 300, "height": 200})).unwrap();
|
||||
let r: NodeId = figcraft_engine::params::id_of(&r["id"]).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
let p = u.app.to_screen(250.0, 200.0);
|
||||
// Nothing is selected: the double-click selects the shape and edits its points, like Figma.
|
||||
double_click_at(&mut u, p);
|
||||
let ve = u.app.session.ds().unwrap().vector_edit.clone();
|
||||
assert_eq!(ve.map(|v| v.id), Some(r));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_that_leaves_vector_edit_does_not_keep_the_next_double_click_out() {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 300, "height": 200})).unwrap();
|
||||
let r: NodeId = figcraft_engine::params::id_of(&r["id"]).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
let shape = u.app.to_screen(250.0, 200.0);
|
||||
let empty = Pos2::new(u.app.canvas.rect.left() + 30.0, u.app.canvas.rect.bottom() - 30.0);
|
||||
double_click_at(&mut u, shape);
|
||||
assert!(u.app.session.ds().unwrap().vector_edit.is_some());
|
||||
// One click on empty canvas leaves the edit mode.
|
||||
click(&mut u, empty);
|
||||
assert!(u.app.session.ds().unwrap().vector_edit.is_none(), "a click outside leaves vector edit");
|
||||
// The next double-click edits the shape again.
|
||||
double_click_at(&mut u, shape);
|
||||
assert!(u.app.session.ds().unwrap().vector_edit.is_some(), "a double-click edits it again");
|
||||
}
|
||||
|
||||
/// Two presses and releases at the bottom left of the canvas, with `modifiers` held.
|
||||
fn double_click(u: &mut Ui, modifiers: Modifiers) {
|
||||
let pos = Pos2::new(u.app.canvas.rect.left() + 30.0, u.app.canvas.rect.bottom() - 30.0);
|
||||
|
||||
@@ -1374,6 +1374,9 @@ impl FigApp {
|
||||
// Cmd/Ctrl+= / - / 0 zoom the canvas (view.zoomIn, view.zoomOut, view.zoom100): egui's
|
||||
// own keyboard zoom would scale the whole interface on the same keys.
|
||||
ctx.options_mut(|o| o.zoom_with_keyboard = false);
|
||||
// A double-click's window is the platform's (0.5 s, Windows' default), not egui's
|
||||
// stricter 0.3 s: a double-click the OS takes must enter a shape's points too.
|
||||
ctx.memory_mut(|m| m.options.input_options.max_double_click_delay = 0.5);
|
||||
theme::install_fonts(ctx, false, 0);
|
||||
self.fonts_installed = true;
|
||||
} else if figcraft_text::system_fonts_scanned() {
|
||||
|
||||
@@ -51,8 +51,9 @@ fn drag(u: &mut Ui, from: Pos2, to: Pos2) {
|
||||
}
|
||||
|
||||
fn double_click(u: &mut Ui, p: Pos2) {
|
||||
// Well after any earlier click (a third quick click isn't a double-click).
|
||||
for _ in 0..40 {
|
||||
// Well after any earlier click (a third quick click, within twice the platform's 0.5 s
|
||||
// double-click window, isn't a double-click).
|
||||
for _ in 0..70 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
for _ in 0..2 {
|
||||
|
||||
Reference in New Issue
Block a user