From e5471be4d97b501ac36176e5029fb011e026c26c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 21 Oct 2026 23:34:02 +0700 Subject: [PATCH 1/2] Commands: an index of 2^32 or more no longer wraps around on 32-bit builds Squashed from #3292 onto main. --- crates/engine/src/cmd/appearance.rs | 7 ++-- crates/engine/src/cmd/draw2.rs | 3 +- crates/engine/src/cmd/edit.rs | 4 +-- crates/engine/src/cmd/freeform.rs | 4 +-- crates/engine/src/cmd/gradient.rs | 2 +- crates/engine/src/cmd/layer.rs | 4 +-- crates/engine/src/cmd/library.rs | 2 +- crates/engine/src/cmd/mod.rs | 4 +++ crates/engine/src/cmd/object.rs | 6 ++-- crates/engine/src/cmd/panelcmds.rs | 2 +- crates/engine/src/cmd/path.rs | 6 ++-- crates/engine/src/cmd/printtiling.rs | 2 +- crates/engine/src/cmd/recolor.rs | 6 ++-- crates/engine/src/cmd/select.rs | 4 +-- crates/engine/src/cmd/slices.rs | 2 +- crates/engine/src/cmd/style.rs | 2 +- crates/engine/src/cmd/swatch.rs | 2 +- crates/engine/src/cmd/textedit.rs | 3 +- crates/engine/src/cmd/textstyles.rs | 13 +++----- crates/engine/src/cmd/typecmd.rs | 4 +-- crates/engine/src/cmd/xform.rs | 2 +- crates/engine/src/tests.rs | 39 +++++++++++++++++++++++ crates/engine/src/tests_draw2.rs | 44 ++++++++++++++++++++++++++ crates/engine/src/tests_freeform.rs | 21 ++++++++++++ crates/engine/src/tests_gradient.rs | 2 ++ crates/engine/src/tests_gradpanel.rs | 2 ++ crates/engine/src/tests_layers.rs | 2 ++ crates/engine/src/tests_menucmds.rs | 23 ++++++++++++++ crates/engine/src/tests_panelcmds.rs | 36 +++++++++++++++++++++ crates/engine/src/tests_printtiling.rs | 2 ++ crates/engine/src/tests_recolor.rs | 2 ++ crates/engine/src/tests_slices.rs | 3 ++ crates/engine/src/tests_stylepanel.rs | 10 ++++++ crates/engine/src/tests_styles.rs | 4 +++ crates/engine/src/tests_swatchcmds.rs | 11 +++++++ crates/engine/src/tests_textedit.rs | 16 ++++++++++ docs/mcp.md | 10 ++++++ 37 files changed, 270 insertions(+), 41 deletions(-) diff --git a/crates/engine/src/cmd/appearance.rs b/crates/engine/src/cmd/appearance.rs index 2dc514170..19f600dfb 100644 --- a/crates/engine/src/cmd/appearance.rs +++ b/crates/engine/src/cmd/appearance.rs @@ -218,7 +218,7 @@ pub(crate) fn item_target_at(s: &Session, p: &Value, key: &str, cmd: &str) -> Re Some(Value::Null) => Ok(ItemTarget::Top), Some(v) => v .as_u64() - .map(|i| ItemTarget::Item { index: i as usize, explicit: true }) + .map(|i| ItemTarget::Item { index: index(i), explicit: true }) .ok_or_else(|| bad(cmd, format!("`{key}` must be an appearance item index (paint order) or null"))), // The active item is a row of the first selected object's own stack. None if p.get("ids").is_some() || p.get("id").is_some() || targets_contents(p)? => Ok(ItemTarget::Top), @@ -383,8 +383,9 @@ pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bo Ok(ids.first().and_then(|id| d.node(*id)?.appearance.items.get(index)).map_or(default, |it| !it.is_fill())) } +/// The whole number of 0 or more that `key` holds, read with [`index`]. pub(crate) fn index_param(p: &Value, key: &str, cmd: &str) -> Result { - p.get(key).and_then(Value::as_u64).map(|i| i as usize).ok_or_else(|| bad(cmd, format!("missing integer `{key}`"))) + p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`"))) } fn set_active_item(s: &mut Session, p: &Value) -> Result { @@ -603,7 +604,7 @@ fn item_indices(p: &Value, cmd: &str) -> Result> { let mut v = match p.get("indices") { Some(a) => a .as_array() - .and_then(|a| a.iter().map(|x| x.as_u64().map(|i| i as usize)).collect::>>()) + .and_then(|a| a.iter().map(|x| x.as_u64().map(index)).collect::>>()) .filter(|v| !v.is_empty()) .ok_or_else(|| bad(cmd, "`indices` must be a non-empty array of item indices"))?, None => vec![index_param(p, "index", cmd)?], diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs index c952f5ee0..7bd7a3227 100644 --- a/crates/engine/src/cmd/draw2.rs +++ b/crates/engine/src/cmd/draw2.rs @@ -211,8 +211,9 @@ fn within_input_bound(q: Point) -> bool { q.x.is_finite() && q.y.is_finite() && q.x.abs() < MAX_INPUT && q.y.abs() < MAX_INPUT } +/// The whole number of 0 or more that `key` holds, read with [`index`]. fn usize_req(p: &Value, key: &str, cmd: &str) -> Result { - p.get(key).and_then(Value::as_u64).map(|v| v as usize).ok_or_else(|| bad(cmd, format!("missing integer `{key}`"))) + p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`"))) } /// The whole number of 0 or more that `key` holds, or `None` when `key` is absent or null. Any other diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 146fee505..f6f889916 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -171,7 +171,7 @@ pub fn specs() -> Vec { } fn file_close(s: &mut Session, p: &Value) -> Result { - let i = p.get("index").and_then(Value::as_u64).map(|v| v as usize).or(s.active_index()).ok_or(EngineError::NoDocument)?; + let i = p.get("index").and_then(Value::as_u64).map(index).or(s.active_index()).ok_or(EngineError::NoDocument)?; if !s.close_document(i) { return Err(bad("file.close", "no such document")); } @@ -179,7 +179,7 @@ fn file_close(s: &mut Session, p: &Value) -> Result { } fn doc_activate(s: &mut Session, p: &Value) -> Result { - let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("document.activate", "missing index"))? as usize; + let i = p.get("index").and_then(Value::as_u64).map(index).ok_or_else(|| bad("document.activate", "missing index"))?; if s.set_active(i) { ok() } else { Err(bad("document.activate", "no such document")) } } diff --git a/crates/engine/src/cmd/freeform.rs b/crates/engine/src/cmd/freeform.rs index 67d248072..c2e9e6d74 100644 --- a/crates/engine/src/cmd/freeform.rs +++ b/crates/engine/src/cmd/freeform.rs @@ -227,7 +227,7 @@ fn edit(s: &mut Session, p: &Value, cmd: &str, mut f: impl FnMut(&mut Freefor /// The `index` param, else the selected point. fn point_index(s: &Session, p: &Value, cmd: &str) -> Result { match p.get("index") { - Some(v) => v.as_u64().map(|i| i as usize).ok_or_else(|| bad(cmd, "`index` must be a point index")), + Some(v) => v.as_u64().map(index).ok_or_else(|| bad(cmd, "`index` must be a point index")), None => s.selected_freeform_point().ok_or_else(|| bad(cmd, "no point selected (give `index`)")), } } @@ -318,7 +318,7 @@ fn select_point(s: &mut Session, p: &Value) -> Result { None => return Err(bad(C, "missing `index` (a point index, or null to clear)")), Some(Value::Null) => None, Some(v) => { - let i = v.as_u64().ok_or_else(|| bad(C, "`index` must be a whole number or null"))? as usize; + let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`index` must be a whole number or null"))?; let (f, _) = s.proxy_freeform(!s.fill_active).ok_or_else(|| bad(C, NOT_FREEFORM))?; check_index(&f, i).map_err(|e| bad(C, e))?; Some(i) diff --git a/crates/engine/src/cmd/gradient.rs b/crates/engine/src/cmd/gradient.rs index 9d48231d6..cdee777a1 100644 --- a/crates/engine/src/cmd/gradient.rs +++ b/crates/engine/src/cmd/gradient.rs @@ -60,7 +60,7 @@ fn select_stop(s: &mut Session, p: &Value) -> Result { None => return Err(bad(C, "missing `index` (a stop index, or null to clear)")), Some(Value::Null) => None, Some(v) => { - let i = v.as_u64().ok_or_else(|| bad(C, "`index` must be a whole number or null"))? as usize; + let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`index` must be a whole number or null"))?; let n = active_gradient(s).ok_or_else(|| bad(C, "the active paint is not a gradient"))?.gradient.stops.len(); if i >= n { return Err(bad(C, format!("no stop {i} (the gradient has {n})"))); diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 33dbc3cf6..852652053 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -289,7 +289,7 @@ pub(crate) fn make_layer(d: &mut Document, name: Option<&str>) -> Node { pub(super) fn layer_color_param(v: &Value) -> std::result::Result { match v { Value::Number(n) => match n.as_u64() { - Some(i) if (i as usize) < LAYER_COLORS.len() => Ok(LayerColor::Preset(i as u8)), + Some(i) if index(i) < LAYER_COLORS.len() => Ok(LayerColor::Preset(i as u8)), _ => Err(format!("a colour index is 0 to {}, not {n}", LAYER_COLORS.len() - 1)), }, Value::String(s) => { @@ -781,7 +781,7 @@ fn move_node(s: &mut Session, p: &Value) -> Result { const C: &str = "node.move"; let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?; let parent = id_param(p, "parent"); - let index = p.get("index").and_then(Value::as_u64).unwrap_or(u64::MAX).min(usize::MAX as u64) as usize; + let index = p.get("index").and_then(Value::as_u64).map_or(usize::MAX, index); let d = &s.doc()?.doc; let n = d.node(id).ok_or(EngineError::NoNode(id))?; if !accepts(d, parent, n) { diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index 88d4faad2..0752c215e 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -1088,7 +1088,7 @@ fn move_group(s: &mut Session, p: &Value) -> Result { let (name, at) = s.libraries.change(&id, |lib| { let i = group_at(lib, group, C)?; let g = lib.groups.remove(i); - let at = usize::try_from(to).unwrap_or(usize::MAX).min(lib.groups.len()); + let at = index(to).min(lib.groups.len()); let name = g.name.clone(); lib.groups.insert(at, g); Ok((name, at)) diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 87ec6c8a5..9d0939c4a 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -341,6 +341,10 @@ pub(crate) fn id_param(p: &Value, key: &str) -> Option { pub(crate) fn ids_param(p: &Value, key: &str) -> Option> { p.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(NodeId).collect()) } +/// `v` as an index, a count or a byte offset: past `usize::MAX` reads as `usize::MAX`, past the end of any list. +pub(crate) fn index(v: u64) -> usize { + usize::try_from(v).unwrap_or(usize::MAX) +} /// Validate all explicit targets before a command changes the document or selection. pub(crate) fn checked_ids_param(s: &Session, p: &Value, key: &str, command: &str) -> Result> { let values = diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 7dda5deeb..dec10ccc6 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -279,7 +279,7 @@ pub fn specs() -> Vec { "Set Bounds", [], None, - "{x?, y?, width?, height?, reference?: 0..8 (9-point grid), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))", + "{x?, y?, width?, height?, reference?: 0..8 (9-point grid; a number past 8 is read as 8), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))", has_selection, set_bounds ), @@ -1791,7 +1791,7 @@ fn bounds_transform(s: &Session, p: &Value, ids: Vec, least: f64) -> Res let margins = |b: Rect| [g.x0 - b.x0, g.y0 - b.y0, b.x1 - g.x1, b.y1 - g.y1]; let (m, all) = (margins(vs), margins(v)); let fixed = [all[0] - m[0], all[1] - m[1], all[2] - m[2], all[3] - m[3]]; - let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize; + let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4).min(8) as usize; let rp = vectorcraft_geom::reference_point(v, refi); let (w, h) = wanted_size(p, v); // The geometric scale that gives the visual size: margins grow by the strokes' scale `f`. It is @@ -1954,7 +1954,7 @@ fn wanted_size(p: &Value, b: Rect) -> (f64, f64) { /// `b`, the box around the anchors (and the objects selected whole) square to the page, and only /// the anchors, their handles with them, and those objects move or scale. fn set_anchor_bounds(s: &mut Session, p: &Value, ids: Vec, b: Rect) -> Result { - let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize; + let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4).min(8) as usize; let rp = vectorcraft_geom::reference_point(b, refi); let (w, h) = wanted_size(p, b); // A single anchor, or anchors in a line, have no size to scale on that side. diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index 65c7be3b1..787f3ee3a 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -55,7 +55,7 @@ pub fn specs() -> Vec { fn artboard_reorder(s: &mut Session, p: &Value) -> Result { const C: &str = "artboard.reorder"; let i = artboard_index(&s.doc()?.doc, p, C)?; - let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(C, "missing to"))? as usize; + let to = p.get("to").and_then(Value::as_u64).map(index).ok_or_else(|| bad(C, "missing to"))?; s.edit("Reorder Artboards", |d, _| { if i >= d.artboards.len() { return Err(EngineError::Other("no such artboard".into())); diff --git a/crates/engine/src/cmd/path.rs b/crates/engine/src/cmd/path.rs index 7a671478a..f1ae27e01 100644 --- a/crates/engine/src/cmd/path.rs +++ b/crates/engine/src/cmd/path.rs @@ -226,7 +226,7 @@ pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_ge fn set_handle(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").ok_or_else(|| bad("path.setHandle", "missing id"))?; let si = usize_opt(p, "subpath", "path.setHandle")?.unwrap_or(0); - let ai = p.get("anchor").and_then(Value::as_u64).ok_or_else(|| bad("path.setHandle", "missing anchor"))? as usize; + let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.setHandle", "missing anchor"))?; let out = str_param(p, "which") != Some("in"); let pos = Point::new(f64_req(p, "x", "path.setHandle")?, f64_req(p, "y", "path.setHandle")?); let independent = bool_or(p, "independent", false); @@ -244,7 +244,7 @@ fn set_handle(s: &mut Session, p: &Value) -> Result { fn move_anchor(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").ok_or_else(|| bad("path.moveAnchor", "missing id"))?; let si = usize_opt(p, "subpath", "path.moveAnchor")?.unwrap_or(0); - let ai = p.get("anchor").and_then(Value::as_u64).ok_or_else(|| bad("path.moveAnchor", "missing anchor"))? as usize; + let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.moveAnchor", "missing anchor"))?; let pos = Point::new(f64_req(p, "x", "path.moveAnchor")?, f64_req(p, "y", "path.moveAnchor")?); s.edit("Move", |d, _| { let path = path_mut(d, id)?; @@ -772,7 +772,7 @@ fn reshape(s: &mut Session, p: &Value) -> Result { fn insert_anchor(s: &mut Session, p: &Value) -> Result { let id = id_param(p, "id").ok_or_else(|| bad("path.insertAnchor", "missing id"))?; let si = usize_opt(p, "subpath", "path.insertAnchor")?.unwrap_or(0); - let seg = p.get("segment").and_then(Value::as_u64).ok_or_else(|| bad("path.insertAnchor", "missing segment"))? as usize; + let seg = p.get("segment").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.insertAnchor", "missing segment"))?; let t = f64_or(p, "t", 0.5).clamp(0.0, 1.0); let idx = s.edit("Add Anchor Point", |d, _| { let path = path_mut(d, id)?; diff --git a/crates/engine/src/cmd/printtiling.rs b/crates/engine/src/cmd/printtiling.rs index 0cb37207c..690ef6196 100644 --- a/crates/engine/src/cmd/printtiling.rs +++ b/crates/engine/src/cmd/printtiling.rs @@ -96,7 +96,7 @@ fn set_origin(s: &mut Session, p: &Value) -> Result { let regions = vectorcraft_pdf::print_regions(doc, &set).map_err(|e| pdf_error(SET, e))?; let picked = match p.get("artboard").filter(|v| !v.is_null()) { Some(v) => { - let i = v.as_u64().ok_or_else(|| bad(SET, "artboard must be a 0-based index"))? as usize; + let i = v.as_u64().map(index).ok_or_else(|| bad(SET, "artboard must be a 0-based index"))?; regions.iter().find(|(a, _)| a.is_none() || *a == Some(i)).ok_or_else(|| bad(SET, format!("artboard {i} doesn't print")))? } None => region_holding(®ions, at).or(regions.first()).ok_or_else(|| bad(SET, "nothing prints"))?, diff --git a/crates/engine/src/cmd/recolor.rs b/crates/engine/src/cmd/recolor.rs index cbab99a88..f48082e6b 100644 --- a/crates/engine/src/cmd/recolor.rs +++ b/crates/engine/src/cmd/recolor.rs @@ -156,11 +156,11 @@ fn reduce(s: &mut Session, p: &Value) -> Result { } } let unit_colors = |g: &[&Used]| g.iter().map(|u| u.color).collect::>(); - let rows: Vec> = match p.get("colors").and_then(Value::as_u64) { - Some(n) if (n as usize) < units.len() => { + let rows: Vec> = match p.get("colors").and_then(Value::as_u64).map(index) { + Some(n) if n < units.len() => { let keys: Vec = units.iter().map(|g| method.key_color(&unit_colors(g)).unwrap_or_default()).collect(); let weights: Vec = units.iter().map(|g| g.iter().map(|u| u.count as f32).sum()).collect(); - cluster(&keys, &weights, n as usize).into_iter().map(|ix| ix.into_iter().flat_map(|i| units[i].iter().copied()).collect()).collect() + cluster(&keys, &weights, n).into_iter().map(|ix| ix.into_iter().flat_map(|i| units[i].iter().copied()).collect()).collect() } _ => units, }; diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index 2197877e3..2f39fe472 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -266,7 +266,7 @@ fn all(s: &mut Session, _: &Value) -> Result { fn all_on_artboard(s: &mut Session, p: &Value) -> Result { let st = s.doc()?; - let i = p.get("artboard").and_then(Value::as_u64).unwrap_or(0) as usize; + let i = p.get("artboard").and_then(Value::as_u64).map_or(0, index); let r = st.doc.artboards.get(i).map(|a| a.rect).ok_or_else(|| bad("select.allOnArtboard", "no such artboard"))?; // Symbol instances are measured by their art. let symbols = st.doc.symbol_boxes(); @@ -644,7 +644,7 @@ fn same_attribute(s: &mut Session, p: &Value) -> Result { let st = s.doc()?; let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).ok_or_else(|| bad(cmd, "nothing selected"))?; let ap = &refn.appearance; - let item = p.get("item").and_then(Value::as_u64).map(|i| i as usize).or(s.appearance_item()); + let item = p.get("item").and_then(Value::as_u64).map(index).or(s.appearance_item()); let attr = match (item.and_then(|i| ap.items.get(i)), ap.effects.first()) { (Some(it), _) => Attribute::Item(it.clone()), (None, Some(e)) => Attribute::Effect(e.clone()), diff --git a/crates/engine/src/cmd/slices.rs b/crates/engine/src/cmd/slices.rs index 2a814a55e..b75680e1a 100644 --- a/crates/engine/src/cmd/slices.rs +++ b/crates/engine/src/cmd/slices.rs @@ -409,7 +409,7 @@ fn splits(lo: f64, hi: f64, count: Option, size: Option) -> std::resul (None, Some(_)) => return Err("a slice size must be a positive number of points".into()), (None, None) => 1, }; - if n == 0 || n as usize > MAX_PIECES { + if n == 0 || index(n) > MAX_PIECES { return Err(format!("divide into 1 to {MAX_PIECES} slices")); } let step = match (count, size) { diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 5f796bcf2..486aa0d4e 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -547,7 +547,7 @@ fn style_move(s: &mut Session, p: &Value) -> Result { let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing `to` (an index)"))?; let d = &s.doc()?.doc; let from = style_index(d, name)?; - let to = (to as usize).min(d.graphic_styles.len() - 1); + let to = index(to).min(d.graphic_styles.len() - 1); if from == to { return ok(); } diff --git a/crates/engine/src/cmd/swatch.rs b/crates/engine/src/cmd/swatch.rs index e52e64849..00b6f838f 100644 --- a/crates/engine/src/cmd/swatch.rs +++ b/crates/engine/src/cmd/swatch.rs @@ -364,7 +364,7 @@ fn insert_before(list: &mut Vec, items: Vec, anchor: Option<&str>, name fn swatch_move(s: &mut Session, p: &Value) -> Result { const C: &str = "swatch.move"; let names = names_param(p, C)?; - let to = p.get("to").and_then(Value::as_u64).map(|v| v as usize); + let to = p.get("to").and_then(Value::as_u64).map(index); let group = str_param(p, "group").map(str::to_string); let moved = names.len(); s.edit("Move Swatch", |d, _| { diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index d0fccf1af..378e71290 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -174,8 +174,9 @@ pub(crate) fn set_plain_text(d: &mut vectorcraft_doc::Document, id: NodeId, text Ok(()) } +/// The byte offset that `k` holds, read with [`index`]. fn byte_param(p: &Value, k: &str) -> Option { - p.get(k).and_then(Value::as_u64).map(|v| v as usize) + p.get(k).and_then(Value::as_u64).map(index) } fn range_of(p: &Value, len: usize) -> (usize, usize) { diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index d1ed3376f..0aaee719c 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -343,10 +343,10 @@ fn selection_attrs(s: &Session, p: &Value, kind: Kind) -> Result text_targets(s, &json!({}), "").ok().and_then(|v| v.first().copied()), }; let Some(NodeKind::Text(t)) = id.and_then(|i| d.node(i)).map(|n| &n.kind) else { return Ok(None) }; - let at = p.get("start").and_then(Value::as_u64).map_or(0, |v| usize::try_from(v).unwrap_or(usize::MAX)); + let at = p.get("start").and_then(Value::as_u64).map_or(0, index); Ok(Some(match kind { Kind::Char => { - let style = match p.get("end").and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)) { + let style = match p.get("end").and_then(Value::as_u64).map(index) { Some(end) => vectorcraft_text::edit::insertion_style(&t.runs, at.min(end), at.max(end)), None => vectorcraft_text::edit::style_at(&t.runs, at), }; @@ -487,12 +487,9 @@ fn apply(s: &mut Session, p: &Value, kind: Kind) -> Result { match kind { Kind::Char => { let len = vectorcraft_text::edit::runs_len(&t.runs); - let (a, b) = match range { - Some(_) => { - let g = |k: &str, def: usize| p.get(k).and_then(Value::as_u64).map_or(def, |v| (v as usize).min(len)); - (g("start", 0), g("end", len)) - } - None => (0, len), + let (a, b) = match (range, text_range) { + (Some(_), Some(r)) => r.bytes(t), + _ => (0, len), }; let mut err = None; vectorcraft_text::edit::style_range(&mut t.runs, a.min(b), a.max(b), |st| match char_fn(st) { diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index a241bff3c..ef0799e9b 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -532,9 +532,7 @@ impl TextRange { let get = |k: &str| -> Result> { match p.get(k) { None | Some(Value::Null) => Ok(None), - Some(v) => { - v.as_u64().map(|n| Some(usize::try_from(n).unwrap_or(usize::MAX))).ok_or_else(|| bad(cmd, format!("`{k}` must be a byte offset"))) - } + Some(v) => v.as_u64().map(|n| Some(index(n))).ok_or_else(|| bad(cmd, format!("`{k}` must be a byte offset"))), } }; let (start, end) = (get("start")?, get("end")?); diff --git a/crates/engine/src/cmd/xform.rs b/crates/engine/src/cmd/xform.rs index c98b0423d..1cc7da7f5 100644 --- a/crates/engine/src/cmd/xform.rs +++ b/crates/engine/src/cmd/xform.rs @@ -345,7 +345,7 @@ fn sample_color(s: &mut Session, p: &Value) -> Result { let (id, mut q) = match p.get("stop") { None | Some(Value::Null) => (if stroke { "paint.setStroke" } else { "paint.setFill" }, json!({ "color": c })), Some(v) => { - let i = v.as_u64().ok_or_else(|| bad(C, "`stop` must be a stop index"))? as usize; + let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`stop` must be a stop index"))?; let color = super::paint::new_color(s, p, color_value(c).ok_or_else(|| bad(C, format!("bad color {c}")))?); let (fill, stroke_paint) = s.proxy_paints(); let Paint::Gradient(g) = (if stroke { stroke_paint } else { fill }) else { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 7ac01ea1a..c18353947 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1084,6 +1084,45 @@ fn set_bounds_acts_on_direct_selected_anchors() { assert_eq!(points(&s), [(10.0, 0.0), (140.0, 20.0), (60.0, 100.0), (10.0, 100.0)]); } +/// `object.setBounds` reads a `reference` past 8 as 8, the bottom-right point, as +/// `object.transformEach` does, for a path selected whole and for direct-selected anchors. Before, +/// 9 read as the bottom-left point, 2^32 as the bottom center, and a 32-bit build read 2^32 as the +/// top-left point. +#[test] +fn set_bounds_reads_a_reference_past_8_as_8() { + for reference in [9, 1u64 << 32] { + for anchors in [false, true] { + let mut s = session(); + let square = json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}], "closed": true}); + let id = NodeId(s.execute("path.create", &square).unwrap()["id"].as_u64().unwrap()); + if anchors { + s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 0], [0, 1], [0, 2], [0, 3]]})).unwrap(); + } else { + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + } + s.execute("object.setBounds", &json!({"width": 50, "height": 50, "reference": reference})).unwrap(); + let NodeKind::Path { path, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("a path") }; + let points: Vec<(f64, f64)> = path.subpaths[0].anchors.iter().map(|a| (a.p.x.round(), a.p.y.round())).collect(); + assert_eq!(points, [(50.0, 50.0), (100.0, 50.0), (100.0, 100.0), (50.0, 100.0)], "reference {reference}, anchors {anchors}"); + } + } +} + +/// `cmd::index` keeps a number that fits in a `usize` and reads a larger one (2^32 or more on a +/// 32-bit build) as `usize::MAX`. +#[test] +fn index_reads_a_number_past_usize_max_as_usize_max() { + let past_u32 = u64::from(u32::MAX) + 1; + #[cfg(target_pointer_width = "64")] + let expected = 1usize << 32; + #[cfg(not(target_pointer_width = "64"))] + let expected = usize::MAX; + assert_eq!(cmd::index(0), 0); + assert_eq!(cmd::index(7), 7); + assert_eq!(cmd::index(past_u32), expected); + assert_eq!(cmd::index(u64::MAX), usize::MAX); +} + /// Arrange commands that leave the stacking order as it is record no undo step: Bring to Front and /// Bring Forward on the front objects, Send to Back and Send Backward on the back ones, and Send to /// Current Layer on objects already on top of the current layer. diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index d635fd291..40c81bc33 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -974,6 +974,50 @@ fn split_refuses_a_subpath_anchor_or_segment_that_is_not_a_whole_number() { } } +/// A subpath, anchor or segment of 2^32 + k fails, where k names one that exists, and the document +/// and the undo history stay as they were. A 32-bit build read the number as k. +#[test] +fn path_commands_refuse_a_subpath_anchor_or_segment_past_u32() { + let mut s = session(); + let id = two_subpaths(&mut s); + let at_anchor = json!({"id": id.0, "anchor": 1}); + let at_segment = json!({"id": id.0, "segment": 0, "t": 0.5}); + let curvature_move = json!({"id": id.0, "op": "move", "anchor": 1, "x": 200, "y": 150}); + let curvature_insert = json!({"id": id.0, "op": "insert", "segment": 0, "t": 0.5}); + let convert = json!({"id": id.0, "anchor": 1, "to": "corner"}); + let reshape = json!({"id": id.0, "segment": 0, "t": 0.5, "dx": 0, "dy": 20}); + let handle = json!({"id": id.0, "anchor": 0, "which": "out", "x": 140, "y": 250}); + let moved = json!({"id": id.0, "anchor": 0, "x": 140, "y": 250}); + for (cmd, p, key) in [ + ("path.curvatureEdit", &curvature_move, "subpath"), + ("path.curvatureEdit", &curvature_move, "anchor"), + ("path.curvatureEdit", &curvature_insert, "segment"), + ("path.removeAnchor", &at_anchor, "subpath"), + ("path.removeAnchor", &at_anchor, "anchor"), + ("path.convertAnchor", &convert, "subpath"), + ("path.convertAnchor", &convert, "anchor"), + ("path.reshapeSegment", &reshape, "subpath"), + ("path.reshapeSegment", &reshape, "segment"), + ("path.split", &at_anchor, "anchor"), + ("path.split", &at_segment, "segment"), + ("path.setHandle", &handle, "subpath"), + ("path.setHandle", &handle, "anchor"), + ("path.moveAnchor", &moved, "subpath"), + ("path.moveAnchor", &moved, "anchor"), + ("path.insertAnchor", &at_segment, "subpath"), + ("path.insertAnchor", &at_segment, "segment"), + ] { + let mut p = p.clone(); + let k = if key == "subpath" { 1 } else { p[key].as_u64().unwrap() }; + p[key] = json!((1u64 << 32) + k); + let before = s.doc().unwrap().doc.clone(); + let undo = undo_steps(&s); + assert!(s.execute(cmd, &p).is_err(), "{cmd} {p} succeeded"); + assert!(s.doc().unwrap().doc == before, "{cmd} {p} changed the document"); + assert_eq!(undo_steps(&s), undo, "{cmd} {p} left an undo step"); + } +} + #[test] fn set_handle_refuses_a_subpath_that_is_not_a_whole_number() { let mut s = session(); diff --git a/crates/engine/src/tests_freeform.rs b/crates/engine/src/tests_freeform.rs index 26ec4203f..41c65dc7b 100644 --- a/crates/engine/src/tests_freeform.rs +++ b/crates/engine/src/tests_freeform.rs @@ -262,6 +262,27 @@ fn point_commands_need_a_freeform_gradient() { assert!(s.execute("paint.freeform.addPoint", &json!({"at": [1, 1]})).is_err()); } +/// The point and line commands refuse a point, line or segment of 2^32 + k, where k names one that +/// exists, and the points and the selected point stay as they were. A 32-bit build read the number +/// as k. +#[test] +fn point_commands_refuse_an_index_past_u32() { + let (mut s, id) = freeform_rect(); + run(&mut s, "paint.freeform.addLine", json!({"points": [0, 1]})); + let (before, selected) = (points(&s, id), s.selected_freeform_point()); + let past = |k: u64| (1u64 << 32) + k; + for (cmd, p) in [ + ("paint.freeform.setPoint", json!({"index": past(1), "color": "#000000"})), + ("paint.freeform.deletePoint", json!({"index": past(1)})), + ("paint.freeform.selectPoint", json!({"index": past(1)})), + ("paint.freeform.splitLine", json!({"line": past(0), "segment": 0})), + ("paint.freeform.splitLine", json!({"line": 0, "segment": past(0)})), + ] { + assert!(s.execute(cmd, &p).is_err(), "{cmd} {p}"); + assert_eq!((points(&s, id), s.selected_freeform_point()), (before.clone(), selected), "{cmd} {p}"); + } +} + #[test] fn selection_follows_the_object_and_the_proxy() { let (mut s, id) = freeform_rect(); diff --git a/crates/engine/src/tests_gradient.rs b/crates/engine/src/tests_gradient.rs index 6db651f41..b22e3df1d 100644 --- a/crates/engine/src/tests_gradient.rs +++ b/crates/engine/src/tests_gradient.rs @@ -288,6 +288,8 @@ fn select_stop_is_validated_and_shared() { assert_eq!(s.execute("gradient.selectStop", &json!({"index": 1})).unwrap(), json!({"index": 1})); assert_eq!(s.selected_stop(), Some(1)); assert!(s.execute("gradient.selectStop", &json!({"index": 2})).is_err(), "two stops"); + // A 32-bit build read 2^32 + 1 as stop 1. + assert!(s.execute("gradient.selectStop", &json!({"index": (1u64 << 32) + 1})).is_err(), "past u32"); assert!(s.execute("gradient.selectStop", &json!({"index": "a"})).is_err()); assert!(s.execute("gradient.selectStop", &json!({})).is_err()); s.execute("gradient.selectStop", &json!({"index": null})).unwrap(); diff --git a/crates/engine/src/tests_gradpanel.rs b/crates/engine/src/tests_gradpanel.rs index ed2737223..b01592ab2 100644 --- a/crates/engine/src/tests_gradpanel.rs +++ b/crates/engine/src/tests_gradpanel.rs @@ -38,6 +38,8 @@ fn sample_color_with_a_stop_recolours_that_stop_in_one_undo_step() { s.execute("paint.sampleColor", &json!({"color": "#ff0000", "stop": 1})).unwrap(); assert_eq!(stop_hexes(&s, id, false), ["#ffffff", "#ff0000"]); assert!(s.execute("paint.sampleColor", &json!({"color": "#ff0000", "stop": 2})).unwrap_err().to_string().contains("no stop 2")); + // A 32-bit build read 2^32 + 1 as stop 1. + assert!(s.execute("paint.sampleColor", &json!({"color": "#00ff00", "stop": (1u64 << 32) + 1})).is_err()); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(stop_hexes(&s, id, false), ["#ffffff", "#000000"]); // The stroke's gradient through `stroke`, the fill left alone. diff --git a/crates/engine/src/tests_layers.rs b/crates/engine/src/tests_layers.rs index 1fc789c80..d78645e82 100644 --- a/crates/engine/src/tests_layers.rs +++ b/crates/engine/src/tests_layers.rs @@ -349,6 +349,8 @@ fn layer_options_validate_and_template_locks_and_dims() { p["id"] = json!(layer.0); assert!(s.execute("layer.setProps", &p).is_err(), "{bad}"); } + // A 32-bit build read color 2^32 + 4 as preset 4. + assert!(s.execute("layer.setProps", &json!({"id": layer.0, "color": (1u64 << 32) + 4})).is_err()); let color = |s: &Session| match node(s, layer).kind { NodeKind::Layer { color, .. } => color, _ => panic!("not a layer"), diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index 1a647ad88..6fe89e7e5 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -1515,6 +1515,17 @@ fn select_all_on_artboard_takes_lines_on_its_right_and_bottom_edges() { assert_eq!(on(1), vec![right, shared]); } +/// All on Active Artboard refuses artboard 2^32 and selects nothing. A 32-bit build read it as +/// artboard 0. +#[test] +fn select_all_on_artboard_refuses_an_artboard_past_u32() { + let mut s = session(); + rect(&mut s, 10.0, 10.0, 20.0, 20.0); + s.execute("select.none", &json!({})).unwrap(); + assert!(s.execute("select.allOnArtboard", &json!({"artboard": 1u64 << 32})).is_err()); + assert!(selected(&s).is_empty()); +} + #[test] fn save_and_recall_selection() { let mut s = session(); @@ -2699,6 +2710,18 @@ fn close_all_documents() { assert!(s.execute("file.closeAll", &json!({})).is_err()); } +/// Close and Activate refuse a document index of 2^32, and both documents stay open with the second +/// one active. A 32-bit build read the index as 0. +#[test] +fn close_and_activate_refuse_a_document_index_past_u32() { + let mut s = session(); + s.execute("file.new", &json!({})).unwrap(); + for cmd in ["document.activate", "file.close"] { + assert!(s.execute(cmd, &json!({"index": 1u64 << 32})).is_err(), "{cmd}"); + assert_eq!((s.documents().len(), s.active_index()), (2, Some(1)), "{cmd}"); + } +} + #[test] fn document_color_mode_converts_colours() { let mut s = session(); diff --git a/crates/engine/src/tests_panelcmds.rs b/crates/engine/src/tests_panelcmds.rs index 68beb8b1d..ad15852d8 100644 --- a/crates/engine/src/tests_panelcmds.rs +++ b/crates/engine/src/tests_panelcmds.rs @@ -81,6 +81,31 @@ fn appearance_duplicate_and_move() { assert!(s.execute("appearance.duplicateItem", &json!({"index": 99})).is_err()); } +/// The appearance and effect commands given an item index of 2^32 + k, where k names one of the +/// object's items, leave the object's appearance and the undo history as they were, as for any index +/// past the last item: `appearance.removeItem` removes nothing, and the others fail. A 32-bit build +/// read the index as k. +#[test] +fn appearance_commands_leave_an_item_past_u32_alone() { + let mut s = session(); + let id = rect(&mut s); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + let before = node(&s, id).appearance; + let undo = s.doc().unwrap().history.undo.len(); + let past = |k: u64| (1u64 << 32) + k; + for (cmd, p, fails) in [ + ("appearance.removeItem", json!({"index": past(0)}), false), + ("appearance.removeItem", json!({"indices": [past(0)]}), false), + ("appearance.duplicateItem", json!({"index": past(0)}), true), + ("appearance.moveItem", json!({"from": past(1), "to": 0}), true), + ("effect.apply", json!({"effect": "distort.roughen", "item": past(1)}), true), + ] { + assert_eq!(s.execute(cmd, &p).is_err(), fails, "{cmd} {p}"); + assert_eq!(node(&s, id).appearance, before, "{cmd} {p}"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{cmd} {p}"); + } +} + #[test] fn artboards_reorder_and_duplicate() { let mut s = session(); @@ -155,6 +180,17 @@ fn artboard_color_round_trips_through_the_native_format() { assert_eq!(back.artboards, s.doc().unwrap().doc.artboards); } +/// An artboard moved to place 2^32 goes last. A 32-bit build read the place as 0 and moved it to the +/// front. +#[test] +fn artboard_reorder_past_u32_goes_last() { + let mut s = session(); + s.execute("artboard.new", &json!({"name": "Second"})).unwrap(); + s.execute("artboard.new", &json!({"name": "Third"})).unwrap(); + s.execute("artboard.reorder", &json!({"index": 1, "to": 1u64 << 32})).unwrap(); + assert_eq!(s.doc().unwrap().doc.artboards.last().map(|a| a.name.as_str()), Some("Second")); +} + /// The artboards' left edges and names. fn boards(s: &Session) -> Vec<(f64, String)> { s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.name.clone())).collect() diff --git a/crates/engine/src/tests_printtiling.rs b/crates/engine/src/tests_printtiling.rs index 122acb3ab..3df7c1298 100644 --- a/crates/engine/src/tests_printtiling.rs +++ b/crates/engine/src/tests_printtiling.rs @@ -144,6 +144,8 @@ fn tiles_start_at_the_tile_origin_and_cover_the_art() { // Bad input is refused. assert!(s.execute("print.tiling.set", &json!({})).is_err()); assert!(s.execute("print.tiling.set", &json!({"origin": [1, 2], "artboard": 7})).is_err()); + // A 32-bit build read 2^32 as artboard 0. + assert!(s.execute("print.tiling.set", &json!({"origin": [1, 2], "artboard": 1u64 << 32})).is_err()); assert!(s.execute("print.tiling.set", &json!({"origin": [1e9, 2]})).is_err()); } diff --git a/crates/engine/src/tests_recolor.rs b/crates/engine/src/tests_recolor.rs index 84d4cc670..6cf08905a 100644 --- a/crates/engine/src/tests_recolor.rs +++ b/crates/engine/src/tests_recolor.rs @@ -72,6 +72,8 @@ fn five_colours_reduce_to_two() { assert_eq!(run(&mut s, "recolor.colors", json!({}))["colors"].as_array().unwrap().len(), 5); // Without a count every colour is its own row. assert_eq!(run(&mut s, "recolor.reduce", json!({}))["map"].as_array().unwrap().len(), 5); + // A count larger than the number of colors also keeps every color in its own row. A 32-bit build read 2^32 + 2 as 2. + assert_eq!(run(&mut s, "recolor.reduce", json!({"colors": (1u64 << 32) + 2}))["map"].as_array().unwrap().len(), 5); } #[test] diff --git a/crates/engine/src/tests_slices.rs b/crates/engine/src/tests_slices.rs index 24674a1ff..897d0569f 100644 --- a/crates/engine/src/tests_slices.rs +++ b/crates/engine/src/tests_slices.rs @@ -140,6 +140,9 @@ fn divide_two_by_three_and_by_size() { assert_eq!(hs, [25.0, 25.0, 10.0]); assert!(s.execute("object.slice.divide", &json!({"rows": 0})).is_err()); assert!(s.execute("object.slice.divide", &json!({"columns": 5000})).is_err()); + // A 32-bit build read 2^32 + 2 as 2, passed the check and went on to list 2^32 + 3 cuts. + let e = s.execute("object.slice.divide", &json!({"rows": (1u64 << 32) + 2})).unwrap_err().to_string(); + assert!(e.contains("divide into 1 to"), "{e}"); assert!(s.execute("object.slice.divide", &json!({})).is_err(), "nothing to divide by"); } diff --git a/crates/engine/src/tests_stylepanel.rs b/crates/engine/src/tests_stylepanel.rs index 07ffb03ff..81a165d1e 100644 --- a/crates/engine/src/tests_stylepanel.rs +++ b/crates/engine/src/tests_stylepanel.rs @@ -102,6 +102,16 @@ fn move_reorders_and_undoes_in_one_step() { assert!(s.execute("graphicStyle.move", &json!({"name": first})).is_err()); } +/// A graphic style moved to place 2^32 goes last. A 32-bit build read the place as 0 and moved it +/// to the front. +#[test] +fn move_past_u32_goes_last() { + let mut s = session(); + let second = names(&s)[1].clone(); + run(&mut s, "graphicStyle.move", json!({"name": second, "to": 1u64 << 32})); + assert_eq!(names(&s).last().unwrap(), &second); +} + #[test] fn override_character_color_clears_the_run_fill() { let mut s = session(); diff --git a/crates/engine/src/tests_styles.rs b/crates/engine/src/tests_styles.rs index 8c0ff4e76..1b464d564 100644 --- a/crates/engine/src/tests_styles.rs +++ b/crates/engine/src/tests_styles.rs @@ -250,6 +250,10 @@ fn select_same_appearance_attribute() { run(&mut s, "select.set", json!({"ids": [a.0]})); run(&mut s, "select.same.appearanceAttribute", json!({"item": 1})); assert_eq!(s.doc().unwrap().selection.objects, vec![a, b, c]); + // An item past the last one leaves the shared effect. A 32-bit build read 2^32 + 1 as item 1. + run(&mut s, "select.set", json!({"ids": [a.0]})); + run(&mut s, "select.same.appearanceAttribute", json!({"item": (1u64 << 32) + 1})); + assert_eq!(s.doc().unwrap().selection.objects, vec![a, b]); } #[test] diff --git a/crates/engine/src/tests_swatchcmds.rs b/crates/engine/src/tests_swatchcmds.rs index 459bf671e..22e7227e6 100644 --- a/crates/engine/src/tests_swatchcmds.rs +++ b/crates/engine/src/tests_swatchcmds.rs @@ -56,6 +56,17 @@ fn move_between_groups_is_one_undo_step() { assert_eq!(names(&s, None).last().map(String::as_str), Some("White")); } +/// A swatch or color group moved to place 2^32 goes last. A 32-bit build read the place as 0 and +/// moved it to the front. +#[test] +fn move_past_u32_goes_last() { + let mut s = session(); + run(&mut s, "swatch.move", json!({"name": "White", "to": 1u64 << 32})); + assert_eq!(names(&s, None).last().map(String::as_str), Some("White")); + run(&mut s, "swatch.move", json!({"name": "Grays", "to": 1u64 << 32})); + assert_eq!(doc(&s).swatch_groups.last().map(|g| g.name.as_str()), Some("Grays")); +} + #[test] fn move_reorders_groups_and_refuses_bad_moves() { let mut s = session(); diff --git a/crates/engine/src/tests_textedit.rs b/crates/engine/src/tests_textedit.rs index 4a761ec0c..63bd2a35b 100644 --- a/crates/engine/src/tests_textedit.rs +++ b/crates/engine/src/tests_textedit.rs @@ -55,6 +55,9 @@ fn edit_range_replaces_and_undoes() { assert_eq!(obj(&s, id).plain_text(), "Hello!"); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(obj(&s, id).plain_text(), "Hello there"); + // An offset of 2^32 is past the end too. A 32-bit build read it as 0 and inserted at the start. + s.execute("text.editRange", &json!({"id": id.0, "start": 1u64 << 32, "end": 1u64 << 32, "insert": "!"})).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "Hello there!"); assert!(s.execute("text.editRange", &json!({"start": 0})).is_err(), "id required"); let rect = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); assert!(s.execute("text.editRange", &json!({"id": rect["id"], "insert": "x"})).is_err(), "not text"); @@ -84,6 +87,17 @@ fn set_range_style_splits_runs() { assert!(s.execute("text.setRangeStyle", &json!({"id": id.0, "size": -1})).is_err()); } +/// A character style applied from byte 2^32 to 2^32 + 5 styles no character, as a range past the +/// end does. A 32-bit build read the range as bytes 0 to 5 and styled "Hello". +#[test] +fn char_style_range_past_u32_styles_nothing() { + let mut s = session(); + let id = text(&mut s, "Hello world"); + s.execute("charStyle.new", &json!({"name": "Big", "attrs": {"size": 40.0}})).unwrap(); + s.execute("charStyle.apply", &json!({"name": "Big", "id": id.0, "start": 1u64 << 32, "end": (1u64 << 32) + 5})).unwrap(); + assert!(obj(&s, id).runs.iter().all(|r| r.style.size == 20.0 && r.style.style_name.is_none()), "{:?}", obj(&s, id).runs); +} + #[test] fn set_range_style_defaults_to_all_text_and_get_range() { let mut s = session(); @@ -147,6 +161,8 @@ fn touch_type_moves_one_character_and_leaves_the_others_where_they_are() { assert!(near(moved_by(&before, &glyph_pens(&s, id), 0), (0.0, 2.0))); assert!(s.execute("text.touchType", &json!({"id": id.0, "start": 1})).is_err(), "nothing to change"); assert!(s.execute("text.touchType", &json!({"id": id.0, "start": 9, "dy": 1})).is_err(), "no character there"); + // A 32-bit build read 2^32 + 1 as character 1. + assert!(s.execute("text.touchType", &json!({"id": id.0, "start": (1u64 << 32) + 1, "dy": 1})).is_err(), "no character there"); } /// Raising, scaling or rotating a character leaves it and the characters after it where they diff --git a/docs/mcp.md b/docs/mcp.md index d334d3345..d4d1e178e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -942,6 +942,16 @@ missing one does. `` `columns` must be a number, not "4" ``. Each message names the key that was given, and the command then changes nothing. +A whole number that is larger than the build's `usize` can hold (4294967295 on a 32-bit build, such as the +`wasm32-unknown-unknown` web build) counts as larger than any index, place, byte offset or count. An index, such as +a `subpath`, `anchor`, `segment`, document `index`, `artboard`, appearance `item` or `index`, gradient `stop` or +freeform point `index`, names nothing, and the command does what it does for any other index past the end: most +commands fail and change nothing, and `appearance.removeItem` removes nothing. A place (`to` in `swatch.move`, +`artboard.reorder`, `graphicStyle.move`, `appearance.moveItem`, `appearance.duplicateItem` and `effect.move`) puts +the item last. A text `start` or `end` is past the end of the text. `object.slice.divide` refuses such a `rows` or +`columns`, `recolor.reduce` keeps each color in its own row for such a `colors`, and `layer.setProps` refuses such a +`color`. `object.setBounds` reads a `reference` past 8 as 8, as `object.transformEach` does. + The blend, envelope and gradient mesh commands refuse a string or another value that is not a number with the same message, as in `` `steps` must be a number, not "8" ``, and change nothing: `steps`, `distance` and `value` in `object.blend.make` and `object.blend.options`; `bend`, `h`, `v`, `horizontalDistortion` and `verticalDistortion` in From c895f640632fce196d37ac9b0ff99058c9a298c9 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 21 Oct 2026 23:35:29 +0700 Subject: [PATCH 2/2] Format: rustfmt after the batch --- crates/doc/src/corners.rs | 43 ++++++++++++++++++++++++---- crates/doc/src/node.rs | 22 ++++++++++++-- crates/effects/src/group.rs | 6 +--- crates/effects/src/pixel/artistic.rs | 6 +--- crates/effects/src/pixel/tests.rs | 5 +--- crates/engine/src/cmd/menucmds.rs | 9 +++++- crates/svg/tests/css_rules.rs | 9 +++++- crates/tools/src/corners.rs | 12 ++++++-- crates/ui-egui/src/dialogs/effect.rs | 6 +--- crates/ui-egui/src/dock.rs | 14 ++------- crates/ui-egui/src/floating.rs | 4 +-- crates/ui-egui/src/panel_docking.rs | 32 ++++----------------- crates/ui-egui/src/tests_labels.rs | 12 +------- crates/ui-egui/src/workspaces.rs | 7 +---- 14 files changed, 98 insertions(+), 89 deletions(-) diff --git a/crates/doc/src/corners.rs b/crates/doc/src/corners.rs index e69b528e5..24228604a 100644 --- a/crates/doc/src/corners.rs +++ b/crates/doc/src/corners.rs @@ -60,7 +60,9 @@ pub fn convert_radii_relative(live: &mut LiveShape, relative: bool) { } } match live { - LiveShape::Rectangle { radii_relative: rel, .. } | LiveShape::Polygon { radii_relative: rel, .. } | LiveShape::Path { radii_relative: rel, .. } => { + LiveShape::Rectangle { radii_relative: rel, .. } + | LiveShape::Polygon { radii_relative: rel, .. } + | LiveShape::Path { radii_relative: rel, .. } => { *rel = relative; } LiveShape::Ellipse { .. } | LiveShape::Line { .. } => {} @@ -290,7 +292,14 @@ mod tests { #[test] fn rectangle_corners_map_to_their_anchors() { - let l = LiveShape::Rectangle { w: 100.0, h: 50.0, radii: [0.0, 10.0, 0.0, 0.0], kinds: Default::default(), radii_relative: false, xf: Affine::IDENTITY }; + let l = LiveShape::Rectangle { + w: 100.0, + h: 50.0, + radii: [0.0, 10.0, 0.0, 0.0], + kinds: Default::default(), + radii_relative: false, + xf: Affine::IDENTITY, + }; let path = l.to_path(); let c = LiveCorners::new(&path, Some(&l)).unwrap(); // Anchors: top-left, the top-right corner's two, bottom-right, bottom-left. @@ -333,7 +342,8 @@ mod tests { /// A polygon's corners are cut in the document and survive a change of sides when shared. #[test] fn polygon_corners_follow_their_sides() { - let mut l = LiveShape::Polygon { radius: 50.0, sides: 6, xf: Affine::translate((100.0, 100.0)), radii: vec![], kinds: vec![], radii_relative: false }; + let mut l = + LiveShape::Polygon { radius: 50.0, sides: 6, xf: Affine::translate((100.0, 100.0)), radii: vec![], kinds: vec![], radii_relative: false }; let (mut path, mut live) = (l.to_path(), Some(Box::new(l.clone()))); set_corners(&mut path, &mut live, &set(&[0, 1, 2, 3, 4, 5]), Some(8.0), None); assert_eq!(path.anchor_count(), 12); @@ -355,11 +365,25 @@ mod tests { #[test] fn relative_radii_scale_with_rectangle_size() { - let l = LiveShape::Rectangle { w: 100.0, h: 100.0, radii: [0.25, 0.25, 0.25, 0.25], kinds: Default::default(), radii_relative: true, xf: Affine::IDENTITY }; + let l = LiveShape::Rectangle { + w: 100.0, + h: 100.0, + radii: [0.25, 0.25, 0.25, 0.25], + kinds: Default::default(), + radii_relative: true, + xf: Affine::IDENTITY, + }; let path = l.to_path(); let c = LiveCorners::new(&path, Some(&l)).unwrap(); let r0 = c.drawn_radius(0); - let l2 = LiveShape::Rectangle { w: 200.0, h: 200.0, radii: [0.25, 0.25, 0.25, 0.25], kinds: Default::default(), radii_relative: true, xf: Affine::IDENTITY }; + let l2 = LiveShape::Rectangle { + w: 200.0, + h: 200.0, + radii: [0.25, 0.25, 0.25, 0.25], + kinds: Default::default(), + radii_relative: true, + xf: Affine::IDENTITY, + }; let path2 = l2.to_path(); let c2 = LiveCorners::new(&path2, Some(&l2)).unwrap(); assert!((c2.drawn_radius(0) / r0 - 2.0).abs() < 0.05, "{r0} → {}", c2.drawn_radius(0)); @@ -367,7 +391,14 @@ mod tests { #[test] fn convert_between_relative_and_absolute_radii() { - let mut l = LiveShape::Rectangle { w: 100.0, h: 50.0, radii: [10.0, 0.0, 0.0, 0.0], kinds: Default::default(), radii_relative: false, xf: Affine::IDENTITY }; + let mut l = LiveShape::Rectangle { + w: 100.0, + h: 50.0, + radii: [10.0, 0.0, 0.0, 0.0], + kinds: Default::default(), + radii_relative: false, + xf: Affine::IDENTITY, + }; let before = LiveCorners::new(&l.to_path(), Some(&l)).unwrap().drawn_radius(0); convert_radii_relative(&mut l, true); assert!(l.radii_relative()); diff --git a/crates/doc/src/node.rs b/crates/doc/src/node.rs index 2a649ec85..ad2d76b19 100644 --- a/crates/doc/src/node.rs +++ b/crates/doc/src/node.rs @@ -146,7 +146,9 @@ fn is_false(b: &bool) -> bool { impl LiveShape { pub fn radii_relative(&self) -> bool { match self { - LiveShape::Rectangle { radii_relative, .. } | LiveShape::Polygon { radii_relative, .. } | LiveShape::Path { radii_relative, .. } => *radii_relative, + LiveShape::Rectangle { radii_relative, .. } | LiveShape::Polygon { radii_relative, .. } | LiveShape::Path { radii_relative, .. } => { + *radii_relative + } LiveShape::Ellipse { .. } | LiveShape::Line { .. } => false, } } @@ -2205,14 +2207,28 @@ mod tests { #[test] fn rect_live_shape_roundtrip() { - let l = LiveShape::Rectangle { w: 10.0, h: 20.0, radii: [0.0; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate((5.0, 5.0)) }; + let l = LiveShape::Rectangle { + w: 10.0, + h: 20.0, + radii: [0.0; 4], + kinds: Default::default(), + radii_relative: false, + xf: Affine::translate((5.0, 5.0)), + }; assert_eq!(l.to_path().bounds(), Some(Rect::new(5.0, 5.0, 15.0, 25.0))); assert_eq!(l.label(), "Rectangle"); } #[test] fn corner_kinds_save_only_when_some_corner_isnt_round() { - let mut l = LiveShape::Rectangle { w: 100.0, h: 50.0, radii: [0.0, 10.0, 0.0, 0.0], kinds: Default::default(), radii_relative: false, xf: Affine::IDENTITY }; + let mut l = LiveShape::Rectangle { + w: 100.0, + h: 50.0, + radii: [0.0, 10.0, 0.0, 0.0], + kinds: Default::default(), + radii_relative: false, + xf: Affine::IDENTITY, + }; // Older files read as round. let json = serde_json::to_value(&l).unwrap(); assert!(json.get("kinds").is_none(), "{json}"); diff --git a/crates/effects/src/group.rs b/crates/effects/src/group.rs index 72dfb3274..4b08295d8 100644 --- a/crates/effects/src/group.rs +++ b/crates/effects/src/group.rs @@ -176,11 +176,7 @@ fn hard_mix_face(path: PathData, covering: &[usize], shapes: &[po::Shape], sourc painted = true; } } - let fill = if painted { - Paint::solid(Color::cmyk(cmyk[0], cmyk[1], cmyk[2], cmyk[3])) - } else { - Paint::None - }; + let fill = if painted { Paint::solid(Color::cmyk(cmyk[0], cmyk[1], cmyk[2], cmyk[3])) } else { Paint::None }; let top = covering.last().and_then(|&si| sources.get(shapes[si].key as usize)); let mut n = Node::path(top.map_or(NodeId(0), |s| s.id), path, Appearance::basic(fill, Paint::None, 0.0)); if let Some(src) = top { diff --git a/crates/effects/src/pixel/artistic.rs b/crates/effects/src/pixel/artistic.rs index adcbaccea..edd5b9342 100644 --- a/crates/effects/src/pixel/artistic.rs +++ b/crates/effects/src/pixel/artistic.rs @@ -463,11 +463,7 @@ pub(super) fn dry_brush(px: &mut [[u8; 4]], w: usize, h: usize, space: &PixelSpa *a /= SAMPLES as f32; } let a = acc[3].clamp(0.0, 1.0); - let c = if a > 1e-6 { - [0, 1, 2].map(|i| (acc[i] / a).clamp(0.0, 1.0)) - } else { - straight(*p) - }; + let c = if a > 1e-6 { [0, 1, 2].map(|i| (acc[i] / a).clamp(0.0, 1.0)) } else { straight(*p) }; let edge = edges[k].min(1.0); let dark = (edge_gain * edge).clamp(0.0, 0.75); let grain = bristle * (2.0 * value_noise(SALT_DRY, q.x / (5.0 * space.px), q.y / (5.0 * space.px)) - 1.0) as f32; diff --git a/crates/effects/src/pixel/tests.rs b/crates/effects/src/pixel/tests.rs index c053bcd47..28f07e380 100644 --- a/crates/effects/src/pixel/tests.rs +++ b/crates/effects/src/pixel/tests.rs @@ -1446,10 +1446,7 @@ fn texturizer_draws_a_loaded_texture_as_relief() { assert_ne!(relief, run(json!({"texture": builtin, "relief": 20})), "{builtin}"); } assert_eq!(run(json!({"texture": "loaded", "textureImage": rings, "relief": 0})), flat(w, mid)); - assert_eq!( - fx("texture.texturizer", json!({"texture": "LOADED", "textureImage": "not a texture"})), - fx("texture.texturizer", json!({})) - ); + assert_eq!(fx("texture.texturizer", json!({"texture": "LOADED", "textureImage": "not a texture"})), fx("texture.texturizer", json!({}))); } /// The Texture filters' patterns lie in document space around the object's centre: the same at diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index acd9a9b28..518c7c59d 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -899,7 +899,14 @@ pub(crate) fn detect_shape(path: &PathData) -> Option { } let rot = Affine::rotate(e0.y.atan2(e0.x)); let flip = if e0.cross(e1) < 0.0 { Affine::scale_non_uniform(1.0, -1.0) } else { Affine::IDENTITY }; - return Some(LiveShape::Rectangle { w, h, radii: [0.0; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate(a[0].p.to_vec2()) * rot * flip }); + return Some(LiveShape::Rectangle { + w, + h, + radii: [0.0; 4], + kinds: Default::default(), + radii_relative: false, + xf: Affine::translate(a[0].p.to_vec2()) * rot * flip, + }); } let b = path.bounds()?; let c = b.center(); diff --git a/crates/svg/tests/css_rules.rs b/crates/svg/tests/css_rules.rs index 70962b559..48e0747ca 100644 --- a/crates/svg/tests/css_rules.rs +++ b/crates/svg/tests/css_rules.rs @@ -30,7 +30,14 @@ fn rect(d: &mut Document, (x, y, w, h): (f64, f64, f64, f64), radius: f64) -> No Appearance::basic(fill, Paint::solid(Color::rgb8(0, 0, 255)), 2.0), ); if let NodeKind::Path { live, .. } = &mut n.kind { - *live = Some(Box::new(LiveShape::Rectangle { w, h, radii: [radius; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate((x, y)) })); + *live = Some(Box::new(LiveShape::Rectangle { + w, + h, + radii: [radius; 4], + kinds: Default::default(), + radii_relative: false, + xf: Affine::translate((x, y)), + })); } n } diff --git a/crates/tools/src/corners.rs b/crates/tools/src/corners.rs index 0183f69dd..c9281fade 100644 --- a/crates/tools/src/corners.rs +++ b/crates/tools/src/corners.rs @@ -485,7 +485,14 @@ mod tests { #[test] fn widgets_of_several_selected_paths_drag_together() { let (mut d, a) = live_rect(0.0, Affine::translate((100.0, 100.0))); - let live = LiveShape::Rectangle { w: 100.0, h: 100.0, radii: [0.0; 4], kinds: Default::default(), radii_relative: false, xf: Affine::translate((300.0, 100.0)) }; + let live = LiveShape::Rectangle { + w: 100.0, + h: 100.0, + radii: [0.0; 4], + kinds: Default::default(), + radii_relative: false, + xf: Affine::translate((300.0, 100.0)), + }; let b = d.alloc_id(); let mut n = Node::path(b, live.to_path(), Appearance::default_art()); if let NodeKind::Path { live: slot, .. } = &mut n.kind { @@ -792,7 +799,8 @@ mod tests { /// cuts outline in red. #[test] fn a_live_polygon_rounds_with_either_tool() { - let live = LiveShape::Polygon { radius: 80.0, sides: 6, xf: Affine::translate((200.0, 200.0)), radii: vec![], kinds: vec![], radii_relative: false }; + let live = + LiveShape::Polygon { radius: 80.0, sides: 6, xf: Affine::translate((200.0, 200.0)), radii: vec![], kinds: vec![], radii_relative: false }; let (d, id) = doc_with(live.to_path(), Some(live)); let s = selected(id); let p = paint(); diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index c50777892..e535e4c1f 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -231,11 +231,7 @@ fn glass_fields(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, id: // The stored texture is shown as its size, not as a text field. let stored = d.fields.remove(TEXTURE_IMAGE); let size = stored.as_ref().and_then(Value::as_str).and_then(texture).map(|t| t.size()); - let menu = if id == TEXTURIZER { - texturizer_textures(size.is_some()) - } else { - glass_textures(size.is_some()) - }; + let menu = if id == TEXTURIZER { texturizer_textures(size.is_some()) } else { glass_textures(size.is_some()) }; let doc = vectorcraft_effects::effect_info(id).map(|e| e.params).unwrap_or_default(); let unit = app.session.general_unit(); let pick = |k: &str| if k == "texture" { Some(menu) } else { choices(id, k) }; diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 321cef6a0..450ef2bc0 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -725,7 +725,6 @@ mod tests { fn arrow(&self) -> egui::Pos2 { egui::pos2(SCREEN.x - 10.0, HEADER / 2.0) } - } fn icon_panel_count() -> usize { @@ -842,11 +841,7 @@ mod tests { let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); assert!(app.ui.docking.is_none()); - app.run( - "window.panel.move", - json!({ "panel": "libraries", "anchor": "properties", "before": "properties" }), - ) - .unwrap(); + app.run("window.panel.move", json!({ "panel": "libraries", "anchor": "properties", "before": "properties" })).unwrap(); let order: Vec<_> = floating::docked_tabs(&app.ui).map(|t| t.info().0).collect(); assert_eq!(order, ["libraries", "properties", "layers"]); let saved: crate::state::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap(); @@ -855,11 +850,8 @@ mod tests { #[test] fn partial_dock_tab_order_appends_tabs_missing_from_preferences() { - let ui = crate::state::UiState { - dock_tab: DockTab::Layers, - dock_tab_order: vec![DockTab::Layers, DockTab::Properties], - ..Default::default() - }; + let ui = + crate::state::UiState { dock_tab: DockTab::Layers, dock_tab_order: vec![DockTab::Layers, DockTab::Properties], ..Default::default() }; let order = floating::resolved_dock_tab_order(&ui); assert_eq!(order.len(), DockTab::ALL.len()); assert_eq!(order[..2], [DockTab::Layers, DockTab::Properties]); diff --git a/crates/ui-egui/src/floating.rs b/crates/ui-egui/src/floating.rs index 72098d1a5..2a4b87e6d 100644 --- a/crates/ui-egui/src/floating.rs +++ b/crates/ui-egui/src/floating.rs @@ -1295,9 +1295,7 @@ mod tests { h.drag(h.icons()[2].center(), pos2(500.0, 250.0)); h.drag(h.icons()[0].center(), h.shared_tab("swatches").center()); assert_eq!(h.groups(), [["swatches", "color"]]); - h.app - .run("window.panel.move", json!({ "panel": "color", "anchor": "swatches", "before": "swatches" })) - .unwrap(); + h.app.run("window.panel.move", json!({ "panel": "color", "anchor": "swatches", "before": "swatches" })).unwrap(); assert_eq!(h.groups(), [["color", "swatches"]]); } diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index e051b904f..a61089997 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -167,12 +167,7 @@ fn legacy_strip_reorder_target( /// Legacy workspaces (no saved `docking` layout): reorder tabs in the dock strip or a floating /// group without promoting to a shared layout. -fn legacy_same_strip_reorder( - app: &mut VectorcraftApp, - panel: &str, - before: &str, - target: LegacyStripReorder, -) -> Result { +fn legacy_same_strip_reorder(app: &mut VectorcraftApp, panel: &str, before: &str, target: LegacyStripReorder) -> Result { use crate::state::DockTab; match target { LegacyStripReorder::DockStrip => { @@ -852,8 +847,7 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool { return app.ui.docking.is_some(); } craft_ui::docking::cancel_drag::(ui.ctx(), egui::Id::new("vectorcraft-panel-docking")); - ui.ctx() - .data_mut(|data| data.remove::(egui::Id::new("vectorcraft-panel-docking").with("legacy-origin"))); + ui.ctx().data_mut(|data| data.remove::(egui::Id::new("vectorcraft-panel-docking").with("legacy-origin"))); } for action in output.actions { if tab_drop @@ -947,13 +941,7 @@ pub(crate) fn legacy_tab(app: &mut VectorcraftApp, ui: &mut egui::Ui, panel: &st legacy_tab_in_strip(app, ui, panel, response, None); } -pub(crate) fn legacy_tab_in_strip( - app: &mut VectorcraftApp, - ui: &mut egui::Ui, - panel: &str, - response: &egui::Response, - strip: Option, -) { +pub(crate) fn legacy_tab_in_strip(app: &mut VectorcraftApp, ui: &mut egui::Ui, panel: &str, response: &egui::Response, strip: Option) { let Some(panel) = normalize(panel) else { return }; response.context_menu(|ui| { if ui.button(tl!("Float Panel")).clicked() { @@ -977,19 +965,11 @@ pub(crate) fn legacy_tab_in_strip( } }); }); - let inside_strip = strip.is_some_and(|strip| { - ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| strip.expand(4.0).contains(p)) - }); + let inside_strip = strip.is_some_and(|strip| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| strip.expand(4.0).contains(p))); let area = egui::Id::new("vectorcraft-panel-docking"); let begun = ui.ctx().data(|d| d.get_temp::(area.with("legacy-origin")).is_some()); - let begin = if strip.is_some() { - !begun && (response.drag_started() || (!inside_strip && response.dragged())) - } else { - response.drag_started() - }; - if begin - && let Some(id) = normalize(panel) - { + let begin = if strip.is_some() { !begun && (response.drag_started() || (!inside_strip && response.dragged())) } else { response.drag_started() }; + if begin && let Some(id) = normalize(panel) { let mut layout = app.ui.docking.clone().unwrap_or_else(|| legacy(app)); let bounds = ui.max_rect(); let source = egui::Rect::from_min_size( diff --git a/crates/ui-egui/src/tests_labels.rs b/crates/ui-egui/src/tests_labels.rs index 02f4c2587..1a2c3dc2c 100644 --- a/crates/ui-egui/src/tests_labels.rs +++ b/crates/ui-egui/src/tests_labels.rs @@ -88,17 +88,7 @@ fn effect_menu_lists_the_artistic_filters_before_blur() { let labels: Vec<&str> = artistic.iter().map(|e| e.label.as_str()).collect(); assert_eq!( labels, - [ - "Cutout…", - "Dry Brush…", - "Film Grain…", - "Neon Glow…", - "Plastic Wrap…", - "Poster Edges…", - "Smudge Stick…", - "Sponge…", - "Watercolor…" - ] + ["Cutout…", "Dry Brush…", "Film Grain…", "Neon Glow…", "Plastic Wrap…", "Poster Edges…", "Smudge Stick…", "Sponge…", "Watercolor…"] ); assert!(artistic.iter().all(|e| e.command.as_deref() == Some("effect.dialog"))); assert_eq!(artistic[1].params, json!({"effect": "artistic.dryBrush"})); diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index dfca5bb31..824ccb934 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -142,12 +142,7 @@ pub fn sync_touch_workspace(app: &mut VectorcraftApp, touch_screen: bool) { } } } else if app.ui.touch_workspace_restore.is_some() && app.ui.workspace == TOUCH { - let back = app - .ui - .touch_workspace_restore - .take() - .filter(|n| find(&app.ui, n).is_some()) - .unwrap_or_else(|| ESSENTIALS.to_string()); + let back = app.ui.touch_workspace_restore.take().filter(|n| find(&app.ui, n).is_some()).unwrap_or_else(|| ESSENTIALS.to_string()); if let Some(w) = find(&app.ui, &back) { apply(&mut app.ui, &w); }