mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 22:36:51 -06:00
The engine notes when a drag has only moved the selected layers by one offset (Interaction::moved_by); the canvas keeps the outlines, box and slots from the drag's start document and shifts them, the selection spacing and the Design panel's values (but the position fields) come from that document, and the Export section's values are kept. Dragging 648 layers: UI pass 2.10 to 1.44 ms a frame.
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -232,6 +232,8 @@ impl Session {
|
||||
let mut in_flow = false;
|
||||
// Placed in a row or column: (is it a column, its gap) for the insertion line.
|
||||
let mut slot_axis: Option<(bool, f64)> = None;
|
||||
// Only the selected layers moved, each by `d` (see `Interaction::moved_by`).
|
||||
let mut shifted = !dup;
|
||||
self.preview(|doc, sel| {
|
||||
let mut moving = ids.clone();
|
||||
if dup {
|
||||
@@ -288,6 +290,7 @@ impl Session {
|
||||
}
|
||||
// A free move. Layers dragged out of an auto layout flow leave it (onto the page
|
||||
// while dragging; the drop puts them in the frame under the pointer).
|
||||
shifted &= only_shifted(doc, &moving);
|
||||
for m in &moving {
|
||||
let in_al = doc.node(*m).and_then(|n| n.parent).and_then(|p| doc.node(p)).is_some_and(|p| p.layout.is_some());
|
||||
if flow && in_al {
|
||||
@@ -298,6 +301,9 @@ impl Session {
|
||||
drop_target = (flow && target != page).then_some(target);
|
||||
Ok(())
|
||||
})?;
|
||||
if let Some(it) = self.ds_mut()?.interaction.as_mut() {
|
||||
it.moved_by = (shifted && !in_flow).then_some((d.x, d.y));
|
||||
}
|
||||
let placed = if dup { self.selection() } else { ids };
|
||||
// Snapping guides mean nothing while the layout places the layers; with Alt held,
|
||||
// live distances to the neighbours replace them.
|
||||
@@ -628,3 +634,21 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Moving `moving` by the same offset moves nothing else and changes nothing about them but where
|
||||
/// they are: none is inside another (it would move twice) or placed by its parent's auto layout
|
||||
/// (the layout would put it back, or close the gap it leaves).
|
||||
fn only_shifted(doc: &Document, moving: &[NodeId]) -> bool {
|
||||
let set: std::collections::HashSet<NodeId> = moving.iter().copied().collect();
|
||||
moving.iter().all(|m| {
|
||||
let Some(n) = doc.node(*m) else { return false };
|
||||
let in_flow = n.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.layout.is_some()) && !n.child_layout.absolute;
|
||||
let mut up = n.parent;
|
||||
let mut nested = false;
|
||||
while let Some(p) = up {
|
||||
nested |= set.contains(&p);
|
||||
up = doc.node(p).and_then(|x| x.parent);
|
||||
}
|
||||
!in_flow && !nested
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ pub struct Interaction {
|
||||
pub label: String,
|
||||
pub doc: Arc<Document>,
|
||||
pub selection: Vec<NodeId>,
|
||||
/// The gesture so far only moved the selected layers by this much (page units) from `doc`:
|
||||
/// their outlines, box and everything else that doesn't depend on where they are can follow
|
||||
/// by shifting what was worked out from `doc` (#1571). `None` for any other change.
|
||||
pub moved_by: Option<(f64, f64)>,
|
||||
}
|
||||
|
||||
/// The text being edited on the canvas.
|
||||
|
||||
@@ -399,6 +399,8 @@ mod measure_tests;
|
||||
#[cfg(test)]
|
||||
mod menu_gaps_tests;
|
||||
#[cfg(test)]
|
||||
mod moved_by_tests;
|
||||
#[cfg(test)]
|
||||
mod nested_override_tests;
|
||||
#[cfg(test)]
|
||||
mod node_find_tests;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! `Interaction::moved_by` (#1571): set while a drag has only moved the selected layers, each by the
|
||||
//! same offset from the document the gesture started from, so the UI can shift what it worked out
|
||||
//! from that document; `None` for anything else (a layer in an auto layout flow, a copy made with
|
||||
//! Alt), and gone with the gesture.
|
||||
|
||||
use figcraft_doc::NodeId;
|
||||
use figcraft_geom::Point;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
use crate::params::id_of;
|
||||
use crate::tools::{PointerEvent, PointerKind};
|
||||
|
||||
fn rect(s: &mut Session, parent: Option<NodeId>, x: f64, y: f64) -> NodeId {
|
||||
let mut p = json!({"x": x, "y": y, "width": 40, "height": 40});
|
||||
if let Some(parent) = parent {
|
||||
p["parent"] = json!(parent);
|
||||
}
|
||||
id_of(&s.execute("shape.rectangle", &p).unwrap()["id"]).unwrap()
|
||||
}
|
||||
|
||||
fn ev(kind: PointerKind, x: f64, y: f64, alt: bool) -> PointerEvent {
|
||||
PointerEvent { alt, ..PointerEvent::at(kind, x, y, 1.0) }
|
||||
}
|
||||
|
||||
/// Press at `from` and move to `to`, holding the button: what the interaction says of it.
|
||||
fn moved_by(s: &mut Session, from: (f64, f64), to: (f64, f64), alt: bool) -> Option<(f64, f64)> {
|
||||
s.pointer(&ev(PointerKind::Down, from.0, from.1, alt)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, (from.0 + to.0) / 2.0, (from.1 + to.1) / 2.0, alt)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Move, to.0, to.1, alt)).unwrap();
|
||||
s.ds().unwrap().interaction.as_ref().and_then(|it| it.moved_by)
|
||||
}
|
||||
|
||||
fn release(s: &mut Session, at: (f64, f64)) {
|
||||
s.pointer(&ev(PointerKind::Up, at.0, at.1, false)).unwrap();
|
||||
}
|
||||
|
||||
fn world(s: &Session, id: NodeId) -> Point {
|
||||
let t = s.doc().unwrap().world_transform(id).translation();
|
||||
Point::new(t.x, t.y)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_plain_move_says_how_far_and_matches_the_document() {
|
||||
let mut s = Session::new();
|
||||
let a = rect(&mut s, None, 0.0, 0.0);
|
||||
let b = rect(&mut s, None, 100.0, 0.0);
|
||||
s.execute("select.set", &json!({"ids": [a, b]})).unwrap();
|
||||
let (before_a, before_b) = (world(&s, a), world(&s, b));
|
||||
let d = moved_by(&mut s, (20.0, 20.0), (57.0, 81.0), false).expect("a plain move");
|
||||
assert_eq!(d, (37.0, 61.0));
|
||||
for (id, was) in [(a, before_a), (b, before_b)] {
|
||||
assert_eq!(world(&s, id), Point::new(was.x + d.0, was.y + d.1), "each layer moved by it");
|
||||
}
|
||||
release(&mut s, (57.0, 81.0));
|
||||
assert!(s.ds().unwrap().interaction.is_none(), "gone with the gesture");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layer_selected_with_its_frame_moves_with_it_once() {
|
||||
let mut s = Session::new();
|
||||
let g = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 200, "height": 200})).unwrap()["id"]).unwrap();
|
||||
let inner = rect(&mut s, Some(g), 20.0, 20.0);
|
||||
s.execute("select.set", &json!({"ids": [g, inner]})).unwrap();
|
||||
let was = world(&s, inner);
|
||||
let d = moved_by(&mut s, (25.0, 25.0), (80.0, 90.0), false).expect("the frame moves, and the layer in it with it");
|
||||
assert_eq!(world(&s, inner), Point::new(was.x + d.0, was.y + d.1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_moves_say_nothing() {
|
||||
// A layer placed by its frame's auto layout.
|
||||
let mut s = Session::new();
|
||||
let f = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 300, "height": 300})).unwrap()["id"]).unwrap();
|
||||
let a = rect(&mut s, Some(f), 10.0, 10.0);
|
||||
rect(&mut s, Some(f), 60.0, 10.0);
|
||||
s.execute("layout.add", &json!({"ids": [f]})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
let at = world(&s, a);
|
||||
assert_eq!(moved_by(&mut s, (at.x + 5.0, at.y + 5.0), (at.x + 9.0, at.y + 200.0), false), None, "in an auto layout flow");
|
||||
release(&mut s, (at.x + 9.0, at.y + 200.0));
|
||||
// A copy dragged out with Alt.
|
||||
let mut s = Session::new();
|
||||
let a = rect(&mut s, None, 0.0, 0.0);
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
assert_eq!(moved_by(&mut s, (20.0, 20.0), (90.0, 20.0), true), None, "a copy");
|
||||
}
|
||||
@@ -57,7 +57,7 @@ impl Session {
|
||||
return Err(EngineError::Disabled(label.to_string(), PREVIEW_READ_ONLY.into()));
|
||||
}
|
||||
if ds.interaction.is_none() {
|
||||
ds.interaction = Some(Interaction { label: label.to_string(), doc: ds.doc.clone(), selection: ds.selection.clone() });
|
||||
ds.interaction = Some(Interaction { label: label.to_string(), doc: ds.doc.clone(), selection: ds.selection.clone(), moved_by: None });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,7 +73,10 @@ pub(super) fn quad(painter: &egui::Painter, q: [Pos2; 4], s: Stroke) {
|
||||
/// content).
|
||||
pub(super) fn slot_badges(app: &FigApp, painter: &egui::Painter, doc: &Arc<Document>, sel: &[NodeId], drop: Option<NodeId>) {
|
||||
let key = [drop.map_or(0, |d| d.0.wrapping_add(1))];
|
||||
let slots = crate::selection_cache::cached(painter.ctx(), "slots", doc, sel, &key, || slots_shown(doc, sel, drop));
|
||||
// Which slots show doesn't change while the selection is only moved: the gesture's start keeps it.
|
||||
let base = app.session.ds().ok().and_then(crate::selection_cache::moved).map(|(b, _)| b);
|
||||
let from = base.as_ref().unwrap_or(doc);
|
||||
let slots = crate::selection_cache::cached(painter.ctx(), "slots", from, sel, &key, || slots_shown(from, sel, drop));
|
||||
for (id, name) in slots {
|
||||
let Some(q) = node_quad(app, doc, id) else { continue };
|
||||
if !Rect::from_points(&q).intersects(app.canvas.rect) {
|
||||
|
||||
@@ -268,6 +268,25 @@ pub(super) fn selection_geometry(app: &FigApp, painter: &egui::Painter, doc: &Ar
|
||||
})
|
||||
}
|
||||
|
||||
/// [`selection_geometry`] as the canvas shows it now, and how far to shift it (on screen, on the
|
||||
/// page): while a gesture has only moved the selection, the geometry where it started (kept for
|
||||
/// the whole gesture) shifted by the move ([`crate::selection_cache::moved`]).
|
||||
fn shown_geometry(
|
||||
app: &FigApp,
|
||||
painter: &egui::Painter,
|
||||
ds: &figcraft_engine::DocState,
|
||||
doc: &Arc<Document>,
|
||||
sel: &[NodeId],
|
||||
) -> (SelectionGeometry, egui::Vec2, figcraft_geom::Vec2) {
|
||||
match crate::selection_cache::moved(ds) {
|
||||
Some((base, (dx, dy))) => {
|
||||
let on_screen = app.to_screen(dx, dy) - app.to_screen(0.0, 0.0);
|
||||
(selection_geometry(app, painter, &base, sel), on_screen, figcraft_geom::Vec2::new(dx, dy))
|
||||
}
|
||||
None => (selection_geometry(app, painter, doc, sel), egui::Vec2::ZERO, figcraft_geom::Vec2::ZERO),
|
||||
}
|
||||
}
|
||||
|
||||
/// The selection's outlines, transform box and handles; returns the size label's box.
|
||||
fn selection_overlay(
|
||||
app: &FigApp,
|
||||
@@ -278,9 +297,16 @@ fn selection_overlay(
|
||||
zoom: f64,
|
||||
) -> Option<Rect> {
|
||||
let mut size_label = None;
|
||||
let geo = selection_geometry(app, painter, doc, sel);
|
||||
let (geo, shift, page_shift) = shown_geometry(app, painter, ds, doc, sel);
|
||||
let col = geo.col;
|
||||
painter.extend(geo.outlines.iter().cloned());
|
||||
if shift == egui::Vec2::ZERO {
|
||||
painter.extend(geo.outlines.iter().cloned());
|
||||
} else {
|
||||
painter.extend(geo.outlines.iter().cloned().map(|mut s| {
|
||||
s.translate(shift);
|
||||
s
|
||||
}));
|
||||
}
|
||||
// Constraint guides belong to the normal selection overlay: not while the Shape Builder
|
||||
// reshapes by regions, nor while measuring to another layer (Alt), whose lines take over.
|
||||
if app.session.tool.tool != ToolId::ShapeBuilder && app.session.tool.overlay.measure_box.is_none() {
|
||||
@@ -288,7 +314,7 @@ fn selection_overlay(
|
||||
}
|
||||
// The shape builder reshapes by regions: no transform box.
|
||||
if app.session.tool.tool != ToolId::ShapeBuilder
|
||||
&& let Some(b) = geo.bbox
|
||||
&& let Some(b) = geo.bbox.map(|b| b.map(|p| p + page_shift))
|
||||
&& let Some((w, h)) = geo.size
|
||||
{
|
||||
let is_line = sel.len() == 1 && matches!(sel.first().and_then(|i| doc.node(*i)).map(|n| &n.kind), Some(NodeKind::Line));
|
||||
|
||||
@@ -13,6 +13,7 @@ use serde_json::{Value, json};
|
||||
use super::overlays::{SelectionGeometry, overlays, selection_geometry};
|
||||
use crate::theme::{Tokens, set_canvas_page};
|
||||
use crate::{FigApp, NoServices};
|
||||
use figcraft_engine::tools::{PointerEvent, PointerKind};
|
||||
|
||||
fn id(v: Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
@@ -176,3 +177,63 @@ fn the_position_fields_follow_edits_and_the_selection() {
|
||||
app.set_props(&[ids[1]], json!({"x": 90}));
|
||||
assert_eq!(x(&app), Some(90.0), "an edit");
|
||||
}
|
||||
|
||||
/// The visual box of every shape one frame of the canvas overlays draws, in drawing order.
|
||||
fn boxes(app: &FigApp, ctx: &Context) -> Vec<Rect> {
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| {
|
||||
overlays(app, &ui.ctx().layer_painter(LayerId::background()), &Tokens::light());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
out.shapes.iter().map(|s| s.shape.visual_bounding_rect()).collect()
|
||||
}
|
||||
|
||||
/// A drag of the selection held at (`x`, `y`) on the page, from (10, 10); snapping is off (Cmd).
|
||||
fn drag_to(app: &mut FigApp, x: f64, y: f64) {
|
||||
let ev = |kind, x, y| PointerEvent { cmd: true, ..PointerEvent::at(kind, x, y, 1.0) };
|
||||
if app.session.ds().unwrap().interaction.is_none() {
|
||||
app.session.pointer(&ev(PointerKind::Down, 10.0, 10.0)).unwrap();
|
||||
}
|
||||
app.session.pointer(&ev(PointerKind::Move, x, y)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_move_shifts_the_geometry_kept_from_where_it_started() {
|
||||
let (mut app, ctx, _) = setup();
|
||||
let start = app.session.ds().unwrap().doc.clone();
|
||||
drag_to(&mut app, 47.0, 71.0);
|
||||
assert_eq!(app.session.ds().unwrap().interaction.as_ref().and_then(|i| i.moved_by), Some((37.0, 61.0)));
|
||||
let moving = boxes(&app, &ctx);
|
||||
let first = selection_geometry(&app, &Painter::new(ctx.clone(), LayerId::background(), Rect::EVERYTHING), &start, &app.session.selection());
|
||||
drag_to(&mut app, 57.0, 91.0);
|
||||
let again = boxes(&app, &ctx);
|
||||
let kept_since = selection_geometry(&app, &Painter::new(ctx.clone(), LayerId::background(), Rect::EVERYTHING), &start, &app.session.selection());
|
||||
assert!(kept(&first, &kept_since), "the geometry where the move started is kept for it");
|
||||
// What shows is what the moved document shows when nothing is kept, to a hundredth of a point.
|
||||
let mut fresh = FigApp::new(Session::new(), Box::new(NoServices));
|
||||
fresh.canvas.rect = app.canvas.rect;
|
||||
{
|
||||
let (ds, now) = (fresh.session.ds_mut().unwrap(), app.session.ds().unwrap());
|
||||
(ds.doc, ds.selection, ds.view) = (now.doc.clone(), now.selection.clone(), now.view.clone());
|
||||
}
|
||||
let ctx2 = Context::default();
|
||||
set_canvas_page(&ctx2, Color32::WHITE);
|
||||
let want = boxes(&fresh, &ctx2);
|
||||
assert_eq!(again.len(), want.len());
|
||||
for (got, want) in again.iter().zip(&want) {
|
||||
assert!((got.min - want.min).length() < 0.01 && (got.max - want.max).length() < 0.01, "{got:?} != {want:?}");
|
||||
}
|
||||
assert_ne!(moving, again, "the second move shows further on");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_gesture_start_stands_in_only_while_the_selection_is_only_moved() {
|
||||
let (mut app, _, _) = setup();
|
||||
let start = app.session.ds().unwrap().doc.clone();
|
||||
assert!(crate::selection_cache::moved(app.session.ds().unwrap()).is_none(), "no gesture");
|
||||
drag_to(&mut app, 30.0, 10.0);
|
||||
let (base, d) = crate::selection_cache::moved(app.session.ds().unwrap()).expect("a plain move");
|
||||
assert!(Arc::ptr_eq(&base, &start) && d == (20.0, 0.0));
|
||||
// The selection changing under the gesture: the start no longer stands in.
|
||||
app.session.ds_mut().unwrap().selection.pop();
|
||||
assert!(crate::selection_cache::moved(app.session.ds().unwrap()).is_none());
|
||||
}
|
||||
|
||||
@@ -28,13 +28,16 @@ struct LayoutCache {
|
||||
/// refreshing next frame after an edit and reusing an unchanged document snapshot.
|
||||
fn layout(app: &FigApp, ctx: &egui::Context, ids: &[NodeId]) -> Arc<SelectionSpacing> {
|
||||
let Ok(ds) = app.session.ds() else { return Arc::new(SelectionSpacing::default()) };
|
||||
// The gaps between layers moved together stay as they were: while a gesture only moves the
|
||||
// selection, they come from where it started.
|
||||
let doc = crate::selection_cache::moved(ds).filter(|_| ds.selection == ids).map_or_else(|| ds.doc.clone(), |(b, _)| b);
|
||||
let key = Id::unique("selection_spacing_layout");
|
||||
let frame = ctx.cumulative_frame_nr();
|
||||
if let Some(mut cache) = ctx.data(|d| d.get_temp::<LayoutCache>(key))
|
||||
&& cache.document == ds.uid
|
||||
&& cache.page == ds.page
|
||||
&& cache.ids == ids
|
||||
&& (cache.frame == frame || Arc::ptr_eq(&cache.doc, &ds.doc))
|
||||
&& (cache.frame == frame || Arc::ptr_eq(&cache.doc, &doc))
|
||||
{
|
||||
if cache.frame != frame {
|
||||
cache.frame = frame;
|
||||
@@ -44,13 +47,10 @@ fn layout(app: &FigApp, ctx: &egui::Context, ids: &[NodeId]) -> Arc<SelectionSpa
|
||||
}
|
||||
// Panel, input and drawing share one calculation per frame. If a command changes the
|
||||
// document during this frame, refresh next frame; unchanged snapshots also reuse it then.
|
||||
let layout = Arc::new(selection_spacing(&ds.doc, ids));
|
||||
let reorder = ReorderLayout::new(&ds.doc, ids).map(Arc::new);
|
||||
let layout = Arc::new(selection_spacing(&doc, ids));
|
||||
let reorder = ReorderLayout::new(&doc, ids).map(Arc::new);
|
||||
ctx.data_mut(|d| {
|
||||
d.insert_temp(
|
||||
key,
|
||||
LayoutCache { document: ds.uid, page: ds.page, doc: ds.doc.clone(), ids: ids.to_vec(), frame, layout: layout.clone(), reorder },
|
||||
);
|
||||
d.insert_temp(key, LayoutCache { document: ds.uid, page: ds.page, doc, ids: ids.to_vec(), frame, layout: layout.clone(), reorder });
|
||||
});
|
||||
layout
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! change runs an `export.*` or `file.export*` command.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Align2, Color32, CornerRadius, Id, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use figcraft_doc::{Document, ExportFormat, ExportSetting, ExportSize, NodeId, NodeKind, PrintSettings, VdCompat};
|
||||
@@ -23,12 +24,39 @@ const PREVIEW_REFRESH: f64 = 0.3;
|
||||
const PREVIEW_H: f32 = 160.0;
|
||||
|
||||
/// The Export section for the selected layers.
|
||||
pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
/// What the Export section shows of the selection, kept while the document and the selection
|
||||
/// stay the same ([`crate::selection_cache`]).
|
||||
#[derive(Clone)]
|
||||
struct Exports {
|
||||
/// The settings every selected layer has (none when they differ: `mixed`).
|
||||
list: Vec<ExportSetting>,
|
||||
mixed: bool,
|
||||
/// Every selected layer is a text layer.
|
||||
text: bool,
|
||||
/// The layers an export writes, the first one's preview, and the button's label.
|
||||
layers: BTreeSet<NodeId>,
|
||||
first: Option<NodeId>,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
fn exports(doc: &Document, sel: &[NodeId]) -> Exports {
|
||||
let shared = common(doc, sel, |n| n.exports.clone());
|
||||
let jobs = export_jobs(doc, sel);
|
||||
let layers: BTreeSet<NodeId> = jobs.iter().map(|j| j.id).collect();
|
||||
Exports {
|
||||
mixed: shared.is_none() && sel.len() > 1,
|
||||
list: shared.unwrap_or_default(),
|
||||
text: sel.iter().all(|i| matches!(doc.node(*i).map(|n| &n.kind), Some(NodeKind::Text(_)))),
|
||||
first: jobs.first().map(|j| j.id),
|
||||
name: layers.first().and_then(|i| doc.node(*i)).map(|n| n.name.clone()),
|
||||
layers,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, sel: &[NodeId]) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ids = sel.to_vec();
|
||||
let shared = common(doc, sel, |n| n.exports.clone());
|
||||
let mixed = shared.is_none() && sel.len() > 1;
|
||||
let list = shared.unwrap_or_default();
|
||||
let Exports { list, mixed, text, layers, first, name } = crate::selection_cache::cached(ui.ctx(), "exports", doc, sel, &[], || exports(doc, sel));
|
||||
let tip = if mixed { tr("Replace mixed export settings") } else { tr("Add export setting") };
|
||||
if widgets::section_header(ui, tr("Export"), &[(Icon::Plus, tip)], list.is_empty()).is_some() {
|
||||
if mixed {
|
||||
@@ -40,20 +68,13 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
if mixed {
|
||||
widgets::note(ui, tr("Click + to replace mixed content"), t.text_secondary);
|
||||
}
|
||||
let text = sel.iter().all(|i| matches!(doc.node(*i).map(|n| &n.kind), Some(NodeKind::Text(_))));
|
||||
for (i, e) in list.iter().enumerate() {
|
||||
setting_row(app, ui, &ids, i, e, text);
|
||||
}
|
||||
let jobs = export_jobs(doc, sel);
|
||||
if jobs.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(first) = jobs.first().map(|j| j.id) {
|
||||
preview(app, ui, doc, first);
|
||||
}
|
||||
let layers: BTreeSet<NodeId> = jobs.iter().map(|j| j.id).collect();
|
||||
let label = match (layers.len(), layers.first().and_then(|i| doc.node(*i))) {
|
||||
(1, Some(n)) => trf("Export {name}", &[("name", &n.name)]),
|
||||
let Some(first) = first else { return };
|
||||
preview(app, ui, doc, first);
|
||||
let label = match (layers.len(), name) {
|
||||
(1, Some(name)) => trf("Export {name}", &[("name", &name)]),
|
||||
(k, _) => trn("Export {n} layers", k, &[]),
|
||||
};
|
||||
if wide_button(ui, &label) {
|
||||
|
||||
@@ -248,58 +248,61 @@ fn design(app: &mut FigApp, ui: &mut Ui) {
|
||||
let Ok(ds) = app.session.ds() else { return };
|
||||
// The panel shows the document itself, not a live text preview over it.
|
||||
let doc = ds.text_preview.clone().unwrap_or_else(|| ds.doc.clone());
|
||||
// While a gesture only moves the selection, nothing the sections show changes but where it
|
||||
// is: they show the document it started from, so what they keep stays kept (#1571).
|
||||
let shown = crate::selection_cache::moved(ds).filter(|_| ds.text_preview.is_none()).map_or_else(|| doc.clone(), |(b, _)| b);
|
||||
let sel = ds.selection.clone();
|
||||
let vector_edit = ds.vector_edit.clone();
|
||||
if sel.is_empty() {
|
||||
page_section(app, ui, &doc);
|
||||
return;
|
||||
}
|
||||
header(app, ui, &doc, &sel);
|
||||
header(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
// Editing a vector with points selected, the points' fields stand in for the layer's position
|
||||
// and dimensions.
|
||||
let points_selected = vector_edit.as_ref().is_some_and(|ve| !ve.vertices.is_empty() || !ve.segments.is_empty());
|
||||
let vector_edit_none = vector_edit.is_none();
|
||||
match vector_edit {
|
||||
Some(ve) => crate::vector_panel::points(app, ui, &doc, &ve),
|
||||
Some(ve) => crate::vector_panel::points(app, ui, &shown, &ve),
|
||||
None => {
|
||||
crate::section_panel::section(app, ui, &doc, &sel);
|
||||
crate::section_panel::section(app, ui, &shown, &sel);
|
||||
if app.session.tool.tool == figcraft_engine::ToolId::Scale {
|
||||
crate::scale_panel::section(app, ui, &doc, &sel);
|
||||
crate::scale_panel::section(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
}
|
||||
position(app, ui, &doc, &sel);
|
||||
position(app, ui, &doc, &shown, &sel);
|
||||
}
|
||||
}
|
||||
widgets::separator(ui);
|
||||
if !points_selected {
|
||||
crate::layout_panel::layout_section(app, ui, &doc, &sel);
|
||||
crate::layout_panel::layout_section(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
}
|
||||
if vector_edit_none && arc_section(app, ui, &doc, &sel) || points_section(app, ui, &doc, &sel) {
|
||||
if vector_edit_none && arc_section(app, ui, &shown, &sel) || points_section(app, ui, &shown, &sel) {
|
||||
widgets::separator(ui);
|
||||
}
|
||||
appearance(app, ui, &doc, &sel);
|
||||
appearance(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
if variable_modes(app, ui, &doc, &sel) {
|
||||
if variable_modes(app, ui, &shown, &sel) {
|
||||
widgets::separator(ui);
|
||||
}
|
||||
if sel.iter().any(|i| matches!(doc.node(*i).map(|n| &n.kind), Some(NodeKind::Text(_)))) {
|
||||
crate::typography::typography(app, ui, &doc, &sel);
|
||||
crate::typography::typography(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
}
|
||||
paints(app, ui, &doc, &sel, true);
|
||||
paints(app, ui, &shown, &sel, true);
|
||||
widgets::separator(ui);
|
||||
paints(app, ui, &doc, &sel, false);
|
||||
paints(app, ui, &shown, &sel, false);
|
||||
widgets::separator(ui);
|
||||
effects(app, ui, &doc, &sel);
|
||||
effects(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
crate::selection_colors::section(app, ui, &doc, &sel);
|
||||
crate::selection_colors::section(app, ui, &shown, &sel);
|
||||
if sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like())) {
|
||||
grids(app, ui, &doc, &sel);
|
||||
grids(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
}
|
||||
crate::export_panel::section(app, ui, &doc, &sel);
|
||||
crate::export_panel::section(app, ui, &shown, &sel);
|
||||
}
|
||||
|
||||
/// Draw the page section: background and colour profile, local variables/styles, and the
|
||||
@@ -519,10 +522,12 @@ pub(crate) fn in_instance(ctx: &egui::Context, doc: &Arc<Document>, sel: &[NodeI
|
||||
})
|
||||
}
|
||||
|
||||
fn position(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, sel: &[NodeId]) {
|
||||
/// The Position section: its values from `doc`, what doesn't change with where the layers are from
|
||||
/// `shown` (the panel's document, see `design`).
|
||||
fn position(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, shown: &Arc<Document>, sel: &[NodeId]) {
|
||||
widgets::section_header(ui, tr("Position"), &[], false);
|
||||
// Layers inside an instance take their place and constraints from the main component.
|
||||
let nested = in_instance(ui.ctx(), doc, sel).nested;
|
||||
let nested = in_instance(ui.ctx(), shown, sel).nested;
|
||||
ui.add_enabled_ui(!nested, |ui| position_fields(app, ui, doc, sel))
|
||||
.response
|
||||
.on_disabled_hover_text(tr("Layers in an instance take their position from the main component"));
|
||||
|
||||
@@ -42,6 +42,16 @@ pub(crate) fn cached<T: Clone + Send + Sync + 'static>(
|
||||
value
|
||||
}
|
||||
|
||||
/// While a gesture has only moved the selection (`Interaction::moved_by`): the document it
|
||||
/// started from and how far it moved them (page units). What depends only on the selected layers'
|
||||
/// shapes, not where they are, is worked out from that document, so it is kept for the whole
|
||||
/// gesture instead of made again on every move (#1571).
|
||||
pub(crate) fn moved(ds: &figcraft_engine::DocState) -> Option<(Arc<Document>, (f64, f64))> {
|
||||
let it = ds.interaction.as_ref()?;
|
||||
let d = it.moved_by?;
|
||||
(it.selection == ds.selection).then(|| (it.doc.clone(), d))
|
||||
}
|
||||
|
||||
/// A screen point as key material.
|
||||
pub(crate) fn bits(p: egui::Pos2) -> [u64; 2] {
|
||||
[u64::from(p.x.to_bits()), u64::from(p.y.to_bits())]
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user