From 861afad3385c7c81fae32d2f6c80ed915f204bc3 Mon Sep 17 00:00:00 2001 From: paaspaas00 <151774837+paaspaas00@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:27:32 +0200 Subject: [PATCH 1/4] Swatches panel: rename a swatch in place As in InDesign, a click on the name of the swatch picked already (a slow double-click) turns the name into a field: Enter renames the swatch (swatch.options, so everything that uses it follows), Escape keeps the name. A double-click still opens Swatch Options, and the special swatches keep their names. Found in the Learn Skills Daily course, chapter 23. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/panels/swatches.rs | 148 +++++++++++++++++++++++++- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index a7b5105d3..44977f5c6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-21** · Swatches panel: a swatch is renamed in place, as in InDesign: click it, then click its name again (a slow double-click), type, and Enter (Escape keeps the name). Everything that uses it follows; a double-click still opens Swatch Options, and [None], [Registration], [Paper] and [Black] keep their names. Found in the Learn Skills Daily course, chapter 23. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 3e7030e87..1abe1d6c9 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -144,6 +144,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { /// Temp-data key of the swatch last clicked (Delete Swatch removes it). const PICKED: &str = "swatches_picked"; +/// Temp-data key of the swatch renamed in place and its name being typed. +const RENAMING: &str = "swatches_renaming"; +/// Temp-data key of a click on the picked swatch's name: (swatch, time). Unless it turns into a +/// double-click (Swatch Options), it starts renaming. +const RENAME_CLICK: &str = "swatches_rename_click"; /// The Swatches panel's top row, as InDesign's: a Fill/Stroke proxy (click: the other in front), /// Formatting Affects Container / Text, and the tint of the colour in front. `target` is @@ -232,6 +237,43 @@ fn folder_button(ui: &mut egui::Ui, tip: &str) -> egui::Response { }) } +/// The name field of a swatch being renamed in place, over `rect`; false when `sw` isn't. Enter or +/// a click elsewhere renames it (`swatch.options`), Escape leaves it as it was. +fn rename_field(app: &mut DesignApp, ui: &mut egui::Ui, sw: &designcraft_color::swatch::Swatch, rect: egui::Rect) -> bool { + let key = egui::Id::new(RENAMING); + let Some((name, mut text)) = ui.data(|d| d.get_temp::<(String, String)>(key)).filter(|(n, _)| *n == sw.name) else { return false }; + let id = egui::Id::new(("swatch_rename", &sw.name)); + let edit = egui::TextEdit::singleline(&mut text).id(id).font(egui::FontId::proportional(12.5)).margin(vec2(2.0, 1.0)); + let resp = ui.put(rect, edit); + if !resp.has_focus() && !resp.lost_focus() { + // The field opens with the name selected. + resp.request_focus(); + if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), id) { + state + .cursor + .set_char_range(Some(egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(text.chars().count())))); + state.store(ui.ctx(), id); + } + } + if resp.lost_focus() { + ui.data_mut(|d| d.remove_temp::<(String, String)>(key)); + let to = text.trim(); + if !ui.input(|i| i.key_pressed(egui::Key::Escape)) && !to.is_empty() && to != name { + match app.run("swatch.options", json!({"name": name, "to": to})) { + Ok(_) => ui.data_mut(|d| { + d.insert_temp(egui::Id::new(PICKED), to.to_string()); + }), + Err(e) => { + app.status_error("Swatch Options", e); + } + } + } + } else { + ui.data_mut(|d| d.insert_temp(key, (name, text))); + } + true +} + /// One Swatches panel row: chip, name, type; click applies it to the fill or stroke in front of /// the proxy (of the frames or their text, see [`super::colors_affect_text`]), the context menu /// to the stroke, colour groups and delete. @@ -266,7 +308,10 @@ fn swatch_row( let (c, g) = crate::widgets::swatch_colors(doc, &sw.name, 1.0); let chip = egui::Rect::from_min_size(row.min + vec2(4.0 + indent, 3.0), vec2(16.0, 16.0)); crate::widgets::paint_chip(ui.painter(), chip, c, g); - ui.painter().text(row.min + vec2(28.0 + indent, 11.0), egui::Align2::LEFT_CENTER, &sw.name, egui::FontId::proportional(12.5), t.text); + let name_rect = egui::Rect::from_min_max(row.min + vec2(26.0 + indent, 1.0), egui::pos2(row.max.x - 72.0, row.max.y - 1.0)); + if !rename_field(app, ui, sw, name_rect) { + ui.painter().text(row.min + vec2(28.0 + indent, 11.0), egui::Align2::LEFT_CENTER, &sw.name, egui::FontId::proportional(12.5), t.text); + } // Type indicators: spot dot / process square, colour mode. let kind = match &sw.value { SwatchValue::Color { color, color_type } => { @@ -286,13 +331,32 @@ fn swatch_row( } ui.painter().text(row.right_center() - vec2(6.0, 0.0), egui::Align2::RIGHT_CENTER, kind, egui::FontId::proportional(10.5), t.text_dim); if resp.clicked() && !resp.double_clicked() { + // A click on the name of the swatch picked already renames it in place, as InDesign's + // (a slow double-click), unless it turns into a double-click. + let picked = ui.data(|d| d.get_temp::(egui::Id::new(PICKED))).is_some_and(|p| p == sw.name); + if picked && !sw.locked && resp.interact_pointer_pos().is_some_and(|p| name_rect.contains(p)) { + let now = ui.input(|i| i.time); + ui.data_mut(|d| d.insert_temp(egui::Id::new(RENAME_CLICK), (sw.name.clone(), now))); + } ui.data_mut(|d| d.insert_temp(egui::Id::new(PICKED), sw.name.clone())); let _ = super::apply_swatch(app, app.ui.stroke_front, &sw.name, None); } // Double-click: Swatch Options (the first click has applied the swatch). if resp.double_clicked() { + ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))); let _ = app.run("app.swatchOptionsDialog", json!({"name": sw.name})); } + if let Some((name, at)) = ui.data(|d| d.get_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))).filter(|(n, _)| *n == sw.name) { + let wait = ui.ctx().options(|o| o.input_options.max_double_click_delay); + if ui.input(|i| i.time) - at > wait { + ui.data_mut(|d| { + d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK)); + d.insert_temp(egui::Id::new(RENAMING), (name.clone(), name)); + }); + } else { + ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(wait)); + } + } resp.context_menu(|ui| { if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Swatch Options…"))).clicked() { let _ = app.run("app.swatchOptionsDialog", json!({"name": sw.name})); @@ -1135,6 +1199,88 @@ mod swatches_header_tests { assert!(h.state().session.doc().unwrap().doc.swatch("Brand").is_none()); } + /// The course's chapter 23: a swatch added as "C=14 M=8 Y=95 K=0" is renamed in the panel by + /// clicking it, then clicking its name again: the name becomes a field, and Enter renames the + /// swatch (and what uses it). A double-click still opens Swatch Options, Escape keeps the name, + /// and [Black] can't be renamed. + #[test] + fn a_second_click_on_the_picked_swatch_renames_it_in_place() { + use egui::accesskit::Role; + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + app.run("frame.create", json!({"rect": [36, 36, 200, 200], "content": "unassigned"})).unwrap(); + let added = app.run("swatch.create", json!({"color": {"c": 14, "m": 8, "y": 95, "k": 0}})).unwrap()["name"].as_str().unwrap().to_string(); + assert_eq!(added, "C=14 M=8 Y=95 K=0"); + // Frames 50 ms apart: the double-click time is six of them. + let mut h = Harness::builder().with_size(vec2(300.0, 520.0)).with_step_dt(0.05).build_ui_state( + |ui, app: &mut DesignApp| { + let id = egui::Id::new("test_fonts"); + if ui.data(|d| d.get_temp::(id)).is_none() { + crate::theme::install_fonts(ui.ctx(), ""); + ui.data_mut(|d| d.insert_temp(id, true)); + return; + } + show(app, ui); + }, + app, + ); + h.run_steps(3); + // The panel's own fields (Tint); renaming adds one. + let fields = h.query_all_by_role(Role::TextInput).count(); + let field = |h: &Harness<'_, DesignApp>| h.query_all_by_role(Role::TextInput).count() > fields; + let has = |h: &Harness<'_, DesignApp>, n: &str| h.state().session.doc().unwrap().doc.swatch(n).is_some(); + let name_at = |h: &Harness<'_, DesignApp>, label: &str| h.get_by_label(label).rect().left_center() + vec2(40.0, 0.0); + let click = |h: &mut Harness<'_, DesignApp>, at: egui::Pos2| { + h.hover_at(at); + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE }); + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE }); + h.step(); + }; + // A click on the name: applied, and picked. + let at = name_at(&h, &added); + click(&mut h, at); + h.run_steps(10); + assert_eq!(items(h.state())[0].fill.swatch, added); + assert!(!field(&h)); + // Again, after the double-click time: the name is a field, with the name selected. + click(&mut h, at); + h.run_steps(10); + assert!(field(&h), "the name is a field"); + h.event(egui::Event::Text("Lemon Yellow Sun".into())); + h.step(); + h.key_press(egui::Key::Enter); + h.run_steps(3); + assert!(has(&h, "Lemon Yellow Sun") && !has(&h, &added)); + assert_eq!(items(h.state())[0].fill.swatch, "Lemon Yellow Sun", "what uses it follows"); + // Escape keeps the name. + let at = name_at(&h, "Lemon Yellow Sun"); + h.run_steps(10); + click(&mut h, at); + h.run_steps(10); + assert!(field(&h)); + h.event(egui::Event::Text("Sour".into())); + h.step(); + h.key_press(egui::Key::Escape); + h.run_steps(3); + assert!(has(&h, "Lemon Yellow Sun") && !field(&h)); + // A double-click on it opens Swatch Options, and doesn't rename. + h.run_steps(20); + click(&mut h, at); + click(&mut h, at); + h.run_steps(10); + assert_eq!(h.state().ui.dialog.as_ref().map(|d| d.id.as_str()), Some("swatchOptions")); + assert!(!field(&h), "no rename"); + // [Black] keeps its name. + h.state_mut().ui.dialog = None; + h.run_steps(20); + let at = name_at(&h, "[Black]"); + click(&mut h, at); + h.run_steps(10); + click(&mut h, at); + h.run_steps(10); + assert!(!field(&h)); + } + #[test] fn the_tint_field_keeps_each_objects_own_swatch() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); From c84ce7a545dd510981a4c4e28991f194c9c38959 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 22 Oct 2026 01:25:12 +0200 Subject: [PATCH 2/4] Swatches: a click on another swatch keeps it picked and cancels a waiting rename Ending a rename by clicking another swatch keeps that swatch picked (for Delete Swatch) instead of the renamed one. A click on another swatch while the rename's double-click wait runs cancels the rename. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/swatches.rs | 37 +++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 1abe1d6c9..a41b71d5b 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -260,8 +260,12 @@ fn rename_field(app: &mut DesignApp, ui: &mut egui::Ui, sw: &designcraft_color:: let to = text.trim(); if !ui.input(|i| i.key_pressed(egui::Key::Escape)) && !to.is_empty() && to != name { match app.run("swatch.options", json!({"name": name, "to": to})) { + // Still picked under its new name, unless the click that ended the rename picked + // another swatch. Ok(_) => ui.data_mut(|d| { - d.insert_temp(egui::Id::new(PICKED), to.to_string()); + if d.get_temp::(egui::Id::new(PICKED)).is_none_or(|p| p == name) { + d.insert_temp(egui::Id::new(PICKED), to.to_string()); + } }), Err(e) => { app.status_error("Swatch Options", e); @@ -334,6 +338,8 @@ fn swatch_row( // A click on the name of the swatch picked already renames it in place, as InDesign's // (a slow double-click), unless it turns into a double-click. let picked = ui.data(|d| d.get_temp::(egui::Id::new(PICKED))).is_some_and(|p| p == sw.name); + // A click on another swatch cancels a rename still waiting. + ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))); if picked && !sw.locked && resp.interact_pointer_pos().is_some_and(|p| name_rect.contains(p)) { let now = ui.input(|i| i.time); ui.data_mut(|d| d.insert_temp(egui::Id::new(RENAME_CLICK), (sw.name.clone(), now))); @@ -348,7 +354,10 @@ fn swatch_row( } if let Some((name, at)) = ui.data(|d| d.get_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))).filter(|(n, _)| *n == sw.name) { let wait = ui.ctx().options(|o| o.input_options.max_double_click_delay); - if ui.input(|i| i.time) - at > wait { + if ui.data(|d| d.get_temp::(egui::Id::new(PICKED))).is_none_or(|p| p != name) { + // Another swatch picked since: no rename. + ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))); + } else if ui.input(|i| i.time) - at > wait { ui.data_mut(|d| { d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK)); d.insert_temp(egui::Id::new(RENAMING), (name.clone(), name)); @@ -1279,6 +1288,30 @@ mod swatches_header_tests { click(&mut h, at); h.run_steps(10); assert!(!field(&h)); + // A click on another swatch while the rename waits: that one is picked, and nothing + // renames. + let picked = |h: &Harness<'_, DesignApp>| h.ctx.data(|d| d.get_temp::(egui::Id::new(super::PICKED))); + let (lemon, paper) = (name_at(&h, "Lemon Yellow Sun"), name_at(&h, "[Paper]")); + h.run_steps(20); + click(&mut h, lemon); + h.run_steps(10); + click(&mut h, lemon); + click(&mut h, paper); + h.run_steps(10); + assert!(!field(&h), "the waiting rename is cancelled"); + assert_eq!(picked(&h).as_deref(), Some("[Paper]")); + // Renaming, a click on another swatch renames, and picks that one (for Delete Swatch). + click(&mut h, lemon); + h.run_steps(10); + click(&mut h, lemon); + h.run_steps(10); + assert!(field(&h)); + h.event(egui::Event::Text("Lemon".into())); + h.step(); + click(&mut h, paper); + h.run_steps(5); + assert!(has(&h, "Lemon") && !field(&h)); + assert_eq!(picked(&h).as_deref(), Some("[Paper]"), "the swatch clicked stays picked"); } #[test] From 14d68ee0b1221190252868a82aa18a86fa1a83da Mon Sep 17 00:00:00 2001 From: moppeeer Date: Sun, 25 Oct 2026 17:15:42 +0100 Subject: [PATCH 3/4] Swatches: a rename in place ends with its document The panel keeps the uid of the document it last showed; another one (switched to, or moved into its place by a close) drops the rename and the pending rename click, so a same-named swatch there isn't renamed. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/swatches.rs | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 14d187655..15b2f5873 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -11,6 +11,16 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; let doc = st.doc.clone(); + // Another document (switched to, or moved to this one's place by a close) drops a rename in + // place: a swatch of the same name there isn't the one being renamed. + let uid = st.uid; + if ui.data(|d| d.get_temp::(egui::Id::new(RENAME_DOC))) != Some(uid) { + ui.data_mut(|d| { + d.remove_temp::<(String, String)>(egui::Id::new(RENAMING)); + d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK)); + d.insert_temp(egui::Id::new(RENAME_DOC), uid); + }); + } let target = super::color_target(app); header(app, ui, &doc, target.as_ref()); // The row of the colour in front of the proxy is highlighted. @@ -159,6 +169,8 @@ const RENAMING: &str = "swatches_renaming"; /// Temp-data key of a click on the picked swatch's name: (swatch, time). Unless it turns into a /// double-click (Swatch Options), it starts renaming. const RENAME_CLICK: &str = "swatches_rename_click"; +/// Temp-data key of the document (its uid) the panel last showed: a rename belongs to it. +const RENAME_DOC: &str = "swatches_rename_doc"; /// The Swatches panel's top row, as InDesign's: a Fill/Stroke proxy (click: the other in front), /// Formatting Affects Container / Text, and the tint of the colour in front. `target` is @@ -1295,6 +1307,30 @@ mod swatches_header_tests { /// clicking it, then clicking its name again: the name becomes a field, and Enter renames the /// swatch (and what uses it). A double-click still opens Swatch Options, Escape keeps the name, /// and [Black] can't be renamed. + /// A rename in place belongs to its document: with another one active, a swatch of the same + /// name there gets no field, and nothing is renamed. + #[test] + fn a_rename_in_place_ends_with_its_document() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let color = json!({"color": {"c": 14, "m": 8, "y": 95, "k": 0}}); + let name = app.run("swatch.create", color.clone()).unwrap()["name"].as_str().unwrap().to_string(); + let mut h = Harness::builder().with_size(vec2(300.0, 520.0)).build_ui_state(|ui, app: &mut DesignApp| super::show(app, ui), app); + h.run_steps(2); + let fields = h.query_all_by_role(egui::accesskit::Role::TextInput).count(); + let renaming = egui::Id::new(super::RENAMING); + h.ctx.data_mut(|d| d.insert_temp(renaming, (name.clone(), "Typed".to_string()))); + h.run_steps(2); + assert!(h.query_all_by_role(egui::accesskit::Role::TextInput).count() > fields, "the name is a field"); + // Another document with a swatch of the same name. + h.state_mut().run("file.new", json!({})).unwrap(); + h.state_mut().run("swatch.create", color).unwrap(); + h.run_steps(3); + assert!(h.ctx.data(|d| d.get_temp::<(String, String)>(renaming)).is_none(), "the rename is dropped"); + let doc = &h.state().session.active().unwrap().doc; + assert!(doc.swatch(&name).is_some() && doc.swatch("Typed").is_none()); + } + #[test] fn a_second_click_on_the_picked_swatch_renames_it_in_place() { use egui::accesskit::Role; From c6573d95577899826ccd000144df52d351c0b60c Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 29 Oct 2026 23:46:49 +0100 Subject: [PATCH 4/4] Swatch rename test: the app's fonts installed first, as main's panel tests do; egui 0.37 Id::unique Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/swatches.rs | 55 ++++++++++++++++----------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 7652251c5..789474bbe 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -14,11 +14,11 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { // Another document (switched to, or moved to this one's place by a close) drops a rename in // place: a swatch of the same name there isn't the one being renamed. let uid = st.uid; - if ui.data(|d| d.get_temp::(egui::Id::new(RENAME_DOC))) != Some(uid) { + if ui.data(|d| d.get_temp::(egui::Id::unique(RENAME_DOC))) != Some(uid) { ui.data_mut(|d| { - d.remove_temp::<(String, String)>(egui::Id::new(RENAMING)); - d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK)); - d.insert_temp(egui::Id::new(RENAME_DOC), uid); + d.remove_temp::<(String, String)>(egui::Id::unique(RENAMING)); + d.remove_temp::<(String, f64)>(egui::Id::unique(RENAME_CLICK)); + d.insert_temp(egui::Id::unique(RENAME_DOC), uid); }); } let target = super::color_target(app); @@ -262,9 +262,9 @@ fn folder_button(ui: &mut egui::Ui, tip: &str) -> egui::Response { /// The name field of a swatch being renamed in place, over `rect`; false when `sw` isn't. Enter or /// a click elsewhere renames it (`swatch.options`), Escape leaves it as it was. fn rename_field(app: &mut DesignApp, ui: &mut egui::Ui, sw: &designcraft_color::swatch::Swatch, rect: egui::Rect) -> bool { - let key = egui::Id::new(RENAMING); + let key = egui::Id::unique(RENAMING); let Some((name, mut text)) = ui.data(|d| d.get_temp::<(String, String)>(key)).filter(|(n, _)| *n == sw.name) else { return false }; - let id = egui::Id::new(("swatch_rename", &sw.name)); + let id = egui::Id::unique(("swatch_rename", &sw.name)); let edit = egui::TextEdit::singleline(&mut text).id(id).font(egui::FontId::proportional(12.5)).margin(vec2(2.0, 1.0)); let resp = ui.put(rect, edit); if !resp.has_focus() && !resp.lost_focus() { @@ -285,8 +285,8 @@ fn rename_field(app: &mut DesignApp, ui: &mut egui::Ui, sw: &designcraft_color:: // Still picked under its new name, unless the click that ended the rename picked // another swatch. Ok(_) => ui.data_mut(|d| { - if d.get_temp::(egui::Id::new(PICKED)).is_none_or(|p| p == name) { - d.insert_temp(egui::Id::new(PICKED), to.to_string()); + if d.get_temp::(egui::Id::unique(PICKED)).is_none_or(|p| p == name) { + d.insert_temp(egui::Id::unique(PICKED), to.to_string()); } }), Err(e) => { @@ -369,30 +369,30 @@ fn swatch_row( if resp.clicked() && !resp.double_clicked() { // A click on the name of the swatch picked already renames it in place, as InDesign's // (a slow double-click), unless it turns into a double-click. - let picked = ui.data(|d| d.get_temp::(egui::Id::new(PICKED))).is_some_and(|p| p == sw.name); + let picked = ui.data(|d| d.get_temp::(egui::Id::unique(PICKED))).is_some_and(|p| p == sw.name); // A click on another swatch cancels a rename still waiting. - ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))); + ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::unique(RENAME_CLICK))); if picked && !sw.locked && resp.interact_pointer_pos().is_some_and(|p| name_rect.contains(p)) { let now = ui.input(|i| i.time); - ui.data_mut(|d| d.insert_temp(egui::Id::new(RENAME_CLICK), (sw.name.clone(), now))); + ui.data_mut(|d| d.insert_temp(egui::Id::unique(RENAME_CLICK), (sw.name.clone(), now))); } ui.data_mut(|d| d.insert_temp(egui::Id::unique(PICKED), sw.name.clone())); let _ = super::apply_swatch(app, app.ui.stroke_front, &sw.name, None); } // Double-click: Swatch Options (the first click has applied the swatch). if resp.double_clicked() { - ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))); + ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::unique(RENAME_CLICK))); let _ = app.run("app.swatchOptionsDialog", json!({"name": sw.name})); } - if let Some((name, at)) = ui.data(|d| d.get_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))).filter(|(n, _)| *n == sw.name) { + if let Some((name, at)) = ui.data(|d| d.get_temp::<(String, f64)>(egui::Id::unique(RENAME_CLICK))).filter(|(n, _)| *n == sw.name) { let wait = ui.ctx().options(|o| o.input_options.max_double_click_delay); - if ui.data(|d| d.get_temp::(egui::Id::new(PICKED))).is_none_or(|p| p != name) { + if ui.data(|d| d.get_temp::(egui::Id::unique(PICKED))).is_none_or(|p| p != name) { // Another swatch picked since: no rename. - ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK))); + ui.data_mut(|d| d.remove_temp::<(String, f64)>(egui::Id::unique(RENAME_CLICK))); } else if ui.input(|i| i.time) - at > wait { ui.data_mut(|d| { - d.remove_temp::<(String, f64)>(egui::Id::new(RENAME_CLICK)); - d.insert_temp(egui::Id::new(RENAMING), (name.clone(), name)); + d.remove_temp::<(String, f64)>(egui::Id::unique(RENAME_CLICK)); + d.insert_temp(egui::Id::unique(RENAMING), (name.clone(), name)); }); } else { ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(wait)); @@ -1373,10 +1373,21 @@ mod swatches_header_tests { app.run("file.new", json!({})).unwrap(); let color = json!({"color": {"c": 14, "m": 8, "y": 95, "k": 0}}); let name = app.run("swatch.create", color.clone()).unwrap()["name"].as_str().unwrap().to_string(); - let mut h = Harness::builder().with_size(vec2(300.0, 520.0)).build_ui_state(|ui, app: &mut DesignApp| super::show(app, ui), app); - h.run_steps(2); + let mut h = Harness::builder().with_size(vec2(300.0, 520.0)).build_ui_state( + |ui, app: &mut DesignApp| { + let id = egui::Id::unique("rename_test_fonts"); + if ui.data(|d| d.get_temp::(id)).is_none() { + crate::theme::install_fonts(ui.ctx(), ""); + ui.data_mut(|d| d.insert_temp(id, true)); + return; + } + super::show(app, ui); + }, + app, + ); + h.run_steps(3); let fields = h.query_all_by_role(egui::accesskit::Role::TextInput).count(); - let renaming = egui::Id::new(super::RENAMING); + let renaming = egui::Id::unique(super::RENAMING); h.ctx.data_mut(|d| d.insert_temp(renaming, (name.clone(), "Typed".to_string()))); h.run_steps(2); assert!(h.query_all_by_role(egui::accesskit::Role::TextInput).count() > fields, "the name is a field"); @@ -1400,7 +1411,7 @@ mod swatches_header_tests { // Frames 50 ms apart: the double-click time is six of them. let mut h = Harness::builder().with_size(vec2(300.0, 520.0)).with_step_dt(0.05).build_ui_state( |ui, app: &mut DesignApp| { - let id = egui::Id::new("test_fonts"); + let id = egui::Id::unique("test_fonts"); if ui.data(|d| d.get_temp::(id)).is_none() { crate::theme::install_fonts(ui.ctx(), ""); ui.data_mut(|d| d.insert_temp(id, true)); @@ -1467,7 +1478,7 @@ mod swatches_header_tests { assert!(!field(&h)); // A click on another swatch while the rename waits: that one is picked, and nothing // renames. - let picked = |h: &Harness<'_, DesignApp>| h.ctx.data(|d| d.get_temp::(egui::Id::new(super::PICKED))); + let picked = |h: &Harness<'_, DesignApp>| h.ctx.data(|d| d.get_temp::(egui::Id::unique(super::PICKED))); let (lemon, paper) = (name_at(&h, "Lemon Yellow Sun"), name_at(&h, "[Paper]")); h.run_steps(20); click(&mut h, lemon);