aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-12-25 15:38:18 +0800
committercrupest <crupest@outlook.com>2020-12-25 15:38:18 +0800
commitda7ad0ff5c5b158be69c6cf9a2c8e9fc9ef2b3cb (patch)
tree9d58ba66119f5a1f6efa5a9b75c22e0453993a07
parenta14704fbd9b9fb377b7009a9fbe641a9b8d0fdfb (diff)
downloadcru-da7ad0ff5c5b158be69c6cf9a2c8e9fc9ef2b3cb.tar.gz
cru-da7ad0ff5c5b158be69c6cf9a2c8e9fc9ef2b3cb.tar.bz2
cru-da7ad0ff5c5b158be69c6cf9a2c8e9fc9ef2b3cb.zip
...
-rw-r--r--include/cru/platform/GraphBase.hpp2
-rw-r--r--include/cru/ui/controls/TextBlock.hpp15
-rw-r--r--include/cru/ui/controls/TextBox.hpp12
-rw-r--r--include/cru/ui/controls/TextHostControlService.hpp141
-rw-r--r--src/ui/CMakeLists.txt5
-rw-r--r--src/ui/controls/TextBlock.cpp3
-rw-r--r--src/ui/controls/TextBox.cpp3
-rw-r--r--src/ui/controls/TextControlService.hpp511
-rw-r--r--src/ui/controls/TextHostControlService.cpp458
9 files changed, 621 insertions, 529 deletions
diff --git a/include/cru/platform/GraphBase.hpp b/include/cru/platform/GraphBase.hpp
index 2b40898e..b580ad31 100644
--- a/include/cru/platform/GraphBase.hpp
+++ b/include/cru/platform/GraphBase.hpp
@@ -277,7 +277,7 @@ struct TextRange final {
gsl::index GetStart() const { return position; }
gsl::index GetEnd() const { return position + count; }
- void AdjustEnd(gsl::index new_end) { count = new_end - position; }
+ void ChangeEnd(gsl::index new_end) { count = new_end - position; }
TextRange Normalize() const {
auto result = *this;
diff --git a/include/cru/ui/controls/TextBlock.hpp b/include/cru/ui/controls/TextBlock.hpp
index 66ebe476..be31816c 100644
--- a/include/cru/ui/controls/TextBlock.hpp
+++ b/include/cru/ui/controls/TextBlock.hpp
@@ -1,11 +1,10 @@
#pragma once
#include "NoChildControl.hpp"
-namespace cru::ui::controls {
-template <typename TControl>
-class TextControlService;
+#include "TextHostControlService.hpp"
-class TextBlock : public NoChildControl {
+namespace cru::ui::controls {
+class TextBlock : public NoChildControl, public virtual ITextHostControl {
public:
static constexpr std::u16string_view control_type = u"TextBlock";
@@ -32,12 +31,14 @@ class TextBlock : public NoChildControl {
bool IsSelectable() const;
void SetSelectable(bool value);
- gsl::not_null<render::TextRenderObject*> GetTextRenderObject();
- render::ScrollRenderObject* GetScrollRenderObject() { return nullptr; }
+ gsl::not_null<render::TextRenderObject*> GetTextRenderObject() override;
+ render::ScrollRenderObject* GetScrollRenderObject() override {
+ return nullptr;
+ }
private:
std::unique_ptr<render::TextRenderObject> text_render_object_;
- std::unique_ptr<TextControlService<TextBlock>> service_;
+ std::unique_ptr<TextHostControlService> service_;
};
} // namespace cru::ui::controls
diff --git a/include/cru/ui/controls/TextBox.hpp b/include/cru/ui/controls/TextBox.hpp
index 75e7cb65..5693b315 100644
--- a/include/cru/ui/controls/TextBox.hpp
+++ b/include/cru/ui/controls/TextBox.hpp
@@ -1,6 +1,8 @@
#pragma once
#include "NoChildControl.hpp"
+
#include "IBorderControl.hpp"
+#include "TextHostControlService.hpp"
#include <memory>
@@ -8,7 +10,9 @@ namespace cru::ui::controls {
template <typename TControl>
class TextControlService;
-class TextBox : public NoChildControl, public IBorderControl {
+class TextBox : public NoChildControl,
+ public virtual IBorderControl,
+ public virtual ITextHostControl {
public:
static constexpr std::u16string_view control_type = u"TextBox";
@@ -27,8 +31,8 @@ class TextBox : public NoChildControl, public IBorderControl {
render::RenderObject* GetRenderObject() const override;
- gsl::not_null<render::TextRenderObject*> GetTextRenderObject();
- render::ScrollRenderObject* GetScrollRenderObject();
+ gsl::not_null<render::TextRenderObject*> GetTextRenderObject() override;
+ render::ScrollRenderObject* GetScrollRenderObject() override;
void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) override;
@@ -37,6 +41,6 @@ class TextBox : public NoChildControl, public IBorderControl {
std::unique_ptr<render::ScrollRenderObject> scroll_render_object_;
std::unique_ptr<render::TextRenderObject> text_render_object_;
- std::unique_ptr<TextControlService<TextBox>> service_;
+ std::unique_ptr<TextHostControlService> service_;
};
} // namespace cru::ui::controls
diff --git a/include/cru/ui/controls/TextHostControlService.hpp b/include/cru/ui/controls/TextHostControlService.hpp
new file mode 100644
index 00000000..0bea52c8
--- /dev/null
+++ b/include/cru/ui/controls/TextHostControlService.hpp
@@ -0,0 +1,141 @@
+#pragma once
+#include "Base.hpp"
+
+#include "cru/platform/gui/InputMethod.hpp"
+#include "cru/platform/gui/UiApplication.hpp"
+#include "cru/ui/controls/Control.hpp"
+#include "cru/ui/helper/ShortcutHub.hpp"
+
+#include <functional>
+#include <string>
+
+namespace cru::ui::render {
+class TextRenderObject;
+class ScrollRenderObject;
+} // namespace cru::ui::render
+
+namespace cru::ui::controls {
+constexpr int k_default_caret_blink_duration = 500;
+
+struct ITextHostControl : virtual Interface {
+ virtual gsl::not_null<render::TextRenderObject*> GetTextRenderObject() = 0;
+ // May return nullptr.
+ virtual render::ScrollRenderObject* GetScrollRenderObject() = 0;
+};
+
+class TextHostControlService : public Object {
+ CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::controls::TextControlService")
+
+ public:
+ TextHostControlService(gsl::not_null<Control*> control);
+
+ CRU_DELETE_COPY(TextHostControlService)
+ CRU_DELETE_MOVE(TextHostControlService)
+
+ ~TextHostControlService() = default;
+
+ public:
+ bool IsEnabled() { return enable_; }
+ void SetEnabled(bool enable);
+
+ bool IsEditable() { return this->editable_; }
+ void SetEditable(bool editable);
+
+ std::u16string GetText() { return this->text_; }
+ std::u16string_view GetTextView() { return this->text_; }
+ void SetText(std::u16string text, bool stop_composition = false);
+
+ void InsertText(gsl::index position, std::u16string_view text,
+ bool stop_composition = false);
+ void DeleteChar(gsl::index position, bool stop_composition = false);
+
+ // Return the position of deleted character.
+ gsl::index DeleteCharPrevious(gsl::index position,
+ bool stop_composition = false);
+ void DeleteText(TextRange range, bool stop_composition = false);
+
+ void CancelComposition();
+
+ std::optional<platform::gui::CompositionText> GetCompositionInfo();
+
+ bool IsCaretVisible() { return caret_visible_; }
+ void SetCaretVisible(bool visible);
+
+ int GetCaretBlinkDuration() { return caret_blink_duration_; }
+ void SetCaretBlinkDuration(int milliseconds);
+
+ gsl::index GetCaretPosition() { return selection_.GetEnd(); }
+ TextRange GetSelection() { return selection_; }
+
+ void SetSelection(gsl::index caret_position);
+ void SetSelection(TextRange selection, bool scroll_to_caret = true);
+
+ void DeleteSelectedText();
+
+ // If some text is selected, then they are deleted first. Then insert text
+ // into caret position.
+ void ReplaceSelectedText(std::u16string_view text);
+
+ void ScrollToCaret();
+
+ private:
+ gsl::not_null<render::TextRenderObject*> GetTextRenderObject();
+ render::ScrollRenderObject* GetScrollRenderObject();
+
+ // May return nullptr.
+ platform::gui::IInputMethodContext* GetInputMethodContext();
+
+ void CoerceSelection();
+
+ void SetupCaret();
+ void TearDownCaret();
+
+ void SyncTextRenderObject();
+
+ void StartSelection(Index start);
+ void UpdateSelection(Index new_end);
+ void AbortSelection();
+
+ void UpdateInputMethodPosition();
+
+ template <typename TArgs>
+ void SetupOneHandler(event::RoutedEvent<TArgs>* (Control::*event)(),
+ void (TextHostControlService::*handler)(
+ typename event::RoutedEvent<TArgs>::EventArgs)) {
+ this->event_guard_ += (this->control_->*event)()->Direct()->AddHandler(
+ std::bind(handler, this, std::placeholders::_1));
+ }
+
+ void SetUpHandlers();
+ void TearDownHandlers();
+
+ void MouseMoveHandler(event::MouseEventArgs& args);
+ void MouseDownHandler(event::MouseButtonEventArgs& args);
+ void MouseUpHandler(event::MouseButtonEventArgs& args);
+ void KeyDownHandler(event::KeyEventArgs& args);
+ void GainFocusHandler(event::FocusChangeEventArgs& args);
+ void LoseFocusHandler(event::FocusChangeEventArgs& args);
+
+ private:
+ gsl::not_null<Control*> control_;
+ gsl::not_null<ITextHostControl*> text_host_control_;
+
+ EventRevokerListGuard event_guard_;
+ EventRevokerListGuard input_method_context_event_guard_;
+
+ std::u16string text_;
+ TextRange selection_;
+
+ bool enable_ = false;
+ bool editable_ = false;
+
+ bool caret_visible_ = false;
+ platform::gui::TimerAutoCanceler caret_timer_canceler_;
+ int caret_blink_duration_ = k_default_caret_blink_duration;
+
+ helper::ShortcutHub shortcut_hub_;
+
+ // true if left mouse is down and selecting
+ bool mouse_move_selecting_;
+};
+} // namespace cru::ui::controls
diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt
index 28200eb5..d1c1e830 100644
--- a/src/ui/CMakeLists.txt
+++ b/src/ui/CMakeLists.txt
@@ -20,7 +20,7 @@ add_library(cru_ui STATIC
controls/StackLayout.cpp
controls/TextBlock.cpp
controls/TextBox.cpp
- controls/TextControlService.hpp
+ controls/TextHostControlService.cpp
controls/Window.cpp
events/UiEvent.cpp
helper/BorderStyle.cpp
@@ -60,8 +60,9 @@ target_sources(cru_ui PUBLIC
${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
diff --git a/src/ui/controls/TextBlock.cpp b/src/ui/controls/TextBlock.cpp
index 1a432582..0724edcf 100644
--- a/src/ui/controls/TextBlock.cpp
+++ b/src/ui/controls/TextBlock.cpp
@@ -1,6 +1,5 @@
#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"
@@ -27,7 +26,7 @@ TextBlock::TextBlock() {
text_render_object_->SetAttachedControl(this);
- service_ = std::make_unique<TextControlService<TextBlock>>(this);
+ service_ = std::make_unique<TextHostControlService>(this);
service_->SetEnabled(false);
service_->SetEditable(false);
diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp
index d8317a8c..e1acaee0 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"
@@ -30,7 +29,7 @@ TextBox::TextBox()
text_render_object_->SetAttachedControl(this);
text_render_object_->SetMinSize(Size{100, 24});
- service_ = std::make_unique<TextControlService<TextBox>>(this);
+ service_ = std::make_unique<TextHostControlService>(this);
service_->SetEnabled(true);
service_->SetEditable(true);
diff --git a/src/ui/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp
deleted file mode 100644
index c535512f..00000000
--- a/src/ui/controls/TextControlService.hpp
+++ /dev/null
@@ -1,511 +0,0 @@
-#pragma once
-#include "../Helper.hpp"
-#include "cru/common/Logger.hpp"
-#include "cru/common/StringUtil.hpp"
-#include "cru/platform/graphics/Font.hpp"
-#include "cru/platform/graphics/Painter.hpp"
-#include "cru/platform/gui/Cursor.hpp"
-#include "cru/platform/gui/InputMethod.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/controls/Control.hpp"
-#include "cru/ui/events/UiEvent.hpp"
-#include "cru/ui/helper/ShortcutHub.hpp"
-#include "cru/ui/host/WindowHost.hpp"
-#include "cru/ui/render/CanvasRenderObject.hpp"
-#include "cru/ui/render/ScrollRenderObject.hpp"
-#include "cru/ui/render/TextRenderObject.hpp"
-
-#include <string>
-
-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() = default;
-
- 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();
- }
- this->control_->SetCursor(
- GetUiApplication()->GetCursorManager()->GetSystemCursor(
- platform::gui::SystemCursorType::IBeam));
- } else {
- this->AbortSelection();
- this->TearDownHandlers();
- this->TearDownCaret();
- this->control_->SetCursor(nullptr);
- }
- }
-
- bool IsEditable() { return this->editable_; }
-
- void SetEditable(bool editable) {
- this->editable_ = editable;
- if (!editable) CancelComposition();
- }
-
- 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);
- CoerceSelection();
- if (stop_composition) {
- CancelComposition();
- }
- SyncTextRenderObject();
- }
-
- void InsertText(gsl::index position, std::u16string_view text,
- bool stop_composition = false) {
- 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 DeleteChar(gsl::index position, bool stop_composition = false) {
- 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 DeleteCharPrevious(gsl::index position,
- bool stop_composition = false) {
- 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 DeleteText(TextRange range, bool stop_composition = false) {
- 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* 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 CancelComposition() {
- auto input_method_context = GetInputMethodContext();
- if (input_method_context == nullptr) return;
- input_method_context->CancelComposition();
- }
-
- std::optional<platform::gui::CompositionText> 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;
- }
-
- 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) {
- this->ScrollToCaret();
- }
- }
-
- void DeleteSelectedText() {
- this->DeleteText(GetSelection());
- SetSelection(GetSelection().Normalize().GetStart());
- }
-
- // If some text is selected, then they are deleted first. Then insert text
- // into caret position.
- void ReplaceSelectedText(std::u16string_view text) {
- DeleteSelectedText();
- InsertText(GetSelection().GetStart(), text);
- SetSelection(GetSelection().GetStart() + text.size());
- }
-
- void 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});
- });
- }
- }
-
- 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();
- this->GetTextRenderObject()->SetDrawCaret(true);
- this->caret_timer_canceler_.Reset(application->SetInterval(
- std::chrono::milliseconds(this->caret_blink_duration_),
- [this] { this->GetTextRenderObject()->ToggleDrawCaret(); }));
- }
-
- void TearDownCaret() {
- this->caret_timer_canceler_.Reset();
- 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());
- }
- }
-
- void StartSelection(Index start) {
- SetSelection(start);
- if constexpr (debug_flags::text_service)
- 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);
- if constexpr (debug_flags::text_service)
- log::TagDebug(log_tag, u"Text selection updated, range: {}, {}.",
- selection.GetStart(), selection.GetEnd());
- }
-
- void 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);
- }
- }
-
- template <typename TArgs>
- void SetupOneHandler(event::RoutedEvent<TArgs>* (Control::*event)(),
- void (TextControlService::*handler)(
- typename event::RoutedEvent<TArgs>::EventArgs)) {
- this->event_guard_ += (this->control_->*event)()->Direct()->AddHandler(
- std::bind(handler, this, std::placeholders::_1));
- }
-
- void SetUpHandlers() {
- Expects(event_guard_.IsEmpty());
-
- SetupOneHandler(&Control::MouseMoveEvent,
- &TextControlService::MouseMoveHandler);
- SetupOneHandler(&Control::MouseDownEvent,
- &TextControlService::MouseDownHandler);
- SetupOneHandler(&Control::MouseUpEvent,
- &TextControlService::MouseUpHandler);
- SetupOneHandler(&Control::KeyDownEvent,
- &TextControlService::KeyDownHandler);
- SetupOneHandler(&Control::GainFocusEvent,
- &TextControlService::GainFocusHandler);
- SetupOneHandler(&Control::LoseFocusEvent,
- &TextControlService::LoseFocusHandler);
-
- shortcut_hub_.Install(control_);
- }
-
- void TearDownHandlers() {
- event_guard_.Clear();
- shortcut_hub_.Uninstall();
- }
-
- 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) {
- if (this->select_down_button_.has_value()) {
- return;
- } else {
- this->control_->SetFocus();
- if (!this->control_->CaptureMouse()) 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::gui::KeyCode;
- using cru::platform::gui::KeyModifiers;
-
- switch (key_code) {
- case KeyCode::Backspace: {
- if (!IsEditable()) return;
- const auto selection = GetSelection();
- if (selection.count == 0) {
- SetSelection(DeleteCharPrevious(GetCaretPosition()));
- } else {
- this->DeleteSelectedText();
- }
- } break;
- case KeyCode::Delete: {
- if (!IsEditable()) return;
- const auto selection = GetSelection();
- if (selection.count == 0) {
- DeleteChar(GetCaretPosition());
- } 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;
- default:
- break;
- }
- }
-
- void 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) {
- if (text == u"\b") return;
- 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 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();
- }
-
- private:
- gsl::not_null<TControl*> control_;
- EventRevokerListGuard event_guard_;
- EventRevokerListGuard input_method_context_event_guard_;
-
- std::u16string text_;
- TextRange selection_;
-
- bool enable_ = false;
- bool editable_ = false;
-
- bool caret_visible_ = false;
- platform::gui::TimerAutoCanceler caret_timer_canceler_;
- int caret_blink_duration_ = k_default_caret_blink_duration;
-
- helper::ShortcutHub shortcut_hub_;
-
- // nullopt means not selecting
- std::optional<MouseButton> select_down_button_;
-};
-} // namespace cru::ui::controls
diff --git a/src/ui/controls/TextHostControlService.cpp b/src/ui/controls/TextHostControlService.cpp
new file mode 100644
index 00000000..1ce3f642
--- /dev/null
+++ b/src/ui/controls/TextHostControlService.cpp
@@ -0,0 +1,458 @@
+#include "cru/ui/controls/TextHostControlService.hpp"
+
+#include "../Helper.hpp"
+#include "cru/common/Logger.hpp"
+#include "cru/common/StringUtil.hpp"
+#include "cru/platform/graphics/Font.hpp"
+#include "cru/platform/graphics/Painter.hpp"
+#include "cru/platform/gui/Cursor.hpp"
+#include "cru/platform/gui/InputMethod.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/controls/Control.hpp"
+#include "cru/ui/events/UiEvent.hpp"
+#include "cru/ui/helper/ShortcutHub.hpp"
+#include "cru/ui/host/WindowHost.hpp"
+#include "cru/ui/render/CanvasRenderObject.hpp"
+#include "cru/ui/render/ScrollRenderObject.hpp"
+#include "cru/ui/render/TextRenderObject.hpp"
+#include "gsl/gsl_assert"
+#include "gsl/pointers"
+
+namespace cru::ui::controls {
+TextHostControlService::TextHostControlService(gsl::not_null<Control*> control)
+ : control_(control),
+ text_host_control_(dynamic_cast<ITextHostControl*>(control.get())) {}
+
+void TextHostControlService::SetEnabled(bool enable) {
+ if (enable == this->enable_) return;
+ this->enable_ = enable;
+ if (enable) {
+ this->SetUpHandlers();
+ if (this->caret_visible_) {
+ this->SetupCaret();
+ }
+ this->control_->SetCursor(
+ GetUiApplication()->GetCursorManager()->GetSystemCursor(
+ platform::gui::SystemCursorType::IBeam));
+ } else {
+ this->AbortSelection();
+ this->TearDownHandlers();
+ 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::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::StartSelection(Index start) {
+ SetSelection(start);
+ if constexpr (debug_flags::text_service)
+ log::TagDebug(log_tag, u"Text selection started, position: {}.", start);
+}
+
+void TextHostControlService::UpdateSelection(Index new_end) {
+ auto selection = GetSelection();
+ selection.ChangeEnd(new_end);
+ this->SetSelection(selection);
+ if constexpr (debug_flags::text_service)
+ log::TagDebug(log_tag, u"Text selection updated, range: {}, {}.",
+ selection.GetStart(), selection.GetEnd());
+}
+
+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::AbortSelection() {
+ if (this->mouse_move_selecting_) {
+ this->control_->ReleaseMouse();
+ this->mouse_move_selecting_ = false;
+ }
+ this->GetTextRenderObject()->SetSelectionRange(std::nullopt);
+}
+
+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::TearDownHandlers() {
+ event_guard_.Clear();
+ shortcut_hub_.Uninstall();
+}
+void TextHostControlService::SetUpHandlers() {
+ Expects(event_guard_.IsEmpty());
+
+ SetupOneHandler(&Control::MouseMoveEvent,
+ &TextHostControlService::MouseMoveHandler);
+ SetupOneHandler(&Control::MouseDownEvent,
+ &TextHostControlService::MouseDownHandler);
+ SetupOneHandler(&Control::MouseUpEvent,
+ &TextHostControlService::MouseUpHandler);
+ SetupOneHandler(&Control::KeyDownEvent,
+ &TextHostControlService::KeyDownHandler);
+ SetupOneHandler(&Control::GainFocusEvent,
+ &TextHostControlService::GainFocusHandler);
+ SetupOneHandler(&Control::LoseFocusEvent,
+ &TextHostControlService::LoseFocusHandler);
+
+ shortcut_hub_.Install(control_);
+}
+
+void TextHostControlService::MouseDownHandler(
+ event::MouseButtonEventArgs& args) {
+ if (this->mouse_move_selecting_) {
+ return;
+ } else {
+ this->control_->SetFocus();
+ 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);
+ StartSelection(position);
+ }
+}
+
+void TextHostControlService::MouseUpHandler(event::MouseButtonEventArgs&) {
+ if (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);
+ UpdateSelection(position);
+ }
+}
+
+void TextHostControlService::KeyDownHandler(event::KeyEventArgs& args) {
+ const auto key_code = args.GetKeyCode();
+ using cru::platform::gui::KeyCode;
+ using cru::platform::gui::KeyModifiers;
+
+ switch (key_code) {
+ case KeyCode::Backspace: {
+ if (!IsEditable()) return;
+ const auto selection = GetSelection();
+ if (selection.count == 0) {
+ SetSelection(DeleteCharPrevious(GetCaretPosition()));
+ } else {
+ this->DeleteSelectedText();
+ }
+ } break;
+ case KeyCode::Delete: {
+ if (!IsEditable()) return;
+ const auto selection = GetSelection();
+ if (selection.count == 0) {
+ DeleteChar(GetCaretPosition());
+ } 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.ChangeEnd(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.ChangeEnd(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;
+ default:
+ break;
+ }
+}
+
+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) {
+ if (text == u"\b") return;
+ 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();
+}
+} // namespace cru::ui::controls