Browse / github.com/storytold/deckcraft / patch
Format: Line has every dash type and separate arrowhead width and length
Format: Line has every dash type and separate arrowhead width and length Format Shape's Line section offered five of PowerPoint's dash styles as small buttons that did not show the current one, and one size per arrowhead that set its width and length together. - **Dash type** is a list of every dash style PowerPoint has and shows the current one. Round Dot and Square Dot set the `dot` dash with a round or square cap, as PowerPoint saves them. - **Arrowheads** have separate Width and Length (small, medium, large), PowerPoint's nine sizes. `shape.line` gains `headWidth`, `headLength`, `tailWidth` and `tailLength`; `headSize` and `tailSize` still set both. - `docs/ui
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Harrison-Booth (imported from pull request #1436, open)
- Base
88c2ceff5fab9d34dd262a8184bbbd61ca369b97- Size
- +88 −15 in 6 files
- Review
- pending
- Shared
- 2026-10-28
- Id
761e977212d38e94796b9213
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/shape.rs crates/engine/src/cmd/shape/appearance.rs crates/engine/src/tests.rs crates/ui-egui/src/panels.rs crates/ui-egui/src/panels/format_shape.rs docs/ui-parity.md
@@ -112,7 +112,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?: 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?}; a group also formats its members",+ "{color?, none?, width?: pt, dash?: solid|dot|dash|lgDash|dashDot|lgDashDot|lgDashDotDot|sysDot|sysDash|sysDashDot|sysDashDotDot (PowerPoint's Round Dot and Square Dot are dot with cap round or square), 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? (both width and length), headWidth?, headLength?, tailWidth?, tailLength? (sm|med|lg), reset?, ids?}; a group also formats its members", has_selection, line ),@@ -211,6 +211,8 @@ pub(super) fn line(s: &mut Session, p: &Value) -> Result<Value> { }); 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 (head_w, head_len) = (end_size("headWidth"), end_size("headLength"));+ let (tail_w, tail_len) = (end_size("tailWidth"), end_size("tailLength")); let join = str_param(p, "join").map(|c| match c { "bevel" => deckcraft_model::style::LineJoin::Bevel, "miter" => deckcraft_model::style::LineJoin::Miter,@@ -268,6 +270,17 @@ pub(super) fn line(s: &mut Session, p: &Value) -> Result<Value> { e.len.clone_from(sz); } }+ // PowerPoint sizes an arrowhead's width and length separately.+ for (end, w, len) in [(&mut l.head, &head_w, &head_len), (&mut l.tail, &tail_w, &tail_len)] {+ if let Some(e) = end.as_mut() {+ if let Some(w) = w {+ e.w.clone_from(w);+ }+ if let Some(len) = len {+ e.len.clone_from(len);+ }+ }+ } Ok(()) }) }@@ -2294,6 +2294,17 @@ fn shape_line_sets_compound_and_arrow_sizes() { 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"));+ // Width and length are set apart, as in PowerPoint's nine arrowhead sizes.+ s.execute("shape.line", &json!({"tailWidth": "sm", "tailLength": "med", "headWidth": "lg", "headLength": "huge"})).unwrap();+ let l = line(&s);+ assert_eq!(l.tail.as_ref().map(|e| (e.w.as_str(), e.len.as_str())), Some(("sm", "med")));+ assert_eq!(l.head.as_ref().map(|e| (e.w.as_str(), e.len.as_str())), Some(("lg", "med")));+ // Round Dot and Square Dot are the dot dash with a cap.+ s.execute("shape.line", &json!({"dash": "dot", "cap": "round"})).unwrap();+ assert_eq!(+ (line(&s).dash, line(&s).cap),+ (Some(deckcraft_model::style::Dash::Preset { name: "dot".into() }), Some(deckcraft_model::style::LineCap::Round))+ ); s.execute("shape.line", &json!({"head": "none", "compound": "single"})).unwrap(); let l = line(&s); assert!(l.head.is_none());@@ -138,6 +138,14 @@ mod tests { use super::{fill_transparency_to_apply, solid_fill_transparency_percent}; use crate::{Services, SlideApp}; + #[test]+ fn dash_types_set_the_dash_and_the_cap_that_shows_them() {+ use super::format_shape::dash_params;+ assert_eq!(dash_params("roundDot"), json!({"dash": "dot", "cap": "round"}));+ assert_eq!(dash_params("dot"), json!({"dash": "dot", "cap": "flat"}));+ assert_eq!(dash_params("lgDash"), json!({"dash": "lgDash"}));+ }+ #[test] fn solid_fill_transparency_reads_the_alpha_shape_fill_writes() { assert_eq!(solid_fill_transparency_percent(None), 0.0);@@ -377,29 +377,65 @@ pub(super) fn format_shape(app: &mut SlideApp, ui: &mut Ui) { run(app, "shape.line", json!({"width": v.max(0.0)})); } });- ui.horizontal_wrapped(|ui| {- ui.label("Dash");- for (l, d) in [("—", "solid"), ("···", "sysDot"), ("- -", "dash"), ("-·-", "dashDot"), ("— —", "lgDash")] {- if ui.small_button(l).clicked() {- run(app, "shape.line", json!({"dash": d}));- }+ use deckcraft_model::style::{Dash, LineCap};+ let l = sh.line.clone().unwrap_or_default();+ let dash = match (&l.dash, l.cap) {+ (Some(Dash::Preset { name }), cap) if name == "dot" && matches!(cap, Some(LineCap::Round | LineCap::Square)) => {+ if cap == Some(LineCap::Round) { "roundDot" } else { "squareDot" } }- });+ (Some(Dash::Preset { name }), _) => name.as_str(),+ _ => "solid",+ };+ line_choice_with(app, ui, "Dash type", "dash", dash, &DASH_TYPES, dash_params); line_details(app, ui, &sh); } } } +/// Dash types, in the order of PowerPoint's list. Round Dot and Square Dot are the `dot` dash with+/// a round or square cap, as PowerPoint saves them.+const DASH_TYPES: [(&str, &str); 13] = [+ ("Solid", "solid"),+ ("Round Dot", "roundDot"),+ ("Square Dot", "squareDot"),+ ("Dot", "dot"),+ ("Dash", "dash"),+ ("Dash Dot", "dashDot"),+ ("Long Dash", "lgDash"),+ ("Long Dash Dot", "lgDashDot"),+ ("Long Dash Dot Dot", "lgDashDotDot"),+ ("System Dash", "sysDash"),+ ("System Dot", "sysDot"),+ ("System Dash Dot", "sysDashDot"),+ ("System Dash Dot Dot", "sysDashDotDot"),+];++/// The `shape.line` parameters for a `DASH_TYPES` value.+pub(super) fn dash_params(v: &str) -> Value {+ match v {+ "roundDot" => json!({"dash": "dot", "cap": "round"}),+ "squareDot" => json!({"dash": "dot", "cap": "square"}),+ // Plain Dot has a flat cap, so it does not stay Round Dot or Square Dot.+ "dot" => json!({"dash": "dot", "cap": "flat"}),+ v => json!({"dash": v}),+ }+}+ /// 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)]) {+ line_choice_with(app, ui, label, key, current, items, |v| json!({key: v}));+}++/// `line_choice` with the `shape.line` parameters made from the picked value.+fn line_choice_with(app: &mut SlideApp, ui: &mut Ui, label: &str, key: &str, current: &str, items: &[(&str, &str)], params: impl Fn(&str) -> Value) { ui.horizontal(|ui| { let field = 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); let combo = craft_ui::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}));+ run(app, "shape.line", params(v)); } } });@@ -441,12 +477,16 @@ fn line_details(app: &mut SlideApp, ui: &mut Ui, sh: &deckcraft_model::Shape) { 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)]- {+ // PowerPoint sizes an arrowhead's width and length apart.+ let ends = [+ ("Begin arrow", "head", "Begin width", "headWidth", "Begin length", "headLength", &l.head),+ ("End arrow", "tail", "End width", "tailWidth", "End length", "tailLength", &l.tail),+ ];+ for (label, key, w_label, w_key, len_label, len_key, end) in ends { 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);+ line_choice(app, ui, w_label, w_key, &e.w, &sizes);+ line_choice(app, ui, len_label, len_key, &e.len, &sizes); } } }@@ -1,6 +1,6 @@ # UI parity with PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (Edit Points: Shift, Ctrl/Cmd or Alt/Option on a handle drag sets the point type; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: major, community PR wave 2026-10-14: 14 new dialogs (41 in all), Eyedropper, Export button, New Window, Grid Options, WordArt gallery and handles, Slide Sorter sections, Quick Access Toolbar, Format Background and Format Shape depth, rich-text clipboard; duplicated rows and headers folded; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)+> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-26 · **Change:** minor (Format Shape Line: every dash type, separate arrowhead width and length; previous: trivial, Edit Points: Shift, Ctrl/Cmd or Alt/Option on a handle drag sets the point type; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: major, community PR wave 2026-10-14: 14 new dialogs (41 in all), Eyedropper, Export button, New Window, Grid Options, WordArt gallery and handles, Slide Sorter sections, Quick Access Toolbar, Format Background and Format Shape depth, rich-text clipboard; duplicated rows and headers folded; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (group fill and outline edits reach members, #1079; scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) How DeckCraft looks and feels next to PowerPoint: the shell, the ribbon, menus, panes, dialogs,@@ -65,7 +65,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 (solid transparency follows the selection), gradient editor, picture/texture fill (source, tile, offsets, scale, alignment, mirror, transparency, rotate with shape), pattern fill (gallery of the 48 presets PowerPoint offers, foreground and background colors), line (dash, compound, cap, join, begin/end arrows and sizes), shadow/glow/reflection details (colour, transparency, blur, angle, distance, size), soft edges, 3-D format and rotation (bevels, depth, contour, material, lighting, camera presets, X/Y/Z rotation), size (lock aspect ratio) |+| Format Shape / Picture / Text pane (Fill & Line, Effects, Size & Properties, Picture, Text Box) | full | partial: fill (solid transparency follows the selection), gradient editor, picture/texture fill (source, tile, offsets, scale, alignment, mirror, transparency, rotate with shape), pattern fill (gallery of the 48 presets PowerPoint offers, foreground and background colors), line (dash type with every PowerPoint dash style, compound, cap, join, begin/end arrows with separate width and length), shadow/glow/reflection details (colour, transparency, blur, angle, distance, size), soft edges, 3-D format and rotation (bevels, depth, contour, material, lighting, camera presets, X/Y/Z rotation), size (lock aspect ratio) | | Slide Master | Insert Layout, Rename, Delete, Preserve, Insert Placeholder, Title and Footers | Insert Layout, Rename, Delete, Insert Placeholder, and Preserve, Title and Footers checkboxes (`master.preserve`, `master.placeholders`). A master thumbnail selects the master | | Format Background pane | yes | yes: solid transparency; picture transparency, tile, offset, scale, alignment and mirror; Apply to All copies a picture. Stretch insets and rotate-with-shape are command parameters | | 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 |@@ -172,6 +172,7 @@ consistency is open (#73, #56). | 2026-10-24 | trivial | Table cells: dragging across cells selects a range of cells | | 2026-10-24 | trivial | Edit Points: Shift, Ctrl/Cmd or Alt/Option held while dragging a handle makes the point smooth, straight or corner | | 2026-10-24 | trivial | Columns dialog: the number of text columns and the spacing between them. Dialog count 41 → 42 |+| 2026-10-26 | minor | Format Shape Line: the Dash type list offers every dash style PowerPoint has (Round Dot and Square Dot are the dot dash with a round or square cap, as it saves them), and each arrowhead has separate Width and Length sizes (`shape.line` headWidth, headLength, tailWidth, tailLength) | | 2026-10-23 | trivial | Scroll bars on the zoomed slide pane (#1157) | | 2026-10-23 | minor | Shape Fill and Shape Outline (Home and Shape Format): the button applies the last colour picked from its menu, Accent 1 and Text 1 until one is picked. Line weight and dashes are not changed by the button; Font Color and Highlight stay as they were (#1080) | | 2026-10-22 | trivial | Group fill and outline commands reach members, including nested groups; selection and single-step undo are preserved (#1079). The existing Arrange / Group checklist score stays D |