aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2018-09-25 13:08:40 +0800
committercrupest <crupest@outlook.com>2018-09-25 13:08:40 +0800
commit4b86554a0354d78efeb40e551eaccaac0fecd1d1 (patch)
treec8a73d848401f523ff91fe8ed1b0887aa88bbfb8 /src/ui
parentcea138417c54d6cf8043b6334c22e3af957d26f8 (diff)
downloadcru-4b86554a0354d78efeb40e551eaccaac0fecd1d1.tar.gz
cru-4b86554a0354d78efeb40e551eaccaac0fecd1d1.tar.bz2
cru-4b86554a0354d78efeb40e551eaccaac0fecd1d1.zip
Change the structure of project.
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/animations/animation.cpp190
-rw-r--r--src/ui/animations/animation.h107
-rw-r--r--src/ui/control.cpp712
-rw-r--r--src/ui/control.h380
-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
-rw-r--r--src/ui/events/ui_event.cpp19
-rw-r--r--src/ui/events/ui_event.h317
-rw-r--r--src/ui/layout_base.cpp79
-rw-r--r--src/ui/layout_base.h136
-rw-r--r--src/ui/ui_base.cpp6
-rw-r--r--src/ui/ui_base.h154
-rw-r--r--src/ui/window.cpp606
-rw-r--r--src/ui/window.h276
24 files changed, 4321 insertions, 0 deletions
diff --git a/src/ui/animations/animation.cpp b/src/ui/animations/animation.cpp
new file mode 100644
index 00000000..9d05860a
--- /dev/null
+++ b/src/ui/animations/animation.cpp
@@ -0,0 +1,190 @@
+#include "animation.h"
+
+#include <cassert>
+#include <utility>
+
+namespace cru::ui::animations
+{
+ namespace details
+ {
+ class AnimationDelegateImpl;
+ constexpr double frame_rate = 60;
+ constexpr AnimationTimeUnit frame_step_time = AnimationTimeUnit(1) / frame_rate;
+
+
+ class AnimationDelegateImpl : public virtual IAnimationDelegate
+ {
+ public:
+ explicit AnimationDelegateImpl(String tag)
+ : tag_(std::move(tag))
+ {
+
+ }
+ AnimationDelegateImpl(const AnimationDelegateImpl& other) = delete;
+ AnimationDelegateImpl(AnimationDelegateImpl&& other) = delete;
+ AnimationDelegateImpl& operator=(const AnimationDelegateImpl& other) = delete;
+ AnimationDelegateImpl& operator=(AnimationDelegateImpl&& other) = delete;
+ ~AnimationDelegateImpl() override = default;
+
+ void Cancel() override
+ {
+ AnimationManager::GetInstance()->RemoveAnimation(tag_);
+ }
+
+ private:
+ String tag_;
+ };
+
+
+ class Animation : public Object
+ {
+ public:
+ Animation(
+ String tag,
+ AnimationTimeUnit duration,
+ Vector<AnimationStepHandlerPtr> step_handlers,
+ Vector<AnimationStartHandlerPtr> start_handlers,
+ Vector<ActionPtr> finish_handlers,
+ Vector<ActionPtr> cancel_handlers,
+ AnimationDelegatePtr delegate
+ );
+ Animation(const Animation& other) = delete;
+ Animation(Animation&& other) = delete;
+ Animation& operator=(const Animation& other) = delete;
+ Animation& operator=(Animation&& other) = delete;
+ ~Animation() override;
+
+
+ // If finish or invalid, return false.
+ bool Step(AnimationTimeUnit time);
+
+ String GetTag() const
+ {
+ return tag_;
+ }
+
+ private:
+ const String tag_;
+ const AnimationTimeUnit duration_;
+ Vector<AnimationStepHandlerPtr> step_handlers_;
+ Vector<AnimationStartHandlerPtr> start_handlers_;
+ Vector<ActionPtr> finish_handlers_;
+ Vector<ActionPtr> cancel_handlers_;
+ AnimationDelegatePtr delegate_;
+
+ AnimationTimeUnit current_time_ = AnimationTimeUnit::zero();
+ };
+
+ AnimationManager::AnimationManager()
+ : timer_action_(CreateActionPtr([this]()
+ {
+ auto i = animations_.cbegin();
+ while (i != animations_.cend())
+ {
+ auto current_i = i++;
+ if (current_i->second->Step(frame_step_time))
+ animations_.erase(current_i);
+ }
+
+ if (animations_.empty())
+ KillTimer();
+ }))
+ {
+
+ }
+
+ AnimationManager::~AnimationManager()
+ {
+ KillTimer();
+ }
+
+ AnimationDelegatePtr AnimationManager::CreateAnimation(String tag, AnimationTimeUnit duration,
+ Vector<AnimationStepHandlerPtr> step_handlers, Vector<AnimationStartHandlerPtr> start_handlers,
+ Vector<ActionPtr> finish_handlers, Vector<ActionPtr> cancel_handlers)
+ {
+ if (animations_.empty())
+ SetTimer();
+
+ auto delegate = std::make_shared<AnimationDelegateImpl>(tag);
+
+ animations_[tag] = std::make_unique<Animation>(tag, duration, std::move(step_handlers), std::move(start_handlers), std::move(finish_handlers), std::move(cancel_handlers), delegate);
+
+ return delegate;
+ }
+
+ void AnimationManager::RemoveAnimation(const String& tag)
+ {
+ const auto find_result = animations_.find(tag);
+ if (find_result != animations_.cend())
+ animations_.erase(find_result);
+
+ if (animations_.empty())
+ KillTimer();
+ }
+
+ void AnimationManager::SetTimer()
+ {
+ if (timer_ == nullptr)
+ timer_ = SetInterval(std::chrono::duration_cast<std::chrono::milliseconds>(frame_step_time), timer_action_);
+ }
+
+ void AnimationManager::KillTimer()
+ {
+ if (timer_ != nullptr)
+ {
+ timer_->Cancel();
+ timer_ = nullptr;
+ }
+ }
+
+ Animation::Animation(
+ String tag,
+ AnimationTimeUnit duration,
+ Vector<AnimationStepHandlerPtr> step_handlers,
+ Vector<AnimationStartHandlerPtr> start_handlers,
+ Vector<ActionPtr> finish_handlers,
+ Vector<ActionPtr> cancel_handlers,
+ AnimationDelegatePtr delegate
+ ) : tag_(std::move(tag)), duration_(duration),
+ step_handlers_(std::move(step_handlers)),
+ start_handlers_(std::move(start_handlers)),
+ finish_handlers_(std::move(finish_handlers)),
+ cancel_handlers_(std::move(cancel_handlers)),
+ delegate_(std::move(delegate))
+ {
+
+ }
+
+ Animation::~Animation()
+ {
+ if (current_time_ < duration_)
+ for (auto& handler : cancel_handlers_)
+ (*handler)();
+ }
+
+ bool Animation::Step(const AnimationTimeUnit time)
+ {
+ current_time_ += time;
+ if (current_time_ > duration_)
+ {
+ for (auto& handler : step_handlers_)
+ (*handler)(delegate_, 1);
+ for (auto& handler : finish_handlers_)
+ (*handler)();
+ return true;
+ }
+ else
+ {
+ for (auto& handler : step_handlers_)
+ (*handler)(delegate_, current_time_ / duration_);
+ return false;
+ }
+ }
+
+ }
+
+ AnimationDelegatePtr AnimationBuilder::Start() const
+ {
+ return details::AnimationManager::GetInstance()->CreateAnimation(tag, duration, step_handlers_, start_handlers_, finish_handlers_, cancel_handlers_);
+ }
+}
diff --git a/src/ui/animations/animation.h b/src/ui/animations/animation.h
new file mode 100644
index 00000000..69b08b0c
--- /dev/null
+++ b/src/ui/animations/animation.h
@@ -0,0 +1,107 @@
+#pragma once
+
+#include <unordered_map>
+
+#include "base.h"
+#include "application.h"
+#include "timer.h"
+
+namespace cru::ui::animations
+{
+ using AnimationTimeUnit = FloatSecond;
+
+
+ using IAnimationDelegate = ICancelable;
+ using AnimationDelegatePtr = CancelablePtr;
+
+ using AnimationStepHandlerPtr = FunctionPtr<void(AnimationDelegatePtr, double)>;
+ using AnimationStartHandlerPtr = FunctionPtr<void(AnimationDelegatePtr)>;
+
+
+ namespace details
+ {
+ class Animation;
+ using AnimationPtr = std::unique_ptr<Animation>;
+
+ class AnimationManager : public Object
+ {
+ public:
+ static AnimationManager* GetInstance()
+ {
+ return Application::GetInstance()->GetAnimationManager();
+ }
+
+ public:
+ AnimationManager();
+ AnimationManager(const AnimationManager& other) = delete;
+ AnimationManager(AnimationManager&& other) = delete;
+ AnimationManager& operator=(const AnimationManager& other) = delete;
+ AnimationManager& operator=(AnimationManager&& other) = delete;
+ ~AnimationManager() override;
+
+ AnimationDelegatePtr CreateAnimation(
+ String tag,
+ AnimationTimeUnit duration,
+ Vector<AnimationStepHandlerPtr> step_handlers,
+ Vector<AnimationStartHandlerPtr> start_handlers,
+ Vector<ActionPtr> finish_handlers,
+ Vector<ActionPtr> cancel_handlers
+ );
+ void RemoveAnimation(const String& tag);
+
+ private:
+ void SetTimer();
+ void KillTimer();
+
+ private:
+ std::unordered_map<String, AnimationPtr> animations_;
+ std::shared_ptr<ICancelable> timer_;
+ ActionPtr timer_action_;
+ };
+ }
+
+ class AnimationBuilder : public Object
+ {
+ public:
+ AnimationBuilder(String tag, const AnimationTimeUnit duration)
+ : tag(std::move(tag)), duration(duration)
+ {
+
+ }
+
+ String tag;
+ AnimationTimeUnit duration;
+
+ AnimationBuilder& AddStepHandler(AnimationStepHandlerPtr handler)
+ {
+ step_handlers_.push_back(std::move(handler));
+ return *this;
+ }
+
+ AnimationBuilder& AddStartHandler(AnimationStartHandlerPtr handler)
+ {
+ start_handlers_.push_back(std::move(handler));
+ return *this;
+ }
+
+ AnimationBuilder& AddFinishHandler(ActionPtr handler)
+ {
+ finish_handlers_.push_back(std::move(handler));
+ return *this;
+ }
+
+ AnimationBuilder& AddCancelHandler(ActionPtr handler)
+ {
+ cancel_handlers_.push_back(std::move(handler));
+ return *this;
+ }
+
+ AnimationDelegatePtr Start() const;
+
+ private:
+ Vector<AnimationStepHandlerPtr> step_handlers_;
+ Vector<AnimationStartHandlerPtr> start_handlers_;
+ Vector<ActionPtr> finish_handlers_;
+ Vector<ActionPtr> cancel_handlers_;
+ };
+}
diff --git a/src/ui/control.cpp b/src/ui/control.cpp
new file mode 100644
index 00000000..8aec8640
--- /dev/null
+++ b/src/ui/control.cpp
@@ -0,0 +1,712 @@
+#include "control.h"
+
+#include <string>
+#include <algorithm>
+#include <chrono>
+
+#include "window.h"
+#include "timer.h"
+#include "debug_base.h"
+
+namespace cru {
+ namespace ui {
+ using namespace events;
+
+ Control::Control(const bool container) :
+ is_container_(container)
+ {
+
+ }
+
+ Control::Control(WindowConstructorTag, Window* window) : Control(true)
+ {
+ window_ = window;
+ }
+
+ Control::~Control()
+ {
+ ForeachChild([](auto control)
+ {
+ delete control;
+ });
+ }
+
+ void Control::ForeachChild(Function<void(Control*)>&& predicate) const
+ {
+ if (is_container_)
+ for (const auto child : children_)
+ predicate(child);
+ }
+
+ void Control::ForeachChild(Function<FlowControl(Control*)>&& predicate) const
+ {
+ if (is_container_)
+ for (const auto child : children_)
+ {
+ if (predicate(child) == FlowControl::Break)
+ break;
+ }
+ }
+
+ void AddChildCheck(Control* control)
+ {
+ if (control->GetParent() != nullptr)
+ throw std::invalid_argument("The control already has a parent.");
+
+ if (dynamic_cast<Window*>(control))
+ throw std::invalid_argument("Can't add a window as child.");
+ }
+
+ void Control::AddChild(Control* control)
+ {
+ ThrowIfNotContainer();
+ AddChildCheck(control);
+
+ this->children_.push_back(control);
+
+ control->parent_ = this;
+
+ this->OnAddChild(control);
+ }
+
+ void Control::AddChild(Control* control, int position)
+ {
+ ThrowIfNotContainer();
+ AddChildCheck(control);
+
+ if (position < 0 || static_cast<decltype(this->children_.size())>(position) > this->children_.size())
+ throw std::invalid_argument("The position is out of range.");
+
+ this->children_.insert(this->children_.cbegin() + position, control);
+
+ control->parent_ = this;
+
+ this->OnAddChild(this);
+ }
+
+ void Control::RemoveChild(Control* child)
+ {
+ ThrowIfNotContainer();
+ const auto i = std::find(this->children_.cbegin(), this->children_.cend(), child);
+ if (i == this->children_.cend())
+ throw std::invalid_argument("The argument child is not a child of this control.");
+
+ this->children_.erase(i);
+
+ child->parent_ = nullptr;
+
+ this->OnRemoveChild(this);
+ }
+
+ void Control::RemoveChild(const int position)
+ {
+ ThrowIfNotContainer();
+ if (position < 0 || static_cast<decltype(this->children_.size())>(position) >= this->children_.size())
+ throw std::invalid_argument("The position is out of range.");
+
+ const auto p = children_.cbegin() + position;
+ const auto child = *p;
+ children_.erase(p);
+
+ child->parent_ = nullptr;
+
+ this->OnRemoveChild(child);
+ }
+
+ Control* Control::GetAncestor()
+ {
+ // if attached to window, the window is the ancestor.
+ if (window_)
+ return window_;
+
+ // otherwise find the ancestor
+ auto ancestor = this;
+ while (const auto parent = ancestor->GetParent())
+ ancestor = parent;
+ return ancestor;
+ }
+
+ void TraverseDescendantsInternal(Control* control, Function<void(Control*)>& predicate)
+ {
+ predicate(control);
+ control->ForeachChild([&predicate](Control* c) {
+ TraverseDescendantsInternal(c, predicate);
+ });
+ }
+
+ void Control::TraverseDescendants(Function<void(Control*)>&& predicate)
+ {
+ if (is_container_)
+ TraverseDescendantsInternal(this, predicate);
+ else
+ predicate(this);
+ }
+
+ Point Control::GetPositionRelative()
+ {
+ return position_;
+ }
+
+ void Control::SetPositionRelative(const Point & position)
+ {
+ if (position != position_)
+ {
+ if (old_position_ == position) // if cache has been refreshed and no pending notify
+ old_position_ = position_;
+ position_ = position;
+ LayoutManager::GetInstance()->InvalidateControlPositionCache(this);
+ if (auto window = GetWindow())
+ {
+ window->Repaint();
+ }
+ }
+ }
+
+ Size Control::GetSize()
+ {
+ return size_;
+ }
+
+ void Control::SetSize(const Size & size)
+ {
+ const auto old_size = size_;
+ size_ = size;
+ SizeChangedEventArgs args(this, this, old_size, size);
+ RaiseSizeChangedEvent(args);
+ if (auto window = GetWindow())
+ window->Repaint();
+ }
+
+ Point Control::GetPositionAbsolute() const
+ {
+ return position_cache_.lefttop_position_absolute;
+ }
+
+ Point Control::LocalToAbsolute(const Point& point) const
+ {
+ return Point(point.x + position_cache_.lefttop_position_absolute.x,
+ point.y + position_cache_.lefttop_position_absolute.y);
+ }
+
+ Point Control::AbsoluteToLocal(const Point & point) const
+ {
+ return Point(point.x - position_cache_.lefttop_position_absolute.x,
+ point.y - position_cache_.lefttop_position_absolute.y);
+ }
+
+ bool Control::IsPointInside(const Point & point)
+ {
+ const auto size = GetSize();
+ return point.x >= 0.0f && point.x < size.width && point.y >= 0.0f && point.y < size.height;
+ }
+
+ void Control::Draw(ID2D1DeviceContext* device_context)
+ {
+ D2D1::Matrix3x2F old_transform;
+ device_context->GetTransform(&old_transform);
+
+ const auto position = GetPositionRelative();
+ device_context->SetTransform(old_transform * D2D1::Matrix3x2F::Translation(position.x, position.y));
+
+ OnDraw(device_context);
+ DrawEventArgs args(this, this, device_context);
+ draw_event.Raise(args);
+
+ for (auto child : GetChildren())
+ child->Draw(device_context);
+
+ device_context->SetTransform(old_transform);
+ }
+
+ void Control::Repaint()
+ {
+ if (window_ != nullptr)
+ window_->Repaint();
+ }
+
+ bool Control::RequestFocus()
+ {
+ auto window = GetWindow();
+ if (window == nullptr)
+ return false;
+
+ return window->RequestFocusFor(this);
+ }
+
+ bool Control::HasFocus()
+ {
+ auto window = GetWindow();
+ if (window == nullptr)
+ return false;
+
+ return window->GetFocusControl() == this;
+ }
+
+ void Control::Relayout()
+ {
+ OnMeasure(GetSize());
+ OnLayout(Rect(GetPositionRelative(), GetSize()));
+ }
+
+ void Control::Measure(const Size& available_size)
+ {
+ SetDesiredSize(OnMeasure(available_size));
+ }
+
+ void Control::Layout(const Rect& rect)
+ {
+ SetPositionRelative(rect.GetLeftTop());
+ SetSize(rect.GetSize());
+ OnLayout(rect);
+ }
+
+ Size Control::GetDesiredSize() const
+ {
+ return desired_size_;
+ }
+
+ void Control::SetDesiredSize(const Size& desired_size)
+ {
+ desired_size_ = desired_size;
+ }
+
+ void Control::OnAddChild(Control* child)
+ {
+ if (auto window = GetWindow())
+ {
+ child->TraverseDescendants([window](Control* control) {
+ control->OnAttachToWindow(window);
+ });
+ window->RefreshControlList();
+ }
+ Relayout();
+ }
+
+ void Control::OnRemoveChild(Control* child)
+ {
+ if (auto window = GetWindow())
+ {
+ child->TraverseDescendants([window](Control* control) {
+ control->OnDetachToWindow(window);
+ });
+ window->RefreshControlList();
+ }
+ Relayout();
+ }
+
+ void Control::OnAttachToWindow(Window* window)
+ {
+ window_ = window;
+ }
+
+ void Control::OnDetachToWindow(Window * window)
+ {
+ window_ = nullptr;
+ }
+
+ void Control::OnDraw(ID2D1DeviceContext * device_context)
+ {
+#ifdef CRU_DEBUG_DRAW_CONTROL_BORDER
+ if (GetWindow()->GetDebugDrawControlBorder())
+ {
+ auto brush = Application::GetInstance()->GetDebugBorderBrush();
+ const auto size = GetSize();
+ device_context->DrawRectangle(D2D1::RectF(0, 0, size.width, size.height), brush.Get());
+ }
+#endif
+ }
+
+ void Control::OnPositionChanged(PositionChangedEventArgs & args)
+ {
+
+ }
+
+ void Control::OnSizeChanged(SizeChangedEventArgs & args)
+ {
+ }
+
+ void Control::OnPositionChangedCore(PositionChangedEventArgs & args)
+ {
+
+ }
+
+ void Control::OnSizeChangedCore(SizeChangedEventArgs & args)
+ {
+
+ }
+
+ void Control::RaisePositionChangedEvent(PositionChangedEventArgs& args)
+ {
+ OnPositionChangedCore(args);
+ OnPositionChanged(args);
+ position_changed_event.Raise(args);
+ }
+
+ void Control::RaiseSizeChangedEvent(SizeChangedEventArgs& args)
+ {
+ OnSizeChangedCore(args);
+ OnSizeChanged(args);
+ size_changed_event.Raise(args);
+ }
+
+ void Control::OnMouseEnter(MouseEventArgs & args)
+ {
+ }
+
+ void Control::OnMouseLeave(MouseEventArgs & args)
+ {
+ }
+
+ void Control::OnMouseMove(MouseEventArgs & args)
+ {
+ }
+
+ void Control::OnMouseDown(MouseButtonEventArgs & args)
+ {
+ }
+
+ void Control::OnMouseUp(MouseButtonEventArgs & args)
+ {
+ }
+
+ void Control::OnMouseClick(MouseButtonEventArgs& args)
+ {
+
+ }
+
+ void Control::OnMouseEnterCore(MouseEventArgs & args)
+ {
+ is_mouse_inside_ = true;
+ }
+
+ void Control::OnMouseLeaveCore(MouseEventArgs & args)
+ {
+ is_mouse_inside_ = false;
+ for (auto& is_mouse_click_valid : is_mouse_click_valid_map_)
+ {
+ if (is_mouse_click_valid.second)
+ {
+ is_mouse_click_valid.second = false;
+ OnMouseClickEnd(is_mouse_click_valid.first);
+ }
+ }
+ }
+
+ void Control::OnMouseMoveCore(MouseEventArgs & args)
+ {
+
+ }
+
+ void Control::OnMouseDownCore(MouseButtonEventArgs & args)
+ {
+ if (is_focus_on_pressed_ && args.GetSender() == args.GetOriginalSender())
+ RequestFocus();
+ is_mouse_click_valid_map_[args.GetMouseButton()] = true;
+ OnMouseClickBegin(args.GetMouseButton());
+ }
+
+ void Control::OnMouseUpCore(MouseButtonEventArgs & args)
+ {
+ if (is_mouse_click_valid_map_[args.GetMouseButton()])
+ {
+ is_mouse_click_valid_map_[args.GetMouseButton()] = false;
+ RaiseMouseClickEvent(args);
+ OnMouseClickEnd(args.GetMouseButton());
+ }
+ }
+
+ void Control::OnMouseClickCore(MouseButtonEventArgs& args)
+ {
+
+ }
+
+ void Control::RaiseMouseEnterEvent(MouseEventArgs& args)
+ {
+ OnMouseEnterCore(args);
+ OnMouseEnter(args);
+ mouse_enter_event.Raise(args);
+ }
+
+ void Control::RaiseMouseLeaveEvent(MouseEventArgs& args)
+ {
+ OnMouseLeaveCore(args);
+ OnMouseLeave(args);
+ mouse_leave_event.Raise(args);
+ }
+
+ void Control::RaiseMouseMoveEvent(MouseEventArgs& args)
+ {
+ OnMouseMoveCore(args);
+ OnMouseMove(args);
+ mouse_move_event.Raise(args);
+ }
+
+ void Control::RaiseMouseDownEvent(MouseButtonEventArgs& args)
+ {
+ OnMouseDownCore(args);
+ OnMouseDown(args);
+ mouse_down_event.Raise(args);
+ }
+
+ void Control::RaiseMouseUpEvent(MouseButtonEventArgs& args)
+ {
+ OnMouseUpCore(args);
+ OnMouseUp(args);
+ mouse_up_event.Raise(args);
+ }
+
+ void Control::RaiseMouseClickEvent(MouseButtonEventArgs& args)
+ {
+ OnMouseClickCore(args);
+ OnMouseClick(args);
+ mouse_click_event.Raise(args);
+ }
+
+ void Control::OnMouseClickBegin(MouseButton button)
+ {
+
+ }
+
+ void Control::OnMouseClickEnd(MouseButton button)
+ {
+
+ }
+
+ void Control::OnKeyDown(KeyEventArgs& args)
+ {
+ }
+
+ void Control::OnKeyUp(KeyEventArgs& args)
+ {
+ }
+
+ void Control::OnChar(CharEventArgs& args)
+ {
+ }
+
+ void Control::OnKeyDownCore(KeyEventArgs& args)
+ {
+ }
+
+ void Control::OnKeyUpCore(KeyEventArgs& args)
+ {
+ }
+
+ void Control::OnCharCore(CharEventArgs& args)
+ {
+ }
+
+ void Control::RaiseKeyDownEvent(KeyEventArgs& args)
+ {
+ OnKeyDownCore(args);
+ OnKeyDown(args);
+ key_down_event.Raise(args);
+ }
+
+ void Control::RaiseKeyUpEvent(KeyEventArgs& args)
+ {
+ OnKeyUpCore(args);
+ OnKeyUp(args);
+ key_up_event.Raise(args);
+ }
+
+ void Control::RaiseCharEvent(CharEventArgs& args)
+ {
+ OnCharCore(args);
+ OnChar(args);
+ char_event.Raise(args);
+ }
+
+ void Control::OnGetFocus(FocusChangeEventArgs& args)
+ {
+
+ }
+
+ void Control::OnLoseFocus(FocusChangeEventArgs& args)
+ {
+
+ }
+
+ void Control::OnGetFocusCore(FocusChangeEventArgs& args)
+ {
+
+ }
+
+ void Control::OnLoseFocusCore(FocusChangeEventArgs& args)
+ {
+
+ }
+
+ void Control::RaiseGetFocusEvent(FocusChangeEventArgs& args)
+ {
+ OnGetFocusCore(args);
+ OnGetFocus(args);
+ get_focus_event.Raise(args);
+ }
+
+ void Control::RaiseLoseFocusEvent(FocusChangeEventArgs& args)
+ {
+ OnLoseFocusCore(args);
+ OnLoseFocus(args);
+ lose_focus_event.Raise(args);
+ }
+
+ Size Control::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();
+ }
+ };
+
+ const Size 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 max_child_size = Size::Zero();
+ ForeachChild([&](Control* control)
+ {
+ control->Measure(size_for_children);
+ const auto&& size = control->GetDesiredSize();
+ if (max_child_size.width < size.width)
+ max_child_size.width = size.width;
+ if (max_child_size.height < size.height)
+ max_child_size.height = size.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, size_for_children.width, max_child_size.width),
+ calculate_final_length(layout_params->height, size_for_children.height, max_child_size.height)
+ );
+ }
+
+ void Control::OnLayout(const Rect& rect)
+ {
+ ForeachChild([rect](Control* control)
+ {
+ const auto layout_params = control->GetLayoutParams();
+ const auto size = control->GetDesiredSize();
+
+ auto&& calculate_anchor = [](const Alignment 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();
+ }
+ };
+
+ control->Layout(Rect(Point(
+ calculate_anchor(layout_params->width.alignment, rect.width, size.width),
+ calculate_anchor(layout_params->height.alignment, rect.height, size.height)
+ ), size));
+ });
+ }
+
+ void Control::CheckAndNotifyPositionChanged()
+ {
+ if (this->old_position_ != this->position_)
+ {
+ PositionChangedEventArgs args(this, this, this->old_position_, this->position_);
+ this->RaisePositionChangedEvent(args);
+ this->old_position_ = this->position_;
+ }
+ }
+
+ std::list<Control*> GetAncestorList(Control* control)
+ {
+ std::list<Control*> l;
+ while (control != nullptr)
+ {
+ l.push_front(control);
+ control = control->GetParent();
+ }
+ return l;
+ }
+
+ Control* FindLowestCommonAncestor(Control * left, Control * right)
+ {
+ if (left == nullptr || right == nullptr)
+ return nullptr;
+
+ auto&& left_list = GetAncestorList(left);
+ auto&& right_list = GetAncestorList(right);
+
+ // the root is different
+ if (left_list.front() != right_list.front())
+ return nullptr;
+
+ // find the last same control or the last control (one is ancestor of the other)
+ auto left_i = left_list.cbegin();
+ auto right_i = right_list.cbegin();
+ while (true)
+ {
+ if (left_i == left_list.cend())
+ return *(--left_i);
+ if (right_i == right_list.cend())
+ return *(--right_i);
+ if (*left_i != *right_i)
+ return *(--left_i);
+ ++left_i;
+ ++right_i;
+ }
+ }
+
+ Control * IsAncestorOrDescendant(Control * left, Control * right)
+ {
+ //Search up along the trunk from "left". Return if find "right".
+ auto control = left;
+ while (control != nullptr)
+ {
+ if (control == right)
+ return control;
+ control = control->GetParent();
+ }
+ //Search up along the trunk from "right". Return if find "left".
+ control = right;
+ while (control != nullptr)
+ {
+ if (control == left)
+ return control;
+ control = control->GetParent();
+ }
+ return nullptr;
+ }
+ }
+}
diff --git a/src/ui/control.h b/src/ui/control.h
new file mode 100644
index 00000000..d6cbae40
--- /dev/null
+++ b/src/ui/control.h
@@ -0,0 +1,380 @@
+#pragma once
+
+#include "system_headers.h"
+#include <unordered_map>
+#include <any>
+#include <typeinfo>
+#include <utility>
+#include <fmt/format.h>
+
+#include "base.h"
+#include "ui_base.h"
+#include "layout_base.h"
+#include "events/ui_event.h"
+
+namespace cru
+{
+ namespace ui
+ {
+ class Control;
+ class Window;
+
+
+ //the position cache
+ struct ControlPositionCache
+ {
+ //The lefttop relative to the ancestor.
+ Point lefttop_position_absolute;
+ };
+
+
+ class Control : public Object
+ {
+ friend class Window;
+ friend class LayoutManager;
+
+ protected:
+ struct WindowConstructorTag {}; //Used for constructor for class Window.
+
+ explicit Control(bool container = false);
+
+ // Used only for creating Window. It will set window_ as window.
+ Control(WindowConstructorTag, Window* window);
+
+ public:
+ Control(const Control& other) = delete;
+ Control(Control&& other) = delete;
+ Control& operator=(const Control& other) = delete;
+ Control& operator=(Control&& other) = delete;
+ ~Control() override;
+
+ public:
+
+ //*************** region: tree ***************
+
+ bool IsContainer() const
+ {
+ return is_container_;
+ }
+
+ //Get parent of control, return nullptr if it has no parent.
+ Control* GetParent() const
+ {
+ return parent_;
+ }
+
+ //Traverse the children
+ void ForeachChild(Function<void(Control*)>&& predicate) const;
+ void ForeachChild(Function<FlowControl(Control*)>&& predicate) const;
+
+ //Return a vector of all children. This function will create a
+ //temporary copy of vector of children. If you just want to
+ //traverse all children, just call ForeachChild.
+ Vector<Control*> GetChildren() const
+ {
+ return children_;
+ }
+
+ //Add a child at tail.
+ void AddChild(Control* control);
+
+ //Add a child before the position.
+ void AddChild(Control* control, int position);
+
+ //Remove a child.
+ void RemoveChild(Control* child);
+
+ //Remove a child at specified position.
+ void RemoveChild(int position);
+
+ //Get the ancestor of the control.
+ Control* GetAncestor();
+
+ //Get the window if attached, otherwise, return nullptr.
+ Window* GetWindow() const
+ {
+ return window_;
+ }
+
+ //Traverse the tree rooted the control including itself.
+ void TraverseDescendants(Function<void(Control*)>&& predicate);
+
+ //*************** region: position and size ***************
+ // Position and size part must be isolated from layout part.
+ // All the operations in this part must be done independently.
+ // And layout part must use api of this part.
+
+ //Get the lefttop relative to its parent.
+ virtual Point GetPositionRelative();
+
+ //Set the lefttop relative to its parent.
+ virtual void SetPositionRelative(const Point& position);
+
+ //Get the actual size.
+ virtual Size GetSize();
+
+ //Set the actual size directly without re-layout.
+ virtual void SetSize(const Size& size);
+
+ //Get lefttop relative to ancestor. This is only valid when
+ //attached to window. Notice that the value is cached.
+ //You can invalidate and recalculate it by calling "InvalidatePositionCache".
+ Point GetPositionAbsolute() const;
+
+ //Local point to absolute point.
+ Point LocalToAbsolute(const Point& point) const;
+
+ //Absolute point to local point.
+ Point AbsoluteToLocal(const Point& point) const;
+
+ virtual bool IsPointInside(const Point& point);
+
+
+ //*************** region: graphic ***************
+
+ //Draw this control and its child controls.
+ void Draw(ID2D1DeviceContext* device_context);
+
+ virtual void Repaint();
+
+ //*************** region: focus ***************
+
+ bool RequestFocus();
+
+ bool HasFocus();
+
+ bool IsFocusOnPressed() const
+ {
+ return is_focus_on_pressed_;
+ }
+
+ void SetFocusOnPressed(const bool value)
+ {
+ is_focus_on_pressed_ = value;
+ }
+
+ //*************** region: layout ***************
+
+ void Relayout();
+
+ void Measure(const Size& available_size);
+
+ void Layout(const Rect& rect);
+
+ Size GetDesiredSize() const;
+
+ void SetDesiredSize(const Size& desired_size);
+
+ BasicLayoutParams* GetLayoutParams()
+ {
+ return &layout_params_;
+ }
+
+ //*************** region: additional properties ***************
+ template <typename T>
+ std::optional<T> GetAdditionalProperty(const String& key)
+ {
+ try
+ {
+ const auto find_result = additional_properties_.find(key);
+ if (find_result != additional_properties_.cend())
+ return std::any_cast<T>(find_result->second);
+ else
+ return std::nullopt;
+ }
+ catch (const std::bad_any_cast&)
+ {
+ throw std::runtime_error(fmt::format("Key \"{}\" is not of the type {}.", ToUtf8String(key), typeid(T).name()));
+ }
+ }
+
+ template <typename T>
+ void SetAdditionalProperty(const String& key, const T& value)
+ {
+ additional_properties_[key] = std::make_any<T>(value);
+ }
+
+ template <typename T>
+ void SetAdditionalProperty(const String& key, T&& value)
+ {
+ additional_properties_[key] = std::make_any<T>(std::move(value));
+ }
+
+ //*************** region: events ***************
+ //Raised when mouse enter the control.
+ events::MouseEvent mouse_enter_event;
+ //Raised when mouse is leave the control.
+ events::MouseEvent mouse_leave_event;
+ //Raised when mouse is move in the control.
+ events::MouseEvent mouse_move_event;
+ //Raised when a mouse button is pressed in the control.
+ events::MouseButtonEvent mouse_down_event;
+ //Raised when a mouse button is released in the control.
+ events::MouseButtonEvent mouse_up_event;
+ //Raised when a mouse button is pressed in the control and released in the control with mouse not leaving it between two operations.
+ events::MouseButtonEvent mouse_click_event;
+
+ events::KeyEvent key_down_event;
+ events::KeyEvent key_up_event;
+ events::CharEvent char_event;
+
+ events::FocusChangeEvent get_focus_event;
+ events::FocusChangeEvent lose_focus_event;
+
+ events::DrawEvent draw_event;
+
+ events::PositionChangedEvent position_changed_event;
+ events::SizeChangedEvent size_changed_event;
+
+ protected:
+ //Invoked when a child is added. Overrides should invoke base.
+ virtual void OnAddChild(Control* child);
+ //Invoked when a child is removed. Overrides should invoke base.
+ virtual void OnRemoveChild(Control* child);
+
+ //Invoked when the control is attached to a window. Overrides should invoke base.
+ virtual void OnAttachToWindow(Window* window);
+ //Invoked when the control is detached to a window. Overrides should invoke base.
+ virtual void OnDetachToWindow(Window* window);
+
+ virtual void OnDraw(ID2D1DeviceContext* device_context);
+
+
+ // For a event, the window event system will first dispatch event to core functions.
+ // Therefore for particular controls, you should do essential actions in core functions,
+ // and override version should invoke base version. The base core function
+ // in "Control" class will call corresponding non-core function and call "Raise" on
+ // event objects. So user custom actions should be done by overriding non-core function
+ // and calling the base version is optional.
+
+ //*************** region: position and size event ***************
+ virtual void OnPositionChanged(events::PositionChangedEventArgs& args);
+ virtual void OnSizeChanged(events::SizeChangedEventArgs& args);
+
+ virtual void OnPositionChangedCore(events::PositionChangedEventArgs& args);
+ virtual void OnSizeChangedCore(events::SizeChangedEventArgs& args);
+
+ void RaisePositionChangedEvent(events::PositionChangedEventArgs& args);
+ void RaiseSizeChangedEvent(events::SizeChangedEventArgs& args);
+
+ //*************** region: mouse event ***************
+ virtual void OnMouseEnter(events::MouseEventArgs& args);
+ virtual void OnMouseLeave(events::MouseEventArgs& args);
+ virtual void OnMouseMove(events::MouseEventArgs& args);
+ virtual void OnMouseDown(events::MouseButtonEventArgs& args);
+ virtual void OnMouseUp(events::MouseButtonEventArgs& args);
+ virtual void OnMouseClick(events::MouseButtonEventArgs& args);
+
+ virtual void OnMouseEnterCore(events::MouseEventArgs& args);
+ virtual void OnMouseLeaveCore(events::MouseEventArgs& args);
+ virtual void OnMouseMoveCore(events::MouseEventArgs& args);
+ virtual void OnMouseDownCore(events::MouseButtonEventArgs& args);
+ virtual void OnMouseUpCore(events::MouseButtonEventArgs& args);
+ virtual void OnMouseClickCore(events::MouseButtonEventArgs& args);
+
+ void RaiseMouseEnterEvent(events::MouseEventArgs& args);
+ void RaiseMouseLeaveEvent(events::MouseEventArgs& args);
+ void RaiseMouseMoveEvent(events::MouseEventArgs& args);
+ void RaiseMouseDownEvent(events::MouseButtonEventArgs& args);
+ void RaiseMouseUpEvent(events::MouseButtonEventArgs& args);
+ void RaiseMouseClickEvent(events::MouseButtonEventArgs& args);
+
+ virtual void OnMouseClickBegin(MouseButton button);
+ virtual void OnMouseClickEnd(MouseButton button);
+
+ //*************** region: keyboard event ***************
+ virtual void OnKeyDown(events::KeyEventArgs& args);
+ virtual void OnKeyUp(events::KeyEventArgs& args);
+ virtual void OnChar(events::CharEventArgs& args);
+
+ virtual void OnKeyDownCore(events::KeyEventArgs& args);
+ virtual void OnKeyUpCore(events::KeyEventArgs& args);
+ virtual void OnCharCore(events::CharEventArgs& args);
+
+ void RaiseKeyDownEvent(events::KeyEventArgs& args);
+ void RaiseKeyUpEvent(events::KeyEventArgs& args);
+ void RaiseCharEvent(events::CharEventArgs& args);
+
+ //*************** region: focus event ***************
+ virtual void OnGetFocus(events::FocusChangeEventArgs& args);
+ virtual void OnLoseFocus(events::FocusChangeEventArgs& args);
+
+ virtual void OnGetFocusCore(events::FocusChangeEventArgs& args);
+ virtual void OnLoseFocusCore(events::FocusChangeEventArgs& args);
+
+ void RaiseGetFocusEvent(events::FocusChangeEventArgs& args);
+ void RaiseLoseFocusEvent(events::FocusChangeEventArgs& args);
+
+ //*************** region: layout ***************
+ virtual Size OnMeasure(const Size& available_size);
+ virtual void OnLayout(const Rect& rect);
+
+ private:
+ // Only for layout manager to use.
+ // Check if the old position is updated to current position.
+ // If not, then a notify of position change and update will
+ // be done.
+ void CheckAndNotifyPositionChanged();
+
+ void ThrowIfNotContainer() const
+ {
+ if (!is_container_)
+ throw std::runtime_error("You can't perform such operation on a non-container control.");
+ }
+
+ private:
+ bool is_container_;
+
+ protected:
+ Window * window_ = nullptr; // protected for Window class to write it as itself in constructor.
+
+ private:
+ Control * parent_ = nullptr;
+ Vector<Control*> children_{};
+
+ // When position is changed and notification hasn't been
+ // sent, it will be the old position. When position is changed
+ // more than once, it will be the oldest position since last
+ // notification. If notification has been sent, it will be updated
+ // to position_.
+ Point old_position_ = Point::Zero();
+ Point position_ = Point::Zero();
+ Size size_ = Size::Zero();
+
+ ControlPositionCache position_cache_{};
+
+ bool is_mouse_inside_ = false;
+
+ std::unordered_map<MouseButton, bool> is_mouse_click_valid_map_
+ {
+ { MouseButton::Left, true },
+ { MouseButton::Middle, true },
+ { MouseButton::Right, true }
+ }; // used for clicking determination
+
+ BasicLayoutParams layout_params_{};
+ Size desired_size_ = Size::Zero();
+
+ std::unordered_map<String, std::any> additional_properties_{};
+
+ bool is_focus_on_pressed_ = true;
+ };
+
+ // Find the lowest common ancestor.
+ // Return nullptr if "left" and "right" are not in the same tree.
+ Control* FindLowestCommonAncestor(Control* left, Control* right);
+
+ // Return the ancestor if one control is the ancestor of the other one, otherwise nullptr.
+ Control* IsAncestorOrDescendant(Control* left, Control* right);
+
+ template <typename TControl, typename... Args>
+ TControl* CreateWithLayout(const LayoutSideParams& width, const LayoutSideParams& height, Args&&... args)
+ {
+ static_assert(std::is_base_of_v<Control, TControl>, "TControl is not a control class.");
+ TControl* control = TControl::Create(std::forward<Args>(args)...);
+ control->GetLayoutParams()->width = width;
+ control->GetLayoutParams()->height = height;
+ return control;
+ }
+ }
+}
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_;
+ };
+}
diff --git a/src/ui/events/ui_event.cpp b/src/ui/events/ui_event.cpp
new file mode 100644
index 00000000..59623bab
--- /dev/null
+++ b/src/ui/events/ui_event.cpp
@@ -0,0 +1,19 @@
+#include "ui_event.h"
+
+#include "ui/control.h"
+
+namespace cru
+{
+ namespace ui
+ {
+ namespace events
+ {
+ Point MouseEventArgs::GetPoint(Control* control) const
+ {
+ if (point_.has_value())
+ return control->AbsoluteToLocal(point_.value());
+ return Point();
+ }
+ }
+ }
+}
diff --git a/src/ui/events/ui_event.h b/src/ui/events/ui_event.h
new file mode 100644
index 00000000..b042b706
--- /dev/null
+++ b/src/ui/events/ui_event.h
@@ -0,0 +1,317 @@
+#pragma once
+
+#include "system_headers.h"
+#include <optional>
+
+#include "base.h"
+#include "cru_event.h"
+#include "ui/ui_base.h"
+
+namespace cru
+{
+ namespace ui
+ {
+ class Control;
+
+ namespace events
+ {
+ class UiEventArgs : public BasicEventArgs
+ {
+ public:
+ UiEventArgs(Object* sender, Object* original_sender)
+ : BasicEventArgs(sender), original_sender_(original_sender)
+ {
+
+ }
+
+ UiEventArgs(const UiEventArgs& other) = default;
+ UiEventArgs(UiEventArgs&& other) = default;
+ UiEventArgs& operator=(const UiEventArgs& other) = default;
+ UiEventArgs& operator=(UiEventArgs&& other) = default;
+ ~UiEventArgs() override = default;
+
+ Object* GetOriginalSender() const
+ {
+ return original_sender_;
+ }
+
+ private:
+ Object* original_sender_;
+ };
+
+
+ class MouseEventArgs : public UiEventArgs
+ {
+ public:
+ MouseEventArgs(Object* sender, Object* original_sender, const std::optional<Point>& point = std::nullopt)
+ : UiEventArgs(sender, original_sender), point_(point)
+ {
+
+ }
+ MouseEventArgs(const MouseEventArgs& other) = default;
+ MouseEventArgs(MouseEventArgs&& other) = default;
+ MouseEventArgs& operator=(const MouseEventArgs& other) = default;
+ MouseEventArgs& operator=(MouseEventArgs&& other) = default;
+ ~MouseEventArgs() override = default;
+
+ Point GetPoint(Control* control) const;
+
+ private:
+ std::optional<Point> point_;
+ };
+
+
+ class MouseButtonEventArgs : public MouseEventArgs
+ {
+ public:
+ MouseButtonEventArgs(Object* sender, Object* original_sender, const Point& point, const MouseButton button)
+ : MouseEventArgs(sender, original_sender, point), button_(button)
+ {
+
+ }
+ MouseButtonEventArgs(const MouseButtonEventArgs& other) = default;
+ MouseButtonEventArgs(MouseButtonEventArgs&& other) = default;
+ MouseButtonEventArgs& operator=(const MouseButtonEventArgs& other) = default;
+ MouseButtonEventArgs& operator=(MouseButtonEventArgs&& other) = default;
+ ~MouseButtonEventArgs() override = default;
+
+ MouseButton GetMouseButton() const
+ {
+ return button_;
+ }
+
+ private:
+ MouseButton button_;
+ };
+
+
+ class DrawEventArgs : public UiEventArgs
+ {
+ public:
+ DrawEventArgs(Object* sender, Object* original_sender, ID2D1DeviceContext* device_context)
+ : UiEventArgs(sender, original_sender), device_context_(device_context)
+ {
+
+ }
+ DrawEventArgs(const DrawEventArgs& other) = default;
+ DrawEventArgs(DrawEventArgs&& other) = default;
+ DrawEventArgs& operator=(const DrawEventArgs& other) = default;
+ DrawEventArgs& operator=(DrawEventArgs&& other) = default;
+ ~DrawEventArgs() = default;
+
+ ID2D1DeviceContext* GetDeviceContext() const
+ {
+ return device_context_;
+ }
+
+ private:
+ ID2D1DeviceContext * device_context_;
+ };
+
+
+ class PositionChangedEventArgs : public UiEventArgs
+ {
+ public:
+ PositionChangedEventArgs(Object* sender, Object* original_sender, const Point& old_position, const Point& new_position)
+ : UiEventArgs(sender, original_sender), old_position_(old_position), new_position_(new_position)
+ {
+
+ }
+ PositionChangedEventArgs(const PositionChangedEventArgs& other) = default;
+ PositionChangedEventArgs(PositionChangedEventArgs&& other) = default;
+ PositionChangedEventArgs& operator=(const PositionChangedEventArgs& other) = default;
+ PositionChangedEventArgs& operator=(PositionChangedEventArgs&& other) = default;
+ ~PositionChangedEventArgs() override = default;
+
+ Point GetOldPosition() const
+ {
+ return old_position_;
+ }
+
+ Point GetNewPosition() const
+ {
+ return new_position_;
+ }
+
+ private:
+ Point old_position_;
+ Point new_position_;
+ };
+
+
+ class SizeChangedEventArgs : public UiEventArgs
+ {
+ public:
+ SizeChangedEventArgs(Object* sender, Object* original_sender, const Size& old_size, const Size& new_size)
+ : UiEventArgs(sender, original_sender), old_size_(old_size), new_size_(new_size)
+ {
+
+ }
+ SizeChangedEventArgs(const SizeChangedEventArgs& other) = default;
+ SizeChangedEventArgs(SizeChangedEventArgs&& other) = default;
+ SizeChangedEventArgs& operator=(const SizeChangedEventArgs& other) = default;
+ SizeChangedEventArgs& operator=(SizeChangedEventArgs&& other) = default;
+ ~SizeChangedEventArgs() override = default;
+
+ Size GetOldSize() const
+ {
+ return old_size_;
+ }
+
+ Size GetNewSize() const
+ {
+ return new_size_;
+ }
+
+ private:
+ Size old_size_;
+ Size new_size_;
+ };
+
+ class FocusChangeEventArgs : public UiEventArgs
+ {
+ public:
+ FocusChangeEventArgs(Object* sender, Object* original_sender, const bool is_window = false)
+ : UiEventArgs(sender, original_sender), is_window_(is_window)
+ {
+
+ }
+ FocusChangeEventArgs(const FocusChangeEventArgs& other) = default;
+ FocusChangeEventArgs(FocusChangeEventArgs&& other) = default;
+ FocusChangeEventArgs& operator=(const FocusChangeEventArgs& other) = default;
+ FocusChangeEventArgs& operator=(FocusChangeEventArgs&& other) = default;
+ ~FocusChangeEventArgs() override = default;
+
+ // Return whether the focus change is caused by the window-wide focus change.
+ bool IsWindow() const
+ {
+ return is_window_;
+ }
+
+ private:
+ bool is_window_;
+ };
+
+ class ToggleEventArgs : public UiEventArgs
+ {
+ public:
+ ToggleEventArgs(Object* sender, Object* original_sender, bool new_state)
+ : UiEventArgs(sender, original_sender), new_state_(new_state)
+ {
+
+ }
+ ToggleEventArgs(const ToggleEventArgs& other) = default;
+ ToggleEventArgs(ToggleEventArgs&& other) = default;
+ ToggleEventArgs& operator=(const ToggleEventArgs& other) = default;
+ ToggleEventArgs& operator=(ToggleEventArgs&& other) = default;
+ ~ToggleEventArgs() override = default;
+
+ bool GetNewState() const
+ {
+ return new_state_;
+ }
+
+ private:
+ bool new_state_;
+ };
+
+ struct WindowNativeMessage
+ {
+ HWND hwnd;
+ int msg;
+ WPARAM w_param;
+ LPARAM l_param;
+ };
+
+ class WindowNativeMessageEventArgs : public UiEventArgs
+ {
+ public:
+ WindowNativeMessageEventArgs(Object* sender, Object* original_sender, const WindowNativeMessage& message)
+ : UiEventArgs(sender, original_sender), message_(message), result_(std::nullopt)
+ {
+
+ }
+ WindowNativeMessageEventArgs(const WindowNativeMessageEventArgs& other) = default;
+ WindowNativeMessageEventArgs(WindowNativeMessageEventArgs&& other) = default;
+ WindowNativeMessageEventArgs& operator=(const WindowNativeMessageEventArgs& other) = default;
+ WindowNativeMessageEventArgs& operator=(WindowNativeMessageEventArgs&& other) = default;
+ ~WindowNativeMessageEventArgs() override = default;
+
+ WindowNativeMessage GetWindowMessage() const
+ {
+ return message_;
+ }
+
+ std::optional<LRESULT> GetResult() const
+ {
+ return result_;
+ }
+
+ void SetResult(const std::optional<LRESULT> result)
+ {
+ result_ = result;
+ }
+
+ private:
+ WindowNativeMessage message_;
+ std::optional<LRESULT> result_;
+ };
+
+ class KeyEventArgs : public UiEventArgs
+ {
+ public:
+ KeyEventArgs(Object* sender, Object* original_sender, int virtual_code)
+ : UiEventArgs(sender, original_sender), virtual_code_(virtual_code)
+ {
+ }
+ KeyEventArgs(const KeyEventArgs& other) = default;
+ KeyEventArgs(KeyEventArgs&& other) = default;
+ KeyEventArgs& operator=(const KeyEventArgs& other) = default;
+ KeyEventArgs& operator=(KeyEventArgs&& other) = default;
+ ~KeyEventArgs() override = default;
+
+ int GetVirtualCode() const
+ {
+ return virtual_code_;
+ }
+
+ private:
+ int virtual_code_;
+ };
+
+ class CharEventArgs : public UiEventArgs
+ {
+ public:
+ CharEventArgs(Object* sender, Object* original_sender, wchar_t c)
+ : UiEventArgs(sender, original_sender), c_(c)
+ {
+ }
+ CharEventArgs(const CharEventArgs& other) = default;
+ CharEventArgs(CharEventArgs&& other) = default;
+ CharEventArgs& operator=(const CharEventArgs& other) = default;
+ CharEventArgs& operator=(CharEventArgs&& other) = default;
+ ~CharEventArgs() override = default;
+
+ wchar_t GetChar() const
+ {
+ return c_;
+ }
+
+ private:
+ wchar_t c_;
+ };
+
+ using UiEvent = Event<UiEventArgs>;
+ using MouseEvent = Event<MouseEventArgs>;
+ using MouseButtonEvent = Event<MouseButtonEventArgs>;
+ using DrawEvent = Event<DrawEventArgs>;
+ using PositionChangedEvent = Event<PositionChangedEventArgs>;
+ using SizeChangedEvent = Event<SizeChangedEventArgs>;
+ using FocusChangeEvent = Event<FocusChangeEventArgs>;
+ using ToggleEvent = Event<ToggleEventArgs>;
+ using WindowNativeMessageEvent = Event<WindowNativeMessageEventArgs>;
+ using KeyEvent = Event<KeyEventArgs>;
+ using CharEvent = Event<CharEventArgs>;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/ui/layout_base.cpp b/src/ui/layout_base.cpp
new file mode 100644
index 00000000..a26379a0
--- /dev/null
+++ b/src/ui/layout_base.cpp
@@ -0,0 +1,79 @@
+#include "layout_base.h"
+
+#include "application.h"
+#include "control.h"
+
+namespace cru::ui
+{
+ LayoutManager* LayoutManager::GetInstance()
+ {
+ static LayoutManager layout_manager;
+ return &layout_manager;
+ }
+
+ void LayoutManager::InvalidateControlPositionCache(Control * control)
+ {
+ if (cache_invalid_controls_.count(control) == 1)
+ return;
+
+ // find descendant then erase it; find ancestor then just return.
+ auto i = cache_invalid_controls_.cbegin();
+ while (i != cache_invalid_controls_.cend())
+ {
+ auto current_i = i++;
+ const auto result = IsAncestorOrDescendant(*current_i, control);
+ if (result == control)
+ cache_invalid_controls_.erase(current_i);
+ else if (result != nullptr)
+ return; // find a ancestor of "control", just return
+ }
+
+ cache_invalid_controls_.insert(control);
+
+ if (cache_invalid_controls_.size() == 1) // when insert just now and not repeat to "InvokeLater".
+ {
+ InvokeLater([this] {
+
+ RefreshInvalidControlPositionCache(); // first refresh position cache.
+ for (const auto i : cache_invalid_controls_) // traverse all descendants of position-invalid controls and notify position change event
+ i->TraverseDescendants([](Control* control)
+ {
+ control->CheckAndNotifyPositionChanged();
+ });
+ cache_invalid_controls_.clear(); // after update and notify, clear the set.
+
+ });
+ }
+ }
+
+ void LayoutManager::RefreshInvalidControlPositionCache()
+ {
+ for (const auto i : cache_invalid_controls_)
+ RefreshControlPositionCache(i);
+ }
+
+ void LayoutManager::RefreshControlPositionCache(Control * control)
+ {
+ auto point = Point::Zero();
+ auto parent = control;
+ while ((parent = parent->GetParent())) {
+ const auto p = parent->GetPositionRelative();
+ point.x += p.x;
+ point.y += p.y;
+ }
+ RefreshControlPositionCacheInternal(control, point);
+ }
+
+ void LayoutManager::RefreshControlPositionCacheInternal(Control * control, const Point & parent_lefttop_absolute)
+ {
+ const auto position = control->GetPositionRelative();
+ Point lefttop(
+ parent_lefttop_absolute.x + position.x,
+ parent_lefttop_absolute.y + position.y
+ );
+ control->position_cache_.lefttop_position_absolute = lefttop;
+ control->ForeachChild([lefttop](Control* c) {
+ RefreshControlPositionCacheInternal(c, lefttop);
+ });
+ }
+}
diff --git a/src/ui/layout_base.h b/src/ui/layout_base.h
new file mode 100644
index 00000000..163b99b2
--- /dev/null
+++ b/src/ui/layout_base.h
@@ -0,0 +1,136 @@
+#pragma once
+
+#include "system_headers.h"
+#include <unordered_set>
+
+#include "base.h"
+#include "ui_base.h"
+
+namespace cru
+{
+ namespace ui
+ {
+ class Control;
+
+ enum class Alignment
+ {
+ Center,
+ Start,
+ End
+ };
+
+ enum class MeasureMode
+ {
+ Exactly,
+ Content,
+ Stretch
+ };
+
+ struct LayoutSideParams final
+ {
+ constexpr static LayoutSideParams Exactly(const float length, const Alignment alignment = Alignment::Center)
+ {
+ return LayoutSideParams(MeasureMode::Exactly, length, alignment);
+ }
+
+ constexpr static LayoutSideParams Content(const Alignment alignment = Alignment::Center)
+ {
+ return LayoutSideParams(MeasureMode::Content, 0, alignment);
+ }
+
+ constexpr static LayoutSideParams Stretch(const Alignment alignment = Alignment::Center)
+ {
+ return LayoutSideParams(MeasureMode::Stretch, 0, alignment);
+ }
+
+ constexpr LayoutSideParams() = default;
+
+ constexpr explicit LayoutSideParams(const MeasureMode mode, const float length, const Alignment alignment)
+ : length(length), mode(mode), alignment(alignment)
+ {
+
+ }
+
+ constexpr bool Validate() const
+ {
+ if (mode == MeasureMode::Exactly && length < 0.0)
+ {
+#ifdef CRU_DEBUG
+ ::OutputDebugStringW(L"LayoutSideParams validation error: mode is Exactly but length is less than 0.\n");
+#endif
+ return false;
+ }
+ return true;
+ }
+
+ float length = 0.0;
+ MeasureMode mode = MeasureMode::Content;
+ Alignment alignment = Alignment::Center;
+ };
+
+ struct BasicLayoutParams final
+ {
+ BasicLayoutParams() = default;
+ BasicLayoutParams(const BasicLayoutParams&) = default;
+ BasicLayoutParams(BasicLayoutParams&&) = default;
+ BasicLayoutParams& operator = (const BasicLayoutParams&) = default;
+ BasicLayoutParams& operator = (BasicLayoutParams&&) = default;
+ ~BasicLayoutParams() = default;
+
+ bool Validate() const
+ {
+ if (!width.Validate())
+ {
+#ifdef CRU_DEBUG
+ ::OutputDebugStringW(L"Width(LayoutSideParams) is not valid.");
+#endif
+ return false;
+ }
+ if (!height.Validate())
+ {
+#ifdef CRU_DEBUG
+ ::OutputDebugStringW(L"Height(LayoutSideParams) is not valid.");
+#endif
+ return false;
+ }
+ return true;
+ }
+
+ LayoutSideParams width;
+ LayoutSideParams height;
+ };
+
+
+ class LayoutManager : public Object
+ {
+ public:
+ static LayoutManager* GetInstance();
+
+ public:
+ LayoutManager() = default;
+ LayoutManager(const LayoutManager& other) = delete;
+ LayoutManager(LayoutManager&& other) = delete;
+ LayoutManager& operator=(const LayoutManager& other) = delete;
+ LayoutManager& operator=(LayoutManager&& other) = delete;
+ ~LayoutManager() override = default;
+
+ //Mark position cache of the control and its descendants invalid,
+ //(which is saved as an auto-managed list internal)
+ //and send a message to refresh them.
+ void InvalidateControlPositionCache(Control* control);
+
+ //Refresh position cache of the control and its descendants whose cache
+ //has been marked as invalid.
+ void RefreshInvalidControlPositionCache();
+
+ //Refresh position cache of the control and its descendants immediately.
+ static void RefreshControlPositionCache(Control* control);
+
+ private:
+ static void RefreshControlPositionCacheInternal(Control* control, const Point& parent_lefttop_absolute);
+
+ private:
+ std::unordered_set<Control*> cache_invalid_controls_;
+ };
+ }
+}
diff --git a/src/ui/ui_base.cpp b/src/ui/ui_base.cpp
new file mode 100644
index 00000000..550432e4
--- /dev/null
+++ b/src/ui/ui_base.cpp
@@ -0,0 +1,6 @@
+#include "ui_base.h"
+
+namespace cru {
+ namespace ui {
+ }
+}
diff --git a/src/ui/ui_base.h b/src/ui/ui_base.h
new file mode 100644
index 00000000..43f3c498
--- /dev/null
+++ b/src/ui/ui_base.h
@@ -0,0 +1,154 @@
+#pragma once
+
+
+namespace cru
+{
+ namespace ui
+ {
+ struct Point
+ {
+ constexpr static Point Zero()
+ {
+ return Point(0, 0);
+ }
+
+ constexpr Point() = default;
+ constexpr Point(const float x, const float y) : x(x), y(y) { }
+
+ float x = 0;
+ float y = 0;
+ };
+
+ constexpr bool operator==(const Point& left, const Point& right)
+ {
+ return left.x == right.x && left.y == right.y;
+ }
+
+ constexpr bool operator!=(const Point& left, const Point& right)
+ {
+ return !(left == right);
+ }
+
+ struct Size
+ {
+ constexpr static Size Zero()
+ {
+ return Size(0, 0);
+ }
+
+ constexpr Size() = default;
+ constexpr Size(const float width, const float height) : width(width), height(height) { }
+
+ float width = 0;
+ float height = 0;
+ };
+
+ constexpr Size operator + (const Size& left, const Size& right)
+ {
+ return Size(left.width + right.width, left.height + right.height);
+ }
+
+ constexpr Size operator - (const Size& left, const Size& right)
+ {
+ return Size(left.width - right.width, left.height - right.height);
+ }
+
+ struct Rect
+ {
+ constexpr Rect() = default;
+ constexpr Rect(const float left, const float top, const float width, const float height)
+ : left(left), top(top), width(width), height(height) { }
+ constexpr Rect(const Point& lefttop, const Size& size)
+ : left(lefttop.x), top(lefttop.y), width(size.width), height(size.height) { }
+
+ constexpr static Rect FromVertices(const float left, const float top, const float right, const float bottom)
+ {
+ return Rect(left, top, right - left, bottom - top);
+ }
+
+ constexpr float GetRight() const
+ {
+ return left + width;
+ }
+
+ constexpr float GetBottom() const
+ {
+ return top + height;
+ }
+
+ constexpr Point GetLeftTop() const
+ {
+ return Point(left, top);
+ }
+
+ constexpr Point GetRightBottom() const
+ {
+ return Point(left + width, top + height);
+ }
+
+ constexpr Size GetSize() const
+ {
+ return Size(width, height);
+ }
+
+ constexpr bool IsPointInside(const Point& point) const
+ {
+ return
+ point.x >= left &&
+ point.x < GetRight() &&
+ point.y >= top &&
+ point.y < GetBottom();
+ }
+
+ float left = 0.0f;
+ float top = 0.0f;
+ float width = 0.0f;
+ float height = 0.0f;
+ };
+
+ struct Thickness
+ {
+ constexpr static Thickness Zero()
+ {
+ return Thickness(0);
+ }
+
+ constexpr Thickness() : Thickness(0) { }
+
+ constexpr explicit Thickness(const float width)
+ : left(width), top(width), right(width), bottom(width) { }
+
+ constexpr explicit Thickness(const float horizontal, const float vertical)
+ : left(horizontal), top(vertical), right(horizontal), bottom(vertical) { }
+
+ constexpr Thickness(const float left, const float top, const float right, const float bottom)
+ : left(left), top(top), right(right), bottom(bottom) { }
+
+
+ float left;
+ float top;
+ float right;
+ float bottom;
+ };
+
+ enum class MouseButton
+ {
+ Left,
+ Right,
+ Middle
+ };
+
+ struct TextRange
+ {
+ constexpr TextRange() = default;
+ constexpr TextRange(const int position, const int count)
+ : position(position), count(count)
+ {
+
+ }
+
+ unsigned position = 0;
+ unsigned count = 0;
+ };
+ }
+}
diff --git a/src/ui/window.cpp b/src/ui/window.cpp
new file mode 100644
index 00000000..34a54512
--- /dev/null
+++ b/src/ui/window.cpp
@@ -0,0 +1,606 @@
+#include "window.h"
+
+#include <fmt/format.h>
+
+#include "application.h"
+#include "graph/graph.h"
+#include "exception.h"
+
+namespace cru
+{
+ namespace ui
+ {
+ WindowClass::WindowClass(const String& name, WNDPROC window_proc, HINSTANCE h_instance)
+ : name_(name)
+ {
+ WNDCLASSEX window_class;
+ window_class.cbSize = sizeof(WNDCLASSEX);
+
+ window_class.style = CS_HREDRAW | CS_VREDRAW;
+ window_class.lpfnWndProc = window_proc;
+ window_class.cbClsExtra = 0;
+ window_class.cbWndExtra = 0;
+ window_class.hInstance = h_instance;
+ window_class.hIcon = LoadIcon(NULL, IDI_APPLICATION);
+ window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
+ window_class.hbrBackground = GetSysColorBrush(COLOR_BTNFACE);
+ window_class.lpszMenuName = NULL;
+ window_class.lpszClassName = name.c_str();
+ window_class.hIconSm = NULL;
+
+ atom_ = RegisterClassEx(&window_class);
+ if (atom_ == 0)
+ throw std::runtime_error("Failed to create window class.");
+ }
+
+ LRESULT __stdcall GeneralWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
+ auto window = Application::GetInstance()->GetWindowManager()->FromHandle(hWnd);
+
+ LRESULT result;
+ if (window != nullptr && window->HandleWindowMessage(hWnd, Msg, wParam, lParam, result))
+ return result;
+
+ return DefWindowProc(hWnd, Msg, wParam, lParam);
+ }
+
+ WindowManager::WindowManager() {
+ general_window_class_ = std::make_unique<WindowClass>(
+ L"CruUIWindowClass",
+ GeneralWndProc,
+ Application::GetInstance()->GetInstanceHandle()
+ );
+ }
+
+ void WindowManager::RegisterWindow(HWND hwnd, Window * window) {
+ const auto find_result = window_map_.find(hwnd);
+ if (find_result != window_map_.end())
+ throw std::runtime_error("The hwnd is already in the map.");
+
+ window_map_.emplace(hwnd, window);
+ }
+
+ void WindowManager::UnregisterWindow(HWND hwnd) {
+ const auto find_result = window_map_.find(hwnd);
+ if (find_result == window_map_.end())
+ throw std::runtime_error("The hwnd is not in the map.");
+ window_map_.erase(find_result);
+
+ if (window_map_.empty())
+ Application::GetInstance()->Quit(0);
+ }
+
+ Window* WindowManager::FromHandle(HWND hwnd) {
+ const auto find_result = window_map_.find(hwnd);
+ if (find_result == window_map_.end())
+ return nullptr;
+ else
+ return find_result->second;
+ }
+
+ Vector<Window*> WindowManager::GetAllWindows() const
+ {
+ Vector<Window*> windows;
+ for (auto [key, value] : window_map_)
+ windows.push_back(value);
+ return windows;
+ }
+
+ inline Point PiToDip(const POINT& pi_point)
+ {
+ return Point(
+ graph::PixelToDipX(pi_point.x),
+ graph::PixelToDipY(pi_point.y)
+ );
+ }
+
+ Window::Window() : Control(WindowConstructorTag{}, this), control_list_({ this }) {
+ const auto app = Application::GetInstance();
+ hwnd_ = CreateWindowEx(0,
+ app->GetWindowManager()->GetGeneralWindowClass()->GetName(),
+ L"", WS_OVERLAPPEDWINDOW,
+ CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+ nullptr, nullptr, app->GetInstanceHandle(), nullptr
+ );
+
+ if (hwnd_ == nullptr)
+ throw std::runtime_error("Failed to create window.");
+
+ app->GetWindowManager()->RegisterWindow(hwnd_, this);
+
+ render_target_ = app->GetGraphManager()->CreateWindowRenderTarget(hwnd_);
+ }
+
+ Window::~Window() {
+ Close();
+ TraverseDescendants([this](Control* control) {
+ control->OnDetachToWindow(this);
+ });
+ }
+
+ void Window::Close() {
+ if (IsWindowValid())
+ DestroyWindow(hwnd_);
+ }
+
+ void Window::Repaint() {
+ if (IsWindowValid()) {
+ InvalidateRect(hwnd_, nullptr, false);
+ }
+ }
+
+ void Window::Show() {
+ if (IsWindowValid()) {
+ ShowWindow(hwnd_, SW_SHOWNORMAL);
+ }
+ }
+
+ void Window::Hide() {
+ if (IsWindowValid()) {
+ ShowWindow(hwnd_, SW_HIDE);
+ }
+ }
+
+ Size Window::GetClientSize() {
+ if (!IsWindowValid())
+ return Size();
+
+ const auto pixel_rect = GetClientRectPixel();
+ return Size(
+ graph::PixelToDipX(pixel_rect.right),
+ graph::PixelToDipY(pixel_rect.bottom)
+ );
+ }
+
+ void Window::SetClientSize(const Size & size) {
+ if (IsWindowValid()) {
+ const auto window_style = static_cast<DWORD>(GetWindowLongPtr(hwnd_, GWL_STYLE));
+ const auto window_ex_style = static_cast<DWORD>(GetWindowLongPtr(hwnd_, GWL_EXSTYLE));
+
+ RECT rect;
+ rect.left = 0;
+ rect.top = 0;
+ rect.right = graph::DipToPixelX(size.width);
+ rect.bottom = graph::DipToPixelY(size.height);
+ AdjustWindowRectEx(&rect, window_style, FALSE, window_ex_style);
+
+ SetWindowPos(
+ hwnd_, nullptr, 0, 0,
+ rect.right - rect.left,
+ rect.bottom - rect.top,
+ SWP_NOZORDER | SWP_NOMOVE
+ );
+ }
+ }
+
+ Rect Window::GetWindowRect() {
+ if (!IsWindowValid())
+ return Rect();
+
+ RECT rect;
+ ::GetWindowRect(hwnd_, &rect);
+
+ return Rect::FromVertices(
+ graph::PixelToDipX(rect.left),
+ graph::PixelToDipY(rect.top),
+ graph::PixelToDipX(rect.right),
+ graph::PixelToDipY(rect.bottom)
+ );
+ }
+
+ void Window::SetWindowRect(const Rect & rect) {
+ if (IsWindowValid()) {
+ SetWindowPos(
+ hwnd_, nullptr,
+ graph::DipToPixelX(rect.left),
+ graph::DipToPixelY(rect.top),
+ graph::DipToPixelX(rect.GetRight()),
+ graph::DipToPixelY(rect.GetBottom()),
+ SWP_NOZORDER
+ );
+ }
+ }
+
+ bool Window::HandleWindowMessage(HWND hwnd, int msg, WPARAM w_param, LPARAM l_param, LRESULT & result) {
+
+ if (!native_message_event.IsNoHandler())
+ {
+ events::WindowNativeMessageEventArgs args(this, this, {hwnd, msg, w_param, l_param});
+ native_message_event.Raise(args);
+ if (args.GetResult().has_value())
+ {
+ result = args.GetResult().value();
+ return true;
+ }
+ }
+
+ switch (msg) {
+ case WM_PAINT:
+ OnPaintInternal();
+ result = 0;
+ return true;
+ case WM_ERASEBKGND:
+ result = 1;
+ return true;
+ case WM_SETFOCUS:
+ OnSetFocusInternal();
+ result = 0;
+ return true;
+ case WM_KILLFOCUS:
+ OnKillFocusInternal();
+ result = 0;
+ return true;
+ case WM_MOUSEMOVE:
+ {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseMoveInternal(point);
+ result = 0;
+ return true;
+ }
+ case WM_LBUTTONDOWN:
+ {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseDownInternal(MouseButton::Left, point);
+ result = 0;
+ return true;
+ }
+ case WM_LBUTTONUP:
+ {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseUpInternal(MouseButton::Left, point);
+ result = 0;
+ return true;
+ }
+ case WM_RBUTTONDOWN:
+ {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseDownInternal(MouseButton::Right, point);
+ result = 0;
+ return true;
+ }
+ case WM_RBUTTONUP:
+ {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseUpInternal(MouseButton::Right, point);
+ result = 0;
+ return true;
+ }
+ case WM_MBUTTONDOWN:
+ {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseDownInternal(MouseButton::Middle, point);
+ result = 0;
+ return true;
+ }
+ case WM_MBUTTONUP:
+ {
+ POINT point;
+ point.x = GET_X_LPARAM(l_param);
+ point.y = GET_Y_LPARAM(l_param);
+ OnMouseUpInternal(MouseButton::Middle, point);
+ result = 0;
+ return true;
+ }
+ case WM_KEYDOWN:
+ OnKeyDownInternal(static_cast<int>(w_param));
+ result = 0;
+ return true;
+ case WM_KEYUP:
+ OnKeyUpInternal(static_cast<int>(w_param));
+ result = 0;
+ return true;
+ case WM_CHAR:
+ OnCharInternal(static_cast<wchar_t>(w_param));
+ result = 0;
+ return true;
+ case WM_SIZE:
+ OnResizeInternal(LOWORD(l_param), HIWORD(l_param));
+ result = 0;
+ return true;
+ case WM_ACTIVATE:
+ if (w_param == WA_ACTIVE || w_param == WA_CLICKACTIVE)
+ OnActivatedInternal();
+ else if (w_param == WA_INACTIVE)
+ OnDeactivatedInternal();
+ result = 0;
+ return true;
+ case WM_DESTROY:
+ OnDestroyInternal();
+ result = 0;
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ Point Window::GetMousePosition()
+ {
+ POINT point;
+ ::GetCursorPos(&point);
+ ::ScreenToClient(hwnd_, &point);
+ return PiToDip(point);
+ }
+
+ Point Window::GetPositionRelative()
+ {
+ return Point();
+ }
+
+ void Window::SetPositionRelative(const Point & position)
+ {
+
+ }
+
+ Size Window::GetSize()
+ {
+ return GetClientSize();
+ }
+
+ void Window::SetSize(const Size & size)
+ {
+
+ }
+
+ void Window::RefreshControlList() {
+ control_list_.clear();
+ TraverseDescendants([this](Control* control) {
+ this->control_list_.push_back(control);
+ });
+ }
+
+ Control * Window::HitTest(const Point & point)
+ {
+ for (auto i = control_list_.crbegin(); i != control_list_.crend(); ++i) {
+ auto control = *i;
+ if (control->IsPointInside(control->AbsoluteToLocal(point))) {
+ return control;
+ }
+ }
+ return nullptr;
+ }
+
+ bool Window::RequestFocusFor(Control * control)
+ {
+ if (control == nullptr)
+ throw std::invalid_argument("The control to request focus can't be null. You can set it as the window.");
+
+ if (!IsWindowValid())
+ return false;
+
+ if (!window_focus_)
+ {
+ focus_control_ = control;
+ ::SetFocus(hwnd_);
+ return true; // event dispatch will be done in window message handling function "OnSetFocusInternal".
+ }
+
+ if (focus_control_ == control)
+ return true;
+
+ DispatchEvent(focus_control_, &Control::RaiseLoseFocusEvent, nullptr, false);
+
+ focus_control_ = control;
+
+ DispatchEvent(control, &Control::RaiseGetFocusEvent, nullptr, false);
+
+ return true;
+ }
+
+ Control* Window::GetFocusControl()
+ {
+ return focus_control_;
+ }
+
+ Control* Window::CaptureMouseFor(Control* control)
+ {
+ if (control != nullptr)
+ {
+ ::SetCapture(hwnd_);
+ std::swap(mouse_capture_control_, control);
+ DispatchMouseHoverControlChangeEvent(control ? control : mouse_hover_control_, mouse_capture_control_, GetMousePosition());
+ return control;
+ }
+ else
+ {
+ return ReleaseCurrentMouseCapture();
+ }
+ }
+
+ Control* Window::ReleaseCurrentMouseCapture()
+ {
+ if (mouse_capture_control_)
+ {
+ const auto previous = mouse_capture_control_;
+ mouse_capture_control_ = nullptr;
+ ::ReleaseCapture();
+ DispatchMouseHoverControlChangeEvent(previous, mouse_hover_control_, GetMousePosition());
+ return previous;
+ }
+ else
+ {
+ return nullptr;
+ }
+ }
+
+#ifdef CRU_DEBUG_DRAW_CONTROL_BORDER
+ void Window::SetDebugDrawControlBorder(const bool value)
+ {
+ if (debug_draw_control_border_ != value)
+ {
+ debug_draw_control_border_ = value;
+ Repaint();
+ }
+ }
+#endif
+
+ RECT Window::GetClientRectPixel() {
+ RECT rect{ };
+ GetClientRect(hwnd_, &rect);
+ return rect;
+ }
+
+ bool Window::IsMessageInQueue(UINT message)
+ {
+ MSG msg;
+ return ::PeekMessageW(&msg, hwnd_, message, message, PM_NOREMOVE) != 0;
+ }
+
+ void Window::OnDestroyInternal() {
+ Application::GetInstance()->GetWindowManager()->UnregisterWindow(hwnd_);
+ hwnd_ = nullptr;
+ }
+
+ void Window::OnPaintInternal() {
+ render_target_->SetAsTarget();
+
+ auto device_context = render_target_->GetD2DDeviceContext();
+
+ device_context->BeginDraw();
+
+ //Clear the background.
+ device_context->Clear(D2D1::ColorF(D2D1::ColorF::White));
+
+ Draw(device_context.Get());
+
+ ThrowIfFailed(
+ device_context->EndDraw(), "Failed to draw window."
+ );
+
+ render_target_->Present();
+
+ ValidateRect(hwnd_, nullptr);
+ }
+
+ void Window::OnResizeInternal(int new_width, int new_height) {
+ render_target_->ResizeBuffer(new_width, new_height);
+ Relayout();
+ }
+
+ void Window::OnSetFocusInternal()
+ {
+ window_focus_ = true;
+ DispatchEvent(focus_control_, &Control::RaiseGetFocusEvent, nullptr, true);
+ }
+
+ void Window::OnKillFocusInternal()
+ {
+ window_focus_ = false;
+ DispatchEvent(focus_control_, &Control::RaiseLoseFocusEvent, nullptr, true);
+ }
+
+ void Window::OnMouseMoveInternal(const POINT point)
+ {
+ const auto dip_point = PiToDip(point);
+
+ //when mouse was previous outside the window
+ if (mouse_hover_control_ == nullptr) {
+ //invoke TrackMouseEvent to have WM_MOUSELEAVE sent.
+ TRACKMOUSEEVENT tme;
+ tme.cbSize = sizeof tme;
+ tme.dwFlags = TME_LEAVE;
+ tme.hwndTrack = hwnd_;
+
+ TrackMouseEvent(&tme);
+ }
+
+ //Find the first control that hit test succeed.
+ const auto new_control_mouse_hover = HitTest(dip_point);
+ const auto old_control_mouse_hover = mouse_hover_control_;
+ mouse_hover_control_ = new_control_mouse_hover;
+
+ if (mouse_capture_control_) // if mouse is captured
+ {
+ DispatchEvent(mouse_capture_control_, &Control::RaiseMouseMoveEvent, nullptr, dip_point);
+ }
+ else
+ {
+ DispatchMouseHoverControlChangeEvent(old_control_mouse_hover, new_control_mouse_hover, dip_point);
+ DispatchEvent(new_control_mouse_hover, &Control::RaiseMouseMoveEvent, nullptr, dip_point);
+ }
+ }
+
+ void Window::OnMouseLeaveInternal()
+ {
+ DispatchEvent(mouse_hover_control_, &Control::RaiseMouseLeaveEvent, nullptr);
+ mouse_hover_control_ = nullptr;
+ }
+
+ void Window::OnMouseDownInternal(MouseButton button, POINT point)
+ {
+ const auto dip_point = PiToDip(point);
+
+ Control* control;
+
+ if (mouse_capture_control_)
+ control = mouse_capture_control_;
+ else
+ control = HitTest(dip_point);
+
+ DispatchEvent(control, &Control::RaiseMouseDownEvent, nullptr, dip_point, button);
+ }
+
+ void Window::OnMouseUpInternal(MouseButton button, POINT point)
+ {
+ const auto dip_point = PiToDip(point);
+
+ Control* control;
+
+ if (mouse_capture_control_)
+ control = mouse_capture_control_;
+ else
+ control = HitTest(dip_point);
+
+ DispatchEvent(control, &Control::RaiseMouseUpEvent, nullptr, dip_point, button);
+ }
+
+ void Window::OnKeyDownInternal(int virtual_code)
+ {
+ DispatchEvent(focus_control_, &Control::RaiseKeyDownEvent, nullptr, virtual_code);
+ }
+
+ void Window::OnKeyUpInternal(int virtual_code)
+ {
+ DispatchEvent(focus_control_, &Control::RaiseKeyUpEvent, nullptr, virtual_code);
+ }
+
+ void Window::OnCharInternal(wchar_t c)
+ {
+ DispatchEvent(focus_control_, &Control::RaiseCharEvent, nullptr, c);
+ }
+
+ void Window::OnActivatedInternal()
+ {
+ events::UiEventArgs args(this, this);
+ activated_event.Raise(args);
+ }
+
+ void Window::OnDeactivatedInternal()
+ {
+ events::UiEventArgs args(this, this);
+ deactivated_event.Raise(args);
+ }
+
+ void Window::DispatchMouseHoverControlChangeEvent(Control* old_control, Control* new_control, const Point& point)
+ {
+ if (new_control != old_control) //if the mouse-hover-on control changed
+ {
+ const auto lowest_common_ancestor = FindLowestCommonAncestor(old_control, new_control);
+ if (old_control != nullptr) // if last mouse-hover-on control exists
+ DispatchEvent(old_control, &Control::RaiseMouseLeaveEvent, lowest_common_ancestor); // dispatch mouse leave event.
+ if (new_control != nullptr)
+ DispatchEvent(new_control, &Control::RaiseMouseEnterEvent, lowest_common_ancestor, point); // dispatch mouse enter event.
+ }
+ }
+ }
+}
diff --git a/src/ui/window.h b/src/ui/window.h
new file mode 100644
index 00000000..40d81a06
--- /dev/null
+++ b/src/ui/window.h
@@ -0,0 +1,276 @@
+#pragma once
+
+#include "system_headers.h"
+#include <map>
+#include <list>
+#include <memory>
+
+#include "control.h"
+#include "events/ui_event.h"
+
+namespace cru {
+ namespace graph {
+ class WindowRenderTarget;
+ }
+
+ namespace ui {
+ class WindowClass : public Object
+ {
+ public:
+ WindowClass(const String& name, WNDPROC window_proc, HINSTANCE h_instance);
+ WindowClass(const WindowClass& other) = delete;
+ WindowClass(WindowClass&& other) = delete;
+ WindowClass& operator=(const WindowClass& other) = delete;
+ WindowClass& operator=(WindowClass&& other) = delete;
+ ~WindowClass() override = default;
+
+
+ const wchar_t* GetName() const
+ {
+ return name_.c_str();
+ }
+
+ ATOM GetAtom() const
+ {
+ return atom_;
+ }
+
+ private:
+ String name_;
+ ATOM atom_;
+ };
+
+ class WindowManager : public Object
+ {
+ public:
+ WindowManager();
+ WindowManager(const WindowManager& other) = delete;
+ WindowManager(WindowManager&& other) = delete;
+ WindowManager& operator=(const WindowManager& other) = delete;
+ WindowManager& operator=(WindowManager&& other) = delete;
+ ~WindowManager() override = default;
+
+
+ //Get the general window class for creating ordinary window.
+ WindowClass* GetGeneralWindowClass() const
+ {
+ return general_window_class_.get();
+ }
+
+ //Register a window newly created.
+ //This function adds the hwnd to hwnd-window map.
+ //It should be called immediately after a window was created.
+ void RegisterWindow(HWND hwnd, Window* window);
+
+ //Unregister a window that is going to be destroyed.
+ //This function removes the hwnd from the hwnd-window map.
+ //It should be called immediately before a window is going to be destroyed,
+ void UnregisterWindow(HWND hwnd);
+
+ //Return a pointer to the Window object related to the HWND or nullptr if the hwnd is not in the map.
+ Window* FromHandle(HWND hwnd);
+
+ Vector<Window*> GetAllWindows() const;
+
+ private:
+ std::unique_ptr<WindowClass> general_window_class_;
+ std::map<HWND, Window*> window_map_;
+ };
+
+
+
+ class Window : public Control
+ {
+ friend class WindowManager;
+ public:
+ Window();
+ Window(const Window& other) = delete;
+ Window(Window&& other) = delete;
+ Window& operator=(const Window& other) = delete;
+ Window& operator=(Window&& other) = delete;
+ ~Window() override;
+
+ public:
+ //*************** region: handle ***************
+
+ //Get the handle of the window. Return null if window is invalid.
+ HWND GetWindowHandle() const
+ {
+ return hwnd_;
+ }
+
+ //Return if the window is still valid, that is, hasn't been closed or destroyed.
+ bool IsWindowValid() const
+ {
+ return hwnd_ != nullptr;
+ }
+
+
+ //*************** region: window operations ***************
+
+ //Close and destroy the window if the window is valid.
+ void Close();
+
+ //Send a repaint message to the window's message queue which may make the window repaint.
+ void Repaint() override;
+
+ //Show the window.
+ void Show();
+
+ //Hide thw window.
+ void Hide();
+
+ //Get the client size.
+ Size GetClientSize();
+
+ //Set the client size and repaint.
+ void SetClientSize(const Size& size);
+
+ //Get the rect of the window containing frame.
+ //The lefttop of the rect is relative to screen lefttop.
+ Rect GetWindowRect();
+
+ //Set the rect of the window containing frame.
+ //The lefttop of the rect is relative to screen lefttop.
+ void SetWindowRect(const Rect& rect);
+
+ //Handle the raw window message.
+ //Return true if the message is handled and get the result through "result" argument.
+ //Return false if the message is not handled.
+ bool HandleWindowMessage(HWND hwnd, int msg, WPARAM w_param, LPARAM l_param, LRESULT& result);
+
+ Point GetMousePosition();
+
+ //*************** region: position and size ***************
+
+ //Always return (0, 0) for a window.
+ Point GetPositionRelative() override final;
+
+ //This method has no effect for a window.
+ void SetPositionRelative(const Point& position) override final;
+
+ //Get the size of client area for a window.
+ Size GetSize() override final;
+
+ //This method has no effect for a window. Use SetClientSize instead.
+ void SetSize(const Size& size) override final;
+
+
+ //*************** region: features ***************
+
+ //Refresh control list.
+ //It should be invoked every time a control is added or removed from the tree.
+ void RefreshControlList();
+
+ //Get the most top control at "point".
+ Control* HitTest(const Point& point);
+
+
+ //*************** region: focus ***************
+
+ //Request focus for specified control.
+ bool RequestFocusFor(Control* control);
+
+ //Get the control that has focus.
+ Control* GetFocusControl();
+
+
+ //*************** region: mouse capture ***************
+
+ Control* CaptureMouseFor(Control* control);
+ Control* ReleaseCurrentMouseCapture();
+
+ //*************** region: debug ***************
+#ifdef CRU_DEBUG_DRAW_CONTROL_BORDER
+ bool GetDebugDrawControlBorder() const
+ {
+ return debug_draw_control_border_;
+ }
+
+ void SetDebugDrawControlBorder(bool value);
+#endif
+
+ public:
+ //*************** region: events ***************
+ events::UiEvent activated_event;
+ events::UiEvent deactivated_event;
+
+ events::WindowNativeMessageEvent native_message_event;
+
+ private:
+ //*************** region: native operations ***************
+
+ //Get the client rect in pixel.
+ RECT GetClientRectPixel();
+
+ bool IsMessageInQueue(UINT message);
+
+
+ //*************** region: native messages ***************
+
+ void OnDestroyInternal();
+ void OnPaintInternal();
+ void OnResizeInternal(int new_width, int new_height);
+
+ void OnSetFocusInternal();
+ void OnKillFocusInternal();
+
+ void OnMouseMoveInternal(POINT point);
+ void OnMouseLeaveInternal();
+ void OnMouseDownInternal(MouseButton button, POINT point);
+ void OnMouseUpInternal(MouseButton button, POINT point);
+
+ void OnKeyDownInternal(int virtual_code);
+ void OnKeyUpInternal(int virtual_code);
+ void OnCharInternal(wchar_t c);
+
+ void OnActivatedInternal();
+ void OnDeactivatedInternal();
+
+ //*************** region: event dispatcher helper ***************
+
+ template<typename EventArgs>
+ using EventMethod = void (Control::*)(EventArgs&);
+
+ // Dispatch the event.
+ //
+ // This will invoke the "event_method" of the control and its parent and parent's
+ // parent ... (until "last_receiver" if it's not nullptr) with appropriate args.
+ //
+ // Args is of type "EventArgs". The first init argument is "sender", which is
+ // automatically bound to each receiving control. The second init argument is
+ // "original_sender", which is unchanged. And "args" will be perfectly forwarded
+ // as the rest arguments.
+ template<typename EventArgs, typename... Args>
+ void DispatchEvent(Control* original_sender, EventMethod<EventArgs> event_method, Control* last_receiver, Args&&... args)
+ {
+ auto control = original_sender;
+ while (control != nullptr && control != last_receiver)
+ {
+ EventArgs event_args(control, original_sender, std::forward<Args>(args)...);
+ (control->*event_method)(event_args);
+ control = control->GetParent();
+ }
+ }
+
+ void DispatchMouseHoverControlChangeEvent(Control* old_control, Control * new_control, const Point& point);
+
+ private:
+ HWND hwnd_ = nullptr;
+ std::shared_ptr<graph::WindowRenderTarget> render_target_{};
+
+ std::list<Control*> control_list_{};
+
+ Control* mouse_hover_control_ = nullptr;
+
+ bool window_focus_ = false;
+ Control* focus_control_ = this; // "focus_control_" can't be nullptr
+
+ Control* mouse_capture_control_ = nullptr;
+
+#ifdef CRU_DEBUG_DRAW_CONTROL_BORDER
+ bool debug_draw_control_border_ = false;
+#endif
+ };
+ }
+}