aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-10-28 17:44:06 +0800
committercrupest <crupest@outlook.com>2020-10-28 17:44:06 +0800
commit864b031211322dc276b220ec0a6e11483503a0e9 (patch)
treec5c0aa1a046b443dc001445f39870cb440b6fe38 /src
parent2df47ffbfff02fb6b64d19e404adc41a93677afe (diff)
downloadcru-864b031211322dc276b220ec0a6e11483503a0e9.tar.gz
cru-864b031211322dc276b220ec0a6e11483503a0e9.tar.bz2
cru-864b031211322dc276b220ec0a6e11483503a0e9.zip
...
Diffstat (limited to 'src')
-rw-r--r--src/common/StringUtil.cpp13
-rw-r--r--src/ui/controls/TextControlService.hpp122
2 files changed, 103 insertions, 32 deletions
diff --git a/src/common/StringUtil.cpp b/src/common/StringUtil.cpp
index fc6d6349..3c312d49 100644
--- a/src/common/StringUtil.cpp
+++ b/src/common/StringUtil.cpp
@@ -1,4 +1,5 @@
#include "cru/common/StringUtil.hpp"
+#include "gsl/gsl_util"
namespace cru {
namespace {
@@ -191,8 +192,8 @@ void Utf8EncodeCodePointAppend(CodePoint code_point, std::string& str) {
}
void Utf16EncodeCodePointAppend(CodePoint code_point, std::u16string& str) {
- if (code_point >= 0 && code_point <= 0xD7FF ||
- code_point >= 0xE000 && code_point <= 0xFFFF) {
+ if ((code_point >= 0 && code_point <= 0xD7FF) ||
+ (code_point >= 0xE000 && code_point <= 0xFFFF)) {
str.push_back(static_cast<char16_t>(code_point));
} else if (code_point >= 0x10000 && code_point <= 0x10FFFF) {
std::uint32_t u = code_point - 0x10000;
@@ -220,4 +221,12 @@ std::u16string ToUtf16(std::string_view s) {
}
return result;
}
+
+bool Utf16IsValidInsertPosition(std::u16string_view s, gsl::index position) {
+ if (position < 0) return false;
+ if (position > static_cast<gsl::index>(s.size())) return false;
+ if (position == 0) return true;
+ if (position == static_cast<gsl::index>(s.size())) return true;
+ return !IsUtf16SurrogatePairTrailing(s[position]);
+}
} // namespace cru
diff --git a/src/ui/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp
index c1a879f6..04807c30 100644
--- a/src/ui/controls/TextControlService.hpp
+++ b/src/ui/controls/TextControlService.hpp
@@ -1,4 +1,5 @@
#pragma once
+#include <string>
#include "../Helper.hpp"
#include "cru/common/Logger.hpp"
#include "cru/common/StringUtil.hpp"
@@ -7,6 +8,7 @@
#include "cru/platform/native/InputMethod.hpp"
#include "cru/platform/native/UiApplication.hpp"
#include "cru/platform/native/Window.hpp"
+#include "cru/ui/Base.hpp"
#include "cru/ui/Control.hpp"
#include "cru/ui/ShortcutHub.hpp"
#include "cru/ui/UiEvent.hpp"
@@ -14,6 +16,7 @@
#include "cru/ui/render/CanvasRenderObject.hpp"
#include "cru/ui/render/ScrollRenderObject.hpp"
#include "cru/ui/render/TextRenderObject.hpp"
+#include "gsl/gsl_util"
namespace cru::ui::controls {
constexpr int k_default_caret_blink_duration = 500;
@@ -64,13 +67,73 @@ class TextControlService : public Object {
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 && this->input_method_context_) {
+ this->input_method_context_->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 && this->input_method_context_) {
this->input_method_context_->CancelComposition();
}
- CoerceSelection();
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 && this->input_method_context_) {
+ this->input_method_context_->CancelComposition();
+ }
+ this->SyncTextRenderObject();
+ }
+
std::optional<platform::native::CompositionText> GetCompositionInfo() {
if (this->input_method_context_ == nullptr) return std::nullopt;
auto composition_info = this->input_method_context_->GetCompositionText();
@@ -126,21 +189,33 @@ class TextControlService : public Object {
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});
- }
+ this->ScrollToCaret();
}
}
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());
+ this->DeleteText(GetSelection());
+ SetSelection(GetSelection().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(bool next_tick = true) {
+ if (next_tick) {
+ scroll_to_caret_timer_canceler_.Reset(
+ GetUiApplication()->GetInstance()->SetImmediate(
+ [this]() { this->ScrollToCaret(false); }));
+ } else {
+ const auto caret_rect = this->GetTextRenderObject()->GetCaretRect();
+ this->GetScrollRenderObject()->ScrollToContain(caret_rect,
+ Thickness{5.f});
+ }
}
private:
@@ -278,14 +353,7 @@ class TextControlService : public Object {
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);
+ SetSelection(DeleteCharPrevious(GetCaretPosition()));
} else {
this->DeleteSelectedText();
}
@@ -294,14 +362,7 @@ class TextControlService : public Object {
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();
+ DeleteChar(GetCaretPosition());
} else {
this->DeleteSelectedText();
}
@@ -362,8 +423,7 @@ class TextControlService : public Object {
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());
+ this->ReplaceSelectedText(text);
});
}
}
@@ -393,6 +453,8 @@ class TextControlService : public Object {
ShortcutHub shortcut_hub_;
+ platform::native::TimerAutoCanceler scroll_to_caret_timer_canceler_;
+
// nullopt means not selecting
std::optional<MouseButton> select_down_button_;