mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 04:46:50 -06:00
Merge fix/679 into batch 13
This commit is contained in:
@@ -20,7 +20,8 @@ use serde_json::{Value, json};
|
||||
use crate::FigApp;
|
||||
use crate::i18n::{tr, tr_noop, trn};
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::right::{menu_item, menu_popup, row};
|
||||
use crate::list_drag::ListDrag;
|
||||
use crate::right::{menu_item, menu_popup, row, row_rect};
|
||||
use crate::theme::{COMPONENT, Tokens, semibold};
|
||||
use crate::widgets;
|
||||
|
||||
@@ -241,9 +242,11 @@ fn properties_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, owner: NodeId) {
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
// Variant properties: name, values (the matrix), add value, delete.
|
||||
for (k, vals) in &variants {
|
||||
row(ui, 32.0, |ui| {
|
||||
// Variant properties: name, values (the matrix), add value, delete. Dragged by their left
|
||||
// margin to reorder them (only among themselves: the shared ones keep the engine's order).
|
||||
let mut drag = ListDrag::in_order(ui, ("variant_props", owner));
|
||||
for (i, (k, vals)) in variants.iter().enumerate() {
|
||||
let header = row_rect(ui, 32.0, |ui| {
|
||||
let (ir, _) = ui.allocate_exact_size(vec2(14.0, 14.0), Sense::hover());
|
||||
icons::paint(ui.painter(), ir, Icon::ComponentSet, COMPONENT);
|
||||
if let Some(nn) = widgets::text_field(ui, Id::new(("vprop_name", owner, k.as_str())), k, 110.0, tr("Name")) {
|
||||
@@ -284,6 +287,12 @@ fn properties_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, owner: NodeId) {
|
||||
});
|
||||
vi += 3;
|
||||
}
|
||||
drag.row(ui, i, header, header.with_max_y(ui.cursor().min.y.max(header.max.y)), || k.clone());
|
||||
}
|
||||
if let Some((from, to)) = drag.finish(ui)
|
||||
&& let Some((k, _)) = variants.get(from)
|
||||
{
|
||||
app.run("component.editProperty", json!({"id": owner, "name": k, "index": to}));
|
||||
}
|
||||
// Shared properties.
|
||||
for (k, d) in &defs {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//! Dragging variant properties in a component set's Properties section, driven through egui
|
||||
//! headless: a property dragged by its left margin moves in one undo step (its values go with it,
|
||||
//! instances follow), its fields don't start a drag, and Escape cancels.
|
||||
|
||||
use egui::{Event, Pos2, vec2};
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
use crate::list_drag::grip_id;
|
||||
|
||||
fn id(v: serde_json::Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// A selected set whose variants list Size then State (two values each), a shared boolean "Icon",
|
||||
/// and an instance of the set. Returns (ui, set, instance).
|
||||
fn setup() -> (Ui, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
let mut variants = Vec::new();
|
||||
for (k, name) in ["Size=Small, State=Default", "Size=Large, State=Default", "Size=Small, State=Hover"].iter().enumerate() {
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": k * 60, "width": 80, "height": 40, "name": name})).unwrap());
|
||||
variants.push(id(s.execute("component.create", &json!({"ids": [f]})).unwrap()));
|
||||
}
|
||||
let set = id(s.execute("component.combineVariants", &json!({"ids": variants})).unwrap());
|
||||
s.execute("component.addProperty", &json!({"id": set, "name": "Icon", "type": "boolean", "default": true})).unwrap();
|
||||
let inst = id(s.execute("instance.create", &json!({"component": set, "x": 400, "y": 0})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [set]})).unwrap();
|
||||
(Ui::new(s), set, inst)
|
||||
}
|
||||
|
||||
/// Property names of `id` (a set or an instance), in the order they're listed.
|
||||
fn names(u: &mut Ui, id: NodeId) -> Vec<String> {
|
||||
let props = u.app.session.execute("component.properties", &json!({"id": id})).unwrap();
|
||||
props["properties"].as_array().unwrap().iter().map(|p| p["name"].as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
fn undo_len(u: &Ui) -> usize {
|
||||
u.app.session.ds().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
/// The middle of the grip of variant property `index` (as laid out last frame).
|
||||
fn grip(u: &Ui, set: NodeId, index: usize) -> Option<Pos2> {
|
||||
u.ctx.read_response(grip_id(("variant_props", set), index)).map(|r| r.rect.center())
|
||||
}
|
||||
|
||||
/// Press at `from`, move to `to` in small steps, and leave the button down.
|
||||
fn drag(u: &mut Ui, from: Pos2, to: Pos2) {
|
||||
u.button(from, true);
|
||||
for k in 1..=10 {
|
||||
u.frame(vec![Event::PointerMoved(from + (to - from) * (k as f32 / 10.0))]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_variant_property_reorders_it_in_one_step() {
|
||||
let (mut u, set, inst) = setup();
|
||||
assert_eq!(names(&mut u, set), ["Size", "State", "Icon"]);
|
||||
let (size, state) = (grip(&u, set, 0).expect("variant properties have a grip"), grip(&u, set, 1).unwrap());
|
||||
assert!(size.y < state.y, "listed in order");
|
||||
let undo = undo_len(&u);
|
||||
// State above Size.
|
||||
let to = size - vec2(0.0, 12.0);
|
||||
drag(&mut u, state, to);
|
||||
assert_eq!(names(&mut u, set), ["Size", "State", "Icon"], "nothing moves before the drop");
|
||||
u.button(to, false);
|
||||
assert_eq!(names(&mut u, set), ["State", "Size", "Icon"]);
|
||||
assert_eq!(names(&mut u, inst), ["State", "Size", "Icon"], "the instance lists the new order");
|
||||
let doc = u.app.session.doc().unwrap();
|
||||
let first = doc.node(set).unwrap().children[0];
|
||||
assert!(doc.node(first).unwrap().name.starts_with("State="), "{}", doc.node(first).unwrap().name);
|
||||
assert_eq!(undo_len(&u), undo + 1);
|
||||
assert!(u.app.ui.last_error.is_none(), "{:?}", u.app.ui.last_error);
|
||||
// Dropped where it is: nothing changes.
|
||||
u.frame(vec![]);
|
||||
u.frame(vec![]);
|
||||
let g = grip(&u, set, 0).unwrap();
|
||||
drag(&mut u, g, g + vec2(0.0, 6.0));
|
||||
u.button(g + vec2(0.0, 6.0), false);
|
||||
assert_eq!(undo_len(&u), undo + 1);
|
||||
assert!(u.app.session.undo().unwrap());
|
||||
assert_eq!(names(&mut u, set), ["Size", "State", "Icon"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_property_dropped_under_the_last_ones_values_lands_last() {
|
||||
let (mut u, set, _) = setup();
|
||||
let size = grip(&u, set, 0).unwrap();
|
||||
// State's header is 32 high and its value row 30 under it: 58 down is on the value row, below
|
||||
// the header's half-row slack, so only an area that takes in the values counts it.
|
||||
let state = u.ctx.read_response(grip_id(("variant_props", set), 1)).unwrap().rect;
|
||||
let to = Pos2::new(size.x, state.min.y + 58.0);
|
||||
drag(&mut u, size, to);
|
||||
u.button(to, false);
|
||||
assert_eq!(names(&mut u, set), ["State", "Size", "Icon"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pressing_the_name_field_does_not_start_a_drag() {
|
||||
// A guard: on main there's no grip at all; it fails if the whole row becomes a drag handle.
|
||||
let (mut u, set, _) = setup();
|
||||
let (size, state) = (grip(&u, set, 0).unwrap(), grip(&u, set, 1).unwrap());
|
||||
let undo = undo_len(&u);
|
||||
let field = size + vec2(60.0, 0.0);
|
||||
drag(&mut u, field, Pos2::new(field.x, state.y + 40.0));
|
||||
u.button(Pos2::new(field.x, state.y + 40.0), false);
|
||||
assert_eq!(names(&mut u, set), ["Size", "State", "Icon"]);
|
||||
assert_eq!(undo_len(&u), undo);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_cancels_a_property_drag_and_keeps_the_selection() {
|
||||
// A guard: the cancel comes from the shared list drag.
|
||||
let (mut u, set, _) = setup();
|
||||
let (size, state) = (grip(&u, set, 0).unwrap(), grip(&u, set, 1).unwrap());
|
||||
let undo = undo_len(&u);
|
||||
let to = size - vec2(0.0, 12.0);
|
||||
drag(&mut u, state, to);
|
||||
u.frame(vec![Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
|
||||
u.button(to, false);
|
||||
assert_eq!(names(&mut u, set), ["Size", "State", "Icon"]);
|
||||
assert_eq!(undo_len(&u), undo);
|
||||
assert_eq!(u.app.session.ds().unwrap().selection, [set], "the Escape only cancels the drag");
|
||||
}
|
||||
@@ -85,6 +85,8 @@ mod colorpicker_selection_tests;
|
||||
#[cfg(test)]
|
||||
mod comments_tests;
|
||||
#[cfg(test)]
|
||||
mod component_props_reorder_tests;
|
||||
#[cfg(test)]
|
||||
mod cursors_tests;
|
||||
#[cfg(test)]
|
||||
mod design_paste_tests;
|
||||
|
||||
Reference in New Issue
Block a user