From 5729a5aa1b443e3e25f3e14dee29636d3b31a6f8 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 18 Oct 2020 21:09:21 +0800 Subject: ... --- include/cru/platform/GraphBase.hpp | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'include/cru/platform/GraphBase.hpp') diff --git a/include/cru/platform/GraphBase.hpp b/include/cru/platform/GraphBase.hpp index 186ee9d0..571b36f7 100644 --- a/include/cru/platform/GraphBase.hpp +++ b/include/cru/platform/GraphBase.hpp @@ -1,9 +1,13 @@ #pragma once #include "cru/common/Base.hpp" +#include "cru/common/Format.hpp" + +#include #include #include #include +#include #include namespace cru::platform { @@ -46,6 +50,11 @@ struct Size final { std::numeric_limits::max()}; } + std::u16string ToDebugString() const { + return fmt::format(u"({}, {})", ToUtf16String(width), + ToUtf16String(height)); + } + float width = 0; float height = 0; }; -- cgit v1.2.3 From c072432e68d7a3d7659add0994b2f8caf387ddf2 Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 18 Oct 2020 21:28:35 +0800 Subject: ... --- demos/main/main.cpp | 5 +++-- include/cru/platform/GraphBase.hpp | 4 ++++ include/cru/ui/controls/FlexLayout.hpp | 3 +++ src/ui/controls/FlexLayout.cpp | 9 +++++++++ src/ui/render/FlexLayoutRenderObject.cpp | 15 ++++++++------- src/ui/render/RenderObject.cpp | 4 ++++ 6 files changed, 31 insertions(+), 9 deletions(-) (limited to 'include/cru/platform/GraphBase.hpp') diff --git a/demos/main/main.cpp b/demos/main/main.cpp index 3026baeb..10b43b0f 100644 --- a/demos/main/main.cpp +++ b/demos/main/main.cpp @@ -6,7 +6,6 @@ #include "cru/ui/Window.hpp" #include "cru/ui/controls/Button.hpp" #include "cru/ui/controls/FlexLayout.hpp" -#include "cru/ui/controls/StackLayout.hpp" #include "cru/ui/controls/TextBlock.hpp" #include "cru/ui/controls/TextBox.hpp" @@ -27,9 +26,11 @@ int main() { const auto window = Window::CreateOverlapped(); const auto flex_layout = FlexLayout::Create(); + flex_layout->SetFlexDirection(cru::ui::FlexDirection::Vertical); + flex_layout->SetContentMainAlign(cru::ui::FlexCrossAlignment::Center); + flex_layout->SetItemCrossAlign(cru::ui::FlexCrossAlignment::Center); window->SetChild(flex_layout); - flex_layout->SetFlexDirection(cru::ui::FlexDirection::Vertical); const auto text_block = TextBlock::Create(); text_block->SetText(u"Hello World from CruUI!"); diff --git a/include/cru/platform/GraphBase.hpp b/include/cru/platform/GraphBase.hpp index 571b36f7..6700765e 100644 --- a/include/cru/platform/GraphBase.hpp +++ b/include/cru/platform/GraphBase.hpp @@ -18,6 +18,10 @@ struct Point final { constexpr Point(const float x, const float y) : x(x), y(y) {} explicit constexpr Point(const Size& size); + std::u16string ToDebugString() const { + return fmt::format(u"({}, {})", ToUtf16String(x), ToUtf16String(y)); + } + float x = 0; float y = 0; }; diff --git a/include/cru/ui/controls/FlexLayout.hpp b/include/cru/ui/controls/FlexLayout.hpp index 87162569..0ffedba5 100644 --- a/include/cru/ui/controls/FlexLayout.hpp +++ b/include/cru/ui/controls/FlexLayout.hpp @@ -28,6 +28,9 @@ class FlexLayout : public LayoutControl { FlexDirection GetFlexDirection() const; void SetFlexDirection(FlexDirection direction); + FlexCrossAlignment GetItemCrossAlign() const; + void SetItemCrossAlign(FlexCrossAlignment alignment); + FlexChildLayoutData GetChildLayoutData(Control* control); void SetChildLayoutData(Control* control, FlexChildLayoutData data); diff --git a/src/ui/controls/FlexLayout.cpp b/src/ui/controls/FlexLayout.cpp index b7f350dc..05f6999f 100644 --- a/src/ui/controls/FlexLayout.cpp +++ b/src/ui/controls/FlexLayout.cpp @@ -60,6 +60,15 @@ void FlexLayout::SetFlexDirection(FlexDirection direction) { render_object_->SetFlexDirection(direction); } +FlexCrossAlignment FlexLayout::GetItemCrossAlign() const { + return render_object_->GetItemCrossAlign(); +} + +void FlexLayout::SetItemCrossAlign(FlexCrossAlignment alignment) { + if (alignment == GetItemCrossAlign()) return; + render_object_->SetItemCrossAlign(alignment); +} + void FlexLayout::OnAddChild(Control* child, const Index position) { render_object_->AddChild(child->GetRenderObject(), position); } diff --git a/src/ui/render/FlexLayoutRenderObject.cpp b/src/ui/render/FlexLayoutRenderObject.cpp index 1c39cc8f..36a2dcea 100644 --- a/src/ui/render/FlexLayoutRenderObject.cpp +++ b/src/ui/render/FlexLayoutRenderObject.cpp @@ -391,10 +391,11 @@ void FlexLayoutRenderObject::OnLayoutContent(const Rect& content_rect) { const auto cross_align = GetChildLayoutDataList()[i].cross_alignment.value_or( GetItemCrossAlign()); - child->Layout( - Point{content_rect.top + current_main_offset, - CalculateAnchorByAlignment(cross_align, content_rect.left, - content_rect.width, size.width)}); + child->Layout(Point{ + CalculateAnchorByAlignment(cross_align, content_rect.left, + content_rect.width, size.width), + content_rect.top + current_main_offset, + }); current_main_offset += size.height; } } else { @@ -406,9 +407,9 @@ void FlexLayoutRenderObject::OnLayoutContent(const Rect& content_rect) { GetChildLayoutDataList()[i].cross_alignment.value_or( GetItemCrossAlign()); child->Layout( - Point{content_rect.GetBottom() - current_main_offset, - CalculateAnchorByAlignment(cross_align, content_rect.left, - content_rect.width, size.width)}); + Point{CalculateAnchorByAlignment(cross_align, content_rect.left, + content_rect.width, size.width), + content_rect.GetBottom() - current_main_offset}); current_main_offset += size.height; } } diff --git a/src/ui/render/RenderObject.cpp b/src/ui/render/RenderObject.cpp index bc2228d3..57116f93 100644 --- a/src/ui/render/RenderObject.cpp +++ b/src/ui/render/RenderObject.cpp @@ -88,6 +88,10 @@ void RenderObject::Measure(const MeasureRequirement& requirement, } void RenderObject::Layout(const Point& offset) { +#if CRUUI_DEBUG_LAYOUT + log::Debug(u"{} Layout :\noffset: {}", this->GetDebugPathInTree(), + offset.ToDebugString()); +#endif offset_ = offset; OnLayoutCore(); } -- cgit v1.2.3 From 2d4a5df468f8bc13fbb657e010c393365ef79bda Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 10 Nov 2020 15:52:15 +0800 Subject: ... --- include/cru/platform/GraphBase.hpp | 6 ++++++ include/cru/platform/gui/InputMethod.hpp | 3 ++- include/cru/ui/DebugFlags.hpp | 2 +- src/ui/controls/TextBox.cpp | 2 -- src/ui/controls/TextControlService.hpp | 26 +++++++++++++++++++++++++- src/ui/host/LayoutPaintCycler.cpp | 4 ++-- src/win/gui/InputMethod.cpp | 2 +- 7 files changed, 37 insertions(+), 8 deletions(-) (limited to 'include/cru/platform/GraphBase.hpp') diff --git a/include/cru/platform/GraphBase.hpp b/include/cru/platform/GraphBase.hpp index 6700765e..2b40898e 100644 --- a/include/cru/platform/GraphBase.hpp +++ b/include/cru/platform/GraphBase.hpp @@ -22,6 +22,12 @@ struct Point final { return fmt::format(u"({}, {})", ToUtf16String(x), ToUtf16String(y)); } + constexpr Point& operator+=(const Point& other) { + this->x += other.x; + this->y += other.y; + return *this; + } + float x = 0; float y = 0; }; diff --git a/include/cru/platform/gui/InputMethod.hpp b/include/cru/platform/gui/InputMethod.hpp index 53a8d671..9d090eab 100644 --- a/include/cru/platform/gui/InputMethod.hpp +++ b/include/cru/platform/gui/InputMethod.hpp @@ -37,7 +37,8 @@ struct IInputMethodContext : virtual INativeResource { virtual CompositionText GetCompositionText() = 0; - // Set the candidate window lefttop. Use this method to prepare typing. + // Set the candidate window lefttop. Relative to window lefttop. Use this + // method to prepare typing. virtual void SetCandidateWindowPosition(const Point& point) = 0; // Triggered when user starts composition. diff --git a/include/cru/ui/DebugFlags.hpp b/include/cru/ui/DebugFlags.hpp index fceef081..7c600d48 100644 --- a/include/cru/ui/DebugFlags.hpp +++ b/include/cru/ui/DebugFlags.hpp @@ -4,5 +4,5 @@ namespace cru::ui::debug_flags { constexpr bool routed_event = false; constexpr bool layout = false; constexpr bool shortcut = false; -constexpr bool text_service = false; +constexpr bool text_service = true; } // namespace cru::ui::debug_flags diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp index 4a8d6658..6ba6ecb2 100644 --- a/src/ui/controls/TextBox.cpp +++ b/src/ui/controls/TextBox.cpp @@ -10,9 +10,7 @@ namespace cru::ui::controls { using render::BorderRenderObject; -using render::CanvasRenderObject; using render::ScrollRenderObject; -using render::StackLayoutRenderObject; using render::TextRenderObject; TextBox::TextBox() diff --git a/src/ui/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp index 92a66f5e..d50621ea 100644 --- a/src/ui/controls/TextControlService.hpp +++ b/src/ui/controls/TextControlService.hpp @@ -298,6 +298,24 @@ class TextControlService : public Object { 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 void SetupOneHandler(event::RoutedEvent* (Control::*event)(), void (TextControlService::*handler)( @@ -449,6 +467,12 @@ class TextControlService : public Object { 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(); }); } } @@ -481,5 +505,5 @@ class TextControlService : public Object { // nullopt means not selecting std::optional select_down_button_; -}; // namespace cru::ui::controls +}; } // namespace cru::ui::controls diff --git a/src/ui/host/LayoutPaintCycler.cpp b/src/ui/host/LayoutPaintCycler.cpp index 9b319da4..fd581e00 100644 --- a/src/ui/host/LayoutPaintCycler.cpp +++ b/src/ui/host/LayoutPaintCycler.cpp @@ -29,7 +29,7 @@ void LayoutPaintCycler::OnCycle() { host_->Repaint(); } } - layout_dirty_ = true; - paint_dirty_ = true; + layout_dirty_ = false; + paint_dirty_ = false; } } // namespace cru::ui::host diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp index 492f1c2f..0ef9a403 100644 --- a/src/win/gui/InputMethod.cpp +++ b/src/win/gui/InputMethod.cpp @@ -198,7 +198,7 @@ void WinInputMethodContext::SetCandidateWindowPosition(const Point& point) { auto himc = GetHIMC(); ::CANDIDATEFORM form; - form.dwIndex = 1; + form.dwIndex = 0; form.dwStyle = CFS_CANDIDATEPOS; form.ptCurrentPos = native_window_->DipToPixel(point); -- cgit v1.2.3 From da7ad0ff5c5b158be69c6cf9a2c8e9fc9ef2b3cb Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 25 Dec 2020 15:38:18 +0800 Subject: ... --- include/cru/platform/GraphBase.hpp | 2 +- include/cru/ui/controls/TextBlock.hpp | 15 +- include/cru/ui/controls/TextBox.hpp | 12 +- include/cru/ui/controls/TextHostControlService.hpp | 141 ++++++ src/ui/CMakeLists.txt | 5 +- src/ui/controls/TextBlock.cpp | 3 +- src/ui/controls/TextBox.cpp | 3 +- src/ui/controls/TextControlService.hpp | 511 --------------------- src/ui/controls/TextHostControlService.cpp | 458 ++++++++++++++++++ 9 files changed, 621 insertions(+), 529 deletions(-) create mode 100644 include/cru/ui/controls/TextHostControlService.hpp delete mode 100644 src/ui/controls/TextControlService.hpp create mode 100644 src/ui/controls/TextHostControlService.cpp (limited to 'include/cru/platform/GraphBase.hpp') 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 -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 GetTextRenderObject(); - render::ScrollRenderObject* GetScrollRenderObject() { return nullptr; } + gsl::not_null GetTextRenderObject() override; + render::ScrollRenderObject* GetScrollRenderObject() override { + return nullptr; + } private: std::unique_ptr text_render_object_; - std::unique_ptr> service_; + std::unique_ptr 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 @@ -8,7 +10,9 @@ namespace cru::ui::controls { template 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 GetTextRenderObject(); - render::ScrollRenderObject* GetScrollRenderObject(); + gsl::not_null 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 scroll_render_object_; std::unique_ptr text_render_object_; - std::unique_ptr> service_; + std::unique_ptr 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 +#include + +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 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); + + 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 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 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 + void SetupOneHandler(event::RoutedEvent* (Control::*event)(), + void (TextHostControlService::*handler)( + typename event::RoutedEvent::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_; + gsl::not_null 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>(this); + service_ = std::make_unique(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>(this); + service_ = std::make_unique(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 - -namespace cru::ui::controls { -constexpr int k_default_caret_blink_duration = 500; - -// TControl should inherits `Control` and has following methods: -// ``` -// gsl::not_null GetTextRenderObject(); -// render::ScrollRenderObject* GetScrollRenderObject(); -// ``` -template -class TextControlService : public Object { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::controls::TextControlService") - - public: - TextControlService(gsl::not_null 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(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 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 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 - void SetupOneHandler(event::RoutedEvent* (Control::*event)(), - void (TextControlService::*handler)( - typename event::RoutedEvent::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 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 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), + text_host_control_(dynamic_cast(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(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 +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 +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 -- cgit v1.2.3 From 9c2f860ea80310f87b62a2947b4ddea5e7d85587 Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 24 Feb 2021 23:01:15 +0800 Subject: feat: Scroll bar. Only collapse state. --- include/cru/platform/GraphBase.hpp | 6 ++ include/cru/ui/render/RenderObject.hpp | 16 +-- include/cru/ui/render/ScrollBarDelegate.hpp | 142 ++++++++++++++++++++++++++ include/cru/ui/render/ScrollRenderObject.hpp | 8 +- src/ui/CMakeLists.txt | 2 + src/ui/render/RenderObject.cpp | 14 +++ src/ui/render/ScrollBarDelegate.cpp | 145 +++++++++++++++++++++++++++ src/ui/render/ScrollRenderObject.cpp | 16 +++ 8 files changed, 342 insertions(+), 7 deletions(-) create mode 100644 include/cru/ui/render/ScrollBarDelegate.hpp create mode 100644 src/ui/render/ScrollBarDelegate.cpp (limited to 'include/cru/platform/GraphBase.hpp') diff --git a/include/cru/platform/GraphBase.hpp b/include/cru/platform/GraphBase.hpp index b580ad31..6bf2736f 100644 --- a/include/cru/platform/GraphBase.hpp +++ b/include/cru/platform/GraphBase.hpp @@ -316,6 +316,12 @@ struct Color { (hex >> 24) & mask); } + constexpr Color WithAlpha(std::uint8_t new_alpha) const { + auto result = *this; + result.alpha = new_alpha; + return result; + } + std::uint8_t red; std::uint8_t green; std::uint8_t blue; diff --git a/include/cru/ui/render/RenderObject.hpp b/include/cru/ui/render/RenderObject.hpp index 2b166efc..8bcd4c62 100644 --- a/include/cru/ui/render/RenderObject.hpp +++ b/include/cru/ui/render/RenderObject.hpp @@ -2,6 +2,7 @@ #include "Base.hpp" #include "MeasureRequirement.hpp" +#include "cru/common/Base.hpp" #include "cru/common/Event.hpp" #include "cru/ui/Base.hpp" @@ -63,9 +64,7 @@ class RenderObject : public Object { ~RenderObject() override = default; controls::Control* GetAttachedControl() const { return control_; } - void SetAttachedControl(controls::Control* new_control) { - control_ = new_control; - } + void SetAttachedControl(controls::Control* new_control); host::WindowHost* GetWindowHost() const { return window_host_; } @@ -76,6 +75,7 @@ class RenderObject : public Object { void AddChild(RenderObject* render_object, Index position); void RemoveChild(Index position); + RenderObject* GetFirstChild() const; void TraverseDescendants(const std::function& action); // Offset from parent's lefttop to lefttop of this render object. Margin is @@ -131,6 +131,9 @@ class RenderObject : public Object { // This will set offset of this render object and call OnLayoutCore. void Layout(const Point& offset); + virtual Rect GetPaddingRect() const; + virtual Rect GetContentRect() const; + void Draw(platform::graphics::IPainter* painter); // Param point must be relative the lefttop of render object including margin. @@ -201,10 +204,11 @@ class RenderObject : public Object { // Lefttop of content_rect should be added when calculated children's offset. virtual void OnLayoutContent(const Rect& content_rect) = 0; - virtual void OnAfterLayout(); + virtual void OnAttachedControlChanged(controls::Control* control) { + CRU_UNUSED(control) + } - virtual Rect GetPaddingRect() const; - virtual Rect GetContentRect() const; + virtual void OnAfterLayout(); private: void SetParent(RenderObject* new_parent); diff --git a/include/cru/ui/render/ScrollBarDelegate.hpp b/include/cru/ui/render/ScrollBarDelegate.hpp new file mode 100644 index 00000000..e5c63f6d --- /dev/null +++ b/include/cru/ui/render/ScrollBarDelegate.hpp @@ -0,0 +1,142 @@ +#pragma once +#include "Base.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/Event.hpp" +#include "cru/platform/graphics/Base.hpp" +#include "cru/platform/graphics/Painter.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/ui/controls/Control.hpp" + +#include +#include +#include + +namespace cru::ui::render { +class ScrollRenderObject; + +enum class ScrollBarAreaKind { + UpArrow, // Line up + DownArrow, // Line down + UpThumb, // Page up + DownThumb, // Page down + Thumb +}; + +class ScrollBar : public Object { + public: + explicit ScrollBar(gsl::not_null render_object); + + CRU_DELETE_COPY(ScrollBar) + CRU_DELETE_MOVE(ScrollBar) + + ~ScrollBar() override = default; + + public: + bool IsEnabled() const { return is_enabled_; } + void SetEnabled(bool value); + + void Draw(platform::graphics::IPainter* painter); + + virtual std::optional HitTest(const Point& point) = 0; + + IEvent* ScrollAttemptEvent() { return &scroll_attempt_event_; } + + void InstallHandlers(controls::Control* control); + void UninstallHandlers() { InstallHandlers(nullptr); } + + gsl::not_null> + GetCollapseThumbBrush() const; + + protected: + virtual void OnDraw(platform::graphics::IPainter* painter, bool expand) = 0; + + protected: + gsl::not_null render_object_; + + private: + bool is_enabled_ = true; + + bool is_expanded_ = false; + + std::shared_ptr collapse_thumb_brush_; + + EventRevokerListGuard event_guard_; + + Event scroll_attempt_event_; +}; + +class HorizontalScrollBar : public ScrollBar { + public: + explicit HorizontalScrollBar( + gsl::not_null render_object); + + CRU_DELETE_COPY(HorizontalScrollBar) + CRU_DELETE_MOVE(HorizontalScrollBar) + + ~HorizontalScrollBar() override = default; + + public: + std::optional HitTest(const Point& point) override; + + protected: + void OnDraw(platform::graphics::IPainter* painter, bool expand) override; +}; + +class VerticalScrollBar : public ScrollBar { + public: + explicit VerticalScrollBar(gsl::not_null render_object); + + CRU_DELETE_COPY(VerticalScrollBar) + CRU_DELETE_MOVE(VerticalScrollBar) + + ~VerticalScrollBar() override = default; + + public: + std::optional HitTest(const Point& point) override; + + protected: + void OnDraw(platform::graphics::IPainter* painter, bool expand) override; +}; + +struct ScrollBarScrollAttemptArgs { + float x_offset; + float y_offset; +}; + +// A delegate to draw scrollbar and register related events. +class ScrollBarDelegate : public Object { + public: + explicit ScrollBarDelegate(gsl::not_null render_object); + + CRU_DELETE_COPY(ScrollBarDelegate) + CRU_DELETE_MOVE(ScrollBarDelegate) + + ~ScrollBarDelegate() override = default; + + public: + bool IsHorizontalBarEnabled() const { return horizontal_bar_.IsEnabled(); } + void SetHorizontalBarEnabled(bool value) { + horizontal_bar_.SetEnabled(value); + } + + bool IsVerticalBarEnabled() const { return horizontal_bar_.IsEnabled(); } + void SetVerticalBarEnabled(bool value) { horizontal_bar_.SetEnabled(value); } + + IEvent* ScrollAttemptEvent() { + return &scroll_attempt_event_; + } + + void DrawScrollBar(platform::graphics::IPainter* painter); + + void InstallHandlers(controls::Control* control); + void UninstallHandlers() { InstallHandlers(nullptr); } + + private: + gsl::not_null render_object_; + + HorizontalScrollBar horizontal_bar_; + VerticalScrollBar vertical_bar_; + + Event scroll_attempt_event_; +}; +} // namespace cru::ui::render diff --git a/include/cru/ui/render/ScrollRenderObject.hpp b/include/cru/ui/render/ScrollRenderObject.hpp index 3cc0e4c4..5a431527 100644 --- a/include/cru/ui/render/ScrollRenderObject.hpp +++ b/include/cru/ui/render/ScrollRenderObject.hpp @@ -2,7 +2,9 @@ #include "RenderObject.hpp" #include "cru/platform/graphics/util/Painter.hpp" +#include "cru/ui/render/ScrollBarDelegate.hpp" +#include #include namespace cru::ui::render { @@ -16,7 +18,7 @@ namespace cru::ui::render { // Or layout by scroll state. class ScrollRenderObject : public RenderObject { public: - ScrollRenderObject() : RenderObject(ChildMode::Single) {} + ScrollRenderObject(); CRU_DELETE_COPY(ScrollRenderObject) CRU_DELETE_MOVE(ScrollRenderObject) @@ -54,7 +56,11 @@ class ScrollRenderObject : public RenderObject { const MeasureSize& preferred_size) override; void OnLayoutContent(const Rect& content_rect) override; + void OnAttachedControlChanged(controls::Control* control) override; + private: Point scroll_offset_; + + std::unique_ptr scroll_bar_delegate_; }; } // namespace cru::ui::render diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index d1c1e830..6153bc07 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(cru_ui STATIC render/FlexLayoutRenderObject.cpp render/LayoutHelper.cpp render/RenderObject.cpp + render/ScrollBarDelegate.cpp render/ScrollRenderObject.cpp render/StackLayoutRenderObject.cpp render/TextRenderObject.cpp @@ -77,6 +78,7 @@ target_sources(cru_ui PUBLIC ${CRU_UI_INCLUDE_DIR}/render/LayoutRenderObject.hpp ${CRU_UI_INCLUDE_DIR}/render/MeasureRequirement.hpp ${CRU_UI_INCLUDE_DIR}/render/RenderObject.hpp + ${CRU_UI_INCLUDE_DIR}/render/ScrollBarDelegate.hpp ${CRU_UI_INCLUDE_DIR}/render/ScrollRenderObject.hpp ${CRU_UI_INCLUDE_DIR}/render/StackLayoutRenderObject.hpp ${CRU_UI_INCLUDE_DIR}/render/TextRenderObject.hpp diff --git a/src/ui/render/RenderObject.cpp b/src/ui/render/RenderObject.cpp index a40ce9b8..7cf750cd 100644 --- a/src/ui/render/RenderObject.cpp +++ b/src/ui/render/RenderObject.cpp @@ -11,6 +11,11 @@ #include namespace cru::ui::render { +void RenderObject::SetAttachedControl(controls::Control* new_control) { + control_ = new_control; + OnAttachedControlChanged(new_control); +} + void RenderObject::AddChild(RenderObject* render_object, const Index position) { Expects(child_mode_ != ChildMode::None); Expects(!(child_mode_ == ChildMode::Single && children_.size() > 0)); @@ -41,6 +46,15 @@ void RenderObject::RemoveChild(const Index position) { OnRemoveChild(removed_child, position); } +RenderObject* RenderObject::GetFirstChild() const { + const auto& children = GetChildren(); + if (children.empty()) { + return nullptr; + } else { + return children.front(); + } +} + void RenderObject::TraverseDescendants( const std::function& action) { action(this); diff --git a/src/ui/render/ScrollBarDelegate.cpp b/src/ui/render/ScrollBarDelegate.cpp new file mode 100644 index 00000000..2814c567 --- /dev/null +++ b/src/ui/render/ScrollBarDelegate.cpp @@ -0,0 +1,145 @@ +#include "cru/ui/render/ScrollBarDelegate.hpp" + +#include "../Helper.hpp" +#include "cru/common/Base.hpp" +#include "cru/platform/GraphBase.hpp" +#include "cru/platform/graphics/Factory.hpp" +#include "cru/platform/graphics/Painter.hpp" +#include "cru/ui/render/ScrollRenderObject.hpp" + +#include +#include + +namespace cru::ui::render { +constexpr float kScrollBarCollapseThumbWidth = 2; + +ScrollBar::ScrollBar(gsl::not_null render_object) + : render_object_(render_object) { + // TODO: Use theme resource and delete this. + auto collapse_thumb_brush = GetUiApplication() + ->GetInstance() + ->GetGraphFactory() + ->CreateSolidColorBrush(); + collapse_thumb_brush->SetColor(colors::gray.WithAlpha(128)); + collapse_thumb_brush_ = std::move(collapse_thumb_brush); +} + +void ScrollBar::SetEnabled(bool value) { + CRU_UNUSED(value) + // TODO: Implement this. +} + +void ScrollBar::Draw(platform::graphics::IPainter* painter) { + if (is_enabled_) { + OnDraw(painter, is_expanded_); + } +} + +void ScrollBar::InstallHandlers(controls::Control* control) { + CRU_UNUSED(control); + // TODO: Implement this. +} + +gsl::not_null> +ScrollBar::GetCollapseThumbBrush() const { + // TODO: Read theme resource. + return collapse_thumb_brush_; +} + +HorizontalScrollBar::HorizontalScrollBar( + gsl::not_null render_object) + : ScrollBar(render_object) {} + +std::optional HorizontalScrollBar::HitTest( + const Point& point) { + // TODO: Implement this. + CRU_UNUSED(point); + return std::nullopt; +} + +void HorizontalScrollBar::OnDraw(platform::graphics::IPainter* painter, + bool expand) { + const auto child = render_object_->GetFirstChild(); + if (child == nullptr) return; + + const auto view_rect = render_object_->GetViewRect(); + const auto padding_rect = render_object_->GetPaddingRect(); + const auto child_size = child->GetSize(); + + if (view_rect.width >= child_size.width) return; + + const float start_percentage = view_rect.left / child_size.width; + const float length_percentage = view_rect.width / child_size.width; + // const float end_percentage = start_percentage + length_percentage; + + if (expand) { + // TODO: Implement this. + } else { + Rect thumb_rect{padding_rect.left + padding_rect.width * start_percentage, + padding_rect.GetBottom() - kScrollBarCollapseThumbWidth, + padding_rect.width * length_percentage, + kScrollBarCollapseThumbWidth}; + painter->FillRectangle(thumb_rect, GetCollapseThumbBrush().get().get()); + } +} + +VerticalScrollBar::VerticalScrollBar( + gsl::not_null render_object) + : ScrollBar(render_object) {} + +std::optional VerticalScrollBar::HitTest( + const Point& point) { + // TODO: Implement this. + CRU_UNUSED(point); + return std::nullopt; +} + +void VerticalScrollBar::OnDraw(platform::graphics::IPainter* painter, + bool expand) { + const auto child = render_object_->GetFirstChild(); + if (child == nullptr) return; + + const auto view_rect = render_object_->GetViewRect(); + const auto padding_rect = render_object_->GetPaddingRect(); + const auto child_size = child->GetSize(); + + if (view_rect.height >= child_size.height) return; + + const float start_percentage = view_rect.top / child_size.height; + const float length_percentage = view_rect.height / child_size.height; + // const float end_percentage = start_percentage + length_percentage; + + if (expand) { + // TODO: Implement this. + } else { + Rect thumb_rect{padding_rect.GetRight() - kScrollBarCollapseThumbWidth, + padding_rect.top + padding_rect.height * start_percentage, + kScrollBarCollapseThumbWidth, + padding_rect.height * length_percentage}; + painter->FillRectangle(thumb_rect, GetCollapseThumbBrush().get().get()); + } +} + +ScrollBarDelegate::ScrollBarDelegate( + gsl::not_null render_object) + : render_object_(render_object), + horizontal_bar_(render_object), + vertical_bar_(render_object) { + horizontal_bar_.ScrollAttemptEvent()->AddHandler([this](float offset) { + this->scroll_attempt_event_.Raise({offset, 0}); + }); + vertical_bar_.ScrollAttemptEvent()->AddHandler([this](float offset) { + this->scroll_attempt_event_.Raise({0, offset}); + }); +} + +void ScrollBarDelegate::DrawScrollBar(platform::graphics::IPainter* painter) { + horizontal_bar_.Draw(painter); + vertical_bar_.Draw(painter); +} + +void ScrollBarDelegate::InstallHandlers(controls::Control* control) { + horizontal_bar_.InstallHandlers(control); + vertical_bar_.InstallHandlers(control); +} +} // namespace cru::ui::render diff --git a/src/ui/render/ScrollRenderObject.cpp b/src/ui/render/ScrollRenderObject.cpp index 5b9cb627..18b0adbf 100644 --- a/src/ui/render/ScrollRenderObject.cpp +++ b/src/ui/render/ScrollRenderObject.cpp @@ -2,8 +2,11 @@ #include "cru/platform/graphics/Painter.hpp" #include "cru/platform/graphics/util/Painter.hpp" +#include "cru/ui/controls/Control.hpp" +#include "cru/ui/render/ScrollBarDelegate.hpp" #include +#include namespace cru::ui::render { namespace { @@ -31,6 +34,10 @@ Point CoerceScroll(const Point& scroll_offset, const Size& content_size, } } // namespace +ScrollRenderObject::ScrollRenderObject() : RenderObject(ChildMode::Single) { + scroll_bar_delegate_ = std::make_unique(this); +} + RenderObject* ScrollRenderObject::HitTest(const Point& point) { if (const auto child = GetSingleChild()) { const auto offset = child->GetOffset(); @@ -52,6 +59,7 @@ void ScrollRenderObject::OnDrawCore(platform::graphics::IPainter* painter) { [child](platform::graphics::IPainter* p) { child->Draw(p); }); painter->PopLayer(); } + scroll_bar_delegate_->DrawScrollBar(painter); } Point ScrollRenderObject::GetScrollOffset() { @@ -141,4 +149,12 @@ void ScrollRenderObject::OnLayoutContent(const Rect& content_rect) { child->Layout(content_rect.GetLeftTop() - GetScrollOffset()); } } + +void ScrollRenderObject::OnAttachedControlChanged(controls::Control* control) { + if (control) { + scroll_bar_delegate_->InstallHandlers(control); + } else { + scroll_bar_delegate_->UninstallHandlers(); + } +} } // namespace cru::ui::render -- cgit v1.2.3