aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/CMakeLists.txt70
-rw-r--r--src/ui/ContentControl.cpp33
-rw-r--r--src/ui/Control.cpp130
-rw-r--r--src/ui/Helper.cpp8
-rw-r--r--src/ui/Helper.hpp4
-rw-r--r--src/ui/LayoutControl.cpp53
-rw-r--r--src/ui/NoChildControl.cpp5
-rw-r--r--src/ui/UiHost.cpp388
-rw-r--r--src/ui/UiManager.cpp122
-rw-r--r--src/ui/Window.cpp28
-rw-r--r--src/ui/components/Component.cpp5
-rw-r--r--src/ui/components/Menu.cpp61
-rw-r--r--src/ui/controls/Button.cpp62
-rw-r--r--src/ui/controls/Container.cpp10
-rw-r--r--src/ui/controls/ContentControl.cpp31
-rw-r--r--src/ui/controls/Control.cpp166
-rw-r--r--src/ui/controls/FlexLayout.cpp12
-rw-r--r--src/ui/controls/LayoutControl.cpp18
-rw-r--r--src/ui/controls/NoChildControl.cpp3
-rw-r--r--src/ui/controls/Popup.cpp22
-rw-r--r--src/ui/controls/RootControl.cpp53
-rw-r--r--src/ui/controls/StackLayout.cpp15
-rw-r--r--src/ui/controls/TextBlock.cpp22
-rw-r--r--src/ui/controls/TextBox.cpp36
-rw-r--r--src/ui/controls/TextControlService.hpp403
-rw-r--r--src/ui/controls/TextHostControlService.cpp469
-rw-r--r--src/ui/controls/Window.cpp24
-rw-r--r--src/ui/events/UiEvent.cpp (renamed from src/ui/UiEvent.cpp)6
-rw-r--r--src/ui/helper/BorderStyle.cpp0
-rw-r--r--src/ui/helper/ClickDetector.cpp (renamed from src/ui/ClickDetector.cpp)51
-rw-r--r--src/ui/helper/ShortcutHub.cpp131
-rw-r--r--src/ui/host/LayoutPaintCycler.cpp35
-rw-r--r--src/ui/host/RoutedEventDispatch.hpp (renamed from src/ui/RoutedEventDispatch.hpp)61
-rw-r--r--src/ui/host/WindowHost.cpp440
-rw-r--r--src/ui/render/BorderRenderObject.cpp34
-rw-r--r--src/ui/render/CanvasRenderObject.cpp2
-rw-r--r--src/ui/render/FlexLayoutRenderObject.cpp23
-rw-r--r--src/ui/render/RenderObject.cpp100
-rw-r--r--src/ui/render/ScrollBar.cpp622
-rw-r--r--src/ui/render/ScrollRenderObject.cpp66
-rw-r--r--src/ui/render/TextRenderObject.cpp40
-rw-r--r--src/ui/render/WindowRenderObject.cpp40
-rw-r--r--src/ui/style/Condition.cpp84
-rw-r--r--src/ui/style/StyleRule.cpp17
-rw-r--r--src/ui/style/StyleRuleSet.cpp97
-rw-r--r--src/ui/style/Styler.cpp29
46 files changed, 2736 insertions, 1395 deletions
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
index 6c50ec57..7d2792d6 100644
--- a/src/ui/CMakeLists.txt
+++ b/src/ui/CMakeLists.txt
@@ -2,53 +2,74 @@ set(CRU_UI_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/ui)
add_library(cru_ui STATIC
Helper.hpp
- RoutedEventDispatch.hpp
+ host/RoutedEventDispatch.hpp
- ClickDetector.cpp
- ContentControl.cpp
- Control.cpp
Helper.cpp
- LayoutControl.cpp
- NoChildControl.cpp
- UiEvent.cpp
- UiHost.cpp
UiManager.cpp
- Window.cpp
+ components/Component.cpp
+ components/Menu.cpp
controls/Button.cpp
controls/Container.cpp
+ controls/ContentControl.cpp
+ controls/Control.cpp
controls/FlexLayout.cpp
+ controls/LayoutControl.cpp
+ controls/NoChildControl.cpp
+ controls/Popup.cpp
+ controls/RootControl.cpp
controls/StackLayout.cpp
controls/TextBlock.cpp
controls/TextBox.cpp
- controls/TextControlService.hpp
+ controls/TextHostControlService.cpp
+ controls/Window.cpp
+ events/UiEvent.cpp
+ helper/BorderStyle.cpp
+ helper/ClickDetector.cpp
+ helper/ShortcutHub.cpp
+ host/LayoutPaintCycler.cpp
+ host/WindowHost.cpp
render/BorderRenderObject.cpp
render/CanvasRenderObject.cpp
render/FlexLayoutRenderObject.cpp
render/LayoutHelper.cpp
render/RenderObject.cpp
+ render/ScrollBar.cpp
render/ScrollRenderObject.cpp
render/StackLayoutRenderObject.cpp
render/TextRenderObject.cpp
- render/WindowRenderObject.cpp
+ style/Condition.cpp
+ style/Styler.cpp
+ style/StyleRule.cpp
+ style/StyleRuleSet.cpp
)
target_sources(cru_ui PUBLIC
${CRU_UI_INCLUDE_DIR}/Base.hpp
- ${CRU_UI_INCLUDE_DIR}/ClickDetector.hpp
- ${CRU_UI_INCLUDE_DIR}/ContentControl.hpp
- ${CRU_UI_INCLUDE_DIR}/Control.hpp
- ${CRU_UI_INCLUDE_DIR}/LayoutControl.hpp
- ${CRU_UI_INCLUDE_DIR}/NoChildControl.hpp
- ${CRU_UI_INCLUDE_DIR}/UiEvent.hpp
- ${CRU_UI_INCLUDE_DIR}/UiHost.hpp
+ ${CRU_UI_INCLUDE_DIR}/DebugFlags.hpp
${CRU_UI_INCLUDE_DIR}/UiManager.hpp
- ${CRU_UI_INCLUDE_DIR}/Window.hpp
+ ${CRU_UI_INCLUDE_DIR}/components/Component.hpp
+ ${CRU_UI_INCLUDE_DIR}/components/Menu.hpp
${CRU_UI_INCLUDE_DIR}/controls/Base.hpp
${CRU_UI_INCLUDE_DIR}/controls/Button.hpp
${CRU_UI_INCLUDE_DIR}/controls/Container.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/ContentControl.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/Control.hpp
${CRU_UI_INCLUDE_DIR}/controls/FlexLayout.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/IBorderControl.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/IClickableControl.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/LayoutControl.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/NoChildControl.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/Popup.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/RootControl.hpp
${CRU_UI_INCLUDE_DIR}/controls/StackLayout.hpp
- ${CRU_UI_INCLUDE_DIR}/controls/TextBox.hpp
${CRU_UI_INCLUDE_DIR}/controls/TextBlock.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/TextBox.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/TextHostControlService.hpp
+ ${CRU_UI_INCLUDE_DIR}/controls/Window.hpp
+ ${CRU_UI_INCLUDE_DIR}/events/UiEvent.hpp
+ ${CRU_UI_INCLUDE_DIR}/helper/ClickDetector.hpp
+ ${CRU_UI_INCLUDE_DIR}/helper/ShortcutHub.hpp
+ ${CRU_UI_INCLUDE_DIR}/host/LayoutPaintCycler.hpp
+ ${CRU_UI_INCLUDE_DIR}/host/WindowHost.hpp
${CRU_UI_INCLUDE_DIR}/render/Base.hpp
${CRU_UI_INCLUDE_DIR}/render/BorderRenderObject.hpp
${CRU_UI_INCLUDE_DIR}/render/CanvasRenderObject.hpp
@@ -57,9 +78,14 @@ target_sources(cru_ui PUBLIC
${CRU_UI_INCLUDE_DIR}/render/LayoutRenderObject.hpp
${CRU_UI_INCLUDE_DIR}/render/MeasureRequirement.hpp
${CRU_UI_INCLUDE_DIR}/render/RenderObject.hpp
+ ${CRU_UI_INCLUDE_DIR}/render/ScrollBar.hpp
${CRU_UI_INCLUDE_DIR}/render/ScrollRenderObject.hpp
${CRU_UI_INCLUDE_DIR}/render/StackLayoutRenderObject.hpp
${CRU_UI_INCLUDE_DIR}/render/TextRenderObject.hpp
- ${CRU_UI_INCLUDE_DIR}/render/WindowRenderObject.hpp
+ ${CRU_UI_INCLUDE_DIR}/style/ApplyBorderStyleInfo.hpp
+ ${CRU_UI_INCLUDE_DIR}/style/Condition.hpp
+ ${CRU_UI_INCLUDE_DIR}/style/Styler.hpp
+ ${CRU_UI_INCLUDE_DIR}/style/StyleRule.hpp
+ ${CRU_UI_INCLUDE_DIR}/style/StyleRuleSet.hpp
)
-target_link_libraries(cru_ui PUBLIC cru_platform_native)
+target_link_libraries(cru_ui PUBLIC cru_platform_gui)
diff --git a/src/ui/ContentControl.cpp b/src/ui/ContentControl.cpp
deleted file mode 100644
index 8d1a17d2..00000000
--- a/src/ui/ContentControl.cpp
+++ /dev/null
@@ -1,33 +0,0 @@
-#include "cru/ui/ContentControl.hpp"
-
-#include "cru/ui/Window.hpp"
-
-namespace cru::ui {
-ContentControl::ContentControl()
- : child_vector_{nullptr}, child_(child_vector_[0]) {}
-
-ContentControl::~ContentControl() { delete child_; }
-
-void ContentControl::SetChild(Control* child) {
- Expects(!dynamic_cast<Window*>(child)); // Can't add a window as child.
- if (child == child_) return;
-
- const auto host = GetUiHost();
- const auto old_child = child_;
- child_ = child;
- if (old_child) {
- old_child->_SetParent(nullptr);
- old_child->_SetDescendantUiHost(nullptr);
- }
- if (child) {
- child->_SetParent(this);
- child->_SetDescendantUiHost(host);
- }
- OnChildChanged(old_child, child);
-}
-
-void ContentControl::OnChildChanged(Control* old_child, Control* new_child) {
- CRU_UNUSED(old_child)
- CRU_UNUSED(new_child)
-}
-} // namespace cru::ui
diff --git a/src/ui/Control.cpp b/src/ui/Control.cpp
deleted file mode 100644
index cd1367fe..00000000
--- a/src/ui/Control.cpp
+++ /dev/null
@@ -1,130 +0,0 @@
-#include "cru/ui/Control.hpp"
-
-#include "cru/platform/native/Cursor.hpp"
-#include "cru/platform/native/UiApplication.hpp"
-#include "cru/ui/Base.hpp"
-#include "cru/ui/UiHost.hpp"
-#include "RoutedEventDispatch.hpp"
-
-#include <memory>
-
-namespace cru::ui {
-using platform::native::ICursor;
-using platform::native::IUiApplication;
-using platform::native::SystemCursorType;
-
-Control::Control() {
- MouseEnterEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) {
- this->is_mouse_over_ = true;
- this->OnMouseHoverChange(true);
- });
-
- MouseLeaveEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) {
- this->is_mouse_over_ = false;
- this->OnMouseHoverChange(true);
- });
-}
-
-void Control::_SetParent(Control* parent) {
- const auto old_parent = GetParent();
- parent_ = parent;
- const auto new_parent = GetParent();
- if (old_parent != new_parent) OnParentChanged(old_parent, new_parent);
-}
-
-void Control::_SetDescendantUiHost(UiHost* host) {
- if (host == nullptr && ui_host_ == nullptr) return;
-
- // You can only attach or detach window.
- Expects((host != nullptr && ui_host_ == nullptr) ||
- (host == nullptr && ui_host_ != nullptr));
-
- if (host == nullptr) {
- const auto old = ui_host_;
- TraverseDescendants([old](Control* control) {
- control->ui_host_ = nullptr;
- control->OnDetachFromHost(old);
- });
- } else
- TraverseDescendants([host](Control* control) {
- control->ui_host_ = host;
- control->OnAttachToHost(host);
- });
-}
-
-void Control::TraverseDescendants(
- const std::function<void(Control*)>& predicate) {
- _TraverseDescendants(this, predicate);
-}
-
-void Control::_TraverseDescendants(
- Control* control, const std::function<void(Control*)>& predicate) {
- predicate(control);
- for (auto c : control->GetChildren()) _TraverseDescendants(c, predicate);
-}
-
-bool Control::RequestFocus() {
- auto host = GetUiHost();
- if (host == nullptr) return false;
-
- return host->RequestFocusFor(this);
-}
-
-bool Control::HasFocus() {
- auto host = GetUiHost();
- if (host == nullptr) return false;
-
- return host->GetFocusControl() == this;
-}
-
-bool Control::CaptureMouse() {
- auto host = GetUiHost();
- if (host == nullptr) return false;
-
- return host->CaptureMouseFor(this);
-}
-
-bool Control::ReleaseMouse() {
- auto host = GetUiHost();
- if (host == nullptr) return false;
-
- return host->CaptureMouseFor(nullptr);
-}
-
-bool Control::IsMouseCaptured() {
- auto host = GetUiHost();
- if (host == nullptr) return false;
-
- return host->GetMouseCaptureControl() == this;
-}
-
-std::shared_ptr<ICursor> Control::GetCursor() { return cursor_; }
-
-std::shared_ptr<ICursor> Control::GetInheritedCursor() {
- Control* control = this;
- while (control != nullptr) {
- const auto cursor = control->GetCursor();
- if (cursor != nullptr) return cursor;
- control = control->GetParent();
- }
- return IUiApplication::GetInstance()->GetCursorManager()->GetSystemCursor(
- SystemCursorType::Arrow);
-}
-
-void Control::SetCursor(std::shared_ptr<ICursor> cursor) {
- cursor_ = std::move(cursor);
- const auto host = GetUiHost();
- if (host != nullptr) {
- host->UpdateCursor();
- }
-}
-
-void Control::OnParentChanged(Control* old_parent, Control* new_parent) {
- CRU_UNUSED(old_parent)
- CRU_UNUSED(new_parent)
-}
-
-void Control::OnAttachToHost(UiHost* host) { CRU_UNUSED(host) }
-
-void Control::OnDetachFromHost(UiHost* host) { CRU_UNUSED(host) }
-} // namespace cru::ui
diff --git a/src/ui/Helper.cpp b/src/ui/Helper.cpp
index 6f67e701..88ead993 100644
--- a/src/ui/Helper.cpp
+++ b/src/ui/Helper.cpp
@@ -1,11 +1,11 @@
#include "Helper.hpp"
-#include "cru/platform/graph/Factory.hpp"
-#include "cru/platform/native/UiApplication.hpp"
+#include "cru/platform/graphics/Factory.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
namespace cru::ui {
-using cru::platform::graph::IGraphFactory;
-using cru::platform::native::IUiApplication;
+using cru::platform::graphics::IGraphFactory;
+using cru::platform::gui::IUiApplication;
IGraphFactory* GetGraphFactory() {
return IUiApplication::GetInstance()->GetGraphFactory();
diff --git a/src/ui/Helper.hpp b/src/ui/Helper.hpp
index 6923852f..327f91ff 100644
--- a/src/ui/Helper.hpp
+++ b/src/ui/Helper.hpp
@@ -12,6 +12,6 @@ struct IUiApplication;
} // namespace cru::platform
namespace cru::ui {
-cru::platform::graph::IGraphFactory* GetGraphFactory();
-cru::platform::native::IUiApplication* GetUiApplication();
+cru::platform::graphics::IGraphFactory* GetGraphFactory();
+cru::platform::gui::IUiApplication* GetUiApplication();
} // namespace cru::ui
diff --git a/src/ui/LayoutControl.cpp b/src/ui/LayoutControl.cpp
deleted file mode 100644
index 4813566b..00000000
--- a/src/ui/LayoutControl.cpp
+++ /dev/null
@@ -1,53 +0,0 @@
-#include "cru/ui/LayoutControl.hpp"
-
-#include "cru/ui/Window.hpp"
-
-namespace cru::ui {
-LayoutControl::~LayoutControl() {
- for (const auto child : children_) delete child;
-}
-
-void LayoutControl::AddChild(Control* control, const Index position) {
- Expects(control->GetParent() ==
- nullptr); // The control already has a parent.
- Expects(!dynamic_cast<Window*>(control)); // Can't add a window as child.
- Expects(position >= 0);
- Expects(position <=
- static_cast<Index>(
- this->children_.size())); // The position is out of range.
-
- children_.insert(this->children_.cbegin() + position, control);
-
- control->_SetParent(this);
- control->_SetDescendantUiHost(GetUiHost());
-
- OnAddChild(control, position);
-}
-
-void LayoutControl::RemoveChild(const Index position) {
- Expects(position >= 0);
- Expects(position <
- static_cast<Index>(
- this->children_.size())); // The position is out of range.
-
- const auto i = children_.cbegin() + position;
- const auto child = *i;
-
- children_.erase(i);
-
- child->_SetParent(nullptr);
- child->_SetDescendantUiHost(nullptr);
-
- OnRemoveChild(child, position);
-}
-
-void LayoutControl::OnAddChild(Control* child, const Index position) {
- CRU_UNUSED(child)
- CRU_UNUSED(position)
-}
-
-void LayoutControl::OnRemoveChild(Control* child, const Index position) {
- CRU_UNUSED(child)
- CRU_UNUSED(position)
-}
-} // namespace cru::ui
diff --git a/src/ui/NoChildControl.cpp b/src/ui/NoChildControl.cpp
deleted file mode 100644
index 86861049..00000000
--- a/src/ui/NoChildControl.cpp
+++ /dev/null
@@ -1,5 +0,0 @@
-#include "cru/ui/NoChildControl.hpp"
-
-namespace cru::ui {
-const std::vector<Control*> NoChildControl::empty_control_vector{};
-}
diff --git a/src/ui/UiHost.cpp b/src/ui/UiHost.cpp
deleted file mode 100644
index d8dcb6da..00000000
--- a/src/ui/UiHost.cpp
+++ /dev/null
@@ -1,388 +0,0 @@
-#include "cru/ui/UiHost.hpp"
-
-#include "RoutedEventDispatch.hpp"
-#include "cru/common/Logger.hpp"
-#include "cru/platform/graph/Painter.hpp"
-#include "cru/platform/native/InputMethod.hpp"
-#include "cru/platform/native/UiApplication.hpp"
-#include "cru/platform/native/Window.hpp"
-#include "cru/ui/Window.hpp"
-#include "cru/ui/render/WindowRenderObject.hpp"
-
-namespace cru::ui {
-using platform::native::INativeWindow;
-using platform::native::IUiApplication;
-
-namespace event_names {
-#ifdef CRU_DEBUG
-// clang-format off
-#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = CRU_MAKE_UNICODE_LITERAL(name);
-// clang-format on
-#else
-#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = u"";
-#endif
-
-CRU_DEFINE_EVENT_NAME(LoseFocus)
-CRU_DEFINE_EVENT_NAME(GainFocus)
-CRU_DEFINE_EVENT_NAME(MouseEnter)
-CRU_DEFINE_EVENT_NAME(MouseLeave)
-CRU_DEFINE_EVENT_NAME(MouseMove)
-CRU_DEFINE_EVENT_NAME(MouseDown)
-CRU_DEFINE_EVENT_NAME(MouseUp)
-CRU_DEFINE_EVENT_NAME(KeyDown)
-CRU_DEFINE_EVENT_NAME(KeyUp)
-
-#undef CRU_DEFINE_EVENT_NAME
-} // namespace event_names
-
-namespace {
-bool IsAncestor(Control* control, Control* ancestor) {
- while (control != nullptr) {
- if (control == ancestor) return true;
- control = control->GetParent();
- }
- return false;
-}
-
-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;
- }
-}
-} // namespace
-
-namespace {
-template <typename T>
-inline void BindNativeEvent(
- UiHost* host, INativeWindow* native_window, IEvent<T>* event,
- void (UiHost::*handler)(INativeWindow*, typename IEvent<T>::EventArgs),
- std::vector<EventRevokerGuard>& guard_pool) {
- guard_pool.push_back(EventRevokerGuard(event->AddHandler(
- std::bind(handler, host, native_window, std::placeholders::_1))));
-}
-} // namespace
-
-UiHost::UiHost(Window* window)
- : window_control_(window),
- mouse_hover_control_(nullptr),
- focus_control_(window),
- mouse_captured_control_(nullptr) {
- const auto ui_application = IUiApplication::GetInstance();
- native_window_resolver_ = ui_application->CreateWindow(nullptr);
-
- const auto native_window = native_window_resolver_->Resolve();
-
- auto input_method_context =
- ui_application->GetInputMethodManager()->GetContext(native_window);
- input_method_context->DisableIME();
-
- window->ui_host_ = this;
-
- root_render_object_ = std::make_unique<render::WindowRenderObject>(this);
- root_render_object_->SetAttachedControl(window);
- window->render_object_ = root_render_object_.get();
-
- BindNativeEvent(this, native_window, native_window->DestroyEvent(),
- &UiHost::OnNativeDestroy, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->PaintEvent(),
- &UiHost::OnNativePaint, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->ResizeEvent(),
- &UiHost::OnNativeResize, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->FocusEvent(),
- &UiHost::OnNativeFocus, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->MouseEnterLeaveEvent(),
- &UiHost::OnNativeMouseEnterLeave, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->MouseMoveEvent(),
- &UiHost::OnNativeMouseMove, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->MouseDownEvent(),
- &UiHost::OnNativeMouseDown, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->MouseUpEvent(),
- &UiHost::OnNativeMouseUp, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->KeyDownEvent(),
- &UiHost::OnNativeKeyDown, event_revoker_guards_);
- BindNativeEvent(this, native_window, native_window->KeyUpEvent(),
- &UiHost::OnNativeKeyUp, event_revoker_guards_);
-}
-
-UiHost::~UiHost() {
- deleting_ = true;
- window_control_->TraverseDescendants(
- [this](Control* control) { control->OnDetachFromHost(this); });
- if (!native_window_destroyed_) {
- const auto native_window = native_window_resolver_->Resolve();
- if (native_window) {
- native_window->Close();
- }
- }
-}
-
-void UiHost::InvalidatePaint() {
- if (const auto native_window = native_window_resolver_->Resolve())
- native_window->RequestRepaint();
-}
-
-void UiHost::InvalidateLayout() {
- log::TagDebug(log_tag, u"A relayout is requested.");
- if (!need_layout_) {
- platform::native::IUiApplication::GetInstance()->InvokeLater(
- [resolver = this->CreateResolver()] {
- if (const auto host = resolver.Resolve()) {
- host->Relayout();
- host->need_layout_ = false;
- host->InvalidatePaint();
- }
- });
- need_layout_ = true;
- }
-}
-
-void UiHost::Relayout() {
- const auto native_window = native_window_resolver_->Resolve();
- const auto client_size = native_window
- ? native_window->GetClientSize()
- : Size{100, 100}; // a reasonable assumed size
- root_render_object_->Measure(
- render::MeasureRequirement{client_size,
- render::MeasureSize::NotSpecified()},
- render::MeasureSize::NotSpecified());
- root_render_object_->Layout(Point{});
- after_layout_event_.Raise(AfterLayoutEventArgs{});
- log::TagDebug(log_tag, u"A relayout is finished.");
-}
-
-bool UiHost::RequestFocusFor(Control* control) {
- Expects(control != nullptr); // The control to request focus can't be null.
- // You can set it as the window.
-
- if (focus_control_ == control) return true;
-
- const auto old_focus_control = focus_control_;
-
- focus_control_ = control;
-
- DispatchEvent(event_names::LoseFocus, old_focus_control,
- &Control::LoseFocusEvent, nullptr, false);
-
- DispatchEvent(event_names::GainFocus, control, &Control::GainFocusEvent,
- nullptr, false);
-
- return true;
-}
-
-Control* UiHost::GetFocusControl() { return focus_control_; }
-
-bool UiHost::CaptureMouseFor(Control* control) {
- const auto native_window = native_window_resolver_->Resolve();
- if (!native_window) return false;
-
- if (control == mouse_captured_control_) return true;
-
- if (control == nullptr) {
- const auto old_capture_control = mouse_captured_control_;
- mouse_captured_control_ =
- nullptr; // update this in case this is used in event handlers
- if (old_capture_control != mouse_hover_control_) {
- DispatchMouseHoverControlChangeEvent(
- old_capture_control, mouse_hover_control_,
- native_window->GetMousePosition(), true, false);
- }
- UpdateCursor();
- return true;
- }
-
- if (mouse_captured_control_) return false;
-
- mouse_captured_control_ = control;
- DispatchMouseHoverControlChangeEvent(
- mouse_hover_control_, mouse_captured_control_,
- native_window->GetMousePosition(), false, true);
- UpdateCursor();
- return true;
-}
-
-Control* UiHost::GetMouseCaptureControl() { return mouse_captured_control_; }
-
-void UiHost::OnNativeDestroy(INativeWindow* window, std::nullptr_t) {
- CRU_UNUSED(window)
- native_window_destroyed_ = true;
- if (!deleting_ && !retain_after_destroy_) delete window_control_;
-}
-
-void UiHost::OnNativePaint(INativeWindow* window, std::nullptr_t) {
- auto painter = window->BeginPaint();
- painter->Clear(colors::white);
- root_render_object_->Draw(painter.get());
- painter->EndDraw();
-}
-
-void UiHost::OnNativeResize(INativeWindow* window, const Size& size) {
- CRU_UNUSED(window)
- CRU_UNUSED(size)
-
- InvalidateLayout();
-}
-
-void UiHost::OnNativeFocus(INativeWindow* window,
- platform::native::FocusChangeType focus) {
- CRU_UNUSED(window)
-
- focus == platform::native::FocusChangeType::Gain
- ? DispatchEvent(event_names::GainFocus, focus_control_,
- &Control::GainFocusEvent, nullptr, true)
- : DispatchEvent(event_names::LoseFocus, focus_control_,
- &Control::LoseFocusEvent, nullptr, true);
-}
-
-void UiHost::OnNativeMouseEnterLeave(
- INativeWindow* window, platform::native::MouseEnterLeaveType type) {
- CRU_UNUSED(window)
-
- if (type == platform::native::MouseEnterLeaveType::Leave) {
- DispatchEvent(event_names::MouseLeave, mouse_hover_control_,
- &Control::MouseLeaveEvent, nullptr);
- mouse_hover_control_ = nullptr;
- }
-}
-
-void UiHost::OnNativeMouseMove(INativeWindow* window, const Point& point) {
- CRU_UNUSED(window)
-
- // Find the first control that hit test succeed.
- const auto new_mouse_hover_control = HitTest(point);
- const auto old_mouse_hover_control = mouse_hover_control_;
- mouse_hover_control_ = new_mouse_hover_control;
-
- if (mouse_captured_control_) {
- const auto n = FindLowestCommonAncestor(new_mouse_hover_control,
- mouse_captured_control_);
- const auto o = FindLowestCommonAncestor(old_mouse_hover_control,
- mouse_captured_control_);
- bool a = IsAncestor(o, n);
- if (a) {
- DispatchEvent(event_names::MouseLeave, o, &Control::MouseLeaveEvent, n);
- } else {
- DispatchEvent(event_names::MouseEnter, n, &Control::MouseEnterEvent, o,
- point);
- }
- DispatchEvent(event_names::MouseMove, mouse_captured_control_,
- &Control::MouseMoveEvent, nullptr, point);
- UpdateCursor();
- return;
- }
-
- DispatchMouseHoverControlChangeEvent(
- old_mouse_hover_control, new_mouse_hover_control, point, false, false);
- DispatchEvent(event_names::MouseMove, new_mouse_hover_control,
- &Control::MouseMoveEvent, nullptr, point);
- UpdateCursor();
-}
-
-void UiHost::OnNativeMouseDown(
- INativeWindow* window,
- const platform::native::NativeMouseButtonEventArgs& args) {
- CRU_UNUSED(window)
-
- Control* control =
- mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point);
- DispatchEvent(event_names::MouseDown, control, &Control::MouseDownEvent,
- nullptr, args.point, args.button, args.modifier);
-}
-
-void UiHost::OnNativeMouseUp(
- INativeWindow* window,
- const platform::native::NativeMouseButtonEventArgs& args) {
- CRU_UNUSED(window)
-
- Control* control =
- mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point);
- DispatchEvent(event_names::MouseUp, control, &Control::MouseUpEvent, nullptr,
- args.point, args.button, args.modifier);
-}
-
-void UiHost::OnNativeKeyDown(INativeWindow* window,
- const platform::native::NativeKeyEventArgs& args) {
- CRU_UNUSED(window)
-
- DispatchEvent(event_names::KeyDown, focus_control_, &Control::KeyDownEvent,
- nullptr, args.key, args.modifier);
-}
-
-void UiHost::OnNativeKeyUp(INativeWindow* window,
- const platform::native::NativeKeyEventArgs& args) {
- CRU_UNUSED(window)
-
- DispatchEvent(event_names::KeyUp, focus_control_, &Control::KeyUpEvent,
- nullptr, args.key, args.modifier);
-}
-
-void UiHost::DispatchMouseHoverControlChangeEvent(Control* old_control,
- Control* new_control,
- const Point& point,
- bool no_leave,
- bool no_enter) {
- if (new_control != old_control) // if the mouse-hover-on control changed
- {
- const auto lowest_common_ancestor =
- FindLowestCommonAncestor(old_control, new_control);
- if (!no_leave && old_control != nullptr)
- DispatchEvent(event_names::MouseLeave, old_control,
- &Control::MouseLeaveEvent,
- lowest_common_ancestor); // dispatch mouse leave event.
- if (!no_enter && new_control != nullptr) {
- DispatchEvent(event_names::MouseEnter, new_control,
- &Control::MouseEnterEvent, lowest_common_ancestor,
- point); // dispatch mouse enter event.
- }
- }
-}
-
-void UiHost::UpdateCursor() {
- if (const auto native_window = native_window_resolver_->Resolve()) {
- const auto capture = GetMouseCaptureControl();
- native_window->SetCursor(
- (capture ? capture : GetMouseHoverControl())->GetInheritedCursor());
- }
-}
-
-Control* UiHost::HitTest(const Point& point) {
- const auto render_object = root_render_object_->HitTest(point);
- if (render_object) {
- const auto control = render_object->GetAttachedControl();
- Ensures(control);
- return control;
- }
- return window_control_;
-}
-} // namespace cru::ui
diff --git a/src/ui/UiManager.cpp b/src/ui/UiManager.cpp
index 4cd38efa..7981aa86 100644
--- a/src/ui/UiManager.cpp
+++ b/src/ui/UiManager.cpp
@@ -1,13 +1,22 @@
#include "cru/ui/UiManager.hpp"
+#include <optional>
#include "Helper.hpp"
-#include "cru/platform/graph/Brush.hpp"
-#include "cru/platform/graph/Factory.hpp"
-#include "cru/platform/graph/Font.hpp"
-#include "cru/platform/native/UiApplication.hpp"
+#include "cru/platform/GraphBase.hpp"
+#include "cru/platform/graphics/Brush.hpp"
+#include "cru/platform/graphics/Factory.hpp"
+#include "cru/platform/graphics/Font.hpp"
+#include "cru/platform/gui/Cursor.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
+#include "cru/ui/Base.hpp"
+#include "cru/ui/style/ApplyBorderStyleInfo.hpp"
+#include "cru/ui/style/Condition.hpp"
+#include "cru/ui/style/Styler.hpp"
namespace cru::ui {
-using namespace cru::platform::graph;
+using namespace cru::platform::graphics;
+using namespace cru::ui::style;
+using namespace cru::ui::helper;
namespace {
std::unique_ptr<ISolidColorBrush> CreateSolidColorBrush(IGraphFactory* factory,
@@ -30,51 +39,80 @@ UiManager* UiManager::GetInstance() {
UiManager::UiManager() {
const auto factory = GetGraphFactory();
- theme_resource_.default_font = factory->CreateFont(u"等线", 24.0f);
+ theme_resource_.default_font_family = u"等线";
- const auto black_brush = std::shared_ptr<platform::graph::ISolidColorBrush>(
- CreateSolidColorBrush(factory, colors::black));
+ theme_resource_.default_font =
+ factory->CreateFont(theme_resource_.default_font_family, 24.0f);
+
+ const auto black_brush =
+ std::shared_ptr<platform::graphics::ISolidColorBrush>(
+ CreateSolidColorBrush(factory, colors::black));
theme_resource_.text_brush = black_brush;
theme_resource_.text_selection_brush =
CreateSolidColorBrush(factory, colors::skyblue);
theme_resource_.caret_brush = black_brush;
- theme_resource_.button_style.normal.border_brush =
- CreateSolidColorBrush(factory, Color::FromHex(0x00bfff));
- theme_resource_.button_style.hover.border_brush =
- CreateSolidColorBrush(factory, Color::FromHex(0x47d1ff));
- theme_resource_.button_style.press.border_brush =
- CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff));
- theme_resource_.button_style.press_cancel.border_brush =
- CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff));
-
- theme_resource_.button_style.normal.border_thickness =
- theme_resource_.button_style.hover.border_thickness =
- theme_resource_.button_style.press.border_thickness =
- theme_resource_.button_style.press_cancel.border_thickness =
- Thickness(3);
-
- theme_resource_.button_style.normal.border_radius =
- theme_resource_.button_style.hover.border_radius =
- theme_resource_.button_style.press.border_radius =
- theme_resource_.button_style.press_cancel.border_radius =
- CornerRadius({5, 5});
-
- theme_resource_.text_box_border_style.normal.border_brush =
- CreateSolidColorBrush(factory, Color::FromHex(0xced4da));
- theme_resource_.text_box_border_style.normal.border_radius = CornerRadius(5);
- theme_resource_.text_box_border_style.normal.border_thickness = Thickness(1);
-
- theme_resource_.text_box_border_style.hover =
- theme_resource_.text_box_border_style.normal;
+ theme_resource_.button_style.AddStyleRule(
+ {NoCondition::Create(),
+ BorderStyler::Create(ApplyBorderStyleInfo{std::nullopt, Thickness(3),
+ CornerRadius(5), std::nullopt,
+ std::nullopt}),
+ u"DefaultButton"});
+ theme_resource_.button_style.AddStyleRule(
+ {ClickStateCondition::Create(ClickState::None),
+ CompoundStyler::Create(
+ BorderStyler::Create(ApplyBorderStyleInfo{
+ CreateSolidColorBrush(factory, Color::FromHex(0x00bfff))}),
+ CursorStyler::Create(platform::gui::SystemCursorType::Arrow)),
+ u"DefaultButtonNormal"});
+ theme_resource_.button_style.AddStyleRule(
+ {ClickStateCondition::Create(ClickState::Hover),
+ CompoundStyler::Create(
+ BorderStyler::Create(ApplyBorderStyleInfo{
+ CreateSolidColorBrush(factory, Color::FromHex(0x47d1ff))}),
+ CursorStyler::Create(platform::gui::SystemCursorType::Hand)),
+ u"DefaultButtonHover"});
+ theme_resource_.button_style.AddStyleRule(
+ {ClickStateCondition::Create(ClickState::Press),
+ CompoundStyler::Create(
+ BorderStyler::Create(ApplyBorderStyleInfo{
+ CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff))}),
+ CursorStyler::Create(platform::gui::SystemCursorType::Hand)),
+ u"DefaultButtonPress"});
+ theme_resource_.button_style.AddStyleRule(
+ {ClickStateCondition::Create(ClickState::PressInactive),
+ CompoundStyler::Create(
+ BorderStyler::Create(ApplyBorderStyleInfo{
+ CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff))}),
+ CursorStyler::Create(platform::gui::SystemCursorType::Arrow)),
+ u"DefaultButtonPressInactive"});
- theme_resource_.text_box_border_style.focus.border_brush =
- CreateSolidColorBrush(factory, Color::FromHex(0x495057));
- theme_resource_.text_box_border_style.focus.border_radius = CornerRadius(5);
- theme_resource_.text_box_border_style.focus.border_thickness = Thickness(1);
+ theme_resource_.text_box_style.AddStyleRule(
+ {NoCondition::Create(),
+ BorderStyler::Create(
+ ApplyBorderStyleInfo{std::nullopt, Thickness{1}, CornerRadius{5}}),
+ u"DefaultTextBox"});
+ theme_resource_.text_box_style.AddStyleRule(
+ {HoverCondition::Create(false),
+ BorderStyler::Create(ApplyBorderStyleInfo{
+ CreateSolidColorBrush(factory, Color::FromHex(0xced4da))}),
+ u"DefaultTextBoxNormal"});
+ theme_resource_.text_box_style.AddStyleRule(
+ {HoverCondition::Create(true),
+ BorderStyler::Create(ApplyBorderStyleInfo{
+ CreateSolidColorBrush(factory, Color::FromHex(0xced4da))}),
+ u"DefaultTextBoxHover"});
+ theme_resource_.text_box_style.AddStyleRule(
+ {FocusCondition::Create(true),
+ BorderStyler::Create(ApplyBorderStyleInfo{
+ CreateSolidColorBrush(factory, Color::FromHex(0x495057))}),
+ u"DefaultTextBoxFocus"});
- theme_resource_.text_box_border_style.focus_hover =
- theme_resource_.text_box_border_style.focus;
+ theme_resource_.menu_item_style.AddStyleRule(
+ {NoCondition::Create(),
+ BorderStyler::Create(
+ ApplyBorderStyleInfo{std::nullopt, Thickness{0}, CornerRadius{0}}),
+ u"DefaultMenuItem"});
}
UiManager::~UiManager() = default;
diff --git a/src/ui/Window.cpp b/src/ui/Window.cpp
deleted file mode 100644
index dca95ebb..00000000
--- a/src/ui/Window.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-#include "cru/ui/Window.hpp"
-
-#include "cru/ui/render/WindowRenderObject.hpp"
-#include "cru/ui/UiHost.hpp"
-
-namespace cru::ui {
-Window* Window::CreateOverlapped() {
- return new Window(tag_overlapped_constructor{});
-}
-
-Window::Window(tag_overlapped_constructor) {
- managed_ui_host_ = std::make_unique<UiHost>(this);
-}
-
-Window::~Window() {
- // explicit destroy ui host first.
- managed_ui_host_.reset();
-}
-
-std::u16string_view Window::GetControlType() const { return control_type; }
-
-render::RenderObject* Window::GetRenderObject() const { return render_object_; }
-
-void Window::OnChildChanged(Control* old_child, Control* new_child) {
- if (old_child) render_object_->RemoveChild(0);
- if (new_child) render_object_->AddChild(new_child->GetRenderObject(), 0);
-}
-} // namespace cru::ui
diff --git a/src/ui/components/Component.cpp b/src/ui/components/Component.cpp
new file mode 100644
index 00000000..5b62ffc9
--- /dev/null
+++ b/src/ui/components/Component.cpp
@@ -0,0 +1,5 @@
+#include "cru/ui/components/Component.hpp"
+
+namespace cru::ui::components {
+
+}
diff --git a/src/ui/components/Menu.cpp b/src/ui/components/Menu.cpp
new file mode 100644
index 00000000..d45bc44f
--- /dev/null
+++ b/src/ui/components/Menu.cpp
@@ -0,0 +1,61 @@
+#include "cru/ui/components/Menu.hpp"
+#include "cru/ui/UiManager.hpp"
+#include "cru/ui/controls/Button.hpp"
+#include "cru/ui/controls/FlexLayout.hpp"
+#include "cru/ui/controls/TextBlock.hpp"
+#include "cru/ui/style/StyleRuleSet.hpp"
+
+#include <string>
+
+namespace cru::ui::components {
+MenuItem::MenuItem() {
+ container_ = controls::Button::Create();
+ text_ = controls::TextBlock::Create();
+ container_->SetChild(text_);
+ container_->GetStyleRuleSet()->SetParent(
+ &UiManager::GetInstance()->GetThemeResources()->menu_item_style);
+}
+
+MenuItem::MenuItem(std::u16string text) : MenuItem() {
+ SetText(std::move(text));
+}
+
+MenuItem::~MenuItem() {
+ if (!container_->GetWindowHost()) {
+ delete container_;
+ delete text_;
+ }
+}
+
+void MenuItem::SetText(std::u16string text) { text_->SetText(std::move(text)); }
+
+Menu::Menu() { container_ = controls::FlexLayout::Create(); }
+
+Menu::~Menu() {
+ if (!container_->GetWindowHost()) {
+ delete container_;
+ }
+
+ for (auto item : items_) {
+ delete item;
+ }
+}
+
+void Menu::AddItem(Component* item, gsl::index index) {
+ Expects(index >= 0 && index <= GetItemCount());
+
+ items_.insert(items_.cbegin() + index, item);
+ container_->AddChild(item->GetRootControl(), index);
+}
+
+Component* Menu::RemoveItem(gsl::index index) {
+ Expects(index >= 0 && index < GetItemCount());
+
+ Component* item = items_[index];
+
+ items_.erase(items_.cbegin() + index);
+ container_->RemoveChild(index);
+
+ return item;
+}
+} // namespace cru::ui::components
diff --git a/src/ui/controls/Button.cpp b/src/ui/controls/Button.cpp
index 6f6af878..c6480b77 100644
--- a/src/ui/controls/Button.cpp
+++ b/src/ui/controls/Button.cpp
@@ -1,61 +1,22 @@
#include "cru/ui/controls/Button.hpp"
-#include <memory>
#include "../Helper.hpp"
-#include "cru/platform/graph/Brush.hpp"
-#include "cru/platform/native/Cursor.hpp"
-#include "cru/platform/native/UiApplication.hpp"
-#include "cru/ui/render/BorderRenderObject.hpp"
+#include "cru/platform/graphics/Brush.hpp"
+#include "cru/platform/gui/Cursor.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
#include "cru/ui/UiManager.hpp"
-#include "cru/ui/Window.hpp"
+#include "cru/ui/helper/ClickDetector.hpp"
+#include "cru/ui/render/BorderRenderObject.hpp"
namespace cru::ui::controls {
-using cru::platform::native::SystemCursorType;
-
-namespace {
-void Set(render::BorderRenderObject* o, const ButtonStateStyle& s) {
- o->SetBorderBrush(s.border_brush);
- o->SetBorderThickness(s.border_thickness);
- o->SetBorderRadius(s.border_radius);
- o->SetForegroundBrush(s.foreground_brush);
- o->SetBackgroundBrush(s.background_brush);
-}
-
-std::shared_ptr<platform::native::ICursor> GetSystemCursor(
- SystemCursorType type) {
- return GetUiApplication()->GetCursorManager()->GetSystemCursor(type);
-}
-} // namespace
-
Button::Button() : click_detector_(this) {
- style_ = UiManager::GetInstance()->GetThemeResources()->button_style;
-
render_object_ = std::make_unique<render::BorderRenderObject>();
render_object_->SetAttachedControl(this);
- Set(render_object_.get(), style_.normal);
+ SetContainerRenderObject(render_object_.get());
render_object_->SetBorderEnabled(true);
- click_detector_.StateChangeEvent()->AddHandler(
- [this](const ClickState& state) {
- switch (state) {
- case ClickState::None:
- Set(render_object_.get(), style_.normal);
- SetCursor(GetSystemCursor(SystemCursorType::Arrow));
- break;
- case ClickState::Hover:
- Set(render_object_.get(), style_.hover);
- SetCursor(GetSystemCursor(SystemCursorType::Hand));
- break;
- case ClickState::Press:
- Set(render_object_.get(), style_.press);
- SetCursor(GetSystemCursor(SystemCursorType::Hand));
- break;
- case ClickState::PressInactive:
- Set(render_object_.get(), style_.press_cancel);
- SetCursor(GetSystemCursor(SystemCursorType::Arrow));
- break;
- }
- });
+ GetStyleRuleSet()->SetParent(
+ &UiManager::GetInstance()->GetThemeResources()->button_style);
}
Button::~Button() = default;
@@ -64,10 +25,7 @@ render::RenderObject* Button::GetRenderObject() const {
return render_object_.get();
}
-void Button::OnChildChanged(Control* old_child, Control* new_child) {
- if (old_child != nullptr) render_object_->RemoveChild(0);
- if (new_child != nullptr)
- render_object_->AddChild(new_child->GetRenderObject(), 0);
+void Button::ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) {
+ render_object_->ApplyBorderStyle(style);
}
-
} // namespace cru::ui::controls
diff --git a/src/ui/controls/Container.cpp b/src/ui/controls/Container.cpp
index de58ee64..30129f64 100644
--- a/src/ui/controls/Container.cpp
+++ b/src/ui/controls/Container.cpp
@@ -1,18 +1,20 @@
#include "cru/ui/controls/Container.hpp"
-#include "cru/platform/graph/Factory.hpp"
+#include "cru/platform/graphics/Factory.hpp"
#include "cru/ui/render/BorderRenderObject.hpp"
+#include "cru/ui/render/RenderObject.hpp"
namespace cru::ui::controls {
Container::Container() {
render_object_ = std::make_unique<render::BorderRenderObject>();
render_object_->SetBorderEnabled(false);
+ render_object_->SetAttachedControl(this);
+ SetContainerRenderObject(render_object_.get());
}
Container::~Container() = default;
-void Container::OnChildChanged(Control*, Control* new_child) {
- render_object_->RemoveChild(0);
- render_object_->AddChild(new_child->GetRenderObject(), 0);
+render::RenderObject* Container::GetRenderObject() const {
+ return render_object_.get();
}
} // namespace cru::ui::controls
diff --git a/src/ui/controls/ContentControl.cpp b/src/ui/controls/ContentControl.cpp
new file mode 100644
index 00000000..8c6f0b00
--- /dev/null
+++ b/src/ui/controls/ContentControl.cpp
@@ -0,0 +1,31 @@
+#include "cru/ui/controls/ContentControl.hpp"
+
+namespace cru::ui::controls {
+Control* ContentControl::GetChild() const {
+ if (GetChildren().empty()) return nullptr;
+ return GetChildren()[0];
+}
+
+void ContentControl::SetChild(Control* child) {
+ Control* old_child = nullptr;
+ if (!GetChildren().empty()) {
+ old_child = GetChildren()[0];
+ this->RemoveChild(0);
+ }
+ if (child) {
+ this->AddChild(child, 0);
+ }
+ OnChildChanged(old_child, child);
+}
+
+void ContentControl::OnChildChanged(Control* old_child, Control* new_child) {
+ if (container_render_object_) {
+ if (old_child) {
+ container_render_object_->RemoveChild(0);
+ }
+ if (new_child) {
+ container_render_object_->AddChild(new_child->GetRenderObject(), 0);
+ }
+ }
+}
+} // namespace cru::ui::controls
diff --git a/src/ui/controls/Control.cpp b/src/ui/controls/Control.cpp
new file mode 100644
index 00000000..29c2c46a
--- /dev/null
+++ b/src/ui/controls/Control.cpp
@@ -0,0 +1,166 @@
+#include "cru/ui/controls/Control.hpp"
+
+#include "cru/common/Base.hpp"
+#include "cru/platform/gui/Cursor.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
+#include "cru/ui/Base.hpp"
+#include "cru/ui/host/WindowHost.hpp"
+#include "cru/ui/render/RenderObject.hpp"
+#include "cru/ui/style/StyleRuleSet.hpp"
+
+#include <memory>
+
+namespace cru::ui::controls {
+using platform::gui::ICursor;
+using platform::gui::IUiApplication;
+using platform::gui::SystemCursorType;
+
+Control::Control() {
+ style_rule_set_ = std::make_unique<style::StyleRuleSet>();
+ style_rule_set_bind_ =
+ std::make_unique<style::StyleRuleSetBind>(this, style_rule_set_.get());
+
+ MouseEnterEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) {
+ this->is_mouse_over_ = true;
+ this->OnMouseHoverChange(true);
+ });
+
+ MouseLeaveEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) {
+ this->is_mouse_over_ = false;
+ this->OnMouseHoverChange(true);
+ });
+}
+
+Control::~Control() {
+ for (const auto child : children_) delete child;
+}
+
+host::WindowHost* Control::GetWindowHost() const { return window_host_; }
+
+void Control::TraverseDescendants(
+ const std::function<void(Control*)>& predicate) {
+ predicate(this);
+ for (auto c : GetChildren()) c->TraverseDescendants(predicate);
+}
+
+bool Control::HasFocus() {
+ auto host = GetWindowHost();
+ if (host == nullptr) return false;
+
+ return host->GetFocusControl() == this;
+}
+
+bool Control::CaptureMouse() {
+ auto host = GetWindowHost();
+ if (host == nullptr) return false;
+
+ return host->CaptureMouseFor(this);
+}
+
+void Control::SetFocus() {
+ auto host = GetWindowHost();
+ if (host == nullptr) return;
+
+ host->SetFocusControl(this);
+}
+
+bool Control::ReleaseMouse() {
+ auto host = GetWindowHost();
+ if (host == nullptr) return false;
+
+ return host->CaptureMouseFor(nullptr);
+}
+
+bool Control::IsMouseCaptured() {
+ auto host = GetWindowHost();
+ if (host == nullptr) return false;
+
+ return host->GetMouseCaptureControl() == this;
+}
+
+std::shared_ptr<ICursor> Control::GetCursor() { return cursor_; }
+
+std::shared_ptr<ICursor> Control::GetInheritedCursor() {
+ Control* control = this;
+ while (control != nullptr) {
+ const auto cursor = control->GetCursor();
+ if (cursor != nullptr) return cursor;
+ control = control->GetParent();
+ }
+ return IUiApplication::GetInstance()->GetCursorManager()->GetSystemCursor(
+ SystemCursorType::Arrow);
+}
+
+void Control::SetCursor(std::shared_ptr<ICursor> cursor) {
+ cursor_ = std::move(cursor);
+ const auto host = GetWindowHost();
+ if (host != nullptr) {
+ host->UpdateCursor();
+ }
+}
+
+style::StyleRuleSet* Control::GetStyleRuleSet() {
+ return style_rule_set_.get();
+}
+
+void Control::AddChild(Control* control, const Index position) {
+ Expects(control->GetParent() ==
+ nullptr); // The control already has a parent.
+ Expects(position >= 0);
+ Expects(position <= static_cast<Index>(
+ children_.size())); // The position is out of range.
+
+ children_.insert(children_.cbegin() + position, control);
+
+ const auto old_parent = control->parent_;
+ control->parent_ = this;
+
+ OnAddChild(control, position);
+ control->OnParentChanged(old_parent, this);
+
+ if (window_host_)
+ control->TraverseDescendants([this](Control* control) {
+ control->window_host_ = window_host_;
+ control->OnAttachToHost(window_host_);
+ });
+}
+
+void Control::RemoveChild(const Index position) {
+ Expects(position >= 0);
+ Expects(position < static_cast<Index>(
+ children_.size())); // The position is out of range.
+
+ const auto i = children_.cbegin() + position;
+ const auto control = *i;
+
+ children_.erase(i);
+ control->parent_ = nullptr;
+
+ OnRemoveChild(control, position);
+ control->OnParentChanged(this, nullptr);
+
+ if (window_host_)
+ control->TraverseDescendants([this](Control* control) {
+ control->window_host_ = nullptr;
+ control->OnDetachFromHost(window_host_);
+ });
+}
+
+void Control::OnAddChild(Control* child, Index position) {
+ CRU_UNUSED(child)
+ CRU_UNUSED(position)
+}
+void Control::OnRemoveChild(Control* child, Index position) {
+ CRU_UNUSED(child)
+ CRU_UNUSED(position)
+}
+
+void Control::OnParentChanged(Control* old_parent, Control* new_parent) {
+ CRU_UNUSED(old_parent)
+ CRU_UNUSED(new_parent)
+}
+
+void Control::OnAttachToHost(host::WindowHost* host) { CRU_UNUSED(host) }
+
+void Control::OnDetachFromHost(host::WindowHost* host) { CRU_UNUSED(host) }
+} // namespace cru::ui::controls
diff --git a/src/ui/controls/FlexLayout.cpp b/src/ui/controls/FlexLayout.cpp
index b7f350dc..e390241f 100644
--- a/src/ui/controls/FlexLayout.cpp
+++ b/src/ui/controls/FlexLayout.cpp
@@ -8,6 +8,7 @@ using render::FlexLayoutRenderObject;
FlexLayout::FlexLayout() {
render_object_.reset(new FlexLayoutRenderObject());
render_object_->SetAttachedControl(this);
+ SetContainerRenderObject(render_object_.get());
}
FlexLayout::~FlexLayout() = default;
@@ -60,13 +61,12 @@ void FlexLayout::SetFlexDirection(FlexDirection direction) {
render_object_->SetFlexDirection(direction);
}
-void FlexLayout::OnAddChild(Control* child, const Index position) {
- render_object_->AddChild(child->GetRenderObject(), position);
+FlexCrossAlignment FlexLayout::GetItemCrossAlign() const {
+ return render_object_->GetItemCrossAlign();
}
-void FlexLayout::OnRemoveChild(Control* child, const Index position) {
- CRU_UNUSED(child)
-
- render_object_->RemoveChild(position);
+void FlexLayout::SetItemCrossAlign(FlexCrossAlignment alignment) {
+ if (alignment == GetItemCrossAlign()) return;
+ render_object_->SetItemCrossAlign(alignment);
}
} // namespace cru::ui::controls
diff --git a/src/ui/controls/LayoutControl.cpp b/src/ui/controls/LayoutControl.cpp
new file mode 100644
index 00000000..5954853e
--- /dev/null
+++ b/src/ui/controls/LayoutControl.cpp
@@ -0,0 +1,18 @@
+#include "cru/ui/controls/LayoutControl.hpp"
+
+#include "cru/ui/render/RenderObject.hpp"
+
+namespace cru::ui::controls {
+void LayoutControl::OnAddChild(Control* child, Index position) {
+ if (container_render_object_ != nullptr) {
+ container_render_object_->AddChild(child->GetRenderObject(), position);
+ }
+}
+
+void LayoutControl::OnRemoveChild(Control* child, Index position) {
+ CRU_UNUSED(child)
+ if (container_render_object_ != nullptr) {
+ container_render_object_->RemoveChild(position);
+ }
+}
+} // namespace cru::ui::controls
diff --git a/src/ui/controls/NoChildControl.cpp b/src/ui/controls/NoChildControl.cpp
new file mode 100644
index 00000000..c62c5819
--- /dev/null
+++ b/src/ui/controls/NoChildControl.cpp
@@ -0,0 +1,3 @@
+#include "cru/ui/controls/NoChildControl.hpp"
+
+namespace cru::ui::controls {}
diff --git a/src/ui/controls/Popup.cpp b/src/ui/controls/Popup.cpp
new file mode 100644
index 00000000..bc217bf5
--- /dev/null
+++ b/src/ui/controls/Popup.cpp
@@ -0,0 +1,22 @@
+#include "cru/ui/controls/Popup.hpp"
+
+#include "cru/platform/gui/UiApplication.hpp"
+#include "cru/ui/controls/RootControl.hpp"
+#include "cru/ui/host/WindowHost.hpp"
+#include "cru/ui/render/StackLayoutRenderObject.hpp"
+
+#include <memory>
+
+namespace cru::ui::controls {
+Popup::Popup(Control* attached_control) : RootControl(attached_control) {}
+
+Popup::~Popup() = default;
+
+gsl::not_null<platform::gui::INativeWindow*> Popup::CreateNativeWindow(
+ gsl::not_null<host::WindowHost*> host,
+ platform::gui::INativeWindow* parent) {
+ return host->CreateNativeWindow(
+ {parent, platform::gui::CreateWindowFlags::NoCaptionAndBorder});
+}
+
+} // namespace cru::ui::controls
diff --git a/src/ui/controls/RootControl.cpp b/src/ui/controls/RootControl.cpp
new file mode 100644
index 00000000..015703c3
--- /dev/null
+++ b/src/ui/controls/RootControl.cpp
@@ -0,0 +1,53 @@
+#include "cru/ui/controls/RootControl.hpp"
+
+#include "cru/common/Base.hpp"
+#include "cru/platform/gui/Base.hpp"
+#include "cru/ui/host/WindowHost.hpp"
+#include "cru/ui/render/Base.hpp"
+#include "cru/ui/render/StackLayoutRenderObject.hpp"
+#include "gsl/pointers"
+
+#include <memory>
+
+namespace cru::ui::controls {
+RootControl::RootControl(Control* attached_control)
+ : attached_control_(attached_control) {
+ render_object_ = std::make_unique<render::StackLayoutRenderObject>();
+ render_object_->SetAttachedControl(this);
+ SetContainerRenderObject(render_object_.get());
+ window_host_ = std::make_unique<host::WindowHost>(this);
+}
+
+RootControl::~RootControl() {}
+
+render::RenderObject* RootControl::GetRenderObject() const {
+ return render_object_.get();
+}
+
+void RootControl::EnsureWindowCreated() { this->GetNativeWindow(true); }
+
+Rect RootControl::GetRect() { return window_host_->GetWindowRect(); }
+
+void RootControl::SetRect(const Rect& rect) {
+ window_host_->SetWindowRect(rect);
+}
+
+void RootControl::Show(bool create) {
+ platform::gui::INativeWindow* native_window = GetNativeWindow(create);
+ if (!native_window) return;
+ native_window->SetVisible(true);
+}
+
+platform::gui::INativeWindow* RootControl::GetNativeWindow(bool create) {
+ const auto host = GetWindowHost();
+ platform::gui::INativeWindow* native_window = host->GetNativeWindow();
+ if (!create) return native_window;
+ if (!native_window) {
+ native_window = this->CreateNativeWindow(
+ host, attached_control_
+ ? attached_control_->GetWindowHost()->GetNativeWindow()
+ : nullptr);
+ }
+ return native_window;
+}
+} // namespace cru::ui::controls
diff --git a/src/ui/controls/StackLayout.cpp b/src/ui/controls/StackLayout.cpp
index ce500b79..89968571 100644
--- a/src/ui/controls/StackLayout.cpp
+++ b/src/ui/controls/StackLayout.cpp
@@ -1,12 +1,15 @@
#include "cru/ui/controls/StackLayout.hpp"
+#include <memory>
#include "cru/ui/render/StackLayoutRenderObject.hpp"
namespace cru::ui::controls {
using render::StackLayoutRenderObject;
-StackLayout::StackLayout() : render_object_(new StackLayoutRenderObject()) {
+StackLayout::StackLayout() {
+ render_object_ = std::make_unique<StackLayoutRenderObject>();
render_object_->SetAttachedControl(this);
+ SetContainerRenderObject(render_object_.get());
}
StackLayout::~StackLayout() = default;
@@ -14,14 +17,4 @@ StackLayout::~StackLayout() = default;
render::RenderObject* StackLayout::GetRenderObject() const {
return render_object_.get();
}
-
-void StackLayout::OnAddChild(Control* child, const Index position) {
- render_object_->AddChild(child->GetRenderObject(), position);
-}
-
-void StackLayout::OnRemoveChild(Control* child, const Index position) {
- CRU_UNUSED(child)
-
- render_object_->RemoveChild(position);
-}
} // namespace cru::ui::controls
diff --git a/src/ui/controls/TextBlock.cpp b/src/ui/controls/TextBlock.cpp
index 9ce99ab6..0724edcf 100644
--- a/src/ui/controls/TextBlock.cpp
+++ b/src/ui/controls/TextBlock.cpp
@@ -1,16 +1,22 @@
#include "cru/ui/controls/TextBlock.hpp"
-#include "TextControlService.hpp"
#include "cru/ui/UiManager.hpp"
#include "cru/ui/render/CanvasRenderObject.hpp"
#include "cru/ui/render/StackLayoutRenderObject.hpp"
#include "cru/ui/render/TextRenderObject.hpp"
namespace cru::ui::controls {
-using render::CanvasRenderObject;
-using render::StackLayoutRenderObject;
using render::TextRenderObject;
+TextBlock* TextBlock::Create() { return new TextBlock(); }
+
+TextBlock* TextBlock::Create(std::u16string text, bool selectable) {
+ auto c = new TextBlock();
+ c->SetText(text);
+ c->SetSelectable(selectable);
+ return c;
+}
+
TextBlock::TextBlock() {
const auto theme_resources = UiManager::GetInstance()->GetThemeResources();
@@ -20,8 +26,10 @@ TextBlock::TextBlock() {
text_render_object_->SetAttachedControl(this);
- service_ = std::make_unique<TextControlService<TextBlock>>(this);
- service_->SetEnabled(true);
+ service_ = std::make_unique<TextHostControlService>(this);
+
+ service_->SetEnabled(false);
+ service_->SetEditable(false);
}
TextBlock::~TextBlock() = default;
@@ -36,6 +44,10 @@ void TextBlock::SetText(std::u16string text) {
service_->SetText(std::move(text));
}
+bool TextBlock::IsSelectable() const { return service_->IsEnabled(); }
+
+void TextBlock::SetSelectable(bool value) { service_->SetEnabled(value); }
+
gsl::not_null<render::TextRenderObject*> TextBlock::GetTextRenderObject() {
return text_render_object_.get();
}
diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp
index 4a8d6658..bfc98c06 100644
--- a/src/ui/controls/TextBox.cpp
+++ b/src/ui/controls/TextBox.cpp
@@ -1,6 +1,5 @@
#include "cru/ui/controls/TextBox.hpp"
-#include "TextControlService.hpp"
#include "cru/ui/UiManager.hpp"
#include "cru/ui/render/BorderRenderObject.hpp"
#include "cru/ui/render/CanvasRenderObject.hpp"
@@ -10,9 +9,7 @@
namespace cru::ui::controls {
using render::BorderRenderObject;
-using render::CanvasRenderObject;
using render::ScrollRenderObject;
-using render::StackLayoutRenderObject;
using render::TextRenderObject;
TextBox::TextBox()
@@ -20,8 +17,6 @@ TextBox::TextBox()
scroll_render_object_(new ScrollRenderObject()) {
const auto theme_resources = UiManager::GetInstance()->GetThemeResources();
- border_style_ = theme_resources->text_box_border_style;
-
text_render_object_ = std::make_unique<TextRenderObject>(
theme_resources->text_brush, theme_resources->default_font,
theme_resources->text_selection_brush, theme_resources->caret_brush);
@@ -33,24 +28,15 @@ TextBox::TextBox()
scroll_render_object_->SetAttachedControl(this);
text_render_object_->SetAttachedControl(this);
text_render_object_->SetMinSize(Size{100, 24});
+ text_render_object_->SetMeasureIncludingTrailingSpace(true);
- service_ = std::make_unique<TextControlService<TextBox>>(this);
+ service_ = std::make_unique<TextHostControlService>(this);
service_->SetEnabled(true);
- service_->SetCaretVisible(true);
service_->SetEditable(true);
border_render_object_->SetBorderEnabled(true);
- border_render_object_->SetBorderStyle(border_style_.normal);
-
- GainFocusEvent()->Direct()->AddHandler([this](event::FocusChangeEventArgs&) {
- this->service_->SetCaretVisible(true);
- this->UpdateBorderStyle();
- });
- LoseFocusEvent()->Direct()->AddHandler([this](event::FocusChangeEventArgs&) {
- this->service_->SetCaretVisible(false);
- this->UpdateBorderStyle();
- });
+ GetStyleRuleSet()->SetParent(&theme_resources->text_box_style);
}
TextBox::~TextBox() {}
@@ -67,19 +53,7 @@ render::ScrollRenderObject* TextBox::GetScrollRenderObject() {
return scroll_render_object_.get();
}
-const TextBoxBorderStyle& TextBox::GetBorderStyle() { return border_style_; }
-
-void TextBox::SetBorderStyle(TextBoxBorderStyle border_style) {
- border_style_ = std::move(border_style);
-}
-
-void TextBox::OnMouseHoverChange(bool) { UpdateBorderStyle(); }
-
-void TextBox::UpdateBorderStyle() {
- const auto focus = HasFocus();
- const auto hover = IsMouseOver();
- border_render_object_->SetBorderStyle(
- focus ? (hover ? border_style_.focus_hover : border_style_.focus)
- : (hover ? border_style_.hover : border_style_.normal));
+void TextBox::ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) {
+ border_render_object_->ApplyBorderStyle(style);
}
} // namespace cru::ui::controls
diff --git a/src/ui/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp
deleted file mode 100644
index 5d8d4645..00000000
--- a/src/ui/controls/TextControlService.hpp
+++ /dev/null
@@ -1,403 +0,0 @@
-#pragma once
-#include "../Helper.hpp"
-#include "cru/common/Logger.hpp"
-#include "cru/common/StringUtil.hpp"
-#include "cru/platform/graph/Font.hpp"
-#include "cru/platform/graph/Painter.hpp"
-#include "cru/platform/native/InputMethod.hpp"
-#include "cru/platform/native/UiApplication.hpp"
-#include "cru/platform/native/Window.hpp"
-#include "cru/ui/Control.hpp"
-#include "cru/ui/UiEvent.hpp"
-#include "cru/ui/UiHost.hpp"
-#include "cru/ui/render/CanvasRenderObject.hpp"
-#include "cru/ui/render/ScrollRenderObject.hpp"
-#include "cru/ui/render/TextRenderObject.hpp"
-
-namespace cru::ui::controls {
-constexpr int k_default_caret_blink_duration = 500;
-
-// TControl should inherits `Control` and has following methods:
-// ```
-// gsl::not_null<render::TextRenderObject*> GetTextRenderObject();
-// render::ScrollRenderObject* GetScrollRenderObject();
-// ```
-template <typename TControl>
-class TextControlService : public Object {
- CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::controls::TextControlService")
-
- public:
- TextControlService(gsl::not_null<TControl*> control) : control_(control) {}
-
- CRU_DELETE_COPY(TextControlService)
- CRU_DELETE_MOVE(TextControlService)
-
- ~TextControlService() override {
- const auto application = GetUiApplication();
- // Don't call TearDownCaret, because it use text render object of control,
- // which may be destroyed already.
- application->CancelTimer(this->caret_timer_id_);
- }
-
- public:
- bool IsEnabled() { return enable_; }
-
- void SetEnabled(bool enable) {
- if (enable == this->enable_) return;
- this->enable_ = enable;
- if (enable) {
- this->SetupHandlers();
- if (this->caret_visible_) {
- this->SetupCaret();
- }
- } else {
- this->AbortSelection();
- this->event_revoker_guards_.clear();
- this->TearDownCaret();
- }
- }
-
- bool IsEditable() { return this->editable_; }
-
- void SetEditable(bool editable) {
- this->editable_ = editable;
- this->input_method_context_.reset();
- }
-
- std::u16string GetText() { return this->text_; }
- std::u16string_view GetTextView() { return this->text_; }
- void SetText(std::u16string text, bool stop_composition = false) {
- this->text_ = std::move(text);
- if (stop_composition && this->input_method_context_) {
- this->input_method_context_->CancelComposition();
- }
- CoerceSelection();
- SyncTextRenderObject();
- }
-
- std::optional<platform::native::CompositionText> GetCompositionInfo() {
- if (this->input_method_context_ == nullptr) return std::nullopt;
- auto composition_info = this->input_method_context_->GetCompositionText();
- if (composition_info.text.empty()) return std::nullopt;
- return composition_info;
- }
-
- bool IsCaretVisible() { return caret_visible_; }
-
- void SetCaretVisible(bool visible) {
- if (visible == this->caret_visible_) return;
-
- this->caret_visible_ = visible;
-
- if (this->enable_) {
- if (visible) {
- this->SetupCaret();
- } else {
- this->TearDownCaret();
- }
- }
- }
-
- int GetCaretBlinkDuration() { return caret_blink_duration_; }
-
- void SetCaretBlinkDuration(int milliseconds) {
- if (this->caret_blink_duration_ == milliseconds) return;
-
- if (this->enable_ && this->caret_visible_) {
- this->TearDownCaret();
- this->SetupCaret();
- }
- }
-
- gsl::not_null<render::TextRenderObject*> GetTextRenderObject() {
- return this->control_->GetTextRenderObject();
- }
-
- render::ScrollRenderObject* GetScrollRenderObject() {
- return this->control_->GetScrollRenderObject();
- }
-
- gsl::index GetCaretPosition() { return selection_.GetEnd(); }
-
- TextRange GetSelection() { return selection_; }
-
- void SetSelection(gsl::index caret_position) {
- this->SetSelection(TextRange{caret_position, 0});
- }
-
- void SetSelection(TextRange selection, bool scroll_to_caret = true) {
- this->selection_ = selection;
- CoerceSelection();
- SyncTextRenderObject();
- if (scroll_to_caret) {
- if (const auto scroll_render_object = this->GetScrollRenderObject()) {
- const auto caret_rect = this->GetTextRenderObject()->GetCaretRect();
- // TODO: Wait a tick for layout completed.
- this->GetScrollRenderObject()->ScrollToContain(caret_rect,
- Thickness{5.f});
- }
- }
- }
-
- void DeleteSelectedText() {
- auto selection = GetSelection().Normalize();
- if (selection.count == 0) return;
- this->text_.erase(this->text_.cbegin() + selection.GetStart(),
- this->text_.cbegin() + selection.GetEnd());
- SetSelection(selection.GetStart());
- }
-
- private:
- void CoerceSelection() {
- this->selection_ = this->selection_.CoerceInto(0, text_.size());
- }
-
- void AbortSelection() {
- if (this->select_down_button_.has_value()) {
- this->control_->ReleaseMouse();
- this->select_down_button_ = std::nullopt;
- }
- this->GetTextRenderObject()->SetSelectionRange(std::nullopt);
- }
-
- void SetupCaret() {
- const auto application = GetUiApplication();
-
- // Cancel first anyhow for safety.
- application->CancelTimer(this->caret_timer_id_);
-
- this->GetTextRenderObject()->SetDrawCaret(true);
- this->caret_timer_id_ = application->SetInterval(
- std::chrono::milliseconds(this->caret_blink_duration_),
- [this] { this->GetTextRenderObject()->ToggleDrawCaret(); });
- }
-
- void TearDownCaret() {
- const auto application = GetUiApplication();
- application->CancelTimer(this->caret_timer_id_);
- this->GetTextRenderObject()->SetDrawCaret(false);
- }
-
- void SyncTextRenderObject() {
- const auto text_render_object = this->GetTextRenderObject();
- const auto composition_info = this->GetCompositionInfo();
- if (composition_info) {
- const auto caret_position = GetCaretPosition();
- auto text = this->text_;
- text.insert(caret_position, composition_info->text);
- text_render_object->SetText(text);
- text_render_object->SetCaretPosition(
- caret_position + composition_info->selection.GetEnd());
- auto selection = composition_info->selection;
- selection.position += caret_position;
- text_render_object->SetSelectionRange(selection);
- } else {
- text_render_object->SetText(this->text_);
- text_render_object->SetCaretPosition(this->GetCaretPosition());
- text_render_object->SetSelectionRange(this->GetSelection());
- }
- }
-
- template <typename TArgs>
- void SetupOneHandler(event::RoutedEvent<TArgs>* (Control::*event)(),
- void (TextControlService::*handler)(
- typename event::RoutedEvent<TArgs>::EventArgs)) {
- this->event_revoker_guards_.push_back(
- EventRevokerGuard{(this->control_->*event)()->Direct()->AddHandler(
- std::bind(handler, this, std::placeholders::_1))});
- }
-
- void StartSelection(Index start) {
- SetSelection(start);
- log::TagDebug(log_tag, u"Text selection started, position: {}.", start);
- }
-
- void UpdateSelection(Index new_end) {
- auto selection = GetSelection();
- selection.AdjustEnd(new_end);
- this->SetSelection(selection);
- log::TagDebug(log_tag, u"Text selection updated, range: {}, {}.",
- selection.GetStart(), selection.GetEnd());
- }
-
- void SetupHandlers() {
- Expects(event_revoker_guards_.empty());
-
- SetupOneHandler(&Control::MouseMoveEvent,
- &TextControlService::MouseMoveHandler);
- SetupOneHandler(&Control::MouseDownEvent,
- &TextControlService::MouseDownHandler);
- SetupOneHandler(&Control::MouseUpEvent,
- &TextControlService::MouseUpHandler);
- SetupOneHandler(&Control::KeyDownEvent,
- &TextControlService::KeyDownHandler);
- SetupOneHandler(&Control::KeyUpEvent, &TextControlService::KeyUpHandler);
- SetupOneHandler(&Control::GainFocusEvent,
- &TextControlService::GainFocusHandler);
- SetupOneHandler(&Control::LoseFocusEvent,
- &TextControlService::LoseFocusHandler);
- }
-
- void MouseMoveHandler(event::MouseEventArgs& args) {
- if (this->select_down_button_.has_value()) {
- const auto text_render_object = this->GetTextRenderObject();
- const auto result = text_render_object->TextHitTest(
- args.GetPointToContent(text_render_object));
- const auto position = result.position + (result.trailing ? 1 : 0);
- UpdateSelection(position);
- }
- }
-
- void MouseDownHandler(event::MouseButtonEventArgs& args) {
- this->control_->RequestFocus();
- if (this->select_down_button_.has_value()) {
- return;
- } else {
- if (!this->control_->CaptureMouse()) return;
- if (!this->control_->RequestFocus()) return;
- const auto text_render_object = this->GetTextRenderObject();
- this->select_down_button_ = args.GetButton();
- const auto result = text_render_object->TextHitTest(
- args.GetPointToContent(text_render_object));
- const auto position = result.position + (result.trailing ? 1 : 0);
- StartSelection(position);
- }
- }
-
- void MouseUpHandler(event::MouseButtonEventArgs& args) {
- if (this->select_down_button_.has_value() &&
- this->select_down_button_.value() == args.GetButton()) {
- this->control_->ReleaseMouse();
- this->select_down_button_ = std::nullopt;
- }
- }
-
- void KeyDownHandler(event::KeyEventArgs& args) {
- const auto key_code = args.GetKeyCode();
- using cru::platform::native::KeyCode;
- using cru::platform::native::KeyModifiers;
-
- switch (key_code) {
- case KeyCode::Backspace: {
- if (!IsEditable()) return;
- const auto selection = GetSelection();
- if (selection.count == 0) {
- const auto text = this->GetTextView();
- const auto caret_position = GetCaretPosition();
- if (caret_position == 0) return;
- gsl::index new_position;
- Utf16PreviousCodePoint(text, caret_position, &new_position);
- text_.erase(text_.cbegin() + new_position,
- text_.cbegin() + caret_position);
- SetSelection(new_position);
- } else {
- this->DeleteSelectedText();
- }
- } break;
- case KeyCode::Delete: {
- if (!IsEditable()) return;
- const auto selection = GetSelection();
- if (selection.count == 0) {
- const auto text = this->GetTextView();
- const auto caret_position = GetCaretPosition();
- if (caret_position == static_cast<gsl::index>(text.size())) return;
- gsl::index new_position;
- Utf16NextCodePoint(text, caret_position, &new_position);
- text_.erase(text_.cbegin() + caret_position,
- text_.cbegin() + new_position);
- SyncTextRenderObject();
- } else {
- this->DeleteSelectedText();
- }
- } break;
- case KeyCode::Left: {
- const auto key_modifier = args.GetKeyModifier();
- const bool shift = key_modifier & KeyModifiers::shift;
- auto text = this->GetTextView();
- if (shift) {
- auto selection = this->GetSelection();
- gsl::index new_position;
- Utf16PreviousCodePoint(text, selection.GetEnd(), &new_position);
- selection.AdjustEnd(new_position);
- this->SetSelection(selection);
- } else {
- const auto caret = this->GetCaretPosition();
- gsl::index new_position;
- Utf16PreviousCodePoint(text, caret, &new_position);
- this->SetSelection(new_position);
- }
- } break;
- case KeyCode::Right: {
- const auto key_modifier = args.GetKeyModifier();
- const bool shift = key_modifier & KeyModifiers::shift;
- auto text = this->GetTextView();
- if (shift) {
- auto selection = this->GetSelection();
- gsl::index new_position;
- Utf16NextCodePoint(text, selection.GetEnd(), &new_position);
- selection.AdjustEnd(new_position);
- this->SetSelection(selection);
- } else {
- const auto caret = this->GetCaretPosition();
- gsl::index new_position;
- Utf16NextCodePoint(text, caret, &new_position);
- this->SetSelection(new_position);
- }
- } break;
- }
- }
-
- void KeyUpHandler(event::KeyEventArgs& args) { CRU_UNUSED(args); }
-
- void GainFocusHandler(event::FocusChangeEventArgs& args) {
- CRU_UNUSED(args);
- if (editable_) {
- UiHost* ui_host = this->control_->GetUiHost();
- auto window = ui_host->GetNativeWindowResolver()->Resolve();
- if (window == nullptr) return;
- input_method_context_ =
- GetUiApplication()->GetInputMethodManager()->GetContext(window);
- input_method_context_->EnableIME();
- auto sync = [this](std::nullptr_t) { this->SyncTextRenderObject(); };
- input_method_context_->CompositionStartEvent()->AddHandler(
- [this](std::nullptr_t) { this->DeleteSelectedText(); });
- input_method_context_->CompositionEvent()->AddHandler(sync);
- input_method_context_->CompositionEndEvent()->AddHandler(sync);
- input_method_context_->TextEvent()->AddHandler(
- [this](const std::u16string_view& text) {
- if (text == u"\b") return;
- this->text_.insert(GetCaretPosition(), text);
- this->SetSelection(GetCaretPosition() + text.size());
- });
- }
- }
-
- void LoseFocusHandler(event::FocusChangeEventArgs& args) {
- if (!args.IsWindow()) this->AbortSelection();
- if (input_method_context_) {
- input_method_context_->DisableIME();
- input_method_context_.reset();
- }
- SyncTextRenderObject();
- }
-
- private:
- gsl::not_null<TControl*> control_;
- std::vector<EventRevokerGuard> event_revoker_guards_;
-
- std::u16string text_;
- TextRange selection_;
-
- bool enable_ = false;
- bool editable_ = false;
-
- bool caret_visible_ = false;
- long long caret_timer_id_ = -1;
- int caret_blink_duration_ = k_default_caret_blink_duration;
-
- // nullopt means not selecting
- std::optional<MouseButton> select_down_button_;
-
- std::unique_ptr<platform::native::IInputMethodContext> input_method_context_;
-}; // namespace cru::ui::controls
-} // namespace cru::ui::controls
diff --git a/src/ui/controls/TextHostControlService.cpp b/src/ui/controls/TextHostControlService.cpp
new file mode 100644
index 00000000..07b4f1e8
--- /dev/null
+++ b/src/ui/controls/TextHostControlService.cpp
@@ -0,0 +1,469 @@
+#include "cru/ui/controls/TextHostControlService.hpp"
+
+#include "../Helper.hpp"
+#include "cru/common/Logger.hpp"
+#include "cru/common/StringUtil.hpp"
+#include "cru/platform/gui/Base.hpp"
+#include "cru/platform/gui/Cursor.hpp"
+#include "cru/platform/gui/InputMethod.hpp"
+#include "cru/platform/gui/Keyboard.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
+#include "cru/platform/gui/Window.hpp"
+#include "cru/ui/Base.hpp"
+#include "cru/ui/DebugFlags.hpp"
+#include "cru/ui/events/UiEvent.hpp"
+#include "cru/ui/helper/ShortcutHub.hpp"
+#include "cru/ui/host/WindowHost.hpp"
+#include "cru/ui/render/ScrollRenderObject.hpp"
+#include "cru/ui/render/TextRenderObject.hpp"
+
+namespace cru::ui::controls {
+TextHostControlService::TextHostControlService(gsl::not_null<Control*> control)
+ : control_(control),
+ text_host_control_(dynamic_cast<ITextHostControl*>(control.get())) {
+ SetUpShortcuts();
+
+ SetupOneHandler(&Control::MouseMoveEvent,
+ &TextHostControlService::MouseMoveHandler);
+ SetupOneHandler(&Control::MouseDownEvent,
+ &TextHostControlService::MouseDownHandler);
+ SetupOneHandler(&Control::MouseUpEvent,
+ &TextHostControlService::MouseUpHandler);
+ SetupOneHandler(&Control::GainFocusEvent,
+ &TextHostControlService::GainFocusHandler);
+ SetupOneHandler(&Control::LoseFocusEvent,
+ &TextHostControlService::LoseFocusHandler);
+
+ shortcut_hub_.Install(control_);
+}
+
+void TextHostControlService::SetEnabled(bool enable) {
+ if (enable == this->enable_) return;
+ this->enable_ = enable;
+ if (enable) {
+ if (this->caret_visible_) {
+ this->SetupCaret();
+ }
+ this->control_->SetCursor(
+ GetUiApplication()->GetCursorManager()->GetSystemCursor(
+ platform::gui::SystemCursorType::IBeam));
+ } else {
+ this->AbortSelection();
+ this->TearDownCaret();
+ this->control_->SetCursor(nullptr);
+ }
+}
+
+void TextHostControlService::SetEditable(bool editable) {
+ this->editable_ = editable;
+ if (!editable) CancelComposition();
+}
+
+void TextHostControlService::SetText(std::u16string text,
+ bool stop_composition) {
+ this->text_ = std::move(text);
+ CoerceSelection();
+ if (stop_composition) {
+ CancelComposition();
+ }
+ SyncTextRenderObject();
+}
+
+void TextHostControlService::InsertText(gsl::index position,
+ std::u16string_view text,
+ bool stop_composition) {
+ if (!Utf16IsValidInsertPosition(this->text_, position)) {
+ log::TagError(log_tag, u"Invalid text insert position.");
+ return;
+ }
+ this->text_.insert(this->text_.cbegin() + position, text.begin(), text.end());
+ if (stop_composition) {
+ CancelComposition();
+ }
+ SyncTextRenderObject();
+}
+
+void TextHostControlService::DeleteChar(gsl::index position,
+ bool stop_composition) {
+ if (!Utf16IsValidInsertPosition(this->text_, position)) {
+ log::TagError(log_tag, u"Invalid text delete position.");
+ return;
+ }
+ if (position == static_cast<gsl::index>(this->text_.size())) return;
+ Index next;
+ Utf16NextCodePoint(this->text_, position, &next);
+ this->DeleteText(TextRange::FromTwoSides(position, next), stop_composition);
+}
+
+// Return the position of deleted character.
+gsl::index TextHostControlService::DeleteCharPrevious(gsl::index position,
+ bool stop_composition) {
+ if (!Utf16IsValidInsertPosition(this->text_, position)) {
+ log::TagError(log_tag, u"Invalid text delete position.");
+ return 0;
+ }
+ if (position == 0) return 0;
+ Index previous;
+ Utf16PreviousCodePoint(this->text_, position, &previous);
+ this->DeleteText(TextRange::FromTwoSides(previous, position),
+ stop_composition);
+ return previous;
+}
+
+void TextHostControlService::DeleteText(TextRange range,
+ bool stop_composition) {
+ if (range.count == 0) return;
+ range = range.Normalize();
+ if (!Utf16IsValidInsertPosition(this->text_, range.GetStart())) {
+ log::TagError(log_tag, u"Invalid text delete start position.");
+ return;
+ }
+ if (!Utf16IsValidInsertPosition(this->text_, range.GetStart())) {
+ log::TagError(log_tag, u"Invalid text delete end position.");
+ return;
+ }
+ this->text_.erase(this->text_.cbegin() + range.GetStart(),
+ this->text_.cbegin() + range.GetEnd());
+ this->CoerceSelection();
+ if (stop_composition) {
+ CancelComposition();
+ }
+ this->SyncTextRenderObject();
+}
+
+platform::gui::IInputMethodContext*
+TextHostControlService ::GetInputMethodContext() {
+ host::WindowHost* host = this->control_->GetWindowHost();
+ if (!host) return nullptr;
+ platform::gui::INativeWindow* native_window = host->GetNativeWindow();
+ if (!native_window) return nullptr;
+ return native_window->GetInputMethodContext();
+}
+
+void TextHostControlService::CancelComposition() {
+ auto input_method_context = GetInputMethodContext();
+ if (input_method_context == nullptr) return;
+ input_method_context->CancelComposition();
+}
+
+std::optional<platform::gui::CompositionText>
+TextHostControlService::GetCompositionInfo() {
+ auto input_method_context = GetInputMethodContext();
+ if (input_method_context == nullptr) return std::nullopt;
+ auto composition_info = input_method_context->GetCompositionText();
+ if (composition_info.text.empty()) return std::nullopt;
+ return composition_info;
+}
+
+void TextHostControlService::SetCaretVisible(bool visible) {
+ if (visible == this->caret_visible_) return;
+
+ this->caret_visible_ = visible;
+
+ if (this->enable_) {
+ if (visible) {
+ this->SetupCaret();
+ } else {
+ this->TearDownCaret();
+ }
+ }
+}
+
+void TextHostControlService::SetCaretBlinkDuration(int milliseconds) {
+ if (this->caret_blink_duration_ == milliseconds) return;
+
+ if (this->enable_ && this->caret_visible_) {
+ this->TearDownCaret();
+ this->SetupCaret();
+ }
+}
+
+void TextHostControlService::ScrollToCaret() {
+ if (const auto scroll_render_object = this->GetScrollRenderObject()) {
+ this->control_->GetWindowHost()->RunAfterLayoutStable(
+ [this, scroll_render_object]() {
+ const auto caret_rect = this->GetTextRenderObject()->GetCaretRect();
+ scroll_render_object->ScrollToContain(caret_rect, Thickness{5.f});
+ });
+ }
+}
+
+gsl::not_null<render::TextRenderObject*>
+TextHostControlService::GetTextRenderObject() {
+ return this->text_host_control_->GetTextRenderObject();
+}
+
+render::ScrollRenderObject* TextHostControlService::GetScrollRenderObject() {
+ return this->text_host_control_->GetScrollRenderObject();
+}
+
+void TextHostControlService::SetSelection(gsl::index caret_position) {
+ this->SetSelection(TextRange{caret_position, 0});
+}
+
+void TextHostControlService::SetSelection(TextRange selection,
+ bool scroll_to_caret) {
+ this->selection_ = selection;
+ CoerceSelection();
+ SyncTextRenderObject();
+ if (scroll_to_caret) {
+ this->ScrollToCaret();
+ }
+}
+
+void TextHostControlService::ChangeSelectionEnd(Index new_end) {
+ auto selection = GetSelection();
+ selection.ChangeEnd(new_end);
+ this->SetSelection(selection);
+}
+
+void TextHostControlService::AbortSelection() {
+ if (this->mouse_move_selecting_) {
+ this->control_->ReleaseMouse();
+ this->mouse_move_selecting_ = false;
+ }
+ SetSelection(GetCaretPosition());
+}
+
+void TextHostControlService::ReplaceSelectedText(std::u16string_view text) {
+ DeleteSelectedText();
+ InsertText(GetSelection().GetStart(), text);
+ SetSelection(GetSelection().GetStart() + text.size());
+}
+
+void TextHostControlService::DeleteSelectedText() {
+ this->DeleteText(GetSelection());
+ SetSelection(GetSelection().Normalize().GetStart());
+}
+
+void TextHostControlService::SetupCaret() {
+ const auto application = GetUiApplication();
+ this->GetTextRenderObject()->SetDrawCaret(true);
+ this->caret_timer_canceler_.Reset(application->SetInterval(
+ std::chrono::milliseconds(this->caret_blink_duration_),
+ [this] { this->GetTextRenderObject()->ToggleDrawCaret(); }));
+}
+
+void TextHostControlService::TearDownCaret() {
+ this->caret_timer_canceler_.Reset();
+ this->GetTextRenderObject()->SetDrawCaret(false);
+}
+
+void TextHostControlService::CoerceSelection() {
+ this->selection_ = this->selection_.CoerceInto(0, text_.size());
+}
+
+void TextHostControlService::SyncTextRenderObject() {
+ const auto text_render_object = this->GetTextRenderObject();
+ const auto composition_info = this->GetCompositionInfo();
+ if (composition_info) {
+ const auto caret_position = GetCaretPosition();
+ auto text = this->text_;
+ text.insert(caret_position, composition_info->text);
+ text_render_object->SetText(text);
+ text_render_object->SetCaretPosition(caret_position +
+ composition_info->selection.GetEnd());
+ auto selection = composition_info->selection;
+ selection.position += caret_position;
+ text_render_object->SetSelectionRange(selection);
+ } else {
+ text_render_object->SetText(this->text_);
+ text_render_object->SetCaretPosition(this->GetCaretPosition());
+ text_render_object->SetSelectionRange(this->GetSelection());
+ }
+}
+
+void TextHostControlService::UpdateInputMethodPosition() {
+ if (auto input_method_context = this->GetInputMethodContext()) {
+ Point right_bottom =
+ this->GetTextRenderObject()->GetTotalOffset() +
+ this->GetTextRenderObject()->GetCaretRect().GetRightBottom();
+ right_bottom.x += 5;
+ right_bottom.y += 5;
+
+ if constexpr (debug_flags::text_service) {
+ log::TagDebug(log_tag,
+ u"Calculate input method candidate window position: {}.",
+ right_bottom.ToDebugString());
+ }
+
+ input_method_context->SetCandidateWindowPosition(right_bottom);
+ }
+}
+
+void TextHostControlService::MouseDownHandler(
+ event::MouseButtonEventArgs& args) {
+ if (IsEnabled()) {
+ this->control_->SetFocus();
+ if (args.GetButton() == mouse_buttons::left &&
+ !this->mouse_move_selecting_) {
+ if (!this->control_->CaptureMouse()) return;
+ this->mouse_move_selecting_ = true;
+ const auto text_render_object = this->GetTextRenderObject();
+ const auto result = text_render_object->TextHitTest(
+ args.GetPointToContent(text_render_object));
+ const auto position = result.position + (result.trailing ? 1 : 0);
+ SetSelection(position);
+ }
+ }
+}
+
+void TextHostControlService::MouseUpHandler(event::MouseButtonEventArgs& args) {
+ if (args.GetButton() == mouse_buttons::left && mouse_move_selecting_) {
+ this->control_->ReleaseMouse();
+ this->mouse_move_selecting_ = false;
+ }
+}
+
+void TextHostControlService::MouseMoveHandler(event::MouseEventArgs& args) {
+ if (this->mouse_move_selecting_) {
+ const auto text_render_object = this->GetTextRenderObject();
+ const auto result = text_render_object->TextHitTest(
+ args.GetPointToContent(text_render_object));
+ const auto position = result.position + (result.trailing ? 1 : 0);
+ ChangeSelectionEnd(position);
+ }
+}
+
+void TextHostControlService::GainFocusHandler(
+ event::FocusChangeEventArgs& args) {
+ CRU_UNUSED(args);
+ if (editable_) {
+ auto input_method_context = GetInputMethodContext();
+ if (input_method_context == nullptr) return;
+ input_method_context->EnableIME();
+ auto sync = [this](std::nullptr_t) {
+ this->SyncTextRenderObject();
+ ScrollToCaret();
+ };
+ input_method_context_event_guard_ +=
+ input_method_context->CompositionStartEvent()->AddHandler(
+ [this](std::nullptr_t) { this->DeleteSelectedText(); });
+ input_method_context_event_guard_ +=
+ input_method_context->CompositionEvent()->AddHandler(sync);
+ input_method_context_event_guard_ +=
+ input_method_context->CompositionEndEvent()->AddHandler(sync);
+ input_method_context_event_guard_ +=
+ input_method_context->TextEvent()->AddHandler(
+ [this](const std::u16string_view& text) {
+ this->ReplaceSelectedText(text);
+ });
+
+ host::WindowHost* window_host = control_->GetWindowHost();
+ if (window_host)
+ input_method_context_event_guard_ +=
+ window_host->AfterLayoutEvent()->AddHandler(
+ [this](auto) { this->UpdateInputMethodPosition(); });
+ SetCaretVisible(true);
+ }
+}
+
+void TextHostControlService::LoseFocusHandler(
+ event::FocusChangeEventArgs& args) {
+ if (!args.IsWindow()) this->AbortSelection();
+ input_method_context_event_guard_.Clear();
+ auto input_method_context = GetInputMethodContext();
+ if (input_method_context) {
+ input_method_context->DisableIME();
+ }
+ SetCaretVisible(false);
+ SyncTextRenderObject();
+}
+
+void TextHostControlService::SetUpShortcuts() {
+ using platform::gui::KeyCode;
+ using platform::gui::KeyModifiers;
+
+ shortcut_hub_.RegisterShortcut(u"Backspace", KeyCode::Backspace, [this] {
+ if (!IsEnabled()) return false;
+ if (!IsEditable()) return false;
+ const auto selection = GetSelection();
+ if (selection.count == 0) {
+ SetSelection(DeleteCharPrevious(GetCaretPosition()));
+ } else {
+ this->DeleteSelectedText();
+ }
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(u"Delete", KeyCode::Delete, [this] {
+ if (!IsEnabled()) return false;
+ if (!IsEditable()) return false;
+ const auto selection = GetSelection();
+ if (selection.count == 0) {
+ DeleteChar(GetCaretPosition());
+ } else {
+ this->DeleteSelectedText();
+ }
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(u"Left", KeyCode::Left, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ Utf16PreviousCodePoint(text, caret, &caret);
+ this->SetSelection(caret);
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(u"ShiftLeft",
+ {KeyCode::Left, KeyModifiers::shift}, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ Utf16PreviousCodePoint(text, caret, &caret);
+ this->ChangeSelectionEnd(caret);
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(
+ u"CtrlLeft", {KeyCode::Left, KeyModifiers::ctrl}, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ this->SetSelection(Utf16PreviousWord(text, caret));
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(
+ u"CtrlShiftLeft",
+ {KeyCode::Left, KeyModifiers::ctrl | KeyModifiers::shift}, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ this->ChangeSelectionEnd(Utf16PreviousWord(text, caret));
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(u"Right", KeyCode::Right, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ Utf16NextCodePoint(text, caret, &caret);
+ this->SetSelection(caret);
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(u"ShiftRight",
+ {KeyCode::Right, KeyModifiers::shift}, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ Utf16NextCodePoint(text, caret, &caret);
+ this->ChangeSelectionEnd(caret);
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(
+ u"CtrlRight", {KeyCode::Right, KeyModifiers::ctrl}, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ this->SetSelection(Utf16NextWord(text, caret));
+ return true;
+ });
+
+ shortcut_hub_.RegisterShortcut(
+ u"CtrlShiftRight",
+ {KeyCode::Right, KeyModifiers::ctrl | KeyModifiers::shift}, [this] {
+ auto text = this->GetTextView();
+ auto caret = this->GetCaretPosition();
+ this->ChangeSelectionEnd(Utf16NextWord(text, caret));
+ return true;
+ });
+}
+} // namespace cru::ui::controls
diff --git a/src/ui/controls/Window.cpp b/src/ui/controls/Window.cpp
new file mode 100644
index 00000000..ba66f42e
--- /dev/null
+++ b/src/ui/controls/Window.cpp
@@ -0,0 +1,24 @@
+#include "cru/ui/controls/Window.hpp"
+
+#include "cru/common/Base.hpp"
+#include "cru/platform/gui/Base.hpp"
+#include "cru/ui/controls/RootControl.hpp"
+#include "cru/ui/host/WindowHost.hpp"
+#include "cru/ui/render/Base.hpp"
+#include "cru/ui/render/StackLayoutRenderObject.hpp"
+
+namespace cru::ui::controls {
+Window* Window::Create(Control* attached_control) {
+ return new Window(attached_control);
+}
+
+Window::Window(Control* attached_control) : RootControl(attached_control) {}
+
+Window::~Window() {}
+
+gsl::not_null<platform::gui::INativeWindow*> Window::CreateNativeWindow(
+ gsl::not_null<host::WindowHost*> host,
+ platform::gui::INativeWindow* parent) {
+ return host->CreateNativeWindow({parent});
+}
+} // namespace cru::ui::controls
diff --git a/src/ui/UiEvent.cpp b/src/ui/events/UiEvent.cpp
index 74dd54dc..4c75f690 100644
--- a/src/ui/UiEvent.cpp
+++ b/src/ui/events/UiEvent.cpp
@@ -1,8 +1,12 @@
-#include "cru/ui/UiEvent.hpp"
+#include "cru/ui/events/UiEvent.hpp"
#include "cru/ui/render/RenderObject.hpp"
namespace cru::ui::event {
+Point MouseEventArgs::GetPoint(render::RenderObject* render_object) const {
+ return GetPoint() - render_object->GetTotalOffset();
+}
+
Point MouseEventArgs::GetPointToContent(
render::RenderObject* render_object) const {
return render_object->FromRootToContent(GetPoint());
diff --git a/src/ui/helper/BorderStyle.cpp b/src/ui/helper/BorderStyle.cpp
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/src/ui/helper/BorderStyle.cpp
diff --git a/src/ui/ClickDetector.cpp b/src/ui/helper/ClickDetector.cpp
index 09f208cd..309685d3 100644
--- a/src/ui/ClickDetector.cpp
+++ b/src/ui/helper/ClickDetector.cpp
@@ -1,11 +1,12 @@
-#include "cru/ui/ClickDetector.hpp"
+#include "cru/ui/helper/ClickDetector.hpp"
#include "cru/common/Logger.hpp"
+#include "cru/ui/DebugFlags.hpp"
#include <optional>
-namespace cru::ui {
-ClickDetector::ClickDetector(Control* control) {
+namespace cru::ui::helper {
+ClickDetector::ClickDetector(controls::Control* control) {
Expects(control);
control_ = control;
@@ -44,8 +45,10 @@ ClickDetector::ClickDetector(Control* control) {
if (this->enable_ && (button & this->trigger_button_) &&
this->state_ == ClickState::Hover) {
if (!this->control_->CaptureMouse()) {
- log::TagDebug(log_tag,
- u"Failed to capture mouse when begin click.");
+ if constexpr (debug_flags::click_detector) {
+ log::TagDebug(log_tag,
+ u"Failed to capture mouse when begin click.");
+ }
return;
}
this->down_point_ = args.GetPoint();
@@ -106,26 +109,26 @@ void ClickDetector::SetTriggerButton(MouseButton trigger_button) {
}
void ClickDetector::SetState(ClickState state) {
-#ifdef CRU_DEBUG
- auto to_string = [](ClickState state) -> std::u16string_view {
- switch (state) {
- case ClickState::None:
- return u"None";
- case ClickState::Hover:
- return u"Hover";
- case ClickState::Press:
- return u"Press";
- case ClickState::PressInactive:
- return u"PressInvactive";
- default:
- UnreachableCode();
- }
- };
- log::TagDebug(log_tag, u"Click state changed, new state: {}.",
- to_string(state));
-#endif
+ if constexpr (debug_flags::click_detector) {
+ auto to_string = [](ClickState state) -> std::u16string_view {
+ switch (state) {
+ case ClickState::None:
+ return u"None";
+ case ClickState::Hover:
+ return u"Hover";
+ case ClickState::Press:
+ return u"Press";
+ case ClickState::PressInactive:
+ return u"PressInvactive";
+ default:
+ UnreachableCode();
+ }
+ };
+ log::TagDebug(log_tag, u"Click state changed, new state: {}.",
+ to_string(state));
+ }
state_ = state;
state_change_event_.Raise(state);
}
-} // namespace cru::ui
+} // namespace cru::ui::helper
diff --git a/src/ui/helper/ShortcutHub.cpp b/src/ui/helper/ShortcutHub.cpp
new file mode 100644
index 00000000..f35ad0ef
--- /dev/null
+++ b/src/ui/helper/ShortcutHub.cpp
@@ -0,0 +1,131 @@
+#include "cru/ui/helper/ShortcutHub.hpp"
+
+#include "cru/common/Logger.hpp"
+#include "cru/ui/DebugFlags.hpp"
+#include "cru/ui/controls/Control.hpp"
+
+#include <algorithm>
+#include <functional>
+#include <iterator>
+#include <optional>
+
+namespace cru::ui::helper {
+int ShortcutHub::RegisterShortcut(Shortcut shortcut) {
+ const int id = current_id_++;
+ map_[shortcut.key_bind].push_back({id, std::move(shortcut.name),
+ shortcut.key_bind,
+ std::move(shortcut.handler)});
+ return id;
+}
+
+void ShortcutHub::UnregisterShortcut(int id) {
+ if (id <= 0) return;
+ for (auto& pair : map_) {
+ auto& list = pair.second;
+ auto result =
+ std::find_if(list.cbegin(), list.cend(),
+ [id](const ShortcutInfo& info) { return info.id == id; });
+ if (result != list.cend()) {
+ list.erase(result);
+ }
+ }
+}
+
+std::vector<ShortcutInfo> ShortcutHub::GetAllShortcuts() const {
+ std::vector<ShortcutInfo> result;
+
+ for (const auto& pair : map_) {
+ std::copy(pair.second.cbegin(), pair.second.cend(),
+ std::back_inserter(result));
+ }
+
+ return result;
+}
+
+std::optional<ShortcutInfo> ShortcutHub::GetShortcut(int id) const {
+ for (auto& pair : map_) {
+ auto& list = pair.second;
+ auto result =
+ std::find_if(list.cbegin(), list.cend(),
+ [id](const ShortcutInfo& info) { return info.id == id; });
+ if (result != list.cend()) {
+ return *result;
+ }
+ }
+ return std::nullopt;
+}
+
+const std::vector<ShortcutInfo>& ShortcutHub::GetShortcutByKeyBind(
+ const ShortcutKeyBind& key_bind) const {
+ auto result = map_.find(key_bind);
+ if (result != map_.cend()) return result->second;
+ return empty_list_;
+}
+
+void ShortcutHub::Install(controls::Control* control) {
+ if (!event_guard_.IsEmpty()) {
+ log::Error(u"Shortcut hub is already installed. Failed to install.");
+ return;
+ }
+
+ event_guard_ += control->KeyDownEvent()->Bubble()->AddHandler(
+ std::bind(&ShortcutHub::OnKeyDown, this, std::placeholders::_1));
+}
+
+void ShortcutHub::Uninstall() {
+ if (event_guard_.IsEmpty()) {
+ log::Warn(u"Shortcut hub is not installed. Failed to uninstall.");
+ return;
+ }
+
+ event_guard_.Clear();
+}
+
+void ShortcutHub::OnKeyDown(event::KeyEventArgs& event) {
+ ShortcutKeyBind key_bind(event.GetKeyCode(), event.GetKeyModifier());
+ const auto& shortcut_list = this->GetShortcutByKeyBind(key_bind);
+
+ bool handled = false;
+
+ if constexpr (debug_flags::shortcut) {
+ if (shortcut_list.empty()) {
+ log::Debug(u"No shortcut for key bind {}.", key_bind.ToString());
+ }
+ log::Debug(u"Begin to handle shortcut for key bind {}.",
+ key_bind.ToString());
+ }
+
+ for (const auto& shortcut : shortcut_list) {
+ auto is_handled = shortcut.handler();
+ if (is_handled) {
+ if constexpr (debug_flags::shortcut) {
+ log::Debug(u"Handle {} handled it.", shortcut.name);
+ }
+
+ handled = true;
+ event.SetHandled();
+
+ break;
+ } else {
+ if constexpr (debug_flags::shortcut) {
+ log::Debug(u"Handle {} didn't handle it.", shortcut.name);
+ }
+ }
+ }
+
+ if constexpr (debug_flags::shortcut) {
+ if (!shortcut_list.empty()) {
+ log::Debug(u"End handling shortcut for key bind {}.",
+ key_bind.ToString());
+ }
+ }
+
+ if (!handled) {
+ if constexpr (debug_flags::shortcut) {
+ log::Debug(u"Raise fallback event for unhandled shortcut of key bind {}.",
+ key_bind.ToString());
+ }
+ fallback_event_.Raise(event);
+ }
+}
+} // namespace cru::ui::helper
diff --git a/src/ui/host/LayoutPaintCycler.cpp b/src/ui/host/LayoutPaintCycler.cpp
new file mode 100644
index 00000000..fd581e00
--- /dev/null
+++ b/src/ui/host/LayoutPaintCycler.cpp
@@ -0,0 +1,35 @@
+#include "cru/ui/host/LayoutPaintCycler.hpp"
+#include <chrono>
+
+#include "../Helper.hpp"
+#include "cru/ui/Base.hpp"
+#include "cru/ui/host/WindowHost.hpp"
+
+namespace cru::ui::host {
+LayoutPaintCycler::LayoutPaintCycler(WindowHost* host) : host_(host) {
+ timer_canceler_ = GetUiApplication()->SetInterval(
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ this->cycle_threshold_),
+ [this] { OnCycle(); });
+}
+
+LayoutPaintCycler::~LayoutPaintCycler() = default;
+
+void LayoutPaintCycler::InvalidateLayout() { layout_dirty_ = true; }
+
+void LayoutPaintCycler::InvalidatePaint() { paint_dirty_ = true; }
+
+void LayoutPaintCycler::OnCycle() {
+ last_cycle_time_ = std::chrono::steady_clock::now();
+ if (layout_dirty_) {
+ host_->Relayout();
+ host_->Repaint();
+ } else {
+ if (paint_dirty_) {
+ host_->Repaint();
+ }
+ }
+ layout_dirty_ = false;
+ paint_dirty_ = false;
+}
+} // namespace cru::ui::host
diff --git a/src/ui/RoutedEventDispatch.hpp b/src/ui/host/RoutedEventDispatch.hpp
index 9337e9ec..52507fc7 100644
--- a/src/ui/RoutedEventDispatch.hpp
+++ b/src/ui/host/RoutedEventDispatch.hpp
@@ -1,9 +1,9 @@
#pragma once
-#include "cru/ui/Control.hpp"
-
#include "cru/common/Logger.hpp"
+#include "cru/ui/DebugFlags.hpp"
+#include "cru/ui/controls/Control.hpp"
-#include <list>
+#include <vector>
namespace cru::ui {
// Dispatch the event.
@@ -20,33 +20,23 @@ namespace cru::ui {
// "original_sender", which is unchanged. And "args" will be perfectly forwarded
// as the rest arguments.
template <typename EventArgs, typename... Args>
-void DispatchEvent(const std::u16string_view& event_name,
- Control* const original_sender,
- event::RoutedEvent<EventArgs>* (Control::*event_ptr)(),
- Control* const last_receiver, Args&&... args) {
-#ifndef CRU_DEBUG
+void DispatchEvent(
+ const std::u16string_view& event_name,
+ controls::Control* const original_sender,
+ event::RoutedEvent<EventArgs>* (controls::Control::*event_ptr)(),
+ controls::Control* const last_receiver, Args&&... args) {
CRU_UNUSED(event_name)
-#endif
-
-#ifdef CRU_DEBUG
- bool do_log = true;
- if (event_name == u"MouseMove") do_log = false;
-#endif
if (original_sender == last_receiver) {
- /*
- #ifdef CRU_DEBUG
- if (do_log)
- log::Debug(
- "Routed event {} no need to dispatch (original_sender == "
- "last_receiver). Original sender is {}.",
- event_name, original_sender->GetControlType());
- #endif
- */
+ if constexpr (debug_flags::routed_event)
+ log::Debug(
+ "Routed event {} no need to dispatch (original_sender == "
+ "last_receiver). Original sender is {}.",
+ event_name, original_sender->GetControlType());
return;
}
- std::list<Control*> receive_list;
+ std::vector<controls::Control*> receive_list;
auto parent = original_sender;
while (parent != last_receiver) {
@@ -54,8 +44,7 @@ void DispatchEvent(const std::u16string_view& event_name,
parent = parent->GetParent();
}
-#ifdef CRU_DEBUG
- if (do_log) {
+ if constexpr (debug_flags::routed_event) {
std::u16string log = u"Dispatch routed event ";
log += event_name;
log += u". Path (parent first): ";
@@ -68,31 +57,24 @@ void DispatchEvent(const std::u16string_view& event_name,
log += (*i)->GetControlType();
log::Debug(log);
}
-#endif
auto handled = false;
-#ifdef CRU_DEBUG
int count = 0;
-#endif
// tunnel
for (auto i = receive_list.crbegin(); i != receive_list.crend(); ++i) {
-#ifdef CRU_DEBUG
count++;
-#endif
EventArgs event_args(*i, original_sender, std::forward<Args>(args)...);
static_cast<Event<EventArgs&>*>(((*i)->*event_ptr)()->Tunnel())
->Raise(event_args);
if (event_args.IsHandled()) {
handled = true;
-#ifdef CRU_DEBUG
- if (do_log)
+ if constexpr (debug_flags::routed_event)
log::Debug(
u"Routed event is short-circuit in TUNNEL at {}-st control (count "
u"from parent).",
count);
-#endif
break;
}
}
@@ -100,20 +82,16 @@ void DispatchEvent(const std::u16string_view& event_name,
// bubble
if (!handled) {
for (auto i : receive_list) {
-#ifdef CRU_DEBUG
count--;
-#endif
EventArgs event_args(i, original_sender, std::forward<Args>(args)...);
static_cast<Event<EventArgs&>*>((i->*event_ptr)()->Bubble())
->Raise(event_args);
if (event_args.IsHandled()) {
-#ifdef CRU_DEBUG
- if (do_log)
+ if constexpr (debug_flags::routed_event)
log::Debug(
u"Routed event is short-circuit in BUBBLE at {}-st control "
u"(count from parent).",
count);
-#endif
break;
}
}
@@ -126,8 +104,7 @@ void DispatchEvent(const std::u16string_view& event_name,
->Raise(event_args);
}
-#ifdef CRU_DEBUG
- if (do_log) log::Debug(u"Routed event dispatch finished.");
-#endif
+ if constexpr (debug_flags::routed_event)
+ log::Debug(u"Routed event dispatch finished.");
}
} // namespace cru::ui
diff --git a/src/ui/host/WindowHost.cpp b/src/ui/host/WindowHost.cpp
new file mode 100644
index 00000000..5e107733
--- /dev/null
+++ b/src/ui/host/WindowHost.cpp
@@ -0,0 +1,440 @@
+#include "cru/ui/host/WindowHost.hpp"
+
+#include "RoutedEventDispatch.hpp"
+#include "cru/common/Base.hpp"
+#include "cru/common/Logger.hpp"
+#include "cru/platform/graphics/Painter.hpp"
+#include "cru/platform/gui/InputMethod.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
+#include "cru/platform/gui/Window.hpp"
+#include "cru/ui/DebugFlags.hpp"
+#include "cru/ui/controls/Window.hpp"
+#include "cru/ui/host/LayoutPaintCycler.hpp"
+#include "cru/ui/render/MeasureRequirement.hpp"
+#include "cru/ui/render/RenderObject.hpp"
+
+#include <cstddef>
+#include <memory>
+
+namespace cru::ui::host {
+using platform::gui::INativeWindow;
+using platform::gui::IUiApplication;
+
+namespace event_names {
+#ifdef CRU_DEBUG
+// clang-format off
+#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = CRU_MAKE_UNICODE_LITERAL(name);
+// clang-format on
+#else
+#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = u"";
+#endif
+
+CRU_DEFINE_EVENT_NAME(LoseFocus)
+CRU_DEFINE_EVENT_NAME(GainFocus)
+CRU_DEFINE_EVENT_NAME(MouseEnter)
+CRU_DEFINE_EVENT_NAME(MouseLeave)
+CRU_DEFINE_EVENT_NAME(MouseMove)
+CRU_DEFINE_EVENT_NAME(MouseDown)
+CRU_DEFINE_EVENT_NAME(MouseUp)
+CRU_DEFINE_EVENT_NAME(KeyDown)
+CRU_DEFINE_EVENT_NAME(KeyUp)
+
+#undef CRU_DEFINE_EVENT_NAME
+} // namespace event_names
+
+namespace {
+bool IsAncestor(controls::Control* control, controls::Control* ancestor) {
+ while (control != nullptr) {
+ if (control == ancestor) return true;
+ control = control->GetParent();
+ }
+ return false;
+}
+
+// Ancestor at last.
+std::vector<controls::Control*> GetAncestorList(controls::Control* control) {
+ std::vector<controls::Control*> l;
+ while (control != nullptr) {
+ l.push_back(control);
+ control = control->GetParent();
+ }
+ return l;
+}
+
+controls::Control* FindLowestCommonAncestor(controls::Control* left,
+ controls::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.back() != right_list.back()) return nullptr;
+
+ // find the last same control or the last control (one is ancestor of the
+ // other)
+ auto left_iter = left_list.crbegin();
+ auto right_iter = right_list.crbegin();
+
+ while (true) {
+ if (left_iter == left_list.crend()) {
+ return left_list.front();
+ }
+ if (right_iter == right_list.crend()) {
+ return right_list.front();
+ }
+ if (*left_iter != *right_iter) {
+ return *(--left_iter);
+ }
+ ++left_iter;
+ ++right_iter;
+ }
+}
+} // namespace
+
+namespace {
+template <typename T>
+inline void BindNativeEvent(
+ WindowHost* host, INativeWindow* native_window, IEvent<T>* event,
+ void (WindowHost::*handler)(INativeWindow*, typename IEvent<T>::EventArgs),
+ std::vector<EventRevokerGuard>& guard_pool) {
+ guard_pool.push_back(EventRevokerGuard(event->AddHandler(
+ std::bind(handler, host, native_window, std::placeholders::_1))));
+}
+} // namespace
+
+WindowHost::WindowHost(controls::Control* root_control)
+ : root_control_(root_control), focus_control_(root_control) {
+ root_control_->TraverseDescendants([this](controls::Control* control) {
+ control->window_host_ = this;
+ control->OnAttachToHost(this);
+ });
+
+ root_render_object_ = root_control->GetRenderObject();
+ root_render_object_->SetWindowHostRecursive(this);
+
+ this->layout_paint_cycler_ = std::make_unique<LayoutPaintCycler>(this);
+}
+
+WindowHost::~WindowHost() {}
+
+gsl::not_null<platform::gui::INativeWindow*> WindowHost::CreateNativeWindow(
+ CreateWindowParams create_window_params) {
+ if (native_window_ != nullptr) return native_window_;
+
+ const auto ui_application = IUiApplication::GetInstance();
+
+ auto native_window = ui_application->CreateWindow(create_window_params.parent,
+ create_window_params.flag);
+
+ native_window_ = native_window;
+
+ BindNativeEvent(this, native_window, native_window->DestroyEvent(),
+ &WindowHost::OnNativeDestroy, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->PaintEvent(),
+ &WindowHost::OnNativePaint, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->ResizeEvent(),
+ &WindowHost::OnNativeResize, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->FocusEvent(),
+ &WindowHost::OnNativeFocus, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->MouseEnterLeaveEvent(),
+ &WindowHost::OnNativeMouseEnterLeave, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->MouseMoveEvent(),
+ &WindowHost::OnNativeMouseMove, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->MouseDownEvent(),
+ &WindowHost::OnNativeMouseDown, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->MouseUpEvent(),
+ &WindowHost::OnNativeMouseUp, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->KeyDownEvent(),
+ &WindowHost::OnNativeKeyDown, event_revoker_guards_);
+ BindNativeEvent(this, native_window, native_window->KeyUpEvent(),
+ &WindowHost::OnNativeKeyUp, event_revoker_guards_);
+
+ if (saved_rect_) {
+ native_window->SetWindowRect(saved_rect_.value());
+ }
+
+ native_window_change_event_.Raise(native_window);
+
+ return native_window_;
+}
+
+void WindowHost::InvalidatePaint() { layout_paint_cycler_->InvalidatePaint(); }
+
+void WindowHost::InvalidateLayout() {
+ layout_paint_cycler_->InvalidateLayout();
+}
+
+bool WindowHost::IsLayoutPreferToFillWindow() const {
+ return layout_prefer_to_fill_window_;
+}
+
+void WindowHost::SetLayoutPreferToFillWindow(bool value) {
+ if (value == layout_prefer_to_fill_window_) return;
+ layout_prefer_to_fill_window_ = value;
+ InvalidateLayout();
+}
+
+void WindowHost::Relayout() {
+ const auto available_size =
+ native_window_ ? native_window_->GetClientSize()
+ : Size{100, 100}; // a reasonable assumed size
+ Relayout(available_size);
+}
+
+void WindowHost::Relayout(const Size& available_size) {
+ root_render_object_->Measure(
+ render::MeasureRequirement{available_size,
+ IsLayoutPreferToFillWindow()
+ ? render::MeasureSize(available_size)
+ : render::MeasureSize::NotSpecified()},
+ render::MeasureSize::NotSpecified());
+ root_render_object_->Layout(Point{});
+ for (auto& action : after_layout_stable_action_) action();
+ after_layout_event_.Raise(AfterLayoutEventArgs{});
+ root_render_object_->TraverseDescendants(
+ [](render::RenderObject* render_object) {
+ render_object->OnAfterLayout();
+ });
+ after_layout_stable_action_.clear();
+ if constexpr (debug_flags::layout)
+ log::TagDebug(log_tag, u"A relayout is finished.");
+}
+
+void WindowHost::Repaint() {
+ auto painter = native_window_->BeginPaint();
+ painter->Clear(colors::white);
+ root_render_object_->Draw(painter.get());
+ painter->EndDraw();
+}
+
+controls::Control* WindowHost::GetFocusControl() { return focus_control_; }
+
+void WindowHost::SetFocusControl(controls::Control* control) {
+ if (focus_control_ == control) return;
+ if (control == nullptr) control = root_control_;
+
+ const auto old_focus_control = focus_control_;
+
+ focus_control_ = control;
+
+ DispatchEvent(event_names::LoseFocus, old_focus_control,
+ &controls::Control::LoseFocusEvent, nullptr, false);
+
+ DispatchEvent(event_names::GainFocus, control,
+ &controls::Control::GainFocusEvent, nullptr, false);
+}
+
+bool WindowHost::CaptureMouseFor(controls::Control* control) {
+ if (!native_window_) return false;
+ if (!native_window_->CaptureMouse()) return false;
+
+ if (control == mouse_captured_control_) return true;
+
+ if (control == nullptr) {
+ native_window_->ReleaseMouse();
+ const auto old_capture_control = mouse_captured_control_;
+ mouse_captured_control_ =
+ nullptr; // update this in case this is used in event handlers
+ if (old_capture_control != mouse_hover_control_) {
+ DispatchMouseHoverControlChangeEvent(
+ old_capture_control, mouse_hover_control_,
+ native_window_->GetMousePosition(), true, false);
+ }
+ UpdateCursor();
+ return true;
+ }
+
+ if (mouse_captured_control_) return false;
+
+ mouse_captured_control_ = control;
+ DispatchMouseHoverControlChangeEvent(
+ mouse_hover_control_, mouse_captured_control_,
+ native_window_->GetMousePosition(), false, true);
+ UpdateCursor();
+ return true;
+}
+
+controls::Control* WindowHost::GetMouseCaptureControl() {
+ return mouse_captured_control_;
+}
+
+void WindowHost::RunAfterLayoutStable(std::function<void()> action) {
+ if (layout_paint_cycler_->IsLayoutDirty()) {
+ after_layout_stable_action_.push_back(std::move(action));
+ } else {
+ action();
+ }
+}
+
+Rect WindowHost::GetWindowRect() {
+ if (native_window_) return native_window_->GetWindowRect();
+ return saved_rect_.value_or(Rect{});
+}
+
+void WindowHost::SetSavedWindowRect(std::optional<Rect> rect) {
+ saved_rect_ = std::move(rect);
+}
+
+void WindowHost::SetWindowRect(const Rect& rect) {
+ SetSavedWindowRect(rect);
+ if (native_window_) native_window_->SetWindowRect(rect);
+}
+
+void WindowHost::OnNativeDestroy(INativeWindow* window, std::nullptr_t) {
+ CRU_UNUSED(window)
+
+ saved_rect_ = this->native_window_->GetWindowRect();
+
+ this->native_window_ = nullptr;
+ event_revoker_guards_.clear();
+
+ native_window_change_event_.Raise(nullptr);
+}
+
+void WindowHost::OnNativePaint(INativeWindow* window, std::nullptr_t) {
+ CRU_UNUSED(window)
+ layout_paint_cycler_->InvalidatePaint();
+}
+
+void WindowHost::OnNativeResize(INativeWindow* window, const Size& size) {
+ CRU_UNUSED(window)
+ CRU_UNUSED(size)
+
+ InvalidateLayout();
+}
+
+void WindowHost::OnNativeFocus(INativeWindow* window,
+ platform::gui::FocusChangeType focus) {
+ CRU_UNUSED(window)
+
+ focus == platform::gui::FocusChangeType::Gain
+ ? DispatchEvent(event_names::GainFocus, focus_control_,
+ &controls::Control::GainFocusEvent, nullptr, true)
+ : DispatchEvent(event_names::LoseFocus, focus_control_,
+ &controls::Control::LoseFocusEvent, nullptr, true);
+}
+
+void WindowHost::OnNativeMouseEnterLeave(
+ INativeWindow* window, platform::gui::MouseEnterLeaveType type) {
+ CRU_UNUSED(window)
+
+ if (type == platform::gui::MouseEnterLeaveType::Leave) {
+ DispatchEvent(event_names::MouseLeave, mouse_hover_control_,
+ &controls::Control::MouseLeaveEvent, nullptr);
+ mouse_hover_control_ = nullptr;
+ }
+}
+
+void WindowHost::OnNativeMouseMove(INativeWindow* window, const Point& point) {
+ CRU_UNUSED(window)
+
+ // Find the first control that hit test succeed.
+ const auto new_mouse_hover_control = HitTest(point);
+ const auto old_mouse_hover_control = mouse_hover_control_;
+ mouse_hover_control_ = new_mouse_hover_control;
+
+ if (mouse_captured_control_) {
+ const auto n = FindLowestCommonAncestor(new_mouse_hover_control,
+ mouse_captured_control_);
+ const auto o = FindLowestCommonAncestor(old_mouse_hover_control,
+ mouse_captured_control_);
+ bool a = IsAncestor(o, n);
+ if (a) {
+ DispatchEvent(event_names::MouseLeave, o,
+ &controls::Control::MouseLeaveEvent, n);
+ } else {
+ DispatchEvent(event_names::MouseEnter, n,
+ &controls::Control::MouseEnterEvent, o, point);
+ }
+ DispatchEvent(event_names::MouseMove, mouse_captured_control_,
+ &controls::Control::MouseMoveEvent, nullptr, point);
+ UpdateCursor();
+ return;
+ }
+
+ DispatchMouseHoverControlChangeEvent(
+ old_mouse_hover_control, new_mouse_hover_control, point, false, false);
+ DispatchEvent(event_names::MouseMove, new_mouse_hover_control,
+ &controls::Control::MouseMoveEvent, nullptr, point);
+ UpdateCursor();
+}
+
+void WindowHost::OnNativeMouseDown(
+ INativeWindow* window,
+ const platform::gui::NativeMouseButtonEventArgs& args) {
+ CRU_UNUSED(window)
+
+ controls::Control* control =
+ mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point);
+ DispatchEvent(event_names::MouseDown, control,
+ &controls::Control::MouseDownEvent, nullptr, args.point,
+ args.button, args.modifier);
+}
+
+void WindowHost::OnNativeMouseUp(
+ INativeWindow* window,
+ const platform::gui::NativeMouseButtonEventArgs& args) {
+ CRU_UNUSED(window)
+
+ controls::Control* control =
+ mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point);
+ DispatchEvent(event_names::MouseUp, control, &controls::Control::MouseUpEvent,
+ nullptr, args.point, args.button, args.modifier);
+}
+
+void WindowHost::OnNativeKeyDown(
+ INativeWindow* window, const platform::gui::NativeKeyEventArgs& args) {
+ CRU_UNUSED(window)
+
+ DispatchEvent(event_names::KeyDown, focus_control_,
+ &controls::Control::KeyDownEvent, nullptr, args.key,
+ args.modifier);
+}
+
+void WindowHost::OnNativeKeyUp(INativeWindow* window,
+ const platform::gui::NativeKeyEventArgs& args) {
+ CRU_UNUSED(window)
+
+ DispatchEvent(event_names::KeyUp, focus_control_,
+ &controls::Control::KeyUpEvent, nullptr, args.key,
+ args.modifier);
+}
+
+void WindowHost::DispatchMouseHoverControlChangeEvent(
+ controls::Control* old_control, controls::Control* new_control,
+ const Point& point, bool no_leave, bool no_enter) {
+ if (new_control != old_control) // if the mouse-hover-on control changed
+ {
+ const auto lowest_common_ancestor =
+ FindLowestCommonAncestor(old_control, new_control);
+ if (!no_leave && old_control != nullptr)
+ DispatchEvent(event_names::MouseLeave, old_control,
+ &controls::Control::MouseLeaveEvent,
+ lowest_common_ancestor); // dispatch mouse leave event.
+ if (!no_enter && new_control != nullptr) {
+ DispatchEvent(event_names::MouseEnter, new_control,
+ &controls::Control::MouseEnterEvent, lowest_common_ancestor,
+ point); // dispatch mouse enter event.
+ }
+ }
+}
+
+void WindowHost::UpdateCursor() {
+ if (native_window_) {
+ const auto capture = GetMouseCaptureControl();
+ native_window_->SetCursor(
+ (capture ? capture : GetMouseHoverControl())->GetInheritedCursor());
+ }
+}
+
+controls::Control* WindowHost::HitTest(const Point& point) {
+ const auto render_object = root_render_object_->HitTest(point);
+ if (render_object) {
+ const auto control = render_object->GetAttachedControl();
+ Ensures(control);
+ return control;
+ }
+ return root_control_;
+}
+} // namespace cru::ui::host
diff --git a/src/ui/render/BorderRenderObject.cpp b/src/ui/render/BorderRenderObject.cpp
index b7e1e709..e2c40f0c 100644
--- a/src/ui/render/BorderRenderObject.cpp
+++ b/src/ui/render/BorderRenderObject.cpp
@@ -2,9 +2,11 @@
#include "../Helper.hpp"
#include "cru/common/Logger.hpp"
-#include "cru/platform/graph/Factory.hpp"
-#include "cru/platform/graph/Geometry.hpp"
-#include "cru/platform/graph/util/Painter.hpp"
+#include "cru/platform/graphics/Factory.hpp"
+#include "cru/platform/graphics/Geometry.hpp"
+#include "cru/platform/graphics/util/Painter.hpp"
+#include "cru/ui/style/ApplyBorderStyleInfo.hpp"
+#include "gsl/gsl_assert"
#include <algorithm>
@@ -16,12 +18,13 @@ BorderRenderObject::BorderRenderObject() {
BorderRenderObject::~BorderRenderObject() {}
-void BorderRenderObject::SetBorderStyle(const BorderStyle& style) {
- border_brush_ = style.border_brush;
- border_thickness_ = style.border_thickness;
- border_radius_ = style.border_radius;
- foreground_brush_ = style.foreground_brush;
- background_brush_ = style.background_brush;
+void BorderRenderObject::ApplyBorderStyle(
+ const style::ApplyBorderStyleInfo& style) {
+ if (style.border_brush) border_brush_ = *style.border_brush;
+ if (style.border_thickness) border_thickness_ = *style.border_thickness;
+ if (style.border_radius) border_radius_ = *style.border_radius;
+ if (style.foreground_brush) foreground_brush_ = *style.foreground_brush;
+ if (style.background_brush) background_brush_ = *style.background_brush;
InvalidateLayout();
}
@@ -51,7 +54,7 @@ RenderObject* BorderRenderObject::HitTest(const Point& point) {
}
}
-void BorderRenderObject::OnDrawCore(platform::graph::IPainter* painter) {
+void BorderRenderObject::OnDrawCore(platform::graphics::IPainter* painter) {
if (background_brush_ != nullptr)
painter->FillGeometry(border_inner_geometry_.get(),
background_brush_.get());
@@ -109,9 +112,10 @@ Size BorderRenderObject::OnMeasureCore(const MeasureRequirement& requirement,
if (!requirement.max.height.IsNotSpecified()) {
const auto max_height = requirement.max.height.GetLengthOrMax();
if (coerced_space_size.height > max_height) {
- log::TagWarn(log_tag,
- u"(Measure) Vertical length of padding, border and margin is "
- u"bigger than required max length.");
+ log::TagWarn(
+ log_tag,
+ u"(Measure) Vertical length of padding, border and margin is "
+ u"bigger than required max length.");
coerced_space_size.height = max_height;
}
content_requirement.max.height = max_height - coerced_space_size.height;
@@ -235,7 +239,7 @@ void BorderRenderObject::RecreateGeometry() {
r.left_bottom - Point{t.left, t.bottom},
r.right_bottom - Point{t.right, t.bottom});
- auto f = [](platform::graph::IGeometryBuilder* builder, const Rect& rect,
+ auto f = [](platform::graphics::IGeometryBuilder* builder, const Rect& rect,
const CornerRadius& corner) {
builder->BeginFigure(Point(rect.left + corner.left_top.x, rect.top));
builder->LineTo(Point(rect.GetRight() - corner.right_top.x, rect.top));
@@ -263,7 +267,7 @@ void BorderRenderObject::RecreateGeometry() {
size.width - margin.GetHorizontalTotal(),
size.height - margin.GetVerticalTotal()};
const auto graph_factory = GetGraphFactory();
- std::unique_ptr<platform::graph::IGeometryBuilder> builder{
+ std::unique_ptr<platform::graphics::IGeometryBuilder> builder{
graph_factory->CreateGeometryBuilder()};
f(builder.get(), outer_rect, outer_radius);
border_outer_geometry_ = builder->Build();
diff --git a/src/ui/render/CanvasRenderObject.cpp b/src/ui/render/CanvasRenderObject.cpp
index 967fdcec..bf1155e1 100644
--- a/src/ui/render/CanvasRenderObject.cpp
+++ b/src/ui/render/CanvasRenderObject.cpp
@@ -10,7 +10,7 @@ RenderObject* CanvasRenderObject::HitTest(const Point& point) {
return padding_rect.IsPointInside(point) ? this : nullptr;
}
-void CanvasRenderObject::OnDrawContent(platform::graph::IPainter* painter) {
+void CanvasRenderObject::OnDrawContent(platform::graphics::IPainter* painter) {
const auto rect = GetContentRect();
CanvasPaintEventArgs args{painter, rect.GetSize()};
paint_event_.Raise(args);
diff --git a/src/ui/render/FlexLayoutRenderObject.cpp b/src/ui/render/FlexLayoutRenderObject.cpp
index ade230b5..b1ef69ee 100644
--- a/src/ui/render/FlexLayoutRenderObject.cpp
+++ b/src/ui/render/FlexLayoutRenderObject.cpp
@@ -1,7 +1,7 @@
#include "cru/ui/render/FlexLayoutRenderObject.hpp"
#include "cru/common/Logger.hpp"
-#include "cru/platform/graph/util/Painter.hpp"
+#include "cru/platform/graphics/util/Painter.hpp"
#include "cru/ui/render/LayoutHelper.hpp"
#include <algorithm>
@@ -10,6 +10,10 @@
namespace cru::ui::render {
+std::u16string_view FlexLayoutRenderObject::GetName() const {
+ return u"FlexLayoutRenderObject";
+}
+
struct tag_horizontal_t {};
struct tag_vertical_t {};
@@ -64,7 +68,7 @@ template <typename TSize>
constexpr TSize CreateTSize(decltype(std::declval<TSize>().width) main,
decltype(std::declval<TSize>().height) cross,
tag_vertical_t) {
- return TSize{main, cross};
+ return TSize{cross, main};
}
enum class FlexLayoutAdjustType { None, Expand, Shrink };
@@ -387,10 +391,11 @@ void FlexLayoutRenderObject::OnLayoutContent(const Rect& content_rect) {
const auto cross_align =
GetChildLayoutDataList()[i].cross_alignment.value_or(
GetItemCrossAlign());
- child->Layout(
- Point{content_rect.top + current_main_offset,
- CalculateAnchorByAlignment(cross_align, content_rect.left,
- content_rect.width, size.width)});
+ child->Layout(Point{
+ CalculateAnchorByAlignment(cross_align, content_rect.left,
+ content_rect.width, size.width),
+ content_rect.top + current_main_offset,
+ });
current_main_offset += size.height;
}
} else {
@@ -402,9 +407,9 @@ void FlexLayoutRenderObject::OnLayoutContent(const Rect& content_rect) {
GetChildLayoutDataList()[i].cross_alignment.value_or(
GetItemCrossAlign());
child->Layout(
- Point{content_rect.GetBottom() - current_main_offset,
- CalculateAnchorByAlignment(cross_align, content_rect.left,
- content_rect.width, size.width)});
+ Point{CalculateAnchorByAlignment(cross_align, content_rect.left,
+ content_rect.width, size.width),
+ content_rect.GetBottom() - current_main_offset});
current_main_offset += size.height;
}
}
diff --git a/src/ui/render/RenderObject.cpp b/src/ui/render/RenderObject.cpp
index 30433868..7cf750cd 100644
--- a/src/ui/render/RenderObject.cpp
+++ b/src/ui/render/RenderObject.cpp
@@ -1,12 +1,21 @@
#include "cru/ui/render/RenderObject.hpp"
#include "cru/common/Logger.hpp"
-#include "cru/platform/graph/util/Painter.hpp"
-#include "cru/ui/UiHost.hpp"
+#include "cru/platform/graphics/util/Painter.hpp"
+#include "cru/ui/DebugFlags.hpp"
+#include "cru/ui/host/WindowHost.hpp"
#include <algorithm>
+#include <string>
+#include <string_view>
+#include <vector>
namespace cru::ui::render {
+void RenderObject::SetAttachedControl(controls::Control* new_control) {
+ control_ = new_control;
+ OnAttachedControlChanged(new_control);
+}
+
void RenderObject::AddChild(RenderObject* render_object, const Index position) {
Expects(child_mode_ != ChildMode::None);
Expects(!(child_mode_ == ChildMode::Single && children_.size() > 0));
@@ -20,7 +29,7 @@ void RenderObject::AddChild(RenderObject* render_object, const Index position) {
children_.insert(children_.cbegin() + position, render_object);
render_object->SetParent(this);
- render_object->SetRenderHostRecursive(GetUiHost());
+ render_object->SetWindowHostRecursive(GetWindowHost());
OnAddChild(render_object, position);
}
@@ -33,10 +42,25 @@ void RenderObject::RemoveChild(const Index position) {
const auto removed_child = *i;
children_.erase(i);
removed_child->SetParent(nullptr);
- removed_child->SetRenderHostRecursive(nullptr);
+ removed_child->SetWindowHostRecursive(nullptr);
OnRemoveChild(removed_child, position);
}
+RenderObject* RenderObject::GetFirstChild() const {
+ const auto& children = GetChildren();
+ if (children.empty()) {
+ return nullptr;
+ } else {
+ return children.front();
+ }
+}
+
+void RenderObject::TraverseDescendants(
+ const std::function<void(RenderObject*)>& action) {
+ action(this);
+ for (auto child : children_) child->TraverseDescendants(action);
+}
+
Point RenderObject::GetTotalOffset() const {
Point result{};
const RenderObject* render_object = this;
@@ -66,18 +90,34 @@ void RenderObject::Measure(const MeasureRequirement& requirement,
MeasureSize merged_preferred_size =
preferred_size.OverrideBy(preferred_size_);
+ if constexpr (cru::ui::debug_flags::layout) {
+ log::Debug(u"{} Measure begins :\nrequirement: {}\npreferred size: {}",
+ this->GetDebugPathInTree(), requirement.ToDebugString(),
+ preferred_size.ToDebugString());
+ }
+
size_ = OnMeasureCore(merged_requirement, merged_preferred_size);
+
+ if constexpr (cru::ui::debug_flags::layout) {
+ log::Debug(u"{} Measure ends :\nresult size: {}",
+ this->GetDebugPathInTree(), size_.ToDebugString());
+ }
+
Ensures(size_.width >= 0);
Ensures(size_.height >= 0);
Ensures(requirement.Satisfy(size_));
}
void RenderObject::Layout(const Point& offset) {
+ if constexpr (cru::ui::debug_flags::layout) {
+ log::Debug(u"{} Layout :\noffset: {}", this->GetDebugPathInTree(),
+ offset.ToDebugString());
+ }
offset_ = offset;
OnLayoutCore();
}
-void RenderObject::Draw(platform::graph::IPainter* painter) {
+void RenderObject::Draw(platform::graphics::IPainter* painter) {
OnDrawCore(painter);
}
@@ -112,29 +152,29 @@ void RenderObject::OnRemoveChild(RenderObject* removed_child, Index position) {
InvalidatePaint();
}
-void RenderObject::DefaultDrawChildren(platform::graph::IPainter* painter) {
+void RenderObject::DefaultDrawChildren(platform::graphics::IPainter* painter) {
for (const auto child : GetChildren()) {
auto offset = child->GetOffset();
- platform::graph::util::WithTransform(
+ platform::graphics::util::WithTransform(
painter, platform::Matrix::Translation(offset.x, offset.y),
[child](auto p) { child->Draw(p); });
}
}
-void RenderObject::DefaultDrawContent(platform::graph::IPainter* painter) {
+void RenderObject::DefaultDrawContent(platform::graphics::IPainter* painter) {
const auto content_rect = GetContentRect();
- platform::graph::util::WithTransform(
+ platform::graphics::util::WithTransform(
painter, Matrix::Translation(content_rect.left, content_rect.top),
[this](auto p) { this->OnDrawContent(p); });
}
-void RenderObject::OnDrawCore(platform::graph::IPainter* painter) {
+void RenderObject::OnDrawCore(platform::graphics::IPainter* painter) {
DefaultDrawContent(painter);
DefaultDrawChildren(painter);
}
-void RenderObject::OnDrawContent(platform::graph::IPainter* painter) {
+void RenderObject::OnDrawContent(platform::graphics::IPainter* painter) {
CRU_UNUSED(painter);
}
@@ -249,24 +289,44 @@ void RenderObject::SetParent(RenderObject* new_parent) {
}
void RenderObject::InvalidateLayout() {
- if (ui_host_ != nullptr) ui_host_->InvalidateLayout();
+ if (window_host_ != nullptr) window_host_->InvalidateLayout();
}
void RenderObject::InvalidatePaint() {
- if (ui_host_ != nullptr) ui_host_->InvalidatePaint();
+ if (window_host_ != nullptr) window_host_->InvalidatePaint();
}
-void RenderObject::NotifyAfterLayoutRecursive(RenderObject* render_object) {
- render_object->OnAfterLayout();
- for (const auto o : render_object->GetChildren()) {
- NotifyAfterLayoutRecursive(o);
+constexpr std::u16string_view kUnamedName(u"UNNAMED");
+
+std::u16string_view RenderObject::GetName() const { return kUnamedName; }
+
+std::u16string RenderObject::GetDebugPathInTree() const {
+ std::vector<std::u16string_view> chain;
+ const RenderObject* parent = this;
+ while (parent != nullptr) {
+ chain.push_back(parent->GetName());
+ parent = parent->GetParent();
}
+
+ std::u16string result(chain.back());
+ for (auto iter = chain.crbegin() + 1; iter != chain.crend(); ++iter) {
+ result += u" -> ";
+ result += *iter;
+ }
+
+ return result;
}
-void RenderObject::SetRenderHostRecursive(UiHost* host) {
- ui_host_ = host;
+void RenderObject::SetWindowHostRecursive(host::WindowHost* host) {
+ if (window_host_ != nullptr) {
+ detach_from_host_event_.Raise(nullptr);
+ }
+ window_host_ = host;
+ if (host != nullptr) {
+ attach_to_host_event_.Raise(nullptr);
+ }
for (const auto child : GetChildren()) {
- child->SetRenderHostRecursive(host);
+ child->SetWindowHostRecursive(host);
}
}
} // namespace cru::ui::render
diff --git a/src/ui/render/ScrollBar.cpp b/src/ui/render/ScrollBar.cpp
new file mode 100644
index 00000000..7f69c1e2
--- /dev/null
+++ b/src/ui/render/ScrollBar.cpp
@@ -0,0 +1,622 @@
+#include "cru/ui/render/ScrollBar.hpp"
+
+#include "../Helper.hpp"
+#include "cru/common/Base.hpp"
+#include "cru/platform/GraphBase.hpp"
+#include "cru/platform/graphics/Factory.hpp"
+#include "cru/platform/graphics/Geometry.hpp"
+#include "cru/platform/graphics/Painter.hpp"
+#include "cru/platform/graphics/util/Painter.hpp"
+#include "cru/platform/gui/Base.hpp"
+#include "cru/platform/gui/Cursor.hpp"
+#include "cru/ui/Base.hpp"
+#include "cru/ui/events/UiEvent.hpp"
+#include "cru/ui/render/ScrollRenderObject.hpp"
+#include "gsl/gsl_assert"
+
+#include <algorithm>
+#include <cassert>
+#include <chrono>
+#include <gsl/pointers>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+
+namespace cru::ui::render {
+using namespace std::chrono_literals;
+constexpr float kScrollBarCollapseThumbWidth = 2;
+constexpr float kScrollBarCollapsedTriggerExpandAreaWidth = 5;
+constexpr float kScrollBarExpandWidth = 10;
+constexpr float kScrollBarArrowHeight = 3.5;
+constexpr auto kScrollBarAutoCollapseDelay = 1500ms;
+
+constexpr std::array<ScrollBarAreaKind, 5> kScrollBarAreaKindList{
+ ScrollBarAreaKind::UpArrow, ScrollBarAreaKind::DownArrow,
+ ScrollBarAreaKind::UpSlot, ScrollBarAreaKind::DownSlot,
+ ScrollBarAreaKind::Thumb};
+
+namespace {
+std::unique_ptr<platform::graphics::IGeometry> CreateScrollBarArrowGeometry() {
+ auto geometry_builder = GetGraphFactory()->CreateGeometryBuilder();
+ geometry_builder->BeginFigure({-kScrollBarArrowHeight / 2, 0});
+ geometry_builder->LineTo({kScrollBarArrowHeight / 2, kScrollBarArrowHeight});
+ geometry_builder->LineTo({kScrollBarArrowHeight / 2, -kScrollBarArrowHeight});
+ geometry_builder->CloseFigure(true);
+ return geometry_builder->Build();
+}
+} // namespace
+
+ScrollBar::ScrollBar(gsl::not_null<ScrollRenderObject*> render_object,
+ Direction direction)
+ : render_object_(render_object), direction_(direction) {
+ // TODO: Use theme resource and delete this.
+
+ auto graphics_factory = GetUiApplication()->GetInstance()->GetGraphFactory();
+
+ collapsed_thumb_brush_ =
+ graphics_factory->CreateSolidColorBrush(colors::gray.WithAlpha(128));
+ expanded_thumb_brush_ = graphics_factory->CreateSolidColorBrush(colors::gray);
+ expanded_slot_brush_ =
+ graphics_factory->CreateSolidColorBrush(colors::seashell);
+ expanded_arrow_brush_ = graphics_factory->CreateSolidColorBrush(colors::gray);
+ expanded_arrow_background_brush_ =
+ graphics_factory->CreateSolidColorBrush(colors::seashell);
+
+ arrow_geometry_ = CreateScrollBarArrowGeometry();
+}
+
+ScrollBar::~ScrollBar() { RestoreCursor(); }
+
+void ScrollBar::SetEnabled(bool value) {
+ if (value == is_enabled_) return;
+ if (!value) {
+ SetExpanded(false);
+ if (move_thumb_start_) {
+ if (const auto control = this->render_object_->GetAttachedControl()) {
+ control->ReleaseMouse();
+ }
+ move_thumb_start_ = std::nullopt;
+ }
+ }
+}
+
+void ScrollBar::SetExpanded(bool value) {
+ if (is_expanded_ == value) return;
+ is_expanded_ = value;
+ render_object_->InvalidatePaint();
+}
+
+void ScrollBar::Draw(platform::graphics::IPainter* painter) {
+ if (is_enabled_) {
+ OnDraw(painter, is_expanded_);
+ }
+}
+
+void ScrollBar::InstallHandlers(controls::Control* control) {
+ event_guard_.Clear();
+ if (control != nullptr) {
+ event_guard_ +=
+ control->MouseDownEvent()->Bubble()->PrependShortCircuitHandler(
+ [control, this](event::MouseButtonEventArgs& event) {
+ if (event.GetButton() == mouse_buttons::left && IsEnabled() &&
+ IsExpanded()) {
+ auto hit_test_result =
+ ExpandedHitTest(event.GetPoint(render_object_));
+ if (!hit_test_result) return false;
+
+ switch (*hit_test_result) {
+ case ScrollBarAreaKind::UpArrow:
+ this->scroll_attempt_event_.Raise(
+ {GetDirection(), ScrollKind::Line, -1});
+ event.SetHandled();
+ return true;
+ case ScrollBarAreaKind::DownArrow:
+ this->scroll_attempt_event_.Raise(
+ {GetDirection(), ScrollKind::Line, 1});
+ event.SetHandled();
+ return true;
+ case ScrollBarAreaKind::UpSlot:
+ this->scroll_attempt_event_.Raise(
+ {GetDirection(), ScrollKind::Page, -1});
+ event.SetHandled();
+ return true;
+ case ScrollBarAreaKind::DownSlot:
+ this->scroll_attempt_event_.Raise(
+ {GetDirection(), ScrollKind::Page, 1});
+ event.SetHandled();
+ return true;
+ case ScrollBarAreaKind::Thumb: {
+ auto thumb_rect =
+ GetExpandedAreaRect(ScrollBarAreaKind::Thumb);
+ assert(thumb_rect);
+
+ if (!control->CaptureMouse()) break;
+ move_thumb_thumb_original_rect_ = *thumb_rect;
+ move_thumb_start_ = event.GetPoint();
+ event.SetHandled();
+ return true;
+ }
+ default:
+ break;
+ }
+ }
+
+ return false;
+ });
+
+ event_guard_ +=
+ control->MouseUpEvent()->Bubble()->PrependShortCircuitHandler(
+ [control, this](event::MouseButtonEventArgs& event) {
+ if (event.GetButton() == mouse_buttons::left &&
+ move_thumb_start_) {
+ move_thumb_start_ = std::nullopt;
+
+ auto hit_test_result =
+ ExpandedHitTest(event.GetPoint(this->render_object_));
+ if (!hit_test_result) {
+ OnMouseLeave();
+ }
+
+ control->ReleaseMouse();
+ event.SetHandled();
+ return true;
+ }
+ return false;
+ });
+
+ event_guard_ +=
+ control->MouseMoveEvent()->Bubble()->PrependShortCircuitHandler(
+ [this](event::MouseEventArgs& event) {
+ if (move_thumb_start_) {
+ auto new_scroll_position = CalculateNewScrollPosition(
+ move_thumb_thumb_original_rect_,
+ event.GetPoint() - *move_thumb_start_);
+
+ this->scroll_attempt_event_.Raise({GetDirection(),
+ ScrollKind::Absolute,
+ new_scroll_position});
+ event.SetHandled();
+ return true;
+ }
+
+ if (IsEnabled()) {
+ if (IsExpanded()) {
+ auto hit_test_result =
+ ExpandedHitTest(event.GetPoint(this->render_object_));
+ if (hit_test_result) {
+ SetCursor();
+ StopAutoCollapseTimer();
+ } else {
+ OnMouseLeave();
+ }
+ } else {
+ auto trigger_expand_area =
+ GetCollapsedTriggerExpandAreaRect();
+ if (trigger_expand_area &&
+ trigger_expand_area->IsPointInside(
+ event.GetPoint(this->render_object_))) {
+ SetExpanded(true);
+ SetCursor();
+ event.SetHandled();
+ return true;
+ }
+ }
+ }
+
+ return false;
+ });
+
+ event_guard_ +=
+ control->MouseLeaveEvent()->Bubble()->PrependShortCircuitHandler(
+ [this](event::MouseEventArgs&) {
+ if (IsExpanded() && !move_thumb_start_) {
+ OnMouseLeave();
+ }
+ return false;
+ });
+ }
+}
+
+gsl::not_null<std::shared_ptr<platform::graphics::IBrush>>
+ScrollBar::GetCollapsedThumbBrush() const {
+ // TODO: Read theme resource.
+ return collapsed_thumb_brush_;
+}
+
+gsl::not_null<std::shared_ptr<platform::graphics::IBrush>>
+ScrollBar::GetExpandedThumbBrush() const {
+ // TODO: Read theme resource.
+ return expanded_thumb_brush_;
+}
+
+gsl::not_null<std::shared_ptr<platform::graphics::IBrush>>
+ScrollBar::GetExpandedSlotBrush() const {
+ // TODO: Read theme resource.
+ return expanded_slot_brush_;
+}
+
+gsl::not_null<std::shared_ptr<platform::graphics::IBrush>>
+ScrollBar::GetExpandedArrowBrush() const {
+ // TODO: Read theme resource.
+ return expanded_arrow_brush_;
+}
+
+gsl::not_null<std::shared_ptr<platform::graphics::IBrush>>
+ScrollBar::GetExpandedArrowBackgroundBrush() const {
+ // TODO: Read theme resource.
+ return expanded_arrow_background_brush_;
+}
+
+void ScrollBar::OnDraw(platform::graphics::IPainter* painter,
+ bool is_expanded) {
+ if (is_expanded) {
+ auto thumb_rect = GetExpandedAreaRect(ScrollBarAreaKind::Thumb);
+ if (thumb_rect)
+ painter->FillRectangle(*thumb_rect, GetExpandedThumbBrush().get().get());
+
+ auto slot_brush = GetExpandedSlotBrush().get().get();
+
+ auto up_slot_rect = GetExpandedAreaRect(ScrollBarAreaKind::UpSlot);
+ if (up_slot_rect) painter->FillRectangle(*up_slot_rect, slot_brush);
+
+ auto down_slot_rect = GetExpandedAreaRect(ScrollBarAreaKind::DownSlot);
+ if (down_slot_rect) painter->FillRectangle(*down_slot_rect, slot_brush);
+
+ auto up_arrow = GetExpandedAreaRect(ScrollBarAreaKind::UpArrow);
+ if (up_arrow) this->DrawUpArrow(painter, *up_arrow);
+
+ auto down_arrow = GetExpandedAreaRect(ScrollBarAreaKind::DownArrow);
+ if (down_arrow) this->DrawDownArrow(painter, *down_arrow);
+ } else {
+ auto optional_rect = GetCollapsedThumbRect();
+ if (optional_rect) {
+ painter->FillRectangle(*optional_rect,
+ GetCollapsedThumbBrush().get().get());
+ }
+ }
+}
+
+void ScrollBar::SetCursor() {
+ if (!old_cursor_) {
+ if (const auto control = render_object_->GetAttachedControl()) {
+ old_cursor_ = control->GetCursor();
+ control->SetCursor(
+ GetUiApplication()->GetCursorManager()->GetSystemCursor(
+ platform::gui::SystemCursorType::Arrow));
+ }
+ }
+}
+
+void ScrollBar::RestoreCursor() {
+ if (old_cursor_) {
+ if (const auto control = render_object_->GetAttachedControl()) {
+ control->SetCursor(*old_cursor_);
+ }
+ old_cursor_ = std::nullopt;
+ }
+}
+
+void ScrollBar::BeginAutoCollapseTimer() {
+ if (!auto_collapse_timer_canceler_ && IsExpanded()) {
+ auto_collapse_timer_canceler_ = GetUiApplication()->SetTimeout(
+ kScrollBarAutoCollapseDelay, [this] { this->SetExpanded(false); });
+ }
+}
+
+void ScrollBar::StopAutoCollapseTimer() {
+ auto_collapse_timer_canceler_.Reset();
+}
+
+void ScrollBar::OnMouseLeave() {
+ RestoreCursor();
+ BeginAutoCollapseTimer();
+}
+
+std::optional<ScrollBarAreaKind> ScrollBar::ExpandedHitTest(
+ const Point& point) {
+ for (auto kind : kScrollBarAreaKindList) {
+ auto rect = this->GetExpandedAreaRect(kind);
+ if (rect) {
+ if (rect->IsPointInside(point)) return kind;
+ }
+ }
+ return std::nullopt;
+}
+
+HorizontalScrollBar::HorizontalScrollBar(
+ gsl::not_null<ScrollRenderObject*> render_object)
+ : ScrollBar(render_object, Direction::Horizontal) {}
+
+void HorizontalScrollBar::DrawUpArrow(platform::graphics::IPainter* painter,
+ const Rect& area) {
+ painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get());
+
+ platform::graphics::util::WithTransform(
+ painter, Matrix::Translation(area.GetCenter()),
+ [this](platform::graphics::IPainter* painter) {
+ painter->FillGeometry(arrow_geometry_.get(),
+ GetExpandedArrowBrush().get().get());
+ });
+}
+
+void HorizontalScrollBar::DrawDownArrow(platform::graphics::IPainter* painter,
+ const Rect& area) {
+ painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get());
+
+ platform::graphics::util::WithTransform(
+ painter, Matrix::Rotation(180) * Matrix::Translation(area.GetCenter()),
+ [this](platform::graphics::IPainter* painter) {
+ painter->FillGeometry(arrow_geometry_.get(),
+ GetExpandedArrowBrush().get().get());
+ });
+}
+
+bool HorizontalScrollBar::IsShowBar() {
+ const auto child = render_object_->GetFirstChild();
+ if (child == nullptr) return false;
+
+ const auto view_rect = render_object_->GetViewRect();
+ const auto child_size = child->GetSize();
+
+ if (view_rect.width >= child_size.width) return false;
+
+ return true;
+}
+
+std::optional<Rect> HorizontalScrollBar::GetExpandedAreaRect(
+ ScrollBarAreaKind area_kind) {
+ auto show = IsShowBar();
+ if (!show) return std::nullopt;
+
+ const auto padding_rect = render_object_->GetPaddingRect();
+
+ const auto child = render_object_->GetFirstChild();
+
+ const auto view_rect = render_object_->GetViewRect();
+ const auto child_size = child->GetSize();
+
+ const float start_percentage = view_rect.left / child_size.width;
+ const float length_percentage = view_rect.width / child_size.width;
+ const float end_percentage = start_percentage + length_percentage;
+
+ const float top = padding_rect.GetBottom() - kScrollBarExpandWidth;
+ const float height = kScrollBarExpandWidth;
+
+ // Without arrow.
+ const float bar_area_length = padding_rect.width - 3 * kScrollBarExpandWidth;
+ const float bar_area_start = padding_rect.left + kScrollBarExpandWidth;
+
+ switch (area_kind) {
+ case ScrollBarAreaKind::UpArrow:
+ return Rect{padding_rect.left, top, kScrollBarExpandWidth, height};
+ case ScrollBarAreaKind::DownArrow:
+ return Rect{padding_rect.GetRight() - 2 * kScrollBarExpandWidth, top,
+ kScrollBarExpandWidth, height};
+ case ScrollBarAreaKind::UpSlot:
+ return Rect{bar_area_start, top, bar_area_length * start_percentage,
+ height};
+ case ScrollBarAreaKind::DownSlot:
+ return Rect{bar_area_start + bar_area_length * end_percentage, top,
+ bar_area_length * (1 - end_percentage), height};
+ case ScrollBarAreaKind::Thumb:
+ return Rect{bar_area_start + bar_area_length * start_percentage, top,
+ bar_area_length * length_percentage, height};
+ default:
+ throw std::invalid_argument("Unsupported scroll area kind.");
+ }
+}
+
+std::optional<Rect> HorizontalScrollBar::GetCollapsedTriggerExpandAreaRect() {
+ auto show = IsShowBar();
+ if (!show) return std::nullopt;
+
+ const auto padding_rect = render_object_->GetPaddingRect();
+
+ return Rect{
+ padding_rect.left,
+ padding_rect.GetBottom() - kScrollBarCollapsedTriggerExpandAreaWidth,
+ padding_rect.width, kScrollBarCollapseThumbWidth};
+}
+
+std::optional<Rect> HorizontalScrollBar::GetCollapsedThumbRect() {
+ auto show = IsShowBar();
+ if (!show) return std::nullopt;
+
+ const auto child = render_object_->GetFirstChild();
+
+ const auto view_rect = render_object_->GetViewRect();
+ const auto child_size = child->GetSize();
+
+ const float start_percentage = view_rect.left / child_size.width;
+ const float length_percentage = view_rect.width / child_size.width;
+ // const float end_percentage = start_percentage + length_percentage;
+
+ const auto padding_rect = render_object_->GetPaddingRect();
+
+ return Rect{padding_rect.left + padding_rect.width * start_percentage,
+ padding_rect.GetBottom() - kScrollBarCollapseThumbWidth,
+ padding_rect.width * length_percentage,
+ kScrollBarCollapseThumbWidth};
+}
+
+float HorizontalScrollBar::CalculateNewScrollPosition(
+ const Rect& thumb_original_rect, const Point& mouse_offset) {
+ auto new_thumb_start = thumb_original_rect.left + mouse_offset.x;
+
+ const auto padding_rect = render_object_->GetPaddingRect();
+
+ auto scroll_area_start = padding_rect.left + kScrollBarExpandWidth;
+ auto scroll_area_end = padding_rect.GetRight() - 2 * kScrollBarExpandWidth;
+
+ auto thumb_head_end = scroll_area_end - thumb_original_rect.width;
+
+ const auto child = render_object_->GetFirstChild();
+ const auto child_size = child->GetSize();
+
+ new_thumb_start =
+ std::clamp(new_thumb_start, scroll_area_start, thumb_head_end);
+
+ auto offset = (new_thumb_start - scroll_area_start) /
+ (scroll_area_end - scroll_area_start) * child_size.width;
+
+ return offset;
+}
+
+VerticalScrollBar::VerticalScrollBar(
+ gsl::not_null<ScrollRenderObject*> render_object)
+ : ScrollBar(render_object, Direction::Vertical) {}
+
+void VerticalScrollBar::DrawUpArrow(platform::graphics::IPainter* painter,
+ const Rect& area) {
+ painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get());
+
+ platform::graphics::util::WithTransform(
+ painter, Matrix::Rotation(90) * Matrix::Translation(area.GetCenter()),
+ [this](platform::graphics::IPainter* painter) {
+ painter->FillGeometry(arrow_geometry_.get(),
+ GetExpandedArrowBrush().get().get());
+ });
+}
+
+void VerticalScrollBar::DrawDownArrow(platform::graphics::IPainter* painter,
+ const Rect& area) {
+ painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get());
+
+ platform::graphics::util::WithTransform(
+ painter, Matrix::Rotation(270) * Matrix::Translation(area.GetCenter()),
+ [this](platform::graphics::IPainter* painter) {
+ painter->FillGeometry(arrow_geometry_.get(),
+ GetExpandedArrowBrush().get().get());
+ });
+}
+
+bool VerticalScrollBar::IsShowBar() {
+ const auto child = render_object_->GetFirstChild();
+ if (child == nullptr) return false;
+
+ const auto view_rect = render_object_->GetViewRect();
+ const auto child_size = child->GetSize();
+
+ if (view_rect.height >= child_size.height) return false;
+
+ return true;
+}
+
+std::optional<Rect> VerticalScrollBar::GetExpandedAreaRect(
+ ScrollBarAreaKind area_kind) {
+ auto show = IsShowBar();
+ if (!show) return std::nullopt;
+
+ const auto padding_rect = render_object_->GetPaddingRect();
+
+ const auto child = render_object_->GetFirstChild();
+
+ const auto view_rect = render_object_->GetViewRect();
+ const auto child_size = child->GetSize();
+
+ const float start_percentage = view_rect.top / child_size.height;
+ const float length_percentage = view_rect.height / child_size.height;
+ const float end_percentage = start_percentage + length_percentage;
+
+ const float left = padding_rect.GetRight() - kScrollBarExpandWidth;
+ const float width = kScrollBarExpandWidth;
+
+ // Without arrow.
+ const float bar_area_length = padding_rect.height - 3 * kScrollBarExpandWidth;
+ const float bar_area_start = padding_rect.top + kScrollBarExpandWidth;
+
+ switch (area_kind) {
+ case ScrollBarAreaKind::UpArrow:
+ return Rect{left, padding_rect.top, width, kScrollBarExpandWidth};
+ case ScrollBarAreaKind::DownArrow:
+ return Rect{left, padding_rect.GetBottom() - 2 * kScrollBarExpandWidth,
+ width, kScrollBarExpandWidth};
+ case ScrollBarAreaKind::UpSlot:
+ return Rect{left, bar_area_start, width,
+ bar_area_length * start_percentage};
+ case ScrollBarAreaKind::DownSlot:
+ return Rect{left, bar_area_start + bar_area_length * end_percentage,
+ width, bar_area_length * (1 - end_percentage)};
+ case ScrollBarAreaKind::Thumb:
+ return Rect{left, bar_area_start + bar_area_length * start_percentage,
+ width, bar_area_length * length_percentage};
+ default:
+ throw std::invalid_argument("Unsupported scroll area kind.");
+ }
+}
+
+std::optional<Rect> VerticalScrollBar::GetCollapsedTriggerExpandAreaRect() {
+ auto show = IsShowBar();
+ if (!show) return std::nullopt;
+
+ const auto padding_rect = render_object_->GetPaddingRect();
+
+ return Rect{
+ padding_rect.GetRight() - kScrollBarCollapsedTriggerExpandAreaWidth,
+ padding_rect.top, kScrollBarCollapseThumbWidth, padding_rect.height};
+}
+
+std::optional<Rect> VerticalScrollBar::GetCollapsedThumbRect() {
+ const auto child = render_object_->GetFirstChild();
+ if (child == nullptr) return std::nullopt;
+
+ const auto view_rect = render_object_->GetViewRect();
+ const auto padding_rect = render_object_->GetPaddingRect();
+ const auto child_size = child->GetSize();
+
+ if (view_rect.height >= child_size.height) return std::nullopt;
+
+ const float start_percentage = view_rect.top / child_size.height;
+ const float length_percentage = view_rect.height / child_size.height;
+ // const float end_percentage = start_percentage + length_percentage;
+
+ return Rect{padding_rect.GetRight() - kScrollBarCollapseThumbWidth,
+ padding_rect.top + padding_rect.height * start_percentage,
+ kScrollBarCollapseThumbWidth,
+ padding_rect.height * length_percentage};
+}
+
+float VerticalScrollBar::CalculateNewScrollPosition(
+ const Rect& thumb_original_rect, const Point& mouse_offset) {
+ auto new_thumb_start = thumb_original_rect.top + mouse_offset.y;
+
+ const auto padding_rect = render_object_->GetPaddingRect();
+
+ auto scroll_area_start = padding_rect.top + kScrollBarExpandWidth;
+ auto scroll_area_end = padding_rect.GetBottom() - 2 * kScrollBarExpandWidth;
+
+ auto thumb_head_end = scroll_area_end - thumb_original_rect.height;
+
+ const auto child = render_object_->GetFirstChild();
+ const auto child_size = child->GetSize();
+
+ new_thumb_start =
+ std::clamp(new_thumb_start, scroll_area_start, thumb_head_end);
+
+ auto offset = (new_thumb_start - scroll_area_start) /
+ (scroll_area_end - scroll_area_start) * child_size.width;
+
+ return offset;
+}
+
+ScrollBarDelegate::ScrollBarDelegate(
+ gsl::not_null<ScrollRenderObject*> render_object)
+ : render_object_(render_object),
+ horizontal_bar_(render_object),
+ vertical_bar_(render_object) {
+ horizontal_bar_.ScrollAttemptEvent()->AddHandler(
+ [this](auto scroll) { this->scroll_attempt_event_.Raise(scroll); });
+ vertical_bar_.ScrollAttemptEvent()->AddHandler(
+ [this](auto scroll) { this->scroll_attempt_event_.Raise(scroll); });
+}
+
+void ScrollBarDelegate::DrawScrollBar(platform::graphics::IPainter* painter) {
+ horizontal_bar_.Draw(painter);
+ vertical_bar_.Draw(painter);
+}
+
+void ScrollBarDelegate::InstallHandlers(controls::Control* control) {
+ horizontal_bar_.InstallHandlers(control);
+ vertical_bar_.InstallHandlers(control);
+}
+} // namespace cru::ui::render
diff --git a/src/ui/render/ScrollRenderObject.cpp b/src/ui/render/ScrollRenderObject.cpp
index 08ce744b..fd5143ff 100644
--- a/src/ui/render/ScrollRenderObject.cpp
+++ b/src/ui/render/ScrollRenderObject.cpp
@@ -1,11 +1,18 @@
#include "cru/ui/render/ScrollRenderObject.hpp"
-#include "cru/platform/graph/Painter.hpp"
-#include "cru/platform/graph/util/Painter.hpp"
+#include "cru/platform/graphics/Painter.hpp"
+#include "cru/platform/graphics/util/Painter.hpp"
+#include "cru/ui/Base.hpp"
+#include "cru/ui/controls/Control.hpp"
+#include "cru/ui/render/ScrollBar.hpp"
#include <algorithm>
+#include <memory>
+#include <optional>
namespace cru::ui::render {
+constexpr float kLineHeight = 16;
+
namespace {
// This method assumes margin offset is already considered.
// It promises that it won't return negetive value.
@@ -24,13 +31,46 @@ Point CoerceScroll(const Point& scroll_offset, const Size& content_size,
n = max;
};
- coerce(result.x, scroll_offset.x);
- coerce(result.y, scroll_offset.y);
+ coerce(result.x, max_scroll.x);
+ coerce(result.y, max_scroll.y);
return result;
}
} // namespace
+ScrollRenderObject::ScrollRenderObject() : RenderObject(ChildMode::Single) {
+ scroll_bar_delegate_ = std::make_unique<ScrollBarDelegate>(this);
+ scroll_bar_delegate_->ScrollAttemptEvent()->AddHandler(
+ [this](const struct Scroll& scroll) { this->Scroll(scroll); });
+}
+
+void ScrollRenderObject::Scroll(const struct Scroll& scroll) {
+ auto direction = scroll.direction;
+
+ switch (scroll.kind) {
+ case ScrollKind::Absolute:
+ SetScrollOffset(direction, scroll.value);
+ break;
+ case ScrollKind::Relative:
+ SetScrollOffset(direction,
+ GetScrollOffset(scroll.direction) + scroll.value);
+ break;
+ case ScrollKind::Page:
+ SetScrollOffset(direction, GetScrollOffset(direction) +
+ (direction == Direction::Horizontal
+ ? GetViewRect().width
+ : GetViewRect().height) *
+ scroll.value);
+ break;
+ case ScrollKind::Line:
+ SetScrollOffset(direction,
+ GetScrollOffset(direction) + kLineHeight * scroll.value);
+ break;
+ default:
+ break;
+ }
+}
+
RenderObject* ScrollRenderObject::HitTest(const Point& point) {
if (const auto child = GetSingleChild()) {
const auto offset = child->GetOffset();
@@ -42,16 +82,17 @@ RenderObject* ScrollRenderObject::HitTest(const Point& point) {
return rect.IsPointInside(point) ? this : nullptr;
} // namespace cru::ui::render
-void ScrollRenderObject::OnDrawCore(platform::graph::IPainter* painter) {
+void ScrollRenderObject::OnDrawCore(platform::graphics::IPainter* painter) {
DefaultDrawContent(painter);
if (const auto child = GetSingleChild()) {
- painter->PushLayer(this->GetPaddingRect());
+ painter->PushLayer(this->GetContentRect());
const auto offset = child->GetOffset();
- platform::graph::util::WithTransform(
+ platform::graphics::util::WithTransform(
painter, Matrix::Translation(offset.x, offset.y),
- [child](platform::graph::IPainter* p) { child->Draw(p); });
+ [child](platform::graphics::IPainter* p) { child->Draw(p); });
painter->PopLayer();
}
+ scroll_bar_delegate_->DrawScrollBar(painter);
}
Point ScrollRenderObject::GetScrollOffset() {
@@ -138,8 +179,15 @@ Size ScrollRenderObject::OnMeasureContent(const MeasureRequirement& requirement,
void ScrollRenderObject::OnLayoutContent(const Rect& content_rect) {
if (const auto child = GetSingleChild()) {
- const auto child_size = child->GetSize();
child->Layout(content_rect.GetLeftTop() - GetScrollOffset());
}
}
+
+void ScrollRenderObject::OnAttachedControlChanged(controls::Control* control) {
+ if (control) {
+ scroll_bar_delegate_->InstallHandlers(control);
+ } else {
+ scroll_bar_delegate_->UninstallHandlers();
+ }
+}
} // namespace cru::ui::render
diff --git a/src/ui/render/TextRenderObject.cpp b/src/ui/render/TextRenderObject.cpp
index cecbe1f3..06092d52 100644
--- a/src/ui/render/TextRenderObject.cpp
+++ b/src/ui/render/TextRenderObject.cpp
@@ -2,19 +2,19 @@
#include "../Helper.hpp"
#include "cru/common/Logger.hpp"
-#include "cru/platform/graph/Factory.hpp"
-#include "cru/platform/graph/TextLayout.hpp"
-#include "cru/platform/graph/util/Painter.hpp"
+#include "cru/platform/graphics/Factory.hpp"
+#include "cru/platform/graphics/TextLayout.hpp"
+#include "cru/platform/graphics/util/Painter.hpp"
#include <algorithm>
#include <limits>
namespace cru::ui::render {
TextRenderObject::TextRenderObject(
- std::shared_ptr<platform::graph::IBrush> brush,
- std::shared_ptr<platform::graph::IFont> font,
- std::shared_ptr<platform::graph::IBrush> selection_brush,
- std::shared_ptr<platform::graph::IBrush> caret_brush) {
+ std::shared_ptr<platform::graphics::IBrush> brush,
+ std::shared_ptr<platform::graphics::IFont> font,
+ std::shared_ptr<platform::graphics::IBrush> selection_brush,
+ std::shared_ptr<platform::graphics::IBrush> caret_brush) {
Expects(brush);
Expects(font);
Expects(selection_brush);
@@ -43,20 +43,22 @@ std::u16string_view TextRenderObject::GetTextView() const {
void TextRenderObject::SetText(std::u16string new_text) {
text_layout_->SetText(std::move(new_text));
+ InvalidateLayout();
}
void TextRenderObject::SetBrush(
- std::shared_ptr<platform::graph::IBrush> new_brush) {
+ std::shared_ptr<platform::graphics::IBrush> new_brush) {
Expects(new_brush);
new_brush.swap(brush_);
InvalidatePaint();
}
-std::shared_ptr<platform::graph::IFont> TextRenderObject::GetFont() const {
+std::shared_ptr<platform::graphics::IFont> TextRenderObject::GetFont() const {
return text_layout_->GetFont();
}
-void TextRenderObject::SetFont(std::shared_ptr<platform::graph::IFont> font) {
+void TextRenderObject::SetFont(
+ std::shared_ptr<platform::graphics::IFont> font) {
Expects(font);
text_layout_->SetFont(std::move(font));
}
@@ -69,7 +71,7 @@ Point TextRenderObject::TextSinglePoint(gsl::index position, bool trailing) {
return text_layout_->TextSinglePoint(position, trailing);
}
-platform::graph::TextHitTestResult TextRenderObject::TextHitTest(
+platform::graphics::TextHitTestResult TextRenderObject::TextHitTest(
const Point& point) {
return text_layout_->HitTest(point);
}
@@ -80,7 +82,7 @@ void TextRenderObject::SetSelectionRange(std::optional<TextRange> new_range) {
}
void TextRenderObject::SetSelectionBrush(
- std::shared_ptr<platform::graph::IBrush> new_brush) {
+ std::shared_ptr<platform::graphics::IBrush> new_brush) {
Expects(new_brush);
new_brush.swap(selection_brush_);
if (selection_range_ && selection_range_->count) {
@@ -105,7 +107,7 @@ void TextRenderObject::SetCaretPosition(gsl::index position) {
}
void TextRenderObject::GetCaretBrush(
- std::shared_ptr<platform::graph::IBrush> brush) {
+ std::shared_ptr<platform::graphics::IBrush> brush) {
Expects(brush);
brush.swap(caret_brush_);
if (draw_caret_) {
@@ -153,12 +155,18 @@ Rect TextRenderObject::GetCaretRect() {
return rect;
}
+void TextRenderObject::SetMeasureIncludingTrailingSpace(bool including) {
+ if (is_measure_including_trailing_space_ == including) return;
+ is_measure_including_trailing_space_ = including;
+ InvalidateLayout();
+}
+
RenderObject* TextRenderObject::HitTest(const Point& point) {
const auto padding_rect = GetPaddingRect();
return padding_rect.IsPointInside(point) ? this : nullptr;
}
-void TextRenderObject::OnDrawContent(platform::graph::IPainter* painter) {
+void TextRenderObject::OnDrawContent(platform::graphics::IPainter* painter) {
if (this->selection_range_.has_value()) {
const auto&& rects =
text_layout_->TextRangeRect(this->selection_range_.value());
@@ -184,7 +192,9 @@ Size TextRenderObject::OnMeasureContent(const MeasureRequirement& requirement,
text_layout_->SetMaxWidth(measure_width);
text_layout_->SetMaxHeight(std::numeric_limits<float>::max());
- const auto text_size = text_layout_->GetTextBounds().GetSize();
+ const auto text_size =
+ text_layout_->GetTextBounds(is_measure_including_trailing_space_)
+ .GetSize();
auto result = text_size;
if (requirement.max.width.IsSpecified() &&
diff --git a/src/ui/render/WindowRenderObject.cpp b/src/ui/render/WindowRenderObject.cpp
deleted file mode 100644
index 4adf559e..00000000
--- a/src/ui/render/WindowRenderObject.cpp
+++ /dev/null
@@ -1,40 +0,0 @@
-#include "cru/ui/render/WindowRenderObject.hpp"
-
-#include "../Helper.hpp"
-#include "cru/platform/graph/util/Painter.hpp"
-#include "cru/ui/UiHost.hpp"
-
-namespace cru::ui::render {
-WindowRenderObject::WindowRenderObject(UiHost* host) {
- SetChildMode(ChildMode::Single);
- ui_host_ = host;
- after_layout_event_guard_.Reset(host->AfterLayoutEvent()->AddHandler(
- [this](auto) { NotifyAfterLayoutRecursive(this); }));
-}
-
-RenderObject* WindowRenderObject::HitTest(const Point& point) {
- if (const auto child = GetChild()) {
- auto offset = child->GetOffset();
- Point p{point.x - offset.x, point.y - offset.y};
- const auto result = child->HitTest(p);
- if (result != nullptr) {
- return result;
- }
- }
- return Rect{Point{}, GetSize()}.IsPointInside(point) ? this : nullptr;
-}
-
-Size WindowRenderObject::OnMeasureContent(const MeasureRequirement& requirement,
- const MeasureSize& preferred_size) {
- if (const auto child = GetChild()) {
- child->Measure(requirement, preferred_size);
- return child->GetSize();
- } else {
- return Size{};
- }
-}
-
-void WindowRenderObject::OnLayoutContent(const Rect& content_rect) {
- if (const auto child = GetChild()) child->Layout(content_rect.GetLeftTop());
-}
-} // namespace cru::ui::render
diff --git a/src/ui/style/Condition.cpp b/src/ui/style/Condition.cpp
new file mode 100644
index 00000000..f4866c04
--- /dev/null
+++ b/src/ui/style/Condition.cpp
@@ -0,0 +1,84 @@
+#include "cru/ui/style/Condition.hpp"
+#include <memory>
+
+#include "cru/common/ClonablePtr.hpp"
+#include "cru/common/Event.hpp"
+#include "cru/ui/controls/Control.hpp"
+#include "cru/ui/controls/IClickableControl.hpp"
+#include "cru/ui/helper/ClickDetector.hpp"
+
+namespace cru::ui::style {
+CompoundCondition::CompoundCondition(
+ std::vector<ClonablePtr<Condition>> conditions)
+ : conditions_(std::move(conditions)) {}
+
+std::vector<IBaseEvent*> CompoundCondition::ChangeOn(
+ controls::Control* control) const {
+ std::vector<IBaseEvent*> result;
+
+ for (auto condition : conditions_) {
+ for (auto e : condition->ChangeOn(control)) {
+ result.push_back(e);
+ }
+ }
+
+ return result;
+}
+
+bool AndCondition::Judge(controls::Control* control) const {
+ for (auto condition : conditions_) {
+ if (!condition->Judge(control)) return false;
+ }
+ return true;
+}
+
+bool OrCondition::Judge(controls::Control* control) const {
+ for (auto condition : conditions_) {
+ if (condition->Judge(control)) return true;
+ }
+ return false;
+}
+
+FocusCondition::FocusCondition(bool has_focus) : has_focus_(has_focus) {}
+
+std::vector<IBaseEvent*> FocusCondition::ChangeOn(
+ controls::Control* control) const {
+ return {control->GainFocusEvent()->Direct(),
+ control->LoseFocusEvent()->Direct()};
+}
+
+bool FocusCondition::Judge(controls::Control* control) const {
+ return control->HasFocus() == has_focus_;
+}
+
+std::vector<IBaseEvent*> HoverCondition::ChangeOn(
+ controls::Control* control) const {
+ return {control->MouseEnterEvent()->Direct(),
+ control->MouseLeaveEvent()->Direct()};
+}
+
+bool HoverCondition::Judge(controls::Control* control) const {
+ return control->IsMouseOver() == hover_;
+}
+
+ClickStateCondition::ClickStateCondition(helper::ClickState click_state)
+ : click_state_(click_state) {}
+
+std::vector<IBaseEvent*> ClickStateCondition::ChangeOn(
+ controls::Control* control) const {
+ auto clickable_control = dynamic_cast<controls::IClickableControl*>(control);
+ if (clickable_control) {
+ return {clickable_control->ClickStateChangeEvent()};
+ } else {
+ return {};
+ }
+}
+
+bool ClickStateCondition::Judge(controls::Control* control) const {
+ auto clickable_control = dynamic_cast<controls::IClickableControl*>(control);
+ if (clickable_control) {
+ return clickable_control->GetClickState() == click_state_;
+ }
+ return false;
+}
+} // namespace cru::ui::style
diff --git a/src/ui/style/StyleRule.cpp b/src/ui/style/StyleRule.cpp
new file mode 100644
index 00000000..1a72a970
--- /dev/null
+++ b/src/ui/style/StyleRule.cpp
@@ -0,0 +1,17 @@
+#include "cru/ui/style/StyleRule.hpp"
+
+namespace cru::ui::style {
+StyleRule::StyleRule(ClonablePtr<Condition> condition,
+ ClonablePtr<Styler> styler, std::u16string name)
+ : condition_(std::move(condition)),
+ styler_(std::move(styler)),
+ name_(std::move(name)) {}
+
+bool StyleRule::CheckAndApply(controls::Control *control) const {
+ auto active = condition_->Judge(control);
+ if (active) {
+ styler_->Apply(control);
+ }
+ return active;
+}
+} // namespace cru::ui::style
diff --git a/src/ui/style/StyleRuleSet.cpp b/src/ui/style/StyleRuleSet.cpp
new file mode 100644
index 00000000..537d1956
--- /dev/null
+++ b/src/ui/style/StyleRuleSet.cpp
@@ -0,0 +1,97 @@
+#include "cru/ui/style/StyleRuleSet.hpp"
+#include "cru/common/Event.hpp"
+#include "cru/ui/controls/Control.hpp"
+#include "gsl/gsl_assert"
+
+#include <unordered_set>
+
+namespace cru::ui::style {
+StyleRuleSet::StyleRuleSet(StyleRuleSet* parent) { SetParent(parent); }
+
+void StyleRuleSet::SetParent(StyleRuleSet* parent) {
+ if (parent == parent_) return;
+ parent_change_event_guard_.Reset();
+ parent_ = parent;
+ if (parent != nullptr) {
+ parent_change_event_guard_.Reset(parent->ChangeEvent()->AddSpyOnlyHandler(
+ [this] { this->RaiseChangeEvent(); }));
+ }
+ RaiseChangeEvent();
+}
+
+void StyleRuleSet::AddStyleRule(StyleRule rule, gsl::index index) {
+ Expects(index >= 0 && index <= GetSize());
+
+ rules_.insert(rules_.cbegin() + index, std::move(rule));
+
+ RaiseChangeEvent();
+}
+
+void StyleRuleSet::RemoveStyleRule(gsl::index index, gsl::index count) {
+ Expects(index >= 0);
+ Expects(count >= 0 && index + count <= GetSize());
+
+ rules_.erase(rules_.cbegin() + index, rules_.cbegin() + index + count);
+
+ RaiseChangeEvent();
+}
+
+void StyleRuleSet::Set(const StyleRuleSet& other, bool set_parent) {
+ rules_ = other.rules_;
+ if (set_parent) parent_ = other.parent_;
+
+ RaiseChangeEvent();
+}
+
+StyleRuleSetBind::StyleRuleSetBind(controls::Control* control,
+ StyleRuleSet* ruleset)
+ : control_(control), ruleset_(ruleset) {
+ Expects(control);
+ Expects(ruleset);
+
+ ruleset->ChangeEvent()->AddSpyOnlyHandler([this] {
+ UpdateRuleSetChainCache();
+ UpdateChangeListener();
+ UpdateStyle();
+ });
+}
+
+void StyleRuleSetBind::UpdateRuleSetChainCache() {
+ ruleset_chain_cache_.clear();
+ auto parent = ruleset_;
+ while (parent != nullptr) {
+ ruleset_chain_cache_.push_back(parent);
+ parent = parent->GetParent();
+ }
+}
+
+void StyleRuleSetBind::UpdateChangeListener() {
+ guard_.Clear();
+
+ std::unordered_set<IBaseEvent*> events;
+
+ // ruleset order does not matter
+ for (auto ruleset : ruleset_chain_cache_) {
+ for (const auto& rule : ruleset->GetRules()) {
+ auto e = rule.GetCondition()->ChangeOn(control_);
+ events.insert(e.cbegin(), e.cend());
+ }
+ }
+
+ for (auto e : events) {
+ guard_ += e->AddSpyOnlyHandler([this] { this->UpdateStyle(); });
+ }
+}
+
+void StyleRuleSetBind::UpdateStyle() {
+ // cache is parent last, but when calculate style, parent first, so iterate
+ // reverse.
+ for (auto iter = ruleset_chain_cache_.crbegin();
+ iter != ruleset_chain_cache_.crend(); ++iter) {
+ for (const auto& rule : (*iter)->GetRules())
+ if (rule.GetCondition()->Judge(control_)) {
+ rule.GetStyler()->Apply(control_);
+ }
+ }
+}
+} // namespace cru::ui::style
diff --git a/src/ui/style/Styler.cpp b/src/ui/style/Styler.cpp
new file mode 100644
index 00000000..da3a2247
--- /dev/null
+++ b/src/ui/style/Styler.cpp
@@ -0,0 +1,29 @@
+#include "cru/ui/style/Styler.hpp"
+
+#include "../Helper.hpp"
+#include "cru/common/ClonablePtr.hpp"
+#include "cru/platform/gui/Cursor.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
+#include "cru/ui/controls/Control.hpp"
+#include "cru/ui/controls/IBorderControl.hpp"
+#include "cru/ui/style/ApplyBorderStyleInfo.hpp"
+
+namespace cru::ui::style {
+BorderStyler::BorderStyler(ApplyBorderStyleInfo style)
+ : style_(std::move(style)) {}
+
+void BorderStyler::Apply(controls::Control *control) const {
+ if (auto border_control = dynamic_cast<controls::IBorderControl *>(control)) {
+ border_control->ApplyBorderStyle(style_);
+ }
+}
+
+ClonablePtr<CursorStyler> CursorStyler::Create(
+ platform::gui::SystemCursorType type) {
+ return Create(GetUiApplication()->GetCursorManager()->GetSystemCursor(type));
+}
+
+void CursorStyler::Apply(controls::Control *control) const {
+ control->SetCursor(cursor_);
+}
+} // namespace cru::ui::style