From d86a71f79afe0e4dac768f61d6bff690567aca5b Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 24 May 2020 01:40:02 +0800 Subject: ... --- src/ui/controls/TextControlService.hpp | 227 +++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 src/ui/controls/TextControlService.hpp (limited to 'src/ui/controls/TextControlService.hpp') diff --git a/src/ui/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp new file mode 100644 index 00000000..ad0db343 --- /dev/null +++ b/src/ui/controls/TextControlService.hpp @@ -0,0 +1,227 @@ +#pragma once +#include "../Helper.hpp" +#include "cru/common/Logger.hpp" +#include "cru/platform/graph/Font.hpp" +#include "cru/platform/graph/Painter.hpp" +#include "cru/platform/native/UiApplication.hpp" +#include "cru/ui/Control.hpp" +#include "cru/ui/render/CanvasRenderObject.hpp" +#include "cru/ui/render/TextRenderObject.hpp" +#include "cru/ui/UiEvent.hpp" + +namespace cru::ui::controls { +constexpr int k_default_caret_blink_duration = 500; + +// TControl should inherits `Control` and has following methods: +// ``` +// render::TextRenderObject* GetTextRenderObject(); +// ``` +template +class TextControlService : public Object { + public: + TextControlService(TControl* control); + + CRU_DELETE_COPY(TextControlService) + CRU_DELETE_MOVE(TextControlService) + + ~TextControlService(); + + public: + bool IsEnabled() { return enable_; } + void SetEnabled(bool enable); + + bool IsCaretVisible() { return caret_visible_; } + void SetCaretVisible(bool visible); + + int GetCaretBlinkDuration() { return caret_blink_duration_; } + void SetCaretBlinkDuration(int milliseconds); + + private: + void AbortSelection(); + + void SetupCaret(); + void TearDownCaret(); + + void SetupHandlers(); + + void MouseMoveHandler(event::MouseEventArgs& args); + void MouseDownHandler(event::MouseButtonEventArgs& args); + void MouseUpHandler(event::MouseButtonEventArgs& args); + void LoseFocusHandler(event::FocusChangeEventArgs& args); + + private: + TControl* control_; + std::vector event_revoker_guards_; + + bool enable_ = 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 select_down_button_; + + // before the char + int select_start_position_; +}; + +template +TextControlService::TextControlService(TControl* control) + : control_(control) {} + +template +TextControlService::~TextControlService() { + 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_); +} + +template +void TextControlService::SetEnabled(bool enable) { + if (enable == this->enable_) return; + if (enable) { + this->SetupHandlers(); + if (this->caret_visible_) { + this->SetupCaret(); + } + } else { + this->AbortSelection(); + this->event_revoker_guards_.clear(); + this->TearDownCaret(); + } +} + +template +void TextControlService::SetCaretVisible(bool visible) { + if (visible == this->caret_visible_) return; + + this->caret_visible_ = visible; + + if (this->enable_) { + if (visible) { + this->SetupCaret(); + } else { + this->TearDownCaret(); + } + } +} + +template +void TextControlService::SetCaretBlinkDuration(int milliseconds) { + if (this->caret_blink_duration_ == milliseconds) return; + + if (this->enable_ && this->caret_visible_) { + this->TearDownCaret(); + this->SetupCaret(); + } +} + +template +void TextControlService::AbortSelection() { + if (this->select_down_button_.has_value()) { + this->control_->ReleaseMouse(); + this->select_down_button_ = std::nullopt; + } + this->control_->GetTextRenderObject()->SetSelectionRange(std::nullopt); +} + +template +void TextControlService::SetupCaret() { + const auto application = GetUiApplication(); + + // Cancel first anyhow for safety. + application->CancelTimer(this->caret_timer_id_); + + this->control_->GetTextRenderObject()->SetDrawCaret(true); + this->caret_timer_id_ = application->SetInterval( + std::chrono::milliseconds(this->caret_blink_duration_), + [this] { this->control_->GetTextRenderObject()->ToggleDrawCaret(); }); +} + +template +void TextControlService::TearDownCaret() { + const auto application = GetUiApplication(); + application->CancelTimer(this->caret_timer_id_); + this->control_->GetTextRenderObject()->SetDrawCaret(false); +} + +template +void TextControlService::SetupHandlers() { + Expects(event_revoker_guards_.empty()); + this->event_revoker_guards_.push_back( + EventRevokerGuard{control_->MouseMoveEvent()->Direct()->AddHandler( + std::bind(&TextControlService::MouseMoveHandler, this, + std::placeholders::_1))}); + this->event_revoker_guards_.push_back( + EventRevokerGuard{control_->MouseDownEvent()->Direct()->AddHandler( + std::bind(&TextControlService::MouseDownHandler, this, + std::placeholders::_1))}); + this->event_revoker_guards_.push_back(EventRevokerGuard{ + control_->MouseUpEvent()->Direct()->AddHandler(std::bind( + &TextControlService::MouseUpHandler, this, std::placeholders::_1))}); + this->event_revoker_guards_.push_back( + EventRevokerGuard{control_->LoseFocusEvent()->Direct()->AddHandler( + std::bind(&TextControlService::LoseFocusHandler, this, + std::placeholders::_1))}); +} + +template +void TextControlService::MouseMoveHandler( + event::MouseEventArgs& args) { + if (this->select_down_button_.has_value()) { + const auto text_render_object = this->control_->GetTextRenderObject(); + const auto result = text_render_object->TextHitTest( + text_render_object->FromRootToContent(args.GetPoint())); + const auto position = result.position + (result.trailing ? 1 : 0); + log::Debug( + "TextControlService: Text selection changed on mouse move, range: {}, " + "{}.", + position, this->select_start_position_); + this->control_->GetTextRenderObject()->SetSelectionRange( + TextRange::FromTwoSides( + static_cast(position), + static_cast(this->select_start_position_))); + text_render_object->SetCaretPosition(position); + } +} + +template +void TextControlService::MouseDownHandler( + event::MouseButtonEventArgs& args) { + 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->control_->GetTextRenderObject(); + this->select_down_button_ = args.GetButton(); + const auto result = text_render_object->TextHitTest( + text_render_object->FromRootToContent(args.GetPoint())); + const auto position = result.position + (result.trailing ? 1 : 0); + text_render_object->SetSelectionRange(std::nullopt); + text_render_object->SetCaretPosition(position); + this->select_start_position_ = position; + log::Debug("TextControlService: Begin to select text, start position: {}.", + position); + } +} + +template +void TextControlService::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; + log::Debug("TextControlService: End selecting text."); + } +} + +template +void TextControlService::LoseFocusHandler( + event::FocusChangeEventArgs& args) { + if (!args.IsWindow()) this->AbortSelection(); +} +} // namespace cru::ui::controls -- cgit v1.2.3