diff --git a/docs/astro/src/content/docs/reference/elements/path.mdx b/docs/astro/src/content/docs/reference/elements/path.mdx
index e6fcf693b80..d701bc0fbb0 100644
--- a/docs/astro/src/content/docs/reference/elements/path.mdx
+++ b/docs/astro/src/content/docs/reference/elements/path.mdx
@@ -42,6 +42,11 @@ The color for drawing the outline of the path.
The width of the outline.
+### stroke-line-cap
+
+The appearance of the ends of the path's outline.
+
+
### width
If non-zero, the path will be scaled to fit into the specified width.
diff --git a/internal/backends/qt/qt_window.rs b/internal/backends/qt/qt_window.rs
index 1ad54962aeb..e929f84563d 100644
--- a/internal/backends/qt/qt_window.rs
+++ b/internal/backends/qt/qt_window.rs
@@ -17,8 +17,9 @@ use i_slint_core::item_rendering::{
};
use i_slint_core::item_tree::{ItemTreeRc, ItemTreeRef};
use i_slint_core::items::{
- self, ColorScheme, FillRule, ImageRendering, ItemRc, ItemRef, Layer, MouseCursor, Opacity,
- PointerEventButton, PopupClosePolicy, RenderingResult, TextOverflow, TextStrokeStyle, TextWrap,
+ self, ColorScheme, FillRule, ImageRendering, ItemRc, ItemRef, Layer, LineCap, MouseCursor,
+ Opacity, PointerEventButton, PopupClosePolicy, RenderingResult, TextOverflow, TextStrokeStyle,
+ TextWrap,
};
use i_slint_core::layout::Orientation;
use i_slint_core::lengths::{
@@ -989,6 +990,11 @@ impl ItemRenderer for QtItemRenderer<'_> {
let fill_brush: qttypes::QBrush = into_qbrush(path.fill(), rect.width, rect.height);
let stroke_brush: qttypes::QBrush = into_qbrush(path.stroke(), rect.width, rect.height);
let stroke_width: f32 = path.stroke_width().get();
+ let stroke_pen_cap_style: i32 = match path.stroke_line_cap() {
+ LineCap::Butt => 0x00,
+ LineCap::Round => 0x20,
+ LineCap::Square => 0x10,
+ };
let pos = qttypes::QPoint { x: offset.x as _, y: offset.y as _ };
let mut painter_path = QPainterPath::default();
@@ -1034,11 +1040,12 @@ impl ItemRenderer for QtItemRenderer<'_> {
fill_brush as "QBrush",
stroke_brush as "QBrush",
stroke_width as "float",
+ stroke_pen_cap_style as "int",
anti_alias as "bool"] {
(*painter)->save();
auto cleanup = qScopeGuard([&] { (*painter)->restore(); });
(*painter)->translate(pos);
- (*painter)->setPen(stroke_width > 0 ? QPen(stroke_brush, stroke_width) : Qt::NoPen);
+ (*painter)->setPen(stroke_width > 0 ? QPen(stroke_brush, stroke_width, Qt::SolidLine, Qt::PenCapStyle(stroke_pen_cap_style)) : Qt::NoPen);
(*painter)->setBrush(fill_brush);
(*painter)->setRenderHint(QPainter::Antialiasing, anti_alias);
(*painter)->drawPath(painter_path);
diff --git a/internal/common/enums.rs b/internal/common/enums.rs
index a6dcc496882..5fda9d96009 100644
--- a/internal/common/enums.rs
+++ b/internal/common/enums.rs
@@ -447,6 +447,16 @@ macro_rules! for_each_enums {
/// Does not close the `PopupWindow` automatically when user clicks.
NoAutoClose,
}
+
+ /// This enum describes the appearance of the ends of stroked paths.
+ enum LineCap {
+ /// The stroke ends with a flat edge that is perpendicular to the path.
+ Butt,
+ /// The stroke ends with a rounded edge.
+ Round,
+ /// The stroke ends with a square projection beyond the path.
+ Square,
+ }
];
};
}
diff --git a/internal/compiler/builtins.slint b/internal/compiler/builtins.slint
index e6efa702cb4..daa45945f2e 100644
--- a/internal/compiler/builtins.slint
+++ b/internal/compiler/builtins.slint
@@ -400,6 +400,7 @@ export component Path {
in property fill-rule;
in property stroke;
in property stroke-width;
+ in property stroke-line-cap;
in property commands; // 'fake' hardcoded in typeregister.rs
in property viewbox-x;
in property viewbox-y;
diff --git a/internal/core/items/path.rs b/internal/core/items/path.rs
index 5add282bb6d..d303e95a4e9 100644
--- a/internal/core/items/path.rs
+++ b/internal/core/items/path.rs
@@ -8,7 +8,7 @@ When adding an item or a property, it needs to be kept in sync with different pl
Lookup the [`crate::items`] module documentation.
*/
-use super::{FillRule, Item, ItemConsts, ItemRc, ItemRendererRef, RenderingResult};
+use super::{FillRule, Item, ItemConsts, ItemRc, ItemRendererRef, LineCap, RenderingResult};
use crate::graphics::{Brush, PathData, PathDataIterator};
use crate::input::{
FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, KeyEvent,
@@ -40,6 +40,7 @@ pub struct Path {
pub fill_rule: Property,
pub stroke: Property,
pub stroke_width: Property,
+ pub stroke_line_cap: Property,
pub viewbox_x: Property,
pub viewbox_y: Property,
pub viewbox_width: Property,
diff --git a/internal/renderers/femtovg/itemrenderer.rs b/internal/renderers/femtovg/itemrenderer.rs
index b0e5145ad16..2c992766eef 100644
--- a/internal/renderers/femtovg/itemrenderer.rs
+++ b/internal/renderers/femtovg/itemrenderer.rs
@@ -637,6 +637,11 @@ impl<'a> ItemRenderer for GLItemRenderer<'a> {
let border_paint = self.brush_to_paint(path.stroke(), &femtovg_path).map(|mut paint| {
paint.set_line_width((path.stroke_width() * self.scale_factor).get());
+ paint.set_line_cap(match path.stroke_line_cap() {
+ items::LineCap::Butt => femtovg::LineCap::Butt,
+ items::LineCap::Round => femtovg::LineCap::Round,
+ items::LineCap::Square => femtovg::LineCap::Square,
+ });
paint.set_anti_alias(anti_alias);
paint
});
diff --git a/internal/renderers/skia/itemrenderer.rs b/internal/renderers/skia/itemrenderer.rs
index 54778e00861..10cecf43419 100644
--- a/internal/renderers/skia/itemrenderer.rs
+++ b/internal/renderers/skia/itemrenderer.rs
@@ -740,6 +740,11 @@ impl<'a> ItemRenderer for SkiaItemRenderer<'a> {
{
border_paint.set_anti_alias(anti_alias);
border_paint.set_stroke_width((path.stroke_width() * self.scale_factor).get());
+ border_paint.set_stroke_cap(match path.stroke_line_cap() {
+ i_slint_core::items::LineCap::Butt => skia_safe::PaintCap::Butt,
+ i_slint_core::items::LineCap::Round => skia_safe::PaintCap::Round,
+ i_slint_core::items::LineCap::Square => skia_safe::PaintCap::Square,
+ });
border_paint.set_stroke(true);
self.canvas.draw_path(&skpath, &border_paint);
}
diff --git a/tests/manual/path-stroke-cap.slint b/tests/manual/path-stroke-cap.slint
new file mode 100644
index 00000000000..e9dda44519d
--- /dev/null
+++ b/tests/manual/path-stroke-cap.slint
@@ -0,0 +1,68 @@
+// Copyright © SixtyFPS GmbH
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+import { Palette } from "std-widgets.slint";
+
+component Line {
+ in property stroke-line-cap <=> p.stroke-line-cap;
+
+ p := Path {
+ viewbox-width: 6;
+ viewbox-height: 6;
+
+ stroke: Palette.foreground;
+ stroke-width: 20px;
+
+ MoveTo {
+ x: 1;
+ y: 1;
+ }
+
+ LineTo {
+ x: 5;
+ y: 1;
+ }
+ }
+}
+
+export component TestCase inherits Window {
+ preferred-width: 300px;
+ preferred-height: 300px;
+
+ Line {
+ x: 10px;
+ y: 10px;
+ width: parent.width - 20px;
+ height: 50px;
+
+ stroke-line-cap: butt;
+ }
+
+ Line {
+ x: 10px;
+ y: 50px;
+ width: parent.width - 20px;
+ height: 50px;
+
+ stroke-line-cap: round;
+ }
+
+ Line {
+ x: 10px;
+ y: 100px;
+ width: parent.width - 20px;
+ height: 50px;
+
+ stroke-line-cap: square;
+ }
+
+ /*
+
+
+
+
+
+
+
+*/
+}