aboutsummaryrefslogtreecommitdiff
path: root/src/ui/controls
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/controls')
-rw-r--r--src/ui/controls/Button.cpp62
-rw-r--r--src/ui/controls/Container.cpp10
-rw-r--r--src/ui/controls/ContentControl.cpp31
-rw-r--r--src/ui/controls/Control.cpp166
-rw-r--r--src/ui/controls/FlexLayout.cpp12
-rw-r--r--src/ui/controls/LayoutControl.cpp18
-rw-r--r--src/ui/controls/NoChildControl.cpp3
-rw-r--r--src/ui/controls/Popup.cpp22
-rw-r--r--src/ui/controls/RootControl.cpp53
-rw-r--r--src/ui/controls/StackLayout.cpp15
-rw-r--r--src/ui/controls/TextBlock.cpp22
-rw-r--r--src/ui/controls/TextBox.cpp36
-rw-r--r--src/ui/controls/TextControlService.hpp403
-rw-r--r--src/ui/controls/TextHostControlService.cpp469
-rw-r--r--src/ui/controls/Window.cpp24
15 files changed, 834 insertions, 512 deletions
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