mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-04 09:19:37 -06:00
M2: a text keeps a max height or a number of max lines, not both (#1832)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Brandon Thomas
parent
8174dd5b74
commit
f9b5693621
@@ -759,6 +759,14 @@ fn apply_sizing(cmd: &str, doc: &mut Document, id: NodeId, k: &str, v: &Value) -
|
||||
other => Some(f64_of(other).ok_or_else(|| bad(cmd, format!("`{k}` must be a number or null")))?.clamp(0.0, 1e7)),
|
||||
};
|
||||
set_limit(&mut doc.get_mut(id)?.child_layout, k, val);
|
||||
// A text has a max height or a number of max lines, not both: a max height sets the
|
||||
// lines back to Auto (Guide to auto layout).
|
||||
if k == "maxHeight"
|
||||
&& val.is_some()
|
||||
&& let Some(t) = doc.get_mut(id)?.kind.text_mut()
|
||||
{
|
||||
t.max_lines = 0;
|
||||
}
|
||||
}
|
||||
"columnTracks" | "gridColumnTracks" | "rowTracks" | "gridRowTracks" => {
|
||||
let a = v.as_array().ok_or_else(|| bad(cmd, format!("`{k}` must be an array of tracks")))?;
|
||||
@@ -900,7 +908,16 @@ fn apply_text(cmd: &str, doc: &mut Document, id: NodeId, props: &Map<String, Val
|
||||
*if k == "hangingList" { &mut t.hanging_list } else { &mut t.hanging_punctuation } = on;
|
||||
}
|
||||
"leadingTrim" => text_mut(cmd, doc.get_mut(id)?)?.leading_trim = enum_of(v).ok_or_else(|| bad(cmd, "leadingTrim: none|capHeight"))?,
|
||||
"maxLines" => text_mut(cmd, doc.get_mut(id)?)?.max_lines = f64_of(v).unwrap_or(0.0).clamp(0.0, 10_000.0) as u32,
|
||||
"maxLines" => {
|
||||
let lines = f64_of(v).unwrap_or(0.0).clamp(0.0, 10_000.0) as u32;
|
||||
let n = doc.get_mut(id)?;
|
||||
text_mut(cmd, n)?.max_lines = lines;
|
||||
// A text has a max height or a number of max lines, not both: a number of lines takes
|
||||
// the max height away (Guide to auto layout).
|
||||
if lines > 0 {
|
||||
n.child_layout.max_h = None;
|
||||
}
|
||||
}
|
||||
"features" | "textFeatures" | "fontFeatures" | "openTypeFeatures" => {
|
||||
let f = features_of(cmd, v)?;
|
||||
let t = text_mut(cmd, doc.get_mut(id)?)?;
|
||||
|
||||
@@ -323,12 +323,30 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
true,
|
||||
|s, p| {
|
||||
let v = finite(req_num("text.maxLines", p, "lines")?).clamp(0.0, 10_000.0) as u32;
|
||||
para(s, p, |t| {
|
||||
let r = para(s, p, |t| {
|
||||
t.max_lines = v;
|
||||
if v > 0 && t.auto_resize != TextAutoResize::Height {
|
||||
t.auto_resize = TextAutoResize::Truncate;
|
||||
}
|
||||
})
|
||||
})?;
|
||||
// A text has a max height or a number of max lines, not both (Guide to auto layout).
|
||||
if v > 0 {
|
||||
let ids = match textedit::selected_range(s) {
|
||||
Some((id, _, _)) => vec![id],
|
||||
None => targets(s, p),
|
||||
};
|
||||
let doc = s.doc_mut()?;
|
||||
for i in ids {
|
||||
for d in doc.subtree(i) {
|
||||
if let Some(n) = doc.node_mut(d)
|
||||
&& n.kind.text().is_some()
|
||||
{
|
||||
n.child_layout.max_h = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(r)
|
||||
}
|
||||
),
|
||||
spec!(
|
||||
|
||||
@@ -551,6 +551,36 @@ fn css_font_family_escapes_backslash_and_newline() {
|
||||
assert!(css.contains(r#"font-family: "Foo\"\3C /style>";"#), "{css}");
|
||||
}
|
||||
|
||||
/// "Text layers cannot have both a max height and a set number of max lines. Adding a max height
|
||||
/// will set max lines to Auto. Setting max lines to a number will remove the layer's max height
|
||||
/// setting." (Guide to auto layout)
|
||||
#[test]
|
||||
fn a_text_has_a_max_height_or_max_lines_not_both() {
|
||||
let mut s = Session::new();
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 300, "height": 100})).unwrap());
|
||||
s.execute("node.set", &json!({"id": f, "props": {"layout": "vertical"}})).unwrap();
|
||||
let t = id(s.execute("text.create", &json!({"text": "Hello there world", "x": 0, "y": 0, "parent": f, "width": 60})).unwrap());
|
||||
let state = |s: &Session| {
|
||||
let n = s.doc().unwrap().node(t).unwrap().clone();
|
||||
(n.kind.text().unwrap().max_lines, n.child_layout.max_h)
|
||||
};
|
||||
s.execute("node.set", &json!({"id": t, "props": {"maxLines": 2}})).unwrap();
|
||||
assert_eq!(state(&s), (2, None));
|
||||
s.execute("node.set", &json!({"id": t, "props": {"maxHeight": 40}})).unwrap();
|
||||
assert_eq!(state(&s), (0, Some(40.0)), "a max height sets max lines to Auto");
|
||||
s.execute("node.set", &json!({"id": t, "props": {"maxLines": 3}})).unwrap();
|
||||
assert_eq!(state(&s), (3, None), "max lines take the max height away");
|
||||
s.execute("node.set", &json!({"id": t, "props": {"maxHeight": 50}})).unwrap();
|
||||
s.execute("text.maxLines", &json!({"ids": [t], "lines": 1})).unwrap();
|
||||
assert_eq!(state(&s), (1, None), "the Max lines command too");
|
||||
// Auto (0) leaves a max height alone, and a frame's max height has no lines to clear.
|
||||
s.execute("node.set", &json!({"id": t, "props": {"maxHeight": 50}})).unwrap();
|
||||
s.execute("node.set", &json!({"id": t, "props": {"maxLines": 0}})).unwrap();
|
||||
assert_eq!(state(&s), (0, Some(50.0)));
|
||||
s.execute("node.set", &json!({"id": f, "props": {"maxHeight": 80}})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().node(f).unwrap().child_layout.max_h, Some(80.0));
|
||||
}
|
||||
|
||||
/// "Links are styled with an underline by default. Use the keyboard shortcut to remove the
|
||||
/// underline: Mac: Command U, Windows: Control U" (Add links to text).
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Auto layout and constraints parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-01 · **Change:** minor (Remove all auto layout, as the help center's More layout options has it: `layout.removeAll` takes the auto layout off a frame and every frame nested in it in one undo step; grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-11-01 · **Change:** minor (a text keeps a max height or a number of max lines, not both, as the Guide to auto layout says: a max height sets max lines to Auto and max lines remove the max height; Remove all auto layout, as the help center's More layout options has it: `layout.removeAll` takes the auto layout off a frame and every frame nested in it in one undo step; grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); Layout sidebar: Add auto layout (+) is available for groups and converts a single group to a frame in place, with mask order, flow-only inference and Fill sizing preserved; adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys only to frames, with warnings (#603); core editor leftovers: auto gap handles, wrapped-line alignment; behaviour suite written from the public documentation, every rule's status; fixes and the missing settings it found) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
Auto layout is the most used layout feature in Figma and the one where small differences (a
|
||||
hugging frame one pixel off, text wrapping at another width) break real files. **Breadth ~98%,
|
||||
@@ -42,6 +42,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
|
||||
| Hug only on auto layout frames (and text); Fill only in an auto layout flow, never on a top-level frame | pass | `al_behaviour_tests::hug_and_fill_only_where_they_apply` |
|
||||
| Min/max width and height bound Hug, Fill and Fixed sizes, on frames and children | pass | `sizing_rules`; a fill child at its max leaves the rest to the other fill children (silent on redistribution: CSS flexbox) |
|
||||
| A hugging text wider than its max width wraps at the max | pass | `sizing_rules` |
|
||||
| A text has a max height or a number of max lines, not both: a max height sets max lines to Auto, a number of max lines removes the max height | **fixed** | both coexisted; `node.set {maxHeight}` and `{maxLines}` and `text.maxLines` keep one of the two (a frame's max height is untouched); `text_behaviour_tests::a_text_has_a_max_height_or_max_lines_not_both` |
|
||||
| Remove all auto layout: a frame and all of its nested objects lose their auto layout at once (More layout options) | **added** | `layout.removeAll {ids?}`, one undo step, in the canvas context menu beside Add auto layout; instances and the layers inside them keep theirs; `al_nesting_tests::remove_all_auto_layout_strips_the_frame_and_every_frame_nested_in_it` |
|
||||
| Text filling a width wraps and its hugging frame grows with it | pass | `sizing_rules` |
|
||||
| A hugging frame follows a child that grows, through nested levels, in one edit and one undo step | pass | `al_behaviour_tests::a_hugging_frame_follows_its_children_through_nested_levels` |
|
||||
@@ -96,6 +97,7 @@ Status: **pass** (a case in the suite holds), **fixed** (the suite found a diver
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-01 | patch | M2: a text layer keeps a max height or a number of max lines, not both ("Text layers cannot have both a max height and a set number of max lines", Guide to auto layout): `node.set {maxHeight}` on a text sets its max lines to Auto, and `node.set {maxLines}` or `text.maxLines` with a number removes its max height. Found by a documentation audit of this page. Test: `text_behaviour_tests::a_text_has_a_max_height_or_max_lines_not_both`. Numbers unchanged |
|
||||
| 2026-11-01 | minor | M2: Remove all auto layout ("To remove auto layout from a frame and all of its nested objects at once, right-click on the frame and go to More layout options > Remove all auto layout", Toggle on auto layout in designs): `layout.removeAll {ids?}` strips the frames and every frame nested in them in one undo step, leaving instances and their layers alone; in the canvas context menu beside Add auto layout and in the Actions palette. Found by a documentation audit of this page. Test: `al_nesting_tests::remove_all_auto_layout_strips_the_frame_and_every_frame_nested_in_it`. Numbers unchanged |
|
||||
| 2026-10-30 | minor | M8, #1083: grid placement finds free cells on a bitmap, a word at a time, with the same placements; crafted grids of wide children no longer stall a layout pass. Numbers unchanged. |
|
||||
| 2026-10-29 | minor | M2 review fixes: conversion preserves mask runs and Fill sizing per axis; layout inference uses visible, non-absolute flow children; the sidebar hides Add for mixed selections containing existing auto layout. Regression coverage includes rendered mask pixels, visible-child positions, parent layout stability and the sidebar accessibility tree. Numbers unchanged |
|
||||
|
||||
Reference in New Issue
Block a user