diff options
Diffstat (limited to 'src/ui/render')
-rw-r--r-- | src/ui/render/border_render_object.cpp | 234 | ||||
-rw-r--r-- | src/ui/render/border_render_object.hpp | 92 | ||||
-rw-r--r-- | src/ui/render/flex_layout_render_object.cpp | 237 | ||||
-rw-r--r-- | src/ui/render/flex_layout_render_object.hpp | 59 | ||||
-rw-r--r-- | src/ui/render/render_object.cpp | 115 | ||||
-rw-r--r-- | src/ui/render/render_object.hpp | 97 | ||||
-rw-r--r-- | src/ui/render/text_render_object.cpp | 148 | ||||
-rw-r--r-- | src/ui/render/text_render_object.hpp | 69 | ||||
-rw-r--r-- | src/ui/render/window_render_object.cpp | 46 | ||||
-rw-r--r-- | src/ui/render/window_render_object.hpp | 40 |
10 files changed, 1137 insertions, 0 deletions
diff --git a/src/ui/render/border_render_object.cpp b/src/ui/render/border_render_object.cpp new file mode 100644 index 00000000..72cea756 --- /dev/null +++ b/src/ui/render/border_render_object.cpp @@ -0,0 +1,234 @@ +#include "border_render_object.hpp" + +#include <d2d1_1.h> +#include <wrl/client.h> +#include <algorithm> + +#include "cru_debug.hpp" +#include "exception.hpp" +#include "graph/graph_manager.hpp" +#include "graph/graph_util.hpp" +#include "util/com_util.hpp" + +namespace cru::ui::render { +BorderRenderObject::BorderRenderObject(ID2D1Brush* brush) { + assert(brush); + brush->AddRef(); + this->border_brush_ = brush; + try { + RecreateGeometry(); + } catch (...) { + brush->Release(); + throw; + } +} + +BorderRenderObject::~BorderRenderObject() { + util::SafeRelease(border_brush_); + util::SafeRelease(geometry_); + util::SafeRelease(border_outer_geometry_); +} + +void BorderRenderObject::SetBrush(ID2D1Brush* new_brush) { + assert(new_brush); + util::SafeRelease(border_brush_); + new_brush->AddRef(); + border_brush_ = new_brush; +} + +void BorderRenderObject::Draw(ID2D1RenderTarget* render_target) { + render_target->FillGeometry(geometry_, border_brush_); + if (const auto child = GetChild()) { + auto offset = child->GetOffset(); + graph::WithTransform(render_target, + D2D1::Matrix3x2F::Translation(offset.x, offset.y), + [child](auto rt) { child->Draw(rt); }); + } +} + +RenderObject* BorderRenderObject::HitTest(const Point& point) { + if (const auto child = GetChild()) { + auto offset = child->GetOffset(); + Point p{point.x - offset.x, point.y - offset.y}; + const auto result = child->HitTest(point); + if (result != nullptr) { + return result; + } + } + + if (is_enabled_) { + BOOL contains; + ThrowIfFailed(border_outer_geometry_->FillContainsPoint( + D2D1::Point2F(point.x, point.y), D2D1::Matrix3x2F::Identity(), + &contains)); + return contains != 0 ? this : nullptr; + } else { + const auto margin = GetMargin(); + const auto size = GetSize(); + return Rect{margin.left, margin.top, + std::max(size.width - margin.GetHorizontalTotal(), 0.0f), + std::max(size.height - margin.GetVerticalTotal(), 0.0f)} + .IsPointInside(point) + ? this + : nullptr; + } +} + +void BorderRenderObject::OnAddChild(RenderObject* new_child, int position) { + assert(GetChildren().size() == 1); +} + +void BorderRenderObject::OnSizeChanged(const Size& old_size, + const Size& new_size) { + RecreateGeometry(); +} + +void BorderRenderObject::OnMeasureCore(const Size& available_size) { + const auto margin = GetMargin(); + const auto padding = GetPadding(); + Size margin_border_padding_size{ + margin.GetHorizontalTotal() + padding.GetHorizontalTotal(), + margin.GetVerticalTotal() + padding.GetVerticalTotal()}; + + if (is_enabled_) { + margin_border_padding_size.width += border_thickness_.GetHorizontalTotal(); + margin_border_padding_size.height += border_thickness_.GetVerticalTotal(); + } + + auto coerced_margin_border_padding_size = margin_border_padding_size; + if (coerced_margin_border_padding_size.width > available_size.width) { + debug::DebugMessage( + L"Measure: horizontal length of padding, border and margin is bigger " + L"than available length."); + coerced_margin_border_padding_size.width = available_size.width; + } + if (coerced_margin_border_padding_size.height > available_size.height) { + debug::DebugMessage( + L"Measure: vertical length of padding, border and margin is bigger " + L"than available length."); + coerced_margin_border_padding_size.height = available_size.height; + } + + const auto coerced_content_available_size = + available_size - coerced_margin_border_padding_size; + + const auto actual_content_size = + OnMeasureContent(coerced_content_available_size); + + SetPreferredSize(coerced_margin_border_padding_size + actual_content_size); +} + +void BorderRenderObject::OnLayoutCore(const Rect& rect) { + const auto margin = GetMargin(); + const auto padding = GetPadding(); + Size margin_border_padding_size{ + margin.GetHorizontalTotal() + padding.GetHorizontalTotal(), + margin.GetVerticalTotal() + padding.GetVerticalTotal()}; + + if (is_enabled_) { + margin_border_padding_size.width += border_thickness_.GetHorizontalTotal(); + margin_border_padding_size.height += border_thickness_.GetVerticalTotal(); + } + + const auto content_available_size = + rect.GetSize() - margin_border_padding_size; + auto coerced_content_available_size = content_available_size; + + if (coerced_content_available_size.width < 0) { + debug::DebugMessage( + L"Layout: horizontal length of padding, border and margin is bigger " + L"than available length."); + coerced_content_available_size.width = 0; + } + if (coerced_content_available_size.height < 0) { + debug::DebugMessage( + L"Layout: vertical length of padding, border and margin is bigger " + L"than " + L"available length."); + coerced_content_available_size.height = 0; + } + + OnLayoutContent(Rect{ + margin.left + (is_enabled_ ? border_thickness_.left : 0) + padding.left, + margin.top + (is_enabled_ ? border_thickness_.top : 0) + padding.top, + coerced_content_available_size.width, + coerced_content_available_size.height}); +} + +Size BorderRenderObject::OnMeasureContent(const Size& available_size) { + const auto child = GetChild(); + if (child) { + child->Measure(available_size); + return child->GetPreferredSize(); + } else { + return Size::Zero(); + } +} + +void BorderRenderObject::OnLayoutContent(const Rect& content_rect) { + const auto child = GetChild(); + if (child) { + child->Layout(content_rect); + } +} + +void BorderRenderObject::RecreateGeometry() { + util::SafeRelease(geometry_); + util::SafeRelease(border_outer_geometry_); + + const auto d2d_factory = graph::GraphManager::GetInstance()->GetD2D1Factory(); + + Microsoft::WRL::ComPtr<ID2D1PathGeometry> geometry; + ThrowIfFailed(d2d_factory->CreatePathGeometry(&geometry)); + + Microsoft::WRL::ComPtr<ID2D1PathGeometry> border_outer_geometry; + ThrowIfFailed(d2d_factory->CreatePathGeometry(&border_outer_geometry)); + + Microsoft::WRL::ComPtr<ID2D1GeometrySink> sink; + auto f = [&sink](const Rect& rect, const CornerRadius& corner) { + sink->BeginFigure(D2D1::Point2F(rect.left + corner.left_top.x, rect.top), + D2D1_FIGURE_BEGIN_FILLED); + sink->AddLine( + D2D1::Point2F(rect.GetRight() - corner.right_top.x, rect.top)); + sink->AddQuadraticBezier(D2D1::QuadraticBezierSegment( + D2D1::Point2F(rect.GetRight(), rect.top), + D2D1::Point2F(rect.GetRight(), rect.top + corner.right_top.y))); + sink->AddLine(D2D1::Point2F(rect.GetRight(), + rect.GetBottom() - corner.right_bottom.y)); + sink->AddQuadraticBezier(D2D1::QuadraticBezierSegment( + D2D1::Point2F(rect.GetRight(), rect.GetBottom()), + D2D1::Point2F(rect.GetRight() - corner.right_bottom.x, + rect.GetBottom()))); + sink->AddLine( + D2D1::Point2F(rect.left + corner.left_bottom.x, rect.GetBottom())); + sink->AddQuadraticBezier(D2D1::QuadraticBezierSegment( + D2D1::Point2F(rect.left, rect.GetBottom()), + D2D1::Point2F(rect.left, rect.GetBottom() - corner.left_bottom.y))); + sink->AddLine(D2D1::Point2F(rect.left, rect.top + corner.left_top.y)); + sink->AddQuadraticBezier(D2D1::QuadraticBezierSegment( + D2D1::Point2F(rect.left, rect.top), + D2D1::Point2F(rect.left + corner.left_top.x, rect.top))); + sink->EndFigure(D2D1_FIGURE_END_CLOSED); + }; + + const auto size = GetSize(); + const auto margin = GetMargin(); + const Rect outer_rect{margin.left, margin.top, + size.width - margin.GetHorizontalTotal(), + size.height - margin.GetVerticalTotal()}; + ThrowIfFailed(border_outer_geometry->Open(&sink)); + f(outer_rect, corner_radius_); + ThrowIfFailed(sink->Close()); + sink = nullptr; + + const Rect inner_rect = outer_rect.Shrink(border_thickness_); + ThrowIfFailed(geometry->Open(&sink)); + f(outer_rect, corner_radius_); + f(inner_rect, corner_radius_); + ThrowIfFailed(sink->Close()); + sink = nullptr; + + geometry_ = geometry.Detach(); + border_outer_geometry_ = border_outer_geometry.Detach(); +} +} // namespace cru::ui::render diff --git a/src/ui/render/border_render_object.hpp b/src/ui/render/border_render_object.hpp new file mode 100644 index 00000000..80db648a --- /dev/null +++ b/src/ui/render/border_render_object.hpp @@ -0,0 +1,92 @@ +#pragma once +#include "pre.hpp" + +#include <wrl/client.h> // for ComPtr + +#include "render_object.hpp" + +// forward declarations +struct ID2D1Brush; +struct ID2D1Geometry; + +namespace cru::ui::render { +struct CornerRadius { + constexpr CornerRadius() + : left_top(), right_top(), left_bottom(), right_bottom() {} + constexpr CornerRadius(const Point& value) + : left_top(value), + right_top(value), + left_bottom(value), + right_bottom(value) {} + constexpr CornerRadius(Point left_top, Point right_top, Point left_bottom, + Point right_bottom) + : left_top(left_top), + right_top(right_top), + left_bottom(left_bottom), + right_bottom(right_bottom) {} + + Point left_top; + Point right_top; + Point left_bottom; + Point right_bottom; +}; + +class BorderRenderObject : public RenderObject { + public: + explicit BorderRenderObject(ID2D1Brush* brush); + BorderRenderObject(const BorderRenderObject& other) = delete; + BorderRenderObject(BorderRenderObject&& other) = delete; + BorderRenderObject& operator=(const BorderRenderObject& other) = delete; + BorderRenderObject& operator=(BorderRenderObject&& other) = delete; + ~BorderRenderObject() override; + + bool IsEnabled() const { return is_enabled_; } + void SetEnabled(bool enabled) { is_enabled_ = enabled; } + + ID2D1Brush* GetBrush() const { return border_brush_; } + void SetBrush(ID2D1Brush* new_brush); + + Thickness GetBorderWidth() const { return border_thickness_; } + void SetBorderWidth(const Thickness& thickness) { + border_thickness_ = thickness; + } + + CornerRadius GetCornerRadius() const { return corner_radius_; } + void SetCornerRadius(const CornerRadius& new_corner_radius) { + corner_radius_ = new_corner_radius; + } + + void Refresh() { RecreateGeometry(); } + + void Draw(ID2D1RenderTarget* render_target) override; + + RenderObject* HitTest(const Point& point) override; + + protected: + void OnAddChild(RenderObject* new_child, int position) override; + + void OnSizeChanged(const Size& old_size, const Size& new_size) override; + + void OnMeasureCore(const Size& available_size) override; + void OnLayoutCore(const Rect& rect) override; + Size OnMeasureContent(const Size& available_size) override; + void OnLayoutContent(const Rect& content_rect) override; + + private: + RenderObject* GetChild() const { + return GetChildren().empty() ? nullptr : GetChildren()[0]; + } + + void RecreateGeometry(); + + private: + bool is_enabled_ = false; + + ID2D1Brush* border_brush_ = nullptr; + Thickness border_thickness_ = Thickness::Zero(); + CornerRadius corner_radius_{}; + + ID2D1Geometry* geometry_ = nullptr; + ID2D1Geometry* border_outer_geometry_ = nullptr; +}; +} // namespace cru::ui::render diff --git a/src/ui/render/flex_layout_render_object.cpp b/src/ui/render/flex_layout_render_object.cpp new file mode 100644 index 00000000..e4d327f1 --- /dev/null +++ b/src/ui/render/flex_layout_render_object.cpp @@ -0,0 +1,237 @@ +#include "flex_layout_render_object.hpp" + +#include <algorithm> +#include <functional> + +#include "cru_debug.hpp" +#include "graph/graph_util.hpp" + +namespace cru::ui::render { +FlexChildLayoutData* FlexLayoutRenderObject::GetChildLayoutData(int position) { + assert(position >= 0 && + position < child_layout_data_.size()); // Position out of bound. + + return &child_layout_data_[position]; +} + +void FlexLayoutRenderObject::Draw(ID2D1RenderTarget* render_target) { + for (const auto child : GetChildren()) { + auto offset = child->GetOffset(); + graph::WithTransform(render_target, + D2D1::Matrix3x2F::Translation(offset.x, offset.y), + [child](auto rt) { child->Draw(rt); }); + } +} + +RenderObject* FlexLayoutRenderObject::HitTest(const Point& point) { + const auto& children = GetChildren(); + for (auto i = children.crbegin(); i != children.crend(); ++i) { + auto offset = (*i)->GetOffset(); + Point p{point.x - offset.x, point.y - offset.y}; + const auto result = (*i)->HitTest(point); + if (result != nullptr) { + return result; + } + } + + const auto margin = GetMargin(); + const auto size = GetSize(); + return Rect{margin.left, margin.top, + std::max(size.width - margin.GetHorizontalTotal(), 0.0f), + std::max(size.height - margin.GetVerticalTotal(), 0.0f)} + .IsPointInside(point) + ? this + : nullptr; +} // namespace cru::ui::render + +void FlexLayoutRenderObject::OnAddChild(RenderObject* new_child, int position) { + child_layout_data_.emplace(child_layout_data_.cbegin() + position); +} + +void FlexLayoutRenderObject::OnRemoveChild(RenderObject* removed_child, + int position) { + child_layout_data_.erase(child_layout_data_.cbegin() + position); +} + +Size FlexLayoutRenderObject::OnMeasureContent(const Size& available_size) { + std::vector<int> has_basis_children; + std::vector<int> no_basis_children; + std::vector<int> grow_children; + std::vector<int> shrink_chilren; + for (int i = 0; i < child_layout_data_.size(); i++) { + const auto& layout_data = child_layout_data_[i]; + if (layout_data.flex_basis.has_value()) + has_basis_children.push_back(i); + else + no_basis_children.push_back(i); + if (layout_data.flex_grow > 0) grow_children.push_back(i); + if (layout_data.flex_shrink > 0) shrink_chilren.push_back(i); + } + + std::function<float(const Size&)> get_main_length; + std::function<float(const Size&)> get_cross_length; + std::function<Size(float main, float cross)> create_size; + + if (direction_ == FlexDirection::Horizontal || + direction_ == FlexDirection::HorizontalReverse) { + get_main_length = [](const Size& size) { return size.width; }; + get_cross_length = [](const Size& size) { return size.height; }; + create_size = [](float main, float cross) { return Size(main, cross); }; + } else { + get_main_length = [](const Size& size) { return size.height; }; + get_cross_length = [](const Size& size) { return size.width; }; + create_size = [](float main, float cross) { return Size(cross, main); }; + } + + const auto& children = GetChildren(); + + float remain_main_length = get_main_length(available_size); + float max_cross_length = 0; + + for (const int i : has_basis_children) { + const auto child = children[i]; + const float basis = child_layout_data_[i].flex_basis.value(); + child->Measure(create_size(basis, get_cross_length(available_size))); + remain_main_length -= basis; + const float child_preferred_cross_length = + get_cross_length(child->GetPreferredSize()); + child->SetPreferredSize(create_size(basis, child_preferred_cross_length)); + max_cross_length = std::max(max_cross_length, child_preferred_cross_length); + } + + for (const int i : no_basis_children) { + const auto child = children[i]; + child->Measure(create_size(remain_main_length > 0 ? remain_main_length : 0, + get_cross_length(available_size))); + remain_main_length -= get_main_length(child->GetPreferredSize()); + max_cross_length = + std::max(max_cross_length, get_cross_length(child->GetPreferredSize())); + } + + if (remain_main_length > 0) { + float total_grow = 0; + for (const int i : grow_children) + total_grow += child_layout_data_[i].flex_grow; + + for (const int i : grow_children) { + const float distributed_grow_length = + remain_main_length * (child_layout_data_[i].flex_grow / total_grow); + const auto child = children[i]; + const float new_main_length = + get_main_length(child->GetPreferredSize()) + distributed_grow_length; + child->Measure( + create_size(new_main_length, get_cross_length(available_size))); + const float new_child_preferred_cross_length = + get_cross_length(child->GetPreferredSize()); + child->SetPreferredSize( + create_size(new_main_length, new_child_preferred_cross_length)); + max_cross_length = + std::max(max_cross_length, new_child_preferred_cross_length); + } + } + + if (remain_main_length < 0) { + float total_shrink = 0; + for (const int i : shrink_chilren) + total_shrink += child_layout_data_[i].flex_shrink; + + for (const int i : shrink_chilren) { + const float distributed_shrink_length = // negative + remain_main_length * + (child_layout_data_[i].flex_shrink / total_shrink); + const auto child = children[i]; + float new_main_length = get_main_length(child->GetPreferredSize()) + + distributed_shrink_length; + new_main_length = new_main_length > 0 ? new_main_length : 0; + child->Measure( + create_size(new_main_length, get_cross_length(available_size))); + const float new_child_preferred_cross_length = + get_cross_length(child->GetPreferredSize()); + child->SetPreferredSize( + create_size(new_main_length, new_child_preferred_cross_length)); + max_cross_length = + std::max(max_cross_length, new_child_preferred_cross_length); + } + } + + return create_size(get_main_length(available_size) - + (remain_main_length > 0 ? remain_main_length : 0), + max_cross_length); +} + +void FlexLayoutRenderObject::OnLayoutContent(const Rect& content_rect) { + auto calculate_anchor = [](Alignment alignment, float start_point, + float total_length, + float content_length) -> float { + switch (alignment) { + case Alignment::Start: + return start_point; + case Alignment::Center: + return start_point + (total_length - content_length) / 2.0f; + case Alignment::End: + return start_point + total_length - content_length; + default: + UnreachableCode(); + } + }; + + const auto& children = GetChildren(); + if (direction_ == FlexDirection::Horizontal || + direction_ == FlexDirection::HorizontalReverse) { + float actual_content_width = 0; + for (const auto child : children) { + actual_content_width += child->GetPreferredSize().width; + } + + const float content_anchor_x = calculate_anchor( + content_main_align_, 0, content_rect.width, actual_content_width); + + auto anchor_x = 0; + for (int i = 0; i < children.size(); i++) { + const auto child = children[i]; + const auto size = child->GetPreferredSize(); + + float real_anchor_x = anchor_x + content_anchor_x; + if (direction_ == FlexDirection::Horizontal) + real_anchor_x = content_rect.left + real_anchor_x; + else + real_anchor_x = content_rect.GetRight() - real_anchor_x; + child->Layout(Rect{ + real_anchor_x, + calculate_anchor(child_layout_data_[i].alignment, content_rect.top, + content_rect.height, size.height), + size.width, size.height}); + + anchor_x += size.width; + } + } else { + float actual_content_height = 0; + for (const auto child : children) { + actual_content_height = child->GetPreferredSize().height; + } + + const float content_anchor_y = calculate_anchor( + content_main_align_, 0, content_rect.height, actual_content_height); + + auto anchor_y = 0; + for (int i = 0; i < children.size(); i++) { + const auto child = children[i]; + const auto size = child->GetPreferredSize(); + + float real_anchor_y = anchor_y + content_anchor_y; + if (direction_ == FlexDirection::Vertical) { + real_anchor_y = content_rect.top + real_anchor_y; + } else { + real_anchor_y = content_rect.GetBottom() - real_anchor_y; + } + child->Layout(Rect{ + real_anchor_y, + calculate_anchor(child_layout_data_[i].alignment, content_rect.left, + content_rect.width, size.width), + size.width, size.height}); + + anchor_y += size.height; + } + } +} +} // namespace cru::ui::render diff --git a/src/ui/render/flex_layout_render_object.hpp b/src/ui/render/flex_layout_render_object.hpp new file mode 100644 index 00000000..ac4c2c0f --- /dev/null +++ b/src/ui/render/flex_layout_render_object.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "pre.hpp" + +#include <optional> + +#include "render_object.hpp" + +namespace cru::ui::render { +enum class FlexDirection { + Horizontal, + HorizontalReverse, + Vertical, + VertivalReverse +}; + +enum class Alignment { Start, End, Center }; + +struct FlexChildLayoutData { + std::optional<float> flex_basis; // nullopt stands for content + float flex_grow = 0; + float flex_shrink = 0; + Alignment alignment = Alignment::Center; +}; + +class FlexLayoutRenderObject : public RenderObject { + public: + FlexLayoutRenderObject() = default; + FlexLayoutRenderObject(const FlexLayoutRenderObject& other) = delete; + FlexLayoutRenderObject& operator=(const FlexLayoutRenderObject& other) = + delete; + FlexLayoutRenderObject(FlexLayoutRenderObject&& other) = delete; + FlexLayoutRenderObject& operator=(FlexLayoutRenderObject&& other) = delete; + ~FlexLayoutRenderObject() override = default; + + FlexDirection GetFlexDirection() const { return direction_; } + void SetFlexDirection(FlexDirection direction) { direction_ = direction; } + + Alignment GetContentMainAlign() const { return content_main_align_; } + void SetContentMainAlign(Alignment align) { content_main_align_ = align; } + + FlexChildLayoutData* GetChildLayoutData(int position); + + void Draw(ID2D1RenderTarget* render_target) override; + + RenderObject* HitTest(const Point& point) override; + + protected: + void OnAddChild(RenderObject* new_child, int position) override; + void OnRemoveChild(RenderObject* removed_child, int position) override; + + Size OnMeasureContent(const Size& available_size) override; + void OnLayoutContent(const Rect& content_rect) override; + + private: + FlexDirection direction_ = FlexDirection::Horizontal; + Alignment content_main_align_ = Alignment::Start; + std::vector<FlexChildLayoutData> child_layout_data_{}; +}; +} // namespace cru::ui::render diff --git a/src/ui/render/render_object.cpp b/src/ui/render/render_object.cpp new file mode 100644 index 00000000..5c6af580 --- /dev/null +++ b/src/ui/render/render_object.cpp @@ -0,0 +1,115 @@ +#include "render_object.hpp" + +#include <algorithm> + +#include "cru_debug.hpp" + +namespace cru::ui::render { +void RenderObject::AddChild(RenderObject* render_object, const int position) { + assert(render_object->GetParent() == + nullptr); // Render object already has a parent. + assert(position >= 0); // Position index is less than 0. + assert(position <= children_.size()); // Position index is out of bound. + + children_.insert(children_.cbegin() + position, render_object); + render_object->SetParent(this); + OnAddChild(render_object, position); +} + +void RenderObject::RemoveChild(const int position) { + assert(position >= 0); // Position index is less than 0. + assert(position < children_.size()); // Position index is out of bound. + + const auto i = children_.cbegin() + position; + const auto removed_child = *i; + children_.erase(i); + removed_child->SetParent(nullptr); + OnRemoveChild(removed_child, position); +} + +void RenderObject::Measure(const Size& available_size) { + OnMeasureCore(available_size); +} + +void RenderObject::Layout(const Rect& rect) { + SetOffset(rect.GetLeftTop()); + SetSize(rect.GetSize()); + OnLayoutCore(Rect{Point::Zero(), rect.GetSize()}); +} + +void RenderObject::OnParentChanged(RenderObject* old_parent, + RenderObject* new_parent) {} + +void RenderObject::OnAddChild(RenderObject* new_child, int position) {} + +void RenderObject::OnRemoveChild(RenderObject* removed_child, int position) {} + +void RenderObject::OnSizeChanged(const Size& old_size, const Size& new_size) {} + +void RenderObject::OnMeasureCore(const Size& available_size) { + Size margin_padding_size{ + margin_.GetHorizontalTotal() + padding_.GetHorizontalTotal(), + margin_.GetVerticalTotal() + padding_.GetVerticalTotal()}; + + auto coerced_margin_padding_size = margin_padding_size; + if (coerced_margin_padding_size.width > available_size.width) { + debug::DebugMessage( + L"Measure: horizontal length of padding and margin is bigger than " + L"available length."); + coerced_margin_padding_size.width = available_size.width; + } + if (coerced_margin_padding_size.height > available_size.height) { + debug::DebugMessage( + L"Measure: vertical length of padding and margin is bigger than " + L"available length."); + coerced_margin_padding_size.height = available_size.height; + } + + const auto coerced_content_available_size = + available_size - coerced_margin_padding_size; + const auto actual_content_size = + OnMeasureContent(coerced_content_available_size); + + SetPreferredSize(coerced_margin_padding_size + actual_content_size); +} + +void RenderObject::OnLayoutCore(const Rect& rect) { + Size margin_padding_size{ + margin_.GetHorizontalTotal() + padding_.GetHorizontalTotal(), + margin_.GetVerticalTotal() + padding_.GetVerticalTotal()}; + const auto content_available_size = rect.GetSize() - margin_padding_size; + auto coerced_content_available_size = content_available_size; + + if (coerced_content_available_size.width < 0) { + debug::DebugMessage( + L"Layout: horizontal length of padding and margin is bigger than " + L"available length."); + coerced_content_available_size.width = 0; + } + if (coerced_content_available_size.height < 0) { + debug::DebugMessage( + L"Layout: vertical length of padding and margin is bigger than " + L"available length."); + coerced_content_available_size.height = 0; + } + + OnLayoutContent(Rect{margin_.left + padding_.left, margin_.top + padding_.top, + coerced_content_available_size.width, + coerced_content_available_size.height}); +} + +Rect RenderObject::GetContentRect() const { + Rect rect{Point::Zero(), GetSize()}; + rect = rect.Shrink(GetMargin()); + rect = rect.Shrink(GetPadding()); + rect.width = std::max(rect.width, 0.0f); + rect.height = std::max(rect.height, 0.0f); + return rect; +} + +void RenderObject::SetParent(RenderObject* new_parent) { + const auto old_parent = parent_; + parent_ = new_parent; + OnParentChanged(old_parent, new_parent); +} +} // namespace cru::ui::render diff --git a/src/ui/render/render_object.hpp b/src/ui/render/render_object.hpp new file mode 100644 index 00000000..824b88e6 --- /dev/null +++ b/src/ui/render/render_object.hpp @@ -0,0 +1,97 @@ +#pragma once +#include "pre.hpp" + +#include <vector> + +#include "base.hpp" +#include "ui/ui_base.hpp" + +// forward declarations +struct ID2D1RenderTarget; +namespace cru::ui { +class Control; +} + +namespace cru::ui::render { + +class RenderObject : public Object { + protected: + RenderObject() = default; + + public: + RenderObject(const RenderObject& other) = delete; + RenderObject(RenderObject&& other) = delete; + RenderObject& operator=(const RenderObject& other) = delete; + RenderObject& operator=(RenderObject&& other) = delete; + ~RenderObject() override = default; + + Control* GetAttachedControl() const { return control_; } + void SetAttachedControl(Control* new_control) { control_ = new_control; } + + RenderObject* GetParent() const { return parent_; } + + const std::vector<RenderObject*>& GetChildren() const { return children_; } + void AddChild(RenderObject* render_object, int position); + void RemoveChild(int position); + + Point GetOffset() const { return offset_; } + void SetOffset(const Point& offset) { offset_ = offset; } + Size GetSize() const { return size_; } + void SetSize(const Size& size) { + const auto old_size = size_; + size_ = size; + OnSizeChanged(old_size, size); + } + + Thickness GetMargin() const { return margin_; } + void SetMargin(const Thickness& margin) { margin_ = margin; } + + Thickness GetPadding() const { return padding_; } + void SetPadding(const Thickness& padding) { padding_ = padding; } + + Size GetPreferredSize() const { return preferred_size_; } + void SetPreferredSize(const Size& preferred_size) { + preferred_size_ = preferred_size; + } + + void Measure(const Size& available_size); + void Layout(const Rect& rect); + + virtual void Draw(ID2D1RenderTarget* render_target) = 0; + + virtual RenderObject* HitTest(const Point& point) = 0; + + protected: + virtual void OnParentChanged(RenderObject* old_parent, + RenderObject* new_parent); + + virtual void OnAddChild(RenderObject* new_child, int position); + virtual void OnRemoveChild(RenderObject* removed_child, int position); + + virtual void OnSizeChanged(const Size& old_size, const Size& new_size); + + virtual void OnMeasureCore(const Size& available_size); + virtual void OnLayoutCore(const Rect& rect); + virtual Size OnMeasureContent(const Size& available_size) = 0; + virtual void OnLayoutContent(const Rect& content_rect) = 0; + + Rect GetContentRect() const; + + private: + void SetParent(RenderObject* new_parent); + + private: + Control* control_ = nullptr; + + RenderObject* parent_ = nullptr; + std::vector<RenderObject*> children_{}; + + Point offset_ = Point::Zero(); + Size size_ = Size::Zero(); + + Thickness margin_ = Thickness::Zero(); + Thickness padding_ = Thickness::Zero(); + + Size preferred_size_ = Size::Zero(); +}; +} // namespace cru::ui::render diff --git a/src/ui/render/text_render_object.cpp b/src/ui/render/text_render_object.cpp new file mode 100644 index 00000000..69563ad7 --- /dev/null +++ b/src/ui/render/text_render_object.cpp @@ -0,0 +1,148 @@ +#include "text_render_object.hpp" + +#include <d2d1.h> +#include <dwrite.h> +#include <algorithm> + +#include "exception.hpp" +#include "graph/graph_manager.hpp" +#include "graph/graph_util.hpp" +#include "util/com_util.hpp" + +namespace cru::ui::render { +TextRenderObject::TextRenderObject(ID2D1Brush* brush, IDWriteTextFormat* format, + ID2D1Brush* selection_brush) { + assert(brush); + assert(format); + assert(selection_brush); + brush->AddRef(); + format->AddRef(); + selection_brush->AddRef(); + this->brush_ = brush; + this->text_format_ = format; + this->selection_brush_ = selection_brush; + try { + RecreateTextLayout(); + } catch (...) { + brush->Release(); + format->Release(); + selection_brush->Release(); + throw; + } +} + +TextRenderObject::~TextRenderObject() { + util::SafeRelease(brush_); + util::SafeRelease(text_format_); + util::SafeRelease(text_layout_); + util::SafeRelease(selection_brush_); +} + +void TextRenderObject::SetBrush(ID2D1Brush* new_brush) { + assert(new_brush); + util::SafeRelease(brush_); + new_brush->AddRef(); + brush_ = new_brush; +} + +void TextRenderObject::SetTextFormat(IDWriteTextFormat* new_text_format) { + assert(new_text_format); + util::SafeRelease(text_format_); + new_text_format->AddRef(); + text_format_ = new_text_format; + RecreateTextLayout(); +} + +void TextRenderObject::SetSelectionBrush(ID2D1Brush* new_brush) { + assert(new_brush); + util::SafeRelease(selection_brush_); + new_brush->AddRef(); + selection_brush_ = new_brush; +} + +namespace { +void DrawSelectionRect(ID2D1RenderTarget* render_target, + IDWriteTextLayout* layout, ID2D1Brush* brush, + const std::optional<TextRange> range) { + if (range.has_value()) { + DWRITE_TEXT_METRICS text_metrics{}; + ThrowIfFailed(layout->GetMetrics(&text_metrics)); + const auto metrics_count = + text_metrics.lineCount * text_metrics.maxBidiReorderingDepth; + + std::vector<DWRITE_HIT_TEST_METRICS> hit_test_metrics(metrics_count); + UINT32 actual_count; + layout->HitTestTextRange(range.value().position, range.value().count, 0, 0, + hit_test_metrics.data(), metrics_count, + &actual_count); + + hit_test_metrics.erase(hit_test_metrics.cbegin() + actual_count, + hit_test_metrics.cend()); + + for (const auto& metrics : hit_test_metrics) + render_target->FillRoundedRectangle( + D2D1::RoundedRect(D2D1::RectF(metrics.left, metrics.top, + metrics.left + metrics.width, + metrics.top + metrics.height), + 3, 3), + brush); + } +} +} // namespace + +void TextRenderObject::Draw(ID2D1RenderTarget* render_target) { + graph::WithTransform( + render_target, + D2D1::Matrix3x2F::Translation(GetMargin().left + GetPadding().left, + GetMargin().top + GetPadding().top), + [this](auto rt) { + DrawSelectionRect(rt, text_layout_, selection_brush_, selection_range_); + rt->DrawTextLayout(D2D1::Point2F(), text_layout_, brush_); + }); +} + +RenderObject* TextRenderObject::HitTest(const Point& point) { + const auto margin = GetMargin(); + const auto size = GetSize(); + return Rect{margin.left, margin.top, + std::max(size.width - margin.GetHorizontalTotal(), 0.0f), + std::max(size.height - margin.GetVerticalTotal(), 0.0f)} + .IsPointInside(point) + ? this + : nullptr; +} + +void TextRenderObject::OnSizeChanged(const Size& old_size, + const Size& new_size) { + const auto&& size = GetContentRect().GetSize(); + ThrowIfFailed(text_layout_->SetMaxWidth(size.width)); + ThrowIfFailed(text_layout_->SetMaxHeight(size.height)); +} + +Size TextRenderObject::OnMeasureContent(const Size& available_size) { + ThrowIfFailed(text_layout_->SetMaxWidth(available_size.width)); + ThrowIfFailed(text_layout_->SetMaxHeight(available_size.height)); + + DWRITE_TEXT_METRICS metrics; + ThrowIfFailed(text_layout_->GetMetrics(&metrics)); + + return Size(metrics.width, metrics.height); +} + +void TextRenderObject::OnLayoutContent(const Rect& content_rect) {} + +void TextRenderObject::RecreateTextLayout() { + assert(text_format_ != nullptr); + + util::SafeRelease(text_layout_); + + const auto dwrite_factory = + graph::GraphManager::GetInstance()->GetDWriteFactory(); + + const auto&& size = GetContentRect().GetSize(); + + ThrowIfFailed(dwrite_factory->CreateTextLayout( + text_.c_str(), static_cast<UINT32>(text_.size()), text_format_, + size.width, size.height, &text_layout_)); +} +} // namespace cru::ui::render diff --git a/src/ui/render/text_render_object.hpp b/src/ui/render/text_render_object.hpp new file mode 100644 index 00000000..7827f994 --- /dev/null +++ b/src/ui/render/text_render_object.hpp @@ -0,0 +1,69 @@ +#pragma once +#include "pre.hpp" + +#include "render_object.hpp" + +// forward declarations +struct ID2D1Brush; +struct IDWriteTextFormat; +struct IDWriteTextLayout; + +namespace cru::ui::render { +class TextRenderObject : public RenderObject { + public: + TextRenderObject(ID2D1Brush* brush, IDWriteTextFormat* format, + ID2D1Brush* selection_brush); + TextRenderObject(const TextRenderObject& other) = delete; + TextRenderObject(TextRenderObject&& other) = delete; + TextRenderObject& operator=(const TextRenderObject& other) = delete; + TextRenderObject& operator=(TextRenderObject&& other) = delete; + ~TextRenderObject() override; + + String GetText() const { return text_; } + void SetText(String new_text) { + text_ = std::move(new_text); + RecreateTextLayout(); + } + + ID2D1Brush* GetBrush() const { return brush_; } + void SetBrush(ID2D1Brush* new_brush); + + IDWriteTextFormat* GetTextFormat() const { return text_format_; } + void SetTextFormat(IDWriteTextFormat* new_text_format); + + std::optional<TextRange> GetSelectionRange() const { + return selection_range_; + } + void SetSelectionRange(std::optional<TextRange> new_range) { + selection_range_ = std::move(new_range); + } + + ID2D1Brush* GetSelectionBrush() const { return selection_brush_; } + void SetSelectionBrush(ID2D1Brush* new_brush); + + void Refresh() { RecreateTextLayout(); } + + void Draw(ID2D1RenderTarget* render_target) override; + + RenderObject* HitTest(const Point& point) override; + + protected: + void OnSizeChanged(const Size& old_size, const Size& new_size) override; + + Size OnMeasureContent(const Size& available_size) override; + void OnLayoutContent(const Rect& content_rect) override; + + private: + void RecreateTextLayout(); + + private: + String text_; + + ID2D1Brush* brush_ = nullptr; + IDWriteTextFormat* text_format_ = nullptr; + IDWriteTextLayout* text_layout_ = nullptr; + + std::optional<TextRange> selection_range_ = std::nullopt; + ID2D1Brush* selection_brush_ = nullptr; +}; +} // namespace cru::ui::render diff --git a/src/ui/render/window_render_object.cpp b/src/ui/render/window_render_object.cpp new file mode 100644 index 00000000..44c3c426 --- /dev/null +++ b/src/ui/render/window_render_object.cpp @@ -0,0 +1,46 @@ +#include "window_render_object.hpp" + +#include "graph/graph_util.hpp" +#include "ui/window.hpp" + +namespace cru::ui::render { +void WindowRenderObject::MeasureAndLayout() { + const auto client_size = window_->GetClientSize(); + Measure(client_size); + Layout(Rect{Point::Zero(), client_size}); +} + +void WindowRenderObject::Draw(ID2D1RenderTarget* render_target) { + if (const auto child = GetChild()) { + auto offset = child->GetOffset(); + graph::WithTransform(render_target, + D2D1::Matrix3x2F::Translation(offset.x, offset.y), + [child](auto rt) { child->Draw(rt); }); + } +} + +RenderObject* WindowRenderObject::HitTest(const Point& point) { + if (const auto child = GetChild()) { + auto offset = child->GetOffset(); + Point p{point.x - offset.x, point.y - offset.y}; + const auto result = child->HitTest(point); + if (result != nullptr) { + return result; + } + } + return Rect{Point::Zero(), GetSize()}.IsPointInside(point) ? this : nullptr; +} + +void WindowRenderObject::OnAddChild(RenderObject* new_child, int position) { + assert(GetChildren().size() == 1); +} + +Size WindowRenderObject::OnMeasureContent(const Size& available_size) { + if (const auto child = GetChild()) child->Measure(available_size); + return available_size; +} + +void WindowRenderObject::OnLayoutContent(const Rect& content_rect) { + if (const auto child = GetChild()) child->Layout(content_rect); +} +} // namespace cru::ui::render diff --git a/src/ui/render/window_render_object.hpp b/src/ui/render/window_render_object.hpp new file mode 100644 index 00000000..63eb8588 --- /dev/null +++ b/src/ui/render/window_render_object.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "pre.hpp" + +#include "render_object.hpp" + +namespace cru::ui { +class Window; +} + +namespace cru::ui::render { +class WindowRenderObject : public RenderObject { + public: + WindowRenderObject(Window* window) : window_(window) {} + WindowRenderObject(const WindowRenderObject& other) = delete; + WindowRenderObject(WindowRenderObject&& other) = delete; + WindowRenderObject& operator=(const WindowRenderObject& other) = delete; + WindowRenderObject& operator=(WindowRenderObject&& other) = delete; + ~WindowRenderObject() override = default; + + void MeasureAndLayout(); + + void Draw(ID2D1RenderTarget* render_target) override; + + RenderObject* HitTest(const Point& point) override; + + protected: + void OnAddChild(RenderObject* new_child, int position) override; + + Size OnMeasureContent(const Size& available_size) override; + void OnLayoutContent(const Rect& content_rect) override; + + private: + RenderObject* GetChild() const { + return GetChildren().empty() ? nullptr : GetChildren()[0]; + } + + private: + Window* window_; +}; +} // namespace cru::ui::render |