aboutsummaryrefslogtreecommitdiff
path: root/src/ui/host
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/host')
-rw-r--r--src/ui/host/RoutedEventDispatch.hpp110
-rw-r--r--src/ui/host/WindowHost.cpp400
2 files changed, 510 insertions, 0 deletions
diff --git a/src/ui/host/RoutedEventDispatch.hpp b/src/ui/host/RoutedEventDispatch.hpp
new file mode 100644
index 00000000..de94a598
--- /dev/null
+++ b/src/ui/host/RoutedEventDispatch.hpp
@@ -0,0 +1,110 @@
+#pragma once
+#include "cru/ui/Control.hpp"
+
+#include "cru/common/Logger.hpp"
+#include "cru/ui/DebugFlags.hpp"
+
+#include <vector>
+
+namespace cru::ui {
+// Dispatch the event.
+//
+// This will raise routed event of the control and its parent and parent's
+// parent ... (until "last_receiver" if it's not nullptr) with appropriate args.
+//
+// First tunnel from top to bottom possibly stopped by "handled" flag in
+// EventArgs. Second bubble from bottom to top possibly stopped by "handled"
+// flag in EventArgs. Last direct to each control.
+//
+// Args is of type "EventArgs". The first init argument is "sender", which is
+// automatically bound to each receiving control. The second init argument is
+// "original_sender", which is unchanged. And "args" will be perfectly forwarded
+// as the rest arguments.
+template <typename EventArgs, typename... Args>
+void DispatchEvent(const std::u16string_view& event_name,
+ Control* const original_sender,
+ event::RoutedEvent<EventArgs>* (Control::*event_ptr)(),
+ Control* const last_receiver, Args&&... args) {
+ CRU_UNUSED(event_name)
+
+ if (original_sender == last_receiver) {
+ 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::vector<Control*> receive_list;
+
+ auto parent = original_sender;
+ while (parent != last_receiver) {
+ receive_list.push_back(parent);
+ parent = parent->GetParent();
+ }
+
+ if constexpr (debug_flags::routed_event) {
+ std::u16string log = u"Dispatch routed event ";
+ log += event_name;
+ log += u". Path (parent first): ";
+ auto i = receive_list.crbegin();
+ const auto end = --receive_list.crend();
+ for (; i != end; ++i) {
+ log += (*i)->GetControlType();
+ log += u" -> ";
+ }
+ log += (*i)->GetControlType();
+ log::Debug(log);
+ }
+
+ auto handled = false;
+
+ int count = 0;
+
+ // tunnel
+ for (auto i = receive_list.crbegin(); i != receive_list.crend(); ++i) {
+ count++;
+ 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;
+ if constexpr (debug_flags::routed_event)
+ log::Debug(
+ u"Routed event is short-circuit in TUNNEL at {}-st control (count "
+ u"from parent).",
+ count);
+ break;
+ }
+ }
+
+ // bubble
+ if (!handled) {
+ for (auto i : receive_list) {
+ count--;
+ 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()) {
+ if constexpr (debug_flags::routed_event)
+ log::Debug(
+ u"Routed event is short-circuit in BUBBLE at {}-st control "
+ u"(count from parent).",
+ count);
+ break;
+ }
+ }
+ }
+
+ // direct
+ for (auto i : receive_list) {
+ EventArgs event_args(i, original_sender, std::forward<Args>(args)...);
+ static_cast<Event<EventArgs&>*>((i->*event_ptr)()->Direct())
+ ->Raise(event_args);
+ }
+
+ 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..eac247c1
--- /dev/null
+++ b/src/ui/host/WindowHost.cpp
@@ -0,0 +1,400 @@
+#include "cru/ui/host/WindowHost.hpp"
+
+#include "RoutedEventDispatch.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/Window.hpp"
+#include "cru/ui/render/MeasureRequirement.hpp"
+#include "cru/ui/render/RenderObject.hpp"
+
+#include <cstddef>
+
+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(Control* control, Control* ancestor) {
+ while (control != nullptr) {
+ if (control == ancestor) return true;
+ control = control->GetParent();
+ }
+ return false;
+}
+
+// Ancestor at last.
+std::vector<Control*> GetAncestorList(Control* control) {
+ std::vector<Control*> l;
+ while (control != nullptr) {
+ l.push_back(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.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(Control* root_control)
+ : root_control_(root_control), focus_control_(root_control) {
+ const auto ui_application = IUiApplication::GetInstance();
+ auto native_window = ui_application->CreateWindow(nullptr);
+ native_window_ = native_window;
+
+ root_control_->TraverseDescendants([this](Control* control) {
+ control->window_host_ = this;
+ control->OnAttachToHost(this);
+ });
+
+ root_render_object_ = root_control->GetRenderObject();
+ root_render_object_->SetWindowHostRecursive(this);
+
+ 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_);
+}
+
+WindowHost::~WindowHost() {
+ if (native_window_) {
+ native_window_->Close();
+ }
+}
+
+void WindowHost::InvalidatePaint() {
+ if (native_window_) native_window_->RequestRepaint();
+}
+
+void WindowHost::InvalidateLayout() {
+ need_layout_ = true;
+ this->InvalidatePaint();
+}
+
+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.");
+}
+
+Control* WindowHost::GetFocusControl() { return focus_control_; }
+
+void WindowHost::SetFocusControl(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,
+ &Control::LoseFocusEvent, nullptr, false);
+
+ DispatchEvent(event_names::GainFocus, control, &Control::GainFocusEvent,
+ nullptr, false);
+}
+
+bool WindowHost::CaptureMouseFor(Control* control) {
+ 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* WindowHost::GetMouseCaptureControl() {
+ return mouse_captured_control_;
+}
+
+void WindowHost::RunAfterLayoutStable(std::function<void()> action) {
+ if (need_layout_) {
+ after_layout_stable_action_.push_back(std::move(action));
+ } else {
+ action();
+ }
+}
+
+void WindowHost::OnNativeDestroy(INativeWindow* window, std::nullptr_t) {
+ CRU_UNUSED(window)
+ this->native_window_ = nullptr;
+}
+
+void WindowHost::OnNativePaint(INativeWindow* window, std::nullptr_t) {
+ if (need_layout_) {
+ Relayout();
+ need_layout_ = false;
+ }
+ auto painter = window->BeginPaint();
+ painter->Clear(colors::white);
+ root_render_object_->Draw(painter.get());
+ painter->EndDraw();
+}
+
+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_,
+ &Control::GainFocusEvent, nullptr, true)
+ : DispatchEvent(event_names::LoseFocus, focus_control_,
+ &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_,
+ &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, &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 WindowHost::OnNativeMouseDown(
+ INativeWindow* window,
+ const platform::gui::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 WindowHost::OnNativeMouseUp(
+ INativeWindow* window,
+ const platform::gui::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 WindowHost::OnNativeKeyDown(
+ INativeWindow* window, const platform::gui::NativeKeyEventArgs& args) {
+ CRU_UNUSED(window)
+
+ DispatchEvent(event_names::KeyDown, focus_control_, &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_, &Control::KeyUpEvent,
+ nullptr, args.key, args.modifier);
+}
+
+void WindowHost::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 WindowHost::UpdateCursor() {
+ if (native_window_) {
+ const auto capture = GetMouseCaptureControl();
+ native_window_->SetCursor(
+ (capture ? capture : GetMouseHoverControl())->GetInheritedCursor());
+ }
+}
+
+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