Merge PR #5365: Brush Settings: expose section lock names, state and focus

docs/brush-parity.md: kept main's header and added the PR's change item; the PR's change-log row added.
docs/gaps.md: kept main's header and added the PR's change item; the PR's G4 progress note and change-log row added.
This commit is contained in:
aiongg
2026-10-22 02:23:09 +00:00
4 changed files with 130 additions and 3 deletions
+4
View File
@@ -323,9 +323,13 @@ fn section_list(app: &mut PhotocraftApp, ui: &mut egui::Ui, b: &mut BrushSetting
if let Some(lock) = section_lock(b, i) {
let lr = egui::Rect::from_center_size(egui::pos2(r.right() - 11.0, r.center().y), vec2(18.0, 18.0));
let lresp = ui.interact(lr, ui.id().with(("brush-sec-lock", i)), Sense::click());
lresp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), *lock, format!("{}: {}", tl!("Lock"), tl!(*name))));
// Like Photoshop: lock icons stay visible on every lockable section row.
let tint = if *lock { t.text } else { t.text_faint };
icons::paint(ui, lr, if *lock { "lock" } else { "lock-open" }, 12.0, tint);
if lresp.has_focus() {
ui.painter().rect_stroke(lr, t.radius_sm, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
}
let tip =
if *lock { tl!("Unlock: picking a preset replaces these settings") } else { tl!("Lock: keep these settings when picking another preset") };
if lresp.on_hover_text(tip).clicked() {
+116
View File
@@ -284,6 +284,122 @@ fn section_locks_are_always_visible_and_toggle_on_click() {
assert_eq!(h.state().ui.brush_section, 0);
}
#[test]
fn section_lock_buttons_report_their_section_and_state_in_every_theme() {
use egui::accesskit::{Role, Toggled};
use egui_kittest::kittest::{NodeT, Queryable};
for theme in crate::theme::ThemeKind::ALL {
let mut h = harness(0, 3);
PhotocraftApp::setup_context(&h.ctx, theme);
let mut brush = h.state().session.tools.brush.clone();
for i in 1..SECTIONS.len() {
*section_lock(&mut brush, i).unwrap() = i % 2 == 0;
}
h.state_mut().session.tools.brush = brush.clone();
let presets = h.state().session.tools.presets.clone();
let journal = h.state().session.journal.len();
h.run_steps(3);
for (i, &(name, _)) in SECTIONS.iter().enumerate().skip(1) {
let label = format!("Lock: {name}");
let node = h.get_by_role_and_label(Role::Button, &label);
assert!(!node.accesskit_node().is_disabled(), "locks remain available for disabled sections");
assert_eq!(h.query_all_by_label(&label).count(), 1, "{theme:?}: {name} has one distinct control");
assert_eq!(node.accesskit_node().toggled(), Some(if i % 2 == 0 { Toggled::True } else { Toggled::False }), "{theme:?}: {name}");
let row = h.get_by_label(name).rect();
assert!(row.contains_rect(node.rect()), "the named control is the lock's existing hit target");
}
assert_eq!(h.state().session.tools.brush, brush, "describing a lock must not edit it");
assert_eq!(h.state().session.tools.presets, presets);
assert_eq!(h.state().session.journal.len(), journal);
assert_eq!(h.state().ui.brush_section, 3);
}
}
#[test]
fn section_lock_buttons_accept_keyboard_without_selecting_or_enabling_sections() {
use egui::accesskit::{Role, Toggled};
use egui_kittest::kittest::{NodeT, Queryable};
for (i, &(name, _)) in SECTIONS.iter().enumerate().skip(1) {
let mut h = harness(0, 0);
let before = h.state().session.tools.brush.clone();
let presets = h.state().session.tools.presets.clone();
let journal = h.state().session.journal.len();
let label = format!("Lock: {name}");
let mut replay = photocraft_engine::Session::new();
replay.tools.brush = before.clone();
h.get_by_role_and_label(Role::Button, &label).focus();
h.run_steps(2);
for locked in [true, false] {
h.key_press(egui::Key::Space);
h.run_steps(3);
let mut expected = before.clone();
*section_lock(&mut expected, i).unwrap() = locked;
assert_eq!(h.state().session.tools.brush, expected, "{name}: only this section's lock changes");
assert_eq!(h.state().ui.brush_section, 0);
assert_eq!(h.state().session.tools.presets, presets);
assert_eq!(h.get_by_role_and_label(Role::Button, &label).accesskit_node().toggled(), Some(if locked { Toggled::True } else { Toggled::False }));
let (id, params) = last_journal(h.state()).unwrap();
assert_eq!(id, "tools.setBrush");
assert_eq!(params["brush"].as_object().unwrap().len(), 1);
assert_eq!(params["brush"]["locks"].as_object().unwrap().len(), 1);
assert_eq!(params["brush"]["locks"].as_object().unwrap().values().next(), Some(&json!(locked)));
replay.execute(&id, params).unwrap();
assert_eq!(replay.tools.brush, expected, "command replay preserves the lock-only edit");
}
assert_eq!(h.state().session.journal.len(), journal + 2, "one command per activation");
assert_eq!(replay.tools.brush, before);
}
}
#[test]
fn section_lock_buttons_show_focus_and_track_keyboard_navigation() {
use egui::accesskit::Role;
use egui_kittest::kittest::Queryable;
fn has_ring(shape: &egui::Shape, target: egui::Rect, color: egui::Color32) -> bool {
match shape {
egui::Shape::Rect(r) => r.rect == target && r.stroke == Stroke::new(1.0, color) && r.stroke_kind == egui::StrokeKind::Inside,
egui::Shape::Vec(v) => v.iter().any(|s| has_ring(s, target, color)),
_ => false,
}
}
for (index, theme) in crate::theme::ThemeKind::ALL.into_iter().enumerate() {
let mut h = harness(0, 0);
PhotocraftApp::setup_context(&h.ctx, theme);
h.state_mut().session.tools.brush.locks.scattering = index % 2 == 0;
h.state_mut().session.tools.brush.locks.texture = index % 2 != 0;
h.run_steps(2);
let before = h.state().session.tools.brush.clone();
let presets = h.state().session.tools.presets.clone();
let journal = h.state().session.journal.len();
let first = h.get_by_role_and_label(Role::Button, "Lock: Scattering").rect();
let second = h.get_by_role_and_label(Role::Button, "Lock: Texture").rect();
let accent = Tokens::get(&h.ctx).accent;
let ring = |h: &Harness<'_, PhotocraftApp>, rect| h.output().shapes.iter().any(|s| s.clip_rect.contains_rect(rect) && has_ring(&s.shape, rect, accent));
assert!(!ring(&h, first) && !ring(&h, second), "{theme:?}: unfocused locks have no focus ring");
h.get_by_role_and_label(Role::Button, "Lock: Scattering").focus();
h.run_steps(2);
assert!(h.get_by_role_and_label(Role::Button, "Lock: Scattering").is_focused());
assert!(ring(&h, first) && !ring(&h, second), "{theme:?}: the focused lock needs a visible indicator");
h.key_press(egui::Key::Tab);
h.run_steps(2);
assert!(!h.get_by_role_and_label(Role::Button, "Lock: Scattering").is_focused());
assert!(!ring(&h, first), "Tab removes the old lock's indicator");
h.get_by_role_and_label(Role::Button, "Lock: Texture").focus();
h.run_steps(2);
assert!(!ring(&h, first) && ring(&h, second), "the ring follows focus");
h.get_by_label("Brush Tip Shape").focus();
h.run_steps(2);
assert!(!ring(&h, first) && !ring(&h, second));
assert_eq!(h.state().session.tools.brush, before, "focus must not edit settings");
assert_eq!(h.state().session.tools.presets, presets);
assert_eq!(h.state().session.journal.len(), journal);
assert_eq!(h.state().ui.brush_section, 0);
}
}
/// A harness that shows the options bar with `tool` in the Studio (non-pro) theme.
fn options_bar_harness(tool: crate::state::Tool) -> Harness<'static, PhotocraftApp> {
let mut app = app();
+3 -2
View File
@@ -1,6 +1,6 @@
# Brush and painting parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (independent Brush Pose tilt-axis overrides; Transfer Wetness/Mix tool applicability; dragged preset tip preview; Hue Jitter amount track; Brush Settings minimum fields follow their Control; checkbox-only rows and non-destructive Smoothing switch; shaped-tip cursor follows the view; Color Replacement is a live toolbar tool; no brush-feel estimate changed; 2026-10-11: Pattern Stamp and Background Eraser draw live, TOOL-213-7) · **Target:** Adobe Photoshop 2026
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (named, stateful Brush Settings locks and visible keyboard focus; independent Brush Pose tilt-axis overrides; Transfer Wetness/Mix tool applicability; dragged preset tip preview; Hue Jitter amount track; Brush Settings minimum fields follow their Control; checkbox-only rows and non-destructive Smoothing switch; shaped-tip cursor follows the view; Color Replacement is a live toolbar tool; no brush-feel estimate changed; 2026-10-11: Pattern Stamp and Background Eraser draw live, TOOL-213-7) · **Target:** Adobe Photoshop 2026
Painting is where Photoshop users notice differences first, so it gets its own checklist: the
brush engine (`crates/paint/src/brush.rs`), the Brush Settings UI
@@ -32,7 +32,7 @@ tablets against Photoshop side by side.
| Brush Pose: separate Override Tilt X/Y flags, preserved through ABR import, presets and commands | done | #3825 (axis overrides only); imported single-axis flags retain the other live stylus component; dab/pixel tests cover 8/16/32-bit RGBA; legacy combined inputs migrate and command deltas contain only the edited axis. |
| Noise, Wet Edges, Build-up, Protect Texture | done | |
| Smoothing: amount, Pulled String, Stroke Catch-up, Catch-up on End, Adjust for Zoom | done | `brush.rs:428-438` |
| Section locks | done | #174 |
| Section locks | done | #174; `brush_panel_tests.rs` verifies section-specific accessible names and locked state for all 12 controls in 14 themes, plus visible themed focus, Tab blur, Space activation, minimal command patches and replay without selecting or enabling a section. |
| Noise, Wet Edges, Build-up, Smoothing and Protect Texture: checkbox-only rows keep the selected controls open | done | `brush_panel_tests.rs`, #4283; Smoothing enable/disable preserves its amount and options, including imported ABR tool settings |
| Brush tip from an image file | missing | TOOL-213-12 |
| Erodible, Airbrush and Bristle tips (Photoshop's special tips) | partial | not audited in detail; "3D-based brushes don't work" (#1660) |
@@ -128,4 +128,5 @@ tablets against Photoshop side by side.
| 2026-10-13 | minor | Shape Dynamics and Transfer minimum inputs follow their Control and retain their values while disabled (#3825, partial). No brush-feel estimate changed. |
| 2026-10-13 | minor | Hue Jitter uses the same themed amount slider as other jitter controls (#3825, partial). No brush-feel estimate changed. |
| 2026-10-13 | minor | Transfer Wetness/Mix controls follow the active tool and retain their settings while disabled (#3825, partial). No brush-feel estimate changed. |
| 2026-10-14 | minor | The 12 existing Brush Settings section locks expose translated names and locked state to AccessKit and show themed keyboard focus (#3477 workflow); icons, hit targets and command behavior retained. No brush-feel estimate changed. |
| 2026-10-13 | minor | Dragging a brush preset shows its cached tip thumbnail and name in the Brushes panel and preset picker (#4225, partial). No brush-feel estimate changed. |
+7 -1
View File
@@ -1,6 +1,6 @@
# Where PhotoCraft falls short of Photoshop
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (G4: named, stateful section locks and visible keyboard focus in Brush Settings; G4: independent Brush Pose tilt-axis overrides, #3825 partial; G4: Transfer Mixer-only controls, #3825 partial; G9: Preferences › File Handling › Save in Background takes effect, #6207, 24 of 169 preferences do nothing; G7: Content-Aware Move previews every selection the command takes, coarse then exact, TOOL-213-4 done, #6680; G4: Pattern Stamp and Background Eraser draw live, TOOL-213-7; G9: Animated Zoom pref read: holding the Zoom tool zooms continuously, #6210, prefs that do nothing 25 of 166; G2: a cancelled 24 MP Gaussian Blur job stops in at most 44 ms, P25 under its 200 ms budget in local runs, baseline not re-run, #6205; G1: generated large-file fixtures, a 4.38 GB PSB opens on Windows through io, FILE-216-2 done, #5107; G9: fill and adjustment layers copy and paste whole, #2344; G1: smart filter masks show on PSD open, Photoshop-oracle floor 188/258; G10: DPX read and write, #1049, Cineon read; G1: CMYK Hue/Saturation and Color Balance keep the black ink; G1: CMYK Posterize on the stored values, psd-tools oracle floor 242/309; G1: CMYK adjustment layers composite on the document's inks, Photoshop-oracle floor 165/258; G1: CMYK Brightness/Contrast on the inks, CMYK Threshold's luma; G1: modern Brightness/Contrast's brightness curve matches Photoshop, Photoshop-oracle floor 151/258; G1: Lab documents composite on their stored L, a, b, Photoshop-oracle floor 152/258; G1: Levels on whole levels at every step, Photoshop-oracle floor 146/258, psd-tools 238/309; G1: 32-bit Brightness/Contrast legacy, Channel Mixer, Photo Filter and Black & White tint unclipped as in Photoshop, Photoshop-oracle floor 149/258; G1: mask feathers blur with their exact variance, psd-tools oracle floor 240/309; G1: Threshold splits on Photoshop's luminosity, psd-tools oracle floor 241/309; G1: pass-through clipping base with an adjustment renders isolated, psd-tools oracle floor 240/309; G1: 16-bit Posterize as Photoshop bins it, psd-tools oracle floor 241/309; G1: Gradient Map driven by the W3C luminosity, Photoshop-oracle floor 147/258; G7: Field Blur's on-canvas pins, #3373; G4: Hue Jitter amount track, #3825 partial; G4: minimum field interaction, #3825 partial; G1: CMYK Invert, Posterize and Threshold on the inks, 16-bit Lab Invert; Photoshop-oracle floor 145/258; G11: `.atn` import maps 112 events: Select and Mask, Filter Gallery Spatter, Ripple, Crystallize, Pattern Fill layers, mask targeting, Step Backward, nudges in points, Bitmap mode; G5: Free Transform H/V skew fields; G9: toolbar tool shortcut editing; refreshed against main: G2, G5, G7, G9, G10, G13, G14, G17 items that landed; G10: DPX read and write, #1049; G4: checkbox-only Brush Settings rows; G11: Load/Save Actions `.atn` landed, AUTO-219-2 partial; G5: hold-a-key spring-loaded tools landed, #2672; G1: #2465 fixed; stale missing-tool rows corrected: Color Replacement, Red Eye and Add/Delete Anchor Point are live tools, so only Art History Brush is missing; no estimate changed) · **Target:** Adobe Photoshop 2026
Every known shortfall against Photoshop 2026 (27.11.0), one entry each, ranked by user impact
on the way to beta. This is the work list agents pick from: take the highest-ranked entry that
@@ -176,6 +176,11 @@ what an agent can't finish alone.
only the selected component replaces live stylus input. Legacy combined JSON and
command inputs retain their two-axis behavior, and minimal command patches preserve
the other axis. Other Brush Settings and brush-feel gaps remain open.
- **Progress (2026-10-14, #3477 workflow):** section locks expose their section-specific
translated names and locked state to AccessKit. A themed outline follows keyboard focus;
focus changes do not edit settings. Keyboard activation preserves the selected
section and other brush settings, using the existing command path. Other panel and brush-feel
gaps remain open.
- **Progress (2026-10-13, #3825 partial):** Hue Jitter shows a standard themed
percentage-amount track rather than a hue spectrum. Other Brush Settings layout
and brush-behaviour gaps remain open.
@@ -306,6 +311,7 @@ what an agent can't finish alone.
| 2026-10-15 | minor | G9: Save in Background off saves in the foreground under a modal progress dialog (#6207); preferences that do nothing 25 → 24 of 169 |
| 2026-10-13 | minor | G4: Transfer Wetness/Mix tool applicability (#3825, partial); no readiness percentage or estimate changed |
| 2026-10-14 | minor | G4: independent Brush Pose X/Y overrides across UI/import/painting/commands (#3825 partial); legacy combined input retained; no readiness percentage or estimate changed |
| 2026-10-14 | minor | G4: existing Brush Settings section locks expose accessible names/state and visible keyboard focus (#3477 workflow); no readiness percentage or estimate changed |
| 2026-10-14 | minor | G1: a PSD smart object's filter mask shows when the file opens (Photoshop stores the filtered pixels unmasked); export stores them that way too; Photoshop-oracle floor 185 → 188 of 258 |
| 2026-10-13 | minor | G14: D90 image area measured (4288 × 2848 at (10, 10)); gap remains open |
| 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 |