diff --git a/.claude/skills/craft-language/references/nodes.md b/.claude/skills/craft-language/references/nodes.md
index ecd2e53..31107ab 100644
--- a/.claude/skills/craft-language/references/nodes.md
+++ b/.claude/skills/craft-language/references/nodes.md
@@ -76,6 +76,7 @@ background/border.
| `background_color` | color | fully transparent |
| `border_color` | color | fully transparent |
| `border_width` | float | `1.0` (invisible unless `border_color` has alpha) |
+| `border_style` | enum: `solid`, `dashed` | `solid` |
| `width`, `height` | float | `0` = shrink-to-fit / use incoming width, not literally zero |
| `padding` | float (common) | `10.0`, but collapses to `0` if the rectangle paints neither fill nor stroke |
@@ -146,6 +147,7 @@ Rectangle/Text).
| Attribute | Type | Notes |
|---|---|---|
| `background_color`, `border_color`, `border_width` | color/color/float | shared shape style |
+| `border_style` | enum: `solid`, `dashed` | default `solid`; applies to the arc too, when present |
| `radius` | float | true circle; mutually exclusive with `width`/`height` |
| `width`, `height` | float | inscribes an ellipse in that bounding box; both required together |
| `start_x`, `start_y`, `finish_x`, `finish_y` | float | arc endpoints, all four or none |
@@ -171,6 +173,7 @@ direction — swapping them picks the complementary arc.
| Attribute | Type | Notes |
|---|---|---|
| `background_color`, `border_color`, `border_width` | color/color/float | |
+| `border_style` | enum: `solid`, `dashed` | default `solid` |
| `points` | string, space-separated `x,y` tokens | **must be exactly 3 points**, else throws |
`points="0,0 50,0 25,50"`. Y-down offsets from the node's own frame origin.
@@ -191,6 +194,7 @@ shape node — no `background_color` (fill) at all.
| `points` | space-separated `x,y` tokens | **minimum 2 points**, else throws |
| `border_color` | color | black |
| `border_width` | float | `1.0` |
+| `border_style` | enum: `solid`, `dashed` | `solid` |
```xml
@@ -204,6 +208,7 @@ shape node — no `background_color` (fill) at all.
| `x2`, `y2` | float | `(100, 0)` |
| `border_color` | color | black |
| `border_width` | float | `1.0` |
+| `border_style` | enum: `solid`, `dashed` | `solid` |
## `` / ``
diff --git a/doc/source/craft_language/shapes.rst b/doc/source/craft_language/shapes.rst
index 1ff374c..aa6b232 100644
--- a/doc/source/craft_language/shapes.rst
+++ b/doc/source/craft_language/shapes.rst
@@ -2,7 +2,18 @@ Shapes
======
Docraft supports several geometric shape elements. All shapes share
-``background_color``, ``border_color``, and ``border_width`` attributes.
+``background_color``, ``border_color``, ``border_width``, and ``border_style``
+attributes. ```` and ```` are stroke-only (no ``background_color``) but
+share the same ``border_color``/``border_width``/``border_style`` trio.
+
+``border_style`` accepts ``solid`` (the default) or ``dashed``, and applies to every
+shape's border as well as to ````/````:
+
+.. code-block:: xml
+
+
+
Rectangle
---------
@@ -33,6 +44,9 @@ Rectangle
* - ``border_width``
- float
- Stroke width in points (default ``1``).
+ * - ``border_style``
+ - ``solid`` | ``dashed``
+ - Stroke pattern (default ``solid``).
Circle
------
@@ -76,6 +90,9 @@ the center is ``(x + width/2, y + height/2)``.
* - ``border_width``
- float
- Stroke width in points.
+ * - ``border_style``
+ - ``solid`` | ``dashed``
+ - Stroke pattern (default ``solid``). Applies to the arc too, when present.
The two sizing methods are exclusive on purpose, and every violation is a parse error
rather than a silently misdrawn shape:
@@ -158,6 +175,9 @@ Triangle
* - ``border_width``
- float
- Stroke width in points.
+ * - ``border_style``
+ - ``solid`` | ``dashed``
+ - Stroke pattern (default ``solid``).
Line
----
@@ -201,6 +221,9 @@ they are clipped at its bounds.
* - ``border_width``
- float
- Stroke width in points.
+ * - ``border_style``
+ - ``solid`` | ``dashed``
+ - Stroke pattern (default ``solid``).
.. _craft-canvas:
@@ -259,6 +282,9 @@ curved counterpart of ````, and what a spline chart's series line is made
* - ``border_width``
- float
- Stroke width in points.
+ * - ``border_style``
+ - ``solid`` | ``dashed``
+ - Stroke pattern (default ``solid``).
The curve **interpolates**: it passes exactly through every point rather than near
them. With exactly 2 points it degenerates to a straight segment, which is why 2 is a
@@ -304,4 +330,7 @@ an open curve through the same points see :ref:`CurveLine ` abo
* - ``border_width``
- float
- Stroke width in points.
+ * - ``border_style``
+ - ``solid`` | ``dashed``
+ - Stroke pattern (default ``solid``).
diff --git a/docraft/CMakeLists.txt b/docraft/CMakeLists.txt
index 8401922..15f6be0 100644
--- a/docraft/CMakeLists.txt
+++ b/docraft/CMakeLists.txt
@@ -69,6 +69,9 @@ set(DOCRAFT_SOURCES
include/docraft/loom/nodes/docraft_loom_rectangle.h
src/docraft/loom/nodes/docraft_loom_canvas.cc
include/docraft/loom/nodes/docraft_loom_canvas.h
+ include/docraft/loom/nodes/docraft_loom_line_style.h
+ src/docraft/loom/nodes/docraft_loom_stroked_line.cc
+ include/docraft/loom/nodes/docraft_loom_stroked_line.h
include/docraft/loom/nodes/docraft_loom_shape_style.h
src/docraft/loom/nodes/docraft_loom_shape.cc
include/docraft/loom/nodes/docraft_loom_shape.h
@@ -141,6 +144,7 @@ set(DOCRAFT_SOURCES
include/docraft/craft/parser/docraft_craft_enum_parsers.h
src/docraft/craft/parser/docraft_shape_parser_utils.cc
include/docraft/craft/parser/docraft_shape_parser_utils.h
+ include/docraft/craft/parser/docraft_parsed_line_style.h
include/docraft/craft/parser/docraft_parser.h
src/docraft/craft/parser/docraft_rectangle_parser.cc
src/docraft/craft/parser/docraft_chart_parser.cc
diff --git a/docraft/include/docraft/backend/docraft_line_rendering_backend.h b/docraft/include/docraft/backend/docraft_line_rendering_backend.h
index bd3f764..ebf1ca2 100644
--- a/docraft/include/docraft/backend/docraft_line_rendering_backend.h
+++ b/docraft/include/docraft/backend/docraft_line_rendering_backend.h
@@ -46,6 +46,14 @@ namespace docraft::backend {
*/
virtual void set_line_width(float thickness) const = 0;
+ /**
+ * @brief Sets the dash pattern used for subsequent line/curve stroking:
+ * alternating on/off segment lengths in points, starting with an "on" segment.
+ * An empty pattern resets to a solid (continuous) line.
+ * @param pattern The on/off segment lengths, in points.
+ */
+ virtual void set_line_dash_pattern(const std::vector& pattern) const = 0;
+
/**
* @brief Draws a line between two points using the current stroke settings.
* @param x1 The x-coordinate of the line start.
diff --git a/docraft/include/docraft/backend/pdf/docraft_haru_line_backend.h b/docraft/include/docraft/backend/pdf/docraft_haru_line_backend.h
index 8a1038a..a31b8cd 100644
--- a/docraft/include/docraft/backend/pdf/docraft_haru_line_backend.h
+++ b/docraft/include/docraft/backend/pdf/docraft_haru_line_backend.h
@@ -45,6 +45,12 @@ namespace docraft::backend::pdf {
*/
void set_line_width(float thickness) const override;
+ /**
+ * @brief Sets the dash pattern used for line/curve stroking via libharu's
+ * HPDF_Page_SetDash. An empty pattern resets to a solid line (num_dash=0).
+ */
+ void set_line_dash_pattern(const std::vector& pattern) const override;
+
/**
* @brief Draws a line segment between two points.
*/
diff --git a/docraft/include/docraft/craft/docraft_craft_language_tokens.h b/docraft/include/docraft/craft/docraft_craft_language_tokens.h
index d262863..1cabbb6 100644
--- a/docraft/include/docraft/craft/docraft_craft_language_tokens.h
+++ b/docraft/include/docraft/craft/docraft_craft_language_tokens.h
@@ -251,12 +251,14 @@ namespace docraft::craft {
constexpr string kBackgroundColor = "background_color";
constexpr string kBorderColor = "border_color";
constexpr string kBorderWidth = "border_width";
+ constexpr string kBorderStyle = "border_style";
}
namespace circle::attribute {
constexpr string kBackgroundColor = "background_color";
constexpr string kBorderColor = "border_color";
constexpr string kBorderWidth = "border_width";
+ constexpr string kBorderStyle = "border_style";
constexpr string kRadius = "radius";
// Arc endpoints: given as points, all four together or none.
constexpr string kStartX = "start_x";
@@ -268,6 +270,7 @@ namespace docraft::craft {
constexpr string kBackgroundColor = "background_color";
constexpr string kBorderColor = "border_color";
constexpr string kBorderWidth = "border_width";
+ constexpr string kBorderStyle = "border_style";
constexpr string kPoints = "points";
}
namespace line::attribute {
@@ -277,17 +280,20 @@ namespace docraft::craft {
constexpr string kY2 = "y2";
constexpr string kBorderColor = "border_color";
constexpr string kBorderWidth = "border_width";
+ constexpr string kBorderStyle = "border_style";
}
namespace polygon::attribute {
constexpr string kBackgroundColor = "background_color";
constexpr string kBorderColor = "border_color";
constexpr string kBorderWidth = "border_width";
+ constexpr string kBorderStyle = "border_style";
constexpr string kPoints = "points";
}
// No background_color: an open curve has no interior to fill.
namespace curve_line::attribute {
constexpr string kBorderColor = "border_color";
constexpr string kBorderWidth = "border_width";
+ constexpr string kBorderStyle = "border_style";
constexpr string kPoints = "points";
}
@@ -361,4 +367,12 @@ namespace docraft::craft {
constexpr string kVertical = "vertical";
}
+ /**
+ * @brief `border_style` string literals for line-like elements and shape borders.
+ */
+ namespace line_style {
+ constexpr string kSolid = "solid";
+ constexpr string kDashed = "dashed";
+ }
+
}
diff --git a/docraft/include/docraft/craft/parser/docraft_circle_parser.h b/docraft/include/docraft/craft/parser/docraft_circle_parser.h
index 96880d6..3e156fe 100644
--- a/docraft/include/docraft/craft/parser/docraft_circle_parser.h
+++ b/docraft/include/docraft/craft/parser/docraft_circle_parser.h
@@ -22,6 +22,7 @@
#include "docraft/docraft_lib.h"
#include "docraft/craft/i_docraft_parser.h"
+#include "docraft/craft/parser/docraft_parsed_line_style.h"
namespace docraft::craft::parser {
/**
@@ -31,11 +32,8 @@ namespace docraft::craft::parser {
* used a width/height bounding box); `docraft::loom::nodes::DocraftLoomCircle` only
* ever has a single radius.
*/
- struct ParsedCircleData
+ struct ParsedCircleData : ParsedShapeStyleData
{
- std::optional background_color;
- std::optional border_color;
- std::optional border_width;
std::optional radius;
/**
* @brief Arc endpoints, as points in the node's own box. All four are required
diff --git a/docraft/include/docraft/craft/parser/docraft_curve_line_parser.h b/docraft/include/docraft/craft/parser/docraft_curve_line_parser.h
index 72967c1..1656405 100644
--- a/docraft/include/docraft/craft/parser/docraft_curve_line_parser.h
+++ b/docraft/include/docraft/craft/parser/docraft_curve_line_parser.h
@@ -24,6 +24,7 @@
#include "docraft/docraft_lib.h"
#include "docraft/craft/i_docraft_parser.h"
+#include "docraft/craft/parser/docraft_parsed_line_style.h"
#include "docraft/docraft_position.h"
namespace docraft::craft::parser {
@@ -34,11 +35,9 @@ namespace docraft::craft::parser {
* `` this element is stroke-only by construction rather than by
* convention.
*/
- struct ParsedCurveLineData
+ struct ParsedCurveLineData : ParsedStrokeStyleData
{
std::vector points;
- std::optional border_color;
- std::optional border_width;
};
/**
diff --git a/docraft/include/docraft/craft/parser/docraft_line_parser.h b/docraft/include/docraft/craft/parser/docraft_line_parser.h
index ff9faa8..d911cce 100644
--- a/docraft/include/docraft/craft/parser/docraft_line_parser.h
+++ b/docraft/include/docraft/craft/parser/docraft_line_parser.h
@@ -22,19 +22,18 @@
#include "docraft/docraft_lib.h"
#include "docraft/craft/i_docraft_parser.h"
+#include "docraft/craft/parser/docraft_parsed_line_style.h"
namespace docraft::craft::parser {
/**
* @brief Tag-specific payload parsed from a `` element.
*/
- struct ParsedLineData
+ struct ParsedLineData : ParsedStrokeStyleData
{
std::optional x1;
std::optional y1;
std::optional x2;
std::optional y2;
- std::optional border_color;
- std::optional border_width;
};
/**
diff --git a/docraft/include/docraft/craft/parser/docraft_parsed_line_style.h b/docraft/include/docraft/craft/parser/docraft_parsed_line_style.h
new file mode 100644
index 0000000..ed26a4b
--- /dev/null
+++ b/docraft/include/docraft/craft/parser/docraft_parsed_line_style.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2026 Matteo Cadoni (https://github.com/cadons)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace docraft::craft::parser {
+ /**
+ * @brief Stroke style for line-like elements (``/``) and shape
+ * borders (``/``/``/``), duplicated from
+ * (rather than shared with) loom's own `docraft::loom::nodes::DocraftLineStyle` --
+ * see `ParsedTextStyle` for the same "duplicate a tiny enum and translate"
+ * rationale.
+ */
+ enum class ParsedLineStyle
+ {
+ kSolid,
+ kDashed
+ };
+
+ /**
+ * @brief Border styling shared by every stroke-only, line-like element
+ * (``/``). Base struct rather than duplicating the same three
+ * fields on each `Parsed*Data`.
+ */
+ struct ParsedStrokeStyleData
+ {
+ std::optional border_color;
+ std::optional border_width;
+ std::optional border_style;
+ };
+
+ /**
+ * @brief Background+border styling shared by every fillable shape element
+ * (``/``/``/``). Extends
+ * `ParsedStrokeStyleData` with the fill color those elements additionally support.
+ */
+ struct ParsedShapeStyleData : ParsedStrokeStyleData
+ {
+ std::optional background_color;
+ };
+} // namespace docraft::craft::parser
diff --git a/docraft/include/docraft/craft/parser/docraft_parser.h b/docraft/include/docraft/craft/parser/docraft_parser.h
index 8fc3293..3cf7a19 100644
--- a/docraft/include/docraft/craft/parser/docraft_parser.h
+++ b/docraft/include/docraft/craft/parser/docraft_parser.h
@@ -25,16 +25,14 @@
#include "docraft/craft/docraft_craft_parsed_element.h"
#include "docraft/craft/i_docraft_parser.h"
+#include "docraft/craft/parser/docraft_parsed_line_style.h"
namespace docraft::craft::parser {
/**
* @brief Tag-specific payload parsed from a `` element.
*/
- struct ParsedRectangleData
+ struct ParsedRectangleData : ParsedShapeStyleData
{
- std::optional background_color;
- std::optional border_color;
- std::optional border_width;
};
/**
diff --git a/docraft/include/docraft/craft/parser/docraft_parser_helpers.h b/docraft/include/docraft/craft/parser/docraft_parser_helpers.h
index 5e4866e..2e15ae3 100644
--- a/docraft/include/docraft/craft/parser/docraft_parser_helpers.h
+++ b/docraft/include/docraft/craft/parser/docraft_parser_helpers.h
@@ -102,6 +102,16 @@ namespace docraft::craft::parser::detail {
*/
ParsedTextAlignment parse_text_alignment(const std::string& alignment_str);
+ /**
+ * @brief Parses a `border_style` attribute value (e.g. "dashed") into a
+ * `ParsedLineStyle`. Shared by every line/shape-border parser (`Line`/`CurveLine`/
+ * `Rectangle`/`Circle`/`Triangle`/`Polygon`).
+ * @param style_str The raw attribute string.
+ * @return The parsed line style.
+ * @throws docraft::exception::InvalidInputException if the string is not a recognized style.
+ */
+ ParsedLineStyle parse_line_style(const std::string& style_str);
+
/**
* @brief Trims leading/trailing whitespace (space, tab, newline, CR, form feed,
* vertical tab) from a string.
diff --git a/docraft/include/docraft/craft/parser/docraft_polygon_parser.h b/docraft/include/docraft/craft/parser/docraft_polygon_parser.h
index 4577c66..a2d15cc 100644
--- a/docraft/include/docraft/craft/parser/docraft_polygon_parser.h
+++ b/docraft/include/docraft/craft/parser/docraft_polygon_parser.h
@@ -24,16 +24,14 @@
#include "docraft/docraft_position.h"
#include "docraft/craft/i_docraft_parser.h"
+#include "docraft/craft/parser/docraft_parsed_line_style.h"
namespace docraft::craft::parser {
/**
* @brief Tag-specific payload parsed from a `` element.
*/
- struct ParsedPolygonData
+ struct ParsedPolygonData : ParsedShapeStyleData
{
- std::optional background_color;
- std::optional border_color;
- std::optional border_width;
std::vector points;
};
diff --git a/docraft/include/docraft/craft/parser/docraft_triangle_parser.h b/docraft/include/docraft/craft/parser/docraft_triangle_parser.h
index c4459dd..c56c703 100644
--- a/docraft/include/docraft/craft/parser/docraft_triangle_parser.h
+++ b/docraft/include/docraft/craft/parser/docraft_triangle_parser.h
@@ -24,16 +24,14 @@
#include "docraft/docraft_position.h"
#include "docraft/craft/i_docraft_parser.h"
+#include "docraft/craft/parser/docraft_parsed_line_style.h"
namespace docraft::craft::parser {
/**
* @brief Tag-specific payload parsed from a `` element.
*/
- struct ParsedTriangleData
+ struct ParsedTriangleData : ParsedShapeStyleData
{
- std::optional background_color;
- std::optional border_color;
- std::optional border_width;
std::vector points; // exactly 3 when present
};
diff --git a/docraft/include/docraft/loom/craft/docraft_loom_tree_builder_utils.h b/docraft/include/docraft/loom/craft/docraft_loom_tree_builder_utils.h
index 7142031..5f97f7c 100644
--- a/docraft/include/docraft/loom/craft/docraft_loom_tree_builder_utils.h
+++ b/docraft/include/docraft/loom/craft/docraft_loom_tree_builder_utils.h
@@ -22,6 +22,7 @@
#include "docraft/craft/docraft_craft_parsed_element.h"
#include "docraft/craft/parser/docraft_parser.h"
#include "docraft/docraft_lib.h"
+#include "docraft/loom/nodes/docraft_loom_line_style.h"
#include "docraft/loom/nodes/docraft_loom_node.h"
#include "docraft/loom/nodes/docraft_loom_text.h"
@@ -34,6 +35,13 @@ namespace docraft::loom::craft {
DOCRAFT_LIB nodes::TextAlignment to_loom_alignment(docraft::craft::parser::ParsedTextAlignment alignment);
+ /**
+ * @brief Translates the parser's `ParsedLineStyle` into loom's own
+ * `DocraftLineStyle`, for the same "duplicate a tiny enum and translate" reason as
+ * to_loom_alignment().
+ */
+ DOCRAFT_LIB nodes::DocraftLineStyle to_loom_line_style(docraft::craft::parser::ParsedLineStyle style);
+
/**
* @brief Normalizes a JSON string that may use single quotes for strings instead of
* double quotes (the `.craft` convention for embedding JSON in an XML attribute, e.g.
@@ -139,5 +147,15 @@ namespace docraft::loom::craft {
{
node.edit_style().border_width = *data.border_width;
}
+ // Not every ShapeDataT carries a border_style (e.g. Canvas/Section/Chart's parsed
+ // data doesn't) -- if constexpr keeps this a no-op for those instead of a hard
+ // compile error, the same idiom apply_common_attributes() uses for width/height.
+ if constexpr (requires { data.border_style; })
+ {
+ if (data.border_style.has_value())
+ {
+ node.edit_style().border_style = to_loom_line_style(*data.border_style);
+ }
+ }
}
} // namespace docraft::loom::craft
diff --git a/docraft/include/docraft/loom/nodes/docraft_loom_curve_line.h b/docraft/include/docraft/loom/nodes/docraft_loom_curve_line.h
index dd14fc4..4563b2d 100644
--- a/docraft/include/docraft/loom/nodes/docraft_loom_curve_line.h
+++ b/docraft/include/docraft/loom/nodes/docraft_loom_curve_line.h
@@ -1,8 +1,7 @@
#pragma once
#include
-#include "docraft/docraft_color.h"
-#include "docraft/loom/nodes/docraft_loom_node.h"
+#include "docraft/loom/nodes/docraft_loom_stroked_line.h"
namespace docraft::loom::nodes {
/**
@@ -19,23 +18,17 @@ namespace docraft::loom::nodes {
* through `IDocraftLineRenderingBackend::draw_curve()`, which interpolates a uniform
* Catmull-Rom spline: it passes exactly through every point rather than near them.
*/
- class DOCRAFT_LIB DocraftLoomCurveLine : public DocraftLoomNode
+ class DOCRAFT_LIB DocraftLoomCurveLine : public DocraftLoomStrokedLine
{
public:
- DocraftLoomCurveLine();
+ DocraftLoomCurveLine() = default;
~DocraftLoomCurveLine() override = default;
void accept(loom::interfaces::DocraftLoomIVisitor& visitor) override;
const std::vector& points() const;
void set_points(const std::vector& points);
- const DocraftColor& border_color() const;
- void set_border_color(const DocraftColor& color);
- float border_width() const;
- void set_border_width(float width);
private:
std::vector points_;
- DocraftColor border_color_;
- float border_width_ = 1.0F;
};
} // docraft
diff --git a/docraft/include/docraft/loom/nodes/docraft_loom_line.h b/docraft/include/docraft/loom/nodes/docraft_loom_line.h
index 40c1f37..bd1dfd7 100644
--- a/docraft/include/docraft/loom/nodes/docraft_loom_line.h
+++ b/docraft/include/docraft/loom/nodes/docraft_loom_line.h
@@ -1,13 +1,11 @@
#pragma once
-#include "docraft/docraft_color.h"
-#include "docraft/loom/nodes/docraft_loom_node.h"
-#include "docraft/loom/nodes/docraft_loom_node.h"
+#include "docraft/loom/nodes/docraft_loom_stroked_line.h"
namespace docraft::loom::nodes {
- class DOCRAFT_LIB DocraftLoomLine : public DocraftLoomNode
+ class DOCRAFT_LIB DocraftLoomLine : public DocraftLoomStrokedLine
{
public:
- DocraftLoomLine();
+ DocraftLoomLine() = default;
~DocraftLoomLine() override = default;
void accept(loom::interfaces::DocraftLoomIVisitor& visitor) override;
@@ -15,15 +13,9 @@ namespace docraft::loom::nodes {
void set_start(const Position& start);
const Position& end() const;
void set_end(const Position& end);
- const DocraftColor& border_color() const;
- void set_border_color(const DocraftColor& color);
- float border_width() const;
- void set_border_width(float width);
private:
Position start_{.x = 0.0F, .y = 0.0F};
Position end_{.x = 100.0F, .y = 0.0F};
- DocraftColor border_color_;
- float border_width_ = 1.0F;
};
} // docraft
\ No newline at end of file
diff --git a/docraft/include/docraft/loom/nodes/docraft_loom_line_style.h b/docraft/include/docraft/loom/nodes/docraft_loom_line_style.h
new file mode 100644
index 0000000..588406e
--- /dev/null
+++ b/docraft/include/docraft/loom/nodes/docraft_loom_line_style.h
@@ -0,0 +1,13 @@
+#pragma once
+
+namespace docraft::loom::nodes {
+ /**
+ * @brief Stroke pattern for line-like nodes (Line, CurveLine) and shape borders
+ * (via DocraftLoomShapeStyle::border_style).
+ */
+ enum class DocraftLineStyle
+ {
+ kSolid,
+ kDashed
+ };
+} // docraft
diff --git a/docraft/include/docraft/loom/nodes/docraft_loom_shape_style.h b/docraft/include/docraft/loom/nodes/docraft_loom_shape_style.h
index 5c948de..2481c55 100644
--- a/docraft/include/docraft/loom/nodes/docraft_loom_shape_style.h
+++ b/docraft/include/docraft/loom/nodes/docraft_loom_shape_style.h
@@ -1,5 +1,6 @@
#pragma once
#include "docraft/docraft_color.h"
+#include "docraft/loom/nodes/docraft_loom_line_style.h"
namespace docraft::loom::nodes {
/**
@@ -12,5 +13,6 @@ namespace docraft::loom::nodes {
DocraftColor background_color = DocraftColor::fromRGB(0.0F, 0.0F, 0.0F, 0.0F);
DocraftColor border_color = DocraftColor::fromRGB(0.0F, 0.0F, 0.0F, 0.0F);
float border_width = 1.0F;
+ DocraftLineStyle border_style = DocraftLineStyle::kSolid;
};
} // docraft
\ No newline at end of file
diff --git a/docraft/include/docraft/loom/nodes/docraft_loom_stroked_line.h b/docraft/include/docraft/loom/nodes/docraft_loom_stroked_line.h
new file mode 100644
index 0000000..d19e92d
--- /dev/null
+++ b/docraft/include/docraft/loom/nodes/docraft_loom_stroked_line.h
@@ -0,0 +1,34 @@
+#pragma once
+#include "docraft/docraft_color.h"
+#include "docraft/loom/nodes/docraft_loom_line_style.h"
+#include "docraft/loom/nodes/docraft_loom_node.h"
+
+namespace docraft::loom::nodes {
+ /**
+ * @brief Common base for stroke-only line-like nodes (Line, CurveLine), holding the
+ * border color/width/style they share. Composition-over-inheritance doesn't apply
+ * here the way it does for DocraftLoomShapeStyle (a plain member on each shape node):
+ * Line and CurveLine have no other divergent styling to keep separate, so a shared
+ * base -- mirroring DocraftLoomShape's role for fillable shapes -- avoids duplicating
+ * the same three members and six accessors twice. Concrete nodes still implement
+ * accept() themselves; DocraftLoomStrokedLine is never instantiated directly.
+ */
+ class DOCRAFT_LIB DocraftLoomStrokedLine : public DocraftLoomNode
+ {
+ public:
+ DocraftLoomStrokedLine();
+ ~DocraftLoomStrokedLine() override = default;
+
+ const DocraftColor& border_color() const;
+ void set_border_color(const DocraftColor& color);
+ float border_width() const;
+ void set_border_width(float width);
+ DocraftLineStyle border_style() const;
+ void set_border_style(DocraftLineStyle style);
+
+ private:
+ DocraftColor border_color_;
+ float border_width_ = 1.0F;
+ DocraftLineStyle border_style_ = DocraftLineStyle::kSolid;
+ };
+} // docraft
diff --git a/docraft/include/docraft/loom/pipeline/docraft_loom_shape_draw_utils.h b/docraft/include/docraft/loom/pipeline/docraft_loom_shape_draw_utils.h
index ee02421..978c2ee 100644
--- a/docraft/include/docraft/loom/pipeline/docraft_loom_shape_draw_utils.h
+++ b/docraft/include/docraft/loom/pipeline/docraft_loom_shape_draw_utils.h
@@ -55,10 +55,28 @@ namespace docraft::loom::pipeline {
ShapeRenderTarget target;
const DocraftColor &border_color;
float border_width;
+ nodes::DocraftLineStyle border_style = nodes::DocraftLineStyle::kSolid;
nodes::Position origin;
const std::vector &points;
};
+ /**
+ * @brief On/off segment length (points) of a dashed stroke's repeating unit.
+ */
+ static constexpr float kDashSegmentLength = 4.0F;
+
+ /**
+ * @brief Gap length (points) between dashes of a dashed stroke.
+ */
+ static constexpr float kDashGapLength = 2.0F;
+
+ /**
+ * @brief Resolves a DocraftLineStyle to the on/off pattern IDocraftLineRenderingBackend::
+ * set_line_dash_pattern() expects -- empty (solid) for kSolid, {kDashSegmentLength,
+ * kDashGapLength} for kDashed.
+ */
+ static std::vector resolve_dash_pattern(nodes::DocraftLineStyle style);
+
static ShapeDrawFlags resolve_shape_draw_flags(const nodes::DocraftLoomShapeStyle &style);
/**
diff --git a/docraft/src/docraft/backend/pdf/docraft_haru_line_backend.cc b/docraft/src/docraft/backend/pdf/docraft_haru_line_backend.cc
index 117fe3c..ddc528d 100644
--- a/docraft/src/docraft/backend/pdf/docraft_haru_line_backend.cc
+++ b/docraft/src/docraft/backend/pdf/docraft_haru_line_backend.cc
@@ -18,9 +18,42 @@
#include
+#include
+#include
+#include
#include
+#include
namespace docraft::backend::pdf {
+ namespace {
+ // Deduces the element type HPDF_Page_SetDash actually expects for its dash_ptn
+ // parameter (e.g. HPDF_UINT16 on some libharu builds, HPDF_REAL/float on others)
+ // straight from the linked function's signature, so the conversion below adapts
+ // to whichever libharu is linked instead of guessing per platform.
+ template
+ ElemT dash_pattern_element_type(HPDF_STATUS (*)(PageT, const ElemT*, NumT, PhaseT));
+
+ using DashPatternElement = decltype(dash_pattern_element_type(&HPDF_Page_SetDash));
+
+ void invoke_set_dash(HPDF_Page page, const std::vector& pattern) {
+ std::vector dash_ptn;
+ dash_ptn.reserve(pattern.size());
+ for (float segment : pattern) {
+ const float clamped = std::max(0.0F, segment);
+ if constexpr (std::is_integral_v) {
+ dash_ptn.push_back(static_cast(std::round(clamped)));
+ } else {
+ dash_ptn.push_back(static_cast(clamped));
+ }
+ }
+ if (dash_ptn.empty()) {
+ HPDF_Page_SetDash(page, nullptr, 0, 0.0F);
+ } else {
+ HPDF_Page_SetDash(page, dash_ptn.data(), static_cast(dash_ptn.size()), 0.0F);
+ }
+ }
+ } // namespace
+
DocraftHaruLineBackend::DocraftHaruLineBackend(const std::shared_ptr &state)
: state_(state) {
}
@@ -35,6 +68,11 @@ namespace docraft::backend::pdf {
HPDF_Page_SetLineWidth(provider->current_page(), thickness);
}
+ void DocraftHaruLineBackend::set_line_dash_pattern(const std::vector& pattern) const {
+ auto *provider = state_->ensure_page_provider();
+ invoke_set_dash(provider->current_page(), pattern);
+ }
+
void DocraftHaruLineBackend::draw_line(float x1, float y1, float x2, float y2) const {
auto *provider = state_->ensure_page_provider();
float px1, py1, px2, py2;
diff --git a/docraft/src/docraft/craft/parser/docraft_circle_parser.cc b/docraft/src/docraft/craft/parser/docraft_circle_parser.cc
index 5f77dfe..71e46ab 100644
--- a/docraft/src/docraft/craft/parser/docraft_circle_parser.cc
+++ b/docraft/src/docraft/craft/parser/docraft_circle_parser.cc
@@ -35,6 +35,10 @@ namespace docraft::craft::parser {
elements::circle::attribute::kBorderWidth.data())) {
data.border_width = border_width_attr.as_float();
}
+ if (auto border_style_attr = craft_language_source.attribute(
+ elements::circle::attribute::kBorderStyle.data())) {
+ data.border_style = detail::parse_line_style(border_style_attr.as_string());
+ }
if (auto radius_attr = craft_language_source.attribute(elements::circle::attribute::kRadius.data()))
{
data.radius = radius_attr.as_float();
@@ -64,6 +68,7 @@ namespace docraft::craft::parser {
elements::circle::attribute::kBackgroundColor,
elements::circle::attribute::kBorderColor,
elements::circle::attribute::kBorderWidth,
+ elements::circle::attribute::kBorderStyle,
elements::circle::attribute::kRadius,
elements::circle::attribute::kStartX,
elements::circle::attribute::kStartY,
diff --git a/docraft/src/docraft/craft/parser/docraft_curve_line_parser.cc b/docraft/src/docraft/craft/parser/docraft_curve_line_parser.cc
index d7f917a..0af5ff1 100644
--- a/docraft/src/docraft/craft/parser/docraft_curve_line_parser.cc
+++ b/docraft/src/docraft/craft/parser/docraft_curve_line_parser.cc
@@ -34,6 +34,11 @@ namespace docraft::craft::parser {
{
data.border_width = border_width_attr.as_float();
}
+ if (auto border_style_attr = craft_language_source.attribute(
+ elements::curve_line::attribute::kBorderStyle.data()))
+ {
+ data.border_style = detail::parse_line_style(border_style_attr.as_string());
+ }
auto points = detail::parse_points_attribute(craft_language_source,
elements::curve_line::attribute::kPoints.data());
@@ -49,7 +54,8 @@ namespace docraft::craft::parser {
return {
elements::curve_line::attribute::kPoints,
elements::curve_line::attribute::kBorderColor,
- elements::curve_line::attribute::kBorderWidth
+ elements::curve_line::attribute::kBorderWidth,
+ elements::curve_line::attribute::kBorderStyle
};
}
} // namespace docraft::craft::parser
diff --git a/docraft/src/docraft/craft/parser/docraft_line_parser.cc b/docraft/src/docraft/craft/parser/docraft_line_parser.cc
index eacf855..a9a1a98 100644
--- a/docraft/src/docraft/craft/parser/docraft_line_parser.cc
+++ b/docraft/src/docraft/craft/parser/docraft_line_parser.cc
@@ -49,6 +49,11 @@ namespace docraft::craft::parser {
{
data.border_width = border_width_attr.as_float();
}
+ if (auto border_style_attr = craft_language_source.attribute(
+ elements::line::attribute::kBorderStyle.data()))
+ {
+ data.border_style = detail::parse_line_style(border_style_attr.as_string());
+ }
return data;
}
@@ -60,7 +65,8 @@ namespace docraft::craft::parser {
elements::line::attribute::kX2,
elements::line::attribute::kY2,
elements::line::attribute::kBorderColor,
- elements::line::attribute::kBorderWidth
+ elements::line::attribute::kBorderWidth,
+ elements::line::attribute::kBorderStyle
};
}
} // namespace docraft::craft::parser
diff --git a/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc b/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc
index c0ce2f2..cda9af7 100644
--- a/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc
+++ b/docraft/src/docraft/craft/parser/docraft_parser_helpers.cc
@@ -312,6 +312,16 @@ namespace docraft::craft::parser::detail {
throw docraft::exception::InvalidInputException("Invalid text alignment: " + alignment_str);
}
+ ParsedLineStyle parse_line_style(const std::string& style_str) {
+ if (style_str == std::string{line_style::kDashed}) {
+ return ParsedLineStyle::kDashed;
+ }
+ if (style_str == std::string{line_style::kSolid}) {
+ return ParsedLineStyle::kSolid;
+ }
+ throw docraft::exception::InvalidInputException("Invalid border style: " + style_str);
+ }
+
std::string trim_whitespace(const std::string& text) {
constexpr auto whitespace = " \t\n\r\f\v";
diff --git a/docraft/src/docraft/craft/parser/docraft_polygon_parser.cc b/docraft/src/docraft/craft/parser/docraft_polygon_parser.cc
index 3522218..81cef40 100644
--- a/docraft/src/docraft/craft/parser/docraft_polygon_parser.cc
+++ b/docraft/src/docraft/craft/parser/docraft_polygon_parser.cc
@@ -39,6 +39,11 @@ namespace docraft::craft::parser {
{
data.border_width = border_width_attr.as_float();
}
+ if (auto border_style_attr = craft_language_source.attribute(
+ elements::polygon::attribute::kBorderStyle.data()))
+ {
+ data.border_style = detail::parse_line_style(border_style_attr.as_string());
+ }
auto points = detail::parse_points_attribute(craft_language_source,
elements::polygon::attribute::kPoints.data());
@@ -56,7 +61,8 @@ namespace docraft::craft::parser {
elements::polygon::attribute::kPoints,
elements::polygon::attribute::kBackgroundColor,
elements::polygon::attribute::kBorderColor,
- elements::polygon::attribute::kBorderWidth
+ elements::polygon::attribute::kBorderWidth,
+ elements::polygon::attribute::kBorderStyle
};
}
} // namespace docraft::craft::parser
diff --git a/docraft/src/docraft/craft/parser/docraft_rectangle_parser.cc b/docraft/src/docraft/craft/parser/docraft_rectangle_parser.cc
index e65fee2..e268c58 100644
--- a/docraft/src/docraft/craft/parser/docraft_rectangle_parser.cc
+++ b/docraft/src/docraft/craft/parser/docraft_rectangle_parser.cc
@@ -34,6 +34,10 @@ namespace docraft::craft::parser {
elements::rectangle::attribute::kBorderWidth.data())) {
data.border_width = border_width_attr.as_float();
}
+ if (auto border_style_attr = craft_language_source.attribute(
+ elements::rectangle::attribute::kBorderStyle.data())) {
+ data.border_style = detail::parse_line_style(border_style_attr.as_string());
+ }
return data;
}
@@ -42,7 +46,8 @@ namespace docraft::craft::parser {
return {
elements::rectangle::attribute::kBackgroundColor,
elements::rectangle::attribute::kBorderColor,
- elements::rectangle::attribute::kBorderWidth
+ elements::rectangle::attribute::kBorderWidth,
+ elements::rectangle::attribute::kBorderStyle
};
}
} // namespace docraft::craft::parser
diff --git a/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc b/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc
index 48cce23..81e63fb 100644
--- a/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc
+++ b/docraft/src/docraft/craft/parser/docraft_triangle_parser.cc
@@ -37,6 +37,10 @@ namespace docraft::craft::parser {
elements::triangle::attribute::kBorderWidth.data())) {
data.border_width = border_width_attr.as_float();
}
+ if (auto border_style_attr = craft_language_source.attribute(
+ elements::triangle::attribute::kBorderStyle.data())) {
+ data.border_style = detail::parse_line_style(border_style_attr.as_string());
+ }
auto points = detail::parse_points_attribute(craft_language_source,
elements::triangle::attribute::kPoints.data());
@@ -56,7 +60,8 @@ namespace docraft::craft::parser {
elements::triangle::attribute::kPoints,
elements::triangle::attribute::kBackgroundColor,
elements::triangle::attribute::kBorderColor,
- elements::triangle::attribute::kBorderWidth
+ elements::triangle::attribute::kBorderWidth,
+ elements::triangle::attribute::kBorderStyle
};
}
} // namespace docraft::craft::parser
diff --git a/docraft/src/docraft/loom/craft/docraft_loom_tree_builder_utils.cc b/docraft/src/docraft/loom/craft/docraft_loom_tree_builder_utils.cc
index 9728964..b5cbb92 100644
--- a/docraft/src/docraft/loom/craft/docraft_loom_tree_builder_utils.cc
+++ b/docraft/src/docraft/loom/craft/docraft_loom_tree_builder_utils.cc
@@ -61,6 +61,18 @@ namespace docraft::loom::craft {
}
}
+ nodes::DocraftLineStyle to_loom_line_style(parser::ParsedLineStyle style)
+ {
+ switch (style)
+ {
+ case parser::ParsedLineStyle::kDashed:
+ return nodes::DocraftLineStyle::kDashed;
+ case parser::ParsedLineStyle::kSolid:
+ default:
+ return nodes::DocraftLineStyle::kSolid;
+ }
+ }
+
namespace {
bool is_word_char(char ch)
{
diff --git a/docraft/src/docraft/loom/craft/handlers/docraft_loom_curve_line_handler.cc b/docraft/src/docraft/loom/craft/handlers/docraft_loom_curve_line_handler.cc
index 310843b..037113f 100644
--- a/docraft/src/docraft/loom/craft/handlers/docraft_loom_curve_line_handler.cc
+++ b/docraft/src/docraft/loom/craft/handlers/docraft_loom_curve_line_handler.cc
@@ -46,6 +46,10 @@ namespace docraft::loom::craft {
{
node->set_border_width(*data.border_width);
}
+ if (data.border_style.has_value())
+ {
+ node->set_border_style(to_loom_line_style(*data.border_style));
+ }
apply_common_attributes(*node, element.common);
return node;
}
diff --git a/docraft/src/docraft/loom/craft/handlers/docraft_loom_line_handler.cc b/docraft/src/docraft/loom/craft/handlers/docraft_loom_line_handler.cc
index 93836df..3b45d4f 100644
--- a/docraft/src/docraft/loom/craft/handlers/docraft_loom_line_handler.cc
+++ b/docraft/src/docraft/loom/craft/handlers/docraft_loom_line_handler.cc
@@ -56,6 +56,10 @@ namespace docraft::loom::craft {
{
node->set_border_width(*data.border_width);
}
+ if (data.border_style.has_value())
+ {
+ node->set_border_style(to_loom_line_style(*data.border_style));
+ }
apply_common_attributes(*node, element.common);
return node;
}
diff --git a/docraft/src/docraft/loom/nodes/docraft_loom_curve_line.cc b/docraft/src/docraft/loom/nodes/docraft_loom_curve_line.cc
index d2f52e8..4125d78 100644
--- a/docraft/src/docraft/loom/nodes/docraft_loom_curve_line.cc
+++ b/docraft/src/docraft/loom/nodes/docraft_loom_curve_line.cc
@@ -1,11 +1,6 @@
#include "docraft/loom/nodes/docraft_loom_curve_line.h"
namespace docraft::loom::nodes {
- DocraftLoomCurveLine::DocraftLoomCurveLine()
- : border_color_(DocraftColor::fromRGB(0.0F, 0.0F, 0.0F, 1.0F))
- {
- }
-
void DocraftLoomCurveLine::accept(loom::interfaces::DocraftLoomIVisitor& visitor)
{
visitor.visit(this);
@@ -20,24 +15,4 @@ namespace docraft::loom::nodes {
{
points_ = points;
}
-
- const DocraftColor& DocraftLoomCurveLine::border_color() const
- {
- return border_color_;
- }
-
- void DocraftLoomCurveLine::set_border_color(const DocraftColor& color)
- {
- border_color_ = color;
- }
-
- float DocraftLoomCurveLine::border_width() const
- {
- return border_width_;
- }
-
- void DocraftLoomCurveLine::set_border_width(float width)
- {
- border_width_ = width;
- }
} // docraft
diff --git a/docraft/src/docraft/loom/nodes/docraft_loom_line.cc b/docraft/src/docraft/loom/nodes/docraft_loom_line.cc
index 79d31be..d43060f 100644
--- a/docraft/src/docraft/loom/nodes/docraft_loom_line.cc
+++ b/docraft/src/docraft/loom/nodes/docraft_loom_line.cc
@@ -1,11 +1,6 @@
#include "docraft/loom/nodes/docraft_loom_line.h"
namespace docraft::loom::nodes {
- DocraftLoomLine::DocraftLoomLine()
- : border_color_(DocraftColor::fromRGB(0.0F, 0.0F, 0.0F, 1.0F))
- {
- }
-
void DocraftLoomLine::accept(loom::interfaces::DocraftLoomIVisitor& visitor)
{
visitor.visit(this);
@@ -30,24 +25,4 @@ namespace docraft::loom::nodes {
{
end_ = end;
}
-
- const DocraftColor& DocraftLoomLine::border_color() const
- {
- return border_color_;
- }
-
- void DocraftLoomLine::set_border_color(const DocraftColor& color)
- {
- border_color_ = color;
- }
-
- float DocraftLoomLine::border_width() const
- {
- return border_width_;
- }
-
- void DocraftLoomLine::set_border_width(float width)
- {
- border_width_ = width;
- }
-} // docraft
\ No newline at end of file
+} // docraft
diff --git a/docraft/src/docraft/loom/nodes/docraft_loom_stroked_line.cc b/docraft/src/docraft/loom/nodes/docraft_loom_stroked_line.cc
new file mode 100644
index 0000000..9a1986f
--- /dev/null
+++ b/docraft/src/docraft/loom/nodes/docraft_loom_stroked_line.cc
@@ -0,0 +1,38 @@
+#include "docraft/loom/nodes/docraft_loom_stroked_line.h"
+
+namespace docraft::loom::nodes {
+ DocraftLoomStrokedLine::DocraftLoomStrokedLine()
+ : border_color_(DocraftColor::fromRGB(0.0F, 0.0F, 0.0F, 1.0F))
+ {
+ }
+
+ const DocraftColor& DocraftLoomStrokedLine::border_color() const
+ {
+ return border_color_;
+ }
+
+ void DocraftLoomStrokedLine::set_border_color(const DocraftColor& color)
+ {
+ border_color_ = color;
+ }
+
+ float DocraftLoomStrokedLine::border_width() const
+ {
+ return border_width_;
+ }
+
+ void DocraftLoomStrokedLine::set_border_width(float width)
+ {
+ border_width_ = width;
+ }
+
+ DocraftLineStyle DocraftLoomStrokedLine::border_style() const
+ {
+ return border_style_;
+ }
+
+ void DocraftLoomStrokedLine::set_border_style(DocraftLineStyle style)
+ {
+ border_style_ = style;
+ }
+} // docraft
diff --git a/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc b/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc
index 6d58597..10e2fcc 100644
--- a/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc
+++ b/docraft/src/docraft/loom/pipeline/docraft_loom_rendering_processor.cc
@@ -371,6 +371,7 @@ namespace docraft::loom::pipeline {
}
line_backend_->set_line_width(line->border_width());
line_backend_->set_stroke_color(rgba.r, rgba.g, rgba.b);
+ line_backend_->set_line_dash_pattern(DocraftLoomShapeDrawUtils::resolve_dash_pattern(line->border_style()));
line_backend_->draw_line(p1.x, p1.y, p2.x, p2.y);
shape_backend_->restore_state();
}
@@ -409,6 +410,8 @@ namespace docraft::loom::pipeline {
}
line_backend_->set_line_width(node->style().border_width);
line_backend_->set_stroke_color(rgba.r, rgba.g, rgba.b);
+ line_backend_->set_line_dash_pattern(
+ DocraftLoomShapeDrawUtils::resolve_dash_pattern(node->style().border_style));
shape_backend_->draw_arc(center.x, center.y, radius_x, node->arc_start_angle(), node->arc_end_angle());
shape_backend_->stroke();
shape_backend_->restore_state();
@@ -467,6 +470,7 @@ namespace docraft::loom::pipeline {
.target = {.shape_backend = shape_backend_, .line_backend = line_backend_},
.border_color = node->border_color(),
.border_width = node->border_width(),
+ .border_style = node->border_style(),
.origin = nodes::sealed_frame(*node).position,
.points = node->points(),
});
diff --git a/docraft/src/docraft/loom/pipeline/docraft_loom_shape_draw_utils.cc b/docraft/src/docraft/loom/pipeline/docraft_loom_shape_draw_utils.cc
index dc8cf92..2240ed4 100644
--- a/docraft/src/docraft/loom/pipeline/docraft_loom_shape_draw_utils.cc
+++ b/docraft/src/docraft/loom/pipeline/docraft_loom_shape_draw_utils.cc
@@ -5,6 +5,13 @@
#include "docraft/loom/pipeline/docraft_loom_shape_draw_utils.h"
namespace docraft::loom::pipeline {
+ std::vector DocraftLoomShapeDrawUtils::resolve_dash_pattern(nodes::DocraftLineStyle style) {
+ if (style == nodes::DocraftLineStyle::kDashed) {
+ return {kDashSegmentLength, kDashGapLength};
+ }
+ return {};
+ }
+
DocraftLoomShapeDrawUtils::ShapeDrawFlags DocraftLoomShapeDrawUtils::resolve_shape_draw_flags(
const nodes::DocraftLoomShapeStyle &style) {
return {
@@ -30,6 +37,7 @@ namespace docraft::loom::pipeline {
}
target.line_backend->set_line_width(style.border_width);
target.line_backend->set_stroke_color(border.r, border.g, border.b);
+ target.line_backend->set_line_dash_pattern(resolve_dash_pattern(style.border_style));
}
}
@@ -90,6 +98,7 @@ namespace docraft::loom::pipeline {
}
request.target.line_backend->set_line_width(request.border_width);
request.target.line_backend->set_stroke_color(rgba.r, rgba.g, rgba.b);
+ request.target.line_backend->set_line_dash_pattern(resolve_dash_pattern(request.border_style));
request.target.line_backend->draw_curve(transformed);
request.target.shape_backend->restore_state();
}
diff --git a/docraft/test/docraft/craft/docraft_shape_parser_test.cc b/docraft/test/docraft/craft/docraft_shape_parser_test.cc
index 416f69c..8f5f1ce 100644
--- a/docraft/test/docraft/craft/docraft_shape_parser_test.cc
+++ b/docraft/test/docraft/craft/docraft_shape_parser_test.cc
@@ -139,3 +139,61 @@ TEST(DocraftRectangleParserTest, ParsesBorderWidth) {
ASSERT_TRUE(data.border_width.has_value());
EXPECT_FLOAT_EQ(*data.border_width, 3.0F);
}
+
+TEST(DocraftRectangleParserTest, ParsesDashedBorderStyle) {
+ const char *xml = R"XML(
+
+)XML";
+
+ pugi::xml_document doc;
+ ASSERT_TRUE(doc.load_string(xml));
+
+ docraft::craft::parser::DocraftRectangleParser parser;
+ const auto data = std::any_cast(parser.parse(doc.child("Rectangle")));
+
+ ASSERT_TRUE(data.border_style.has_value());
+ EXPECT_EQ(*data.border_style, docraft::craft::parser::ParsedLineStyle::kDashed);
+}
+
+TEST(DocraftLineParserTest, ParsesDashedBorderStyle) {
+ const char *xml = R"XML(
+
+)XML";
+
+ pugi::xml_document doc;
+ ASSERT_TRUE(doc.load_string(xml));
+
+ docraft::craft::parser::DocraftLineParser parser;
+ const auto data = std::any_cast(parser.parse(doc.child("Line")));
+
+ ASSERT_TRUE(data.border_style.has_value());
+ EXPECT_EQ(*data.border_style, docraft::craft::parser::ParsedLineStyle::kDashed);
+}
+
+TEST(DocraftLineParserTest, DefaultsBorderStyleToUnset)
+{
+ const char *xml = R"XML(
+
+)XML";
+
+ pugi::xml_document doc;
+ ASSERT_TRUE(doc.load_string(xml));
+
+ docraft::craft::parser::DocraftLineParser parser;
+ const auto data = std::any_cast(parser.parse(doc.child("Line")));
+
+ EXPECT_FALSE(data.border_style.has_value());
+}
+
+TEST(DocraftLineParserTest, RejectsUnrecognizedBorderStyle)
+{
+ const char *xml = R"XML(
+
+)XML";
+
+ pugi::xml_document doc;
+ ASSERT_TRUE(doc.load_string(xml));
+
+ docraft::craft::parser::DocraftLineParser parser;
+ EXPECT_THROW(parser.parse(doc.child("Line")), docraft::exception::InvalidInputException);
+}
diff --git a/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc b/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc
index 6b74f5c..2e73c3a 100644
--- a/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc
+++ b/docraft/test/docraft/loom/nodes/docraft_loom_rectangle_test.cc
@@ -149,4 +149,24 @@ namespace docraft::test {
EXPECT_FLOAT_EQ(backend.clip_calls()[0].width, frame.size.width);
EXPECT_FLOAT_EQ(backend.clip_calls()[0].height, frame.size.height);
}
+
+ TEST_F(DocraftLoomRectangleTest, DashedBorderStyleSendsTheDashPatternBeforeDrawing)
+ {
+ utils::MockRenderingBackend backend;
+ loom::pipeline::DocraftLoomRenderingProcessor rendering(&backend);
+
+ auto rect = std::make_shared();
+ rect->set_width(80.0F);
+ rect->set_height(60.0F);
+ rect->edit_style().border_color = DocraftColor::fromRGB(0.0F, 0.0F, 0.0F, 1.0F);
+ rect->edit_style().border_width = 2.0F;
+ rect->edit_style().border_style = loom::nodes::DocraftLineStyle::kDashed;
+
+ rect->accept(*measure_);
+ rect->accept(*layout_);
+ rect->accept(rendering);
+
+ ASSERT_EQ(backend.dash_pattern_calls().size(), 1U);
+ EXPECT_FALSE(backend.dash_pattern_calls()[0].pattern.empty());
+ }
} // namespace docraft::test
\ No newline at end of file
diff --git a/docraft/test/docraft/loom/nodes/docraft_loom_shape_nodes_test.cc b/docraft/test/docraft/loom/nodes/docraft_loom_shape_nodes_test.cc
index e22fc93..67f0b08 100644
--- a/docraft/test/docraft/loom/nodes/docraft_loom_shape_nodes_test.cc
+++ b/docraft/test/docraft/loom/nodes/docraft_loom_shape_nodes_test.cc
@@ -4,6 +4,7 @@
#include "docraft/exception/docraft_input_exceptions.h"
#include "docraft/loom/nodes/docraft_loom_circle.h"
#include "docraft/loom/nodes/docraft_loom_curve_line.h"
+#include "docraft/loom/nodes/docraft_loom_line.h"
#include "docraft/loom/nodes/docraft_loom_polygon.h"
#include "docraft/loom/nodes/docraft_loom_triangle.h"
#include "docraft/loom/pipeline/docraft_loom_layout_processor.h"
@@ -26,6 +27,39 @@ namespace docraft::test {
std::unique_ptr layout_;
};
+ // ── Line ────────────────────────────────────────────────────────────────────
+
+ TEST_F(DocraftLoomShapeNodesTest, LineDefaultsToSolidAndSendsAnEmptyDashPattern)
+ {
+ utils::MockRenderingBackend backend;
+ loom::pipeline::DocraftLoomRenderingProcessor rendering(&backend);
+
+ auto line = std::make_shared();
+ line->accept(*measure_);
+ line->accept(*layout_);
+ line->accept(rendering);
+
+ ASSERT_EQ(backend.dash_pattern_calls().size(), 1U);
+ EXPECT_TRUE(backend.dash_pattern_calls()[0].pattern.empty());
+ }
+
+ TEST_F(DocraftLoomShapeNodesTest, DashedLineSendsTheDashPatternBeforeDrawing)
+ {
+ utils::MockRenderingBackend backend;
+ loom::pipeline::DocraftLoomRenderingProcessor rendering(&backend);
+
+ auto line = std::make_shared();
+ line->set_border_style(loom::nodes::DocraftLineStyle::kDashed);
+
+ line->accept(*measure_);
+ line->accept(*layout_);
+ line->accept(rendering);
+
+ ASSERT_EQ(backend.dash_pattern_calls().size(), 1U);
+ EXPECT_FALSE(backend.dash_pattern_calls()[0].pattern.empty());
+ ASSERT_EQ(backend.draw_line_calls().size(), 1U);
+ }
+
// ── Circle ──────────────────────────────────────────────────────────────────
TEST_F(DocraftLoomShapeNodesTest, CircleMeasuresToDiameterFromRadius)
@@ -156,6 +190,26 @@ namespace docraft::test {
EXPECT_TRUE(backend.draw_ellipse_calls().empty());
}
+ TEST_F(DocraftLoomShapeNodesTest, DashedArcSendsTheDashPatternBeforeDrawing)
+ {
+ utils::MockRenderingBackend backend;
+ loom::pipeline::DocraftLoomRenderingProcessor rendering(&backend);
+
+ auto circle = std::make_shared();
+ circle->set_radius(50.0F);
+ circle->set_arc(0.0F, 90.0F);
+ circle->edit_style().border_color = DocraftColor::fromRGB(1.0F, 0.0F, 0.0F, 1.0F);
+ circle->edit_style().border_width = 2.0F;
+ circle->edit_style().border_style = loom::nodes::DocraftLineStyle::kDashed;
+
+ circle->accept(*measure_);
+ circle->accept(*layout_);
+ circle->accept(rendering);
+
+ ASSERT_EQ(backend.dash_pattern_calls().size(), 1U);
+ EXPECT_FALSE(backend.dash_pattern_calls()[0].pattern.empty());
+ }
+
TEST_F(DocraftLoomShapeNodesTest, ArcIsSkippedWhenItWouldHaveNoStroke)
{
utils::MockRenderingBackend backend;
@@ -296,6 +350,25 @@ namespace docraft::test {
EXPECT_EQ(backend.draw_curve_calls()[0].points.size(), 3U);
}
+ TEST_F(DocraftLoomShapeNodesTest, DashedCurveLineSendsTheDashPatternBeforeDrawing)
+ {
+ utils::MockRenderingBackend backend;
+ loom::pipeline::DocraftLoomRenderingProcessor rendering(&backend);
+
+ auto curve = std::make_shared();
+ curve->set_points({{.x = 0.0F, .y = 0.0F}, {.x = 10.0F, .y = 5.0F}, {.x = 20.0F, .y = 0.0F}});
+ curve->set_border_color(DocraftColor::fromRGB(0.0F, 0.0F, 1.0F, 1.0F));
+ curve->set_border_width(2.0F);
+ curve->set_border_style(loom::nodes::DocraftLineStyle::kDashed);
+
+ curve->accept(*measure_);
+ curve->accept(*layout_);
+ curve->accept(rendering);
+
+ ASSERT_EQ(backend.dash_pattern_calls().size(), 1U);
+ EXPECT_FALSE(backend.dash_pattern_calls()[0].pattern.empty());
+ }
+
TEST_F(DocraftLoomShapeNodesTest, CurveLineMeasuresToTheBoundingBoxOfItsPoints)
{
loom::nodes::DocraftLoomCurveLine curve;
diff --git a/docraft/test/docraft/utils/docraft_mock_rendering_backend.h b/docraft/test/docraft/utils/docraft_mock_rendering_backend.h
index d4ebc26..656c2d2 100644
--- a/docraft/test/docraft/utils/docraft_mock_rendering_backend.h
+++ b/docraft/test/docraft/utils/docraft_mock_rendering_backend.h
@@ -79,6 +79,11 @@ namespace docraft::test::utils {
std::vector points;
};
+ struct DashPatternCall
+ {
+ std::vector pattern;
+ };
+
// Records draw_circle() and draw_ellipse() alike -- a circle is stored with
// radius_x == radius_y, so a test can assert on the resulting geometry without
// caring which of the two primitives the rendering stage picked.
@@ -108,6 +113,7 @@ namespace docraft::test::utils {
mutable std::vector clip_calls;
mutable std::vector draw_line_calls;
mutable std::vector draw_curve_calls;
+ mutable std::vector dash_pattern_calls;
mutable std::vector draw_ellipse_calls;
mutable std::vector draw_arc_calls;
@@ -138,6 +144,13 @@ namespace docraft::test::utils {
state_->ensure_page_available();
}
+ void set_line_dash_pattern(const std::vector& pattern) const override {
+ MockBackendSharedState::ensure_supported(state_->config.supports_line_backend,
+ "Line backend capability not supported");
+ state_->ensure_page_available();
+ state_->dash_pattern_calls.push_back({.pattern = pattern});
+ }
+
void draw_line(float x1, float y1, float x2, float y2) const override {
MockBackendSharedState::ensure_supported(state_->config.supports_line_backend,
"Line backend capability not supported");
@@ -617,6 +630,11 @@ namespace docraft::test::utils {
return state_->draw_curve_calls;
}
+ [[nodiscard]] const std::vector& dash_pattern_calls() const
+ {
+ return state_->dash_pattern_calls;
+ }
+
[[nodiscard]] const std::string &last_saved_path() const {
return state_->last_saved_path;
}