M8: the Design panel and the selection overlays stop going over a large selection every frame (#1571) (#1660)

This commit is contained in:
BFlat
2026-10-30 21:58:41 +07:00
committed by GitHub
parent 139d5e94a9
commit db725dc73f
12 changed files with 267 additions and 46 deletions
+2 -1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -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;
+19 -9
View File
@@ -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());
});
+4 -4
View File
@@ -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| {
+26 -21
View File
@@ -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;
}
+6 -6
View File
@@ -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(
+1 -1
View File
@@ -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| {
+1 -1
View File
@@ -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}));
}
+52 -1
View File
@@ -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
View File
File diff suppressed because one or more lines are too long