diff options
Diffstat (limited to 'src/ui')
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 |