From 2d4a5df468f8bc13fbb657e010c393365ef79bda Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 10 Nov 2020 15:52:15 +0800 Subject: ... --- src/ui/controls/TextBox.cpp | 2 -- 1 file changed, 2 deletions(-) (limited to 'src/ui/controls/TextBox.cpp') 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() -- cgit v1.2.3 From b29fb11be2f043a3438a50d8942b4ad7d2af0034 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 3 Dec 2020 22:44:57 +0800 Subject: ... --- include/cru/common/ClonablePtr.hpp | 7 ++- include/cru/common/Event.hpp | 4 +- include/cru/ui/Base.hpp | 12 ++-- include/cru/ui/UiManager.hpp | 5 +- include/cru/ui/controls/Base.hpp | 22 +------ include/cru/ui/controls/Button.hpp | 5 -- include/cru/ui/controls/Control.hpp | 5 ++ include/cru/ui/controls/TextBox.hpp | 14 +---- include/cru/ui/helper/ClickDetector.hpp | 2 +- include/cru/ui/render/BorderRenderObject.hpp | 2 - include/cru/ui/style/Condition.hpp | 42 +++++++++++++ include/cru/ui/style/StyleRuleSet.hpp | 55 +++++++++++++++++ include/cru/ui/style/Styler.hpp | 7 ++- src/ui/UiManager.cpp | 91 ++++++++++++++++------------ src/ui/controls/Button.cpp | 19 +----- src/ui/controls/Control.cpp | 7 +++ src/ui/controls/TextBox.cpp | 29 +-------- src/ui/render/BorderRenderObject.cpp | 9 --- src/ui/style/Condition.cpp | 10 +++ src/ui/style/StyleRuleSet.cpp | 58 ++++++++++++++++++ src/win/gui/Window.cpp | 2 +- 21 files changed, 262 insertions(+), 145 deletions(-) (limited to 'src/ui/controls/TextBox.cpp') diff --git a/include/cru/common/ClonablePtr.hpp b/include/cru/common/ClonablePtr.hpp index 47a1d3bd..5e4b80c9 100644 --- a/include/cru/common/ClonablePtr.hpp +++ b/include/cru/common/ClonablePtr.hpp @@ -8,13 +8,16 @@ namespace cru { template class ClonablePtr { + template + friend class ClonablePtr; + public: using element_type = typename std::unique_ptr::element_type; using pointer = typename std::unique_ptr::pointer; ClonablePtr() = default; ClonablePtr(std::nullptr_t) noexcept : ptr_(nullptr) {} - ClonablePtr(pointer p) noexcept : ptr_(p) {} + explicit ClonablePtr(pointer p) noexcept : ptr_(p) {} ClonablePtr(std::unique_ptr&& p) noexcept : ptr_(std::move(p)) {} template (other.ptr->Clone()); + ptr_ = std::unique_ptr(other.ptr_->Clone()); } return *this; } diff --git a/include/cru/common/Event.hpp b/include/cru/common/Event.hpp index 59502527..7f7b4dd4 100644 --- a/include/cru/common/Event.hpp +++ b/include/cru/common/Event.hpp @@ -98,7 +98,7 @@ struct IBaseEvent { using SpyOnlyHandler = std::function; public: - virtual EventRevoker AddHandler(SpyOnlyHandler handler) = 0; + virtual EventRevoker AddSpyOnlyHandler(SpyOnlyHandler handler) = 0; }; // Provides an interface of event. @@ -147,7 +147,7 @@ class Event : public details::EventBase, public IEvent { CRU_DEFAULT_MOVE(Event) ~Event() = default; - EventRevoker AddHandler(SpyOnlyHandler handler) override { + EventRevoker AddSpyOnlyHandler(SpyOnlyHandler handler) override { const auto token = current_token_++; this->handler_data_list_.emplace_back(token, std::move(handler)); return CreateRevoker(token); diff --git a/include/cru/ui/Base.hpp b/include/cru/ui/Base.hpp index 8595258d..57beb723 100644 --- a/include/cru/ui/Base.hpp +++ b/include/cru/ui/Base.hpp @@ -40,6 +40,10 @@ namespace render { class RenderObject; } +namespace style { +class StyleRuleSet; +} + //-------------------- region: basic types -------------------- namespace internal { constexpr int align_start = 0; @@ -87,14 +91,6 @@ inline bool operator!=(const CornerRadius& left, const CornerRadius& right) { return !(left == right); } -struct BorderStyle { - std::shared_ptr border_brush; - Thickness border_thickness; - CornerRadius border_radius; - std::shared_ptr foreground_brush; - std::shared_ptr background_brush; -}; - class CanvasPaintEventArgs { public: CanvasPaintEventArgs(platform::graphics::IPainter* painter, diff --git a/include/cru/ui/UiManager.hpp b/include/cru/ui/UiManager.hpp index 64599d99..e747fcd2 100644 --- a/include/cru/ui/UiManager.hpp +++ b/include/cru/ui/UiManager.hpp @@ -2,6 +2,7 @@ #include "Base.hpp" #include "controls/Base.hpp" +#include "style/StyleRuleSet.hpp" #include #include @@ -13,8 +14,8 @@ struct ThemeResources { std::shared_ptr text_brush; std::shared_ptr text_selection_brush; std::shared_ptr caret_brush; - controls::ButtonStyle button_style; - controls::TextBoxBorderStyle text_box_border_style; + style::StyleRuleSet button_style; + style::StyleRuleSet text_box_style; }; class UiManager : public Object { diff --git a/include/cru/ui/controls/Base.hpp b/include/cru/ui/controls/Base.hpp index 82c31d1e..7c85cdb2 100644 --- a/include/cru/ui/controls/Base.hpp +++ b/include/cru/ui/controls/Base.hpp @@ -1,24 +1,4 @@ #pragma once #include "../Base.hpp" -namespace cru::ui::controls { -using ButtonStateStyle = ui::BorderStyle; - -struct ButtonStyle { - // corresponds to ClickState::None - ButtonStateStyle normal; - // corresponds to ClickState::Hover - ButtonStateStyle hover; - // corresponds to ClickState::Press - ButtonStateStyle press; - // corresponds to ClickState::PressInactive - ButtonStateStyle press_cancel; -}; - -struct TextBoxBorderStyle { - ui::BorderStyle normal; - ui::BorderStyle hover; - ui::BorderStyle focus; - ui::BorderStyle focus_hover; -}; -} // namespace cru::ui::controls +namespace cru::ui::controls {} // namespace cru::ui::controls diff --git a/include/cru/ui/controls/Button.hpp b/include/cru/ui/controls/Button.hpp index 7299c146..1c9b1216 100644 --- a/include/cru/ui/controls/Button.hpp +++ b/include/cru/ui/controls/Button.hpp @@ -41,14 +41,9 @@ class Button : public ContentControl, void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) override; - const ButtonStyle& GetStyle() const { return style_; } - void SetStyle(ButtonStyle style); - private: std::unique_ptr render_object_{}; - ButtonStyle style_; - helper::ClickDetector click_detector_; }; } // namespace cru::ui::controls diff --git a/include/cru/ui/controls/Control.hpp b/include/cru/ui/controls/Control.hpp index 96aad2bd..0d34bc63 100644 --- a/include/cru/ui/controls/Control.hpp +++ b/include/cru/ui/controls/Control.hpp @@ -66,6 +66,9 @@ class Control : public Object { // null to unset void SetCursor(std::shared_ptr cursor); + public: + style::StyleRuleSet* GetStyleRuleSet(); + //*************** region: events *************** public: // Raised when mouse enter the control. Even when the control itself captures @@ -147,5 +150,7 @@ class Control : public Object { bool is_mouse_over_ = false; std::shared_ptr cursor_ = nullptr; + + std::unique_ptr style_rule_set_; }; } // namespace cru::ui::controls diff --git a/include/cru/ui/controls/TextBox.hpp b/include/cru/ui/controls/TextBox.hpp index 91d38c61..75e7cb65 100644 --- a/include/cru/ui/controls/TextBox.hpp +++ b/include/cru/ui/controls/TextBox.hpp @@ -1,5 +1,6 @@ #pragma once #include "NoChildControl.hpp" +#include "IBorderControl.hpp" #include @@ -7,7 +8,7 @@ namespace cru::ui::controls { template class TextControlService; -class TextBox : public NoChildControl { +class TextBox : public NoChildControl, public IBorderControl { public: static constexpr std::u16string_view control_type = u"TextBox"; @@ -29,22 +30,13 @@ class TextBox : public NoChildControl { gsl::not_null GetTextRenderObject(); render::ScrollRenderObject* GetScrollRenderObject(); - const TextBoxBorderStyle& GetBorderStyle(); - void SetBorderStyle(TextBoxBorderStyle border_style); - - protected: - void OnMouseHoverChange(bool newHover) override; - - private: - void UpdateBorderStyle(); + void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) override; private: std::unique_ptr border_render_object_; std::unique_ptr scroll_render_object_; std::unique_ptr text_render_object_; - TextBoxBorderStyle border_style_; - std::unique_ptr> service_; }; } // namespace cru::ui::controls diff --git a/include/cru/ui/helper/ClickDetector.hpp b/include/cru/ui/helper/ClickDetector.hpp index 0df77c60..b58297b1 100644 --- a/include/cru/ui/helper/ClickDetector.hpp +++ b/include/cru/ui/helper/ClickDetector.hpp @@ -71,7 +71,7 @@ class ClickDetector : public Object { private: controls::Control* control_; - ClickState state_; + ClickState state_ = ClickState::None; bool enable_ = true; MouseButton trigger_button_ = mouse_buttons::left | mouse_buttons::right; diff --git a/include/cru/ui/render/BorderRenderObject.hpp b/include/cru/ui/render/BorderRenderObject.hpp index ec0bd52b..3d4f4dad 100644 --- a/include/cru/ui/render/BorderRenderObject.hpp +++ b/include/cru/ui/render/BorderRenderObject.hpp @@ -64,8 +64,6 @@ class BorderRenderObject : public RenderObject { InvalidatePaint(); } - void SetBorderStyle(const BorderStyle& style); - void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style); RenderObject* HitTest(const Point& point) override; diff --git a/include/cru/ui/style/Condition.hpp b/include/cru/ui/style/Condition.hpp index 13ab7764..d5cf16f2 100644 --- a/include/cru/ui/style/Condition.hpp +++ b/include/cru/ui/style/Condition.hpp @@ -21,6 +21,21 @@ class Condition : public Object { virtual Condition* Clone() const = 0; }; +class NoCondition : public Condition { + public: + static ClonablePtr Create() { + return ClonablePtr(new NoCondition); + }; + + std::vector ChangeOn(controls::Control*) const override { + return {}; + } + + bool Judge(controls::Control*) const override { return true; } + + NoCondition* Clone() const override { return new NoCondition; } +}; + class CompoundCondition : public Condition { public: explicit CompoundCondition(std::vector> conditions); @@ -51,6 +66,10 @@ class OrCondition : public CompoundCondition { class FocusCondition : public Condition { public: + static ClonablePtr Create(bool has_focus) { + return ClonablePtr(new FocusCondition(has_focus)); + } + explicit FocusCondition(bool has_focus); std::vector ChangeOn(controls::Control* control) const override; @@ -64,8 +83,31 @@ class FocusCondition : public Condition { bool has_focus_; }; +class HoverCondition : public Condition { + public: + static ClonablePtr Create(bool hover) { + return ClonablePtr(new HoverCondition(hover)); + } + + explicit HoverCondition(bool hover) : hover_(hover) {} + + std::vector ChangeOn(controls::Control* control) const override; + bool Judge(controls::Control* control) const override; + + HoverCondition* Clone() const override { return new HoverCondition(hover_); } + + private: + bool hover_; +}; + class ClickStateCondition : public Condition { public: + static ClonablePtr Create( + helper::ClickState click_state) { + return ClonablePtr( + new ClickStateCondition(click_state)); + } + explicit ClickStateCondition(helper::ClickState click_state); std::vector ChangeOn(controls::Control* control) const override; diff --git a/include/cru/ui/style/StyleRuleSet.hpp b/include/cru/ui/style/StyleRuleSet.hpp index e69de29b..3ec71730 100644 --- a/include/cru/ui/style/StyleRuleSet.hpp +++ b/include/cru/ui/style/StyleRuleSet.hpp @@ -0,0 +1,55 @@ +#pragma once +#include "StyleRule.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/Event.hpp" +#include "gsl/gsl_assert" + +namespace cru::ui::style { +class StyleRuleSet : public Object { + public: + StyleRuleSet() : control_(nullptr) {} + explicit StyleRuleSet(controls::Control* control) : control_(control) {} + + CRU_DELETE_COPY(StyleRuleSet) + CRU_DELETE_MOVE(StyleRuleSet) + + ~StyleRuleSet() override = default; + + public: + gsl::index GetSize() const { return static_cast(rules_.size()); } + const std::vector& GetRules() const { return rules_; } + + void AddStyleRule(StyleRule rule) { + AddStyleRule(std::move(rule), GetSize()); + } + + void AddStyleRule(StyleRule rule, gsl::index index); + + template + void AddStyleRuleRange(Iter start, Iter end, gsl::index index) { + Expects(index >= 0 && index <= GetSize()); + rules_.insert(rules_.cbegin() + index, std::move(start), std::move(end)); + UpdateChangeListener(); + UpdateStyle(); + } + + void RemoveStyleRule(gsl::index index, gsl::index count = 1); + + void Clear() { RemoveStyleRule(0, GetSize()); } + + void Set(const StyleRuleSet& other); + + const StyleRule& operator[](gsl::index index) const { return rules_[index]; } + + private: + void UpdateChangeListener(); + void UpdateStyle(); + + private: + controls::Control* control_; + + std::vector rules_; + + EventRevokerListGuard guard_; +}; +} // namespace cru::ui::style diff --git a/include/cru/ui/style/Styler.hpp b/include/cru/ui/style/Styler.hpp index 2aece114..10b169b1 100644 --- a/include/cru/ui/style/Styler.hpp +++ b/include/cru/ui/style/Styler.hpp @@ -2,19 +2,24 @@ #include "../Base.hpp" #include "ApplyBorderStyleInfo.hpp" #include "cru/common/Base.hpp" +#include "cru/common/ClonablePtr.hpp" #include namespace cru::ui::style { class Styler : public Object { public: - virtual void Apply(controls::Control* control) const; + virtual void Apply(controls::Control* control) const = 0; virtual Styler* Clone() const = 0; }; class BorderStyler : public Styler { public: + static ClonablePtr Create(ApplyBorderStyleInfo style) { + return ClonablePtr(new BorderStyler(std::move(style))); + } + explicit BorderStyler(ApplyBorderStyleInfo style); void Apply(controls::Control* control) const override; diff --git a/src/ui/UiManager.cpp b/src/ui/UiManager.cpp index 62995f86..bb7f5841 100644 --- a/src/ui/UiManager.cpp +++ b/src/ui/UiManager.cpp @@ -5,9 +5,14 @@ #include "cru/platform/graphics/Factory.hpp" #include "cru/platform/graphics/Font.hpp" #include "cru/platform/gui/UiApplication.hpp" +#include "cru/ui/style/ApplyBorderStyleInfo.hpp" +#include "cru/ui/style/Condition.hpp" +#include "cru/ui/style/Styler.hpp" namespace cru::ui { using namespace cru::platform::graphics; +using namespace cru::ui::style; +using namespace cru::ui::helper; namespace { std::unique_ptr CreateSolidColorBrush(IGraphFactory* factory, @@ -35,49 +40,59 @@ UiManager::UiManager() { theme_resource_.default_font = factory->CreateFont(theme_resource_.default_font_family, 24.0f); - const auto black_brush = std::shared_ptr( - CreateSolidColorBrush(factory, colors::black)); + const auto black_brush = + std::shared_ptr( + CreateSolidColorBrush(factory, colors::black)); theme_resource_.text_brush = black_brush; theme_resource_.text_selection_brush = CreateSolidColorBrush(factory, colors::skyblue); theme_resource_.caret_brush = black_brush; - theme_resource_.button_style.normal.border_brush = - CreateSolidColorBrush(factory, Color::FromHex(0x00bfff)); - theme_resource_.button_style.hover.border_brush = - CreateSolidColorBrush(factory, Color::FromHex(0x47d1ff)); - theme_resource_.button_style.press.border_brush = - CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff)); - theme_resource_.button_style.press_cancel.border_brush = - CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff)); - - theme_resource_.button_style.normal.border_thickness = - theme_resource_.button_style.hover.border_thickness = - theme_resource_.button_style.press.border_thickness = - theme_resource_.button_style.press_cancel.border_thickness = - Thickness(3); - - theme_resource_.button_style.normal.border_radius = - theme_resource_.button_style.hover.border_radius = - theme_resource_.button_style.press.border_radius = - theme_resource_.button_style.press_cancel.border_radius = - CornerRadius({5, 5}); - - theme_resource_.text_box_border_style.normal.border_brush = - CreateSolidColorBrush(factory, Color::FromHex(0xced4da)); - theme_resource_.text_box_border_style.normal.border_radius = CornerRadius(5); - theme_resource_.text_box_border_style.normal.border_thickness = Thickness(1); - - theme_resource_.text_box_border_style.hover = - theme_resource_.text_box_border_style.normal; - - theme_resource_.text_box_border_style.focus.border_brush = - CreateSolidColorBrush(factory, Color::FromHex(0x495057)); - theme_resource_.text_box_border_style.focus.border_radius = CornerRadius(5); - theme_resource_.text_box_border_style.focus.border_thickness = Thickness(1); - - theme_resource_.text_box_border_style.focus_hover = - theme_resource_.text_box_border_style.focus; + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::None), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x00bfff)), + Thickness(3), CornerRadius(5), nullptr, nullptr}), + u"DefaultButtonNormal"}); + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::Hover), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x47d1ff)), + Thickness(3), CornerRadius(5), nullptr, nullptr}), + u"DefaultButtonHover"}); + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::Press), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff)), + Thickness(3), CornerRadius(5), nullptr, nullptr}), + u"DefaultButtonPress"}); + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::PressInactive), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff)), + Thickness(3), CornerRadius(5), nullptr, nullptr}), + u"DefaultButtonPressInactive"}); + + theme_resource_.text_box_style.AddStyleRule( + {HoverCondition::Create(false), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0xced4da)), + Thickness(1), CornerRadius(5), nullptr, nullptr}), + u"DefaultTextBoxNormal"}); + + theme_resource_.text_box_style.AddStyleRule( + {HoverCondition::Create(true), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0xced4da)), + Thickness(1), CornerRadius(5), nullptr, nullptr}), + u"DefaultTextBoxHover"}); + + theme_resource_.text_box_style.AddStyleRule( + {FocusCondition::Create(true), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x495057)), + Thickness(1), CornerRadius(5), nullptr, nullptr}), + u"DefaultTextBoxHover"}); } UiManager::~UiManager() = default; diff --git a/src/ui/controls/Button.cpp b/src/ui/controls/Button.cpp index 6f19e6b9..7858eadb 100644 --- a/src/ui/controls/Button.cpp +++ b/src/ui/controls/Button.cpp @@ -13,50 +13,37 @@ namespace cru::ui::controls { using cru::platform::gui::SystemCursorType; namespace { -void Set(render::BorderRenderObject* o, const ButtonStateStyle& s) { - o->SetBorderBrush(s.border_brush); - o->SetBorderThickness(s.border_thickness); - o->SetBorderRadius(s.border_radius); - o->SetForegroundBrush(s.foreground_brush); - o->SetBackgroundBrush(s.background_brush); -} - std::shared_ptr GetSystemCursor(SystemCursorType type) { return GetUiApplication()->GetCursorManager()->GetSystemCursor(type); } } // namespace Button::Button() : click_detector_(this) { - style_ = UiManager::GetInstance()->GetThemeResources()->button_style; - render_object_ = std::make_unique(); render_object_->SetAttachedControl(this); SetContainerRenderObject(render_object_.get()); - - Set(render_object_.get(), style_.normal); render_object_->SetBorderEnabled(true); click_detector_.StateChangeEvent()->AddHandler( [this](const helper::ClickState& state) { switch (state) { case helper::ClickState::None: - Set(render_object_.get(), style_.normal); SetCursor(GetSystemCursor(SystemCursorType::Arrow)); break; case helper::ClickState::Hover: - Set(render_object_.get(), style_.hover); SetCursor(GetSystemCursor(SystemCursorType::Hand)); break; case helper::ClickState::Press: - Set(render_object_.get(), style_.press); SetCursor(GetSystemCursor(SystemCursorType::Hand)); break; case helper::ClickState::PressInactive: - Set(render_object_.get(), style_.press_cancel); SetCursor(GetSystemCursor(SystemCursorType::Arrow)); break; } }); + + GetStyleRuleSet()->Set( + UiManager::GetInstance()->GetThemeResources()->button_style); } Button::~Button() = default; diff --git a/src/ui/controls/Control.cpp b/src/ui/controls/Control.cpp index c1316a62..1c4ffe51 100644 --- a/src/ui/controls/Control.cpp +++ b/src/ui/controls/Control.cpp @@ -6,6 +6,7 @@ #include "cru/ui/Base.hpp" #include "cru/ui/host/WindowHost.hpp" #include "cru/ui/render/RenderObject.hpp" +#include "cru/ui/style/StyleRuleSet.hpp" #include @@ -15,6 +16,8 @@ using platform::gui::IUiApplication; using platform::gui::SystemCursorType; Control::Control() { + style_rule_set_ = std::make_unique(this); + MouseEnterEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) { this->is_mouse_over_ = true; this->OnMouseHoverChange(true); @@ -94,6 +97,10 @@ void Control::SetCursor(std::shared_ptr cursor) { } } +style::StyleRuleSet* Control::GetStyleRuleSet() { + return style_rule_set_.get(); +} + void Control::AddChild(Control* control, const Index position) { Expects(control->GetParent() == nullptr); // The control already has a parent. diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp index 6ba6ecb2..031894c0 100644 --- a/src/ui/controls/TextBox.cpp +++ b/src/ui/controls/TextBox.cpp @@ -18,8 +18,6 @@ TextBox::TextBox() scroll_render_object_(new ScrollRenderObject()) { const auto theme_resources = UiManager::GetInstance()->GetThemeResources(); - border_style_ = theme_resources->text_box_border_style; - text_render_object_ = std::make_unique( theme_resources->text_brush, theme_resources->default_font, theme_resources->text_selection_brush, theme_resources->caret_brush); @@ -38,17 +36,8 @@ TextBox::TextBox() service_->SetEditable(true); border_render_object_->SetBorderEnabled(true); - border_render_object_->SetBorderStyle(border_style_.normal); - - GainFocusEvent()->Direct()->AddHandler([this](event::FocusChangeEventArgs&) { - this->service_->SetCaretVisible(true); - this->UpdateBorderStyle(); - }); - LoseFocusEvent()->Direct()->AddHandler([this](event::FocusChangeEventArgs&) { - this->service_->SetCaretVisible(false); - this->UpdateBorderStyle(); - }); + GetStyleRuleSet()->Set(theme_resources->text_box_style); } TextBox::~TextBox() {} @@ -65,19 +54,7 @@ render::ScrollRenderObject* TextBox::GetScrollRenderObject() { return scroll_render_object_.get(); } -const TextBoxBorderStyle& TextBox::GetBorderStyle() { return border_style_; } - -void TextBox::SetBorderStyle(TextBoxBorderStyle border_style) { - border_style_ = std::move(border_style); -} - -void TextBox::OnMouseHoverChange(bool) { UpdateBorderStyle(); } - -void TextBox::UpdateBorderStyle() { - const auto focus = HasFocus(); - const auto hover = IsMouseOver(); - border_render_object_->SetBorderStyle( - focus ? (hover ? border_style_.focus_hover : border_style_.focus) - : (hover ? border_style_.hover : border_style_.normal)); +void TextBox::ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) { + border_render_object_->ApplyBorderStyle(style); } } // namespace cru::ui::controls diff --git a/src/ui/render/BorderRenderObject.cpp b/src/ui/render/BorderRenderObject.cpp index 5abc7832..c176e760 100644 --- a/src/ui/render/BorderRenderObject.cpp +++ b/src/ui/render/BorderRenderObject.cpp @@ -17,15 +17,6 @@ BorderRenderObject::BorderRenderObject() { BorderRenderObject::~BorderRenderObject() {} -void BorderRenderObject::SetBorderStyle(const BorderStyle& style) { - border_brush_ = style.border_brush; - border_thickness_ = style.border_thickness; - border_radius_ = style.border_radius; - foreground_brush_ = style.foreground_brush; - background_brush_ = style.background_brush; - InvalidateLayout(); -} - void BorderRenderObject::ApplyBorderStyle( const style::ApplyBorderStyleInfo& style) { if (style.border_brush != nullptr) border_brush_ = style.border_brush; diff --git a/src/ui/style/Condition.cpp b/src/ui/style/Condition.cpp index 891eb062..f4866c04 100644 --- a/src/ui/style/Condition.cpp +++ b/src/ui/style/Condition.cpp @@ -51,6 +51,16 @@ bool FocusCondition::Judge(controls::Control* control) const { return control->HasFocus() == has_focus_; } +std::vector HoverCondition::ChangeOn( + controls::Control* control) const { + return {control->MouseEnterEvent()->Direct(), + control->MouseLeaveEvent()->Direct()}; +} + +bool HoverCondition::Judge(controls::Control* control) const { + return control->IsMouseOver() == hover_; +} + ClickStateCondition::ClickStateCondition(helper::ClickState click_state) : click_state_(click_state) {} diff --git a/src/ui/style/StyleRuleSet.cpp b/src/ui/style/StyleRuleSet.cpp index e69de29b..403fe114 100644 --- a/src/ui/style/StyleRuleSet.cpp +++ b/src/ui/style/StyleRuleSet.cpp @@ -0,0 +1,58 @@ +#include "cru/ui/style/StyleRuleSet.hpp" +#include "cru/common/Event.hpp" +#include "gsl/gsl_assert" + +#include + +namespace cru::ui::style { +void StyleRuleSet::AddStyleRule(StyleRule rule, gsl::index index) { + Expects(index >= 0 && index <= GetSize()); + + rules_.insert(rules_.cbegin() + index, std::move(rule)); + + UpdateChangeListener(); + UpdateStyle(); +} + +void StyleRuleSet::RemoveStyleRule(gsl::index index, gsl::index count) { + Expects(index >= 0); + Expects(count >= 0 && index + count <= GetSize()); + + rules_.erase(rules_.cbegin() + index, rules_.cbegin() + index + count); + + UpdateChangeListener(); + UpdateStyle(); +} + +void StyleRuleSet::Set(const StyleRuleSet& other) { + rules_ = other.rules_; + UpdateChangeListener(); + UpdateStyle(); +} + +void StyleRuleSet::UpdateChangeListener() { + if (control_ == nullptr) return; + + guard_.Clear(); + + std::unordered_set events; + for (const auto& rule : rules_) { + auto e = rule.GetCondition()->ChangeOn(control_); + events.insert(e.cbegin(), e.cend()); + } + + for (auto e : events) { + guard_ += e->AddSpyOnlyHandler([this] { this->UpdateStyle(); }); + } +} + +void StyleRuleSet::UpdateStyle() { + if (control_ == nullptr) return; + + for (const auto& rule : rules_) { + if (rule.GetCondition()->Judge(control_)) { + rule.GetStyler()->Apply(control_); + } + } +} +} // namespace cru::ui::style diff --git a/src/win/gui/Window.cpp b/src/win/gui/Window.cpp index 174b8931..efd3bfcc 100644 --- a/src/win/gui/Window.cpp +++ b/src/win/gui/Window.cpp @@ -367,9 +367,9 @@ RECT WinNativeWindow::GetClientRectPixel() { } void WinNativeWindow::OnDestroyInternal() { + destroy_event_.Raise(nullptr); application_->GetWindowManager()->UnregisterWindow(hwnd_); hwnd_ = nullptr; - destroy_event_.Raise(nullptr); if (!sync_flag_) { sync_flag_ = true; delete this; -- cgit v1.2.3 From 51cc89106193cc4695507d678adab7388d940577 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 3 Dec 2020 22:48:12 +0800 Subject: ... --- src/ui/controls/TextBox.cpp | 1 - src/ui/controls/TextControlService.hpp | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/ui/controls/TextBox.cpp') diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp index 031894c0..0d14dbcf 100644 --- a/src/ui/controls/TextBox.cpp +++ b/src/ui/controls/TextBox.cpp @@ -32,7 +32,6 @@ TextBox::TextBox() service_ = std::make_unique>(this); service_->SetEnabled(true); - service_->SetCaretVisible(true); service_->SetEditable(true); border_render_object_->SetBorderEnabled(true); diff --git a/src/ui/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp index d50621ea..c535512f 100644 --- a/src/ui/controls/TextControlService.hpp +++ b/src/ui/controls/TextControlService.hpp @@ -473,6 +473,7 @@ class TextControlService : public Object { input_method_context_event_guard_ += window_host->AfterLayoutEvent()->AddHandler( [this](auto) { this->UpdateInputMethodPosition(); }); + SetCaretVisible(true); } } @@ -483,6 +484,7 @@ class TextControlService : public Object { if (input_method_context) { input_method_context->DisableIME(); } + SetCaretVisible(false); SyncTextRenderObject(); } -- cgit v1.2.3 From 9fcebe16b5ad4acc8b2e206dbe163e58b1f75cdf Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 9 Dec 2020 17:21:44 +0800 Subject: ... --- include/cru/common/Event.hpp | 2 + include/cru/ui/Base.hpp | 1 + include/cru/ui/controls/Control.hpp | 1 + include/cru/ui/style/StyleRuleSet.hpp | 40 +++++++++++++++++-- src/ui/controls/Button.cpp | 4 +- src/ui/controls/Control.cpp | 4 +- src/ui/controls/TextBox.cpp | 2 +- src/ui/style/StyleRuleSet.cpp | 74 ++++++++++++++++++++++++++--------- 8 files changed, 102 insertions(+), 26 deletions(-) (limited to 'src/ui/controls/TextBox.cpp') diff --git a/include/cru/common/Event.hpp b/include/cru/common/Event.hpp index 7f7b4dd4..aa8fadbb 100644 --- a/include/cru/common/Event.hpp +++ b/include/cru/common/Event.hpp @@ -233,6 +233,8 @@ class EventRevokerGuard { EventRevoker Release() { return std::move(*revoker_.release()); } + void Reset() { revoker_.reset(); } + void Reset(EventRevoker&& revoker) { revoker_.reset(new EventRevoker(std::move(revoker))); } diff --git a/include/cru/ui/Base.hpp b/include/cru/ui/Base.hpp index 57beb723..b2939a0b 100644 --- a/include/cru/ui/Base.hpp +++ b/include/cru/ui/Base.hpp @@ -42,6 +42,7 @@ class RenderObject; namespace style { class StyleRuleSet; +class StyleRuleSetBind; } //-------------------- region: basic types -------------------- diff --git a/include/cru/ui/controls/Control.hpp b/include/cru/ui/controls/Control.hpp index 0d34bc63..341b6ef2 100644 --- a/include/cru/ui/controls/Control.hpp +++ b/include/cru/ui/controls/Control.hpp @@ -152,5 +152,6 @@ class Control : public Object { std::shared_ptr cursor_ = nullptr; std::unique_ptr style_rule_set_; + std::unique_ptr style_rule_set_bind_; }; } // namespace cru::ui::controls diff --git a/include/cru/ui/style/StyleRuleSet.hpp b/include/cru/ui/style/StyleRuleSet.hpp index 3ec71730..ba3f8b4c 100644 --- a/include/cru/ui/style/StyleRuleSet.hpp +++ b/include/cru/ui/style/StyleRuleSet.hpp @@ -2,13 +2,14 @@ #include "StyleRule.hpp" #include "cru/common/Base.hpp" #include "cru/common/Event.hpp" -#include "gsl/gsl_assert" + +#include namespace cru::ui::style { class StyleRuleSet : public Object { public: - StyleRuleSet() : control_(nullptr) {} - explicit StyleRuleSet(controls::Control* control) : control_(control) {} + StyleRuleSet() = default; + explicit StyleRuleSet(StyleRuleSet* parent); CRU_DELETE_COPY(StyleRuleSet) CRU_DELETE_MOVE(StyleRuleSet) @@ -16,6 +17,9 @@ class StyleRuleSet : public Object { ~StyleRuleSet() override = default; public: + StyleRuleSet* GetParent() const { return parent_; } + void SetParent(StyleRuleSet* parent); + gsl::index GetSize() const { return static_cast(rules_.size()); } const std::vector& GetRules() const { return rules_; } @@ -41,14 +45,42 @@ class StyleRuleSet : public Object { const StyleRule& operator[](gsl::index index) const { return rules_[index]; } + // Triggered whenever a change happened to this (rule add or remove, parent + // change ...). Subscribe to this and update style change listeners and style. + IEvent* ChangeEvent() { return &change_event_; } + + private: + void RaiseChangeEvent() { change_event_.Raise(nullptr); } + + private: + Event change_event_; + + StyleRuleSet* parent_ = nullptr; + EventRevokerGuard parent_change_event_guard_; + + std::vector rules_; +}; + +class StyleRuleSetBind { + public: + StyleRuleSetBind(controls::Control* control, StyleRuleSet* ruleset); + + CRU_DELETE_COPY(StyleRuleSetBind) + CRU_DELETE_MOVE(StyleRuleSetBind) + + ~StyleRuleSetBind() = default; + private: + void UpdateRuleSetChainCache(); void UpdateChangeListener(); void UpdateStyle(); private: controls::Control* control_; + StyleRuleSet* ruleset_; - std::vector rules_; + // child first, parent last. + std::vector ruleset_chain_cache_; EventRevokerListGuard guard_; }; diff --git a/src/ui/controls/Button.cpp b/src/ui/controls/Button.cpp index 7858eadb..8bd9f93f 100644 --- a/src/ui/controls/Button.cpp +++ b/src/ui/controls/Button.cpp @@ -42,8 +42,8 @@ Button::Button() : click_detector_(this) { } }); - GetStyleRuleSet()->Set( - UiManager::GetInstance()->GetThemeResources()->button_style); + GetStyleRuleSet()->SetParent( + &UiManager::GetInstance()->GetThemeResources()->button_style); } Button::~Button() = default; diff --git a/src/ui/controls/Control.cpp b/src/ui/controls/Control.cpp index 1c4ffe51..29c2c46a 100644 --- a/src/ui/controls/Control.cpp +++ b/src/ui/controls/Control.cpp @@ -16,7 +16,9 @@ using platform::gui::IUiApplication; using platform::gui::SystemCursorType; Control::Control() { - style_rule_set_ = std::make_unique(this); + style_rule_set_ = std::make_unique(); + style_rule_set_bind_ = + std::make_unique(this, style_rule_set_.get()); MouseEnterEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) { this->is_mouse_over_ = true; diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp index 0d14dbcf..d8317a8c 100644 --- a/src/ui/controls/TextBox.cpp +++ b/src/ui/controls/TextBox.cpp @@ -36,7 +36,7 @@ TextBox::TextBox() border_render_object_->SetBorderEnabled(true); - GetStyleRuleSet()->Set(theme_resources->text_box_style); + GetStyleRuleSet()->SetParent(&theme_resources->text_box_style); } TextBox::~TextBox() {} diff --git a/src/ui/style/StyleRuleSet.cpp b/src/ui/style/StyleRuleSet.cpp index 403fe114..24b88af9 100644 --- a/src/ui/style/StyleRuleSet.cpp +++ b/src/ui/style/StyleRuleSet.cpp @@ -1,17 +1,30 @@ #include "cru/ui/style/StyleRuleSet.hpp" #include "cru/common/Event.hpp" +#include "cru/ui/controls/Control.hpp" #include "gsl/gsl_assert" #include namespace cru::ui::style { +StyleRuleSet::StyleRuleSet(StyleRuleSet* parent) { SetParent(parent); } + +void StyleRuleSet::SetParent(StyleRuleSet* parent) { + if (parent == parent_) return; + parent_change_event_guard_.Reset(); + parent_ = parent; + if (parent != nullptr) { + parent_change_event_guard_.Reset(parent->ChangeEvent()->AddSpyOnlyHandler( + [this] { this->RaiseChangeEvent(); })); + } + RaiseChangeEvent(); +} + void StyleRuleSet::AddStyleRule(StyleRule rule, gsl::index index) { Expects(index >= 0 && index <= GetSize()); rules_.insert(rules_.cbegin() + index, std::move(rule)); - UpdateChangeListener(); - UpdateStyle(); + RaiseChangeEvent(); } void StyleRuleSet::RemoveStyleRule(gsl::index index, gsl::index count) { @@ -20,25 +33,48 @@ void StyleRuleSet::RemoveStyleRule(gsl::index index, gsl::index count) { rules_.erase(rules_.cbegin() + index, rules_.cbegin() + index + count); - UpdateChangeListener(); - UpdateStyle(); + RaiseChangeEvent(); } void StyleRuleSet::Set(const StyleRuleSet& other) { rules_ = other.rules_; - UpdateChangeListener(); - UpdateStyle(); + + RaiseChangeEvent(); +} + +StyleRuleSetBind::StyleRuleSetBind(controls::Control* control, + StyleRuleSet* ruleset) + : control_(control), ruleset_(ruleset) { + Expects(control); + Expects(ruleset); + + ruleset->ChangeEvent()->AddSpyOnlyHandler([this] { + UpdateRuleSetChainCache(); + UpdateChangeListener(); + UpdateStyle(); + }); } -void StyleRuleSet::UpdateChangeListener() { - if (control_ == nullptr) return; +void StyleRuleSetBind::UpdateRuleSetChainCache() { + ruleset_chain_cache_.clear(); + auto parent = ruleset_; + while (parent != nullptr) { + ruleset_chain_cache_.push_back(parent); + parent = parent->GetParent(); + } +} +void StyleRuleSetBind::UpdateChangeListener() { guard_.Clear(); std::unordered_set events; - for (const auto& rule : rules_) { - auto e = rule.GetCondition()->ChangeOn(control_); - events.insert(e.cbegin(), e.cend()); + + // ruleset order does not matter + for (auto ruleset : ruleset_chain_cache_) { + for (const auto& rule : ruleset->GetRules()) { + auto e = rule.GetCondition()->ChangeOn(control_); + events.insert(e.cbegin(), e.cend()); + } } for (auto e : events) { @@ -46,13 +82,15 @@ void StyleRuleSet::UpdateChangeListener() { } } -void StyleRuleSet::UpdateStyle() { - if (control_ == nullptr) return; - - for (const auto& rule : rules_) { - if (rule.GetCondition()->Judge(control_)) { - rule.GetStyler()->Apply(control_); - } +void StyleRuleSetBind::UpdateStyle() { + // cache is parent last, but when calculate style, parent first, so iterate + // reverse. + for (auto iter = ruleset_chain_cache_.crbegin(); + iter != ruleset_chain_cache_.crend(); ++iter) { + for (const auto& rule : (*iter)->GetRules()) + if (rule.GetCondition()->Judge(control_)) { + rule.GetStyler()->Apply(control_); + } } } } // namespace cru::ui::style -- 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 'src/ui/controls/TextBox.cpp') 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 1e1170a89330881c5fad60988bc27c824dfcf454 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 31 Dec 2020 18:27:57 +0800 Subject: ... --- include/cru/platform/graphics/TextLayout.hpp | 4 ++-- include/cru/ui/render/TextRenderObject.hpp | 11 ++++++++++- include/cru/win/graphics/direct/TextLayout.hpp | 2 +- src/ui/controls/TextBox.cpp | 1 + src/ui/render/TextRenderObject.cpp | 13 +++++++++++-- src/win/graphics/direct/TextLayout.cpp | 7 +++++-- 6 files changed, 30 insertions(+), 8 deletions(-) (limited to 'src/ui/controls/TextBox.cpp') diff --git a/include/cru/platform/graphics/TextLayout.hpp b/include/cru/platform/graphics/TextLayout.hpp index efd017d6..b363fb77 100644 --- a/include/cru/platform/graphics/TextLayout.hpp +++ b/include/cru/platform/graphics/TextLayout.hpp @@ -16,9 +16,9 @@ struct ITextLayout : virtual IGraphResource { virtual void SetMaxWidth(float max_width) = 0; virtual void SetMaxHeight(float max_height) = 0; - virtual Rect GetTextBounds() = 0; + virtual Rect GetTextBounds(bool includingTrailingSpace = false) = 0; virtual std::vector TextRangeRect(const TextRange& text_range) = 0; virtual Point TextSinglePoint(Index position, bool trailing) = 0; virtual TextHitTestResult HitTest(const Point& point) = 0; }; -} // namespace cru::platform::graph +} // namespace cru::platform::graphics diff --git a/include/cru/ui/render/TextRenderObject.hpp b/include/cru/ui/render/TextRenderObject.hpp index fa569c8c..bdec18d1 100644 --- a/include/cru/ui/render/TextRenderObject.hpp +++ b/include/cru/ui/render/TextRenderObject.hpp @@ -38,7 +38,9 @@ class TextRenderObject : public RenderObject { std::u16string_view GetTextView() const; void SetText(std::u16string new_text); - std::shared_ptr GetBrush() const { return brush_; } + std::shared_ptr GetBrush() const { + return brush_; + } void SetBrush(std::shared_ptr new_brush); std::shared_ptr GetFont() const; @@ -80,6 +82,11 @@ class TextRenderObject : public RenderObject { float GetCaretWidth() const { return caret_width_; } void SetCaretWidth(float width); + bool IsMeasureIncludingTrailingSpace() const { + return is_measure_including_trailing_space_; + } + void SetMeasureIncludingTrailingSpace(bool including); + RenderObject* HitTest(const Point& point) override; protected: @@ -104,5 +111,7 @@ class TextRenderObject : public RenderObject { gsl::index caret_position_ = 0; std::shared_ptr caret_brush_; float caret_width_ = default_caret_width; + + bool is_measure_including_trailing_space_ = false; }; } // namespace cru::ui::render diff --git a/include/cru/win/graphics/direct/TextLayout.hpp b/include/cru/win/graphics/direct/TextLayout.hpp index 3320431f..aa040278 100644 --- a/include/cru/win/graphics/direct/TextLayout.hpp +++ b/include/cru/win/graphics/direct/TextLayout.hpp @@ -38,7 +38,7 @@ class DWriteTextLayout : public DirectGraphResource, void SetMaxWidth(float max_width) override; void SetMaxHeight(float max_height) override; - Rect GetTextBounds() override; + Rect GetTextBounds(bool includingTrailingSpace = false) override; // Return empty vector if text_range.count is 0. Text range could be in // reverse direction, it should be normalized first in implementation. std::vector TextRangeRect(const TextRange& text_range) override; diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp index e1acaee0..bfc98c06 100644 --- a/src/ui/controls/TextBox.cpp +++ b/src/ui/controls/TextBox.cpp @@ -28,6 +28,7 @@ TextBox::TextBox() scroll_render_object_->SetAttachedControl(this); text_render_object_->SetAttachedControl(this); text_render_object_->SetMinSize(Size{100, 24}); + text_render_object_->SetMeasureIncludingTrailingSpace(true); service_ = std::make_unique(this); service_->SetEnabled(true); diff --git a/src/ui/render/TextRenderObject.cpp b/src/ui/render/TextRenderObject.cpp index 9faab622..06092d52 100644 --- a/src/ui/render/TextRenderObject.cpp +++ b/src/ui/render/TextRenderObject.cpp @@ -57,7 +57,8 @@ std::shared_ptr TextRenderObject::GetFont() const { return text_layout_->GetFont(); } -void TextRenderObject::SetFont(std::shared_ptr font) { +void TextRenderObject::SetFont( + std::shared_ptr font) { Expects(font); text_layout_->SetFont(std::move(font)); } @@ -154,6 +155,12 @@ Rect TextRenderObject::GetCaretRect() { return rect; } +void TextRenderObject::SetMeasureIncludingTrailingSpace(bool including) { + if (is_measure_including_trailing_space_ == including) return; + is_measure_including_trailing_space_ = including; + InvalidateLayout(); +} + RenderObject* TextRenderObject::HitTest(const Point& point) { const auto padding_rect = GetPaddingRect(); return padding_rect.IsPointInside(point) ? this : nullptr; @@ -185,7 +192,9 @@ Size TextRenderObject::OnMeasureContent(const MeasureRequirement& requirement, text_layout_->SetMaxWidth(measure_width); text_layout_->SetMaxHeight(std::numeric_limits::max()); - const auto text_size = text_layout_->GetTextBounds().GetSize(); + const auto text_size = + text_layout_->GetTextBounds(is_measure_including_trailing_space_) + .GetSize(); auto result = text_size; if (requirement.max.width.IsSpecified() && diff --git a/src/win/graphics/direct/TextLayout.cpp b/src/win/graphics/direct/TextLayout.cpp index 0c6e797f..0b3c68ca 100644 --- a/src/win/graphics/direct/TextLayout.cpp +++ b/src/win/graphics/direct/TextLayout.cpp @@ -58,10 +58,13 @@ void DWriteTextLayout::SetMaxHeight(float max_height) { ThrowIfFailed(text_layout_->SetMaxHeight(max_height_)); } -Rect DWriteTextLayout::GetTextBounds() { +Rect DWriteTextLayout::GetTextBounds(bool includingTrailingSpace) { DWRITE_TEXT_METRICS metrics; ThrowIfFailed(text_layout_->GetMetrics(&metrics)); - return Rect{metrics.left, metrics.top, metrics.width, metrics.height}; + return Rect{metrics.left, metrics.top, + includingTrailingSpace ? metrics.widthIncludingTrailingWhitespace + : metrics.width, + metrics.height}; } std::vector DWriteTextLayout::TextRangeRect( -- cgit v1.2.3