mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
M4, #1747: a text layer's name follows the characters typed into it
This commit is contained in:
1 parent
0e86f3597a
commit
58af1a6237
5 files changed
+101
No files matched your search
@@ -184,14 +184,21 @@ fn edit_if(s: &mut Session, f: impl FnOnce(&mut TextData, &mut TextEdit) -> bool
|
||||
let doc = ds.doc_mut();
|
||||
let n = doc.get_mut(te.id)?;
|
||||
let NodeKind::Text(t) = &mut n.kind else { return Err(EngineError::Other("not a text layer".into())) };
|
||||
let derived_before = crate::cmd::props::text_layer_name(&t.characters);
|
||||
if !f(t, &mut te) {
|
||||
return Ok(false);
|
||||
}
|
||||
// Any edit ends a composition (a commit replaces it with the final text).
|
||||
te.preedit = None;
|
||||
let len = t.char_count();
|
||||
let derived_after = crate::cmd::props::text_layer_name(&t.characters);
|
||||
te.caret = te.caret.min(len);
|
||||
te.anchor = te.anchor.min(len);
|
||||
// The layer's name follows the characters while it is the name they derive, so the Layers
|
||||
// panel keeps up with what the canvas shows; a name the author typed is left alone.
|
||||
if crate::cmd::props::text_name_is_derived(&n.name, derived_before.as_ref()) {
|
||||
n.name = derived_after.unwrap_or_default();
|
||||
}
|
||||
if n.source.is_some() {
|
||||
n.overrides.insert("characters".into());
|
||||
}
|
||||
|
||||
@@ -41,6 +41,30 @@ pub(crate) fn normalize_line_endings(s: &str) -> String {
|
||||
s.replace("\r\n", "\n").replace(['\r', '\u{2028}', '\u{2029}'], "\n")
|
||||
}
|
||||
|
||||
/// The name a text layer derives from its characters: the first line, at most 64 characters, or
|
||||
/// None when that line is blank (a layer with no name of its own, which the Layers panel shows by
|
||||
/// its kind).
|
||||
///
|
||||
/// A layer whose name is this derived name is one nobody has renamed, so the editor keeps its name
|
||||
/// in step as the characters are typed ([`crate::textedit`]); any other name was chosen by the
|
||||
/// author and is kept. Commands never rename, so a layer named for its role keeps that name as a
|
||||
/// field key even when `characters` replace text that reads like it.
|
||||
pub(crate) fn text_layer_name(characters: &str) -> Option<String> {
|
||||
let first: String = characters.lines().next().unwrap_or("").chars().take(64).collect();
|
||||
(!first.trim().is_empty()).then_some(first)
|
||||
}
|
||||
|
||||
/// Whether `name` is one a text layer was given for its characters rather than by its author, so
|
||||
/// the editor may keep it in step with them. Blank text leaves a layer named `""` when the Text
|
||||
/// tool draws it and `Text` when `node.create` makes it, and both read as unnamed in the panel.
|
||||
pub(crate) fn text_name_is_derived(name: &str, derived: Option<&String>) -> bool {
|
||||
match derived {
|
||||
Some(d) => name == d,
|
||||
// `NodeKind::Text(_).label()`, which the panel falls back to for an unnamed layer.
|
||||
None => name.is_empty() || name == "Text",
|
||||
}
|
||||
}
|
||||
|
||||
/// camelCase object keys → snake_case (accept the inspect/camel forms for serde-backed values).
|
||||
pub fn snake(v: &Value) -> Value {
|
||||
match v {
|
||||
|
||||
@@ -748,3 +748,71 @@ fn a_link_is_underlined_by_default_and_underline_takes_it_off() {
|
||||
s.execute("text.underline", &json!({"ids": [f]})).unwrap();
|
||||
assert_eq!(inner_style(&s).decoration, TextDecoration::None, "Underline on the frame reads the text inside it");
|
||||
}
|
||||
|
||||
fn layer_name(s: &Session, t: NodeId) -> String {
|
||||
s.doc().unwrap().node(t).unwrap().name.clone()
|
||||
}
|
||||
|
||||
/// Select everything in the layer being edited and delete it.
|
||||
fn clear_edited_text(s: &mut Session, t: NodeId) {
|
||||
let len = s.doc().unwrap().node(t).unwrap().kind.text().unwrap().char_count();
|
||||
crate::textedit::set_caret(s, 0, false).unwrap();
|
||||
crate::textedit::set_caret(s, len, true).unwrap();
|
||||
crate::textedit::key(s, "Backspace", false, false, false).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_renames_a_text_layer_that_still_carries_the_name_its_characters_gave_it() {
|
||||
// The Layers panel shows a layer's name, so a layer still carrying the name its characters
|
||||
// gave it has to follow them while they are typed or the panel and the canvas disagree
|
||||
// (#1747). The target app's plugin API calls this `autoRename`: "Whether updating the
|
||||
// characters in the text node should update the name of the node… This is automatically reset
|
||||
// to false if `name` is modified in order to allow the node to keep the new name."
|
||||
let mut s = Session::new();
|
||||
|
||||
// Nobody renamed this layer, so typing in it keeps its name up to date.
|
||||
let t = text(&mut s, "hello", json!({}));
|
||||
assert_eq!(layer_name(&s, t), "hello", "a new layer is named after its first line");
|
||||
crate::textedit::begin(&mut s, t, Some(5), false).unwrap();
|
||||
crate::textedit::insert(&mut s, " there").unwrap();
|
||||
assert_eq!((chars(&s, t).as_str(), layer_name(&s, t).as_str()), ("hello there", "hello there"), "the name follows what is typed");
|
||||
|
||||
// A name the author typed is theirs: later typing leaves it alone.
|
||||
s.execute("node.set", &json!({"id": t, "props": {"name": "Greeting"}})).unwrap();
|
||||
crate::textedit::begin(&mut s, t, Some(11), false).unwrap();
|
||||
crate::textedit::insert(&mut s, "!").unwrap();
|
||||
assert_eq!((chars(&s, t).as_str(), layer_name(&s, t).as_str()), ("hello there!", "Greeting"), "a chosen name is kept");
|
||||
|
||||
// Only the first line names the layer.
|
||||
let v = text(&mut s, "first", json!({}));
|
||||
crate::textedit::begin(&mut s, v, Some(5), false).unwrap();
|
||||
crate::textedit::insert(&mut s, "\nsecond").unwrap();
|
||||
assert_eq!((chars(&s, v).as_str(), layer_name(&s, v).as_str()), ("first\nsecond", "first"), "the first line alone names it");
|
||||
|
||||
// Emptied text reads as a new empty text layer does, not as the word that was deleted.
|
||||
clear_edited_text(&mut s, v);
|
||||
assert_eq!((chars(&s, v).as_str(), layer_name(&s, v).as_str()), ("", ""), "emptied text has no name of its own, so the panel shows its kind");
|
||||
|
||||
// The usual way a layer is made: the Text tool, a click, then typing into the empty layer.
|
||||
use crate::tools::{PointerEvent, PointerKind, ToolId};
|
||||
s.set_tool(ToolId::Text);
|
||||
s.pointer(&PointerEvent::at(PointerKind::Down, 300.0, 300.0, 1.0)).unwrap();
|
||||
s.pointer(&PointerEvent::at(PointerKind::Up, 300.0, 300.0, 1.0)).unwrap();
|
||||
let drawn = s.ds().unwrap().text_edit.as_ref().unwrap().id;
|
||||
assert_eq!(layer_name(&s, drawn), "", "a layer the tool drew has no name of its own yet");
|
||||
crate::textedit::insert(&mut s, "Pricing").unwrap();
|
||||
assert_eq!((chars(&s, drawn).as_str(), layer_name(&s, drawn).as_str()), ("Pricing", "Pricing"), "the first word typed names it");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_characters_as_a_command_never_renames_the_layer() {
|
||||
// Only the editor follows the characters. Layer names are field keys for `data.fill`,
|
||||
// component property bindings and templates, which write `characters` into a layer named for
|
||||
// its role ("Title") and look it up by that name again, so a command must leave the name as it
|
||||
// is even when it reads like the text it replaces.
|
||||
let mut s = Session::new();
|
||||
let t = text(&mut s, "Title", json!({}));
|
||||
assert_eq!(layer_name(&s, t), "Title");
|
||||
s.execute("node.set", &json!({"id": t, "props": {"characters": "Site em 7 dias"}})).unwrap();
|
||||
assert_eq!((chars(&s, t).as_str(), layer_name(&s, t).as_str()), ("Site em 7 dias", "Title"), "the field keeps its name");
|
||||
}
|
||||
@@ -75,6 +75,7 @@ blockers.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-03 | patch | M4, #1747: a text layer still carrying the name its characters gave it is renamed as they are typed, so the Layers panel no longer shows the word a layer started with while the canvas shows another (`hello` typed into `hello there` read as `hello` in the panel). A name the author typed is kept, and only the editor follows the characters: commands leave the name alone, so the layer names `data.fill`, component property bindings and templates use as field keys stay put. Tests: `text_behaviour_tests::typing_renames_a_text_layer_that_still_carries_the_name_its_characters_gave_it`, `setting_characters_as_a_command_never_renames_the_layer`. Grade unchanged |
|
||||
| 2026-10-31 | minor | M12: as the help describes, holding Cmd/Ctrl while moving a connector's end places it on any part of an object instead of snapping to a side. It keeps that spot as a fraction of the object's box, so it follows the object when it moves or resizes, and without the key an end snaps as before. `board.connector {fromAt, toAt}` and `board.reattach {at}` take it; saved only when set. Numbers unchanged |
|
||||
| 2026-10-31 | patch | M11: a trackpad or touch tap sends its press and release in one frame, so a plugin panel's field committed in the frame its button was clicked and the value was lost: the Counter's Set count… kept the old count. The plugin now gets both, the field's value first. The `window.figcraft` docs name `screenshot`'s `data`. Numbers unchanged |
|
||||
| 2026-10-31 | patch | M4: Dash and Custom strokes have a Dash cap (None, Round, Square) in the advanced stroke settings and `node.set {dashCap}`, on both ends of every dash on the canvas and in PNG, PDF and SVG. Unset, dashes keep taking the end point caps, so older files look the same; the saved file writes it only when set. Numbers unchanged |
|
||||
|
||||
@@ -648,6 +648,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-03 | patch | M4, #1747: a text layer still carrying the name its characters gave it is renamed as they are typed, so the Layers panel no longer shows the word a layer started with while the canvas shows another (`hello` typed into `hello there` read as `hello` in the panel). A name the author typed is kept, and only the editor follows the characters: commands leave the name alone, so the layer names `data.fill`, component property bindings and templates use as field keys stay put. Tests: `text_behaviour_tests::typing_renames_a_text_layer_that_still_carries_the_name_its_characters_gave_it`, `setting_characters_as_a_command_never_renames_the_layer`. Grade unchanged |
|
||||
| 2026-11-01 | patch | M4: bulleted lists draw the same bullet at every level of indentation ("Figma bullets will keep the same styling at all levels of indentation", Create bulleted and numbered lists); the deeper levels drew a white bullet and a small square, pinned as silent before. Numbered levels (1., a., i.) are unchanged. Found by a documentation audit of the text rules. Test: `tail_tests::list_numbering_by_level` (the bullets row). Numbers unchanged |
|
||||
| 2026-11-01 | patch | M4: typing `1)` followed by a space at the start of a plain paragraph starts a numbered list, as `1.` already did ("Enter one of the creation characters below followed by a Space: 1. 1)", Create bulleted and numbered lists). Found by a documentation audit of the text rules. Test: `text_tail_tests::typing_a_creation_character_and_a_space_starts_a_list`. Numbers unchanged |
|
||||
| 2026-11-01 | patch | M4: a link made with `text.link` (Cmd/Ctrl+K, Shift+Cmd/Ctrl+U) underlines the linked text unless it has a decoration of its own ("Links are styled with an underline by default. Use the keyboard shortcut to remove the underline", Add links to text); Underline (Cmd/Ctrl+U) takes the underline off and the link stays. Found by a documentation audit of the text rules. Test: `text_behaviour_tests::a_link_is_underlined_by_default_and_underline_takes_it_off`. Numbers unchanged |
|
||||
|
||||
Reference in new issue
Block a user