diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index d305be1ff..fa4c8dcee 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -722,7 +722,7 @@ fn drop_shadow_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc:: d.fields.insert("shade".into(), json!("darkness")); } ui.add_enabled_ui(darkness, |ui| { - if let Some(v) = widgets::range_field(ui, "fx-darkness", d.f64("darkness", 100.0), 0.0..=100.0, "%", 0, 70.0) { + if let Some(v) = widgets::spin_range(ui, "fx-darkness", Some(d.f64("darkness", 100.0)), 0.0..=100.0, "%", 0, 86.0, 1.0) { d.fields.insert("darkness".into(), json!(v)); } }); @@ -1041,4 +1041,82 @@ mod tests { let fx = &st.doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0]; assert_eq!(fx.params.get("color"), Some(&json!("#00ff00")), "Outer Glow takes the picked colour"); } + + /// Effect > Stylize > Drop Shadow and Outer Glow: Opacity, the offsets, Blur and Darkness have + /// spinner arrows (#5367, #5368). A click on the top half steps up, on the bottom half down; + /// Opacity stops at 100 % and Blur at 0, and OK applies the stepped values. + #[test] + fn drop_shadow_and_outer_glow_fields_have_spinner_arrows() { + for (effect, count) in [("stylize.dropShadow", 5), ("stylize.outerGlow", 2)] { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap(); + app.run("select.all", json!({})).unwrap(); + app.run("effect.dialog", json!({"effect": effect})).unwrap(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + crate::theme::install_fonts(&ctx); + crate::theme::apply(&ctx, Default::default()); + let time = std::cell::Cell::new(0.0); + // One frame → the spinners' bounds, top to bottom. + let frame = |app: &mut VectorcraftApp, events: Vec| { + time.set(time.get() + 0.05); + let mut out = + ctx.run_ui(egui::RawInput { time: Some(time.get()), events, ..Default::default() }, |ui| super::super::show(app, ui.ctx())); + out.textures_delta.clear(); + let mut spinners: Vec = out + .platform_output + .accesskit_update + .map(|u| u.nodes) + .unwrap_or_default() + .iter() + .filter(|(_, n)| n.label() == Some("Step up or down")) + .filter_map(|(_, n)| n.bounds()) + .map(|b| egui::Rect::from_min_max(egui::pos2(b.x0 as f32, b.y0 as f32), egui::pos2(b.x1 as f32, b.y1 as f32))) + .collect(); + spinners.sort_by(|a, b| a.top().total_cmp(&b.top())); + spinners + }; + for _ in 0..6 { + frame(&mut app, vec![]); + } + let spinners = frame(&mut app, vec![]); + assert_eq!(spinners.len(), count, "{effect}: a spinner beside each number field: {spinners:?}"); + let click = |app: &mut VectorcraftApp, r: egui::Rect, up: bool| { + let pos = r.center() + egui::vec2(0.0, if up { -r.height() / 4.0 } else { r.height() / 4.0 }); + let button = + |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + frame(app, vec![egui::Event::PointerMoved(pos), button(true)]); + frame(app, vec![button(false)]); + frame(app, vec![]); + }; + let (opacity, blur) = (spinners[0], spinners[count - 1 - usize::from(count == 5)]); + click(&mut app, opacity, true); + let field = |app: &VectorcraftApp, k: &str| app.ui.dialog.as_ref().unwrap().fields.get(k).and_then(Value::as_f64); + assert_eq!(field(&app, "opacity"), Some(76.0), "{effect}: the up arrow steps Opacity up"); + click(&mut app, blur, false); + assert_eq!(field(&app, "blur"), Some(4.0), "{effect}: the down arrow steps Blur down"); + app.ui.dialog.as_mut().unwrap().fields.insert("opacity".into(), json!(100.0)); + app.ui.dialog.as_mut().unwrap().fields.insert("blur".into(), json!(0.0)); + frame(&mut app, vec![]); + click(&mut app, opacity, true); + click(&mut app, blur, false); + assert_eq!((field(&app, "opacity"), field(&app, "blur")), (Some(100.0), Some(0.0)), "{effect}: the arrows stop at the ends"); + click(&mut app, opacity, false); + if count == 5 { + click(&mut app, spinners[1], true); + assert_eq!(field(&app, "x"), Some(8.0), "the X offset steps up"); + let darkness = spinners[4]; + app.ui.dialog.as_mut().unwrap().fields.insert("shade".into(), json!("darkness")); + frame(&mut app, vec![]); + click(&mut app, darkness, false); + assert_eq!(field(&app, "darkness"), Some(99.0), "Darkness steps down"); + } + super::super::confirm(&mut app).unwrap(); + let st = app.session.doc().unwrap(); + let fx = &st.doc.node(vectorcraft_doc::NodeId(id)).unwrap().appearance.effects[0]; + let param = |k: &str| fx.params.get(k).and_then(Value::as_f64); + assert_eq!((param("opacity"), param("blur")), (Some(99.0), Some(0.0)), "{effect}: OK applies them"); + } + } } diff --git a/crates/ui-egui/src/dialogs/form.rs b/crates/ui-egui/src/dialogs/form.rs index 7238e191e..5e5b4ea9c 100644 --- a/crates/ui-egui/src/dialogs/form.rs +++ b/crates/ui-egui/src/dialogs/form.rs @@ -410,10 +410,16 @@ pub(crate) fn params(d: &Dialog) -> Value { /// The (label, value) choices of a parameter that picks one of some values. pub(super) type Choices = &'static [(&'static str, &'static str)]; +/// Effects whose dialogs give their number fields spinner arrows (#5367, #5368): Drop Shadow's +/// and the glows' Opacity, offsets and Blur, which are stepped more than typed. +pub(super) fn has_spinners(effect: &str) -> bool { + matches!(effect, "stylize.dropShadow" | "stylize.outerGlow" | "stylize.innerGlow") +} + /// Generic editor for command/effect parameters: numbers, booleans, strings and colours; the /// parameters `is_length` names are distances shown and typed in `unit`, those `choices` gives /// choices for are dropdowns. Fields come in `rank` order (by name among equals). Returns true when -/// a value changed. +/// a value changed. The fields of [`has_spinners`]' effects carry spinner arrows. pub(super) fn param_fields( ui: &mut egui::Ui, d: &mut Dialog, @@ -425,6 +431,7 @@ pub(super) fn param_fields( let t = Tokens::get(ui.ctx()); let mut changed = false; let mut pick: Option<(String, Color)> = None; + let spinners = has_spinners(&d.str("__effect")); egui::Grid::new("fxgrid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { let mut keys: Vec<(String, Value)> = d.fields.iter().filter(|(k, _)| !k.starts_with("__") && k.as_str() != "preview").map(|(k, v)| (k.clone(), v.clone())).collect(); @@ -446,7 +453,16 @@ pub(super) fn param_fields( if is_length(&k) { // An effect's documented range keeps the field to the distance it applies. let range = vectorcraft_effects::length_range(&d.str("__effect"), &k); - changed |= length_within(ui, d, &k, unit, 140.0, range); + if spinners { + let bounds = range.unwrap_or(if k == "blur" { 0.0..=f64::INFINITY } else { crate::widgets::ANY }); + let value = d.fields.contains_key(&k).then(|| d.f64(&k, 0.0)); + if let Some(x) = crate::widgets::spin_length_within(ui, ("dlg-len", k.as_str()), value, unit, 140.0, bounds) { + d.fields.insert(k.clone(), json!(x)); + changed = true; + } + } else { + changed |= length_within(ui, d, &k, unit, 140.0, range); + } return; } // A size in pixels of the raster effects resolution (Color Halftone's Max Radius) @@ -472,6 +488,13 @@ pub(super) fn param_fields( return; } match v { + Value::Number(n) if spinners => { + let range = if k == "opacity" { 0.0..=100.0 } else { crate::widgets::ANY }; + if let Some(x) = crate::widgets::spin_range(ui, ("fx-num", k.as_str()), n.as_f64(), range, "", 3, 140.0, 1.0) { + d.fields.insert(k.clone(), typed_number(x)); + changed = true; + } + } Value::Number(n) => { if let Some(x) = crate::widgets::plain_field(ui, ("fx-num", &k), n.as_f64().unwrap_or(0.0), "", 3, 140.0) { d.fields.insert(k.clone(), typed_number(x)); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 5c8a701dd..efde0d0b1 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -54,7 +54,8 @@ pub(crate) use numeric::{ANY, round_to, set_bare_numbers_are_points, typed_unit} pub use numeric::{ SpinPick, mixed_field, mixed_field_named, num_field, num_field_live, num_field_named, num_field_within, num_field_within_empty, opt_field, opt_field_named, plain_field_live, range_field, range_field_live, range_field_named, range_field_stepped, size_field, size_field_named, slider, - spin_field, spin_field_auto, spin_field_auto_named, spin_field_named, spin_field_stops_named, spin_kerning, spin_plain, spin_plain_named, + spin_field, spin_field_auto, spin_field_auto_named, spin_field_named, spin_field_stops_named, spin_kerning, spin_length_within, spin_plain, + spin_plain_named, spin_range, }; #[cfg(test)] use numeric::{decimal_point, field_number, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth}; diff --git a/crates/ui-egui/src/widgets/numeric.rs b/crates/ui-egui/src/widgets/numeric.rs index ed1f257ee..d38838782 100644 --- a/crates/ui-egui/src/widgets/numeric.rs +++ b/crates/ui-egui/src/widgets/numeric.rs @@ -623,6 +623,53 @@ pub fn spin_field_stops_named( .and_then(SpinPick::value) } +/// A distance field with spinner arrows kept within `bounds` (points), as the Drop Shadow and +/// Outer Glow dialogs show their offsets and blur: the spinner steps one `unit` at a time +/// (Shift ×10, Ctrl/Cmd ÷10), and it, ↑/↓, the wheel, a scrub and a typed value stop at both +/// ends. Returns the committed or stepped value in points. +pub fn spin_length_within( + ui: &mut Ui, + id: impl std::hash::Hash + Copy, + value: Option, + unit: Unit, + width: f32, + bounds: RangeInclusive, +) -> Option { + let label = take_previous_label(ui); + let step = Some(unit.to_pt(1.0)).filter(|s| s.is_finite() && *s > 0.0).unwrap_or(1.0); + let field = bounds.clone(); + spin_generic(ui, label, value, None, None, None, width, step, *bounds.start(), &[], &[], &|v| unit.format(v), &mut |ui, fw, label| { + unit_field(ui, id, label, None, value, unit, fw, false, field.clone(), &[], false, "") + }) + .and_then(SpinPick::value) + .map(|v| within(v, &bounds)) +} + +/// [`range_field`] with spinner arrows (Opacity and Darkness in the Drop Shadow and Outer Glow +/// dialogs): the spinner steps by `step` (Shift ×10, Ctrl/Cmd ÷10) and, like ↑/↓, the wheel, a +/// scrub and a typed value, stops at the ends of `range`. +#[allow(clippy::too_many_arguments)] +pub fn spin_range( + ui: &mut Ui, + id: impl std::hash::Hash + Copy, + value: Option, + range: RangeInclusive, + suffix: &str, + decimals: usize, + width: f32, + step: f64, +) -> Option { + let label = take_previous_label(ui); + let places = decimals.min(6) as i32; + let value = value.map(|v| within(v, &range)); + let fmt = |v: f64| format!("{v}{suffix}"); + spin_generic(ui, label, value, None, None, None, width, step, *range.start(), &[], &[], &fmt, &mut |ui, fw, label| { + bounded_field(ui, id, label, None, value, suffix, decimals, fw, &range, None, false, false) + }) + .and_then(SpinPick::value) + .map(|v| within(round_to(v, places), &range)) +} + /// What a [`spin_field_auto`] returns: a value (points), or its Auto entry. #[derive(Clone, Copy, Debug, PartialEq)] pub enum SpinPick { diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e7bde0bb4..c7f408423 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -1087,6 +1087,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur View › Proof Setup › Color blindness – Tritanopia-type turns Proof Colors on and proofs the canvas with the tritanopia simulation of Brettel, Viénot and Mollon (1997). The Separations Preview panel's Proof Setup list offers the same target (`view.proofSetup {target: "tritanopia"}`). Grays keep their value. The Separations Preview panel's Gamut Check button reports one color outside the CMYK gamut in the singular and more in the plural, in English and in each language's plural forms (`gamut_check_status_takes_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`). - **Document Color Mode outside the layers:** File › Document Color Mode (in one undo step) and `document.open {colorMode}` also convert the colors of graph designs, graphic styles, opacity-mask art, the tile that pattern editing's Cancel puts back and the art of brushes (with a Hue Shift brush's key color); a tint of a global swatch there becomes the converted swatch's color at its tint, as in the art. + - **Spinner arrows in the shadow and glow dialogs (#5367, #5368):** Drop Shadow's Opacity, X Offset, Y Offset, Blur and Darkness and the Outer and Inner Glow's Opacity and Blur have up and down arrows beside the field, as the other number fields with arrows do: a click steps by one (a unit for distances; Shift ×10, Ctrl/Cmd ÷10), holding repeats, Opacity and Darkness stop at 0 and 100 % and Blur at 0 (`dialogs::effect` test `drop_shadow_and_outer_glow_fields_have_spinner_arrows`). - **Colour swatches in effect dialogs (#5365):** a colour parameter of an effect dialog (Outer Glow's Color, Inner Glow's, Drop Shadow's) shows a swatch beside its hex field that follows the value typed in it; a double-click on the swatch opens the Color Picker over the dialog on that colour, whose OK fills the field and whose Cancel comes back unchanged, and the dialog previews the colour when it returns (`dialogs::effect` test `outer_glow_colour_swatch_opens_the_color_picker`). - **Document Color Mode for effects and graphs:** File › Document Color Mode and `document.open {colorMode}` convert the colors of effects (Drop Shadow, Inner Glow, Outer Glow and the other effects with a color, on objects, their fills and strokes and in graphic styles) and the series paints and label styles that a graph keeps, so a graph drawn again from new data keeps the converted colors; File › Document Color Mode also converts the fill and stroke that new art takes. Undo restores the original new-art fill and stroke and Redo applies the conversion again. - **Recolor Artwork layout (#3416):** the Edit chip list uses the modal scroll helper and its actual remaining height; the tab's minimum-height space scrolls with the chips. `tests_recolor` covers 5/20/200/400 colors, window resizing, 100/125/150/200% UI scaling, scrolling to and editing the final chip, Preview, Cancel, Confirm and Undo, alongside the existing #1733 drag regressions. Scores and estimates unchanged. @@ -1702,6 +1703,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | Drop Shadow, Outer Glow and Inner Glow: their number fields have spinner arrows (#5367, #5368); evidence added; scores unchanged. | | 2026-11-06 | minor | Stroke Weight Up and Down (Cmd/Ctrl+Alt+. and ,) step each selected stroke's weight (#5797); evidence added; scores unchanged. | | 2026-11-06 | minor | Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged | | 2026-11-06 | minor | Properties in Edit Artboards: a Transform section (reference point, X, Y, W, H, preset, orientation, Move and Scale Artwork) above the Artboards section (name, Artboard Options, Fill), X and Y of the shared reference point (#5219); evidence updated; scores unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 629834c3e..5e65fc58f 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -38,6 +38,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | Drop Shadow, Outer Glow and Inner Glow: spinner arrows beside Opacity, the offsets, Blur and Darkness (#5367, #5368); scores and estimates unchanged | | 2026-11-06 | minor | Rename (`ui.rename`, no default key) opens the Layers panel's name field on the selected layer, group or object's row, as double-clicking its name does; a key can be assigned in Keyboard Shortcuts (#5324, a VectorCraft addition); scores and estimates unchanged | | 2026-11-06 | minor | Properties panel in Edit Artboards (Shift+O): a Transform section with the shared reference point, X, Y, W and H, the preset and orientation, and Move and Scale Artwork with Artboard, above the Artboards section with the name, Artboard Options and Fill (#5219); scores and estimates unchanged | | 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: and the Properties panel's Stroke, links to their panels, scrub the field after them as other labels do; a click still opens the panel (#4088); scores and estimates unchanged | @@ -143,6 +144,7 @@ tools, never checked side by side) isn't captured row by row. | Add/Delete/Anchor Point tools; Anchor Point tool drags segments to reshape | done | #2267: a click on a handle end retracts that handle and leaves the anchor a corner with the other one; Shift held at the press on an anchor moves it, handles and all (Shift kept down: at 45° steps) instead of pulling out handles | | Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Fidelity goes down to 0.01 pt in the dialog, the tool and `path.freehand` (the Smooth tool keeps 0.05); its slider runs across the Tool Options row on a logarithmic rail from 0.01 to 20 pt, the field shows hundredths, and the slider's and the field's arrow keys step hundredths up to 0.1 pt, tenths up to 1 pt and whole points above (Shift ×10) (#3328); Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor while Shift or Alt is held before the press, and a path-continuation cursor over an end of a selected open path, where the line continues the path; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) on, a stroke selects the path it draws or continues and replaces the earlier selection, and off a new stroke leaves nothing selected, the earlier selection included, while a continued or redrawn path is deselected and the rest of the selection stays; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path | | Pencil: Fidelity, Keep selected, Alt to Smooth, edit selected paths, close by proximity | done | Alt key toggles to Smooth Tool (#2001); Shift held at the press draws a straight line at 45° steps from the Constrain Angle, and Alt held at the press (with Alt key toggles to Smooth Tool off) a straight line at any angle (#3799), with a straight-line cursor while Shift or Alt is held before the press, and a path-continuation cursor over an end of a selected open path, where the line continues the path; Edit Selected Paths continues a selected path from an end and, from a stroke that starts and ends on a selected path, redraws the section between (the shorter way round a closed path, #2850); with Keep selected (Pencil and Paintbrush, on by default) on, a stroke selects the path it draws or continues and replaces the earlier selection, and off a new stroke leaves nothing selected, the earlier selection included, while a continued or redrawn path is deselected and the rest of the selection stays; a filled Pencil path keeps a stroke of None (#1804, part of #1679); while a freehand stroke is drawn back to within Close Paths When Ends Are Within of its start, the Pencil and the Paintbrush show the closing cursor, and a release there closes the path; a Pencil stroke from an end of one selected open path to an end of another connects the two into one path | +| Drop Shadow, Outer Glow and Inner Glow: Opacity, the offsets, Blur and Darkness have spinner arrows that step by one (a unit for distances), repeat when held and stop at the fields' ends (#5367, #5368) | done | dialogs::effect test drop_shadow_and_outer_glow_fields_have_spinner_arrows; other effect dialogs' number fields still have none | | Effect dialogs' colour fields: a swatch beside the hex follows typed values, and a double-click opens the Color Picker over the dialog, whose OK fills it and Cancel comes back unchanged (#5365) | done | dialogs::effect test outer_glow_colour_swatch_opens_the_color_picker; the picker previews nothing itself, the effect dialog previews the returned colour | | Join tool scrubbing, Average, Join | done | Join merges the direct-selected anchors lying on one another along a path, as after Average, into one (#1533); the Join tool trims two crossing open paths whose overhanging ends it scrubs over and joins them at the crossing, instead of drawing a segment between their ends (#5844) | | Average and Join in one step, with a corner or smooth choice for the joined anchor | done | Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J, "Create corner or smooth join" in [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): two open path ends meet at their midpoint and join, a corner or smooth point as the dialog's Points choice asks (Corner first, then the last choice), in one undo step |