mirror of
https://github.com/penpot/penpot.git
synced 2026-05-22 16:33:55 +00:00
🎉 Add dash and gap inputs for dashed strokes (#9765)
* ✨ Add dash and gap customization for dashed strokes Signed-off-by: eureka0928 <meobius123@gmail.com> * ♻️ Change old numeric-inputs for new components --------- Signed-off-by: eureka0928 <meobius123@gmail.com> Signed-off-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: eureka0928 <meobius123@gmail.com> Co-authored-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: Alejandro Alonso <alejandro.alonso@kaleidos.net>
This commit is contained in:
parent
05fa8af479
commit
2c453e4a00
@ -137,6 +137,8 @@
|
||||
[:stroke-style {:optional true}
|
||||
[::sm/one-of #{:solid :dotted :dashed :mixed}]]
|
||||
[:stroke-width {:optional true} ::sm/safe-number]
|
||||
[:stroke-dash {:optional true} ::sm/safe-number]
|
||||
[:stroke-gap {:optional true} ::sm/safe-number]
|
||||
[:stroke-alignment {:optional true}
|
||||
[::sm/one-of #{:center :inner :outer}]]
|
||||
[:stroke-cap-start {:optional true}
|
||||
|
||||
@ -45,6 +45,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
@ -151,6 +153,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
@ -206,6 +210,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
@ -261,6 +267,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
@ -315,6 +323,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
@ -433,6 +443,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
@ -488,6 +500,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
|
||||
@ -19,14 +19,14 @@
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- calculate-dasharray
|
||||
[style width]
|
||||
[style width dash gap]
|
||||
(let [w+5 (+ 5 width)
|
||||
w+1 (+ 1 width)
|
||||
w+10 (+ 10 width)]
|
||||
(case style
|
||||
:mixed (str/concat "" w+5 "," w+5 "," w+1 "," w+5)
|
||||
:dotted (str/concat "0," w+5)
|
||||
:dashed (str/concat "" w+10 "," w+10)
|
||||
:dashed (str/concat "" (or dash w+10) "," (or gap w+10))
|
||||
"")))
|
||||
|
||||
(defn get-border-props
|
||||
@ -97,6 +97,8 @@
|
||||
(let [style (:stroke-style data :solid)]
|
||||
(when-not (= style :none)
|
||||
(let [width (:stroke-width data 1)
|
||||
dash (:stroke-dash data)
|
||||
gap (:stroke-gap data)
|
||||
gradient (:stroke-color-gradient data)
|
||||
color (:stroke-color data)
|
||||
opacity (:stroke-opacity data)]
|
||||
@ -114,7 +116,7 @@
|
||||
(obj/set! attrs "strokeOpacity" opacity))
|
||||
|
||||
(when (not= style :svg)
|
||||
(obj/set! attrs "strokeDasharray" (calculate-dasharray style width)))
|
||||
(obj/set! attrs "strokeDasharray" (calculate-dasharray style width dash gap)))
|
||||
|
||||
;; For simple line caps we use svg stroke-line-cap attribute. This
|
||||
;; only works if all caps are the same and we are not using the tricks
|
||||
|
||||
@ -333,6 +333,8 @@
|
||||
:penpot:stroke-opacity (d/name (:stroke-opacity stroke))
|
||||
:penpot:stroke-style (d/name (:stroke-style stroke))
|
||||
:penpot:stroke-width (d/name (:stroke-width stroke))
|
||||
:penpot:stroke-dash ((d/nilf str) (:stroke-dash stroke))
|
||||
:penpot:stroke-gap ((d/nilf str) (:stroke-gap stroke))
|
||||
:penpot:stroke-alignment (d/name (:stroke-alignment stroke))
|
||||
:penpot:stroke-cap-start (d/name (:stroke-cap-start stroke))
|
||||
:penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}]))]))))
|
||||
|
||||
@ -30,6 +30,8 @@
|
||||
:stroke-style
|
||||
:stroke-alignment
|
||||
:stroke-width
|
||||
:stroke-dash
|
||||
:stroke-gap
|
||||
:stroke-color
|
||||
:stroke-color-ref-id
|
||||
:stroke-color-ref-file
|
||||
@ -113,6 +115,18 @@
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(st/emit! (dc/change-stroke-attrs ids {:stroke-width value} index))))
|
||||
|
||||
on-stroke-dash-change
|
||||
(fn [index value]
|
||||
(when-not (str/empty? value)
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(st/emit! (dc/change-stroke-attrs ids {:stroke-dash value} index))))
|
||||
|
||||
on-stroke-gap-change
|
||||
(fn [index value]
|
||||
(when-not (str/empty? value)
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(st/emit! (dc/change-stroke-attrs ids {:stroke-gap value} index))))
|
||||
|
||||
open-caps-select
|
||||
(fn [caps-state]
|
||||
(fn [event]
|
||||
@ -226,6 +240,8 @@
|
||||
:on-color-change on-color-change
|
||||
:on-color-detach on-color-detach
|
||||
:on-stroke-width-change on-stroke-width-change
|
||||
:on-stroke-dash-change on-stroke-dash-change
|
||||
:on-stroke-gap-change on-stroke-gap-change
|
||||
:on-stroke-style-change on-stroke-style-change
|
||||
:on-stroke-alignment-change on-stroke-alignment-change
|
||||
:open-caps-select open-caps-select
|
||||
|
||||
@ -35,6 +35,8 @@
|
||||
on-color-detach
|
||||
on-remove
|
||||
on-stroke-width-change
|
||||
on-stroke-dash-change
|
||||
on-stroke-gap-change
|
||||
on-stroke-style-change
|
||||
on-stroke-alignment-change
|
||||
on-stroke-cap-start-change
|
||||
@ -106,6 +108,23 @@
|
||||
ids
|
||||
#{:stroke-width})))
|
||||
|
||||
;; The SVG renderer defaults dash and gap to `stroke-width + 10` when
|
||||
;; unset. Showing that value as placeholder makes the override obvious.
|
||||
default-dash-gap (when (number? stroke-width) (+ 10 stroke-width))
|
||||
|
||||
stroke-gap (or (:stroke-gap stroke) default-dash-gap)
|
||||
stroke-dash (or (:stroke-dash stroke) default-dash-gap)
|
||||
|
||||
on-dash-change
|
||||
(mf/use-fn
|
||||
(mf/deps index on-stroke-dash-change)
|
||||
#(on-stroke-dash-change index %))
|
||||
|
||||
on-gap-change
|
||||
(mf/use-fn
|
||||
(mf/deps index on-stroke-gap-change)
|
||||
#(on-stroke-gap-change index %))
|
||||
|
||||
stroke-alignment (or (:stroke-alignment stroke) :center)
|
||||
|
||||
stroke-alignment-options
|
||||
@ -294,6 +313,30 @@
|
||||
:disabled hidden?
|
||||
:on-change on-style-change}]])])
|
||||
|
||||
;; Stroke Dash / Gap (only visible for dashed style)
|
||||
(when (= stroke-style :dashed)
|
||||
[:div {:class (stl/css :stroke-dash-options)
|
||||
:data-testid "stroke.dash-options"}
|
||||
[:> numeric-input-wrapper* {:on-change on-dash-change
|
||||
:text-icon "DASH"
|
||||
:min 0
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
:attr :stroke-dash
|
||||
:class (stl/css :numeric-input-wrapper)
|
||||
:property (tr "workspace.options.stroke-dash")
|
||||
:value stroke-dash}]
|
||||
[:> numeric-input-wrapper* {:on-change on-gap-change
|
||||
:text-icon "GAP"
|
||||
:min 0
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
:attr :stroke-gap
|
||||
:tooltip-placement "top-left"
|
||||
:class (stl/css :numeric-input-wrapper)
|
||||
:property (tr "workspace.options.stroke-gap")
|
||||
:value stroke-gap}]])
|
||||
|
||||
;; Stroke Caps
|
||||
(when show-caps
|
||||
[:div {:class (stl/css :stroke-caps-options)}
|
||||
|
||||
@ -77,6 +77,12 @@
|
||||
column-gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.stroke-dash-options {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.stroke-options-tokens {
|
||||
@include sidebar.option-grid-structure;
|
||||
|
||||
|
||||
@ -724,13 +724,18 @@
|
||||
style (-> stroke :stroke-style sr/translate-stroke-style)
|
||||
cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap)
|
||||
cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap)
|
||||
;; Sentinel -1 means "unset" on the Rust side — keeps the
|
||||
;; FFI signature flat while letting the renderer fall back
|
||||
;; to its default dash pattern when no override is stored.
|
||||
dash (or (:stroke-dash stroke) -1)
|
||||
gap (or (:stroke-gap stroke) -1)
|
||||
offset (mem/alloc types.fills.impl/FILL-U8-SIZE)
|
||||
heap (mem/get-heap-u8)
|
||||
dview (js/DataView. (.-buffer heap))]
|
||||
(case align
|
||||
:inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end)
|
||||
:outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end)
|
||||
(h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end))
|
||||
:inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end dash gap)
|
||||
:outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end dash gap)
|
||||
(h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end dash gap))
|
||||
|
||||
(cond
|
||||
(some? gradient)
|
||||
|
||||
@ -7560,6 +7560,14 @@ msgstr "Center"
|
||||
msgid "workspace.options.stroke.dashed"
|
||||
msgstr "Dashed"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs
|
||||
msgid "workspace.options.stroke-dash"
|
||||
msgstr "Dash"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs
|
||||
msgid "workspace.options.stroke-gap"
|
||||
msgstr "Gap"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:138
|
||||
msgid "workspace.options.stroke.dotted"
|
||||
msgstr "Dotted"
|
||||
|
||||
@ -41,6 +41,10 @@ pub struct Stroke {
|
||||
pub cap_end: Option<StrokeCap>,
|
||||
pub cap_start: Option<StrokeCap>,
|
||||
pub kind: StrokeKind,
|
||||
// Dash and gap overrides for the `Dashed` style. `None` falls back to the
|
||||
// default `width + 10` pattern to keep existing designs visually identical.
|
||||
pub dash: Option<f32>,
|
||||
pub gap: Option<f32>,
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
@ -72,6 +76,8 @@ impl Stroke {
|
||||
style: StrokeStyle,
|
||||
cap_start: Option<StrokeCap>,
|
||||
cap_end: Option<StrokeCap>,
|
||||
dash: Option<f32>,
|
||||
gap: Option<f32>,
|
||||
) -> Self {
|
||||
Stroke {
|
||||
fill: Fill::Solid(SolidColor(skia::Color::TRANSPARENT)),
|
||||
@ -80,6 +86,8 @@ impl Stroke {
|
||||
cap_end,
|
||||
cap_start,
|
||||
kind: StrokeKind::Center,
|
||||
dash,
|
||||
gap,
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,6 +96,8 @@ impl Stroke {
|
||||
style: StrokeStyle,
|
||||
cap_start: Option<StrokeCap>,
|
||||
cap_end: Option<StrokeCap>,
|
||||
dash: Option<f32>,
|
||||
gap: Option<f32>,
|
||||
) -> Self {
|
||||
Stroke {
|
||||
fill: Fill::Solid(SolidColor(skia::Color::TRANSPARENT)),
|
||||
@ -96,6 +106,8 @@ impl Stroke {
|
||||
cap_end,
|
||||
cap_start,
|
||||
kind: StrokeKind::Inner,
|
||||
dash,
|
||||
gap,
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +116,8 @@ impl Stroke {
|
||||
style: StrokeStyle,
|
||||
cap_start: Option<StrokeCap>,
|
||||
cap_end: Option<StrokeCap>,
|
||||
dash: Option<f32>,
|
||||
gap: Option<f32>,
|
||||
) -> Self {
|
||||
Stroke {
|
||||
fill: Fill::Solid(SolidColor(skia::Color::TRANSPARENT)),
|
||||
@ -112,11 +126,19 @@ impl Stroke {
|
||||
cap_end,
|
||||
cap_start,
|
||||
kind: StrokeKind::Outer,
|
||||
dash,
|
||||
gap,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale_content(&mut self, value: f32) {
|
||||
self.width *= value;
|
||||
if let Some(dash) = self.dash {
|
||||
self.dash = Some(dash * value);
|
||||
}
|
||||
if let Some(gap) = self.gap {
|
||||
self.gap = Some(gap * value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the clip operation for dotted inner/outer strokes.
|
||||
@ -256,7 +278,9 @@ impl Stroke {
|
||||
)
|
||||
}
|
||||
StrokeStyle::Dashed => {
|
||||
skia::PathEffect::dash(&[self.width + 10., self.width + 10.], 0.)
|
||||
let dash = self.dash.unwrap_or(self.width + 10.);
|
||||
let gap = self.gap.unwrap_or(self.width + 10.);
|
||||
skia::PathEffect::dash(&[dash, gap], 0.)
|
||||
}
|
||||
StrokeStyle::Mixed => skia::PathEffect::dash(
|
||||
&[
|
||||
|
||||
@ -68,8 +68,26 @@ impl TryFrom<RawStrokeCap> for StrokeCap {
|
||||
}
|
||||
}
|
||||
|
||||
// A negative value means "unset" — the renderer falls back to its default
|
||||
// dash pattern. We use a sentinel instead of passing a bool because adding
|
||||
// two f32 params keeps the FFI signature flat and allocation-free.
|
||||
fn decode_optional(value: f32) -> Option<f32> {
|
||||
if value.is_finite() && value >= 0.0 {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_center_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) {
|
||||
pub extern "C" fn add_shape_center_stroke(
|
||||
width: f32,
|
||||
style: u8,
|
||||
cap_start: u8,
|
||||
cap_end: u8,
|
||||
dash: f32,
|
||||
gap: f32,
|
||||
) {
|
||||
let stroke_style = RawStrokeStyle::from(style);
|
||||
let cap_start = RawStrokeCap::from(cap_start);
|
||||
let cap_end = RawStrokeCap::from(cap_end);
|
||||
@ -80,12 +98,21 @@ pub extern "C" fn add_shape_center_stroke(width: f32, style: u8, cap_start: u8,
|
||||
stroke_style.into(),
|
||||
cap_start.try_into().ok(),
|
||||
cap_end.try_into().ok(),
|
||||
decode_optional(dash),
|
||||
decode_optional(gap),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_inner_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) {
|
||||
pub extern "C" fn add_shape_inner_stroke(
|
||||
width: f32,
|
||||
style: u8,
|
||||
cap_start: u8,
|
||||
cap_end: u8,
|
||||
dash: f32,
|
||||
gap: f32,
|
||||
) {
|
||||
let stroke_style = RawStrokeStyle::from(style);
|
||||
let cap_start = RawStrokeCap::from(cap_start);
|
||||
let cap_end = RawStrokeCap::from(cap_end);
|
||||
@ -96,12 +123,21 @@ pub extern "C" fn add_shape_inner_stroke(width: f32, style: u8, cap_start: u8, c
|
||||
stroke_style.into(),
|
||||
cap_start.try_into().ok(),
|
||||
cap_end.try_into().ok(),
|
||||
decode_optional(dash),
|
||||
decode_optional(gap),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_outer_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) {
|
||||
pub extern "C" fn add_shape_outer_stroke(
|
||||
width: f32,
|
||||
style: u8,
|
||||
cap_start: u8,
|
||||
cap_end: u8,
|
||||
dash: f32,
|
||||
gap: f32,
|
||||
) {
|
||||
let stroke_style = RawStrokeStyle::from(style);
|
||||
let cap_start = RawStrokeCap::from(cap_start);
|
||||
let cap_end = RawStrokeCap::from(cap_end);
|
||||
@ -112,6 +148,8 @@ pub extern "C" fn add_shape_outer_stroke(width: f32, style: u8, cap_start: u8, c
|
||||
stroke_style.into(),
|
||||
cap_start.try_into().ok(),
|
||||
cap_end.try_into().ok(),
|
||||
decode_optional(dash),
|
||||
decode_optional(gap),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user