//! Dragging layers across nested auto layout frames, and fill/hug sizing that stays coherent. use serde_json::{Value, json}; use crate::Session; use crate::params::id_of; use crate::tools::{PointerEvent, PointerKind}; use figcraft_doc::{NodeId, Sizing}; use figcraft_geom::{Point, Rect}; fn id(v: Value) -> NodeId { id_of(&v["id"]).unwrap() } fn rect(s: &mut Session, parent: Option, x: f64, y: f64, w: f64, h: f64) -> NodeId { let mut p = json!({"x": x, "y": y, "width": w, "height": h}); if let Some(parent) = parent { p["parent"] = json!(parent); } id(s.execute("shape.rectangle", &p).unwrap()) } fn frame(s: &mut Session, parent: Option, x: f64, y: f64, w: f64, h: f64) -> NodeId { let mut p = json!({"x": x, "y": y, "width": w, "height": h}); if let Some(parent) = parent { p["parent"] = json!(parent); } id(s.execute("frame.create", &p).unwrap()) } fn set(s: &mut Session, node: NodeId, props: Value) { s.execute("node.set", &json!({"id": node, "props": props})).unwrap(); } fn bounds(s: &Session, node: NodeId) -> Rect { s.doc().unwrap().world_bounds(node).unwrap() } fn kids(s: &Session, node: NodeId) -> Vec { s.doc().unwrap().node(node).unwrap().children.clone() } fn parent(s: &Session, node: NodeId) -> NodeId { s.doc().unwrap().node(node).unwrap().parent.unwrap() } fn sizing(s: &Session, node: NodeId) -> (Sizing, Sizing) { let cl = &s.doc().unwrap().node(node).unwrap().child_layout; (cl.sizing_h, cl.sizing_v) } /// Select a nested layer first (a plain click picks the frame's top-level child). fn select(s: &mut Session, node: NodeId) { s.execute("select.set", &json!({"ids": [node]})).unwrap(); } fn ev(kind: PointerKind, p: Point) -> PointerEvent { PointerEvent::at(kind, p.x, p.y, 1.0) } /// Press on `from`, move halfway and to `to`, and leave the button down (the caller checks the /// overlay, then releases with [`release`]). fn drag_to(s: &mut Session, from: Point, to: Point) { s.pointer(&ev(PointerKind::Down, from)).unwrap(); s.pointer(&ev(PointerKind::Move, from.lerp(to, 0.5))).unwrap(); s.pointer(&ev(PointerKind::Move, to)).unwrap(); } fn release(s: &mut Session, at: Point) { s.pointer(&ev(PointerKind::Up, at)).unwrap(); } fn drag(s: &mut Session, from: Point, to: Point) { drag_to(s, from, to); release(s, to); } struct Nest { outer: NodeId, a: NodeId, b: NodeId, inner: NodeId, x: NodeId, y: NodeId, } /// A vertical auto layout column (fixed 300 wide, hugging its height) holding rectangles `a` and /// `b` and a horizontal auto layout row `inner` (hugging) with rectangles `x` and `y`. fn nest(s: &mut Session) -> Nest { let outer = frame(s, None, 0.0, 0.0, 300.0, 300.0); let a = rect(s, Some(outer), 0.0, 0.0, 120.0, 40.0); let b = rect(s, Some(outer), 0.0, 50.0, 120.0, 40.0); let inner = frame(s, Some(outer), 0.0, 100.0, 200.0, 60.0); let x = rect(s, Some(inner), 0.0, 0.0, 50.0, 50.0); let y = rect(s, Some(inner), 60.0, 0.0, 50.0, 50.0); s.execute("layout.add", &json!({"ids": [inner], "direction": "horizontal"})).unwrap(); set(s, inner, json!({"padding": 10, "gap": 10})); s.execute("layout.add", &json!({"ids": [outer], "direction": "vertical"})).unwrap(); set(s, outer, json!({"padding": 10, "gap": 10, "sizingH": "fixed", "width": 300})); s.execute("select.none", &json!({})).unwrap(); Nest { outer, a, b, inner, x, y } } #[test] fn drag_into_a_nested_auto_layout_frame_takes_the_slot_there() { let mut s = Session::new(); let n = nest(&mut s); assert_eq!(kids(&s, n.outer), vec![n.a, n.b, n.inner]); let undo_depth = s.ds().unwrap().history.undo.len(); // A goes between x and y: just left of y's centre. let to = Point::new(bounds(&s, n.y).x0 + 5.0, bounds(&s, n.y).center().y); let from = bounds(&s, n.a).center(); drag_to(&mut s, from, to); let ov = &s.tool.overlay; assert_eq!(ov.drop_target, Some(n.inner), "the receiving frame is outlined"); let line = ov.insert_line.expect("insertion line"); assert!(line[0] == line[2] && line[0] < bounds(&s, n.y).x0, "a vertical line before y: {line:?}"); // The preview shows a in its slot already; the line marks it there, mid-gap (gap 10). assert_eq!(line[0], bounds(&s, n.a).x0 - 5.0); assert!(ov.guides.is_empty()); release(&mut s, to); assert_eq!(parent(&s, n.a), n.inner); assert_eq!(kids(&s, n.inner), vec![n.x, n.a, n.y]); assert_eq!(kids(&s, n.outer), vec![n.b, n.inner]); // Laid out in the row: x, then a, then y. assert!(bounds(&s, n.a).x0 > bounds(&s, n.x).x1 && bounds(&s, n.y).x0 > bounds(&s, n.a).x1); let ov = &s.tool.overlay; assert!(ov.insert_line.is_none() && ov.drop_target.is_none(), "the overlay clears on release"); // One undo step puts everything back. assert_eq!(s.ds().unwrap().history.undo.len(), undo_depth + 1); s.undo().unwrap(); assert_eq!(kids(&s, n.outer), vec![n.a, n.b, n.inner]); assert_eq!(kids(&s, n.inner), vec![n.x, n.y]); } #[test] fn drag_out_of_a_nested_frame_into_its_parent_layout() { let mut s = Session::new(); let n = nest(&mut s); // x goes between a and b in the outer column (just above b's centre, left of the row). let to = Point::new(bounds(&s, n.b).x0 + 5.0, bounds(&s, n.b).center().y - 5.0); select(&mut s, n.x); let from = bounds(&s, n.x).center(); drag_to(&mut s, from, to); assert_eq!(s.tool.overlay.drop_target, Some(n.outer)); let line = s.tool.overlay.insert_line.expect("insertion line"); assert!(line[1] == line[3], "a horizontal line in a column: {line:?}"); release(&mut s, to); assert_eq!(kids(&s, n.outer), vec![n.a, n.x, n.b, n.inner]); assert_eq!(kids(&s, n.inner), vec![n.y]); // Still within a few units of its own row (and over nothing deeper): it stays there. let row = bounds(&s, n.inner); select(&mut s, n.y); let from = bounds(&s, n.y).center(); drag(&mut s, from, Point::new(row.x1 + 4.0, row.center().y)); assert_eq!(parent(&s, n.y), n.inner); } #[test] fn the_dragged_frame_never_receives_itself_and_plain_frames_in_a_flow_are_items() { let mut s = Session::new(); let n = nest(&mut s); // Dragging the row by its child area over its own children: it reorders in the column (above a). let grab = Point::new(bounds(&s, n.inner).x0 + 3.0, bounds(&s, n.inner).y0 + 3.0); let to = Point::new(bounds(&s, n.a).x0 + 3.0, bounds(&s, n.a).y0 + 3.0); drag(&mut s, grab, to); assert_eq!(kids(&s, n.outer), vec![n.inner, n.a, n.b]); assert_eq!(kids(&s, n.inner), vec![n.x, n.y]); // A list of plain cards: dropping onto a card inserts next to it rather than into it. let list = frame(&mut s, None, 500.0, 0.0, 200.0, 400.0); let cards: Vec = (0..3).map(|i| frame(&mut s, Some(list), 0.0, i as f64 * 70.0, 150.0, 60.0)).collect(); s.execute("layout.add", &json!({"ids": [list], "direction": "vertical"})).unwrap(); s.execute("select.none", &json!({})).unwrap(); let card0 = bounds(&s, cards[0]); let grab = Point::new(bounds(&s, cards[2]).x0 + 3.0, bounds(&s, cards[2]).y0 + 3.0); let to = Point::new(card0.center().x, card0.y0 + 5.0); drag_to(&mut s, grab, to); assert_eq!(s.tool.overlay.drop_target, None, "reordering in its own list"); assert!(s.tool.overlay.insert_line.is_some()); release(&mut s, to); assert_eq!(kids(&s, list), vec![cards[2], cards[0], cards[1]]); assert!(kids(&s, cards[0]).is_empty()); // A layer inside a plain card moves within the card; dragged out over the list it joins it. let card = cards[1]; let dot = rect(&mut s, Some(card), 10.0, 10.0, 40.0, 30.0); s.ds_mut().unwrap().view.snap_objects = false; select(&mut s, dot); let start = bounds(&s, dot); drag(&mut s, start.center(), start.center() + figcraft_geom::Vec2::new(30.0, 5.0)); assert_eq!(parent(&s, dot), card); assert_eq!(bounds(&s, dot).x0, start.x0 + 30.0); let below = bounds(&s, cards[1]); let from = bounds(&s, dot).center(); drag(&mut s, from, Point::new(below.x1 + 10.0, below.y1 + 2.0)); assert_eq!(parent(&s, dot), list); } #[test] fn slots_skip_children_that_ignore_auto_layout() { let mut s = Session::new(); let row = frame(&mut s, None, 0.0, 0.0, 400.0, 100.0); let a = rect(&mut s, Some(row), 0.0, 0.0, 50.0, 50.0); let b = rect(&mut s, Some(row), 60.0, 0.0, 50.0, 50.0); s.execute("layout.add", &json!({"ids": [row], "direction": "horizontal"})).unwrap(); set(&mut s, row, json!({"sizingH": "fixed", "width": 400})); // A badge pinned at the far right, first in the layer order. let badge = rect(&mut s, Some(row), 350.0, 0.0, 20.0, 20.0); set(&mut s, badge, json!({"absolute": true})); s.execute("node.reorder", &json!({"id": badge, "index": 0})).unwrap(); let hidden = rect(&mut s, Some(row), 0.0, 0.0, 20.0, 20.0); set(&mut s, hidden, json!({"visible": false})); s.execute("node.reorder", &json!({"id": hidden, "index": 0})).unwrap(); s.execute("select.none", &json!({})).unwrap(); assert_eq!(kids(&s, row), vec![hidden, badge, a, b]); // b goes before a: the line sits at a's left edge, not at the badge or the hidden layer. let a_left = bounds(&s, a).x0; let to = Point::new(a_left + 3.0, bounds(&s, a).center().y); let from = bounds(&s, b).center(); drag_to(&mut s, from, to); let line = s.tool.overlay.insert_line.expect("insertion line"); // b now sits where a started; the line is mid-gap before it (gap 10). assert!((line[0] - (a_left - 5.0)).abs() < 1e-9, "{line:?}"); release(&mut s, to); let order: Vec = kids(&s, row).into_iter().filter(|k| *k == a || *k == b).collect(); assert_eq!(order, vec![b, a]); } #[test] fn drag_out_of_auto_layout_to_the_canvas_or_a_plain_frame() { let mut s = Session::new(); let n = nest(&mut s); set(&mut s, n.a, json!({"sizingH": "fill"})); let plain = frame(&mut s, None, 600.0, 0.0, 300.0, 300.0); s.execute("select.none", &json!({})).unwrap(); // Exact drop points (no snapping to the other layers' edges). s.ds_mut().unwrap().view.snap_objects = false; // Onto the empty canvas: a free layer on the page where it was dropped. let start = bounds(&s, n.a); let (from, to) = (start.center(), Point::new(400.0, 500.0)); drag_to(&mut s, from, to); assert!(s.tool.overlay.insert_line.is_none() && s.tool.overlay.drop_target.is_none()); release(&mut s, to); let page = s.page().unwrap(); assert_eq!(parent(&s, n.a), page); let b = bounds(&s, n.a); assert_eq!((b.x0, b.y0), (start.x0 + to.x - from.x, start.y0 + to.y - from.y)); // It filled the column; out of auto layout that becomes its fixed width. assert_eq!(sizing(&s, n.a), (Sizing::Fixed, Sizing::Fixed)); assert_eq!(b.width(), start.width()); // From the nested row into a plain frame: a positioned child at the drop point. select(&mut s, n.x); let start = bounds(&s, n.x); let (from, to) = (start.center(), Point::new(700.0, 150.0)); drag_to(&mut s, from, to); assert_eq!(s.tool.overlay.drop_target, Some(plain)); release(&mut s, to); assert_eq!(parent(&s, n.x), plain); let b = bounds(&s, n.x); assert_eq!((b.x0, b.y0), (start.x0 + to.x - from.x, start.y0 + to.y - from.y)); assert_eq!(kids(&s, n.inner), vec![n.y]); s.undo().unwrap(); assert_eq!(kids(&s, n.inner), vec![n.x, n.y], "one undo step"); } #[test] fn a_free_layer_dropped_into_auto_layout_takes_the_slot_under_the_pointer() { let mut s = Session::new(); let n = nest(&mut s); let free = rect(&mut s, None, 600.0, 600.0, 30.0, 30.0); s.execute("select.none", &json!({})).unwrap(); // Before b, not appended at the end. let to = Point::new(bounds(&s, n.b).x0 + 5.0, bounds(&s, n.b).y0 + 5.0); let from = bounds(&s, free).center(); drag_to(&mut s, from, to); assert_eq!(s.tool.overlay.drop_target, Some(n.outer)); assert!(s.tool.overlay.insert_line.is_some()); release(&mut s, to); assert_eq!(kids(&s, n.outer), vec![n.a, free, n.b, n.inner]); } #[test] fn fill_and_hug_stay_coherent() { let mut s = Session::new(); let n = nest(&mut s); // The row hugs its width: a child that fills it turns the row fixed at its current width. let w = bounds(&s, n.inner).width(); set(&mut s, n.x, json!({"sizingH": "fill"})); assert_eq!(sizing(&s, n.inner).0, Sizing::Fixed); assert_eq!(bounds(&s, n.inner).width(), w); assert_eq!(bounds(&s, n.x).width(), 50.0, "it fills what its content left"); // Back to hug: the child that filled it becomes fixed at its size. set(&mut s, n.inner, json!({"sizingH": "hug"})); assert_eq!(sizing(&s, n.x).0, Sizing::Fixed); assert_eq!(bounds(&s, n.x).width(), 50.0); // Removing auto layout: fill children become fixed, the frame's hug becomes fixed, and its // own fill in the column is kept. set(&mut s, n.y, json!({"sizingV": "fill"})); set(&mut s, n.inner, json!({"sizingH": "fill"})); s.execute("layout.remove", &json!({"ids": [n.inner]})).unwrap(); assert_eq!(sizing(&s, n.y), (Sizing::Fixed, Sizing::Fixed)); assert_eq!(sizing(&s, n.inner), (Sizing::Fill, Sizing::Fixed)); // Fill without an auto layout parent is refused. assert!(s.execute("node.set", &json!({"id": n.y, "props": {"sizingH": "fill"}})).is_err()); } #[test] fn fill_text_in_a_hugging_card_sets_the_height_in_one_step() { let mut s = Session::new(); let card = frame(&mut s, None, 0.0, 0.0, 160.0, 50.0); let t = id(s .execute("text.create", &json!({"text": "A fairly long line of text that has to wrap inside the card", "x": 0, "y": 0, "parent": card})) .unwrap()); s.execute("layout.add", &json!({"ids": [card], "direction": "vertical"})).unwrap(); set(&mut s, card, json!({"padding": 10, "sizingH": "fixed", "width": 160})); set(&mut s, t, json!({"sizingH": "fill"})); let (tb, cb) = (bounds(&s, t), bounds(&s, card)); assert_eq!(tb.width(), 140.0); // Whatever the fonts, the card hugs the wrapped text exactly. assert!((cb.height() - (tb.height() + 20.0)).abs() < 1e-6, "card {cb:?} text {tb:?}"); }