aboutsummaryrefslogtreecommitdiff
path: root/src/ui/render
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/render')
-rw-r--r--src/ui/render/border_render_object.cpp234
-rw-r--r--src/ui/render/border_render_object.hpp92
-rw-r--r--src/ui/render/flex_layout_render_object.cpp237
-rw-r--r--src/ui/render/flex_layout_render_object.hpp59
-rw-r--r--src/ui/render/render_object.cpp115
-rw-r--r--src/ui/render/render_object.hpp97
-rw-r--r--src/ui/render/text_render_object.cpp148
-rw-r--r--src/ui/render/text_render_object.hpp69
-rw-r--r--src/ui/render/window_render_object.cpp46
-rw-r--r--src/ui/render/window_render_object.hpp40
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