aboutsummaryrefslogtreecommitdiff
path: root/src/ui/controls
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/controls')
-rw-r--r--src/ui/controls/button.cpp33
-rw-r--r--src/ui/controls/button.h41
-rw-r--r--src/ui/controls/linear_layout.cpp178
-rw-r--r--src/ui/controls/linear_layout.h41
-rw-r--r--src/ui/controls/margin_container.cpp63
-rw-r--r--src/ui/controls/margin_container.h44
-rw-r--r--src/ui/controls/text_block.cpp283
-rw-r--r--src/ui/controls/text_block.h116
-rw-r--r--src/ui/controls/text_box.cpp244
-rw-r--r--src/ui/controls/text_box.h85
-rw-r--r--src/ui/controls/toggle_button.cpp150
-rw-r--r--src/ui/controls/toggle_button.h61
12 files changed, 1339 insertions, 0 deletions
diff --git a/src/ui/controls/button.cpp b/src/ui/controls/button.cpp
new file mode 100644
index 00000000..b7614f93
--- /dev/null
+++ b/src/ui/controls/button.cpp
@@ -0,0 +1,33 @@
+#include "button.h"
+
+#include "graph/graph.h"
+
+namespace cru::ui::controls
+{
+ using graph::CreateSolidBrush;
+
+ Button::Button() : Control(true)
+ {
+ normal_border_brush_ = CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::RoyalBlue));
+ pressed_border_brush_ = CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::MediumBlue));
+ current_border_brush_ = normal_border_brush_.Get();
+ }
+
+ void Button::OnDraw(ID2D1DeviceContext* device_context)
+ {
+ Control::OnDraw(device_context);
+ device_context->DrawRoundedRectangle(D2D1::RoundedRect(D2D1::RectF(0, 0, GetSize().width, GetSize().height), 6, 6), current_border_brush_, 2);
+ }
+
+ void Button::OnMouseClickBegin(MouseButton button)
+ {
+ current_border_brush_ = pressed_border_brush_.Get();
+ Repaint();
+ }
+
+ void Button::OnMouseClickEnd(MouseButton button)
+ {
+ current_border_brush_ = normal_border_brush_.Get();
+ Repaint();
+ }
+}
diff --git a/src/ui/controls/button.h b/src/ui/controls/button.h
new file mode 100644
index 00000000..bd3f6eb3
--- /dev/null
+++ b/src/ui/controls/button.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <initializer_list>
+
+#include "ui/control.h"
+
+namespace cru::ui::controls
+{
+ class Button : public Control
+ {
+ public:
+ static Button* Create(const std::initializer_list<Control*>& children = std::initializer_list<Control*>())
+ {
+ const auto button = new Button();
+ for (const auto control : children)
+ button->AddChild(control);
+ return button;
+ }
+
+ protected:
+ Button();
+
+ public:
+ Button(const Button& other) = delete;
+ Button(Button&& other) = delete;
+ Button& operator=(const Button& other) = delete;
+ Button& operator=(Button&& other) = delete;
+ ~Button() override = default;
+
+ protected:
+ void OnDraw(ID2D1DeviceContext* device_context) override;
+
+ void OnMouseClickBegin(MouseButton button) override final;
+ void OnMouseClickEnd(MouseButton button) override final;
+
+ private:
+ Microsoft::WRL::ComPtr<ID2D1Brush> normal_border_brush_;
+ Microsoft::WRL::ComPtr<ID2D1Brush> pressed_border_brush_;
+ ID2D1Brush* current_border_brush_;
+ };
+}
diff --git a/src/ui/controls/linear_layout.cpp b/src/ui/controls/linear_layout.cpp
new file mode 100644
index 00000000..8f537ea8
--- /dev/null
+++ b/src/ui/controls/linear_layout.cpp
@@ -0,0 +1,178 @@
+#include "linear_layout.h"
+
+namespace cru::ui::controls
+{
+ LinearLayout::LinearLayout(const Orientation orientation)
+ : Control(true), orientation_(orientation)
+ {
+
+ }
+
+ inline float AtLeast0(const float value)
+ {
+ return value < 0 ? 0 : value;
+ }
+
+ inline Size AtLeast0(const Size& size)
+ {
+ return Size(AtLeast0(size.width), AtLeast0(size.height));
+ }
+
+ Size LinearLayout::OnMeasure(const Size& available_size)
+ {
+ const auto layout_params = GetLayoutParams();
+
+ if (!layout_params->Validate())
+ throw std::runtime_error("LayoutParams is not valid. Please check it.");
+
+ auto&& get_available_length_for_child = [](const LayoutSideParams& layout_length, const float available_length) -> float
+ {
+ switch (layout_length.mode)
+ {
+ case MeasureMode::Exactly:
+ {
+ return std::min(layout_length.length, available_length);
+ }
+ case MeasureMode::Stretch:
+ case MeasureMode::Content:
+ {
+ return available_length;
+ }
+ default:
+ UnreachableCode();
+ }
+ };
+
+ Size total_available_size_for_children(
+ get_available_length_for_child(layout_params->width, available_size.width),
+ get_available_length_for_child(layout_params->height, available_size.height)
+ );
+
+ auto rest_available_size_for_children = total_available_size_for_children;
+
+ float secondary_side_child_max_length = 0;
+
+ std::list<Control*> stretch_control_list;
+
+ // First measure Content and Exactly and count Stretch.
+ if (orientation_ == Orientation::Horizontal)
+ ForeachChild([&](Control* const control)
+ {
+ const auto mode = control->GetLayoutParams()->width.mode;
+ if (mode == MeasureMode::Content || mode == MeasureMode::Exactly)
+ {
+ control->Measure(AtLeast0(rest_available_size_for_children));
+ const auto size = control->GetDesiredSize();
+ rest_available_size_for_children.width -= size.width;
+ secondary_side_child_max_length = std::max(size.height, secondary_side_child_max_length);
+ }
+ else
+ stretch_control_list.push_back(control);
+ });
+ else
+ ForeachChild([&](Control* const control)
+ {
+ const auto mode = control->GetLayoutParams()->height.mode;
+ if (mode == MeasureMode::Content || mode == MeasureMode::Exactly)
+ {
+ control->Measure(AtLeast0(rest_available_size_for_children));
+ const auto size = control->GetDesiredSize();
+ rest_available_size_for_children.height -= size.height;
+ secondary_side_child_max_length = std::max(size.width, secondary_side_child_max_length);
+ }
+ else
+ stretch_control_list.push_back(control);
+ });
+
+ if (orientation_ == Orientation::Horizontal)
+ {
+ const auto available_width = rest_available_size_for_children.width / stretch_control_list.size();
+ for (const auto control : stretch_control_list)
+ {
+ control->Measure(Size(AtLeast0(available_width), rest_available_size_for_children.height));
+ const auto size = control->GetDesiredSize();
+ rest_available_size_for_children.width -= size.width;
+ secondary_side_child_max_length = std::max(size.height, secondary_side_child_max_length);
+ }
+ }
+ else
+ {
+ const auto available_height = rest_available_size_for_children.height / stretch_control_list.size();
+ for (const auto control : stretch_control_list)
+ {
+ control->Measure(Size(rest_available_size_for_children.width, AtLeast0(available_height)));
+ const auto size = control->GetDesiredSize();
+ rest_available_size_for_children.height -= size.height;
+ secondary_side_child_max_length = std::max(size.width, secondary_side_child_max_length);
+ }
+ }
+
+ auto actual_size_for_children = total_available_size_for_children;
+ if (orientation_ == Orientation::Horizontal)
+ {
+ actual_size_for_children.width -= rest_available_size_for_children.width;
+ actual_size_for_children.height = secondary_side_child_max_length;
+ }
+ else
+ {
+ actual_size_for_children.width = secondary_side_child_max_length;
+ actual_size_for_children.height -= rest_available_size_for_children.height;
+ }
+
+ auto&& calculate_final_length = [](const LayoutSideParams& layout_length, const float length_for_children, const float max_child_length) -> float
+ {
+ switch (layout_length.mode)
+ {
+ case MeasureMode::Exactly:
+ case MeasureMode::Stretch:
+ return length_for_children;
+ case MeasureMode::Content:
+ return max_child_length;
+ default:
+ UnreachableCode();
+ }
+ };
+
+ return Size(
+ calculate_final_length(layout_params->width, total_available_size_for_children.width, actual_size_for_children.width),
+ calculate_final_length(layout_params->height, total_available_size_for_children.height, actual_size_for_children.height)
+ );
+ }
+
+ void LinearLayout::OnLayout(const Rect& rect)
+ {
+ float current_anchor_length = 0;
+ ForeachChild([this, &current_anchor_length, rect](Control* control)
+ {
+ const auto layout_params = control->GetLayoutParams();
+ const auto size = control->GetDesiredSize();
+ const auto alignment = orientation_ == Orientation::Horizontal ? layout_params->height.alignment : layout_params->width.alignment;
+
+ auto&& calculate_anchor = [alignment](const float layout_length, const float control_length) -> float
+ {
+ switch (alignment)
+ {
+ case Alignment::Center:
+ return (layout_length - control_length) / 2;
+ case Alignment::Start:
+ return 0;
+ case Alignment::End:
+ return layout_length - control_length;
+ default:
+ UnreachableCode();
+ }
+ };
+
+ if (orientation_ == Orientation::Horizontal)
+ {
+ control->Layout(Rect(Point(current_anchor_length, calculate_anchor(rect.height, size.height)), size));
+ current_anchor_length += size.width;
+ }
+ else
+ {
+ control->Layout(Rect(Point(calculate_anchor(rect.width, size.width), current_anchor_length), size));
+ current_anchor_length += size.height;
+ }
+ });
+ }
+}
diff --git a/src/ui/controls/linear_layout.h b/src/ui/controls/linear_layout.h
new file mode 100644
index 00000000..369824d4
--- /dev/null
+++ b/src/ui/controls/linear_layout.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "ui/control.h"
+
+namespace cru::ui::controls
+{
+ class LinearLayout : public Control
+ {
+ public:
+ enum class Orientation
+ {
+ Horizontal,
+ Vertical
+ };
+
+ static LinearLayout* Create(const Orientation orientation = Orientation::Vertical, const std::initializer_list<Control*>& children = std::initializer_list<Control*>())
+ {
+ const auto linear_layout = new LinearLayout(orientation);
+ for (const auto control : children)
+ linear_layout->AddChild(control);
+ return linear_layout;
+ }
+
+ protected:
+ explicit LinearLayout(Orientation orientation = Orientation::Vertical);
+
+ public:
+ LinearLayout(const LinearLayout& other) = delete;
+ LinearLayout(LinearLayout&& other) = delete;
+ LinearLayout& operator=(const LinearLayout& other) = delete;
+ LinearLayout& operator=(LinearLayout&& other) = delete;
+ ~LinearLayout() override = default;
+
+ protected:
+ Size OnMeasure(const Size& available_size) override;
+ void OnLayout(const Rect& rect) override;
+
+ private:
+ Orientation orientation_;
+ };
+}
diff --git a/src/ui/controls/margin_container.cpp b/src/ui/controls/margin_container.cpp
new file mode 100644
index 00000000..8f9101b2
--- /dev/null
+++ b/src/ui/controls/margin_container.cpp
@@ -0,0 +1,63 @@
+#include "margin_container.h"
+
+namespace cru::ui::controls
+{
+ inline float AtLeast0(const float value)
+ {
+ return value < 0 ? 0 : value;
+ }
+
+ inline Size AtLeast0(const Size& size)
+ {
+ return Size(AtLeast0(size.width), AtLeast0(size.height));
+ }
+
+ MarginContainer::MarginContainer(const Thickness& margin)
+ : Control(true), margin_(margin)
+ {
+ }
+
+ void MarginContainer::SetMargin(const Thickness& margin)
+ {
+ margin_ = margin;
+ Relayout();
+ }
+
+ Size MarginContainer::OnMeasure(const Size& available_size)
+ {
+ const auto margin_size = Size(margin_.left + margin_.right, margin_.top + margin_.bottom);
+ const auto coerced_available_size = AtLeast0(available_size - margin_size);
+ return Control::OnMeasure(coerced_available_size) + margin_size;
+ }
+
+ void MarginContainer::OnLayout(const Rect& rect)
+ {
+ const auto anchor = Point(margin_.left, margin_.top);
+ const auto margin_size = Size(margin_.left + margin_.right, margin_.top + margin_.bottom);
+ ForeachChild([anchor, margin_size, rect](Control* control)
+ {
+ const auto layout_params = control->GetLayoutParams();
+ const auto size = control->GetDesiredSize();
+
+ auto&& calculate_anchor = [](const float anchor, const Alignment alignment, const float layout_length, const float control_length) -> float
+ {
+ switch (alignment)
+ {
+ case Alignment::Center:
+ return anchor + (layout_length - control_length) / 2;
+ case Alignment::Start:
+ return anchor;
+ case Alignment::End:
+ return anchor + layout_length - control_length;
+ default:
+ UnreachableCode();
+ }
+ };
+
+ control->Layout(Rect(Point(
+ calculate_anchor(anchor.x, layout_params->width.alignment, rect.width - margin_size.width, size.width),
+ calculate_anchor(anchor.y, layout_params->height.alignment, rect.height - margin_size.height, size.height)
+ ), size));
+ });
+ }
+}
diff --git a/src/ui/controls/margin_container.h b/src/ui/controls/margin_container.h
new file mode 100644
index 00000000..0eafc40e
--- /dev/null
+++ b/src/ui/controls/margin_container.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <initializer_list>
+
+#include "ui/control.h"
+
+namespace cru::ui::controls
+{
+ class MarginContainer : public Control
+ {
+ public:
+ static MarginContainer* Create(const Thickness& margin = Thickness::Zero(), const std::initializer_list<Control*>& children = std::initializer_list<Control*>())
+ {
+ const auto margin_container = new MarginContainer(margin);
+ for (const auto control : children)
+ margin_container->AddChild(control);
+ return margin_container;
+ }
+
+ protected:
+ explicit MarginContainer(const Thickness& margin);
+
+ public:
+ MarginContainer(const MarginContainer& other) = delete;
+ MarginContainer(MarginContainer&& other) = delete;
+ MarginContainer& operator=(const MarginContainer& other) = delete;
+ MarginContainer& operator=(MarginContainer&& other) = delete;
+ ~MarginContainer() override = default;
+
+ Thickness GetMargin() const
+ {
+ return margin_;
+ }
+
+ void SetMargin(const Thickness& margin);
+
+ protected:
+ Size OnMeasure(const Size& available_size) override;
+ void OnLayout(const Rect& rect) override;
+
+ private:
+ Thickness margin_;
+ };
+}
diff --git a/src/ui/controls/text_block.cpp b/src/ui/controls/text_block.cpp
new file mode 100644
index 00000000..93d66ba6
--- /dev/null
+++ b/src/ui/controls/text_block.cpp
@@ -0,0 +1,283 @@
+#include "text_block.h"
+
+#include "ui/window.h"
+#include "graph/graph.h"
+#include "exception.h"
+
+namespace cru
+{
+ namespace ui
+ {
+ namespace controls
+ {
+ using graph::CreateSolidBrush;
+
+ inline Microsoft::WRL::ComPtr<IDWriteFactory> GetDWriteFactory()
+ {
+ return graph::GraphManager::GetInstance()->GetDWriteFactory();
+ }
+
+ TextBlock::TextBlock(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& init_text_format,
+ const Microsoft::WRL::ComPtr<ID2D1Brush>& init_brush) : Control(false)
+ {
+ text_format_ = init_text_format == nullptr ? graph::CreateDefaultTextFormat() : init_text_format;
+
+ RecreateTextLayout();
+
+ brush_ = init_brush == nullptr ? CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::Black)) : init_brush;
+
+ selection_brush_ = CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::LightSkyBlue));
+ }
+
+ TextBlock::~TextBlock() = default;
+
+ void TextBlock::SetText(const String& text)
+ {
+ if (text_ != text)
+ {
+ const auto old_text = text_;
+ text_ = text;
+ OnTextChangedCore(old_text, text);
+ }
+ }
+
+ void TextBlock::SetBrush(const Microsoft::WRL::ComPtr<ID2D1Brush>& brush)
+ {
+ brush_ = brush;
+ Repaint();
+ }
+
+ void TextBlock::SetTextFormat(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& text_format)
+ {
+ text_format_ = text_format;
+ RecreateTextLayout();
+ Repaint();
+ }
+
+ void TextBlock::AddTextLayoutHandler(TextLayoutHandlerPtr handler)
+ {
+ text_layout_handlers_.push_back(std::move(handler));
+ }
+
+ void TextBlock::RemoveTextLayoutHandler(const TextLayoutHandlerPtr& handler)
+ {
+ const auto find_result = std::find(text_layout_handlers_.cbegin(), text_layout_handlers_.cend(),
+ handler);
+ if (find_result != text_layout_handlers_.cend())
+ text_layout_handlers_.erase(find_result);
+ }
+
+ void TextBlock::SetSelectable(const bool is_selectable)
+ {
+ if (!is_selectable)
+ {
+ is_selecting_ = false;
+ selected_range_ = std::nullopt;
+ Repaint();
+ }
+ is_selectable_ = is_selectable;
+ }
+
+ void TextBlock::SetSelectedRange(std::optional<TextRange> text_range)
+ {
+ if (is_selectable_)
+ {
+ selected_range_ = text_range;
+ Repaint();
+ }
+ }
+
+ void TextBlock::OnSizeChangedCore(events::SizeChangedEventArgs& args)
+ {
+ Control::OnSizeChangedCore(args);
+ text_layout_->SetMaxWidth(args.GetNewSize().width);
+ text_layout_->SetMaxHeight(args.GetNewSize().height);
+ Repaint();
+ }
+
+ void TextBlock::OnDraw(ID2D1DeviceContext* device_context)
+ {
+ Control::OnDraw(device_context);
+ if (selected_range_.has_value())
+ {
+ DWRITE_TEXT_METRICS text_metrics{};
+ ThrowIfFailed(text_layout_->GetMetrics(&text_metrics));
+ const auto metrics_count = text_metrics.lineCount * text_metrics.maxBidiReorderingDepth;
+
+ Vector<DWRITE_HIT_TEST_METRICS> hit_test_metrics(metrics_count);
+ UINT32 actual_count;
+ text_layout_->HitTestTextRange(
+ selected_range_.value().position, selected_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)
+ {
+ device_context->FillRoundedRectangle(D2D1::RoundedRect(D2D1::RectF(metrics.left, metrics.top, metrics.left + metrics.width, metrics.top + metrics.height), 3, 3), selection_brush_.Get());
+ }
+ }
+ device_context->DrawTextLayout(D2D1::Point2F(), text_layout_.Get(), brush_.Get());
+ }
+
+ namespace
+ {
+ std::optional<unsigned> TextLayoutHitTest(IDWriteTextLayout* text_layout, const Point& point, const bool test_inside = true)
+ {
+ BOOL is_trailing, is_inside;
+ DWRITE_HIT_TEST_METRICS metrics{};
+ text_layout->HitTestPoint(point.x, point.y, &is_trailing, &is_inside, &metrics);
+ if (!test_inside || is_inside)
+ return is_trailing == 0 ? metrics.textPosition : metrics.textPosition + 1;
+ else
+ return std::nullopt;
+ }
+ }
+
+ void TextBlock::OnMouseDownCore(events::MouseButtonEventArgs& args)
+ {
+ Control::OnMouseDownCore(args);
+ if (is_selectable_ && args.GetMouseButton() == MouseButton::Left)
+ {
+ selected_range_ = std::nullopt;
+ const auto hit_test_result = TextLayoutHitTest(text_layout_.Get(), args.GetPoint(this), true);
+ if (hit_test_result.has_value())
+ {
+ mouse_down_position_ = hit_test_result.value();
+ is_selecting_ = true;
+ GetWindow()->CaptureMouseFor(this);
+ }
+ Repaint();
+ }
+ }
+
+ void TextBlock::OnMouseMoveCore(events::MouseEventArgs& args)
+ {
+ Control::OnMouseMoveCore(args);
+ if (is_selecting_)
+ {
+ const auto hit_test_result = TextLayoutHitTest(text_layout_.Get(), args.GetPoint(this), false).value();
+ if (hit_test_result > mouse_down_position_)
+ selected_range_ = TextRange(mouse_down_position_, hit_test_result - mouse_down_position_);
+ else if (hit_test_result < mouse_down_position_)
+ selected_range_ = TextRange(hit_test_result, mouse_down_position_ - hit_test_result);
+ else
+ selected_range_ = std::nullopt;
+ Repaint();
+ }
+ }
+
+ void TextBlock::OnMouseUpCore(events::MouseButtonEventArgs& args)
+ {
+ Control::OnMouseUpCore(args);
+ if (args.GetMouseButton() == MouseButton::Left)
+ {
+ if (is_selecting_)
+ {
+ is_selecting_ = false;
+ GetWindow()->ReleaseCurrentMouseCapture();
+ }
+ }
+ }
+
+ void TextBlock::OnLoseFocusCore(events::FocusChangeEventArgs& args)
+ {
+ Control::OnLoseFocusCore(args);
+ if (is_selecting_)
+ {
+ is_selecting_ = false;
+ GetWindow()->ReleaseCurrentMouseCapture();
+ }
+ if (!args.IsWindow()) // If the focus lose is triggered window-wide, then save the selection state. Otherwise, clear selection.
+ {
+ selected_range_ = std::nullopt;
+ Repaint();
+ }
+ }
+
+ Size TextBlock::OnMeasure(const Size& available_size)
+ {
+ const auto layout_params = GetLayoutParams();
+
+ if (layout_params->width.mode == MeasureMode::Stretch && layout_params->height.mode == MeasureMode::Stretch)
+ return available_size;
+
+ auto&& get_measure_length = [](const LayoutSideParams& layout_length, const float available_length) -> float
+ {
+ switch (layout_length.mode)
+ {
+ case MeasureMode::Exactly:
+ {
+ return std::min(layout_length.length, available_length);
+ }
+ case MeasureMode::Stretch:
+ case MeasureMode::Content:
+ {
+ return available_length;
+ }
+ default:
+ UnreachableCode();
+ }
+ };
+
+ const Size measure_size(get_measure_length(layout_params->width, available_size.width),
+ get_measure_length(layout_params->height, available_size.height));
+
+ ThrowIfFailed(text_layout_->SetMaxWidth(measure_size.width));
+ ThrowIfFailed(text_layout_->SetMaxHeight(measure_size.height));
+
+ DWRITE_TEXT_METRICS metrics{};
+
+ ThrowIfFailed(text_layout_->GetMetrics(&metrics));
+
+ const Size measure_result(metrics.width, metrics.height);
+
+ auto&& calculate_final_length = [](const LayoutSideParams& layout_length, const float measure_length, const float measure_result_length) -> float
+ {
+ if ((layout_length.mode == MeasureMode::Stretch ||
+ layout_length.mode == MeasureMode::Exactly)
+ && measure_result_length < measure_length)
+ return measure_length;
+ else
+ return measure_result_length;
+ };
+
+ const Size result_size(
+ calculate_final_length(layout_params->width, measure_size.width, measure_result.width),
+ calculate_final_length(layout_params->height, measure_size.height, measure_result.height)
+ );
+
+ return result_size;
+ }
+
+ void TextBlock::OnTextChangedCore(const String& old_text, const String& new_text)
+ {
+ RecreateTextLayout();
+ Repaint();
+ }
+
+ void TextBlock::RecreateTextLayout()
+ {
+ assert(text_format_ != nullptr);
+
+ const auto dwrite_factory = GetDWriteFactory();
+
+ const auto&& size = GetSize();
+
+ ThrowIfFailed(dwrite_factory->CreateTextLayout(
+ text_.c_str(), static_cast<UINT32>(text_.size()),
+ text_format_.Get(),
+ size.width, size.height,
+ &text_layout_
+ ));
+
+ std::for_each(text_layout_handlers_.cbegin(), text_layout_handlers_.cend(), [this](const TextLayoutHandlerPtr& handler)
+ {
+ (*handler)(text_layout_);
+ });
+ }
+ }
+ }
+}
diff --git a/src/ui/controls/text_block.h b/src/ui/controls/text_block.h
new file mode 100644
index 00000000..c87ffc51
--- /dev/null
+++ b/src/ui/controls/text_block.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include <memory>
+#include <optional>
+
+#include "ui/control.h"
+
+namespace cru
+{
+ namespace ui
+ {
+ namespace controls
+ {
+ class TextBlock : public Control
+ {
+ public:
+ using TextLayoutHandlerPtr = FunctionPtr<void(Microsoft::WRL::ComPtr<IDWriteTextLayout>)>;
+
+ static TextBlock* Create(
+ const String& text = L"",
+ const Microsoft::WRL::ComPtr<IDWriteTextFormat>& init_text_format = nullptr,
+ const Microsoft::WRL::ComPtr<ID2D1Brush>& init_brush = nullptr)
+ {
+ const auto text_block = new TextBlock(init_text_format, init_brush);
+ text_block->SetText(text);
+ return text_block;
+ }
+
+ protected:
+ explicit TextBlock(
+ const Microsoft::WRL::ComPtr<IDWriteTextFormat>& init_text_format = nullptr,
+ const Microsoft::WRL::ComPtr<ID2D1Brush>& init_brush = nullptr
+ );
+ public:
+ TextBlock(const TextBlock& other) = delete;
+ TextBlock(TextBlock&& other) = delete;
+ TextBlock& operator=(const TextBlock& other) = delete;
+ TextBlock& operator=(TextBlock&& other) = delete;
+ ~TextBlock() override;
+
+ String GetText() const
+ {
+ return text_;
+ }
+
+ void SetText(const String& text);
+
+ Microsoft::WRL::ComPtr<ID2D1Brush> GetBrush() const
+ {
+ return brush_;
+ }
+
+ void SetBrush(const Microsoft::WRL::ComPtr<ID2D1Brush>& brush);
+
+ Microsoft::WRL::ComPtr<IDWriteTextFormat> GetTextFormat() const
+ {
+ return text_format_;
+ }
+
+ void SetTextFormat(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& text_format);
+
+
+ void AddTextLayoutHandler(TextLayoutHandlerPtr handler);
+
+ void RemoveTextLayoutHandler(const TextLayoutHandlerPtr& handler);
+
+ bool IsSelectable() const
+ {
+ return is_selectable_;
+ }
+
+ void SetSelectable(bool is_selectable);
+
+ std::optional<TextRange> GetSelectedRange() const
+ {
+ return selected_range_;
+ }
+
+ void SetSelectedRange(std::optional<TextRange> text_range);
+
+ protected:
+ void OnSizeChangedCore(events::SizeChangedEventArgs& args) override final;
+ void OnDraw(ID2D1DeviceContext* device_context) override;
+
+ void OnMouseDownCore(events::MouseButtonEventArgs& args) override final;
+ void OnMouseMoveCore(events::MouseEventArgs& args) override final;
+ void OnMouseUpCore(events::MouseButtonEventArgs& args) override final;
+
+ void OnLoseFocusCore(events::FocusChangeEventArgs& args) override final;
+
+ Size OnMeasure(const Size& available_size) override final;
+
+ private:
+ void OnTextChangedCore(const String& old_text, const String& new_text);
+
+ void RecreateTextLayout();
+
+ private:
+ String text_;
+
+ Microsoft::WRL::ComPtr<ID2D1Brush> brush_;
+ Microsoft::WRL::ComPtr<ID2D1Brush> selection_brush_;
+ Microsoft::WRL::ComPtr<IDWriteTextFormat> text_format_;
+ Microsoft::WRL::ComPtr<IDWriteTextLayout> text_layout_;
+
+ Vector<TextLayoutHandlerPtr> text_layout_handlers_;
+
+ bool is_selectable_ = false;
+
+ bool is_selecting_ = false;
+ unsigned mouse_down_position_ = 0;
+ std::optional<TextRange> selected_range_ = std::nullopt;
+ };
+ }
+ }
+}
diff --git a/src/ui/controls/text_box.cpp b/src/ui/controls/text_box.cpp
new file mode 100644
index 00000000..a8d78398
--- /dev/null
+++ b/src/ui/controls/text_box.cpp
@@ -0,0 +1,244 @@
+#include "text_box.h"
+
+#include <cwctype>
+
+#include "graph/graph.h"
+#include "exception.h"
+
+namespace cru::ui::controls
+{
+ using graph::CreateSolidBrush;
+
+ inline Microsoft::WRL::ComPtr<IDWriteFactory> GetDWriteFactory()
+ {
+ return graph::GraphManager::GetInstance()->GetDWriteFactory();
+ }
+
+ TextBox::TextBox(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& init_text_format,
+ const Microsoft::WRL::ComPtr<ID2D1Brush>& init_brush) : Control(false)
+ {
+ text_format_ = init_text_format == nullptr ? graph::CreateDefaultTextFormat() : init_text_format;
+
+ RecreateTextLayout();
+
+ brush_ = init_brush == nullptr ? CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::Black)) : init_brush;
+
+ caret_brush_ = CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::Black));
+
+ caret_action_ = CreateActionPtr([this]
+ {
+ is_caret_show_ = !is_caret_show_;
+ Repaint();
+ });
+
+ //selection_brush_ = CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::LightSkyBlue));
+ }
+
+ TextBox::~TextBox() = default;
+
+ void TextBox::SetText(const String& text)
+ {
+ if (text_ != text)
+ {
+ const auto old_text = text_;
+ text_ = text;
+ OnTextChangedCore(old_text, text);
+ }
+ }
+
+ void TextBox::SetBrush(const Microsoft::WRL::ComPtr<ID2D1Brush>& brush)
+ {
+ brush_ = brush;
+ Repaint();
+ }
+
+ void TextBox::SetTextFormat(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& text_format)
+ {
+ text_format_ = text_format;
+ RecreateTextLayout();
+ Repaint();
+ }
+
+ void TextBox::OnSizeChangedCore(events::SizeChangedEventArgs& args)
+ {
+ Control::OnSizeChangedCore(args);
+ text_layout_->SetMaxWidth(args.GetNewSize().width);
+ text_layout_->SetMaxHeight(args.GetNewSize().height);
+ Repaint();
+ }
+
+ void TextBox::OnDraw(ID2D1DeviceContext* device_context)
+ {
+ Control::OnDraw(device_context);
+ if (text_layout_ != nullptr)
+ {
+ device_context->DrawTextLayout(D2D1::Point2F(), text_layout_.Get(), brush_.Get());
+ if (is_caret_show_)
+ {
+ const auto caret_half_width = Application::GetInstance()->GetCaretInfo().half_caret_width;
+ FLOAT x, y;
+ DWRITE_HIT_TEST_METRICS metrics{};
+ ThrowIfFailed(text_layout_->HitTestTextPosition(position_, FALSE, &x, &y, &metrics));
+ device_context->FillRectangle(D2D1::RectF(metrics.left - caret_half_width, metrics.top, metrics.left + caret_half_width, metrics.top + metrics.height), caret_brush_.Get());
+ }
+ }
+ }
+
+ namespace
+ {
+ std::optional<unsigned> TextLayoutHitTest(IDWriteTextLayout* text_layout, const Point& point, bool test_inside = true)
+ {
+ BOOL is_trailing, is_inside;
+ DWRITE_HIT_TEST_METRICS metrics{};
+ text_layout->HitTestPoint(point.x, point.y, &is_trailing, &is_inside, &metrics);
+ if (!test_inside || is_inside)
+ return is_trailing == 0 ? metrics.textPosition : metrics.textPosition + 1;
+ else
+ return std::nullopt;
+ }
+ }
+
+ void TextBox::OnMouseDownCore(events::MouseButtonEventArgs& args)
+ {
+ Control::OnMouseDownCore(args);
+ if (args.GetMouseButton() == MouseButton::Left)
+ {
+ position_ = TextLayoutHitTest(text_layout_.Get(), args.GetPoint(this), false).value();
+
+ Repaint();
+ }
+ }
+
+ void TextBox::OnGetFocusCore(events::FocusChangeEventArgs& args)
+ {
+ Control::OnGetFocusCore(args);
+ assert(caret_timer_ == nullptr);
+ is_caret_show_ = true;
+ caret_timer_ = SetInterval(Application::GetInstance()->GetCaretInfo().caret_blink_duration, caret_action_);
+ }
+
+ void TextBox::OnLoseFocusCore(events::FocusChangeEventArgs& args)
+ {
+ Control::OnLoseFocusCore(args);
+ assert(caret_timer_ != nullptr);
+ caret_timer_->Cancel();
+ is_caret_show_ = false;
+ }
+
+ void TextBox::OnKeyDownCore(events::KeyEventArgs& args)
+ {
+ Control::OnKeyDownCore(args);
+ if (args.GetVirtualCode() == VK_LEFT && position_ > 0)
+ {
+ position_--;
+ Repaint();
+ }
+
+ if (args.GetVirtualCode() == VK_RIGHT && position_ < GetText().size())
+ {
+ position_++;
+ Repaint();
+ }
+ }
+
+ void TextBox::OnCharCore(events::CharEventArgs& args)
+ {
+ Control::OnCharCore(args);
+ if (args.GetChar() == L'\b')
+ {
+ auto text = GetText();
+ if (!text.empty() && position_ > 0)
+ {
+ const auto position = --position_;
+ text.erase(position);
+ SetText(text);
+ }
+ return;
+ }
+
+ if (std::iswprint(args.GetChar()))
+ {
+ const auto position = position_++;
+ auto text = GetText();
+ text.insert(text.cbegin() + position, { args.GetChar() });
+ SetText(text);
+ }
+ }
+
+ Size TextBox::OnMeasure(const Size& available_size)
+ {
+ const auto layout_params = GetLayoutParams();
+
+ if (layout_params->width.mode == MeasureMode::Stretch && layout_params->height.mode == MeasureMode::Stretch)
+ return available_size;
+
+ auto&& get_measure_length = [](const LayoutSideParams& layout_length, const float available_length) -> float
+ {
+ switch (layout_length.mode)
+ {
+ case MeasureMode::Exactly:
+ {
+ return std::min(layout_length.length, available_length);
+ }
+ case MeasureMode::Stretch:
+ case MeasureMode::Content:
+ {
+ return available_length;
+ }
+ default:
+ UnreachableCode();
+ }
+ };
+
+ const Size measure_size(get_measure_length(layout_params->width, available_size.width),
+ get_measure_length(layout_params->height, available_size.height));
+
+ ThrowIfFailed(text_layout_->SetMaxWidth(measure_size.width));
+ ThrowIfFailed(text_layout_->SetMaxHeight(measure_size.height));
+
+ DWRITE_TEXT_METRICS metrics{};
+
+ ThrowIfFailed(text_layout_->GetMetrics(&metrics));
+
+ const Size measure_result(metrics.width, metrics.height);
+
+ auto&& calculate_final_length = [](const LayoutSideParams& layout_length, const float measure_length, const float measure_result_length) -> float
+ {
+ if ((layout_length.mode == MeasureMode::Stretch ||
+ layout_length.mode == MeasureMode::Exactly)
+ && measure_result_length < measure_length)
+ return measure_length;
+ else
+ return measure_result_length;
+ };
+
+ const Size result_size(
+ calculate_final_length(layout_params->width, measure_size.width, measure_result.width),
+ calculate_final_length(layout_params->height, measure_size.height, measure_result.height)
+ );
+
+ return result_size;
+ }
+
+ void TextBox::OnTextChangedCore(const String& old_text, const String& new_text)
+ {
+ RecreateTextLayout();
+ Repaint();
+ }
+
+ void TextBox::RecreateTextLayout()
+ {
+ assert(text_format_ != nullptr);
+
+ const auto dwrite_factory = GetDWriteFactory();
+
+ const auto&& size = GetSize();
+
+ ThrowIfFailed(dwrite_factory->CreateTextLayout(
+ text_.c_str(), static_cast<UINT32>(text_.size()),
+ text_format_.Get(),
+ size.width, size.height,
+ &text_layout_
+ ));
+ }
+}
diff --git a/src/ui/controls/text_box.h b/src/ui/controls/text_box.h
new file mode 100644
index 00000000..b815ed1f
--- /dev/null
+++ b/src/ui/controls/text_box.h
@@ -0,0 +1,85 @@
+#pragma once
+
+#include "ui/control.h"
+#include "timer.h"
+
+namespace cru::ui::controls
+{
+ class TextBox : public Control
+ {
+ public:
+ static TextBox* Create(
+ const Microsoft::WRL::ComPtr<IDWriteTextFormat>& init_text_format = nullptr,
+ const Microsoft::WRL::ComPtr<ID2D1Brush>& init_brush = nullptr)
+ {
+ return new TextBox(init_text_format, init_brush);
+ }
+
+ protected:
+ explicit TextBox(
+ const Microsoft::WRL::ComPtr<IDWriteTextFormat>& init_text_format = nullptr,
+ const Microsoft::WRL::ComPtr<ID2D1Brush>& init_brush = nullptr
+ );
+ public:
+ TextBox(const TextBox& other) = delete;
+ TextBox(TextBox&& other) = delete;
+ TextBox& operator=(const TextBox& other) = delete;
+ TextBox& operator=(TextBox&& other) = delete;
+ ~TextBox() override;
+
+ String GetText() const
+ {
+ return text_;
+ }
+
+ void SetText(const String& text);
+
+ Microsoft::WRL::ComPtr<ID2D1Brush> GetBrush() const
+ {
+ return brush_;
+ }
+
+ void SetBrush(const Microsoft::WRL::ComPtr<ID2D1Brush>& brush);
+
+ Microsoft::WRL::ComPtr<IDWriteTextFormat> GetTextFormat() const
+ {
+ return text_format_;
+ }
+
+ void SetTextFormat(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& text_format);
+
+ protected:
+ void OnSizeChangedCore(events::SizeChangedEventArgs& args) override final;
+ void OnDraw(ID2D1DeviceContext* device_context) override;
+
+ void OnMouseDownCore(events::MouseButtonEventArgs& args) override final;
+
+ void OnGetFocusCore(events::FocusChangeEventArgs& args) override final;
+ void OnLoseFocusCore(events::FocusChangeEventArgs& args) override final;
+
+ void OnKeyDownCore(events::KeyEventArgs& args) override final;
+ void OnCharCore(events::CharEventArgs& args) override final;
+
+ Size OnMeasure(const Size& available_size) override final;
+
+ private:
+ void OnTextChangedCore(const String& old_text, const String& new_text);
+
+ void RecreateTextLayout();
+
+ private:
+ String text_;
+
+ Microsoft::WRL::ComPtr<ID2D1Brush> brush_;
+ Microsoft::WRL::ComPtr<ID2D1Brush> caret_brush_;
+ //Microsoft::WRL::ComPtr<ID2D1Brush> selection_brush_;
+ Microsoft::WRL::ComPtr<IDWriteTextFormat> text_format_;
+ Microsoft::WRL::ComPtr<IDWriteTextLayout> text_layout_;
+
+ unsigned position_ = 0;
+
+ TimerTask caret_timer_;
+ ActionPtr caret_action_;
+ bool is_caret_show_;
+ };
+}
diff --git a/src/ui/controls/toggle_button.cpp b/src/ui/controls/toggle_button.cpp
new file mode 100644
index 00000000..68bd0fc9
--- /dev/null
+++ b/src/ui/controls/toggle_button.cpp
@@ -0,0 +1,150 @@
+#include "toggle_button.h"
+
+#include <fmt/format.h>
+
+#include "graph/graph.h"
+#include "ui/animations/animation.h"
+
+namespace cru::ui::controls
+{
+ using graph::CreateSolidBrush;
+ using animations::AnimationBuilder;
+
+ // ui length parameters of toggle button.
+ constexpr float half_height = 15;
+ constexpr float half_width = half_height * 2;
+ constexpr float stroke_width = 3;
+ constexpr float inner_circle_radius = half_height - stroke_width;
+ constexpr float inner_circle_x = half_width - half_height;
+
+ ToggleButton::ToggleButton() : current_circle_position_(-inner_circle_x)
+ {
+ graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRoundedRectangleGeometry(D2D1::RoundedRect(D2D1::RectF(-half_width, -half_height, half_width, half_height), half_height, half_height), &frame_path_);
+
+ on_brush_ = CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::DeepSkyBlue));
+ off_brush_ = CreateSolidBrush(D2D1::ColorF(D2D1::ColorF::LightGray));
+ }
+
+ inline D2D1_POINT_2F ConvertPoint(const Point& point)
+ {
+ return D2D1::Point2F(point.x, point.y);
+ }
+
+ bool ToggleButton::IsPointInside(const Point& point)
+ {
+ const auto size = GetSize();
+ const auto transform = D2D1::Matrix3x2F::Translation(size.width / 2, size.height / 2);
+ BOOL contains;
+ frame_path_->FillContainsPoint(ConvertPoint(point), transform, &contains);
+ if (!contains)
+ frame_path_->StrokeContainsPoint(ConvertPoint(point), stroke_width, nullptr, transform, &contains);
+ return contains != 0;
+ }
+
+ void ToggleButton::SetState(const bool state)
+ {
+ if (state != state_)
+ {
+ state_ = state;
+ float destination_x;
+
+ if (state)
+ destination_x = inner_circle_x;
+ else
+ destination_x = -inner_circle_x;
+
+ const auto previous_position = current_circle_position_;
+ const auto delta = destination_x - current_circle_position_;
+
+ constexpr auto total_time = FloatSecond(0.2);
+
+ const auto time = total_time * (std::abs(delta) / (inner_circle_x * 2));
+
+ // ReSharper disable once CppExpressionWithoutSideEffects
+ AnimationBuilder(fmt::format(L"ToggleButton {}", reinterpret_cast<size_t>(this)), time)
+ .AddStepHandler(CreatePtr<animations::AnimationStepHandlerPtr>([=](animations::AnimationDelegatePtr, const double percentage)
+ {
+ current_circle_position_ = static_cast<float>(previous_position + delta * percentage);
+ Repaint();
+ })).Start();
+
+ RaiseToggleEvent(state);
+ Repaint();
+ }
+ }
+
+ void ToggleButton::Toggle()
+ {
+ SetState(!GetState());
+ }
+
+ void ToggleButton::OnToggle(events::ToggleEventArgs& args)
+ {
+
+ }
+
+ void ToggleButton::OnDraw(ID2D1DeviceContext* device_context)
+ {
+ Control::OnDraw(device_context);
+ const auto size = GetSize();
+ graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(size.width / 2, size.height / 2), [this](ID2D1DeviceContext* device_context)
+ {
+ if (state_)
+ {
+ device_context->DrawGeometry(frame_path_.Get(), on_brush_.Get(), stroke_width);
+ device_context->FillEllipse(D2D1::Ellipse(D2D1::Point2F(current_circle_position_, 0), inner_circle_radius, inner_circle_radius), on_brush_.Get());
+ }
+ else
+ {
+ device_context->DrawGeometry(frame_path_.Get(), off_brush_.Get(), stroke_width);
+ device_context->FillEllipse(D2D1::Ellipse(D2D1::Point2F(current_circle_position_, 0), inner_circle_radius, inner_circle_radius), off_brush_.Get());
+ }
+ });
+ }
+
+ void ToggleButton::OnMouseClickCore(events::MouseButtonEventArgs& args)
+ {
+ Control::OnMouseClickCore(args);
+ Toggle();
+ }
+
+ Size ToggleButton::OnMeasure(const Size& available_size)
+ {
+ const auto layout_params = GetLayoutParams();
+
+ auto&& get_measure_length = [](const LayoutSideParams& layout_length, const float available_length, const float fix_length) -> float
+ {
+ switch (layout_length.mode)
+ {
+ case MeasureMode::Exactly:
+ {
+ return std::max(std::min(layout_length.length, available_length), fix_length);
+ }
+ case MeasureMode::Stretch:
+ {
+ return std::max(available_length, fix_length);
+ }
+ case MeasureMode::Content:
+ {
+ return fix_length;
+ }
+ default:
+ UnreachableCode();
+ }
+ };
+
+ const Size result_size(
+ get_measure_length(layout_params->width, available_size.width, half_width * 2 + stroke_width),
+ get_measure_length(layout_params->height, available_size.height, half_height * 2 + stroke_width)
+ );
+
+ return result_size;
+ }
+
+ void ToggleButton::RaiseToggleEvent(bool new_state)
+ {
+ events::ToggleEventArgs args(this, this, new_state);
+ OnToggle(args);
+ toggle_event.Raise(args);
+ }
+}
diff --git a/src/ui/controls/toggle_button.h b/src/ui/controls/toggle_button.h
new file mode 100644
index 00000000..d496f21a
--- /dev/null
+++ b/src/ui/controls/toggle_button.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "ui/control.h"
+
+namespace cru::ui::controls
+{
+ class ToggleButton : public Control
+ {
+ public:
+ static ToggleButton* Create()
+ {
+ return new ToggleButton();
+ }
+
+ protected:
+ ToggleButton();
+
+ public:
+ ToggleButton(const ToggleButton& other) = delete;
+ ToggleButton(ToggleButton&& other) = delete;
+ ToggleButton& operator=(const ToggleButton& other) = delete;
+ ToggleButton& operator=(ToggleButton&& other) = delete;
+ ~ToggleButton() override = default;
+
+ bool IsPointInside(const Point& point) override;
+
+ bool GetState() const
+ {
+ return state_;
+ }
+
+ void SetState(bool state);
+
+ void Toggle();
+
+ public:
+ events::ToggleEvent toggle_event;
+
+ protected:
+ virtual void OnToggle(events::ToggleEventArgs& args);
+
+ protected:
+ void OnDraw(ID2D1DeviceContext* device_context) override;
+
+ void OnMouseClickCore(events::MouseButtonEventArgs& args) override;
+
+ Size OnMeasure(const Size& available_size) override;
+
+ private:
+ void RaiseToggleEvent(bool new_state);
+
+ private:
+ bool state_ = false;
+
+ float current_circle_position_;
+
+ Microsoft::WRL::ComPtr<ID2D1RoundedRectangleGeometry> frame_path_;
+ Microsoft::WRL::ComPtr<ID2D1Brush> on_brush_;
+ Microsoft::WRL::ComPtr<ID2D1Brush> off_brush_;
+ };
+}