aboutsummaryrefslogtreecommitdiff
path: root/src/ui/controls/TextHostControlService.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/controls/TextHostControlService.cpp')
-rw-r--r--src/ui/controls/TextHostControlService.cpp469
1 files changed, 469 insertions, 0 deletions
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