mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #500 from NikitkaRa: Format Shape: line compound, cap, join, arrows and sizes; lock aspect ratio
# Conflicts: # crates/engine/src/tests.rs
This commit is contained in:
@@ -51,7 +51,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Shape Outline",
|
||||
["Shape Format", "Shape Styles"],
|
||||
None,
|
||||
"{color?, none?, width?: pt, dash?: solid|dot|dash|lgDash|dashDot|sysDot|sysDash, cap?, join?, head?: none|triangle|stealth|diamond|oval|arrow, tail?, reset?, ids?}",
|
||||
"{color?, none?, width?: pt, dash?: solid|dot|dash|lgDash|dashDot|sysDot|sysDash, cap?: flat|round|square, join?: round|bevel|miter, compound?: single|double|thickThin|thinThick|triple, head?: none|triangle|stealth|diamond|oval|arrow, tail?, headSize?: sm|med|lg, tailSize?, reset?, ids?}",
|
||||
has_selection,
|
||||
line
|
||||
),
|
||||
@@ -350,6 +350,15 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"square" => deckcraft_model::style::LineCap::Square,
|
||||
_ => deckcraft_model::style::LineCap::Flat,
|
||||
});
|
||||
let compound = str_param(p, "compound").map(|c| match c {
|
||||
"double" => deckcraft_model::style::Compound::Double,
|
||||
"thickThin" => deckcraft_model::style::Compound::ThickThin,
|
||||
"thinThick" => deckcraft_model::style::Compound::ThinThick,
|
||||
"triple" => deckcraft_model::style::Compound::Triple,
|
||||
_ => deckcraft_model::style::Compound::Single,
|
||||
});
|
||||
let end_size = |k: &str| str_param(p, k).filter(|v| matches!(*v, "sm" | "med" | "lg")).map(str::to_string);
|
||||
let (head_size, tail_size) = (end_size("headSize"), end_size("tailSize"));
|
||||
let join = str_param(p, "join").map(|c| match c {
|
||||
"bevel" => deckcraft_model::style::LineJoin::Bevel,
|
||||
"miter" => deckcraft_model::style::LineJoin::Miter,
|
||||
@@ -376,11 +385,20 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if dash.is_some() {
|
||||
l.dash.clone_from(&dash);
|
||||
}
|
||||
let keep_size = |old: &Option<LineEnd>, new: &Option<LineEnd>| {
|
||||
new.clone().map(|mut n| {
|
||||
if let Some(o) = old {
|
||||
n.w.clone_from(&o.w);
|
||||
n.len.clone_from(&o.len);
|
||||
}
|
||||
n
|
||||
})
|
||||
};
|
||||
if let Some(h) = &head {
|
||||
l.head.clone_from(h);
|
||||
l.head = keep_size(&l.head, h);
|
||||
}
|
||||
if let Some(t) = &tail {
|
||||
l.tail.clone_from(t);
|
||||
l.tail = keep_size(&l.tail, t);
|
||||
}
|
||||
if cap.is_some() {
|
||||
l.cap = cap;
|
||||
@@ -388,6 +406,16 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if join.is_some() {
|
||||
l.join = join;
|
||||
}
|
||||
if compound.is_some() {
|
||||
l.compound = compound;
|
||||
}
|
||||
// Arrowhead size (width and length together), kept when the kind changes.
|
||||
for (end, size) in [(&mut l.head, &head_size), (&mut l.tail, &tail_size)] {
|
||||
if let (Some(e), Some(sz)) = (end.as_mut(), size) {
|
||||
e.w.clone_from(sz);
|
||||
e.len.clone_from(sz);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1207,3 +1207,27 @@ fn pdf_options_take_paper_and_orientation() {
|
||||
assert!(cmd::file::pdf_options(&json!({"paper": "tabloid"}), 1).is_err());
|
||||
assert!(cmd::file::pdf_options(&json!({"orientation": "sideways"}), 1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_line_sets_compound_and_arrow_sizes() {
|
||||
// The Format Shape pane's Line page: compound type, arrowheads and their sizes.
|
||||
use deckcraft_model::style::Compound;
|
||||
let mut s = Session::with_new();
|
||||
s.execute("slide.new", &json!({"layout": "blank"})).unwrap();
|
||||
s.execute("shape.insert", &json!({"preset": "line", "rect": [100, 100, 200, 0]})).unwrap();
|
||||
s.execute("shape.line", &json!({"compound": "thickThin", "head": "oval", "tail": "triangle", "tailSize": "lg"})).unwrap();
|
||||
let line = |s: &Session| s.doc().unwrap().selected_shapes()[0].line.clone().unwrap();
|
||||
let l = line(&s);
|
||||
assert_eq!(l.compound, Some(Compound::ThickThin));
|
||||
assert_eq!(l.head.as_ref().map(|e| (e.kind.as_str(), e.w.as_str())), Some(("oval", "med")));
|
||||
assert_eq!(l.tail.as_ref().map(|e| (e.kind.as_str(), e.w.as_str(), e.len.as_str())), Some(("triangle", "lg", "lg")));
|
||||
// Changing the arrow keeps its size; an unknown size is ignored; "none" removes it.
|
||||
s.execute("shape.line", &json!({"tail": "stealth", "headSize": "huge"})).unwrap();
|
||||
let l = line(&s);
|
||||
assert_eq!(l.tail.as_ref().map(|e| (e.kind.as_str(), e.w.as_str())), Some(("stealth", "lg")));
|
||||
assert_eq!(l.head.as_ref().map(|e| e.w.as_str()), Some("med"));
|
||||
s.execute("shape.line", &json!({"head": "none", "compound": "single"})).unwrap();
|
||||
let l = line(&s);
|
||||
assert!(l.head.is_none());
|
||||
assert_eq!(l.compound, Some(Compound::Single));
|
||||
}
|
||||
|
||||
@@ -156,11 +156,18 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
"size" => {
|
||||
let lock_id = egui::Id::new("fmt-lock-aspect");
|
||||
let mut lock: bool = ui.data_mut(|d| d.get_temp(lock_id).unwrap_or(false));
|
||||
let locked = lock;
|
||||
let mut fields = |ui: &mut Ui, label: &str, id: &str, v: f64, key: &str, cmd: &str, unit: f64| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_sized(vec2(90.0, 20.0), egui::Label::new(label));
|
||||
if let Some(nv) = widgets::spinner(ui, id, v / unit, 0.1, if unit == 72.0 { "\"" } else { "°" }, 70.0) {
|
||||
run(app, cmd, json!({key: nv * unit}));
|
||||
let mut p = json!({key: nv * unit});
|
||||
if cmd == "shape.resize" && locked {
|
||||
p["lockAspect"] = json!(true);
|
||||
}
|
||||
run(app, cmd, p);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -168,6 +175,9 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
fields(ui, "Height", "fh", x.h, "h", "shape.resize", 72.0);
|
||||
fields(ui, "Width", "fw", x.w, "w", "shape.resize", 72.0);
|
||||
fields(ui, "Rotation", "fr", x.rot, "deg", "shape.rotate", 1.0);
|
||||
if ui.checkbox(&mut lock, "Lock aspect ratio").changed() {
|
||||
ui.data_mut(|d| d.insert_temp(lock_id, lock));
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Position").font(theme::bold(13.0)));
|
||||
fields(ui, "Horizontal", "fx", x.x, "x", "shape.move", 72.0);
|
||||
@@ -337,6 +347,67 @@ fn format_shape(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
});
|
||||
line_details(app, ui, &sh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A labelled drop-down on the Format Shape pane: picks one of `items` (label, value) and runs
|
||||
/// `shape.line` with `{key: value}`.
|
||||
fn line_choice(app: &mut SlideApp, ui: &mut Ui, label: &str, key: &str, current: &str, items: &[(&str, &str)]) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_sized(vec2(90.0, 20.0), egui::Label::new(label));
|
||||
let shown = items.iter().find(|(_, v)| *v == current).map_or(current, |(l, _)| *l);
|
||||
egui::ComboBox::from_id_salt(("fmt-line", key)).selected_text(shown).width(120.0).show_ui(ui, |ui| {
|
||||
for (l, v) in items {
|
||||
if ui.selectable_label(*v == current, *l).clicked() {
|
||||
run(app, "shape.line", json!({key: v}));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// The rest of PowerPoint's Line page: compound type, cap and join types, and the begin and end
|
||||
/// arrows with their sizes.
|
||||
fn line_details(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) {
|
||||
use deckcraft_model::style::{Compound, LineCap, LineJoin};
|
||||
let l = sh.line.clone().unwrap_or_default();
|
||||
let compound = match l.compound.unwrap_or_default() {
|
||||
Compound::Single => "single",
|
||||
Compound::Double => "double",
|
||||
Compound::ThickThin => "thickThin",
|
||||
Compound::ThinThick => "thinThick",
|
||||
Compound::Triple => "triple",
|
||||
};
|
||||
line_choice(
|
||||
app,
|
||||
ui,
|
||||
"Compound type",
|
||||
"compound",
|
||||
compound,
|
||||
&[("Single", "single"), ("Double", "double"), ("Thick Thin", "thickThin"), ("Thin Thick", "thinThick"), ("Triple", "triple")],
|
||||
);
|
||||
let cap = match l.cap.unwrap_or_default() {
|
||||
LineCap::Flat => "flat",
|
||||
LineCap::Round => "round",
|
||||
LineCap::Square => "square",
|
||||
};
|
||||
line_choice(app, ui, "Cap type", "cap", cap, &[("Flat", "flat"), ("Round", "round"), ("Square", "square")]);
|
||||
let join = match l.join.unwrap_or_default() {
|
||||
LineJoin::Round => "round",
|
||||
LineJoin::Bevel => "bevel",
|
||||
LineJoin::Miter => "miter",
|
||||
};
|
||||
line_choice(app, ui, "Join type", "join", join, &[("Round", "round"), ("Bevel", "bevel"), ("Miter", "miter")]);
|
||||
let arrows = [("None", "none"), ("Arrow", "triangle"), ("Open Arrow", "arrow"), ("Stealth", "stealth"), ("Diamond", "diamond"), ("Oval", "oval")];
|
||||
let sizes = [("Small", "sm"), ("Medium", "med"), ("Large", "lg")];
|
||||
for (label, key, size_label, size_key, end) in
|
||||
[("Begin arrow", "head", "Begin size", "headSize", &l.head), ("End arrow", "tail", "End size", "tailSize", &l.tail)]
|
||||
{
|
||||
line_choice(app, ui, label, key, end.as_ref().map_or("none", |e| e.kind.as_str()), &arrows);
|
||||
if let Some(e) = end {
|
||||
line_choice(app, ui, size_label, size_key, &e.w, &sizes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
|
||||
|
||||
| Pane / dialog | PowerPoint | DeckCraft |
|
||||
|---|---|---|
|
||||
| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill, gradient editor, line, some effects, size; no 3-D, thin picture/texture options |
|
||||
| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill, gradient editor, line (dash, compound, cap, join, begin/end arrows and sizes), some effects, size (lock aspect ratio); no 3-D, thin picture/texture options |
|
||||
| Format Background pane | yes | yes |
|
||||
| Selection Pane | front-most first, groups fold, drag to restack, Bring Forward / Send Backward arrows, Show All / Hide All, eye, padlock (Lock), rename in place | same: drag restacks among siblings (a child stays in its group), Ctrl+click toggles, Shift+click selects a run, arrows / Home / End / F2 / Esc in the list, right-click menu |
|
||||
| Animation Pane | list + advanced timeline | list, reorder, timing; no timeline |
|
||||
|
||||
Reference in New Issue
Block a user