mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-10 20:29:33 -06:00
M8: the Design panel and the selection overlays stop going over a large selection every frame (#1571) (#1660)
This commit is contained in:
+2
-1
File diff suppressed because one or more lines are too long
@@ -376,6 +376,9 @@ mod builder_poly_tests;
|
||||
#[cfg(test)]
|
||||
mod outline_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod panel_nodes_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod selection_cache_tests;
|
||||
|
||||
|
||||
@@ -57,7 +57,12 @@ pub(super) fn overlays(app: &FigApp, painter: &egui::Painter, t: &Tokens) -> Vec
|
||||
}
|
||||
// Inside a boolean group (an operand selected or in vector edit mode): every operand's outline,
|
||||
// so the shapes making the live result stay visible while one of them changes.
|
||||
let operands = figcraft_engine::booleans::outlined_operands(&doc, &sel, ds.vector_edit.as_ref().map(|ve| ve.id));
|
||||
let editing = ds.vector_edit.as_ref().map(|ve| ve.id);
|
||||
let unmoved = crate::selection_cache::unmoved(ds, &doc);
|
||||
let key: Vec<u64> = editing.map(|e| e.0).into_iter().collect();
|
||||
let operands = crate::selection_cache::cached(painter.ctx(), "operands", &unmoved, &sel, &key, || {
|
||||
figcraft_engine::booleans::outlined_operands(&unmoved, &sel, editing)
|
||||
});
|
||||
for id in operands.iter().filter(|o| !sel.contains(o)) {
|
||||
draw_outline(app, painter, &doc, *id, stroke(outline_colour(&doc, *id, accent(painter)).gamma_multiply(0.6), 1.0));
|
||||
}
|
||||
@@ -310,7 +315,7 @@ fn selection_overlay(
|
||||
// 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() {
|
||||
constraint_lines(app, painter, doc, sel);
|
||||
constraint_lines(app, painter, ds, doc, sel);
|
||||
}
|
||||
// The shape builder reshapes by regions: no transform box.
|
||||
if app.session.tool.tool != ToolId::ShapeBuilder
|
||||
@@ -494,7 +499,17 @@ fn transform_box(
|
||||
/// short bar through the layer's middle — a horizontal one, a vertical one, or both (a cross when
|
||||
/// the layer is centred on both axes). Auto-layout frames ignore constraints, so no guides are
|
||||
/// drawn there, except for a layer that ignores the auto layout (absolute position).
|
||||
fn constraint_lines(app: &FigApp, painter: &egui::Painter, doc: &Document, sel: &[NodeId]) {
|
||||
fn constraint_lines(app: &FigApp, painter: &egui::Painter, ds: &figcraft_engine::DocState, doc: &Arc<Document>, sel: &[NodeId]) {
|
||||
// Constraints bind a layer only to a frame it sits inside: one without auto layout, or one
|
||||
// whose auto layout the layer ignores (absolute position). A move changes neither.
|
||||
let unmoved = crate::selection_cache::unmoved(ds, doc);
|
||||
let bound = crate::selection_cache::cached(painter.ctx(), "constrained", &unmoved, sel, &[], || -> Vec<NodeId> {
|
||||
let binds = |n: &figcraft_doc::Node, p: &figcraft_doc::Node| p.kind.is_frame_like() && (p.layout.is_none() || n.child_layout.absolute);
|
||||
sel.iter()
|
||||
.copied()
|
||||
.filter(|i| unmoved.node(*i).is_some_and(|n| n.parent.and_then(|p| unmoved.node(p)).is_some_and(|p| binds(n, p))))
|
||||
.collect()
|
||||
});
|
||||
let s = stroke(accent(painter), 1.0);
|
||||
let line = |a: Pos2, b: Pos2| {
|
||||
painter.add(Shape::dashed_line(&[a, b], s, 4.0, 3.0));
|
||||
@@ -508,15 +523,10 @@ fn constraint_lines(app: &FigApp, painter: &egui::Painter, doc: &Document, sel:
|
||||
painter.line_segment([pos2(c.x, c.y - half), pos2(c.x, c.y + half)], s);
|
||||
}
|
||||
};
|
||||
for &id in sel {
|
||||
for &id in &bound {
|
||||
let Some(n) = doc.node(id) else { continue };
|
||||
let Some(parent_id) = n.parent else { continue };
|
||||
let Some(parent) = doc.node(parent_id) else { continue };
|
||||
// Constraints bind a layer only to a frame it sits inside: one without auto layout, or one
|
||||
// whose auto layout the layer ignores (absolute position).
|
||||
if !parent.kind.is_frame_like() || (parent.layout.is_some() && !n.child_layout.absolute) {
|
||||
continue;
|
||||
}
|
||||
let nw = doc.world_transform(id);
|
||||
let pw = doc.world_transform(parent_id);
|
||||
let (nw_w, nw_h) = (n.width, n.height);
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//! What a large selection's frames keep (#1571): the Design panel looks the selected layers up once
|
||||
//! per document snapshot and selection, and the canvas keeps which layers its constraint guides and
|
||||
//! boolean operand outlines go with. Each answers as the document does, after edits, another
|
||||
//! selection, a deleted layer, and for any other document.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Color32, Context, Rect, pos2, vec2};
|
||||
use figcraft_doc::{Document, NodeId, NodeKind};
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::selection_cache_tests::drawn;
|
||||
use crate::right::common;
|
||||
use crate::selection_cache::{every, show};
|
||||
use crate::theme::set_canvas_page;
|
||||
use crate::{FigApp, NoServices};
|
||||
|
||||
fn id(v: Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn rect(s: &mut Session, x: i32, parent: Option<NodeId>) -> NodeId {
|
||||
let mut p = json!({"x": x, "y": 10, "width": 40, "height": 40});
|
||||
if let Some(parent) = parent {
|
||||
p["parent"] = json!(parent);
|
||||
}
|
||||
id(s.execute("shape.rectangle", &p).unwrap())
|
||||
}
|
||||
|
||||
/// The panel's questions (the selection's common opacity, whether it is all rectangles), asked as
|
||||
/// the panel asks them.
|
||||
fn answers(doc: &Document, sel: &[NodeId]) -> (Option<u32>, bool) {
|
||||
(common(doc, sel, |n| n.opacity.to_bits()), every(doc, sel, |n| matches!(n.kind, NodeKind::Rectangle)))
|
||||
}
|
||||
|
||||
/// The same questions asked of the document directly.
|
||||
fn direct(doc: &Document, sel: &[NodeId]) -> (Option<u32>, bool) {
|
||||
let opacities: Vec<u32> = sel.iter().filter_map(|i| doc.node(*i)).map(|n| n.opacity.to_bits()).collect();
|
||||
let common = opacities.first().copied().filter(|o| opacities.iter().all(|x| x == o));
|
||||
(common, sel.iter().all(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Rectangle))))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panels_kept_layers_answer_as_the_document_does() {
|
||||
let mut s = Session::new();
|
||||
let (a, b) = (rect(&mut s, 0, None), rect(&mut s, 80, None));
|
||||
s.execute("node.set", &json!({"ids": [a], "props": {"opacity": 0.5}})).unwrap();
|
||||
let sel = vec![a, b];
|
||||
let doc = s.ds().unwrap().doc.clone();
|
||||
show(&doc, &sel);
|
||||
assert_eq!(answers(&doc, &sel), direct(&doc, &sel));
|
||||
assert_eq!(answers(&doc, &sel).0, None, "mixed");
|
||||
// An edit is a new snapshot: answered from itself, before it is shown and after.
|
||||
s.execute("node.set", &json!({"ids": [a, b], "props": {"opacity": 0.25}})).unwrap();
|
||||
let edited = s.ds().unwrap().doc.clone();
|
||||
assert!(!Arc::ptr_eq(&doc, &edited));
|
||||
assert_eq!(answers(&edited, &sel), (Some(0.25f32.to_bits()), true));
|
||||
show(&edited, &sel);
|
||||
assert_eq!(answers(&edited, &sel), (Some(0.25f32.to_bits()), true));
|
||||
assert_eq!(answers(&doc, &sel), direct(&doc, &sel), "the document shown before still answers for itself");
|
||||
// Another selection.
|
||||
s.execute("node.set", &json!({"ids": [b], "props": {"opacity": 1.0}})).unwrap();
|
||||
let doc = s.ds().unwrap().doc.clone();
|
||||
show(&doc, &sel);
|
||||
for ids in [vec![a], vec![b], vec![b, a]] {
|
||||
assert_eq!(answers(&doc, &ids), direct(&doc, &ids), "{ids:?}");
|
||||
show(&doc, &ids);
|
||||
assert_eq!(answers(&doc, &ids), direct(&doc, &ids), "{ids:?} shown");
|
||||
}
|
||||
// A layer that isn't in the document: `every` says no, `common` goes by the others.
|
||||
s.execute("edit.delete", &json!({"ids": [b]})).unwrap();
|
||||
let doc = s.ds().unwrap().doc.clone();
|
||||
show(&doc, &sel);
|
||||
assert_eq!(answers(&doc, &sel), (Some(0.25f32.to_bits()), false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_kept_selection_never_answers_for_another_document() {
|
||||
let mut s = Session::new();
|
||||
let a = rect(&mut s, 0, None);
|
||||
let sel = vec![a];
|
||||
let base = s.doc().unwrap().clone();
|
||||
let first = Arc::new(base.clone());
|
||||
let kept_at = Arc::as_ptr(&first);
|
||||
show(&first, &sel);
|
||||
drop(first);
|
||||
let mut changed = base;
|
||||
changed.node_mut(a).unwrap().opacity = 0.1;
|
||||
let second = Arc::new(changed);
|
||||
assert_ne!(Arc::as_ptr(&second), kept_at, "the kept document's place stays taken while it is kept");
|
||||
assert_eq!(answers(&second, &sel), (Some(0.1f32.to_bits()), true));
|
||||
}
|
||||
|
||||
fn app_of(s: Session) -> (FigApp, Context) {
|
||||
let mut app = FigApp::new(s, Box::new(NoServices));
|
||||
app.canvas.rect = Rect::from_min_size(pos2(0.0, 0.0), vec2(800.0, 600.0));
|
||||
let ctx = Context::default();
|
||||
set_canvas_page(&ctx, Color32::WHITE);
|
||||
(app, ctx)
|
||||
}
|
||||
|
||||
/// What one frame draws, and what it draws with nothing kept.
|
||||
fn drawn_now(app: &FigApp, ctx: &Context) -> (usize, usize) {
|
||||
let fresh = Context::default();
|
||||
set_canvas_page(&fresh, Color32::WHITE);
|
||||
(drawn(app, ctx), drawn(app, &fresh))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constraint_guides_follow_the_layers_frame() {
|
||||
let mut s = Session::new();
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 300, "height": 300})).unwrap());
|
||||
let r = rect(&mut s, 40, Some(f));
|
||||
let page = s.page().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
let (mut app, ctx) = app_of(s);
|
||||
let (pinned, fresh) = drawn_now(&app, &ctx);
|
||||
assert_eq!(pinned, fresh);
|
||||
for (parent, what) in [(page, "on the page: no frame to pin it to"), (f, "back in its frame")] {
|
||||
app.session.execute("node.reparent", &json!({"ids": [r], "parent": parent})).unwrap();
|
||||
app.session.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
let (now, fresh) = drawn_now(&app, &ctx);
|
||||
assert_eq!(now, fresh, "{what}");
|
||||
assert_eq!(now < pinned, parent == page, "{what}");
|
||||
}
|
||||
// In an auto layout flow its constraints don't apply; ignoring the layout, they do.
|
||||
app.session.execute("layout.add", &json!({"ids": [f]})).unwrap();
|
||||
let (placed, fresh) = drawn_now(&app, &ctx);
|
||||
assert_eq!(placed, fresh, "auto layout places the layer");
|
||||
app.set_props(&[r], json!({"absolute": true}));
|
||||
let (absolute, fresh) = drawn_now(&app, &ctx);
|
||||
assert_eq!(absolute, fresh, "ignoring the auto layout");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operand_outlines_follow_the_boolean_group() {
|
||||
let mut s = Session::new();
|
||||
let ops = [rect(&mut s, 0, None), rect(&mut s, 20, None), rect(&mut s, 40, None)];
|
||||
s.execute("bool.union", &json!({"ids": ops})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [ops[0]]})).unwrap();
|
||||
let (mut app, ctx) = app_of(s);
|
||||
let (three, fresh) = drawn_now(&app, &ctx);
|
||||
assert_eq!(three, fresh);
|
||||
app.session.execute("edit.delete", &json!({"ids": [ops[2]]})).unwrap();
|
||||
app.session.execute("select.set", &json!({"ids": [ops[0]]})).unwrap();
|
||||
let (two, fresh) = drawn_now(&app, &ctx);
|
||||
assert_eq!(two, fresh, "one operand fewer to outline");
|
||||
assert!(two < three);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ fn every_input_the_geometry_shows_makes_it_again() {
|
||||
}
|
||||
|
||||
/// How many shapes one frame of the canvas overlays draws.
|
||||
fn drawn(app: &FigApp, ctx: &Context) -> usize {
|
||||
pub(super) fn drawn(app: &FigApp, ctx: &Context) -> usize {
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| {
|
||||
overlays(app, &ui.ctx().layer_painter(LayerId::background()), &Tokens::light());
|
||||
});
|
||||
|
||||
@@ -29,9 +29,9 @@ fn parent_of(doc: &Document, id: NodeId) -> Option<&Node> {
|
||||
/// no auto layout and the selection is editable outside an instance.
|
||||
pub fn layout_section(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, sel: &[NodeId]) {
|
||||
let ids = sel.to_vec();
|
||||
let frames = sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like()));
|
||||
let layout_containers = sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like() || matches!(n.kind, NodeKind::Group)));
|
||||
let no_al = !sel.is_empty() && sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.layout.is_none()));
|
||||
let frames = crate::selection_cache::every(doc, sel, |n| n.kind.is_frame_like());
|
||||
let layout_containers = crate::selection_cache::every(doc, sel, |n| n.kind.is_frame_like() || matches!(n.kind, NodeKind::Group));
|
||||
let no_al = !sel.is_empty() && crate::selection_cache::every(doc, sel, |n| n.layout.is_none());
|
||||
let al = common(doc, sel, |n| n.layout.clone());
|
||||
let has_al = matches!(al, Some(Some(_)));
|
||||
let title = if has_al { tr("Auto layout") } else { tr("Layout") };
|
||||
@@ -187,7 +187,7 @@ fn dimensions(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId], has
|
||||
return;
|
||||
}
|
||||
// Min/max apply to auto layout frames and their children; absolute children neither fill nor limit.
|
||||
let can_fill = parent_al && sel.iter().all(|i| doc.node(*i).is_some_and(|n| !n.child_layout.absolute));
|
||||
let can_fill = parent_al && crate::selection_cache::every(doc, sel, |n| !n.child_layout.absolute);
|
||||
let can_limit = has_al || can_fill;
|
||||
let first = sel.first().and_then(|i| doc.node(*i));
|
||||
row(ui, 32.0, |ui| {
|
||||
|
||||
@@ -42,9 +42,11 @@ pub(crate) use stroke::{CAP_ART_W, cap_art, join_label};
|
||||
|
||||
/// Common value of `f` across `ids` (None when mixed or empty).
|
||||
pub(crate) fn common<T: PartialEq + Clone>(doc: &Document, ids: &[NodeId], f: impl Fn(&Node) -> T) -> Option<T> {
|
||||
let mut it = ids.iter().filter_map(|i| doc.node(*i));
|
||||
let first = f(it.next()?);
|
||||
if it.all(|n| f(n) == first) { Some(first) } else { None }
|
||||
crate::selection_cache::with_nodes(doc, ids, |it| {
|
||||
let mut it = it.flatten();
|
||||
let first = f(it.next()?);
|
||||
if it.all(|n| f(n) == first) { Some(first) } else { None }
|
||||
})
|
||||
}
|
||||
|
||||
impl FigApp {
|
||||
@@ -257,6 +259,7 @@ fn design(app: &mut FigApp, ui: &mut Ui) {
|
||||
page_section(app, ui, &doc);
|
||||
return;
|
||||
}
|
||||
crate::selection_cache::show(&shown, &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
|
||||
@@ -287,7 +290,7 @@ fn design(app: &mut FigApp, ui: &mut Ui) {
|
||||
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(_)))) {
|
||||
if crate::selection_cache::with_nodes(&shown, &sel, |it| it.flatten().any(|n| matches!(n.kind, NodeKind::Text(_)))) {
|
||||
crate::typography::typography(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
}
|
||||
@@ -298,7 +301,7 @@ fn design(app: &mut FigApp, ui: &mut Ui) {
|
||||
effects(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
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())) {
|
||||
if crate::selection_cache::every(&shown, &sel, |n| n.kind.is_frame_like()) {
|
||||
grids(app, ui, &shown, &sel);
|
||||
widgets::separator(ui);
|
||||
}
|
||||
@@ -376,7 +379,7 @@ fn header(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
};
|
||||
let is_frame = sel.len() == 1 && sel.first().and_then(|i| doc.node(*i)).is_some_and(|n| matches!(n.kind, NodeKind::Frame));
|
||||
// Frames and groups convert into each other from the type menu.
|
||||
let is_group = !sel.is_empty() && sel.iter().all(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Group)));
|
||||
let is_group = !sel.is_empty() && crate::selection_cache::every(doc, sel, |n| matches!(n.kind, NodeKind::Group));
|
||||
let kind_items = |app: &mut FigApp, ui: &mut Ui, frame: bool| {
|
||||
for (to_frame, label, cmd) in [(true, tr("Frame"), "object.convertToFrame"), (false, tr("Group"), "object.convertToGroup")] {
|
||||
if menu_check(ui, to_frame == frame, label, "") {
|
||||
@@ -718,13 +721,14 @@ fn constraints_row(app: &mut FigApp, ui: &mut Ui, sel: &[NodeId], c: Option<figc
|
||||
/// (`node.set`'s `arcStart` / `arcSweep` / `arcInner`; bindable to number variables, sweep and
|
||||
/// ratio ones holding percentages). Returns whether it showed.
|
||||
fn arc_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) -> bool {
|
||||
let arcs: Vec<(f64, f64, f64)> = sel
|
||||
.iter()
|
||||
.filter_map(|i| match doc.node(*i)?.kind {
|
||||
NodeKind::Ellipse { start, sweep, inner } => Some((start, sweep, inner)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let arcs: Vec<(f64, f64, f64)> = crate::selection_cache::with_nodes(doc, sel, |it| {
|
||||
it.flatten()
|
||||
.filter_map(|n| match n.kind {
|
||||
NodeKind::Ellipse { start, sweep, inner } => Some((start, sweep, inner)),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
if arcs.is_empty() || arcs.len() != sel.len() {
|
||||
return false;
|
||||
}
|
||||
@@ -761,14 +765,15 @@ fn arc_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) ->
|
||||
/// inner radius as a share of the outer one), as `node.set`'s `pointCount` / `innerRadius`.
|
||||
/// Returns whether it showed.
|
||||
fn points_section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) -> bool {
|
||||
let shapes: Vec<(u32, Option<f64>)> = sel
|
||||
.iter()
|
||||
.filter_map(|i| match doc.node(*i)?.kind {
|
||||
NodeKind::Polygon { count } => Some((count, None)),
|
||||
NodeKind::Star { count, inner_ratio } => Some((count, Some(inner_ratio))),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let shapes: Vec<(u32, Option<f64>)> = crate::selection_cache::with_nodes(doc, sel, |it| {
|
||||
it.flatten()
|
||||
.filter_map(|n| match n.kind {
|
||||
NodeKind::Polygon { count } => Some((count, None)),
|
||||
NodeKind::Star { count, inner_ratio } => Some((count, Some(inner_ratio))),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
if shapes.is_empty() || shapes.len() != sel.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(super) fn appearance(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[N
|
||||
let col = widgets::col_width(ui.available_width());
|
||||
crate::variable_ui::bound_row(app, ui, &ids, "visible", VariableType::Boolean, Lead::Icon(Icon::Eye, tr("Visible")), col * 2.0 + 8.0);
|
||||
// Masks: how they mask.
|
||||
if sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.is_mask)) {
|
||||
if crate::selection_cache::every(doc, sel, |n| n.is_mask) {
|
||||
let mt = common(doc, sel, |n| n.mask_type);
|
||||
widgets::sublabels(ui, tr("Mask"), None);
|
||||
row(ui, 32.0, |ui| {
|
||||
@@ -50,11 +50,11 @@ pub(super) fn appearance(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[N
|
||||
}
|
||||
let op_of = |n: &Node| (f64::from(n.opacity) * 100.0).round();
|
||||
let op = common(doc, sel, op_of);
|
||||
let radius_ok = sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.has_corner_radius()));
|
||||
let radius_ok = crate::selection_cache::every(doc, sel, |n| n.kind.has_corner_radius());
|
||||
// Only rectangles and frames round each corner on its own.
|
||||
let indep_ok = radius_ok && sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.has_independent_corners()));
|
||||
let indep_ok = radius_ok && crate::selection_cache::every(doc, sel, |n| n.kind.has_independent_corners());
|
||||
// For polygons and stars the same panel holds only the Corner smoothing row.
|
||||
let smooth_ok = radius_ok && sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.has_corner_smoothing()));
|
||||
let smooth_ok = radius_ok && crate::selection_cache::every(doc, sel, |n| n.kind.has_corner_smoothing());
|
||||
let r = common(doc, sel, |n| n.corners.uniform().map(|r| (r * 100.0).round() / 100.0));
|
||||
widgets::sublabels(ui, tr("Opacity"), radius_ok.then_some(tr("Corner radius")));
|
||||
row(ui, 32.0, |ui| {
|
||||
@@ -147,7 +147,7 @@ pub(super) fn appearance(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[N
|
||||
/// of its own (`variable.setMode`). Collections from enabled libraries follow under their
|
||||
/// library's name (`library.setVariableMode` copies the collection in). Returns whether it showed.
|
||||
pub(super) fn variable_modes(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) -> bool {
|
||||
if !sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like())) {
|
||||
if !crate::selection_cache::every(doc, sel, |n| n.kind.is_frame_like()) {
|
||||
return false;
|
||||
}
|
||||
let libs = crate::variable_ui::libraries(app, doc);
|
||||
@@ -351,7 +351,7 @@ pub(super) fn paints(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeI
|
||||
// The row is the panel's width: the two fields split what the buttons at its end leave,
|
||||
// so the align field and the weight field are the same column (76 at the default width
|
||||
// with two buttons, 88 with one). The buttons are 24 pt each, 4 apart on the icon grid.
|
||||
let sided = sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.kind.is_frame_like() || matches!(n.kind, NodeKind::Rectangle)));
|
||||
let sided = crate::selection_cache::every(doc, sel, |n| n.kind.is_frame_like() || matches!(n.kind, NodeKind::Rectangle));
|
||||
let group = if sided { 24.0 + 4.0 + 24.0 } else { 24.0 };
|
||||
let field_w = ((ui.available_width() - widgets::GUTTER * 2.0 - group) / 2.0).max(40.0);
|
||||
let resp = widgets::dropdown(
|
||||
|
||||
@@ -240,7 +240,7 @@ pub(super) fn stroke_settings(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel
|
||||
}
|
||||
});
|
||||
// End points only mean something on open paths.
|
||||
let open_paths = sel.iter().all(|i| doc.node(*i).is_some_and(|n| matches!(n.kind, NodeKind::Line | NodeKind::Vector { .. })));
|
||||
let open_paths = crate::selection_cache::every(doc, sel, |n| matches!(n.kind, NodeKind::Line | NodeKind::Vector { .. }));
|
||||
if open_paths {
|
||||
widgets::sublabels(ui, tr("Start point"), Some(tr("End point")));
|
||||
row(ui, 32.0, |ui| {
|
||||
|
||||
@@ -107,7 +107,7 @@ pub fn section(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId]) {
|
||||
}
|
||||
});
|
||||
row(ui, 28.0, |ui| {
|
||||
let hidden = sel.iter().all(|i| doc.node(*i).is_some_and(|n| n.hides_contents()));
|
||||
let hidden = crate::selection_cache::every(doc, sel, |n| n.hides_contents());
|
||||
if widgets::checkbox(ui, hidden, tr("Hide contents")) {
|
||||
app.run("section.toggleContents", json!({"ids": ids, "hidden": !hidden}));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use figcraft_doc::{Document, NodeId};
|
||||
use figcraft_doc::{Document, Node, NodeId};
|
||||
|
||||
/// One kept value and what it was made from. The document is held weakly, so a kept value never
|
||||
/// keeps an old snapshot alive; the weak reference keeps its address from being reused.
|
||||
@@ -52,6 +52,57 @@ pub(crate) fn moved(ds: &figcraft_engine::DocState) -> Option<(Arc<Document>, (f
|
||||
(it.selection == ds.selection).then(|| (it.doc.clone(), d))
|
||||
}
|
||||
|
||||
/// The document to work out from what a move doesn't change (which layers, their kinds and
|
||||
/// parents): the one a gesture that only moved the selection started from, kept for the whole
|
||||
/// gesture ([`moved`]), else `doc`.
|
||||
pub(crate) fn unmoved(ds: &figcraft_engine::DocState, doc: &Arc<Document>) -> Arc<Document> {
|
||||
moved(ds).map_or_else(|| doc.clone(), |(base, _)| base)
|
||||
}
|
||||
|
||||
/// The selected layers of the document the Design panel shows, looked up once: its sections go
|
||||
/// over the whole selection many times a frame (mixed values, which kinds are selected), and with
|
||||
/// hundreds of layers selected the lookups cost more than the rest of the panel (#1571).
|
||||
struct Shown {
|
||||
doc: Weak<Document>,
|
||||
sel: Vec<NodeId>,
|
||||
nodes: Vec<Option<Arc<Node>>>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static SHOWN: std::cell::RefCell<Option<Shown>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// The panel is about to show `sel` of `doc`: [`with_nodes`] over exactly these reads the layers
|
||||
/// looked up here, kept while the document snapshot and the selection stay the same.
|
||||
pub(crate) fn show(doc: &Arc<Document>, sel: &[NodeId]) {
|
||||
SHOWN.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
if s.as_ref().is_some_and(|k| std::ptr::eq(k.doc.as_ptr(), Arc::as_ptr(doc)) && k.sel == sel) {
|
||||
return;
|
||||
}
|
||||
*s = Some(Shown { doc: Arc::downgrade(doc), sel: sel.to_vec(), nodes: sel.iter().map(|i| doc.node_arc(*i).cloned()).collect() });
|
||||
});
|
||||
}
|
||||
|
||||
/// `f` over the layers of `ids` in `doc`, in order (`None` for one not in it): the ones [`show`]
|
||||
/// kept when `doc` and `ids` are what it was given, else looked up. The kept document is held
|
||||
/// weakly, so its address can't be another document's and its contents can't change.
|
||||
pub(crate) fn with_nodes<R>(doc: &Document, ids: &[NodeId], f: impl FnOnce(&mut dyn Iterator<Item = Option<&Node>>) -> R) -> R {
|
||||
SHOWN.with(|s| {
|
||||
let s = s.borrow();
|
||||
match s.as_ref().filter(|k| std::ptr::eq(k.doc.as_ptr(), doc) && k.sel == ids) {
|
||||
Some(k) => f(&mut k.nodes.iter().map(Option::as_deref)),
|
||||
None => f(&mut ids.iter().map(|i| doc.node(*i))),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether every one of `ids` is in `doc` and passes `f`.
|
||||
pub(crate) fn every(doc: &Document, ids: &[NodeId], f: impl Fn(&Node) -> bool) -> bool {
|
||||
// `Iterator::all` on the `&mut dyn Iterator` itself (sized), not on the trait object.
|
||||
with_nodes(doc, ids, |mut it| Iterator::all(&mut it, |n| n.is_some_and(&f)))
|
||||
}
|
||||
|
||||
/// 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