Browse / github.com/storytold/designcraft / patch
fix(ui): add Fit Frame to Content to text-frame context menu (#1852)
Add Fit Frame to Content to the text-frame context menu ## Summary Please add **Fit Frame to Content** to the context menu for a selected text frame. ## Environment - DesignCraft 0.13.0 - macOS - Context menu opened with a two-finger trackpad click ## Steps to reproduce 1. Create a text frame and enter some text. 2. Select the text frame with the Selection tool. 3. Open its context menu with a two-finger trackpad click. ## Actual behavior The text-frame context menu does not include Fitting or Fit Frame to Content. ## Expected behavior The context menu should offer Fit Frame to Content, either directly or under Fitting, using the existing Objec
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Zaxaerith (imported from pull request #1866, open)
- Base
ed8aabd1dc5a88652a076631c2d46886ca60c031- Size
- +70 −5 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
7b8ec0ccc82af7de597b7ad4
How it went
No other operator has reported using it yet.
Diff
raw · ROADMAP.md crates/ui-egui/src/menus.rs
@@ -12,6 +12,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web. +- **2026-10-26** · Text-frame context menus reuse Object › Fitting, including Fit Frame to Content (#1852); mixed text/graphic selections show it once. No readiness percentage change. - **2026-10-25** · The application bar uses craft-window's shared title-bar code. A drag that starts on a floating panel over the bar moves the panel, not the window, and in full screen the bar no longer zooms or moves the window. No readiness percentage change.@@ -2994,8 +2994,8 @@ const CONTEXT_OBJECT: &[&str] = &[ "cmd:object.showAll", "-", ];-/// Added to the object menu when a graphic frame is selected.-const CONTEXT_GRAPHIC_FRAME: &[&str] = &["menu:Object/Fitting"];+/// Added once to the object menu when a graphic or text frame is selected.+const CONTEXT_FITTING: &[&str] = &["menu:Object/Fitting"]; const CONTEXT_OBJECT_TAIL: &[&str] = &["menu:Object/Content", "menu:Object/Effects"]; /// Added to the object menu when a text frame is selected. const CONTEXT_TEXT_FRAME: &[&str] = &["-", "cmd:type.fillWithPlaceholder", "cmd:object.textFrameOptions"];@@ -3049,8 +3049,8 @@ fn context_entries(app: &DesignApp, kind: ContextMenu) -> Vec<&'static str> { (items().any(|it| matches!(it.content, designcraft_doc::Content::Graphic(_))), items().any(|it| it.is_text_frame())) }); let mut v = CONTEXT_OBJECT.to_vec();- if graphic {- v.extend(CONTEXT_GRAPHIC_FRAME);+ if graphic || text {+ v.extend(CONTEXT_FITTING); } v.extend(CONTEXT_OBJECT_TAIL); if text {@@ -5691,7 +5691,7 @@ mod tests { #[test] fn context_menus_hold_commands_and_menu_bar_submenus() { let tree = menu_tree();- for entries in [CONTEXT_OBJECT, CONTEXT_GRAPHIC_FRAME, CONTEXT_OBJECT_TAIL, CONTEXT_TEXT_FRAME, CONTEXT_TEXT, CONTEXT_CANVAS] {+ for entries in [CONTEXT_OBJECT, CONTEXT_FITTING, CONTEXT_OBJECT_TAIL, CONTEXT_TEXT_FRAME, CONTEXT_TEXT, CONTEXT_CANVAS] { for path in entries.iter().filter_map(|e| e.strip_prefix("menu:")) { assert!(menu_bar_submenu(&tree, path).is_some(), "no menu bar submenu {path}"); }@@ -5705,6 +5705,70 @@ mod tests { } } + #[test]+ fn frame_context_menus_share_the_existing_fitting_submenu() {+ let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());+ app.run("file.new", json!({})).unwrap();+ let text = app.run("frame.create", json!({"rect": [36, 36, 200, 100], "content": "text", "caret": false})).unwrap()["id"].clone();+ let graphic = app.run("frame.create", json!({"rect": [236, 36, 400, 100]})).unwrap()["id"].clone();+ let png = designcraft_render::Rendered { width: 1, height: 1, pixels: vec![200; 4] }.to_png();+ app.run("file.place", json!({"base64": designcraft_engine::cmd::base64_encode(&png), "name": "a.png", "frame": graphic})).unwrap();+ let mut expected = Vec::new();+ walk(&[menu_bar_submenu(&menu_tree(), "Object/Fitting").unwrap()], &mut expected);+ for (ids, has_text) in [(json!([text]), true), (json!([graphic]), false), (json!([text, graphic]), true), (json!([graphic, text]), true)] {+ app.run("selection.set", json!({"ids": ids})).unwrap();+ let entries = context_entries(&app, ContextMenu::Object);+ assert_eq!(entries.iter().filter(|e| **e == "menu:Object/Fitting").count(), 1);+ assert_eq!(entries.contains(&"cmd:type.fillWithPlaceholder"), has_text);+ assert_eq!(entries.contains(&"cmd:object.textFrameOptions"), has_text);+ let runs = context_parse(&entries);+ let fitting: Vec<_> = runs+ .iter()+ .flat_map(|(_, items)| items)+ .filter_map(|item| if let Item::Sub(name, children) = item { (name == "Fitting").then_some(children) } else { None })+ .collect();+ assert_eq!(fitting.len(), 1);+ let fitting = fitting.first().unwrap();+ let mut commands = Vec::new();+ walk(fitting, &mut commands);+ assert_eq!(commands, expected);+ assert_eq!(commands.iter().filter(|(_, id, _)| id == "fit.frameToContent").count(), 1);+ let fit = fitting.iter().find_map(|item| match item {+ Item::Cmd { label, id, params, shortcut } if id == "fit.frameToContent" => Some((label.as_str(), params, *shortcut)),+ _ => None,+ });+ assert_eq!(fit, Some(("Fit Frame to Content", &Value::Null, Some("Cmd+Alt+C"))));+ assert_eq!(ui_label("fit.frameToContent"), Some(("Fit Frame to Content", Some("Cmd+Alt+C"))));+ assert_eq!(shortcut_of(&app, "fit.frameToContent").as_deref(), Some("Cmd+Alt+C"));+ assert_eq!(menu_enabled(&app, "fit.frameToContent"), enabled(&app, "object.fit"));+ }+ }++ #[test]+ fn context_menus_without_selected_frames_have_no_frame_actions() {+ let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());+ let check = |app: &DesignApp| {+ for kind in [ContextMenu::Object, ContextMenu::Canvas] {+ let entries = context_entries(app, kind);+ assert!(!entries.contains(&"menu:Object/Fitting"));+ assert!(!entries.contains(&"cmd:type.fillWithPlaceholder"));+ assert!(!entries.contains(&"cmd:object.textFrameOptions"));+ let mut commands = Vec::new();+ for (_, items) in context_parse(&entries) {+ walk(&items, &mut commands);+ }+ assert!(!commands.iter().any(|(_, id, _)| id == "fit.frameToContent"));+ }+ };+ check(&app); // No document.+ app.run("file.new", json!({})).unwrap();+ check(&app); // Empty selection.+ app.run("frame.create", json!({"rect": [36, 36, 200, 100], "content": "unassigned"})).unwrap();+ check(&app); // A selected shape without text or graphic content.+ app.session.doc_mut().unwrap().selection.items = vec![designcraft_doc::ItemId(u64::MAX)];+ check(&app); // A stale selection must not expose frame actions.+ }+ #[test] fn the_type_menu_follows_indesigns_order() { // InDesign's Type menu, as Chris Nielsen opens it in "Numbering Page Sections" (2:09).