Merge PR #542 from NikitkaRa: Design: Variants gallery with four colour variations of the theme

# Conflicts:
#	crates/engine/src/cmd/design.rs
#	crates/engine/src/tests.rs
This commit is contained in:
Brandon Thomas
2026-10-14 14:37:07 -04:00
5 changed files with 107 additions and 1 deletions
+28
View File
@@ -34,6 +34,8 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
colors
),
cmd!("design.variant", "Variants", ["Design", "Variants"], None, "{index: 0..3} (see design.variants)", has_doc, variant),
cmd!(query "design.variants", "Variant Gallery", [], None, "{} → the current theme's four colour variants and which is applied", has_doc, variants),
cmd!("design.fonts", "Fonts", ["Design", "Variants"], None, "{} opens Create New Theme Fonts | {name} | {major, minor}", has_doc, fonts),
cmd!("design.effects", "Effects", ["Design", "Variants"], None, "{name} (see design.themes)", has_doc, effects),
cmd!(
@@ -257,6 +259,32 @@ fn colors(s: &mut Session, p: &Value) -> Result<Value> {
ok()
}
fn variant(s: &mut Session, p: &Value) -> Result<Value> {
let index = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("design.variant", "missing `index` (0..3)"))? as usize;
s.edit(|doc, sel| {
let mi = master_index(doc, sel);
let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("design.variant", "no master"))?);
let v = deckcraft_model::theme::theme_variants(&m.theme);
m.theme.colors =
v.get(index).cloned().ok_or_else(|| bad("design.variant", format!("no variant {index}; 0..{}", v.len().saturating_sub(1))))?;
Ok(())
})?;
Ok(json!({"variant": index}))
}
fn variants(s: &mut Session, _p: &Value) -> Result<Value> {
let st = s.doc()?;
let mi = master_index(&st.doc, &st.selection);
let th = st.doc.masters.get(mi).map(|m| m.theme.clone()).unwrap_or_default();
let v = deckcraft_model::theme::theme_variants(&th);
let current = v.iter().position(|c| c.colors == th.colors.colors);
Ok(json!({
"theme": th.name,
"current": current,
"variants": v.iter().map(|c| json!({"name": c.name, "colors": c.colors.iter().map(|x| format!("#{}", x.hex())).collect::<Vec<_>>()})).collect::<Vec<_>>(),
}))
}
fn current_colors(s: &Session) -> Result<deckcraft_color::ColorScheme> {
let st = s.doc()?;
let mi = master_index(&st.doc, &st.selection);
+28
View File
@@ -2314,3 +2314,31 @@ fn autocorrect_options_as_you_type() {
s.execute("text.insert", &json!({"text": "monday notes "})).unwrap();
assert_eq!(s.execute("text.get", &json!({"id": title})).unwrap()["text"], "monday notes ");
}
#[test]
fn theme_variants_apply_and_report_the_current_one() {
let mut s = session();
let name = deckcraft_model::theme::builtin_themes()[1].name.clone();
s.execute("design.theme", &json!({"name": name})).unwrap();
let v = s.execute("design.variants", &json!({})).unwrap();
assert_eq!(v["variants"].as_array().unwrap().len(), 4);
assert_eq!(v["current"], json!(0));
let base = s.doc().unwrap().doc.masters[0].theme.colors.clone();
s.execute("design.variant", &json!({"index": 1})).unwrap();
let c = s.doc().unwrap().doc.masters[0].theme.colors.clone();
assert_eq!(c.colors[4], base.colors[5], "accents rotate");
assert_eq!(c.colors[0], base.colors[0]);
assert_eq!(s.execute("design.variants", &json!({})).unwrap()["current"], json!(1));
// From a variant, another variant is built from the theme's own colours, not the variant's.
s.execute("design.variant", &json!({"index": 3})).unwrap();
let d = s.doc().unwrap().doc.masters[0].theme.colors.clone();
assert_eq!((d.colors[0], d.colors[1], d.colors[4]), (base.colors[1], base.colors[0], base.colors[4]));
s.execute("design.variant", &json!({"index": 0})).unwrap();
assert_eq!(s.doc().unwrap().doc.masters[0].theme.colors.colors, base.colors);
assert!(s.execute("design.variant", &json!({"index": 9})).is_err());
// It survives PPTX.
s.execute("design.variant", &json!({"index": 2})).unwrap();
let p = s.doc().unwrap().doc.as_ref().clone();
let back = deckcraft_pptx::import(&deckcraft_pptx::export(&p).unwrap()).unwrap();
assert_eq!(back.masters[0].theme.colors.colors, p.masters[0].theme.colors.colors);
}
+25
View File
@@ -149,6 +149,31 @@ pub fn default_format() -> FormatScheme {
}
}
/// Design ▸ Variants: four colour variations of `t`, built from its built-in colours when it is
/// a built-in theme (so picking one variant after another doesn't drift), else from its own.
/// 1: as designed; 2 and 3: the accents rotated; 4: dark (light and dark colours swapped).
pub fn theme_variants(t: &Theme) -> Vec<ColorScheme> {
let base = builtin_themes().into_iter().find(|b| b.name == t.name).map_or_else(|| t.colors.clone(), |b| b.colors);
let rotate = |by: usize| {
let mut c = base.clone();
let accents: Vec<_> = base.colors.get(4..10).map(<[_]>::to_vec).unwrap_or_default();
for (i, slot) in c.colors.iter_mut().skip(4).take(6).enumerate() {
if let Some(a) = accents.get((i + by) % accents.len().max(1)) {
*slot = *a;
}
}
c
};
let mut dark = base.clone();
dark.colors.swap(0, 1);
dark.colors.swap(2, 3);
let mut out = vec![base.clone(), rotate(1), rotate(3), dark];
for (i, c) in out.iter_mut().enumerate().skip(1) {
c.name = format!("{} {}", base.name, i + 1);
}
out
}
/// Our original built-in themes (shown in Design ▸ Themes). Colours and names are our own.
pub fn builtin_themes() -> Vec<Theme> {
let t = |name: &str, colors: ColorScheme, major: &str, minor: &str| Theme {
+23
View File
@@ -1452,6 +1452,29 @@ fn design(app: &mut SlideApp, ui: &mut Ui) {
run(app, "app.browseThemeDialog", json!({}));
}
});
// Variants: the current theme in its four colour variations.
if let Some(th) = app.session.active().and_then(|d| d.doc.masters.first().map(|m| m.theme.clone())) {
let variants = deckcraft_model::theme::theme_variants(&th);
let tiles: Vec<deckcraft_model::Theme> = variants.iter().map(|c| deckcraft_model::Theme { colors: c.clone(), ..th.clone() }).collect();
let names: Vec<String> = variants.iter().map(|c| c.name.clone()).collect();
let sel = variants.iter().position(|c| c.colors == th.colors.colors);
group(ui, |ui| {
if let Some(i) = widgets::gallery(
ui,
tiles.len(),
vec2(84.0, 56.0),
sel,
|p, r, i| {
if let Some(t) = tiles.get(i) {
paint_theme_tile(p, r, t);
}
},
|i| names.get(i).cloned().unwrap_or_default(),
) {
run(app, "design.variant", json!({"index": i}));
}
});
}
group(ui, |ui| {
let r = big_button(ui, Icon::Colors, "Colors", true);
r.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, "Colors"));
+3 -1
View File
@@ -26,6 +26,8 @@ The summary between `<!-- SUMMARY -->
| Masters & themes | 85% |
| Views | 90% |
| Masters & themes | 88% |
| Views | 90% |
| Masters & themes | 88% |
| Shapes | 88% |
| Shapes | 91% |
| Format | 83% |
@@ -105,7 +107,7 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou
| Masters & themes | Insert/rename/delete layouts, insert placeholders | P1 | D |
| Masters & themes | Handout and Notes masters | P2 | M |
| Masters & themes | Built-in themes (original designs) | P0 | D |
| Masters & themes | Theme variants | P1 | P |
| Masters & themes | Theme variants | P1 | D |
| Masters & themes | Colour schemes (built-in and custom) | P0 | D |
| Masters & themes | Font schemes (built-in and custom) | P0 | D |
| Masters & themes | Effect schemes / background styles | P2 | P |