mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
Merge fix/766 into batch 16
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Frame name labels as pointer targets: a press on a label (its area as the canvas drew it,
|
||||
//! turned with a rotated frame) selects the frame and drags it in one undo step, hovering it
|
||||
//! highlights the frame, and labels of locked, hidden, deleted or other pages' frames do nothing.
|
||||
//! highlights the frame, a double-click asks the UI to rename it there, and labels of locked,
|
||||
//! hidden, deleted or other pages' frames do nothing.
|
||||
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_geom::Point;
|
||||
@@ -90,3 +91,24 @@ fn labels_of_frames_that_cant_be_picked_do_nothing() {
|
||||
let flat = [Point::new(1.0, 1.0); 4];
|
||||
assert_eq!(label_at(s.doc().unwrap(), page, &[(f, nan), (f, flat)], Point::new(1.0, 1.0)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_label_asks_to_rename_its_frame() {
|
||||
let (mut s, f, q) = setup();
|
||||
let c = center(&q);
|
||||
let before = s.doc().unwrap().world_bounds(f).unwrap();
|
||||
let undo = s.ds().unwrap().history.undo.len();
|
||||
for kind in [PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::DoubleClick] {
|
||||
s.pointer(&ev(kind, c)).unwrap();
|
||||
}
|
||||
assert_eq!(s.selection(), vec![f]);
|
||||
let asked = s.ui_requests.iter().find(|r| r.kind == "canvas.renameLabel").map(|r| id_of(&r.data["id"]));
|
||||
assert_eq!(asked, Some(Some(f)), "the UI renames the frame at its label");
|
||||
assert_eq!(s.doc().unwrap().world_bounds(f), Some(before), "not moved");
|
||||
assert_eq!(s.ds().unwrap().history.undo.len(), undo, "nothing to undo yet");
|
||||
// Other tools ignore the label.
|
||||
s.ui_requests.clear();
|
||||
s.set_tool(crate::tools::ToolId::Hand);
|
||||
s.pointer(&ev(PointerKind::DoubleClick, c)).unwrap();
|
||||
assert!(s.ui_requests.iter().all(|r| r.kind != "canvas.renameLabel"));
|
||||
}
|
||||
|
||||
@@ -1859,6 +1859,20 @@ impl Session {
|
||||
if self.tool.tool == ToolId::Pen {
|
||||
return self.pen_finish();
|
||||
}
|
||||
// A frame's or section's name label: rename it there (the canvas puts a field over it).
|
||||
// Labels come before widgets, as on a press; the second press began a move: undo it.
|
||||
if matches!(self.tool.tool, ToolId::Move | ToolId::Scale)
|
||||
&& !(ev.shift || ev.cmd || ev.alt)
|
||||
&& let Some(id) = label_at(&doc, page, &self.tool.labels, ev.p())
|
||||
{
|
||||
self.tool.gesture = Gesture::None;
|
||||
self.cancel_interaction()?;
|
||||
if sel != [id] {
|
||||
self.set_selection(vec![id]);
|
||||
}
|
||||
self.request_ui("canvas.renameLabel", serde_json::json!({"id": id}));
|
||||
return Ok(());
|
||||
}
|
||||
// Quick clicks on a widget's clickable part are clicks, one by one.
|
||||
if matches!(self.tool.tool, ToolId::Move | ToolId::Scale)
|
||||
&& !(ev.shift || ev.cmd || ev.alt)
|
||||
|
||||
@@ -242,6 +242,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
|
||||
painter.image(tex.id(), image_rect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
app.session.tool.labels = overlays(app, &painter, &t);
|
||||
crate::frame_labels::rename_field(app, ui);
|
||||
// The zoom tool's area.
|
||||
if let (Some(start), Some(p)) = (app.canvas.zoom_drag, ctx.input(|i| i.pointer.hover_pos())) {
|
||||
let area = Rect::from_two_pos(start, p);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Frame name labels on the canvas, driven through egui headless: an upright frame's label sits
|
||||
//! just above its top-left corner, a rotated frame's runs along its turned top edge, and pressing
|
||||
//! either label selects and drags its frame.
|
||||
//! either label selects and drags its frame; double-clicking one renames the frame or section there.
|
||||
|
||||
use egui::{Event, Pos2, pos2, vec2};
|
||||
use figcraft_doc::NodeId;
|
||||
@@ -79,3 +79,80 @@ fn pressing_a_rotated_label_selects_and_drags_the_frame() {
|
||||
assert!(after.x0 > before.x0 + 1.0 && after.y0 > before.y0 + 1.0, "{before:?} → {after:?}");
|
||||
assert_eq!(u.app.session.ds().unwrap().history.undo.len(), undo + 1);
|
||||
}
|
||||
|
||||
fn key(k: egui::Key) -> Vec<Event> {
|
||||
vec![
|
||||
Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() },
|
||||
Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers: Default::default() },
|
||||
]
|
||||
}
|
||||
|
||||
/// Double-click `f`'s label and wait for the field to take the keyboard.
|
||||
fn double_click_label(u: &mut Ui, f: NodeId) {
|
||||
let at = mid(&label(u, f));
|
||||
for _ in 0..2 {
|
||||
u.button(at, true);
|
||||
u.button(at, false);
|
||||
}
|
||||
u.frame(vec![]);
|
||||
u.frame(vec![]);
|
||||
}
|
||||
|
||||
fn name(u: &Ui, f: NodeId) -> String {
|
||||
u.app.session.doc().unwrap().node(f).unwrap().name.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_clicking_a_label_renames_the_frame_there() {
|
||||
let (mut u, a, b) = setup();
|
||||
let before = u.app.session.doc().unwrap().world_bounds(a).unwrap();
|
||||
double_click_label(&mut u, a);
|
||||
assert_eq!(u.app.session.selection(), vec![a]);
|
||||
assert_eq!(u.app.ui.rename_label.map(|r| r.id), Some(a), "a field over the label");
|
||||
// The name starts selected: typing replaces it, Enter keeps it.
|
||||
u.frame(vec![Event::Text("Sign in".into())]);
|
||||
u.frame(key(egui::Key::Enter));
|
||||
u.frame(vec![]);
|
||||
assert_eq!(name(&u, a), "Sign in");
|
||||
assert!(u.app.ui.rename_label.is_none());
|
||||
// The double-click didn't move the frame or rename the other one.
|
||||
assert_eq!(u.app.session.doc().unwrap().world_bounds(a).unwrap(), before);
|
||||
assert_eq!(name(&u, b), "Turned");
|
||||
// One undo step puts the old name back.
|
||||
u.app.session.undo().unwrap();
|
||||
assert_eq!(name(&u, a), "Upright");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_leaves_a_label_rename_with_the_old_name() {
|
||||
let (mut u, _, b) = setup();
|
||||
double_click_label(&mut u, b);
|
||||
assert_eq!(u.app.ui.rename_label.map(|r| r.id), Some(b), "rotated labels rename too");
|
||||
u.frame(vec![Event::Text("Nope".into())]);
|
||||
u.frame(key(egui::Key::Escape));
|
||||
u.frame(vec![]);
|
||||
assert_eq!(name(&u, b), "Turned");
|
||||
assert!(u.app.ui.rename_label.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_sections_label_renames_it_and_a_click_elsewhere_commits() {
|
||||
let mut s = Session::new();
|
||||
let a = id(s.execute("node.create", &json!({"type": "section", "x": 0, "y": 0, "width": 300, "height": 200, "name": "Area"})).unwrap());
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.frame(vec![]);
|
||||
let undo = u.app.session.ds().unwrap().history.undo.len();
|
||||
double_click_label(&mut u, a);
|
||||
assert_eq!(u.app.ui.rename_label.map(|r| r.id), Some(a), "a field over the section's label");
|
||||
assert_eq!(u.app.session.selection(), vec![a]);
|
||||
u.frame(vec![Event::Text("Zone".into())]);
|
||||
let at = mid(&label(&u, a));
|
||||
let away = pos2(at.x + 200.0, at.y + 500.0);
|
||||
u.button(away, true);
|
||||
u.button(away, false);
|
||||
u.frame(vec![]);
|
||||
assert_eq!(name(&u, a), "Zone", "a click on the canvas commits");
|
||||
assert!(u.app.ui.rename_label.is_none());
|
||||
assert_eq!(u.app.session.ds().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
}
|
||||
|
||||
@@ -151,6 +151,28 @@ pub fn draw(app: &FigApp, painter: &Painter, t: &Tokens, doc: &Document, id: Nod
|
||||
Some([area.left_top(), area.right_top(), area.right_bottom(), area.left_bottom()].map(|p| tl + rot * (p - tl)))
|
||||
}
|
||||
|
||||
/// The field over a frame's or section's name label being renamed (`UiState::rename_label`, from a
|
||||
/// double-click on the label): Enter or a click elsewhere renames the layer, Escape keeps the name.
|
||||
/// The rename ends when the field lets go of the keyboard or the label is gone.
|
||||
pub fn rename_field(app: &mut FigApp, ui: &mut egui::Ui) {
|
||||
let Some(r) = app.ui.rename_label else { return };
|
||||
let quad = app.session.tool.labels.iter().find(|(i, _)| *i == r.id).map(|(_, q)| q.map(|p| app.to_screen(p.x, p.y)));
|
||||
let name = app.session.doc().ok().and_then(|d| d.node(r.id)).map(|n| n.name.clone());
|
||||
let (Some(quad), Some(name)) = (quad, name) else {
|
||||
app.ui.rename_label = None;
|
||||
return;
|
||||
};
|
||||
let area = Rect::from_points(&quad);
|
||||
let font = regular(11.0);
|
||||
let text_w = ui.painter().layout_no_wrap(name.clone(), font.clone(), Color32::WHITE).size().x;
|
||||
let rect = Rect::from_min_size(pos2(area.min.x, area.max.y - 16.0), vec2((text_w + 24.0).max(area.width()).clamp(80.0, 480.0), 16.0));
|
||||
let (done, open) = crate::widgets::rename_field(ui, egui::Id::new(("frame_label_rename", r.id)), &name, rect, font, r.start);
|
||||
app.ui.rename_label = open.then_some(crate::layers::Rename { id: r.id, start: false });
|
||||
if let Some(new) = done {
|
||||
app.run("object.rename", serde_json::json!({"ids": [r.id], "name": new}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -434,6 +434,8 @@ pub struct UiState {
|
||||
/// The layer being renamed in the layers panel.
|
||||
pub rename: Option<layers::Rename>,
|
||||
pub rename_page: Option<(NodeId, String)>,
|
||||
/// The frame or section whose name label on the canvas is being renamed (a double-click on it).
|
||||
pub rename_label: Option<layers::Rename>,
|
||||
pub main_menu: bool,
|
||||
pub palette: Option<String>,
|
||||
/// The Actions palette's tab.
|
||||
@@ -591,6 +593,7 @@ impl Default for UiState {
|
||||
expanded: BTreeSet::new(),
|
||||
rename: None,
|
||||
rename_page: None,
|
||||
rename_label: None,
|
||||
main_menu: false,
|
||||
palette: None,
|
||||
palette_tab: menus::PaletteTab::All,
|
||||
@@ -965,6 +968,9 @@ impl FigApp {
|
||||
"layers.collapseAll" => layers::collapse_all(self),
|
||||
"contextMenu" => self.ui.key_menu = true,
|
||||
"selectArea" => self.start_key_box(),
|
||||
"canvas.renameLabel" => {
|
||||
self.ui.rename_label = r.data.get("id").and_then(figcraft_engine::params::id_of).map(|id| layers::Rename { id, start: true });
|
||||
}
|
||||
"layers.rename" => {
|
||||
let ids: Vec<NodeId> =
|
||||
r.data.get("ids").and_then(Value::as_array).into_iter().flatten().filter_map(figcraft_engine::params::id_of).collect();
|
||||
|
||||
@@ -198,7 +198,8 @@ A mouse gesture with the current tool. `kind`: `down`, `move`, `up`, `click`, `d
|
||||
(`x`, `y`) or screen pixels (`sx`, `sy`). `pressure` (0–1) is pen pressure (the Pencil records
|
||||
it); a drag's pressure goes from `pressure` to `pressureTo`. Returns `{selection, tool}`. With the Move and Scale
|
||||
tools, a press on a frame's or section's name label (as the canvas last drew it, along a rotated
|
||||
frame's top edge) selects and drags that frame.
|
||||
frame's top edge) selects and drags that frame; a double-click there renames the frame in place in
|
||||
the desktop app (the name field opens over the label).
|
||||
|
||||
```json
|
||||
{"id":4,"method":"engine.execute","params":{"command":"tool.set","params":{"tool":"ellipse"}}}
|
||||
|
||||
Reference in New Issue
Block a user