Browse / github.com/storytold/designcraft / patch
Story Editor: View › Story Editor › Show/Hide Style Name Column, and a draggable column width, as in InDesign
Story Editor: View › Story Editor › Show/Hide Style Name Column, and a draggable column width, as in InDesign Builds on #1795 (merged), which adds the View › Story Editor submenu. InDesign's help says: "With the story editor active, choose View > Story Editor > Show Style Name Column or Hide Style Name Column. You can also adjust the width of the style name column by dragging the vertical bar. Subsequent story editor windows have the same column width." ([Editing text](https://web.archive.org/web/20251115065057/https://helpx.adobe.com/indesign/using/editing-text.html), "Use the Story Editor"). DesignCraft's Story Editor always showed the style name column at a fixed width of 130 points, and the Story
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- dotCooCoo (imported from pull request #1797, open)
- Base
1abeda4459032ad6e935e2ebfb66b65c2d75c8a4- Size
- +242 −16 in 21 files
- Review
- pending
- Shared
- 2026-10-28
- Id
b47ada1bb744973741083e43
How it went
No other operator has reported using it yet.
Diff
raw · ROADMAP.md crates/ui-egui/src/i18n/ar.rs crates/ui-egui/src/i18n/cs.rs crates/ui-egui/src/i18n/de.rs crates/ui-egui/src/i18n/it.rs crates/ui-egui/src/i18n/ja.rs crates/ui-egui/src/i18n/ka.rs crates/ui-egui/src/i18n/pl.rs crates/ui-egui/src/i18n/pt_br.rs crates/ui-egui/src/i18n/ru.rs crates/ui-egui/src/i18n/table.rs crates/ui-egui/src/i18n/uk.rs crates/ui-egui/src/menus/run.rs crates/ui-egui/src/menus/state.rs crates/ui-egui/src/menus/tables.rs crates/ui-egui/src/menus/tests.rs crates/ui-egui/src/menus/ui_commands.rs crates/ui-egui/src/state.rs crates/ui-egui/src/story_editor.rs docs/gaps.md docs/ui-parity.md
@@ -403,6 +403,7 @@ Newest first. - **2026-10-23** · `designcraft-cli mcp` takes `--automation-read-root` and `--automation-write-root` more than once, as the suite's other apps do (#1552): a path inside any root of its kind passes, and the roots' common parent, `..` or a link out of them and the other kind's roots stay refused. No readiness percentage change. - **2026-10-23** · Layout › Go Back and Go Forward (Cmd+Page Up / Cmd+Page Down, Ctrl on Windows and Linux) retrace the jumps between pages (Go to Page, the page and spread commands, the Pages panel) as InDesign's do, returning to where the view was on each page at the current zoom; a new jump after going back drops the pages ahead, scrolling adds no entry, and pages deleted since are skipped. No readiness percentage change. - **2026-10-24** · The Story Editor has InDesign's vertical depth ruler between the style column and the text, and View › Story Editor › Show/Hide Depth Ruler (`view.storyEditorDepthRuler`) shows or hides it in the Story Editor, remembered with the preferences. The ruler is marked in the document's vertical ruler units with how deep the story's text fills its frames, counted through its columns and threaded frames (lines set side by side, in the sub-columns of a split paragraph or on both sides of a wrap object, count once), and is shaded down to the overset point, where a red line now crosses the editor. No readiness percentage change.+- **2026-10-24** · View › Story Editor › Show/Hide Style Name Column (`view.storyEditorStyleColumn`) shows or hides the Story Editor's column of paragraph style names, as in InDesign, and dragging the bar between the column and the text makes the column wider or narrower (40 to 400 points; the command's `width` sets it too). Both settings are remembered with the preferences, so the editor opens with the same column the next time. No readiness percentage change. - **2026-10-24** · Split Note (`note.split {story, id, at}`, the Notes panel's Split Note button): as in InDesign, a note splits in two at the insertion point in its text. DesignCraft moves the text after it into a new note by the same author, anchored right after the first. No readiness percentage change. - **2026-10-24** · Type › Notes › Previous Note and Next Note (`note.previous`, `note.next`): as in InDesign, they move the insertion point from note to note in the story, putting the caret after the note's anchor. Each item is unavailable when no note lies in its direction in the story, or when the insertion point is in a table cell or a footnote. No readiness percentage change. - **2026-10-24** · Type › Notes › Convert to Note (`note.convertFromText {author?}`): as in InDesign, the selected text leaves the story and becomes the text of a new note, whose anchor takes its place; Convert Note to Text puts it back. DesignCraft refuses a selection in a table cell, or one that holds a note, footnote, endnote, anchored object, index marker, text anchor, cross-reference or table. No readiness percentage change.@@ -1562,6 +1562,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "إظهار/إخفاء اللوحات"), ("Show/Hide Panels Except Tools", "إظهار/إخفاء اللوحات باستثناء الأدوات"), ("Show/Hide Rulers", "إظهار/إخفاء المساطر"),+ ("Show/Hide Style Name Column", "إظهار/إخفاء عمود أسماء الأنماط"), ("Show/Hide Tagged Frames", "إظهار/إخفاء الإطارات الموسومة"), ("Show/Hide Text Threads", "إظهار/إخفاء روابط الإطارات النصية"), ("Smooth", "تنعيم"),@@ -717,6 +717,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "Zobrazit/skrýt panely"), ("Show/Hide Panels Except Tools", "Zobrazit/skrýt panely kromě nástrojů"), ("Show/Hide Rulers", "Zobrazit/skrýt pravítka"),+ ("Show/Hide Style Name Column", "Zobrazit/skrýt sloupec názvů stylů"), ("Show/Hide Tagged Frames", "Zobrazit/skrýt otagované rámečky"), ("Show/Hide Text Threads", "Zobrazit/skrýt zřetězení textu"), ("Smooth", "Vyhladit"),@@ -753,6 +753,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "Bedienfelder ein-/ausblenden"), ("Show/Hide Panels Except Tools", "Bedienfelder außer Werkzeugen ein-/ausblenden"), ("Show/Hide Rulers", "Lineale ein-/ausblenden"),+ ("Show/Hide Style Name Column", "Spalte mit Formatnamen ein-/ausblenden"), ("Show/Hide Tagged Frames", "Getaggte Rahmen ein-/ausblenden"), ("Show/Hide Text Threads", "Textverkettungen ein-/ausblenden"), ("Smooth", "Glätten"),@@ -846,6 +846,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "Mostra/nascondi pannelli"), ("Show/Hide Panels Except Tools", "Mostra/nascondi pannelli tranne Strumenti"), ("Show/Hide Rulers", "Mostra/nascondi righelli"),+ ("Show/Hide Style Name Column", "Mostra/nascondi colonna nomi stili"), ("Show/Hide Tagged Frames", "Mostra/nascondi cornici con tag"), ("Show/Hide Text Threads", "Mostra/nascondi concatenamenti testo"), ("Smooth", "Arrotonda"),@@ -946,6 +946,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "パネルを表示・非表示"), ("Show/Hide Panels Except Tools", "ツール以外のパネルを表示・非表示"), ("Show/Hide Rulers", "定規を表示・非表示"),+ ("Show/Hide Style Name Column", "スタイル名の列を表示・非表示"), ("Show/Hide Tagged Frames", "タグ付きフレームを表示・非表示"), ("Show/Hide Text Threads", "テキストフレームの連結を表示・非表示"), ("Smooth", "平滑化"),@@ -773,6 +773,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "პანელების ჩვენება/დამალვა"), ("Show/Hide Panels Except Tools", "პანელების ჩვენება/დამალვა, ხელსაწყოების გარდა"), ("Show/Hide Rulers", "სახაზავების ჩვენება/დამალვა"),+ ("Show/Hide Style Name Column", "სტილების სახელების სვეტის ჩვენება/დამალვა"), ("Show/Hide Tagged Frames", "ტეგირებული ჩარჩოების ჩვენება/დამალვა"), ("Show/Hide Text Threads", "ტექსტის კავშირების ჩვენება/დამალვა"), ("Smooth", "გლუვი"),@@ -724,6 +724,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "Pokaż/ukryj panele"), ("Show/Hide Panels Except Tools", "Pokaż/ukryj panele oprócz narzędzi"), ("Show/Hide Rulers", "Pokaż/ukryj linijki"),+ ("Show/Hide Style Name Column", "Pokaż/ukryj kolumnę nazw stylów"), ("Show/Hide Tagged Frames", "Pokaż/ukryj ramki z tagami"), ("Show/Hide Text Threads", "Pokaż/ukryj powiązania tekstu"), ("Smooth", "Wygładź"),@@ -1692,6 +1692,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "Mostrar/ocultar painéis"), ("Show/Hide Panels Except Tools", "Mostrar/ocultar painéis, exceto ferramentas"), ("Show/Hide Rulers", "Mostrar/ocultar réguas"),+ ("Show/Hide Style Name Column", "Mostrar/ocultar coluna de nomes de estilo"), ("Show/Hide Tagged Frames", "Mostrar/ocultar quadros marcados"), ("Show/Hide Text Threads", "Mostrar/ocultar encadeamentos de texto"), ("Sort", "Ordenar"),@@ -704,6 +704,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "Показать/скрыть панели"), ("Show/Hide Panels Except Tools", "Показать/скрыть панели, кроме инструментов"), ("Show/Hide Rulers", "Показать/скрыть линейки"),+ ("Show/Hide Style Name Column", "Показать/скрыть столбец имен стилей"), ("Show/Hide Tagged Frames", "Показать/скрыть рамки с тегами"), ("Show/Hide Text Threads", "Показать/скрыть связи текстовых рамок"), ("Smooth", "Сгладить"),@@ -2346,6 +2346,16 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[ ], ), ("Show/Hide Rulers", ["Show/Hide Rulers", "Afficher/Masquer les règles", "Show/Hide Rulers", "Show/Hide Rulers", "显示/隐藏标尺"]),+ (+ "Show/Hide Style Name Column",+ [+ "Show/Hide Style Name Column",+ "Afficher/Masquer la colonne des noms de style",+ "Mostrar/ocultar columna de nombres de estilo",+ "Show/Hide Style Name Column",+ "显示/隐藏样式名称栏",+ ],+ ), ( "Show/Hide Tagged Frames", [@@ -849,6 +849,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Show/Hide Panels", "Показати/приховати панелі"), ("Show/Hide Panels Except Tools", "Показати/приховати панелі, крім інструментів"), ("Show/Hide Rulers", "Показати/приховати лінійки"),+ ("Show/Hide Style Name Column", "Показати/приховати стовпець назв стилів"), ("Show/Hide Tagged Frames", "Показати/приховати фрейми з тегами"), ("Show/Hide Text Threads", "Показати/приховати зв’язки текстових фреймів"), ("Smooth", "Згладити"),@@ -590,6 +590,24 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value "view.textThreads" => flag(&mut app.ui.text_threads), "view.hiddenCharacters" => flag(&mut app.ui.hidden_characters), "view.taggedFrames" => flag(&mut app.ui.tagged_frames),+ "view.storyEditorStyleColumn" => {+ let on = p.get("on");+ let width = p.get("width");+ if on.is_some_and(|v| !v.is_boolean()) || width.is_some_and(|v| !v.is_number()) {+ return Some(Err(format!("{id}: `on` is a bool and `width` a number")));+ }+ match (on.and_then(Value::as_bool), width.and_then(Value::as_f64)) {+ (None, None) => app.ui.story_editor_style_column = !app.ui.story_editor_style_column,+ (on, width) => {+ app.ui.story_editor_style_column = on.unwrap_or(app.ui.story_editor_style_column);+ if let Some(w) = width {+ app.ui.story_editor_style_width =+ crate::story_editor::style_column_width(w.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32);+ }+ }+ }+ Ok(json!({"on": app.ui.story_editor_style_column, "width": app.ui.story_editor_style_width}))+ } "view.storyEditorDepthRuler" => flag(&mut app.ui.story_editor_depth_ruler), "window.hidePanels" | "window.hidePanelsExceptTools" => { let mode = if id == "window.hidePanels" { 1 } else { 2 };@@ -103,6 +103,7 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option<bool> { "view.textThreads" => app.ui.text_threads, "view.hiddenCharacters" => app.ui.hidden_characters, "view.taggedFrames" => app.ui.tagged_frames,+ "view.storyEditorStyleColumn" => app.ui.story_editor_style_column, "view.storyEditorDepthRuler" => app.ui.story_editor_depth_ruler, "changes.track" => app.session.active().is_some_and(|d| d.doc.settings.track_changes), // Pressed, as the Bulleted and Numbered List buttons: every paragraph the command applies@@ -530,6 +530,7 @@ pub const MENUS: &[(&str, &[&str])] = &[ "ui:view.tagMarkers", "<", ">Story Editor",+ "ui:view.storyEditorStyleColumn", "ui:view.storyEditorDepthRuler", "<", ],@@ -17,6 +17,7 @@ fn view_items_are_in_the_view_menu_as_in_indesign() { "view.separations", "view.taggedFrames", "view.tagMarkers",+ "view.storyEditorStyleColumn", "view.storyEditorDepthRuler", ] { assert!(view.contains(&id), "{id} is in View");@@ -232,6 +232,12 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("view.textThreads", "Show/Hide Text Threads", Some("Cmd+Alt+Y"), "{}"), ("view.hiddenCharacters", "Show Hidden Characters", Some("Cmd+Alt+I"), "{}"), ("view.taggedFrames", "Show/Hide Tagged Frames", None, "{} — XML-tagged frames outlined in their tag colour"),+ (+ "view.storyEditorStyleColumn",+ "Show/Hide Style Name Column",+ None,+ "{on?: bool, width?: 40–400 (points)} — the Story Editor's column of paragraph style names; toggles without either → {on, width}",+ ), ( "view.storyEditorDepthRuler", "Show/Hide Depth Ruler",@@ -224,6 +224,11 @@ pub struct UiState { pub story_editor_size: f32, /// View › Story Editor › Show/Hide Depth Ruler. pub story_editor_depth_ruler: bool,+ /// View › Story Editor › Show/Hide Style Name Column.+ pub story_editor_style_column: bool,+ /// Width of the Story Editor's style name column, in screen points (set by dragging its bar or by+ /// `view.storyEditorStyleColumn` `width`).+ pub story_editor_style_width: f32, /// Preferences › General › Interface Language: supported codes are listed in `i18n::LANGUAGES`. pub language: String, /// Edit › Transparency Flattener Presets: "" (none), "high", "medium" or "low" for PDF export.@@ -397,6 +402,8 @@ impl Default for UiState { show_full_menus: false, story_editor_size: 14.0, story_editor_depth_ruler: true,+ story_editor_style_column: true,+ story_editor_style_width: story_editor::STYLE_COLUMN_WIDTH, scripts: vec![ ("Number the pages".into(), "# A page-number frame at the bottom of the current page.\nframe.create {\"rect\": [288, 740, 324, 760], \"content\": \"text\", \"text\": \"\"}\ntext.select {\"story\": \"$0.story\", \"anchor\": 0, \"focus\": 0}\ntext.insert {\"text\": \"\\ue000\", \"raw\": true}".into()), ("Two-column grid".into(), "# Four text frames in a 2 × 2 grid.\nframe.grid {\"rect\": [36, 36, 576, 756], \"cols\": 2, \"rows\": 2, \"gutter\": 12}".into()),@@ -11,6 +11,12 @@ use designcraft_doc::StoryId; use crate::DesignApp; use crate::theme::Tokens; +/// Width of the style name column before its bar is dragged, in screen points.+pub const STYLE_COLUMN_WIDTH: f32 = 130.0;+/// Narrowest and widest the style name column can be dragged, in screen points.+const STYLE_COLUMN_RANGE: (f32, f32) = (40.0, 400.0);+/// Width of the bar between the style name column and the text, in screen points.+const STYLE_BAR_WIDTH: f32 = 6.0; /// Width of the depth ruler, in screen points. const RULER_WIDTH: f32 = 34.0; /// Least distance between two of the depth ruler's labels, in screen points.@@ -51,8 +57,10 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { let overset = cs.overset_at; let depths = app.ui.story_editor_depth_ruler.then(|| line_depths(&cs)); let unit = st.doc.settings.vertical_units;+ let style_width = app.ui.story_editor_style_column.then(|| style_column_width(app.ui.story_editor_style_width)); let mut open = true; let mut edit: Option<(usize, usize, String)> = None;+ let mut dragged_width: Option<f32> = None; let words = story.text.split_whitespace().count().to_string(); egui::Window::new(crate::i18n::tr(&app.ui.language, "Story Editor — {count} words").replace("{count}", &words)) .id(egui::Id::unique("story_editor"))@@ -61,16 +69,20 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { .show(ctx, |ui| { egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { ui.horizontal_top(|ui| {- // Style column: the labels are painted after the text, at the rows the text really wraps to.- let (column, _) = ui.allocate_exact_size(vec2(130.0, 0.0), egui::Sense::hover());- ui.separator();+ // Style column: the labels are painted after the text, at the rows the text really wraps to,+ // and so is the bar that resizes it.+ let column = style_width.map(|w| {+ let column = ui.allocate_exact_size(vec2(w, 0.0), egui::Sense::hover()).0;+ let bar = ui.allocate_exact_size(vec2(STYLE_BAR_WIDTH, 0.0), egui::Sense::hover()).0;+ (column, bar)+ }); // Depth ruler: painted after the text too, beside the rows the lines start on. let ruler = depths.as_ref().map(|_| ui.allocate_exact_size(vec2(RULER_WIDTH, 0.0), egui::Sense::hover()).0); let mut text = story.text.clone();- // (a fixed id: the ruler's allocation above would otherwise move the editor's auto id,- // and its focus and caret with it, whenever the ruler is shown or hidden)+ // The text field has a fixed id, so it keeps its focus and caret when the columns before it+ // are shown or hidden. let out = egui::TextEdit::multiline(&mut text)- .id_salt("story_text")+ .id(egui::Id::unique("story_editor_text")) .font(egui::FontId::proportional(app.ui.story_editor_size.clamp(8.0, 36.0))) .desired_width(f32::INFINITY) .desired_rows(20)@@ -78,14 +90,27 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { .show(ui); let r = out.response; let changed = r.changed();- let starts = paragraph_start_offsets(out.galley.rows.iter().map(|row| (row.pos.y, row.ends_with_newline)));- for (i, y) in starts.into_iter().enumerate() {- let Some(para) = story.paras.get(i) else { break };- let rect = egui::Rect::from_min_size(egui::pos2(column.min.x, out.galley_pos.y + y), vec2(130.0, 14.0));- ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {- let name = crate::i18n::style_name(&app.ui.language, ¶.style);- crate::rtl::label(ui, egui::RichText::new(name).size(11.0).color(t.text_dim));- });+ if let Some((column, bar)) = column {+ let starts = paragraph_start_offsets(out.galley.rows.iter().map(|row| (row.pos.y, row.ends_with_newline)));+ for (i, y) in starts.into_iter().enumerate() {+ let Some(para) = story.paras.get(i) else { break };+ let rect = egui::Rect::from_min_size(egui::pos2(column.min.x, out.galley_pos.y + y), vec2(column.width(), 14.0));+ ui.scope_builder(egui::UiBuilder::new().max_rect(rect), |ui| {+ ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);+ let name = crate::i18n::style_name(&app.ui.language, ¶.style);+ crate::rtl::label(ui, egui::RichText::new(name).size(11.0).color(t.text_dim));+ });+ }+ // The bar between the column and the text: drag it to make the column wider or narrower.+ let bar = egui::Rect::from_x_y_ranges(bar.x_range(), r.rect.y_range());+ let drag = ui.interact(bar, egui::Id::unique("story_editor_style_bar"), egui::Sense::drag());+ let line = egui::Rect::from_center_size(bar.center(), vec2(1.0, bar.height()));+ ui.painter().rect_filled(line, 0.0, if drag.hovered() || drag.dragged() { t.text_dim } else { t.section_divider });+ let drag = drag.on_hover_cursor(egui::CursorIcon::ResizeHorizontal);+ if drag.dragged() {+ dragged_width = Some(style_column_width(column.width() + drag.drag_delta().x));+ }+ drag.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Unknown, true, "Style name column divider")); } let overset_y = overset.filter(|_| !changed).map(|o| { let ci = char_offsets(&story.text, std::iter::once(o));@@ -145,6 +170,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { } }); });+ if let Some(w) = dragged_width {+ let _ = app.run("view.storyEditorStyleColumn", json!({"width": w}));+ } if let Some((a, b, text)) = edit { let _ = app.run("story.replaceRange", json!({"story": sid.0, "start": a, "end": b, "text": text})); }@@ -153,6 +181,12 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { } } +/// The style name column's width for a saved or dragged `width`: kept between the narrowest and+/// widest the bar allows, and the starting width when `width` is not a number.+pub(crate) fn style_column_width(width: f32) -> f32 {+ if width.is_finite() { width.clamp(STYLE_COLUMN_RANGE.0, STYLE_COLUMN_RANGE.1) } else { STYLE_COLUMN_WIDTH }+}+ /// Vertical offset of the first laid-out row of each paragraph, from `(row y, row ends with a newline)` /// in layout order. A paragraph that wraps spans several rows, so counting characters per line /// drifts from what the editor draws; the rows themselves are the truth.@@ -471,6 +505,135 @@ mod tests { assert!(!crate::UiState::from_preferences_json(&saved).unwrap().story_editor_depth_ruler); } + fn story_editor_harness(text: &str) -> egui_kittest::Harness<'static, crate::test_window::Window> {+ let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());+ app.run("file.new", json!({})).unwrap();+ let r = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": text, "caret": false})).unwrap();+ app.run("app.storyEditor", json!({"story": r["story"].as_u64().unwrap()})).unwrap();+ let mut h = crate::test_window::open(app, egui::vec2(1440.0, 900.0));+ h.run_steps(2);+ h+ }++ fn text_left(h: &egui_kittest::Harness<'_, crate::test_window::Window>, text: &str) -> f32 {+ h.get_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput && n.value().as_deref() == Some(text)).rect().min.x+ }++ /// View › Story Editor › Show/Hide Style Name Column: hiding the column takes away the style+ /// names and the bar beside them, and the text moves left by their width; the setting is+ /// remembered.+ #[test]+ fn style_name_column_shows_and_hides_from_the_view_menu() {+ let mut h = story_editor_harness("Column story");+ assert!(h.state().app.ui.story_editor_style_column, "shown by default");+ let app = &h.state().app;+ let para_style = &app.session.active().unwrap().doc.stories.values().next().unwrap().paras[0].style;+ let style = crate::i18n::style_name(&app.ui.language, para_style).to_string();+ assert!(h.query_by_label(&style).is_some(), "the column names the paragraph's style {style:?}");+ assert!(h.query_by_label("Style name column divider").is_some());+ let with = text_left(&h, "Column story");+ h.state_mut().app.run("view.storyEditorStyleColumn", json!({})).unwrap();+ h.run_steps(2);+ let without = text_left(&h, "Column story");+ assert!(h.query_by_label(&style).is_none(), "no style names while the column is hidden");+ assert!(h.query_by_label("Style name column divider").is_none());+ assert!(+ (with - without - super::STYLE_COLUMN_WIDTH - super::STYLE_BAR_WIDTH).abs() < 20.0,+ "the text moves by the column: {with} vs {without}"+ );+ let app = &h.state().app;+ assert_eq!(crate::menus::checked(app, "view.storyEditorStyleColumn", &json!({})), Some(false));+ let saved = serde_json::to_vec(&app.ui).unwrap();+ assert!(!crate::UiState::from_preferences_json(&saved).unwrap().story_editor_style_column);+ }++ /// A style name wider than the column is cut short at the column's edge, so it does not run+ /// into the story text.+ #[test]+ fn a_long_style_name_stays_in_the_column() {+ let mut h = story_editor_harness("Narrow story");+ h.state_mut().app.run("view.storyEditorStyleColumn", json!({"width": 40})).unwrap();+ h.run_steps(2);+ let app = &h.state().app;+ let para_style = &app.session.active().unwrap().doc.stories.values().next().unwrap().paras[0].style;+ let style = crate::i18n::style_name(&app.ui.language, para_style).to_string();+ let name = h.get_by_label(&style).rect();+ let text = text_left(&h, "Narrow story");+ assert!(name.width() <= 40.5, "the name {style:?} is cut to the column: {name:?}");+ assert!(name.max.x <= text, "the name ends before the text: {name:?}, text at {text}");+ }++ /// Dragging the bar between the style names and the text resizes the column; the text moves+ /// with the bar, and the width is remembered for the next time the editor opens.+ #[test]+ fn dragging_the_style_column_bar_resizes_the_column() {+ use egui::{Event, Modifiers, PointerButton};+ let mut h = story_editor_harness("Resize story");+ let before = text_left(&h, "Resize story");+ let a = h.get_by_label("Style name column divider").rect().center();+ let b = a + egui::vec2(60.0, 0.0);+ h.input_mut().events.push(Event::PointerMoved(a));+ h.input_mut().events.push(Event::PointerButton { pos: a, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });+ h.step();+ for k in 1..=4 {+ h.input_mut().events.push(Event::PointerMoved(a + (b - a) * (k as f32 / 4.0)));+ h.step();+ }+ h.input_mut().events.push(Event::PointerButton { pos: b, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });+ h.run_steps(3);+ let width = h.state().app.ui.story_editor_style_width;+ assert!((width - super::STYLE_COLUMN_WIDTH - 60.0).abs() < 1.0, "the column is 60 points wider: {width}");+ let after = text_left(&h, "Resize story");+ assert!((after - before - 60.0).abs() < 1.0, "the text moves with the bar: {before} → {after}");+ let saved = serde_json::to_vec(&h.state().app.ui).unwrap();+ assert_eq!(crate::UiState::from_preferences_json(&saved).unwrap().story_editor_style_width, width);+ }++ /// A saved width out of range, or not a number, gives a column the bar can still be dragged in.+ #[test]+ fn style_column_width_stays_in_range() {+ assert_eq!(super::style_column_width(200.0), 200.0);+ assert_eq!(super::style_column_width(-50.0), super::STYLE_COLUMN_RANGE.0);+ assert_eq!(super::style_column_width(1e9), super::STYLE_COLUMN_RANGE.1);+ assert_eq!(super::style_column_width(f32::NAN), super::STYLE_COLUMN_WIDTH);+ assert_eq!(super::style_column_width(f32::INFINITY), super::STYLE_COLUMN_WIDTH);+ }++ /// Showing or hiding the style name column or the depth ruler keeps the text's focus and caret:+ /// typing goes on into the story.+ #[test]+ fn typing_goes_on_after_the_columns_show_or_hide() {+ let mut h = story_editor_harness("Focus story");+ h.get_by(|n| n.role() == egui::accesskit::Role::MultilineTextInput && n.value().as_deref() == Some("Focus story")).focus();+ h.run_steps(2);+ h.event(egui::Event::Text("1".into()));+ h.run_steps(2);+ for (i, id) in ["view.storyEditorStyleColumn", "view.storyEditorDepthRuler", "view.storyEditorStyleColumn"].into_iter().enumerate() {+ h.state_mut().app.run(id, json!({})).unwrap();+ h.run_steps(2);+ h.event(egui::Event::Text((i + 2).to_string()));+ h.run_steps(2);+ }+ let text = story_text(&h.state().app, 0);+ assert!(text.contains("1234"), "every key reaches the story at the caret: {text:?}");+ }++ /// `view.storyEditorStyleColumn` sets the column's width and whether it shows, as the bar and+ /// the menu item do, and toggles it without either.+ #[test]+ fn the_style_column_command_takes_a_width() {+ let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());+ assert_eq!(app.run("view.storyEditorStyleColumn", json!({"width": 250})).unwrap(), json!({"on": true, "width": 250.0}));+ assert_eq!(app.run("view.storyEditorStyleColumn", json!({"width": 1000})).unwrap()["width"], json!(400.0));+ assert_eq!(app.run("view.storyEditorStyleColumn", json!({"width": -1e40})).unwrap()["width"], json!(40.0));+ assert_eq!(app.run("view.storyEditorStyleColumn", json!({"width": 1e40})).unwrap()["width"], json!(400.0));+ assert_eq!(app.run("view.storyEditorStyleColumn", json!({"on": false})).unwrap(), json!({"on": false, "width": 400.0}));+ assert_eq!(app.run("view.storyEditorStyleColumn", json!({})).unwrap()["on"], json!(true));+ assert!(app.run("view.storyEditorStyleColumn", json!({"width": "wide"})).is_err());+ assert!(app.run("view.storyEditorStyleColumn", json!({"on": 1})).is_err());+ assert_eq!((app.ui.story_editor_style_column, app.ui.story_editor_style_width), (true, 400.0));+ }+ #[test] fn depth_marks_wait_for_composition_after_editing() { fn marks(shape: &egui::epaint::Shape, tick: egui::Color32) -> usize {@@ -517,6 +680,14 @@ mod tests { } } + /// The Show/Hide Style Name Column menu label is translated in every interface language.+ #[test]+ fn style_name_column_menu_label_is_translated_in_every_language() {+ for (lang, _) in crate::i18n::LANGUAGES.iter().filter(|(code, _)| !code.is_empty()) {+ assert_ne!(crate::i18n::tr(lang, "Show/Hide Style Name Column"), "Show/Hide Style Name Column", "{lang}");+ }+ }+ /// Showing or hiding the depth ruler keeps the text's focus and caret: typing after the toggle /// still reaches the story. #[test]@@ -189,7 +189,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format · Profile / Edit Original, File ▸ Open PDF / Document and Print Presets / User Settings, Type ▸ Size presets, Table ▸ Table Options / Cell Options submenus, Paste Before/After, Convert Columns, Object ▸ Interactive ▸ Convert to (form fields),- View ▸ Extras badges and Story Editor options (Show/Hide Depth Ruler is there). Pages panel ▸ Parent Options (Based on Parent is+ View ▸ Extras badges and Story Editor options (Show/Hide Depth Ruler and Show/Hide Style Name Column are there). Pages panel ▸ Parent Options (Based on Parent is the `layout.parents.options` command only); overriding a base parent's items on a parent spread itself (overrides work on document pages only). - **Estimate:** 40–70 h (most are thin wrappers over existing engine features).@@ -50,7 +50,7 @@ basics of editing fail for too many users on Windows and Linux. | Panels: Pages, Layers, Swatches, Color, Stroke, Character, Paragraph, Styles, Links, Effects, Text Wrap, Align, Pathfinder, Table, Index, TOC, Hyperlinks, Bookmarks, Articles, Tags, Notes, Track Changes, Object States, Buttons and Forms, Preflight, Data Merge, Library, Scripts, Glyphs, Info, Tabs, Story | ✓ | all present | ~ | Navigator, Trap Presets, Animation, Timing missing; pull-down panels can't scroll on Linux (#267) | | Dialogs: New Document, Document Setup, Preferences (all pages), Paragraph/Character/Object/Table/Cell Style Options, Export PDF, Print, Package, Find/Change | ✓ | present | ~ | generated command dialogs fill the long tail; depth varies | | Workspaces | Essentials, Essentials Classic, Advanced, Book, Digital Publishing, Interactive for PDF, Printing and Proofing, Review, Typography, Touch | ✓ named workspaces (Essentials Classic: Control panel and iconic panel groups) + custom; Window › Workspace; no Review or Touch | ~ | tab bar overlaps menus in Book/Advanced (#182) |-| Story Editor | ✓ | ✓ | ~ | depth ruler and View › Story Editor › Show/Hide Depth Ruler (`story_editor.rs` `depth_ruler_shows_and_hides_from_the_view_menu`); hiding or resizing the style-name column missing |+| Story Editor | ✓ | ✓ | ~ | depth ruler and View › Story Editor › Show/Hide Depth Ruler (`story_editor.rs` `depth_ruler_shows_and_hides_from_the_view_menu`); Show/Hide Style Name Column and dragging the column's width (`style_name_column_shows_and_hides_from_the_view_menu`, `dragging_the_style_column_bar_resizes_the_column`); Expand/Collapse All Footnotes and Show/Hide Paragraph Break Marks missing | | Undo granularity (a drag = one step) | ✓ | ✓ (Begin/Preview/Commit) | ✓ | | | Closing with unsaved changes asks | ✓ | ✓ File › Close and window/menu quit (#146, #545) | ✓ | macOS Dock Quit still bypasses the prompt (#544) | | UI scaling, System appearance | ✓ | scaling ✓ (#281); follows macOS light/dark ✗ (#237) | ~ | |