From a6ff24d0c40d68f78728e3fad153f77e7003a604 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 23:41:42 -0800 Subject: [PATCH] Tabs: the ruler and Repeat Tab stop at 256 tab stops With 256 stops, a click on an empty part of the Tabs panel's ruler adds no stop and shows "at most 256 tab stops" in the status bar; the stops and the selection stay as they were. Repeat Tab adds copies only up to 256 stops and shows the same message when it stops early. Before, both built a longer list that text.tabs.set refused, so nothing changed and no message showed. --- crates/ui-egui/src/panels/tabs.rs | 32 +++++++++++++++++-------- crates/ui-egui/src/panels/tabs/tests.rs | 29 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/panels/tabs.rs b/crates/ui-egui/src/panels/tabs.rs index fb12e4fdc0..6a984a1ced 100644 --- a/crates/ui-egui/src/panels/tabs.rs +++ b/crates/ui-egui/src/panels/tabs.rs @@ -686,15 +686,22 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // A click that lands on a stop's whole point or tick selects that stop and records no // undo step. let added = !stops.iter().any(|s| s.position == position); - if added { - stops.push(TabStop { position, align: ALIGNS[align_idx.min(3)].0, leader: String::new(), align_on: '.' }); - stops.sort_by(|a, b| a.position.total_cmp(&b.position)); - } - let i = stops.iter().position(|s| s.position == position).unwrap_or(0); - set_pstate::>(ui.ctx(), "tabs-marker-sel", None); - set_pstate(ui.ctx(), "tabs-sel", i); - if added { - apply(app, &stops); + let max = TabStop::MAX_STOPS; + if added && stops.len() >= max { + // A full list takes no more stops: the click leaves the stops and the + // selection as they are and shows the limit. + app.status(format!("at most {max} tab stops")); + } else { + if added { + stops.push(TabStop { position, align: ALIGNS[align_idx.min(3)].0, leader: String::new(), align_on: '.' }); + stops.sort_by(|a, b| a.position.total_cmp(&b.position)); + } + let i = stops.iter().position(|s| s.position == position).unwrap_or(0); + set_pstate::>(ui.ctx(), "tabs-marker-sel", None); + set_pstate(ui.ctx(), "tabs-sel", i); + if added { + apply(app, &stops); + } } } } @@ -729,7 +736,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } // Repeat Tab: copies of the selected stop across the ruler, spaced by its distance from the // previous stop, or from the paragraph's left indent plus its list level's indent when it is - // the first stop. + // the first stop. The copies stop at the 256-stop limit, which the status bar then shows. if menu_item(ui, tl!("Repeat Tab"), sel < stops.len() && selected_marker.is_none(), false) { let step = match sel { 0 => (stops[0].position - left_indent).abs(), @@ -738,8 +745,13 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { .max(1.0); let base = stops[sel].clone(); stops.truncate(sel + 1); + let max = TabStop::MAX_STOPS; let mut pos = base.position + step; while pos <= base.position + step * 20.0 && pos < 2000.0 { + if stops.len() >= max { + app.status(format!("at most {max} tab stops")); + break; + } stops.push(TabStop { position: pos, ..base.clone() }); pos += step; } diff --git a/crates/ui-egui/src/panels/tabs/tests.rs b/crates/ui-egui/src/panels/tabs/tests.rs index 9e96866ab2..97e8c8a00a 100644 --- a/crates/ui-egui/src/panels/tabs/tests.rs +++ b/crates/ui-egui/src/panels/tabs/tests.rs @@ -1255,6 +1255,35 @@ fn panel_menu_does_not_change_stops_while_an_indent_marker_is_selected() { assert_eq!(current(&app).unwrap().1, before); } +/// With 256 stops, a click on an empty part of the ruler adds none and shows the limit in the +/// status bar. Repeat Tab near the end of the list adds copies only up to 256 stops and shows the +/// limit too. +#[test] +fn the_ruler_and_repeat_tab_stop_at_256_stops() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({})).unwrap(); + app.session.execute("text.create", &json!({"x": 10, "y": 20, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap(); + // 250 stops 1 pt apart from 0 pt, then six 2 pt apart from 260 pt. + let start: Vec = (0..250).map(f64::from).chain((0..6).map(|i| 260.0 + 2.0 * f64::from(i))).collect(); + let stops: Vec = start.iter().map(|p| json!({"position": p})).collect(); + app.session.execute("text.tabs.set", &json!({ "stops": stops })).unwrap(); + let ctx = egui::Context::default(); + let positions = |app: &VectorcraftApp| current(app).unwrap().1.iter().map(|s| s.position).collect::>(); + set_pstate(&ctx, "tabs-sel", 3usize); + drag_on_ruler(&ctx, &mut app, 290.0, 290.0, false); + assert_eq!(positions(&app), start); + assert_eq!(pstate::(&ctx, "tabs-sel"), 3); + assert_eq!(app.ui.status, "at most 256 tab stops"); + // Repeat Tab on the stop at 260 pt (11 pt past the one before it): five copies, not 20. + app.status(""); + set_pstate(&ctx, "tabs-sel", 250usize); + choose(&ctx, &mut app, "Repeat Tab"); + let repeated = positions(&app); + assert_eq!(repeated.len(), 256); + assert_eq!(repeated[249..], [249.0, 260.0, 271.0, 282.0, 293.0, 304.0, 315.0]); + assert_eq!(app.ui.status, "at most 256 tab stops"); +} + /// Repeat Tab on the first stop spaces the copies by the stop's distance from the paragraph's /// left indent (18 pt further in for each list level past the first), from the paragraph the /// caret is in while the Type tool edits text.