mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-14 04:10:55 +00:00
M5: Select layer lists locked layers with a padlock and picks them (#1560)
The context menu's Select layer now lists locked layers (and what a locked frame holds) with a padlock in the leading column, and picking one selects it, as the help center's "Lock and unlock layers" says and as measured in Figma's web app, 2026-11-10. The submenu shows only with more than one layer under the pointer (measured on a single locked layer only, the least certain rule). Clicks, hover and marquee still never reach a locked layer.
- doc: geometry::HitOpts { locked } and layers_at_opts; layers_at and the other public hit functions keep their signatures and pass the default.
- engine: select.layersAt lists locked layers and its rows gain locked (the layer or an ancestor is locked).
- ui-egui: indented_item takes locked and paints the Layers panel's padlock icon; a row both selected and locked shows the check.
- docs: ui-parity Select layer row, revision row and status line; mcp.md select.layersAt.
Tests: layers_at_a_point_list_locked_layers_with_a_flag_and_select_set_takes_them, select_layer_marks_locked_layers_with_a_padlock_and_picks_them, select_layer_needs_more_than_one_layer_under_the_pointer.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
0d6ce1124c
commit
6cb015d4f8
+41
-16
@@ -152,7 +152,7 @@ fn near_path(path: &BezPath, p: Point, dist: f64) -> bool {
|
||||
/// boolean groups are hit where their `result` is.
|
||||
pub fn hit_leaf(doc: &Document, page: NodeId, pt: Point, tol: f64, result: BoolResult) -> Option<NodeId> {
|
||||
let p = doc.node(page)?;
|
||||
p.children.iter().rev().find_map(|c| hit_rec(doc, *c, pt, tol, 0, result, None).filter(|_| !masked_out(doc, p, *c, pt)))
|
||||
p.children.iter().rev().find_map(|c| hit_rec(doc, *c, pt, tol, 0, result, None, HitOpts::default()).filter(|_| !masked_out(doc, p, *c, pt)))
|
||||
}
|
||||
|
||||
/// Whether child `c` of `parent` is masked at `pt`: a visible mask below it (the nearest mask
|
||||
@@ -175,7 +175,7 @@ fn covers(doc: &Document, id: NodeId, n: &Node, pt: Point, depth: usize) -> bool
|
||||
|
||||
/// Like [`hit_leaf`], but only within layer `root` (it included).
|
||||
pub fn hit_in(doc: &Document, root: NodeId, pt: Point, tol: f64, result: BoolResult) -> Option<NodeId> {
|
||||
hit_rec(doc, root, pt, tol, 0, result, None)
|
||||
hit_rec(doc, root, pt, tol, 0, result, None, HitOpts::default())
|
||||
}
|
||||
|
||||
/// [`hit_leaf`] (or [`hit_in`] for a `root` that isn't a page) in outline mode: shapes are hit
|
||||
@@ -185,8 +185,8 @@ pub fn hit_in(doc: &Document, root: NodeId, pt: Point, tol: f64, result: BoolRes
|
||||
/// when it shows them, and every layer on the edge of its box when it shows object bounds.
|
||||
pub fn hit_outlines(doc: &Document, root: NodeId, pt: Point, tol: f64, result: BoolResult, also: OutlineHits) -> Option<NodeId> {
|
||||
match doc.node(root) {
|
||||
Some(p) if p.is_page() => p.children.iter().rev().find_map(|c| hit_rec(doc, *c, pt, tol, 0, result, Some(also))),
|
||||
_ => hit_rec(doc, root, pt, tol, 0, result, Some(also)),
|
||||
Some(p) if p.is_page() => p.children.iter().rev().find_map(|c| hit_rec(doc, *c, pt, tol, 0, result, Some(also), HitOpts::default())),
|
||||
_ => hit_rec(doc, root, pt, tol, 0, result, Some(also), HitOpts::default()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,13 +204,31 @@ fn on_box_edge(doc: &Document, id: NodeId, n: &Node, pt: Point, tol: f64) -> boo
|
||||
r.inflate(tol, tol).contains(lp) && !(r.width() > 2.0 * tol && r.height() > 2.0 * tol && r.inflate(-tol, -tol).contains(lp))
|
||||
}
|
||||
|
||||
fn hit_rec(doc: &Document, id: NodeId, pt: Point, tol: f64, depth: usize, result: BoolResult, also: Option<OutlineHits>) -> Option<NodeId> {
|
||||
/// Options for the hit tests that the plain pick never sets.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct HitOpts {
|
||||
/// Reach locked layers (and what a locked frame holds) too. A click, hover or marquee never
|
||||
/// does; the Select layer list does.
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn hit_rec(
|
||||
doc: &Document,
|
||||
id: NodeId,
|
||||
pt: Point,
|
||||
tol: f64,
|
||||
depth: usize,
|
||||
result: BoolResult,
|
||||
also: Option<OutlineHits>,
|
||||
opts: HitOpts,
|
||||
) -> Option<NodeId> {
|
||||
if depth > 512 {
|
||||
return None;
|
||||
}
|
||||
let n = doc.node(id)?;
|
||||
let outline = also.is_some();
|
||||
if (!n.visible && !also.is_some_and(|a| a.hidden)) || n.locked {
|
||||
if (!n.visible && !also.is_some_and(|a| a.hidden)) || n.locked && !opts.locked {
|
||||
return None;
|
||||
}
|
||||
if let NodeKind::Section { contents_hidden: true } = n.kind {
|
||||
@@ -228,7 +246,7 @@ fn hit_rec(doc: &Document, id: NodeId, pt: Point, tol: f64, depth: usize, result
|
||||
return None;
|
||||
}
|
||||
for c in n.paint_order().rev() {
|
||||
if let Some(h) = hit_rec(doc, c, pt, tol, depth + 1, result, also).filter(|_| !masked_out(doc, n, c, pt)) {
|
||||
if let Some(h) = hit_rec(doc, c, pt, tol, depth + 1, result, also, opts).filter(|_| !masked_out(doc, n, c, pt)) {
|
||||
return Some(h);
|
||||
}
|
||||
}
|
||||
@@ -239,14 +257,21 @@ fn hit_rec(doc: &Document, id: NodeId, pt: Point, tol: f64, depth: usize, result
|
||||
None
|
||||
}
|
||||
|
||||
/// Every visible, unlocked layer under `pt` (page coordinates) with its depth below the page, in
|
||||
/// layer-tree order (each layer before what it contains, the front-most first): what
|
||||
/// "Select layer" in the context menu lists. Clip content is honoured; boolean groups (hit where
|
||||
/// their `result` is) and sections with hidden contents count as one layer.
|
||||
/// Every visible, unlocked layer under `pt`: [`layers_at_opts`] with the default options.
|
||||
pub fn layers_at(doc: &Document, page: NodeId, pt: Point, tol: f64, result: BoolResult) -> Vec<(NodeId, usize)> {
|
||||
fn rec(doc: &Document, id: NodeId, pt: Point, tol: f64, depth: usize, result: BoolResult, out: &mut Vec<(NodeId, usize)>) {
|
||||
layers_at_opts(doc, page, pt, tol, result, HitOpts::default())
|
||||
}
|
||||
|
||||
/// Every visible layer under `pt` (page coordinates) with its depth below the page, in
|
||||
/// layer-tree order (each layer before what it contains, the front-most first): what
|
||||
/// "Select layer" in the context menu lists. Locked layers are left out unless `opts.locked`.
|
||||
/// Clip content is honoured; boolean groups (hit where their `result` is) and sections with
|
||||
/// hidden contents count as one layer.
|
||||
pub fn layers_at_opts(doc: &Document, page: NodeId, pt: Point, tol: f64, result: BoolResult, opts: HitOpts) -> Vec<(NodeId, usize)> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn rec(doc: &Document, id: NodeId, pt: Point, tol: f64, depth: usize, result: BoolResult, opts: HitOpts, out: &mut Vec<(NodeId, usize)>) {
|
||||
let Some(n) = doc.node(id) else { return };
|
||||
if !n.visible || n.locked || depth > 512 {
|
||||
if !n.visible || n.locked && !opts.locked || depth > 512 {
|
||||
return;
|
||||
}
|
||||
let clips = n.clip && n.kind.is_frame_like();
|
||||
@@ -256,14 +281,14 @@ pub fn layers_at(doc: &Document, page: NodeId, pt: Point, tol: f64, result: Bool
|
||||
let at = out.len();
|
||||
let whole = matches!(n.kind, NodeKind::BooleanOp { .. } | NodeKind::Section { contents_hidden: true });
|
||||
if whole {
|
||||
if hit_rec(doc, id, pt, tol, depth, result, None).is_some() {
|
||||
if hit_rec(doc, id, pt, tol, depth, result, None, opts).is_some() {
|
||||
out.push((id, depth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
for c in n.paint_order().rev() {
|
||||
let before = out.len();
|
||||
rec(doc, c, pt, tol, depth + 1, result, out);
|
||||
rec(doc, c, pt, tol, depth + 1, result, opts, out);
|
||||
if out.len() > before && masked_out(doc, n, c, pt) {
|
||||
out.truncate(before);
|
||||
}
|
||||
@@ -276,7 +301,7 @@ pub fn layers_at(doc: &Document, page: NodeId, pt: Point, tol: f64, result: Bool
|
||||
if let Some(p) = doc.node(page) {
|
||||
for c in p.children.iter().rev() {
|
||||
let before = out.len();
|
||||
rec(doc, *c, pt, tol, 0, result, &mut out);
|
||||
rec(doc, *c, pt, tol, 0, result, opts, &mut out);
|
||||
if out.len() > before && masked_out(doc, p, *c, pt) {
|
||||
out.truncate(before);
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"select.layersAt",
|
||||
"Layers at a point",
|
||||
"{x, y (page coordinates)} → {layers: [{id, name, type, depth}]} (every layer under the point, front-most first; what the context menu's Select layer lists)",
|
||||
"{x, y (page coordinates)} → {layers: [{id, name, type, depth, locked}]} (every layer under the point, locked ones too, front-most first; locked is true when the layer or an ancestor is locked; what the context menu's Select layer lists)",
|
||||
false,
|
||||
layers_at
|
||||
),
|
||||
|
||||
@@ -8,9 +8,13 @@ pub(super) fn layers_at(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let zoom = s.ds()?.view.zoom.max(1e-6);
|
||||
let sel = s.selection();
|
||||
let result = crate::booleans::result_outline(doc, &sel);
|
||||
let layers: Vec<Value> = figcraft_doc::geometry::layers_at(doc, s.page()?, figcraft_geom::Point::new(x, y), 2.0 / zoom, Some(&result))
|
||||
let opts = figcraft_doc::geometry::HitOpts { locked: true };
|
||||
let layers: Vec<Value> = figcraft_doc::geometry::layers_at_opts(doc, s.page()?, figcraft_geom::Point::new(x, y), 2.0 / zoom, Some(&result), opts)
|
||||
.into_iter()
|
||||
.filter_map(|(id, depth)| doc.node(id).map(|n| json!({"id": id, "name": n.name, "type": n.kind.type_name(), "depth": depth})))
|
||||
.filter_map(|(id, depth)| {
|
||||
doc.node(id)
|
||||
.map(|n| json!({"id": id, "name": n.name, "type": n.kind.type_name(), "depth": depth, "locked": crate::tools::locked_in(doc, id)}))
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({"layers": layers}))
|
||||
}
|
||||
|
||||
@@ -118,8 +118,33 @@ fn layers_at_a_point_list_the_stack_front_most_first() {
|
||||
let got: Vec<(NodeId, u64)> =
|
||||
out["layers"].as_array().unwrap().iter().map(|l| (id_of(&l["id"]).unwrap(), l["depth"].as_u64().unwrap())).collect();
|
||||
assert_eq!(got, vec![(f, 0), (over, 1), (g, 1), (a, 2)]);
|
||||
assert!(out["layers"].as_array().unwrap().iter().all(|l| l["locked"] == json!(false)), "none is locked");
|
||||
assert_eq!(out["layers"][0]["type"], json!("FRAME"));
|
||||
// Nothing under empty canvas; junk is refused.
|
||||
assert_eq!(ex(&mut s, "select.layersAt", json!({"x": 2000, "y": 2000}))["layers"], json!([]));
|
||||
assert!(s.execute("select.layersAt", &json!({"x": "left"})).is_err());
|
||||
}
|
||||
|
||||
/// Measured in Figma's web app, 2026-11-10 (Select layer lists a locked layer, and picking it selects it).
|
||||
#[test]
|
||||
fn layers_at_a_point_list_locked_layers_with_a_flag_and_select_set_takes_them() {
|
||||
let mut s = Session::new();
|
||||
let (f, g, [a, ..], _) = scene(&mut s);
|
||||
let over = rect_in(&mut s, 10.0, 10.0, Some(f));
|
||||
let lk = rect_in(&mut s, 10.0, 10.0, Some(f));
|
||||
ex(&mut s, "object.toggleLock", json!({"ids": [lk], "locked": true}));
|
||||
let out = ex(&mut s, "select.layersAt", json!({"x": 30, "y": 30}));
|
||||
let got: Vec<(NodeId, u64, bool)> = out["layers"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|l| (id_of(&l["id"]).unwrap(), l["depth"].as_u64().unwrap(), l["locked"].as_bool().unwrap()))
|
||||
.collect();
|
||||
assert_eq!(got, vec![(f, 0, false), (lk, 1, true), (over, 1, false), (g, 1, false), (a, 2, false)]);
|
||||
ex(&mut s, "select.set", json!({"ids": [lk]}));
|
||||
assert_eq!(s.selection(), vec![lk]);
|
||||
ex(&mut s, "select.none", json!({}));
|
||||
s.pointer(&ev(PointerKind::Down, 30.0, 30.0, false, false)).unwrap();
|
||||
s.pointer(&ev(PointerKind::Up, 30.0, 30.0, false, false)).unwrap();
|
||||
assert_eq!(s.selection(), vec![over], "a click still skips the locked layer");
|
||||
}
|
||||
|
||||
@@ -127,3 +127,80 @@ fn copying_a_link_from_a_frames_name_label_links_that_frame() {
|
||||
crate::menus::invoke(&mut u.app, "edit.copyLink");
|
||||
assert_eq!(copied.lock().unwrap().last().cloned(), Some(format!("figcraft://node/{}", a.0)));
|
||||
}
|
||||
|
||||
/// Two overlapping rectangles, the upper one locked; the menu opens on the overlap with nothing
|
||||
/// selected. Returns the app's UI and the ids (lower, locked).
|
||||
fn overlap_ui() -> (Ui, figcraft_doc::NodeId, figcraft_doc::NodeId) {
|
||||
let mut s = Session::new();
|
||||
let lower = id(&s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 80, "height": 80, "name": "Lower"})).unwrap());
|
||||
let upper = id(&s.execute("shape.rectangle", &json!({"x": 120, "y": 120, "width": 80, "height": 80, "name": "Upper"})).unwrap());
|
||||
s.execute("object.toggleLock", &json!({"ids": [upper], "locked": true})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.frame(vec![]);
|
||||
(u, lower, upper)
|
||||
}
|
||||
|
||||
/// Measured in Figma's web app, 2026-11-10 (Lock and unlock layers: locked layers are listed in Select layer).
|
||||
#[test]
|
||||
fn select_layer_marks_locked_layers_with_a_padlock_and_picks_them() {
|
||||
let (mut u, lower, upper) = overlap_ui();
|
||||
let at = u.app.to_screen(150.0, 150.0);
|
||||
crate::canvas::open_context_menu(&mut u.app, at, false);
|
||||
u.app.session.set_selection(vec![]);
|
||||
crate::menus::open_select_layer(&u.ctx, true);
|
||||
u.frame(vec![]);
|
||||
let shapes = flat(&u.frame_output(vec![]).shapes);
|
||||
// The Layers panel lists the same names; the menu's rows are the right-most copies.
|
||||
let menu_row = |name: &str| {
|
||||
let (_, pos, size) = crate::live_tests::texts(&shapes)
|
||||
.into_iter()
|
||||
.filter(|(t, _, _)| t == name)
|
||||
.max_by(|a, b| a.1.x.total_cmp(&b.1.x))
|
||||
.unwrap_or_else(|| panic!("no row {name:?}"));
|
||||
(pos, size)
|
||||
};
|
||||
let (lower_at, upper_at) = (menu_row("Lower"), menu_row("Upper"));
|
||||
assert!(lower_at.0.x > 300.0 && upper_at.0.x > 300.0, "both layers are listed in the menu");
|
||||
let in_leading = |(pos, size): (Pos2, egui::Vec2)| {
|
||||
let c = pos + size / 2.0;
|
||||
shapes
|
||||
.iter()
|
||||
.filter(|s| !matches!(s, egui::Shape::Text(_)))
|
||||
.filter(|s| {
|
||||
let r = s.visual_bounding_rect();
|
||||
r.is_positive()
|
||||
&& (r.center().y - c.y).abs() < 8.0
|
||||
&& r.max.x < c.x - size.x / 2.0 - 4.0
|
||||
&& r.min.x > c.x - size.x / 2.0 - 40.0
|
||||
&& r.width() < 20.0
|
||||
})
|
||||
.count()
|
||||
};
|
||||
assert!(in_leading(upper_at) > 0, "the locked row has a padlock in its leading column");
|
||||
assert_eq!(in_leading(lower_at), 0, "the other row has nothing there");
|
||||
let p = upper_at.0 + upper_at.1 / 2.0;
|
||||
for pressed in [true, false] {
|
||||
u.frame(vec![
|
||||
egui::Event::PointerMoved(p),
|
||||
egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE },
|
||||
]);
|
||||
}
|
||||
assert_eq!(u.app.session.selection(), vec![upper], "picking the locked row selects it");
|
||||
let _ = lower;
|
||||
}
|
||||
|
||||
/// Measured on a single locked layer only (Figma's web app, 2026-11-10): the least certain rule.
|
||||
#[test]
|
||||
fn select_layer_needs_more_than_one_layer_under_the_pointer() {
|
||||
let (mut u, _, _) = overlap_ui();
|
||||
let one = u.app.to_screen(105.0, 105.0);
|
||||
crate::canvas::open_context_menu(&mut u.app, one, false);
|
||||
u.frame(vec![]);
|
||||
let shapes = flat(&u.frame_output(vec![]).shapes);
|
||||
assert!(!has_text(&shapes, "Select layer"), "one layer under the pointer: no Select layer");
|
||||
let two = u.app.to_screen(150.0, 150.0);
|
||||
crate::canvas::open_context_menu(&mut u.app, two, false);
|
||||
u.frame(vec![]);
|
||||
let shapes = flat(&u.frame_output(vec![]).shapes);
|
||||
assert!(has_text(&shapes, "Select layer"), "two layers: it shows");
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ pub fn open_select_layer(ctx: &egui::Context, open: bool) {
|
||||
}
|
||||
|
||||
/// The context menu's "Select layer" submenu (right-click on the canvas): every layer under the
|
||||
/// click, nested ones indented, the selected ones checked. Returns whether one was picked.
|
||||
/// click (locked ones with a padlock), nested ones indented, the selected ones checked; shown
|
||||
/// only with more than one layer under the pointer. Returns whether one was picked.
|
||||
pub(super) fn select_layer_menu(app: &mut FigApp, ui: &mut Ui, pos: egui::Pos2, t: &Tokens) -> bool {
|
||||
if !app.canvas.rect.contains(pos) {
|
||||
return false;
|
||||
@@ -23,15 +24,21 @@ pub(super) fn select_layer_menu(app: &mut FigApp, ui: &mut Ui, pos: egui::Pos2,
|
||||
let p = app.to_page(pos);
|
||||
let Ok(ds) = app.session.ds() else { return false };
|
||||
let tol = 2.0 / ds.view.zoom.max(1e-6);
|
||||
let layers: Vec<(figcraft_doc::NodeId, usize, String)> =
|
||||
figcraft_doc::geometry::layers_at(&ds.doc, ds.page, p, tol, Some(&figcraft_engine::booleans::result_outline(&ds.doc, &ds.selection)))
|
||||
let opts = figcraft_doc::geometry::HitOpts { locked: true };
|
||||
let layers: Vec<(figcraft_doc::NodeId, usize, String, bool)> = {
|
||||
let result = figcraft_engine::booleans::result_outline(&ds.doc, &ds.selection);
|
||||
figcraft_doc::geometry::layers_at_opts(&ds.doc, ds.page, p, tol, Some(&result), opts)
|
||||
.into_iter()
|
||||
.take(40)
|
||||
.filter_map(|(id, depth)| {
|
||||
ds.doc.node(id).map(|n| (id, depth, if n.name.is_empty() { n.kind.type_name().to_lowercase() } else { n.name.clone() }))
|
||||
ds.doc.node(id).map(|n| {
|
||||
let name = if n.name.is_empty() { n.kind.type_name().to_lowercase() } else { n.name.clone() };
|
||||
(id, depth, name, figcraft_engine::tools::locked_in(&ds.doc, id))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if layers.is_empty() {
|
||||
.collect()
|
||||
};
|
||||
if layers.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
let sel = ds.selection.clone();
|
||||
@@ -40,8 +47,8 @@ pub(super) fn select_layer_menu(app: &mut FigApp, ui: &mut Ui, pos: egui::Pos2,
|
||||
SubmenuOptions { area: Id::unique("ctx_select_layer"), key: SELECT_LAYER, label: tr("Select layer"), gap: 4.0, constrain: true, tokens: t },
|
||||
|ui| {
|
||||
let mut picked = None;
|
||||
for (id, depth, name) in &layers {
|
||||
if indented_item(ui, name, "", true, sel.contains(id), 12.0 * (*depth).min(8) as f32, t) {
|
||||
for (id, depth, name, locked) in &layers {
|
||||
if indented_item(ui, name, "", true, sel.contains(id), *locked, 12.0 * (*depth).min(8) as f32, t) {
|
||||
picked = Some(*id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,11 +395,13 @@ fn need_width(ui: &Ui, w: f32) {
|
||||
}
|
||||
|
||||
pub(crate) fn item(ui: &mut Ui, label: &str, shortcut: &str, enabled: bool, checked: bool, t: &Tokens) -> bool {
|
||||
indented_item(ui, label, shortcut, enabled, checked, 0.0, t)
|
||||
indented_item(ui, label, shortcut, enabled, checked, false, 0.0, t)
|
||||
}
|
||||
|
||||
/// A menu row whose label starts `indent` pixels further in (nested layers).
|
||||
pub(super) fn indented_item(ui: &mut Ui, label: &str, shortcut: &str, enabled: bool, checked: bool, indent: f32, t: &Tokens) -> bool {
|
||||
/// A menu row whose label starts `indent` pixels further in (nested layers); `locked` paints a
|
||||
/// padlock in the leading column (a checked row shows the check instead).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn indented_item(ui: &mut Ui, label: &str, shortcut: &str, enabled: bool, checked: bool, locked: bool, indent: f32, t: &Tokens) -> bool {
|
||||
let w = ui.available_width();
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 28.0), Sense::click());
|
||||
a11y::menu_item(&resp, label, shortcut, checked.then_some(true));
|
||||
@@ -415,6 +417,8 @@ pub(super) fn indented_item(ui: &mut Ui, label: &str, shortcut: &str, enabled: b
|
||||
let col = if enabled { t.menu_text } else { t.menu_dim };
|
||||
if checked {
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 12.0, r.center().y), vec2(12.0, 12.0)), Icon::Check, col);
|
||||
} else if locked {
|
||||
icons::paint(ui.painter(), Rect::from_center_size(pos2(r.min.x + 12.0, r.center().y), vec2(12.0, 12.0)), Icon::Lock, col);
|
||||
}
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), regular(12.0), col);
|
||||
let keys = (!shortcut.is_empty())
|
||||
|
||||
+2
-2
@@ -2082,8 +2082,8 @@ turned off 0°.
|
||||
`node.set {pointCount}` keeps stars to 3–60 points; `miterAngle` (degrees, the Design panel's
|
||||
Miter angle) sets the miter limit (`limit = 1 / sin(angle / 2)`); `get_node` reports a join
|
||||
other than miter (`strokeJoin`) or a miter angle other than the default ~28.96° (`miterAngle`).
|
||||
- **Select**: `select.layersAt {x, y}` lists every layer under a page point, front-most first with its
|
||||
depth (the context menu's Select layer); `select.set`, `select.children` / `select.parent` (Enter
|
||||
- **Select**: `select.layersAt {x, y}` lists every layer under a page point, locked ones too, front-most first with its
|
||||
depth and a `locked` flag (true when the layer or an ancestor is locked; the context menu's Select layer); `select.set`, `select.children` / `select.parent` (Enter
|
||||
/ Shift+Enter), `select.next` / `select.previous` (Tab / Shift+Tab). With `pointer`, `cmd: true`
|
||||
clicks select the nested layer, `cmd` drags from empty canvas select the nested layers they touch,
|
||||
and `shift` drags toggle what they touch. A press on the background of a top-level frame with
|
||||
|
||||
+3
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user