Merge PR #1141: Color panel: edit future-shape colors with nothing selected

This commit is contained in:
aiongg
2026-10-30 19:44:10 +00:00
13 changed files with 115 additions and 2 deletions
+1
View File
@@ -392,6 +392,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Object Layer Options", "خيارات طبقات الكائن"),
("Colour Picker", "منتقي الألوان"),
("Color Picker", "أداة اختيار الألوان"),
("Color spectrum", "طيف الألوان"),
("Color Settings", "إعدادات الألوان"),
("User Dictionary", "قاموس المستخدم"),
("Menu Customization", "تخصيص القوائم"),
+1
View File
@@ -195,6 +195,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "Vybrat obrázek…"),
("Class:", "Třída:"),
("Color Picker", "Výběr barvy"),
("Color spectrum", "Barevné spektrum"),
("Color Settings", "Nastavení barev"),
("Color:", "Barva:"),
("Columns", "Sloupce"),
+1
View File
@@ -224,6 +224,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "Bild auswählen …"),
("Class:", "Klasse:"),
("Color Picker", "Farbwähler"),
("Color spectrum", "Farbspektrum"),
("Color Settings", "Farbeinstellungen"),
("Color:", "Farbe:"),
("Columns", "Spalten"),
+1
View File
@@ -306,6 +306,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "Scegli immagine…"),
("Class:", "Classe:"),
("Color Picker", "Selettore colore"),
("Color spectrum", "Spettro dei colori"),
("Color Settings", "Impostazioni colore"),
("Color:", "Colore:"),
("Columns", "Colonne"),
+1
View File
@@ -409,6 +409,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "画像を選択…"),
("Class:", "クラス:"),
("Color Picker", "カラーピッカー"),
("Color spectrum", "カラースペクトル"),
("Color Settings", "カラー設定"),
("Color:", "カラー:"),
("Columns", "列数"),
+1
View File
@@ -228,6 +228,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "გამოსახულების არჩევა…"),
("Class:", "კლასი:"),
("Color Picker", "ფერის ამრჩევი"),
("Color spectrum", "ფერთა სპექტრი"),
("Color Settings", "ფერის პარამეტრები"),
("Color:", "ფერი:"),
("Columns", "სვეტები"),
+1
View File
@@ -195,6 +195,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "Wybierz obraz…"),
("Class:", "Klasa:"),
("Color Picker", "Próbnik kolorów"),
("Color spectrum", "Spektrum kolorów"),
("Color Settings", "Ustawienia kolorów"),
("Color:", "Kolor:"),
("Columns", "Łamy"),
+1
View File
@@ -400,6 +400,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Object Layer Options", "Opções de camada do objeto"),
("Colour Picker", "Seletor de cores"),
("Color Picker", "Seletor de cores"),
("Color spectrum", "Espectro de cores"),
("Color Settings", "Configurações de cores"),
("User Dictionary", "Dicionário do usuário"),
("Menu Customization", "Personalização de menus"),
+1
View File
@@ -195,6 +195,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "Выбрать изображение…"),
("Class:", "Класс:"),
("Color Picker", "Палитра цветов"),
("Color spectrum", "Цветовой спектр"),
("Color Settings", "Настройка цветов"),
("Color:", "Цвет:"),
("Columns", "Колонки"),
+1
View File
@@ -959,6 +959,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[
("Choose Image…", ["Choose Image…", "Choisir une image…", "Choose Image…", "Choose Image…", "选择图像…"]),
("Class:", ["Class:", "Classe :", "Class:", "Class:", "类别:"]),
("Color Picker", ["Color Picker", "Sélecteur de couleurs", "Color Picker", "Color Picker", "拾色器"]),
("Color spectrum", ["Farbspektrum", "Spectre de couleurs", "Espectro de color", "カラースペクトル", "色谱"]),
("Color Settings", ["Color Settings", "Réglages des couleurs", "Color Settings", "Color Settings", "颜色设置"]),
("Color:", ["Color:", "Couleur :", "Color:", "Color:", "颜色:"]),
("Columns", ["Columns", "Colonnes", "Columns", "Columns", "栏"]),
+1
View File
@@ -309,6 +309,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Choose Image…", "Вибрати зображення…"),
("Class:", "Клас:"),
("Color Picker", "Палітра кольорів"),
("Color spectrum", "Колірний спектр"),
("Color Settings", "Налаштування кольору"),
("Color:", "Колір:"),
("Columns", "Колонки"),
+13
View File
@@ -82,6 +82,19 @@ pub fn color_target(app: &mut DesignApp) -> Option<(String, f32, String, f32)> {
Some((i.fill, sel_tint(app, false), i.stroke, sel_tint(app, true)))
}
/// [`color_target`], or with nothing selected the colours new shapes get (the Color and Swatches
/// panels then edit those, as InDesign's do).
pub fn color_target_or_defaults(app: &mut DesignApp) -> Option<(String, f32, String, f32)> {
color_target(app).or_else(|| {
let state = app.session.active()?;
if !state.selection.items.is_empty() || state.selection.text.is_some() || state.selection.cells.is_some() {
return None;
}
let colors = &state.doc.settings.new_object;
Some((colors.fill.clone(), colors.fill_tint, colors.stroke.clone(), colors.stroke_tint))
})
}
/// Apply a swatch (and tint) to the fill or stroke of what colour edits apply to.
pub fn apply_swatch(app: &mut DesignApp, stroke: bool, swatch: &str, tint: Option<f32>) -> Result<Value, String> {
remember_swatch(app, swatch);
+91 -2
View File
@@ -11,7 +11,8 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
let t = Tokens::get(ui.ctx());
let Some(st) = app.session.active() else { return };
let doc = st.doc.clone();
let target = super::color_target(app);
// With nothing selected, the proxy, Tint field and highlighted row are those of new shapes.
let target = super::color_target_or_defaults(app);
header(app, ui, &doc, target.as_ref());
// The row of the colour in front of the proxy is highlighted.
let fill_cur = target.map(|(fill, _, stroke, _)| if app.ui.stroke_front { stroke } else { fill });
@@ -510,7 +511,8 @@ pub enum ColorMode {
/// swatch), the spectrum ramp, Add to Swatches. Edits apply unnamed colours (`object.color`).
pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
let t = Tokens::get(ui.ctx());
let Some((fill, fill_tint, stroke_sw, stroke_tint)) = super::color_target(app) else {
let target = super::color_target_or_defaults(app);
let Some((fill, fill_tint, stroke_sw, stroke_tint)) = target else {
ui.label(crate::rtl::widget(
ui,
egui::RichText::new(crate::i18n::tr(&app.ui.language, "Select an object or text to color its fill or stroke.")).color(t.text_dim),
@@ -723,6 +725,7 @@ fn cmyk_ramp(k: usize, x: f32) -> egui::Color32 {
/// The spectrum ramp: hue across, light to dark down; click to apply.
fn spectrum(app: &mut DesignApp, ui: &mut egui::Ui, target: &str) {
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 36.0), Sense::click());
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, crate::i18n::tr(&app.ui.language, "Color spectrum")));
let at = |u: f32, v: f32| -> [f32; 3] {
let c = egui::ecolor::Hsva::new(u, (v * 2.0).min(1.0), (2.0 - v * 2.0).min(1.0), 1.0).to_rgb();
[c[0], c[1], c[2]]
@@ -768,6 +771,92 @@ fn color_json(c: &designcraft_color::Color) -> serde_json::Value {
}
}
#[cfg(test)]
mod color_panel_defaults_tests {
use super::*;
use egui_kittest::kittest::Queryable;
#[test]
fn color_panel_with_no_selection_edits_future_shapes_without_recoloring_existing_ones() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let old =
designcraft_doc::ItemId(app.run("frame.create", json!({"rect":[72,72,200,200],"content":"unassigned"})).unwrap()["id"].as_u64().unwrap());
let original = app.session.active().unwrap().doc.item(old).unwrap().stroke.clone();
app.run("selection.set", json!({"ids":[]})).unwrap();
app.ui.stroke_front = true;
app.run("window.panel", json!({"panel":"color"})).unwrap();
let mut h = crate::test_window::open(app, vec2(1440.0, 900.0));
assert!(h.query_by_label("Select an object or text to color its fill or stroke.").is_none());
let ramp = h.get_by_label("Color spectrum").rect();
crate::test_window::click_at(&mut h, egui::pos2(ramp.left() + ramp.width() / 3.0, ramp.center().y));
let defaults = h.state().app.session.active().unwrap().doc.settings.new_object.clone();
assert_ne!(defaults.stroke, original.swatch);
assert_eq!(h.state().app.session.active().unwrap().doc.item(old).unwrap().stroke, original);
h.state_mut().app.run("selection.set", json!({"ids":[old.0]})).unwrap();
assert_eq!(crate::panels::color_target(&mut h.state_mut().app).unwrap().2, original.swatch);
h.state_mut().app.run("selection.set", json!({"ids":[]})).unwrap();
let new = designcraft_doc::ItemId(
h.state_mut().app.run("frame.create", json!({"rect":[240,72,368,200],"content":"unassigned"})).unwrap()["id"].as_u64().unwrap(),
);
assert_eq!(h.state().app.session.active().unwrap().doc.item(new).unwrap().stroke.swatch, defaults.stroke);
// Two undo steps: the new frame, then the spectrum click's default stroke.
h.state_mut().app.run("edit.undo", json!({})).unwrap();
h.state_mut().app.run("edit.undo", json!({})).unwrap();
assert_eq!(h.state().app.session.active().unwrap().doc.settings.new_object.stroke, original.swatch);
assert_eq!(h.state().app.session.active().unwrap().doc.item(old).unwrap().stroke, original);
// With the Fill box in front, the ramp sets the default fill instead.
let original_fill = h.state().app.session.active().unwrap().doc.item(old).unwrap().fill.clone();
h.state_mut().app.ui.stroke_front = false;
h.run_steps(2);
let ramp = h.get_by_label("Color spectrum").rect();
crate::test_window::click_at(&mut h, egui::pos2(ramp.left() + ramp.width() * 2.0 / 3.0, ramp.center().y));
let defaults = h.state().app.session.active().unwrap().doc.settings.new_object.clone();
assert_ne!(defaults.fill, original_fill.swatch);
assert_eq!(defaults.stroke, original.swatch, "the stroke default is left alone");
assert_eq!(h.state().app.session.active().unwrap().doc.item(old).unwrap().fill, original_fill);
}
/// The Swatches panel shows the same defaults: its Tint field reads the default fill's tint,
/// and a click on a swatch row sets the default fill, not an existing frame's.
#[test]
fn swatches_panel_with_no_selection_shows_and_edits_future_shape_colors() {
use egui::accesskit::Role;
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let old =
designcraft_doc::ItemId(app.run("frame.create", json!({"rect":[72,72,200,200],"content":"unassigned"})).unwrap()["id"].as_u64().unwrap());
let original = app.session.active().unwrap().doc.item(old).unwrap().fill.clone();
app.run("selection.set", json!({"ids":[]})).unwrap();
app.run("object.fill", json!({"swatch": "[Black]", "tint": 0.4})).unwrap();
app.ui.stroke_front = false;
let mut h = egui_kittest::Harness::builder().with_size(vec2(320.0, 480.0)).build_ui_state(
|ui, app: &mut DesignApp| {
// The panel's fonts are installed on the first frame and used from the next.
let id = egui::Id::unique("test_fonts");
if ui.data(|d| d.get_temp::<bool>(id)).is_none() {
crate::theme::install_fonts(ui.ctx(), "");
ui.data_mut(|d| d.insert_temp(id, true));
return;
}
show(app, ui);
},
app,
);
h.run_steps(3);
let fields = h.query_all_by(|n| matches!(n.role(), Role::SpinButton | Role::TextInput)).filter_map(|n| n.value()).collect::<Vec<_>>();
assert!(fields.iter().any(|v| v.starts_with("40")), "the Tint field shows the default fill's 40 %: {fields:?}");
let row = h.get_by_label("[Paper]").rect().center();
h.hover_at(row);
h.drag_at(row);
h.drop_at(row);
h.run_steps(3);
let defaults = h.state().session.active().unwrap().doc.settings.new_object.clone();
assert_eq!((defaults.fill.as_str(), defaults.fill_tint), ("[Paper]", 1.0));
assert_eq!(h.state().session.active().unwrap().doc.item(old).unwrap().fill, original);
}
}
/// Stops in `object.gradient` form: a stop on a swatch also names it, so it keeps its inks.
fn stops_json(g: &designcraft_color::Gradient) -> Vec<serde_json::Value> {
g.stops