diff options
Diffstat (limited to 'src/ui/controls')
-rw-r--r-- | src/ui/controls/button.cpp | 33 | ||||
-rw-r--r-- | src/ui/controls/button.h | 41 | ||||
-rw-r--r-- | src/ui/controls/linear_layout.cpp | 178 | ||||
-rw-r--r-- | src/ui/controls/linear_layout.h | 41 | ||||
-rw-r--r-- | src/ui/controls/margin_container.cpp | 63 | ||||
-rw-r--r-- | src/ui/controls/margin_container.h | 44 | ||||
-rw-r--r-- | src/ui/controls/text_block.cpp | 283 | ||||
-rw-r--r-- | src/ui/controls/text_block.h | 116 | ||||
-rw-r--r-- | src/ui/controls/text_box.cpp | 244 | ||||
-rw-r--r-- | src/ui/controls/text_box.h | 85 | ||||
-rw-r--r-- | src/ui/controls/toggle_button.cpp | 150 | ||||
-rw-r--r-- | src/ui/controls/toggle_button.h | 61 |
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, ¤t_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_; + }; +} |