Numeric: clamp committed values in point units, schedule rejection highlight repaint, and update roadmap/area evidence

This commit is contained in:
Taha-Mahmoodi
2026-10-30 16:55:55 +07:00
committed by bflatastic
parent 418f1bffc3
commit 5da8c8e495
6 changed files with 41 additions and 8 deletions
+27 -2
View File
@@ -2534,9 +2534,34 @@ fn color_halftone_invalid_input_does_not_revert_to_sub_minimum_value() {
frame(&mut app, vec![egui::Event::Text("0.5 px".into())]);
frame(&mut app, vec![egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"], serde_json::json!(4.0));
// 2. Type invalid input "1jgghgh"
// 2. Refocus Max Radius before typing invalid text
crate::widgets::arm_field_focus(&ctx, ("dlg-len", "maxRadius"));
frame(&mut app, vec![]);
frame(&mut app, vec![egui::Event::Text("1jgghgh".into())]);
frame(&mut app, vec![egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
// The value remains the valid clamped minimum 4.0 pt, never 0.5 px
// The value remains the valid clamped minimum 4.0 pt, never 0.5 px, and rejection flash is set
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"], serde_json::json!(4.0));
assert!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"] == serde_json::json!(4.0));
}
#[test]
fn bounded_field_with_inches_does_not_clamp_point_bounds_to_inch_numbers() {
let mut app = app();
let _id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
// Switch document units to Inches
app.run("document.setup", json!({"units": "Inches"})).unwrap();
let ctx = egui::Context::default();
theme::install_fonts(&ctx);
theme::apply(&ctx, Default::default());
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| show(app, ui.ctx()));
out.textures_delta.clear();
};
app.run("select.all", json!({})).unwrap();
app.run("effect.dialog", json!({"effect": "pixelate.colorHalftone"})).unwrap();
frame(&mut app, vec![]);
// Type 1 in: 72 pt is within 4..127 pt range and must not clamp to ~1.76 pt
frame(&mut app, vec![egui::Event::Text("1 in".into())]);
frame(&mut app, vec![egui::Event::Key { key: egui::Key::Tab, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }]);
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["maxRadius"], serde_json::json!(72.0));
}
+1 -1
View File
@@ -87,7 +87,7 @@ impl<T> std::fmt::Debug for HashOnly<T> {
}
/// `parent.with(salt)` for a salt that needs only `Hash`.
fn id_with(parent: egui::Id, salt: impl std::hash::Hash) -> egui::Id {
pub(crate) fn id_with(parent: egui::Id, salt: impl std::hash::Hash) -> egui::Id {
parent.with(HashOnly(salt))
}
+6 -4
View File
@@ -102,10 +102,11 @@ fn unit_field(
// ↑/↓ and the wheel step in the field's unit; a drag on its label scrubs it.
let typed = typed_unit(ui.ctx(), unit);
let read = |b: &str| typed.parse(&decimal_point(b));
let bounds = unit.from_pt(*bounds.start())..=unit.from_pt(*bounds.end());
let bounds_unit = unit.from_pt(*bounds.start())..=unit.from_pt(*bounds.end());
let stops: Vec<f64> = stops.iter().map(|&s| unit.from_pt(s)).collect();
let stepped = step_value(ui, &resp, &mut buf, &bounds, (|b: &str| Some(unit.from_pt(read(b)?)), |v| unit.format(unit.to_pt(v))), None, &stops);
let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS, &bounds);
let stepped =
step_value(ui, &resp, &mut buf, &bounds_unit, (|b: &str| Some(unit.from_pt(read(b)?)), |v| unit.format(unit.to_pt(v))), None, &stops);
let scrubbed = scrub::field(ui, id, rect, &buf, value.map(|v| unit.from_pt(v)), STEP_DECIMALS, &bounds_unit);
let commit = commit && buf != shown;
if commit {
match read(&buf).or_else(|| if share_of_value { share_of(&decimal_point(&buf), value?) } else { None }) {
@@ -117,7 +118,8 @@ fn unit_field(
}
None => {
ui.data_mut(|d| d.remove::<String>(id));
ui.data_mut(|d| d.insert_temp(id.with("invalid-flash"), true));
ui.data_mut(|d| d.insert_temp(id.with("invalid-flash"), ui.input(|i| i.time + 0.25)));
ui.ctx().request_repaint();
None
}
}
+5 -1
View File
@@ -237,7 +237,11 @@ pub(super) fn recessed_text(
.allocate_ui_with_layout(vec2(width, height), layout, |ui| {
ui.set_min_size(vec2(width, height));
let rect = ui.max_rect();
let invalid = ui.data_mut(|d| d.remove_temp::<bool>(id.with("invalid-flash"))).unwrap_or(false);
let now = ui.input(|i| i.time);
let invalid = ui.data(|d| d.get_temp::<f64>(id.with("invalid-flash"))).is_some_and(|end| now <= end);
if invalid {
ui.ctx().request_repaint();
}
let stroke = if invalid {
Stroke::new(1.5, t.accent_strong)
} else if editing {
+1
View File
@@ -19,6 +19,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Raster effects: bounded length fields in effect dialogs clamp on commit and reject invalid inputs visibly (#4638); estimates unchanged. |
| 2026-10-22 | minor | Reconcile upstream documentation after review F-3; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. |
| 2026-10-22 | minor | Reconcile upstream documentation after review F-2; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. |
| 2026-10-22 | minor | Reconcile subsequent upstream progress updates without changing the runtime or repeating completed evidence; retain every dated upstream and feature revision. |
+1
View File
@@ -1129,6 +1129,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Glass's Load Texture:** Effect › Distort › Glass… has a Load Texture… button that reads a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD, PSB or PCX file and adds Loaded Texture to the Texture menu (`textureImage`: `{path}` or `{name?, dataBase64}`, stored in the effect and saved with the document as a grayscale PNG in base64). The texture lies one image pixel per point at Scaling 100%, centered on the object and repeated; each pixel's luma is the surface's height, from 0 for black to 1 pt for white, and a transparent pixel counts as middle gray. Scaling and Invert act on it as on the built-in textures. A Glass saved without `textureImage` draws and saves as before.
- **Bounded effect length inputs (#4638):** Effect dialog fields with defined length ranges (such as Color Halftone Max Radius 4..127 pt) clamp immediately on commit and reject invalid inputs visibly with an accent highlight without reverting to sub-minimum values.
- **Raster effects after document and view changes:** a cached drop shadow, outer glow or filtered content is made again when something its content reads from outside the object changes: a pattern's art or tiling, a symbol shown inside another symbol, a brush definition, an image's pixels, the raster resolution (De-Interlace), Page Knockout Group, the substitution highlights (Highlight Substituted Fonts and Highlight Alternate Glyphs in Preferences › Type, Highlight Substituted Glyphs in Document Setup), a layer's Dim Images, the color settings, the fonts, Pixel Preview, Anti-aliased Artwork, Overprint Preview and isolation mode. A thumbnail under Color Halftone or Mezzotint is made again when the document's color mode changes. Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536).
- **Drop Shadow's Darkness:** Effect › Stylize › Drop Shadow… has Color and Darkness as radio buttons, each with its field, and the Appearance panel's inline editor has the two radio buttons in its Shade row (`shade`: `color` or `darkness`; `darkness`: 0 to 100%, 100 by default). Color is the default, and a shadow saved without these keys draws as before. With Darkness, the stored source-color model determines the adjustment before document/profile conversion and blur: subtract Darkness/100 from each RGB channel, clamped at zero, or add it to CMYK black K and Gray black, clamped at one, preserving CMYK C/M/Y. At the near-100% endpoint the shadow source becomes gray black before conversion. Shadow opacity stays separate. Original gradient stops follow the same application rule before interpolation; exact resource-level gradient parity remains open. Application SVG export keeps a live filter for simple uniform sRGB sources and uses rendered shadow images for other working RGB profiles, source models and gradients. Low-level SVG filter output warns when this preparation is needed; proportional intermediate SVG filters are not misread as additive Darkness. Exact spot/custom-color and composite-source parity remain open, and resulting CMYK ink values follow the application color pipeline. CSS export leaves the shadow out and marks the object unsupported. When a `.ai`, EPS or PDF opens from its editing data, a Drop Shadow with Darkness in an object's art style is read as one with Color is, with the shadow's Darkness percentage (100% when the style gives none). Behavior reference: Illustrator CC Help (2014, [PDF](https://help.adobe.com/archive/en/illustrator/cc/2014/illustrator_reference.pdf)), page 428 (PDF page 434).