In the Photoshop themes (the default ProMedium included) the Brush and Eraser options bar drew three icon buttons that did nothing when clicked. The engine already supports everything behind them: - Airbrush toggles the brush's Build-up (lit while on). - The gear beside Smoothing opens Photoshop's smoothing options: Pulled String Mode, Stroke Catch-up, Catch-up on Stroke End and Adjust for Zoom. They edit the brush like the Smoothing % does, so they stay per tool and reach the live stroke and the commit. - The symmetry button opens a menu with Symmetry Off and every path the Paths panel lists (saved paths, the Work Path, the selected layer's shape path or vector mask), running paint.symmetryFromPath / paint.symmetryDisable. It is lit while symmetry is on; with no path it says how to make one. The three buttons get accessible names (brush_picker::named), and "Symmetry Off" is added to the twelve complete catalogs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
15cfdcd9e8
commit
acddca58b8
|
|
@ -286,6 +286,56 @@ fn options_bar_edits_are_one_set_brush_per_gesture() {
|
|||
assert_eq!(s.tools.brush, h.state().session.tools.brush);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airbrush_smoothing_options_and_symmetry_buttons_work() {
|
||||
// #1177: in the default (Photoshop) theme these options-bar buttons did nothing.
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let mut h = app_harness();
|
||||
let brush = |h: &Harness<'static, PhotocraftApp>| h.state().session.tools.brush.clone();
|
||||
|
||||
// The airbrush toggles Build-up, one setBrush each click.
|
||||
let was = brush(&h).build_up;
|
||||
h.get_by_label("Enable airbrush-style build-up effects").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(brush(&h).build_up, !was);
|
||||
let (id, p) = last_journal(h.state()).unwrap();
|
||||
assert_eq!((id.as_str(), &p["brush"]), ("tools.setBrush", &json!({"buildUp": !was})));
|
||||
|
||||
// The gear opens Photoshop's four smoothing options, each editing the brush.
|
||||
assert!(h.query_by_label("Pulled String Mode").is_none());
|
||||
h.get_by_label("Set additional smoothing options").click();
|
||||
h.run_steps(3);
|
||||
let before = brush(&h).smoothing;
|
||||
for label in ["Pulled String Mode", "Stroke Catch-up", "Catch-up on Stroke End", "Adjust for Zoom"] {
|
||||
h.get_by_label(label).click();
|
||||
h.run_steps(3);
|
||||
}
|
||||
let after = brush(&h).smoothing;
|
||||
assert_eq!(
|
||||
(after.pulled_string, after.catch_up, after.catch_up_on_end, after.adjust_for_zoom),
|
||||
(!before.pulled_string, !before.catch_up, !before.catch_up_on_end, !before.adjust_for_zoom)
|
||||
);
|
||||
assert_eq!(after.amount, before.amount);
|
||||
|
||||
// Symmetry: the menu lists the document's paths and turns symmetry on and off.
|
||||
h.state_mut().run("file.new", json!({"width": 80, "height": 60})).unwrap();
|
||||
h.state_mut().run("path.set", json!({"name": "work", "path": {"subpaths": [{"closed": false, "knots": [[40, 0], [40, 60]]}]}})).unwrap();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Set painting symmetry options").click();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Work Path").click();
|
||||
h.run_steps(3);
|
||||
let source = |h: &Harness<'static, PhotocraftApp>| h.state().session.active().unwrap().symmetry_path.as_ref().map(|a| a.source.clone());
|
||||
assert_eq!(source(&h).as_deref(), Some("work"));
|
||||
assert_eq!(last_journal(h.state()).unwrap(), ("paint.symmetryFromPath".to_string(), json!({"name": "work"})));
|
||||
h.get_by_label("Set painting symmetry options").click();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Symmetry Off").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(source(&h), None);
|
||||
assert!(h.state().ui.status.is_empty(), "{}", h.state().ui.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixer_brush_options_update_the_persisted_mixer_settings() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ pub fn settings_toggle(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
}
|
||||
|
||||
/// An icon button's accessible name (icon buttons have only a tooltip).
|
||||
fn named(resp: egui::Response, label: &str) -> egui::Response {
|
||||
pub(crate) fn named(resp: egui::Response, label: &str) -> egui::Response {
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, label));
|
||||
resp
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1357,6 +1357,7 @@
|
|||
Set background color Nastavit barvu pozadí
|
||||
Set foreground color Nastavit barvu popředí
|
||||
Set painting symmetry options Nastavit možnosti symetrie malby
|
||||
Symmetry Off Symetrie vypnuta
|
||||
Set shape fill type Nastavit typ výplně tvaru
|
||||
Set shape stroke type Nastavit typ tahu tvaru
|
||||
Set the text color Nastavit barvu textu
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1340,6 +1340,7 @@
|
|||
Set background color Hintergrundfarbe festlegen
|
||||
Set foreground color Vordergrundfarbe festlegen
|
||||
Set painting symmetry options Symmetrieoptionen für das Malen festlegen
|
||||
Symmetry Off Symmetrie aus
|
||||
Set shape fill type Fülltyp der Form festlegen
|
||||
Set shape stroke type Konturtyp der Form festlegen
|
||||
Set the text color Textfarbe festlegen
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1342,6 +1342,7 @@
|
|||
Set background color Establecer el color de fondo
|
||||
Set foreground color Establecer el color frontal
|
||||
Set painting symmetry options Establecer opciones de simetría de pintura
|
||||
Symmetry Off Simetría desactivada
|
||||
Set shape fill type Establecer el tipo de relleno de la forma
|
||||
Set shape stroke type Establecer el tipo de trazo de la forma
|
||||
Set the text color Establecer el color del texto
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -1329,6 +1329,7 @@
|
|||
Set background color Définir la couleur d'arrière-plan
|
||||
Set foreground color Définir la couleur de premier plan
|
||||
Set painting symmetry options Définir les options de symétrie de peinture
|
||||
Symmetry Off Symétrie désactivée
|
||||
Set shape fill type Définir le type de remplissage de la forme
|
||||
Set shape stroke type Définir le type de contour de la forme
|
||||
Set the text color Définir la couleur du texte
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1323,6 +1323,7 @@
|
|||
Set background color Atur warna background
|
||||
Set foreground color Atur warna foreground
|
||||
Set painting symmetry options Atur opsi simetri lukisan
|
||||
Symmetry Off Simetri Mati
|
||||
Set shape fill type Atur jenis isian bentuk
|
||||
Set shape stroke type Atur jenis stroke bentuk
|
||||
Set the text color Atur warna teks
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -1340,6 +1340,7 @@
|
|||
Set background color Imposta colore sfondo
|
||||
Set foreground color Imposta colore primo piano
|
||||
Set painting symmetry options Imposta opzioni simmetria pittura
|
||||
Symmetry Off Simmetria disattivata
|
||||
Set shape fill type Imposta tipo riempimento forma
|
||||
Set shape stroke type Imposta tipo traccia forma
|
||||
Set the text color Imposta colore testo
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -1347,6 +1347,7 @@
|
|||
Set background color 背景色を設定
|
||||
Set foreground color 描画色を設定
|
||||
Set painting symmetry options 対称描画のオプションを設定
|
||||
Symmetry Off 対称オフ
|
||||
Set shape fill type シェイプの塗りの種類を設定
|
||||
Set shape stroke type シェイプの線の種類を設定
|
||||
Set the text color 文字色を設定
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1324,6 +1324,7 @@
|
|||
Set background color 배경색 설정
|
||||
Set foreground color 전경색 설정
|
||||
Set painting symmetry options 페인팅 대칭 옵션 설정
|
||||
Symmetry Off 대칭 끔
|
||||
Set shape fill type 모양 채우기 유형 설정
|
||||
Set shape stroke type 모양 획 유형 설정
|
||||
Set the text color 텍스트 색상 설정
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 10.
|
|
|
@ -1328,6 +1328,7 @@
|
|||
Set background color Definir cor de fundo
|
||||
Set foreground color Definir cor de primeiro plano
|
||||
Set painting symmetry options Definir opções de simetria de pintura
|
||||
Symmetry Off Simetria desativada
|
||||
Set shape fill type Definir tipo de preenchimento da forma
|
||||
Set shape stroke type Definir tipo de traçado da forma
|
||||
Set the text color Definir a cor do texto
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1637,6 +1637,7 @@
|
|||
Measure Измерить
|
||||
Heal Восстановление
|
||||
Off Выкл
|
||||
Symmetry Off Без симметрии
|
||||
Heal Off Восстановление выкл
|
||||
Heal On Восстановление вкл
|
||||
Heal Auto Автовосстановление
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1352,6 +1352,7 @@
|
|||
Set background color 设置背景色
|
||||
Set foreground color 设置前景色
|
||||
Set painting symmetry options 设置绘画对称选项
|
||||
Symmetry Off 关闭对称
|
||||
Set shape fill type 设置形状填充类型
|
||||
Set shape stroke type 设置形状描边类型
|
||||
Set the text color 设置文字颜色
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -1348,6 +1348,7 @@
|
|||
Set background color 設定背景色
|
||||
Set foreground color 設定前景色
|
||||
Set painting symmetry options 設定繪畫對稱選項
|
||||
Symmetry Off 關閉對稱
|
||||
Set shape fill type 設定形狀填滿類型
|
||||
Set shape stroke type 設定形狀筆畫類型
|
||||
Set the text color 設定文字顏色
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -522,15 +522,18 @@ pub fn options_bar(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
|||
b.pressure_opacity = !b.pressure_opacity;
|
||||
}
|
||||
percent_field(ui, tl!("Flow"), &mut b.flow, 1.0..=100.0, 62.0);
|
||||
let _ = icons::button(ui, "sparkles", 24.0, false, tl!("Enable airbrush-style build-up effects"));
|
||||
let airbrush = icons::button(ui, "sparkles", 24.0, b.build_up, tl!("Enable airbrush-style build-up effects"));
|
||||
if crate::brush_picker::named(airbrush, tl!("Enable airbrush-style build-up effects")).clicked() {
|
||||
b.build_up = !b.build_up;
|
||||
}
|
||||
opt_label(ui, tl!("Smoothing"));
|
||||
smoothing_field(ui, b, 58.0);
|
||||
let _ = icons::button(ui, "settings", 24.0, false, tl!("Set additional smoothing options"));
|
||||
smoothing_options(ui, b);
|
||||
widgets::vline(ui, 22.0);
|
||||
if icons::button(ui, "circle-dot", 24.0, b.pressure_size, tl!("Always use pressure for size")).clicked() {
|
||||
b.pressure_size = !b.pressure_size;
|
||||
}
|
||||
let _ = icons::button(ui, "arrow-left-right", 24.0, false, tl!("Set painting symmetry options"));
|
||||
symmetry_menu(app, ui);
|
||||
}
|
||||
// Pencil: Photoshop's options (no hardness or flow: the pencil is always hard).
|
||||
Tool::Pencil => {
|
||||
|
|
@ -2465,6 +2468,68 @@ fn smoothing_field(ui: &mut egui::Ui, b: &mut photocraft_engine::BrushSettings,
|
|||
}
|
||||
}
|
||||
|
||||
/// Options-bar gear beside Smoothing: Photoshop's smoothing options popup, edited on the brush
|
||||
/// like the Smoothing % (so they stay per tool and reach the live stroke and the commit).
|
||||
fn smoothing_options(ui: &mut egui::Ui, b: &mut photocraft_engine::BrushSettings) {
|
||||
let resp = icons::button(ui, "settings", 24.0, false, tl!("Set additional smoothing options"));
|
||||
let resp = crate::brush_picker::named(resp, tl!("Set additional smoothing options"));
|
||||
egui::Popup::from_toggle_button_response(&resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
let s = &mut b.smoothing;
|
||||
widgets::checkbox(ui, &mut s.pulled_string, tl!("Pulled String Mode"));
|
||||
widgets::checkbox(ui, &mut s.catch_up, tl!("Stroke Catch-up"));
|
||||
widgets::checkbox(ui, &mut s.catch_up_on_end, tl!("Catch-up on Stroke End"));
|
||||
widgets::checkbox(ui, &mut s.adjust_for_zoom, tl!("Adjust for Zoom"));
|
||||
});
|
||||
}
|
||||
|
||||
/// The symmetry menu's rows: `paint.symmetryFromPath` name and label for each path the Paths
|
||||
/// panel lists (saved paths, the Work Path, the selected layer's shape path or vector mask).
|
||||
fn symmetry_choices(app: &PhotocraftApp) -> Vec<(String, String)> {
|
||||
use crate::vector_ui::PathRow;
|
||||
let Some(st) = app.session.active() else { return Vec::new() };
|
||||
crate::vector_ui::path_rows(&st.doc, st.active_layer)
|
||||
.into_iter()
|
||||
.map(|row| match row.kind {
|
||||
PathRow::Saved => (row.name.clone(), row.name),
|
||||
PathRow::Work => ("work".to_string(), tl!("Work Path").to_string()),
|
||||
PathRow::Layer => ("layer".to_string(), row.name),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Options-bar painting symmetry: Symmetry Off, or mirror Brush and Eraser strokes across one of
|
||||
/// the document's paths (`paint.symmetryFromPath`). The button is lit while symmetry is on.
|
||||
fn symmetry_menu(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let current = app.session.active().and_then(|st| st.symmetry_path.as_ref()).map(|axis| axis.source.clone());
|
||||
let choices = symmetry_choices(app);
|
||||
let resp = icons::button(ui, "arrow-left-right", 24.0, current.is_some(), tl!("Set painting symmetry options"));
|
||||
let resp = crate::brush_picker::named(resp, tl!("Set painting symmetry options"));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.set_min_width(200.0);
|
||||
let mut run = None;
|
||||
if ui.add(egui::Button::selectable(current.is_none(), tl!("Symmetry Off"))).clicked() {
|
||||
run = current.is_some().then(|| ("paint.symmetryDisable", json!({})));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if choices.is_empty() {
|
||||
ui.label(RichText::new(tl!("Draw with the Pen tool (P) or make a work path from a selection.")).color(t.text_faint));
|
||||
}
|
||||
for (name, label) in &choices {
|
||||
if ui.add(egui::Button::selectable(current.as_ref() == Some(name), label)).clicked() {
|
||||
run = Some(("paint.symmetryFromPath", json!({ "name": name })));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
if let Some((id, params)) = run
|
||||
&& let Err(e) = app.run(id, params)
|
||||
{
|
||||
app.ui.status = e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Options-bar brush chip; opens Photoshop's Brush Preset picker (size, hardness, the preset
|
||||
/// library). Returns what the picker asked for beyond the size and hardness edits in `b`.
|
||||
fn brush_preset_chip(
|
||||
|
|
|
|||
Loading…
Reference in New Issue