mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 11:56:50 -06:00
Merge pull request #1373 from Ridzzz0Alam/fix/1338-instance-swap-scroll
fix: Issue #1338 an instance-swap property's default menu scrolls
This commit is contained in:
@@ -338,13 +338,20 @@ fn properties_ui(app: &mut FigApp, ui: &mut Ui, doc: &Document, owner: NodeId) {
|
||||
PropDef::InstanceSwap { default, .. } => {
|
||||
let label = doc.node(*default).map(|n| n.name.clone()).unwrap_or_else(|| tr("Missing").into());
|
||||
let resp = widgets::dropdown(ui, &label, 84.0, None);
|
||||
menu_popup(&resp).show(|ui| {
|
||||
for (c, name) in comps.iter() {
|
||||
if menu_item(ui, name, "") {
|
||||
app.run("component.editProperty", json!({"id": owner, "name": k, "default": c.0}));
|
||||
ui.close();
|
||||
crate::a11y::combo(&resp, k, &label, crate::a11y::menu_open(&resp));
|
||||
// A file with many components (an icon library) scrolls instead of running off the screen.
|
||||
menu_popup(&resp).width(200.0).show(|ui| {
|
||||
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| {
|
||||
for (c, name) in comps.iter() {
|
||||
let current = c == default;
|
||||
if menu_item(ui, &format!("{}{name}", if current { "✓ " } else { "" }), "") {
|
||||
if !current {
|
||||
app.run("component.editProperty", json!({"id": owner, "name": k, "default": c.0}));
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
PropDef::Variant { .. } | PropDef::Slot { .. } => {}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//! The default of a main component's instance-swap property, driven through egui headless: with
|
||||
//! more components than fit on the screen (an icon library), its menu scrolls, so the last one can
|
||||
//! be picked (#1338).
|
||||
|
||||
use egui::accesskit::{Node, Role};
|
||||
use egui::{Event, MouseWheelUnit, Pos2, TouchPhase, pos2, vec2};
|
||||
use figcraft_doc::{NodeId, NodeKind, PropDef};
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
|
||||
fn id(v: serde_json::Value) -> NodeId {
|
||||
NodeId(v["id"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
/// A selected "Button" component whose instance-swap property "Icon" defaults to "Heart", then 80
|
||||
/// more components, the last "Zebra". Returns (ui, button, zebra).
|
||||
fn setup() -> (Ui, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
let mut icon = |name: &str, k: usize| {
|
||||
let f =
|
||||
id(s.execute("frame.create", &json!({"x": (k % 20) * 40, "y": 200 + (k / 20) * 40, "width": 24, "height": 24, "name": name})).unwrap());
|
||||
id(s.execute("component.create", &json!({"ids": [f]})).unwrap())
|
||||
};
|
||||
let heart = icon("Heart", 0);
|
||||
let mut zebra = heart;
|
||||
for k in 1..=80 {
|
||||
zebra = icon(&if k == 80 { "Zebra".to_string() } else { format!("Glyph {k}") }, k);
|
||||
}
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 120, "height": 40, "name": "Button"})).unwrap());
|
||||
let nested = id(s.execute("instance.create", &json!({"component": heart, "x": 8, "y": 8, "parent": f})).unwrap());
|
||||
let button = id(s.execute("component.create", &json!({"ids": [f]})).unwrap());
|
||||
s.execute("component.addProperty", &json!({"id": button, "name": "Icon", "type": "instanceSwap", "default": heart, "layer": nested})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [button]})).unwrap();
|
||||
let u = Ui::new(s);
|
||||
u.ctx.enable_accesskit();
|
||||
(u, button, zebra)
|
||||
}
|
||||
|
||||
fn tree(u: &mut Ui) -> Vec<Node> {
|
||||
u.frame_output(vec![]).platform_output.accesskit_update.unwrap().nodes.into_iter().map(|(_, n)| n).collect()
|
||||
}
|
||||
|
||||
fn centre(n: &Node) -> Pos2 {
|
||||
let b = n.bounds().unwrap();
|
||||
pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32)
|
||||
}
|
||||
|
||||
fn click(u: &mut Ui, at: Pos2) {
|
||||
u.button(at, true);
|
||||
u.button(at, false);
|
||||
}
|
||||
|
||||
/// Turn the mouse wheel down by `dy` points over `at`, and let the scroll settle.
|
||||
fn wheel(u: &mut Ui, at: Pos2, dy: f32) {
|
||||
let wheel = Event::MouseWheel {
|
||||
unit: MouseWheelUnit::Point,
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
delta: vec2(0.0, -dy),
|
||||
phase: TouchPhase::Move,
|
||||
modifiers: Default::default(),
|
||||
};
|
||||
u.frame(vec![Event::PointerMoved(at), wheel]);
|
||||
for _ in 0..30 {
|
||||
u.frame(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_default(u: &Ui, button: NodeId) -> Option<NodeId> {
|
||||
match u.app.session.ds().ok()?.doc.node(button).map(|n| &n.kind) {
|
||||
Some(NodeKind::Component { props, .. }) => match props.get("Icon") {
|
||||
Some(PropDef::InstanceSwap { default, .. }) => Some(*default),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The default's dropdown on the "Icon" property row, scrolled into view in the Design panel.
|
||||
fn default_combo(u: &mut Ui) -> Option<Node> {
|
||||
for _ in 0..20 {
|
||||
let combo = tree(u).into_iter().find(|n| n.role() == Role::ComboBox && n.label() == Some("Icon"))?;
|
||||
if centre(&combo).y < 860.0 {
|
||||
return Some(combo);
|
||||
}
|
||||
wheel(u, pos2(1300.0, 500.0), 100.0);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_default_menu_scrolls_to_its_last_component() {
|
||||
let (mut u, button, zebra) = setup();
|
||||
let combo = default_combo(&mut u).expect("the Icon property has a dropdown for its default");
|
||||
assert_eq!(combo.value(), Some("Heart"));
|
||||
// Layer rows share the names; the menu's items are the ones that weren't there before it opened.
|
||||
let before: Vec<Pos2> = tree(&mut u).iter().filter(|n| n.label().is_some_and(|l| l == "Zebra" || l == "Glyph 1")).map(centre).collect();
|
||||
click(&mut u, centre(&combo));
|
||||
let item = |u: &mut Ui, name: &str| tree(u).into_iter().find(|n| n.label() == Some(name) && !before.contains(¢re(n)));
|
||||
let top = centre(&item(&mut u, "Glyph 1").expect("the menu lists the components"));
|
||||
let bottom = centre(&item(&mut u, "Zebra").expect("the menu lists the last component"));
|
||||
assert!(bottom.y > 900.0, "80 components don't fit on the screen: Zebra at {bottom:?}");
|
||||
// The menu is capped and scrolls: wheel over it to the end of the list, and pick Zebra.
|
||||
let mut at = bottom;
|
||||
for _ in 0..40 {
|
||||
wheel(&mut u, top, 200.0);
|
||||
let next = centre(&item(&mut u, "Zebra").expect("the menu stays open while it scrolls"));
|
||||
if next == at {
|
||||
break;
|
||||
}
|
||||
at = next;
|
||||
}
|
||||
assert!(at.y < 880.0, "the menu scrolls Zebra onto the screen: at {at:?}");
|
||||
assert!(at.y - top.y < 400.0, "Zebra ends up inside the capped menu, near its top row: {top:?} → {at:?}");
|
||||
click(&mut u, at);
|
||||
assert_eq!(swap_default(&u, button), Some(zebra), "picking the last component makes it the default");
|
||||
}
|
||||
@@ -106,6 +106,8 @@ mod comments_tests;
|
||||
#[cfg(test)]
|
||||
mod component_props_reorder_tests;
|
||||
#[cfg(test)]
|
||||
mod component_swap_default_tests;
|
||||
#[cfg(test)]
|
||||
mod context_menu_tests;
|
||||
#[cfg(test)]
|
||||
mod corner_individual_tests;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Components, variants, styles and variables parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (an instance-swap property's default menu scrolls (#1338); variables on a text style's fields from the style editor; instance-swap properties in the component playground; duplicating or Alt-dragging a main component makes an instance of it (#892); a bound colour variable's alpha applies once (#802); variables import reads `variables` outside `meta` and warns when it imports none (#790); variant properties reorder by drag; library files saved right after a read are read again; Hide when publishing note corrected; size and resizing overrides of layers inside instances (#731); variables export in the browser as a download; core editor leftovers: component playground, variables inside styles; layers and assets panels behaviour suite and fixes; extended collections tail; styles and variables behaviour suite and fixes; components behaviour suite and fixes; team libraries on the server, library usage and swapping a library; extended collections, variables import/export, gradient stops and variant properties bound to variables) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Design systems are why teams adopt Figma. **Components: breadth ~96%, ready ~72%. Styles and
|
||||
variables: breadth ~95%, ready ~76% (estimated).** QA'd with a synthetic design system of ~300
|
||||
@@ -21,7 +21,7 @@ accounts).
|
||||
| Switching variants keeps overrides where names match and both variants had the same value; text layers keep theirs | done | |
|
||||
| Default variant: the top-left one (new instances, Assets thumbnail, generated code) | done | |
|
||||
| Variant property order (names rewritten), duplicate combinations reported | done | drag a property in the Properties section to reorder it, one undo step (#679), or `component.editProperty {index}` |
|
||||
| Properties: boolean, text, instance swap (preferred values), variant | done | |
|
||||
| Properties: boolean, text, instance swap (preferred values), variant | done | every component menu scrolls past 320 pt, the default of an instance-swap property's too (#1338); no search in them yet |
|
||||
| Slots (preferred components, fill menu, kept across updates) | done | Figma 2025 feature |
|
||||
| Exposed nested instances | done | hidden ones hide their properties |
|
||||
| Descriptions | done | |
|
||||
@@ -77,6 +77,7 @@ updates with overrides kept: `library_tests.rs`, `components_scale_tests.rs`.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-26 | minor | An instance-swap property's default menu scrolls (#1338): on the main component's Properties row the menu listed every component in the file at full height, so in a file with many (an icon library) the bottom ones ran off the screen and couldn't be picked. It now scrolls past 320 pt like the instance's swap menu, is at least 200 pt wide, ticks the current default and is named for assistive technology (`component_swap_default_tests`). Left: searching these menus. Numbers unchanged. |
|
||||
| 2026-10-24 | minor | Variables on a text style's fields from the style editor: the font and font style menus start with Apply variable (string variables), and the size, line height and letter spacing fields show a variable button on hover (number variables). A bound field shows its variable in place of the value; a click on it picks another variable and its detach button unbinds it (`style_picker_tests`). Left: the other effect and grid fields (commands only), library variables in the picker. Numbers unchanged. |
|
||||
| 2026-10-23 | minor | Instance-swap properties in the component playground: the Inspect tab's Playground shows a dropdown for each, with the same menu as the Design panel's (the property's preferred components first, then every component in the file). The component picked changes the preview and the code only, never the file. Left: slots in the playground. Numbers unchanged. |
|
||||
| 2026-10-20 | minor | A main component's copy within its file is an instance of it (#892): Duplicate (`edit.duplicate`) and Alt-drag on the canvas made a second main; they now place an instance where the copy went (same offset, the instance's default name, selected), as Copy then Paste already did. A variant duplicated in its set stays a new variant. |
|
||||
|
||||
Reference in New Issue
Block a user