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; + } + + /* + + + + + + + +*/ +}