mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-29 21:16:57 -06:00
Design: Variants gallery with four colour variations of the theme
PowerPoint's Design tab shows the current theme in four variants. The Variants group had only the Colors and Fonts menus. theme_variants builds four colour schemes from the theme's built-in colours (as designed, two accent rotations, and a dark variant with light and dark swapped), so switching between variants doesn't drift. design.variant applies one to the master; design.variants reports them and which is applied. The Design tab shows them as a gallery before Colors and Fonts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!("design.theme", "Themes", ["Design", "Themes"], None, "{name} (see design.themes)", has_doc, theme),
|
||||
cmd!(query "design.themes", "Theme Gallery", [], None, "{} → themes, colour schemes and font schemes", always, themes),
|
||||
cmd!("design.colors", "Colors", ["Design", "Variants"], None, "{name} | {colors: {accent1: #RRGGBB, …}}", 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, "{name} | {major, minor}", has_doc, fonts),
|
||||
cmd!(
|
||||
"design.background",
|
||||
@@ -125,6 +127,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 fonts(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let scheme = if let Some(name) = str_param(p, "name") {
|
||||
builtin_font_schemes()
|
||||
|
||||
@@ -835,3 +835,31 @@ fn slide_ranges_parse_like_powerpoints_print_box() {
|
||||
let opts = cmd::file::pdf_options(&json!({"slides": [0, 7, 1]}), 2).unwrap();
|
||||
assert_eq!(opts.slides, Some(vec![0, 1]), "indices past the end are dropped");
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
|
||||
@@ -150,6 +150,31 @@ pub fn default_format() -> FormatScheme {
|
||||
}
|
||||
|
||||
/// Our original built-in themes (shown in Design ▸ Themes). Colours and names are our own.
|
||||
/// 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
|
||||
}
|
||||
|
||||
pub fn builtin_themes() -> Vec<Theme> {
|
||||
let t = |name: &str, colors: ColorScheme, major: &str, minor: &str| Theme {
|
||||
name: name.into(),
|
||||
|
||||
@@ -1233,6 +1233,29 @@ fn design(app: &mut SlideApp, ui: &mut Ui) {
|
||||
run(app, "design.theme", json!({"name": n}));
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
egui::Popup::menu(&r).width(240.0).show(|ui| {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
> **Generated:** 2026-10-10 (summary by `cargo xtask parity`; rows re-scored 2026-10-10 against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
The summary between `<!-- SUMMARY -->
|
||||
**Weighted breadth parity: 79%** over 191 features.
|
||||
**Weighted breadth parity: 80%** over 191 features.
|
||||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 72 | 61 | 11 | 0 | 92% |
|
||||
| P1 | 65 | 38 | 21 | 6 | 75% |
|
||||
| P1 | 65 | 39 | 20 | 6 | 75% |
|
||||
| P2 | 54 | 13 | 15 | 26 | 38% |
|
||||
|
||||
| Area | Parity |
|
||||
@@ -17,7 +17,7 @@ The summary between `<!-- SUMMARY -->
|
||||
| Files | 71% |
|
||||
| Slides | 90% |
|
||||
| Views | 90% |
|
||||
| Masters & themes | 85% |
|
||||
| Masters & themes | 88% |
|
||||
| Shapes | 88% |
|
||||
| Format | 83% |
|
||||
| Arrange | 100% |
|
||||
@@ -94,7 +94,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 |
|
||||
|
||||
Reference in New Issue
Block a user