From 5b2252a789369daf65b6f14fd1cf33cfb446f872 Mon Sep 17 00:00:00 2001 From: PiMinusMax <121135695+PiMinusMax@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:11:14 +0200 Subject: [PATCH 1/3] Rulers: a guide being dragged out keeps its resize cursor until dropped The canvas set the tool's cursor every frame the pointer was over it, after the guide drag had set its resize cursor, so the cursor turned back into the tool's as soon as the pointer left the ruler and the drag looked as if it had stopped. A ruler guide drag now keeps the cursor across the guide, as a column guide drag already did. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/canvas.rs | 7 ++++++ crates/ui-egui/src/guide_selection_tests.rs | 28 +++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index 8c7e3be5a..9e91acdfe 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -408,6 +408,7 @@ Newest first. - **2026-10-24** · Optical Margin Alignment per story with a size (`story.options {opticalMargin, opticalMarginSize}`, off by default): when it is on, the story's paragraphs hang punctuation and letter edges by amounts set for that type size, except paragraphs with Ignore Optical Margin (`ignoreOpticalMargin` in `type.para` and paragraph styles). Table cells and footnotes don't take the story setting yet. When it is off, the per-paragraph switch still hangs at the text's own size. IDML imports and exports both (`StoryPreference/@OpticalMarginAlignment`, `@OpticalMarginSize`, `IgnoreEdgeAlignment`) (#947). No readiness percentage change. - **2026-10-24** · Text frames: a custom baseline grid's Relative To (Text Frame Options › Baseline Options, `object.textFrameOptions {baselineGrid: {relativeTo}}`, IDML `BaselineFrameGridRelativeOption`) measures the grid's start from the top of the page, the top margin, the top of the frame or its top inset. Text Frame Options had no custom baseline grid controls, imports dropped the setting and every frame grid started from the page top, so grid-aligned text in a frame measured from its own top sat on the wrong lines. No readiness percentage change. - **2026-10-24** · Paragraph Style Options › Indents and Spacing has an Align to Grid menu: None, All Lines or First Line Only. It shows the style's current setting. A First Line Only style could be set only in the CJK Grid Settings section, which shows only with CJK features on, or through commands. No readiness percentage change. +- **2026-10-24** · A guide pulled out of a ruler keeps the resize cursor across it until it is dropped; the tool's cursor took over as soon as the pointer left the ruler, so the drag looked as if it had stopped. No readiness percentage change. - **2026-10-24** · Anchored text frames, such as marginal notes, compose where their host story places them: they align to the baseline grid at that position, and page-number markers and spine-relative alignment inside them read the host's page. They composed against a grid measured from their stored position and on no page, so a note set to Only Align First Line to Grid could sit a fraction of a line off the body text's grid line. No readiness percentage change. - **2026-10-24** · Tables: a gradient swatch in a cell fill draws as a gradient on the canvas and in PDF, in one gradient space for the whole table, so it runs across cells without restarting. Cells showed the gradient's first stop as a solid colour. No readiness percentage change. - **2026-10-24** · New documents start with Hyphenate Capitalized Words, Hyphenate Last Word and Hyphenate Across Column off in [Basic Paragraph], as in InDesign. Opened and imported files keep the values they carry. No readiness percentage change. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 64bed51d8..17f649959 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -743,6 +743,13 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { Some(_) => Cursor::ResizeV, None => c, }; + // A ruler guide being dragged keeps the cursor across it until it is dropped, over the + // canvas too (the tool's cursor took over as soon as the pointer left the ruler). + let c = match ui.data(|d| d.get_temp::(egui::Id::new(GUIDE_DRAG))).map(|g| g.orientation) { + Some(designcraft_doc::Orientation::Horizontal) => Cursor::ResizeV, + Some(designcraft_doc::Orientation::Vertical) => Cursor::ResizeH, + None => c, + }; let icon = crate::cursors::show(ui.ctx(), &mut app.cursors, c, p); ui.ctx().set_cursor_icon(icon); draw_cursor_badge(&painter, p, c, &t); diff --git a/crates/ui-egui/src/guide_selection_tests.rs b/crates/ui-egui/src/guide_selection_tests.rs index 626a7b826..8a1cde599 100644 --- a/crates/ui-egui/src/guide_selection_tests.rs +++ b/crates/ui-egui/src/guide_selection_tests.rs @@ -328,3 +328,31 @@ fn a_guide_dragged_out_of_a_ruler_snaps() { drag(&mut h, ruler, dest); assert_eq!(positions(&h), [200.0, 300.0, 400.0, 400.0]); } + +/// A guide pulled out of a ruler keeps the resize cursor across it over the canvas until it is +/// dropped; the tool's arrow came back as soon as the pointer left the ruler. +#[test] +fn a_guide_dragged_out_of_a_ruler_keeps_its_cursor_until_dropped() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let mut h = harness(app); + let rect = h.state().canvas_rect.unwrap(); + for (from, icon) in [ + (pos2(rect.center().x, rect.min.y - crate::canvas::RULER / 2.0), egui::CursorIcon::ResizeVertical), + (pos2(rect.min.x - crate::canvas::RULER / 2.0, rect.center().y), egui::CursorIcon::ResizeHorizontal), + ] { + let to = rect.center(); + h.hover_at(from); + h.step(); + press(&mut h, from, true, Modifiers::NONE); + for k in 1..=4 { + h.hover_at(from + (to - from) * (k as f32 / 4.0)); + h.step(); + assert_eq!(h.output().platform_output.cursor_icon, icon, "step {k} of the drag from {from:?}"); + } + press(&mut h, to, false, Modifiers::NONE); + h.hover_at(to); + h.run_steps(2); + assert_ne!(h.output().platform_output.cursor_icon, icon, "dropped: the tool's cursor again"); + } +} From 30b64114c2ce5f75a4ed18567644f2a3217e3dcc Mon Sep 17 00:00:00 2001 From: PiMinusMax <121135695+PiMinusMax@users.noreply.github.com> Date: Mon, 26 Oct 2026 21:39:07 +0100 Subject: [PATCH 2/3] Adapt to the current egui (Id::unique) Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 17f649959..19bcb87e3 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -745,7 +745,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { }; // A ruler guide being dragged keeps the cursor across it until it is dropped, over the // canvas too (the tool's cursor took over as soon as the pointer left the ruler). - let c = match ui.data(|d| d.get_temp::(egui::Id::new(GUIDE_DRAG))).map(|g| g.orientation) { + let c = match ui.data(|d| d.get_temp::(egui::Id::unique(GUIDE_DRAG))).map(|g| g.orientation) { Some(designcraft_doc::Orientation::Horizontal) => Cursor::ResizeV, Some(designcraft_doc::Orientation::Vertical) => Cursor::ResizeH, None => c, From 794b7a29de1c754f73ec415f4187061e856cf1a7 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 29 Oct 2026 21:39:56 -0400 Subject: [PATCH 3/3] Hold the guide-drag cursor only while the primary button is down Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 19bcb87e3..bf8a88563 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -744,8 +744,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { None => c, }; // A ruler guide being dragged keeps the cursor across it until it is dropped, over the - // canvas too (the tool's cursor took over as soon as the pointer left the ruler). - let c = match ui.data(|d| d.get_temp::(egui::Id::unique(GUIDE_DRAG))).map(|g| g.orientation) { + // canvas too (the tool's cursor took over as soon as the pointer left the ruler). Only + // while the button is down: a drag state left behind by a skipped release doesn't stick. + let dragging = ui.input(|i| i.pointer.primary_down()); + let c = match ui.data(|d| d.get_temp::(egui::Id::unique(GUIDE_DRAG))).filter(|_| dragging).map(|g| g.orientation) { Some(designcraft_doc::Orientation::Horizontal) => Cursor::ResizeV, Some(designcraft_doc::Orientation::Vertical) => Cursor::ResizeH, None => c,