From 5965da0beb9e68176d17086d890b6fdcc93d7fb1 Mon Sep 17 00:00:00 2001 From: cadons Date: Mon, 15 Jun 2026 22:52:25 +0200 Subject: [PATCH 1/5] refactor(layout): streamline cursor state management in layout computation --- .../layout/docraft_layout_engine_impl.cc | 121 +++++++++++------- .../layout/docraft_layout_engine_impl.h | 33 ++++- 2 files changed, 105 insertions(+), 49 deletions(-) diff --git a/docraft/src/docraft/layout/docraft_layout_engine_impl.cc b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc index 7444d00..4972f5d 100644 --- a/docraft/src/docraft/layout/docraft_layout_engine_impl.cc +++ b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc @@ -206,58 +206,36 @@ namespace docraft::layout { return false; } - /** - * @brief Captures cursor, width, and positioning state before laying out a node. - */ - DocraftLayoutEngine::Impl::LayoutComputationState DocraftLayoutEngine::Impl::prepare_layout_state( - const std::shared_ptr &node, - DocraftCursor &cursor) { - LayoutComputationState state; - auto &layout_service = context_->edit_layout(); - state.max_width = layout_service.available_space(); - state.flow_origin_x = cursor.x(); - state.flow_origin_y = cursor.y(); - state.is_absolute = (node->position_mode() == model::DocraftPositionType::kAbsolute); - - DocraftCursor local_cursor = cursor; - state.active_cursor = state.is_absolute ? local_cursor : cursor; - if (state.is_absolute) { - state.active_cursor.move_to(state.flow_origin_x + node->position().x, - state.flow_origin_y - node->position().y); - } - - state.local_node_cursor = state.active_cursor; - state.rect_origin_cursor = state.active_cursor; - state.layout_cursor = &state.active_cursor; - - if (!state.is_absolute && (std::dynamic_pointer_cast(node) || - std::dynamic_pointer_cast(node))) { - state.local_node_cursor.move_to(state.active_cursor.x(), state.active_cursor.y()); - state.layout_cursor = &state.local_node_cursor; - } - + void DocraftLayoutEngine::Impl::setup_container_cursor_state(const std::shared_ptr &node, + LayoutComputationState &state) { if (auto rect_container = std::dynamic_pointer_cast(node)) { + auto &layout_service = context_->edit_layout(); if (!std::dynamic_pointer_cast(node) && !rect_container->children().empty()) { - DocraftCursor rect_cursor = state.active_cursor; + DocraftCursor rect_cursor = state.node_cursor; if (rect_container->position_mode() == model::DocraftPositionType::kAbsolute) { rect_cursor.move_to(state.flow_origin_x + rect_container->position().x, state.flow_origin_y - rect_container->position().y); } else { - rect_cursor.move_to(state.active_cursor.x(), state.active_cursor.y()); + rect_cursor.move_to(state.node_cursor.x(), state.node_cursor.y()); } rect_container->set_position({.x = rect_cursor.x(), .y = rect_cursor.y()}); - state.rect_origin_cursor = rect_cursor; - state.rect_uses_origin_cursor = true; - state.local_node_cursor = rect_cursor; - state.layout_cursor = &state.local_node_cursor; + state.box_origin_cursor = rect_cursor; + state.use_box_origin_cursor = true; + state.child_cursor = rect_cursor; + // Rectangle children flow from a local cursor, while the node box keeps its own origin. + state.selected_cursor = &state.child_cursor; if (rect_container->width() > 0.0F) { state.max_width = rect_container->width(); } layout_service.set_current_rect_width(state.max_width); } } + } + void DocraftLayoutEngine::Impl::setup_section_bounds_state(const std::shared_ptr &node, + LayoutComputationState &state) { if (auto section = std::dynamic_pointer_cast(node)) { + auto &layout_service = context_->edit_layout(); const float left_margin = section->margin_left(); const float right_margin = section->margin_right(); const float top_margin = section->margin_top(); @@ -266,7 +244,7 @@ namespace docraft::layout { if (base_x == 0.0F && left_margin > 0.0F) { base_x = left_margin; } - state.active_cursor.move_to(base_x, state.active_cursor.y() - top_margin - padding); + state.node_cursor.move_to(base_x, state.node_cursor.y() - top_margin - padding); if (section->width() > 0.0F) { state.max_width = section->width(); } else { @@ -279,15 +257,71 @@ namespace docraft::layout { state.section_has_bounds = true; } } + } + void DocraftLayoutEngine::Impl::setup_layout_direction_state(const std::shared_ptr &node, + LayoutComputationState &state) { if (auto layout_node = std::dynamic_pointer_cast(node)) { if (layout_node->orientation() == model::LayoutOrientation::kHorizontal) { - state.layout_cursor->push_direction(DocraftCursorDirection::kHorizontal); + state.selected_cursor->push_direction(DocraftCursorDirection::kHorizontal); } else { - state.layout_cursor->push_direction(DocraftCursorDirection::kVertical); + state.selected_cursor->push_direction(DocraftCursorDirection::kVertical); } } + } + + /** + * @brief Captures cursor, width, and positioning state before laying out a node. + + * Cursor model used by LayoutComputationState: + * - node_cursor: base cursor for the current node (flow position or absolute anchor). + * - child_cursor: optional scratch cursor for child layout when child flow must be isolated. + * - box_origin_cursor: cursor used to place the node box when box anchoring differs + * from child flow (for example rectangle-like containers). + * - selected_cursor: pointer to the cursor currently driving child traversal + * (usually node_cursor, sometimes child_cursor). + * + * Typical flow: + * 1) prepare_layout_state() initializes node_cursor and defaults selected_cursor. + * 2) setup_section_bounds_state()/setup_container_cursor_state() may adjust limits and + * switch selected_cursor to child_cursor. + * 3) layout_node_children() consumes *selected_cursor. + * 4) finalize_layout() computes the node box using box_origin_cursor only when needed; + * otherwise it uses *selected_cursor and then advances the external flow cursor. + */ + DocraftLayoutEngine::Impl::LayoutComputationState DocraftLayoutEngine::Impl::prepare_layout_state( + const std::shared_ptr &node, DocraftCursor &cursor) { + //general state setup + LayoutComputationState state; + auto &layout_service = context_->edit_layout(); + state.max_width = layout_service.available_space(); + state.flow_origin_x = cursor.x(); + state.flow_origin_y = cursor.y(); + state.is_absolute = (node->position_mode() == model::DocraftPositionType::kAbsolute); + // configure cursor and width based on node type + DocraftCursor local_cursor = cursor; + state.node_cursor = state.is_absolute ? local_cursor : cursor; + if (state.is_absolute) { + //if absolute positions set cursor to the node's position + const float x = state.flow_origin_x + node->position().x; //absolute position is relative to the flow origin + const float y = state.flow_origin_y + node->position().y; //absolute position is relative to the flow origin + state.node_cursor.move_to(x, y); + } + // Default behavior: children consume node_cursor directly. + state.child_cursor = state.node_cursor; + state.box_origin_cursor = state.node_cursor; + state.selected_cursor = &state.node_cursor; + if (!state.is_absolute && (std::dynamic_pointer_cast(node) || + std::dynamic_pointer_cast(node))) { + // Text/List measure with an isolated cursor to avoid mutating parent flow prematurely. + state.child_cursor.move_to(state.node_cursor.x(), state.node_cursor.y()); + state.selected_cursor = &state.child_cursor; + } + // Apply section limits first, then optional container-origin cursor behavior. + setup_section_bounds_state(node, state); + setup_container_cursor_state(node, state); + setup_layout_direction_state(node, state); return state; } @@ -300,7 +334,7 @@ namespace docraft::layout { void DocraftLayoutEngine::Impl::layout_node_children(const std::shared_ptr &node, LayoutComputationState &state) { auto &layout_service = context_->edit_layout(); - auto &layout_cursor = *state.layout_cursor; + auto &layout_cursor = *state.selected_cursor; if (auto list_node = std::dynamic_pointer_cast(node)) { if (!list_handler_) { @@ -342,9 +376,10 @@ namespace docraft::layout { DocraftCursor &cursor, LayoutComputationState &state, model::DocraftTransform &max_rect) { - auto &layout_cursor = *state.layout_cursor; - if (state.rect_uses_origin_cursor) { - if (!compute_node(node, &max_rect, state.rect_origin_cursor)) { + auto &layout_cursor = *state.selected_cursor; + // Some containers layout children with a local cursor but resolve their own box from box_origin_cursor. + if (state.use_box_origin_cursor) { + if (!compute_node(node, &max_rect, state.box_origin_cursor)) { throw docraft::exception::LayoutException("compute node failed"); } } else if (!compute_node(node, &max_rect, layout_cursor)) { diff --git a/docraft/src/docraft/layout/docraft_layout_engine_impl.h b/docraft/src/docraft/layout/docraft_layout_engine_impl.h index c5397dd..31ef46c 100644 --- a/docraft/src/docraft/layout/docraft_layout_engine_impl.h +++ b/docraft/src/docraft/layout/docraft_layout_engine_impl.h @@ -114,13 +114,17 @@ namespace docraft::layout { float flow_origin_x = 0.0F; float flow_origin_y = 0.0F; bool is_absolute = false; - bool rect_uses_origin_cursor = false; + bool use_box_origin_cursor = false; bool section_has_bounds = false; float section_content_bottom = 0.0F; - DocraftCursor active_cursor; - DocraftCursor local_node_cursor; - DocraftCursor rect_origin_cursor; - DocraftCursor *layout_cursor = nullptr; + // Base cursor for the current node (flow cursor or absolute-position anchor). + DocraftCursor node_cursor; + // Scratch cursor used when children need isolated progression from node_cursor. + DocraftCursor child_cursor; + // Origin used to place the node box when container anchoring differs from child flow. + DocraftCursor box_origin_cursor; + // Pointer to the cursor currently driving child layout (node_cursor or child_cursor). + DocraftCursor *selected_cursor = nullptr; LayoutComputationState() = default; @@ -277,10 +281,27 @@ namespace docraft::layout { const std::shared_ptr &body, const SectionPlan &plan); + /** + * @brief Applies section margins/limits and updates section-bound constraints. + */ + void setup_section_bounds_state(const std::shared_ptr &node, + LayoutComputationState &state); + + /** + * @brief Configures container-origin cursors for rectangle-like nodes with children. + */ + void setup_container_cursor_state(const std::shared_ptr &node, + LayoutComputationState &state); + + /** + * @brief Pushes layout orientation (horizontal/vertical) on the selected cursor. + */ + static void setup_layout_direction_state(const std::shared_ptr &node, + LayoutComputationState &state); + std::shared_ptr context_; std::vector > > handlers_; handler::DocraftLayoutListHandler *list_handler_ = nullptr; }; } // namespace docraft::layout - From 6bc44b5884e7aec604ecc292759da50fc6a08f0f Mon Sep 17 00:00:00 2001 From: cadons Date: Mon, 15 Jun 2026 22:57:12 +0200 Subject: [PATCH 2/5] refactor(parser): improve metadata parsing by modularizing functions --- .../docraft/craft/parser/docraft_parser.h | 3 +- .../painter/docraft_rectangle_painter.h | 3 +- .../renderer/painter/docraft_table_painter.h | 2 +- .../craft/docraft_craft_language_parser.cc | 183 ++++++++++-------- 4 files changed, 112 insertions(+), 79 deletions(-) diff --git a/docraft/include/docraft/craft/parser/docraft_parser.h b/docraft/include/docraft/craft/parser/docraft_parser.h index af6322e..5ad56c2 100644 --- a/docraft/include/docraft/craft/parser/docraft_parser.h +++ b/docraft/include/docraft/craft/parser/docraft_parser.h @@ -211,7 +211,8 @@ namespace docraft::craft::parser { */ class DOCRAFT_LIB DocraftForeachParser : public IDocraftParser { public: - DocraftForeachParser(DocraftCraftLanguageParser* craft_language_parser); + explicit DocraftForeachParser(DocraftCraftLanguageParser *craft_language_parser); + /** * @brief Parses a foreach XML node. * @param craft_language_source XML node. diff --git a/docraft/include/docraft/renderer/painter/docraft_rectangle_painter.h b/docraft/include/docraft/renderer/painter/docraft_rectangle_painter.h index f6a0f32..70efe4e 100644 --- a/docraft/include/docraft/renderer/painter/docraft_rectangle_painter.h +++ b/docraft/include/docraft/renderer/painter/docraft_rectangle_painter.h @@ -32,7 +32,8 @@ namespace docraft::renderer::painter { * @brief Creates a rectangle painter bound to the rectangle node. * @param rectangle_node Rectangle node. */ - DocraftRectanglePainter(const model::DocraftRectangle &rectangle_node); + explicit DocraftRectanglePainter(const model::DocraftRectangle &rectangle_node); + /** * @brief Draws the rectangle using the provided context. * @param context Document context. diff --git a/docraft/include/docraft/renderer/painter/docraft_table_painter.h b/docraft/include/docraft/renderer/painter/docraft_table_painter.h index db447e5..71982b2 100644 --- a/docraft/include/docraft/renderer/painter/docraft_table_painter.h +++ b/docraft/include/docraft/renderer/painter/docraft_table_painter.h @@ -32,7 +32,7 @@ namespace docraft::renderer::painter { * @brief Creates a table painter bound to the table node. * @param table_node Table node. */ - DocraftTablePainter(const model::DocraftTable &table_node); + explicit DocraftTablePainter(const model::DocraftTable &table_node); /** * @brief Draws the table using the provided context. diff --git a/docraft/src/docraft/craft/docraft_craft_language_parser.cc b/docraft/src/docraft/craft/docraft_craft_language_parser.cc index 4a08527..cb1da27 100644 --- a/docraft/src/docraft/craft/docraft_craft_language_parser.cc +++ b/docraft/src/docraft/craft/docraft_craft_language_parser.cc @@ -335,15 +335,8 @@ namespace { return supported_languages.find(normalized) != supported_languages.end(); } - DocraftMetadataParseOutcome parse_metadata_node(const pugi::xml_node &document_node) { - DocraftMetadataParseOutcome outcome; - const std::string metadata_tag = normalize_tag_name(std::string{docraft::craft::section::kMetadata}); - const pugi::xml_node metadata_node = document_node.child(metadata_tag.c_str()); - if (!metadata_node) { - return outcome; - } - outcome.has_metadata = true; - + void parse_metadata_text_fields(const pugi::xml_node &metadata_node, + docraft::DocraftDocumentMetadata &metadata) { const std::string title_tag = normalize_tag_name(std::string{docraft::craft::elements::metadata::kDocumentTitle}); const std::string author_tag = normalize_tag_name(std::string{docraft::craft::elements::metadata::kAuthor}); const std::string creator_tag = normalize_tag_name(std::string{docraft::craft::elements::metadata::kCreator}); @@ -352,103 +345,141 @@ namespace { const std::string keywords_tag = normalize_tag_name(std::string{docraft::craft::elements::metadata::kKeywords}); const std::string trapped_tag = normalize_tag_name(std::string{docraft::craft::elements::metadata::kTrapped}); const std::string gts_pdfx_tag = normalize_tag_name(std::string{docraft::craft::elements::metadata::kGtsPdfx}); - const std::string creation_date_tag = normalize_tag_name( - std::string{docraft::craft::elements::metadata::kCreationDate}); - const std::string modification_date_tag = normalize_tag_name( - std::string{docraft::craft::elements::metadata::kModificationDate}); - const std::string auto_keywords_tag = normalize_tag_name( - std::string{docraft::craft::elements::metadata::kAutoKeywords}); if (const auto value = read_child_text(metadata_node, title_tag)) { - outcome.metadata.set_title(*value); + metadata.set_title(*value); } if (const auto value = read_child_text(metadata_node, author_tag)) { - outcome.metadata.set_author(*value); + metadata.set_author(*value); } if (const auto value = read_child_text(metadata_node, creator_tag)) { - outcome.metadata.set_creator(*value); + metadata.set_creator(*value); } if (const auto value = read_child_text(metadata_node, producer_tag)) { - outcome.metadata.set_producer(*value); + metadata.set_producer(*value); } if (const auto value = read_child_text(metadata_node, subject_tag)) { - outcome.metadata.set_subject(*value); + metadata.set_subject(*value); } if (const auto value = read_child_text(metadata_node, keywords_tag)) { - outcome.metadata.set_keywords(*value); + metadata.set_keywords(*value); } if (const auto value = read_child_text(metadata_node, trapped_tag)) { - outcome.metadata.set_trapped(*value); + metadata.set_trapped(*value); } if (const auto value = read_child_text(metadata_node, gts_pdfx_tag)) { - outcome.metadata.set_gts_pdfx(*value); + metadata.set_gts_pdfx(*value); } + } + + void parse_metadata_date_fields(const pugi::xml_node &metadata_node, + docraft::DocraftDocumentMetadata &metadata) { + const std::string creation_date_tag = normalize_tag_name( + std::string{docraft::craft::elements::metadata::kCreationDate}); + const std::string modification_date_tag = normalize_tag_name( + std::string{docraft::craft::elements::metadata::kModificationDate}); + if (const pugi::xml_node creation_date_node = metadata_node.child(creation_date_tag.c_str())) { - outcome.metadata.set_creation_date(parse_metadata_date(creation_date_node, creation_date_tag)); + metadata.set_creation_date(parse_metadata_date(creation_date_node, creation_date_tag)); } if (const pugi::xml_node modification_date_node = metadata_node.child(modification_date_tag.c_str())) { - outcome.metadata.set_modification_date(parse_metadata_date(modification_date_node, modification_date_tag)); + metadata.set_modification_date(parse_metadata_date(modification_date_node, modification_date_tag)); } + } - if (const pugi::xml_node auto_keywords_node = metadata_node.child(auto_keywords_tag.c_str())) { - outcome.auto_keyword_config.enabled = true; - if (const pugi::xml_attribute enabled_attr = auto_keywords_node.attribute( - std::string{docraft::craft::elements::metadata::auto_keywords::attribute::kEnabled}.c_str())) { - outcome.auto_keyword_config.enabled = parse_bool_string( - enabled_attr.as_string(), auto_keywords_tag + ".enabled"); - } else { - const std::string node_value = trim_copy(auto_keywords_node.child_value()); - if (!node_value.empty()) { - outcome.auto_keyword_config.enabled = parse_bool_string( - node_value, auto_keywords_tag + " body value"); - } - } + std::vector parse_validated_auto_keyword_languages(const pugi::xml_attribute &language_attr) { + const auto parsed_languages = split_languages(language_attr.as_string()); + if (parsed_languages.empty()) { + throw docraft::exception::InvalidInputException("Metadata AutoKeywords language cannot be empty"); + } - if (const pugi::xml_attribute max_keywords_attr = auto_keywords_node.attribute( - std::string{ - docraft::craft::elements::metadata::auto_keywords::attribute::kMaxKeywords}.c_str())) { - const int max_keywords = max_keywords_attr.as_int(); - if (max_keywords <= 0) { - throw docraft::exception::InvalidInputException( - "Metadata AutoKeywords max_keywords must be greater than 0"); - } - outcome.auto_keyword_config.max_keywords = static_cast(max_keywords); + std::vector validated_languages; + validated_languages.reserve(parsed_languages.size()); + for (const auto &language: parsed_languages) { + std::string normalized = language; + for (char &ch: normalized) { + ch = static_cast(std::tolower(static_cast(ch))); + } + if (!is_supported_stopword_language(normalized)) { + throw docraft::exception::InvalidInputException( + "Unsupported AutoKeywords language '" + language + "'. Supported: it,en,fr,de,es"); } + validated_languages.push_back(normalized); + } + return validated_languages; + } - if (const pugi::xml_attribute min_length_attr = auto_keywords_node.attribute( - std::string{ - docraft::craft::elements::metadata::auto_keywords::attribute::kMinLength}.c_str())) { - const int min_length = min_length_attr.as_int(); - if (min_length <= 0) { - throw docraft::exception::InvalidInputException( - "Metadata AutoKeywords min_length must be greater than 0"); - } - outcome.auto_keyword_config.min_length = static_cast(min_length); + void parse_auto_keyword_numeric_limits(const pugi::xml_node &auto_keywords_node, + DocraftAutoKeywordConfig &config) { + if (const pugi::xml_attribute max_keywords_attr = auto_keywords_node.attribute( + std::string{docraft::craft::elements::metadata::auto_keywords::attribute::kMaxKeywords}.c_str())) { + const int max_keywords = max_keywords_attr.as_int(); + if (max_keywords <= 0) { + throw docraft::exception::InvalidInputException( + "Metadata AutoKeywords max_keywords must be greater than 0"); } + config.max_keywords = static_cast(max_keywords); + } - if (const pugi::xml_attribute language_attr = auto_keywords_node.attribute( - std::string{ - docraft::craft::elements::metadata::auto_keywords::attribute::kLanguage}.c_str())) { - const auto parsed_languages = split_languages(language_attr.as_string()); - if (parsed_languages.empty()) { - throw docraft::exception::InvalidInputException("Metadata AutoKeywords language cannot be empty"); - } - std::vector validated_languages; - for (const auto &language: parsed_languages) { - std::string normalized = language; - for (char &ch: normalized) { - ch = static_cast(std::tolower(static_cast(ch))); - } - if (!is_supported_stopword_language(normalized)) { - throw docraft::exception::InvalidInputException( - "Unsupported AutoKeywords language '" + language + "'. Supported: it,en,fr,de,es"); - } - validated_languages.push_back(normalized); - } - outcome.auto_keyword_config.stop_word_languages = std::move(validated_languages); + if (const pugi::xml_attribute min_length_attr = auto_keywords_node.attribute( + std::string{docraft::craft::elements::metadata::auto_keywords::attribute::kMinLength}.c_str())) { + const int min_length = min_length_attr.as_int(); + if (min_length <= 0) { + throw docraft::exception::InvalidInputException( + "Metadata AutoKeywords min_length must be greater than 0"); } + config.min_length = static_cast(min_length); + } + } + + void parse_auto_keyword_enabled_flag(const pugi::xml_node &auto_keywords_node, + const std::string &auto_keywords_tag, + DocraftAutoKeywordConfig &config) { + config.enabled = true; + if (const pugi::xml_attribute enabled_attr = auto_keywords_node.attribute( + std::string{docraft::craft::elements::metadata::auto_keywords::attribute::kEnabled}.c_str())) { + config.enabled = parse_bool_string(enabled_attr.as_string(), auto_keywords_tag + ".enabled"); + return; + } + + const std::string node_value = trim_copy(auto_keywords_node.child_value()); + if (!node_value.empty()) { + config.enabled = parse_bool_string(node_value, auto_keywords_tag + " body value"); + } + } + + void parse_auto_keyword_config(const pugi::xml_node &metadata_node, + const std::string &auto_keywords_tag, + DocraftAutoKeywordConfig &config) { + const pugi::xml_node auto_keywords_node = metadata_node.child(auto_keywords_tag.c_str()); + if (!auto_keywords_node) { + return; } + parse_auto_keyword_enabled_flag(auto_keywords_node, auto_keywords_tag, config); + parse_auto_keyword_numeric_limits(auto_keywords_node, config); + + if (const pugi::xml_attribute language_attr = auto_keywords_node.attribute( + std::string{docraft::craft::elements::metadata::auto_keywords::attribute::kLanguage}.c_str())) { + config.stop_word_languages = parse_validated_auto_keyword_languages(language_attr); + } + } + + DocraftMetadataParseOutcome parse_metadata_node(const pugi::xml_node &document_node) { + DocraftMetadataParseOutcome outcome; + const std::string metadata_tag = normalize_tag_name(std::string{docraft::craft::section::kMetadata}); + const pugi::xml_node metadata_node = document_node.child(metadata_tag.c_str()); + if (!metadata_node) { + return outcome; + } + outcome.has_metadata = true; + const std::string auto_keywords_tag = normalize_tag_name( + std::string{docraft::craft::elements::metadata::kAutoKeywords}); + + parse_metadata_text_fields(metadata_node, outcome.metadata); + parse_metadata_date_fields(metadata_node, outcome.metadata); + parse_auto_keyword_config(metadata_node, auto_keywords_tag, outcome.auto_keyword_config); + return outcome; } } // namespace From 61282e0827b656fd78a243fec8bbb4ce40cf7eef Mon Sep 17 00:00:00 2001 From: cadons Date: Mon, 15 Jun 2026 23:11:49 +0200 Subject: [PATCH 3/5] refactor(renderer): consolidate polygon shape painting logic into base class --- docraft/CMakeLists.txt | 2 + .../docraft_layout_horizontal_table_handler.h | 10 +-- .../docraft_layout_table_pipeline_overrides.h | 26 ++++++ .../docraft_layout_vertical_table_handler.h | 10 +-- .../painter/docraft_polygon_painter.h | 23 +++-- .../docraft_polygonal_shape_painter_base.h | 46 ++++++++++ .../painter/docraft_triangle_painter.h | 23 +++-- .../services/docraft_navigation_service.h | 62 +------------ .../painter/docraft_polygon_painter.cc | 70 ++++----------- .../docraft_polygonal_shape_painter_base.cc | 81 +++++++++++++++++ .../docraft_polygonal_shape_painter_utils.h | 86 +++++++++++++++++++ .../painter/docraft_triangle_painter.cc | 67 +++------------ .../services/docraft_navigation_service.cc | 65 +------------- 13 files changed, 312 insertions(+), 259 deletions(-) create mode 100644 docraft/include/docraft/layout/handler/docraft_layout_table_pipeline_overrides.h create mode 100644 docraft/include/docraft/renderer/painter/docraft_polygonal_shape_painter_base.h create mode 100644 docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_base.cc create mode 100644 docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_utils.h diff --git a/docraft/CMakeLists.txt b/docraft/CMakeLists.txt index 4615c3b..638ffa1 100644 --- a/docraft/CMakeLists.txt +++ b/docraft/CMakeLists.txt @@ -108,6 +108,8 @@ set(DOCRAFT_SOURCES include/docraft/renderer/painter/docraft_rectangle_painter.h src/docraft/renderer/painter/docraft_circle_painter.cc include/docraft/renderer/painter/docraft_circle_painter.h + src/docraft/renderer/painter/docraft_polygonal_shape_painter_base.cc + include/docraft/renderer/painter/docraft_polygonal_shape_painter_base.h src/docraft/renderer/painter/docraft_triangle_painter.cc include/docraft/renderer/painter/docraft_triangle_painter.h src/docraft/renderer/painter/docraft_line_painter.cc diff --git a/docraft/include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h b/docraft/include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h index 6fe87b9..c1deb44 100644 --- a/docraft/include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h +++ b/docraft/include/docraft/layout/handler/docraft_layout_horizontal_table_handler.h @@ -24,6 +24,7 @@ #include "docraft/docraft_lib.h" #include "docraft/layout/docraft_layout_engine.h" #include "docraft/layout/handler/docraft_layout_table_handler.h" +#include "docraft/layout/handler/docraft_layout_table_pipeline_overrides.h" namespace docraft::layout::handler { /** @@ -49,14 +50,7 @@ namespace docraft::layout::handler { DocraftCursor &cursor) override; protected: - void setup_pipeline_state(const std::shared_ptr &node, - DocraftCursor &cursor) override; - - void prepare_table_layout(const std::shared_ptr &node) override; - - [[nodiscard]] float layout_table_content(const std::shared_ptr &node) override; - - [[nodiscard]] float resolve_table_width() const override; + DOCRAFT_TABLE_PIPELINE_OVERRIDES; private: struct TableContent { diff --git a/docraft/include/docraft/layout/handler/docraft_layout_table_pipeline_overrides.h b/docraft/include/docraft/layout/handler/docraft_layout_table_pipeline_overrides.h new file mode 100644 index 0000000..d55df83 --- /dev/null +++ b/docraft/include/docraft/layout/handler/docraft_layout_table_pipeline_overrides.h @@ -0,0 +1,26 @@ +/* + * 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 + +// Shared override declarations used by horizontal/vertical table handlers. +#define DOCRAFT_TABLE_PIPELINE_OVERRIDES \ + void setup_pipeline_state(const std::shared_ptr &node, \ + DocraftCursor &cursor) override; \ + void prepare_table_layout(const std::shared_ptr &node) override; \ + [[nodiscard]] float layout_table_content(const std::shared_ptr &node) override; \ + [[nodiscard]] float resolve_table_width() const override + diff --git a/docraft/include/docraft/layout/handler/docraft_layout_vertical_table_handler.h b/docraft/include/docraft/layout/handler/docraft_layout_vertical_table_handler.h index 33f4313..c3cdb8e 100644 --- a/docraft/include/docraft/layout/handler/docraft_layout_vertical_table_handler.h +++ b/docraft/include/docraft/layout/handler/docraft_layout_vertical_table_handler.h @@ -23,6 +23,7 @@ #include "docraft/docraft_lib.h" #include "docraft/layout/docraft_layout_engine.h" #include "docraft/layout/handler/docraft_layout_table_handler.h" +#include "docraft/layout/handler/docraft_layout_table_pipeline_overrides.h" namespace docraft::layout::handler { /** @@ -49,14 +50,7 @@ namespace docraft::layout::handler { DocraftCursor &cursor) override; protected: - void setup_pipeline_state(const std::shared_ptr &node, - DocraftCursor &cursor) override; - - void prepare_table_layout(const std::shared_ptr &node) override; - - [[nodiscard]] float layout_table_content(const std::shared_ptr &node) override; - - [[nodiscard]] float resolve_table_width() const override; + DOCRAFT_TABLE_PIPELINE_OVERRIDES; private: struct TableData { diff --git a/docraft/include/docraft/renderer/painter/docraft_polygon_painter.h b/docraft/include/docraft/renderer/painter/docraft_polygon_painter.h index ac9f5f3..7aeeefc 100644 --- a/docraft/include/docraft/renderer/painter/docraft_polygon_painter.h +++ b/docraft/include/docraft/renderer/painter/docraft_polygon_painter.h @@ -18,24 +18,33 @@ #include "docraft/docraft_lib.h" #include "docraft/model/docraft_polygon.h" -#include "docraft/renderer/painter/i_painter.h" +#include "docraft/renderer/painter/docraft_polygonal_shape_painter_base.h" namespace docraft::renderer::painter { /** * @brief Painter that draws DocraftPolygon nodes. */ - class DOCRAFT_LIB DocraftPolygonPainter : public IPainter { + class DOCRAFT_LIB DocraftPolygonPainter : public DocraftPolygonalShapePainterBase { public: /** * @brief Creates a polygon painter bound to the polygon node. * @param polygon_node Polygon node. */ explicit DocraftPolygonPainter(const model::DocraftPolygon &polygon_node); - /** - * @brief Draws the polygon using the provided context. - * @param context Document context. - */ - void draw(const std::shared_ptr &context) override; + + // draw(...) is inherited from DocraftPolygonalShapePainterBase. + + protected: + [[nodiscard]] const std::vector &shape_points() const override; + + [[nodiscard]] const model::DocraftPoint &shape_origin() const override; + + [[nodiscard]] const DocraftColor &shape_background_color() const override; + + [[nodiscard]] const DocraftColor &shape_border_color() const override; + + [[nodiscard]] float shape_border_width() const override; + private: model::DocraftPolygon polygon_node_; }; diff --git a/docraft/include/docraft/renderer/painter/docraft_polygonal_shape_painter_base.h b/docraft/include/docraft/renderer/painter/docraft_polygonal_shape_painter_base.h new file mode 100644 index 0000000..7986727 --- /dev/null +++ b/docraft/include/docraft/renderer/painter/docraft_polygonal_shape_painter_base.h @@ -0,0 +1,46 @@ +/* + * 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 + +#include "docraft/docraft_color.h" +#include "docraft/model/docraft_position.h" +#include "docraft/renderer/painter/i_painter.h" + +namespace docraft::renderer::painter { + /** + * @brief Shared painter base for polygonal shapes (triangle, polygon, ...). + */ + class DOCRAFT_LIB DocraftPolygonalShapePainterBase : public IPainter { + public: + void draw(const std::shared_ptr &context) override; + + protected: + [[nodiscard]] virtual const std::vector &shape_points() const = 0; + + [[nodiscard]] virtual const model::DocraftPoint &shape_origin() const = 0; + + [[nodiscard]] virtual const DocraftColor &shape_background_color() const = 0; + + [[nodiscard]] virtual const DocraftColor &shape_border_color() const = 0; + + [[nodiscard]] virtual float shape_border_width() const = 0; + }; +} // namespace docraft::renderer::painter + diff --git a/docraft/include/docraft/renderer/painter/docraft_triangle_painter.h b/docraft/include/docraft/renderer/painter/docraft_triangle_painter.h index ce78edb..c2558af 100644 --- a/docraft/include/docraft/renderer/painter/docraft_triangle_painter.h +++ b/docraft/include/docraft/renderer/painter/docraft_triangle_painter.h @@ -18,24 +18,33 @@ #include "docraft/docraft_lib.h" #include "docraft/model/docraft_triangle.h" -#include "docraft/renderer/painter/i_painter.h" +#include "docraft/renderer/painter/docraft_polygonal_shape_painter_base.h" namespace docraft::renderer::painter { /** * @brief Painter that draws DocraftTriangle nodes. */ - class DOCRAFT_LIB DocraftTrianglePainter : public IPainter { + class DOCRAFT_LIB DocraftTrianglePainter : public DocraftPolygonalShapePainterBase { public: /** * @brief Creates a triangle painter bound to the triangle node. * @param triangle_node Triangle node. */ explicit DocraftTrianglePainter(const model::DocraftTriangle &triangle_node); - /** - * @brief Draws the triangle using the provided context. - * @param context Document context. - */ - void draw(const std::shared_ptr &context) override; + + // draw(...) is inherited from DocraftPolygonalShapePainterBase. + + protected: + [[nodiscard]] const std::vector &shape_points() const override; + + [[nodiscard]] const model::DocraftPoint &shape_origin() const override; + + [[nodiscard]] const DocraftColor &shape_background_color() const override; + + [[nodiscard]] const DocraftColor &shape_border_color() const override; + + [[nodiscard]] float shape_border_width() const override; + private: model::DocraftTriangle triangle_node_; }; diff --git a/docraft/include/docraft/services/docraft_navigation_service.h b/docraft/include/docraft/services/docraft_navigation_service.h index 60ceb2e..324c106 100644 --- a/docraft/include/docraft/services/docraft_navigation_service.h +++ b/docraft/include/docraft/services/docraft_navigation_service.h @@ -20,12 +20,6 @@ #include "docraft/management/docraft_document_section_manager.h" #include -namespace docraft::model { - class DocraftHeader; - class DocraftBody; - class DocraftFooter; -} - namespace docraft::services { /** * @brief Manages document structure: header, body, footer, and page navigation. @@ -35,11 +29,11 @@ namespace docraft::services { * - Handling section ratios * - Navigating pages */ - class DOCRAFT_LIB NavigationService { + class DOCRAFT_LIB NavigationService : public management::DocraftDocumentSectionManager { public: - NavigationService(); + NavigationService() = default; - ~NavigationService(); + ~NavigationService() = default; /** * @brief Returns the section manager. @@ -47,56 +41,6 @@ namespace docraft::services { management::DocraftDocumentSectionManager §ion_manager(); [[nodiscard]] const management::DocraftDocumentSectionManager §ion_manager() const; - - /** - * @brief Sets the header section. - */ - void set_header(const std::shared_ptr &header); - - /** - * @brief Returns the header. - */ - [[nodiscard]] std::shared_ptr header() const; - - [[nodiscard]] std::shared_ptr edit_header(); - - /** - * @brief Sets the body section. - */ - void set_body(const std::shared_ptr &body); - - /** - * @brief Returns the body. - */ - [[nodiscard]] std::shared_ptr body() const; - - [[nodiscard]] std::shared_ptr edit_body(); - - /** - * @brief Sets the footer section. - */ - void set_footer(const std::shared_ptr &footer); - - /** - * @brief Returns the footer. - */ - [[nodiscard]] std::shared_ptr footer() const; - - [[nodiscard]] std::shared_ptr edit_footer(); - - /** - * @brief Sets section height ratios (header:body:footer). - */ - void set_section_ratios(float header_ratio, float body_ratio, float footer_ratio); - - [[nodiscard]] float header_ratio() const; - - [[nodiscard]] float body_ratio() const; - - [[nodiscard]] float footer_ratio() const; - - private: - std::unique_ptr section_manager_; }; } // namespace docraft::services diff --git a/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc b/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc index 8f42f1f..ff7b08e 100644 --- a/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_polygon_painter.cc @@ -16,66 +16,28 @@ #include "docraft/renderer/painter/docraft_polygon_painter.h" -#include "docraft/backend/docraft_shape_rendering_backend.h" - namespace docraft::renderer::painter { - DocraftPolygonPainter::DocraftPolygonPainter(const model::DocraftPolygon &polygon_node) : polygon_node_(polygon_node) { + DocraftPolygonPainter::DocraftPolygonPainter(const model::DocraftPolygon &polygon_node) : polygon_node_( + polygon_node) { } - void DocraftPolygonPainter::draw(const std::shared_ptr &context) { - if (!context) return; - auto &rendering_service = context->rendering(); - auto shape_backend = rendering_service.shape_rendering(); - auto line_backend = rendering_service.line_rendering(); - if (!shape_backend || !line_backend) return; - - const auto &bg_color = polygon_node_.background_color().toRGB(); - const auto &border_color = polygon_node_.border_color().toRGB(); - float border_width = polygon_node_.border_width(); - - if (bg_color.a <= 0.0F && (border_width <= 0.0F || border_color.a <= 0.0F)) { - return; - } - - const auto &points = polygon_node_.points(); - if (points.size() < 3U) { - return; - } - - std::vector transformed; - transformed.reserve(points.size()); - const auto &origin = polygon_node_.position(); - for (const auto &pt : points) { - transformed.push_back({.x = origin.x + pt.x, .y = origin.y - pt.y}); - } - - shape_backend->save_state(); - - if (bg_color.a < 1.0F || border_color.a < 1.0F) { - shape_backend->set_fill_alpha(bg_color.a); - shape_backend->set_stroke_alpha(border_color.a); - } - - if (border_width > 0.0F) { - line_backend->set_line_width(border_width); - } - - shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); - line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + const std::vector &DocraftPolygonPainter::shape_points() const { + return polygon_node_.points(); + } - shape_backend->draw_polygon(transformed); + const model::DocraftPoint &DocraftPolygonPainter::shape_origin() const { + return polygon_node_.position(); + } - const bool has_fill = bg_color.a > 0.0F; - const bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; + const DocraftColor &DocraftPolygonPainter::shape_background_color() const { + return polygon_node_.background_color(); + } - if (has_fill && has_stroke) { - shape_backend->fill_stroke(); - } else if (has_fill) { - shape_backend->fill(); - } else if (has_stroke) { - shape_backend->stroke(); - } + const DocraftColor &DocraftPolygonPainter::shape_border_color() const { + return polygon_node_.border_color(); + } - shape_backend->restore_state(); + float DocraftPolygonPainter::shape_border_width() const { + return polygon_node_.border_width(); } } // docraft::renderer::painter diff --git a/docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_base.cc b/docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_base.cc new file mode 100644 index 0000000..91359a8 --- /dev/null +++ b/docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_base.cc @@ -0,0 +1,81 @@ +/* + * 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. + */ + +#include "docraft/renderer/painter/docraft_polygonal_shape_painter_base.h" + +#include "docraft/backend/docraft_shape_rendering_backend.h" + +namespace docraft::renderer::painter { + void DocraftPolygonalShapePainterBase::draw(const std::shared_ptr &context) { + if (!context) { + return; + } + + const auto &points = shape_points(); + const auto &origin = shape_origin(); + const auto &background_color = shape_background_color(); + const auto &border_color = shape_border_color(); + const float border_width = shape_border_width(); + + const auto &rendering_service = context->rendering(); + auto shape_backend = rendering_service.shape_rendering(); + auto line_backend = rendering_service.line_rendering(); + if (!shape_backend || !line_backend) { + return; + } + + const auto bg_color = background_color.toRGB(); + const auto stroke_color = border_color.toRGB(); + if (bg_color.a <= 0.0F && (border_width <= 0.0F || stroke_color.a <= 0.0F)) { + return; + } + if (points.size() < 3U) { + return; + } + + std::vector transformed; + transformed.reserve(points.size()); + for (const auto &pt: points) { + transformed.push_back({.x = origin.x + pt.x, .y = origin.y - pt.y}); + } + + shape_backend->save_state(); + if (bg_color.a < 1.0F || stroke_color.a < 1.0F) { + shape_backend->set_fill_alpha(bg_color.a); + shape_backend->set_stroke_alpha(stroke_color.a); + } + if (border_width > 0.0F) { + line_backend->set_line_width(border_width); + } + + shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); + line_backend->set_stroke_color(stroke_color.r, stroke_color.g, stroke_color.b); + shape_backend->draw_polygon(transformed); + + const bool has_fill = bg_color.a > 0.0F; + const bool has_stroke = border_width > 0.0F && stroke_color.a > 0.0F; + if (has_fill && has_stroke) { + shape_backend->fill_stroke(); + } else if (has_fill) { + shape_backend->fill(); + } else if (has_stroke) { + shape_backend->stroke(); + } + + shape_backend->restore_state(); + } +} // namespace docraft::renderer::painter + diff --git a/docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_utils.h b/docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_utils.h new file mode 100644 index 0000000..d437cf2 --- /dev/null +++ b/docraft/src/docraft/renderer/painter/docraft_polygonal_shape_painter_utils.h @@ -0,0 +1,86 @@ +/* + * 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 + +#include "docraft/backend/docraft_shape_rendering_backend.h" +#include "docraft/docraft_document_context.h" + +namespace docraft::renderer::painter { + template + void draw_polygonal_shape(const TShapeNode &shape_node, + const std::shared_ptr &context) { + if (!context) { + return; + } + + const auto &rendering_service = context->rendering(); + auto shape_backend = rendering_service.shape_rendering(); + auto line_backend = rendering_service.line_rendering(); + if (!shape_backend || !line_backend) { + return; + } + + const auto &bg_color = shape_node.background_color().toRGB(); + const auto &border_color = shape_node.border_color().toRGB(); + const float border_width = shape_node.border_width(); + if (bg_color.a <= 0.0F && (border_width <= 0.0F || border_color.a <= 0.0F)) { + return; + } + + const auto &points = shape_node.points(); + if (points.size() < 3U) { + return; + } + + std::vector transformed; + transformed.reserve(points.size()); + const auto &origin = shape_node.position(); + for (const auto &pt: points) { + transformed.push_back({.x = origin.x + pt.x, .y = origin.y - pt.y}); + } + + shape_backend->save_state(); + if (bg_color.a < 1.0F || border_color.a < 1.0F) { + shape_backend->set_fill_alpha(bg_color.a); + shape_backend->set_stroke_alpha(border_color.a); + } + if (border_width > 0.0F) { + line_backend->set_line_width(border_width); + } + + shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); + line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + shape_backend->draw_polygon(transformed); + + const bool has_fill = bg_color.a > 0.0F; + const bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; + if (has_fill && has_stroke) { + shape_backend->fill_stroke(); + } else if (has_fill) { + shape_backend->fill(); + } else if (has_stroke) { + shape_backend->stroke(); + } + + shape_backend->restore_state(); + } +} // namespace docraft::renderer::painter + + diff --git a/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc b/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc index b0392dd..574d894 100644 --- a/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_triangle_painter.cc @@ -16,66 +16,27 @@ #include "docraft/renderer/painter/docraft_triangle_painter.h" -#include "docraft/backend/docraft_shape_rendering_backend.h" - namespace docraft::renderer::painter { DocraftTrianglePainter::DocraftTrianglePainter(const model::DocraftTriangle &triangle_node) : triangle_node_(triangle_node) { } - void DocraftTrianglePainter::draw(const std::shared_ptr &context) { - if (!context) return; - auto &rendering_service = context->rendering(); - auto shape_backend = rendering_service.shape_rendering(); - auto line_backend = rendering_service.line_rendering(); - if (!shape_backend || !line_backend) return; - - const auto &bg_color = triangle_node_.background_color().toRGB(); - const auto &border_color = triangle_node_.border_color().toRGB(); - float border_width = triangle_node_.border_width(); - - if (bg_color.a <= 0.0F && (border_width <= 0.0F || border_color.a <= 0.0F)) { - return; - } - - const auto &points = triangle_node_.points(); - if (points.size() < 3U) { - return; - } - - std::vector transformed; - transformed.reserve(points.size()); - const auto &origin = triangle_node_.position(); - for (const auto &pt : points) { - transformed.push_back({.x = origin.x + pt.x, .y = origin.y - pt.y}); - } - - shape_backend->save_state(); - - if (bg_color.a < 1.0F || border_color.a < 1.0F) { - shape_backend->set_fill_alpha(bg_color.a); - shape_backend->set_stroke_alpha(border_color.a); - } - - if (border_width > 0.0F) { - line_backend->set_line_width(border_width); - } - - shape_backend->set_fill_color(bg_color.r, bg_color.g, bg_color.b); - line_backend->set_stroke_color(border_color.r, border_color.g, border_color.b); + const std::vector &DocraftTrianglePainter::shape_points() const { + return triangle_node_.points(); + } - shape_backend->draw_polygon(transformed); + const model::DocraftPoint &DocraftTrianglePainter::shape_origin() const { + return triangle_node_.position(); + } - const bool has_fill = bg_color.a > 0.0F; - const bool has_stroke = border_width > 0.0F && border_color.a > 0.0F; + const DocraftColor &DocraftTrianglePainter::shape_background_color() const { + return triangle_node_.background_color(); + } - if (has_fill && has_stroke) { - shape_backend->fill_stroke(); - } else if (has_fill) { - shape_backend->fill(); - } else if (has_stroke) { - shape_backend->stroke(); - } + const DocraftColor &DocraftTrianglePainter::shape_border_color() const { + return triangle_node_.border_color(); + } - shape_backend->restore_state(); + float DocraftTrianglePainter::shape_border_width() const { + return triangle_node_.border_width(); } } // docraft::renderer::painter diff --git a/docraft/src/docraft/services/docraft_navigation_service.cc b/docraft/src/docraft/services/docraft_navigation_service.cc index a4ea464..1565827 100644 --- a/docraft/src/docraft/services/docraft_navigation_service.cc +++ b/docraft/src/docraft/services/docraft_navigation_service.cc @@ -15,75 +15,14 @@ */ #include "docraft/services/docraft_navigation_service.h" -#include "docraft/model/docraft_header.h" -#include "docraft/model/docraft_body.h" -#include "docraft/model/docraft_footer.h" namespace docraft::services { - NavigationService::NavigationService() - : section_manager_(std::make_unique()) { - } - - NavigationService::~NavigationService() = default; - management::DocraftDocumentSectionManager &NavigationService::section_manager() { - return *section_manager_; + return *this; } const management::DocraftDocumentSectionManager &NavigationService::section_manager() const { - return *section_manager_; - } - - void NavigationService::set_header(const std::shared_ptr &header) { - section_manager_->set_header(header); - } - - std::shared_ptr NavigationService::header() const { - return section_manager_->header(); - } - - std::shared_ptr NavigationService::edit_header() { - return section_manager_->edit_header(); - } - - void NavigationService::set_body(const std::shared_ptr &body) { - section_manager_->set_body(body); - } - - std::shared_ptr NavigationService::body() const { - return section_manager_->body(); - } - - std::shared_ptr NavigationService::edit_body() { - return section_manager_->edit_body(); - } - - void NavigationService::set_footer(const std::shared_ptr &footer) { - section_manager_->set_footer(footer); - } - - std::shared_ptr NavigationService::footer() const { - return section_manager_->footer(); - } - - std::shared_ptr NavigationService::edit_footer() { - return section_manager_->edit_footer(); - } - - void NavigationService::set_section_ratios(float header_ratio, float body_ratio, float footer_ratio) { - section_manager_->set_section_ratios(header_ratio, body_ratio, footer_ratio); - } - - float NavigationService::header_ratio() const { - return section_manager_->header_ratio(); - } - - float NavigationService::body_ratio() const { - return section_manager_->body_ratio(); - } - - float NavigationService::footer_ratio() const { - return section_manager_->footer_ratio(); + return *this; } } // namespace docraft::services From 7381254b7fe31ce7e5a63892db5d35a4de39fd69 Mon Sep 17 00:00:00 2001 From: cadons Date: Mon, 15 Jun 2026 23:39:59 +0200 Subject: [PATCH 4/5] feat(layout): add line handler for absolute positioning of line nodes --- docraft/CMakeLists.txt | 2 + .../handler/docraft_layout_line_handler.h | 45 ++++++++++++++++ docraft/include/docraft/model/docraft_line.h | 6 +-- .../layout/docraft_layout_engine_impl.cc | 2 + .../handler/docraft_layout_line_handler.cc | 54 +++++++++++++++++++ .../renderer/painter/docraft_line_painter.cc | 25 ++++++--- 6 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 docraft/include/docraft/layout/handler/docraft_layout_line_handler.h create mode 100644 docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc diff --git a/docraft/CMakeLists.txt b/docraft/CMakeLists.txt index 638ffa1..8ce2677 100644 --- a/docraft/CMakeLists.txt +++ b/docraft/CMakeLists.txt @@ -120,6 +120,8 @@ set(DOCRAFT_SOURCES include/docraft/model/docraft_blank_line.h src/docraft/layout/handler/docraft_layout_blank_line.cc include/docraft/layout/handler/docraft_layout_blank_line.h + src/docraft/layout/handler/docraft_layout_line_handler.cc + include/docraft/layout/handler/docraft_layout_line_handler.h src/docraft/renderer/painter/docraft_blank_line_painter.cc include/docraft/renderer/painter/docraft_blank_line_painter.h src/docraft/craft/docraft_craft_language_parser.cc diff --git a/docraft/include/docraft/layout/handler/docraft_layout_line_handler.h b/docraft/include/docraft/layout/handler/docraft_layout_line_handler.h new file mode 100644 index 0000000..146cec8 --- /dev/null +++ b/docraft/include/docraft/layout/handler/docraft_layout_line_handler.h @@ -0,0 +1,45 @@ +/* + * 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 "docraft/docraft_lib.h" +#include "abstract_docraft_layout_handler.h" +#include "docraft/model/docraft_line.h" + +namespace docraft::layout::handler { + /** + * @brief Layout handler for Line nodes. + * + * Lines are drawn at absolute positions within the current layout context. + * Coordinates (x1, y1, x2, y2) are interpreted as global positions within the section, + * not as local offsets relative to the layout cursor. + */ + class DOCRAFT_LIB DocraftLayoutLineHandler : public AbstractDocraftLayoutHandler { + public: + using AbstractDocraftLayoutHandler::AbstractDocraftLayoutHandler; + + void compute(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) override; + + bool handle(const std::shared_ptr &request, + model::DocraftTransform *result, + DocraftCursor &cursor) override; + }; +} // namespace docraft::layout::handler + + diff --git a/docraft/include/docraft/model/docraft_line.h b/docraft/include/docraft/model/docraft_line.h index 514f371..a3d5203 100644 --- a/docraft/include/docraft/model/docraft_line.h +++ b/docraft/include/docraft/model/docraft_line.h @@ -81,9 +81,9 @@ namespace docraft::model { */ float border_width() const; private: - DocraftPoint start_{}; - DocraftPoint end_{}; - DocraftColor border_color_ = DocraftColor(0, 0, 0, 0); + DocraftPoint start_{.x = 0.0F, .y = 0.0F}; + DocraftPoint end_{.x = 100.0F, .y = 0.0F}; + DocraftColor border_color_ = DocraftColor(0, 0, 0, 1); float border_width_ = 1.0F; }; } // docraft::model diff --git a/docraft/src/docraft/layout/docraft_layout_engine_impl.cc b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc index 4972f5d..4480a19 100644 --- a/docraft/src/docraft/layout/docraft_layout_engine_impl.cc +++ b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc @@ -31,6 +31,7 @@ #include "docraft/layout/handler/docraft_basic_layout_handler.h" #include "docraft/layout/handler/docraft_layout_blank_line.h" #include "docraft/layout/handler/docraft_layout_handler.h" +#include "docraft/layout/handler/docraft_layout_line_handler.h" #include "docraft/layout/handler/docraft_layout_list_handler.h" #include "docraft/layout/handler/docraft_layout_table_handler.h" #include "docraft/layout/handler/docraft_layout_text_handler.h" @@ -187,6 +188,7 @@ namespace docraft::layout { handlers_.emplace_back(std::make_unique(context_)); // dispatches internally to horizontal/vertical sub-handlers handlers_.emplace_back(std::make_unique(context_)); + handlers_.emplace_back(std::make_unique(context_)); handlers_.emplace_back(std::make_unique(context_)); list_handler_ = static_cast(handlers_.back().get()); handlers_.emplace_back(std::make_unique(context_)); diff --git a/docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc new file mode 100644 index 0000000..b07a7b8 --- /dev/null +++ b/docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc @@ -0,0 +1,54 @@ +/* + * 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. + */ + +#include "docraft/layout/handler/docraft_layout_line_handler.h" + +namespace docraft::layout::handler { + void DocraftLayoutLineHandler::compute(const std::shared_ptr &node, + model::DocraftTransform *box, + DocraftCursor &cursor) { + if (!box) { + throw docraft::exception::InvalidInputException("result transform is null"); + } + + // In block mode with explicit width: treat as horizontal line from cursor. + // Otherwise: use explicit start/end points (absolute positioning). + if (node->position_mode() == model::DocraftPositionType::kBlock && node->width() > 0.0F) { + box->set_position({.x = cursor.x(), .y = cursor.y()}); + box->set_width(node->width()); + // Provide minimal height so the box occupies vertical space in flow. + box->set_height(node->border_width() + 4.0F); + } + } + + bool DocraftLayoutLineHandler::handle(const std::shared_ptr &request, + model::DocraftTransform *result, + DocraftCursor &cursor) { + auto line_node = std::dynamic_pointer_cast(request); + if (!line_node) { + return false; + } + + compute(line_node, result, cursor); + // Note: cursor advancement is handled by finalize_layout in the layout engine, + // which uses the box height we set in compute(). + return true; + } +} // namespace docraft::layout::handler + + + + diff --git a/docraft/src/docraft/renderer/painter/docraft_line_painter.cc b/docraft/src/docraft/renderer/painter/docraft_line_painter.cc index dcb2fff..c850183 100644 --- a/docraft/src/docraft/renderer/painter/docraft_line_painter.cc +++ b/docraft/src/docraft/renderer/painter/docraft_line_painter.cc @@ -43,16 +43,27 @@ namespace docraft::renderer::painter { line_backend->set_line_width(stroke_width); line_backend->set_stroke_color(stroke_color.r, stroke_color.g, stroke_color.b); - const auto &start = line_node_.start(); - const auto &end = line_node_.end(); const auto &origin = line_node_.position(); - const float x1 = origin.x + start.x; - const float y1 = origin.y + start.y; - const float x2 = origin.x + end.x; - const float y2 = origin.y + end.y; + // If line has width in block mode: draw as horizontal line from origin. + if (line_node_.width() > 0.0F && line_node_.position_mode() == model::DocraftPositionType::kBlock) { + const float x1 = origin.x; + const float y1 = origin.y; + const float x2 = origin.x + line_node_.width(); + const float y2 = origin.y; + line_backend->draw_line(x1, y1, x2, y2); + } else { + // Use explicit start/end points (absolute positioning). + const auto &start = line_node_.start(); + const auto &end = line_node_.end(); - line_backend->draw_line(x1, y1, x2, y2); + const float x1 = origin.x + start.x; + const float y1 = origin.y + start.y; + const float x2 = origin.x + end.x; + const float y2 = origin.y + end.y; + + line_backend->draw_line(x1, y1, x2, y2); + } shape_backend->restore_state(); } From 2fd27ab79c41a0f5567a9b578e783b303e41e9ab Mon Sep 17 00:00:00 2001 From: cadons Date: Tue, 16 Jun 2026 00:19:09 +0200 Subject: [PATCH 5/5] feat(layout): enhance horizontal layout handling for lines and containers --- .../handler/docraft_layout_line_handler.h | 12 +++ .../layout/docraft_layout_engine_impl.cc | 19 ++++ .../handler/docraft_layout_line_handler.cc | 76 +++++++++++++-- .../layout/docraft_layout_engine_test.cc | 95 +++++++++++++++++++ 4 files changed, 195 insertions(+), 7 deletions(-) diff --git a/docraft/include/docraft/layout/handler/docraft_layout_line_handler.h b/docraft/include/docraft/layout/handler/docraft_layout_line_handler.h index 146cec8..e062517 100644 --- a/docraft/include/docraft/layout/handler/docraft_layout_line_handler.h +++ b/docraft/include/docraft/layout/handler/docraft_layout_line_handler.h @@ -16,6 +16,9 @@ #pragma once +#include +#include + #include "docraft/docraft_lib.h" #include "abstract_docraft_layout_handler.h" #include "docraft/model/docraft_line.h" @@ -30,8 +33,17 @@ namespace docraft::layout::handler { */ class DOCRAFT_LIB DocraftLayoutLineHandler : public AbstractDocraftLayoutHandler { public: + struct HorizontalChildPlacement { + std::shared_ptr node; + std::size_t box_index; + }; + using AbstractDocraftLayoutHandler::AbstractDocraftLayoutHandler; + static void align_horizontal_row_lines_to_baseline( + const std::vector &placements, + std::vector &row_boxes); + void compute(const std::shared_ptr &node, model::DocraftTransform *box, DocraftCursor &cursor) override; diff --git a/docraft/src/docraft/layout/docraft_layout_engine_impl.cc b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc index 4480a19..0d602e0 100644 --- a/docraft/src/docraft/layout/docraft_layout_engine_impl.cc +++ b/docraft/src/docraft/layout/docraft_layout_engine_impl.cc @@ -391,6 +391,16 @@ namespace docraft::layout { node->set_position(max_rect.position()); node->set_width(max_rect.width()); node->set_height(max_rect.height()); + + // A Layout container pushes its orientation onto the cursor so that children are arranged + // correctly (horizontal vs vertical). However, the Layout *block itself* must always advance + // the parent flow vertically (like any other block element). Pop the direction that was + // pushed in setup_layout_direction_state before deciding how to move the outer cursor. + const bool is_layout_container = (std::dynamic_pointer_cast(node) != nullptr); + if (is_layout_container) { + layout_cursor.pop_direction(); + } + if (!state.is_absolute && layout_cursor.direction() == DocraftCursorDirection::kHorizontal) { cursor.move_to(max_rect.anchors().top_right.x + kHorizontalSpacing, max_rect.anchors().top_right.y); } else if (!state.is_absolute) { @@ -538,6 +548,7 @@ namespace docraft::layout { const float section_content_bottom) { auto &layout_service = context_->edit_layout(); const float available = compute_horizontal_available_width(max_width, container->children().size()); + std::vector row_placements; for (const auto &child: container->children()) { if (child->z_index() != parent_z_index) { continue; @@ -547,9 +558,17 @@ namespace docraft::layout { const float start_x = cursor.x(); const float start_y = cursor.y(); process_child_layout(child, cursor, out_boxes, section_has_bounds, section_content_bottom); + + if (!out_boxes.empty()) { + row_placements.push_back({.node = child, .box_index = out_boxes.size() - 1}); + } + // Advance the cursor to the next horizontal slot. cursor.move_to(start_x + child_width + kHorizontalSpacing, start_y); } + + // Post-process horizontal placements to align them vertically within the row. + handler::DocraftLayoutLineHandler::align_horizontal_row_lines_to_baseline(row_placements, out_boxes); } // ─────────────────────────────────────────────────────────────────────────── diff --git a/docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc b/docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc index b07a7b8..da30919 100644 --- a/docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc +++ b/docraft/src/docraft/layout/handler/docraft_layout_line_handler.cc @@ -16,7 +16,46 @@ #include "docraft/layout/handler/docraft_layout_line_handler.h" +#include +#include + namespace docraft::layout::handler { + void DocraftLayoutLineHandler::align_horizontal_row_lines_to_baseline( + const std::vector &placements, + std::vector &row_boxes) { + if (placements.empty() || row_boxes.empty()) { + return; + } + + float row_bottom = row_boxes.front().anchors().bottom_left.y; + for (const auto &box: row_boxes) { + row_bottom = std::min(row_bottom, box.anchors().bottom_left.y); + } + + for (const auto &placement: placements) { + if (!placement.node || placement.box_index >= row_boxes.size()) { + continue; + } + + auto line = std::dynamic_pointer_cast(placement.node); + if (!line || line->position_mode() != model::DocraftPositionType::kBlock) { + continue; + } + + auto &line_box = row_boxes[placement.box_index]; + const float stroke_width = std::max(1.0F, line->border_width()); + const float baseline_y = row_bottom + (stroke_width * 0.5F); + + // In block mode, painter uses origin + width; keep local segment flat at origin.y. + line->set_start({.x = 0.0F, .y = 0.0F}); + line->set_end({.x = line->width(), .y = 0.0F}); + line->set_position({.x = line_box.position().x, .y = baseline_y}); + + line_box.set_position({.x = line_box.position().x, .y = baseline_y}); + line_box.set_height(std::max(line_box.height(), stroke_width)); + } + } + void DocraftLayoutLineHandler::compute(const std::shared_ptr &node, model::DocraftTransform *box, DocraftCursor &cursor) { @@ -24,14 +63,37 @@ namespace docraft::layout::handler { throw docraft::exception::InvalidInputException("result transform is null"); } - // In block mode with explicit width: treat as horizontal line from cursor. - // Otherwise: use explicit start/end points (absolute positioning). - if (node->position_mode() == model::DocraftPositionType::kBlock && node->width() > 0.0F) { - box->set_position({.x = cursor.x(), .y = cursor.y()}); - box->set_width(node->width()); - // Provide minimal height so the box occupies vertical space in flow. - box->set_height(node->border_width() + 4.0F); + // Check if the line is positioned absolutely with explicit coordinates + if (node->position_mode() == model::DocraftPositionType::kAbsolute) { + // Absolute positioning: use the node's coordinates as-is + return; + } + + // Block mode: adapt line width to the current layout slot. + // - If node width is set, keep it but clamp to the available slot width. + // - Otherwise derive width from explicit points or fallback to slot width. + const auto &layout_context = edit_context(); + const float allocated_width = std::max(0.0F, layout_context->layout().current_rect_width()); + const float intrinsic_width = std::fabs(node->end().x - node->start().x); + float effective_width = node->width() > 0.0F ? node->width() : allocated_width; + if (effective_width <= 0.0F) { + effective_width = intrinsic_width; } + if (allocated_width > 0.0F) { + effective_width = std::min(effective_width, allocated_width); + } + + box->set_width(effective_width); + + // Center the stroke in a thin line box while keeping the box top aligned with siblings. + const float line_height = std::max(node->border_width(), 4.0F); + const float mid_y = line_height / 2.0F; + + box->set_position({.x = cursor.x(), .y = cursor.y()}); + node->set_start({.x = 0.0F, .y = mid_y}); + node->set_end({.x = effective_width, .y = mid_y}); + // Provide minimal height so the box occupies space in flow + box->set_height(line_height); } bool DocraftLayoutLineHandler::handle(const std::shared_ptr &request, diff --git a/docraft/test/docraft/layout/docraft_layout_engine_test.cc b/docraft/test/docraft/layout/docraft_layout_engine_test.cc index e7de6cc..6bc1e23 100644 --- a/docraft/test/docraft/layout/docraft_layout_engine_test.cc +++ b/docraft/test/docraft/layout/docraft_layout_engine_test.cc @@ -12,6 +12,7 @@ #include "docraft/model/docraft_rectangle.h" #include "docraft/model/docraft_table.h" #include "docraft/model/docraft_text.h" +#include "docraft/model/docraft_line.h" namespace docraft::test::layout { class DocraftLayoutEngineTest : public ::testing::Test { @@ -694,4 +695,98 @@ namespace docraft::test::layout { EXPECT_GT(c01->width(), c00->width()); } + TEST_F(DocraftLayoutEngineTest, ComputeLineInHorizontalLayout) { + auto &engine = this->engine(); + auto context = this->context(); + + // Create a horizontal layout with a line and a rectangle + auto layout_node = std::make_shared(); + layout_node->set_orientation(docraft::model::LayoutOrientation::kHorizontal); + + // Add a line as the first child + auto line = std::make_shared(); + line->set_weight(0.5F); + line->set_border_width(2.0F); + layout_node->add_child(line); + + // Add a rectangle as the second child + auto rect = std::make_shared(); + rect->set_weight(0.5F); + rect->set_height(40); + layout_node->add_child(rect); + + EXPECT_EQ(layout_node->children().size(), 2); + + auto layout = engine->compute_layout(layout_node); + + // Check that the line has been properly laid out + EXPECT_GT(line->width(), 0.0F); + EXPECT_GT(line->height(), 0.0F); + + // Line should have equal Y coordinates for horizontal alignment + EXPECT_FLOAT_EQ(line->start().y, line->end().y); + + // Line end X should be at the allocated width. + // With 2 equal-weight children and one spacing gap between them: + // available = page_width - kHorizontalSpacing_ + // child_width = available * 0.5 (weight = 0.5) + const float allocated_width = (context->layout().page_width() - kHorizontalSpacing_) * 0.5F; + EXPECT_NEAR(line->end().x, allocated_width, 1.0F); + + // Rectangle should be positioned to the right of the line + EXPECT_GT(rect->position().x, line->position().x + line->width()); + + // In horizontal layout, line is baseline-aligned to row bottom. + const float expected_baseline_y = rect->anchors().bottom_left.y + (line->border_width() * 0.5F); + EXPECT_NEAR(line->position().y, expected_baseline_y, 1.0F); + } + + TEST_F(DocraftLayoutEngineTest, ComputeLineWidthClampedToHorizontalSlot) { + auto &engine = this->engine(); + auto context = this->context(); + + auto layout_node = std::make_shared(); + layout_node->set_orientation(docraft::model::LayoutOrientation::kHorizontal); + + auto line = std::make_shared(); + line->set_weight(0.5F); + line->set_width(10000.0F); // Deliberately oversized to verify clamping to slot. + layout_node->add_child(line); + + auto rect = std::make_shared(); + rect->set_weight(0.5F); + rect->set_height(20.0F); + layout_node->add_child(rect); + + (void) engine->compute_layout(layout_node); + + const float allocated_width = (context->layout().page_width() - kHorizontalSpacing_) * 0.5F; + EXPECT_NEAR(line->width(), allocated_width, 1.0F); + EXPECT_NEAR(line->end().x, allocated_width, 1.0F); + const float expected_baseline_y = rect->anchors().bottom_left.y + (line->border_width() * 0.5F); + EXPECT_NEAR(line->position().y, expected_baseline_y, 1.0F); + } + + TEST_F(DocraftLayoutEngineTest, ComputeLineBaselineAlignedWithTextInHorizontalLayout) { + auto &engine = this->engine(); + + auto layout_node = std::make_shared(); + layout_node->set_orientation(docraft::model::LayoutOrientation::kHorizontal); + + auto text = std::make_shared(); + text->set_text("Baseline text"); + text->set_weight(0.5F); + layout_node->add_child(text); + + auto line = std::make_shared(); + line->set_weight(0.5F); + line->set_border_width(2.0F); + layout_node->add_child(line); + + (void) engine->compute_layout(layout_node); + + // In horizontal rows, block lines should sit on the row baseline (text box bottom). + const float expected_baseline_y = text->anchors().bottom_left.y + (line->border_width() * 0.5F); + EXPECT_NEAR(line->position().y, expected_baseline_y, 1.0F); + } }