diff options
Diffstat (limited to 'src/ui/controls')
-rw-r--r-- | src/ui/controls/Button.cpp | 62 | ||||
-rw-r--r-- | src/ui/controls/Container.cpp | 10 | ||||
-rw-r--r-- | src/ui/controls/ContentControl.cpp | 31 | ||||
-rw-r--r-- | src/ui/controls/Control.cpp | 166 | ||||
-rw-r--r-- | src/ui/controls/FlexLayout.cpp | 12 | ||||
-rw-r--r-- | src/ui/controls/LayoutControl.cpp | 18 | ||||
-rw-r--r-- | src/ui/controls/NoChildControl.cpp | 3 | ||||
-rw-r--r-- | src/ui/controls/Popup.cpp | 22 | ||||
-rw-r--r-- | src/ui/controls/RootControl.cpp | 53 | ||||
-rw-r--r-- | src/ui/controls/StackLayout.cpp | 15 | ||||
-rw-r--r-- | src/ui/controls/TextBlock.cpp | 22 | ||||
-rw-r--r-- | src/ui/controls/TextBox.cpp | 36 | ||||
-rw-r--r-- | src/ui/controls/TextControlService.hpp | 403 | ||||
-rw-r--r-- | src/ui/controls/TextHostControlService.cpp | 469 | ||||
-rw-r--r-- | src/ui/controls/Window.cpp | 24 |
15 files changed, 834 insertions, 512 deletions
diff --git a/src/ui/controls/Button.cpp b/src/ui/controls/Button.cpp index 6f6af878..c6480b77 100644 --- a/src/ui/controls/Button.cpp +++ b/src/ui/controls/Button.cpp @@ -1,61 +1,22 @@ #include "cru/ui/controls/Button.hpp" -#include <memory> #include "../Helper.hpp" -#include "cru/platform/graph/Brush.hpp" -#include "cru/platform/native/Cursor.hpp" -#include "cru/platform/native/UiApplication.hpp" -#include "cru/ui/render/BorderRenderObject.hpp" +#include "cru/platform/graphics/Brush.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/platform/gui/UiApplication.hpp" #include "cru/ui/UiManager.hpp" -#include "cru/ui/Window.hpp" +#include "cru/ui/helper/ClickDetector.hpp" +#include "cru/ui/render/BorderRenderObject.hpp" namespace cru::ui::controls { -using cru::platform::native::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<platform::native::ICursor> 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::BorderRenderObject>(); render_object_->SetAttachedControl(this); - Set(render_object_.get(), style_.normal); + SetContainerRenderObject(render_object_.get()); render_object_->SetBorderEnabled(true); - click_detector_.StateChangeEvent()->AddHandler( - [this](const ClickState& state) { - switch (state) { - case ClickState::None: - Set(render_object_.get(), style_.normal); - SetCursor(GetSystemCursor(SystemCursorType::Arrow)); - break; - case ClickState::Hover: - Set(render_object_.get(), style_.hover); - SetCursor(GetSystemCursor(SystemCursorType::Hand)); - break; - case ClickState::Press: - Set(render_object_.get(), style_.press); - SetCursor(GetSystemCursor(SystemCursorType::Hand)); - break; - case ClickState::PressInactive: - Set(render_object_.get(), style_.press_cancel); - SetCursor(GetSystemCursor(SystemCursorType::Arrow)); - break; - } - }); + GetStyleRuleSet()->SetParent( + &UiManager::GetInstance()->GetThemeResources()->button_style); } Button::~Button() = default; @@ -64,10 +25,7 @@ render::RenderObject* Button::GetRenderObject() const { return render_object_.get(); } -void Button::OnChildChanged(Control* old_child, Control* new_child) { - if (old_child != nullptr) render_object_->RemoveChild(0); - if (new_child != nullptr) - render_object_->AddChild(new_child->GetRenderObject(), 0); +void Button::ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) { + render_object_->ApplyBorderStyle(style); } - } // namespace cru::ui::controls diff --git a/src/ui/controls/Container.cpp b/src/ui/controls/Container.cpp index de58ee64..30129f64 100644 --- a/src/ui/controls/Container.cpp +++ b/src/ui/controls/Container.cpp @@ -1,18 +1,20 @@ #include "cru/ui/controls/Container.hpp" -#include "cru/platform/graph/Factory.hpp" +#include "cru/platform/graphics/Factory.hpp" #include "cru/ui/render/BorderRenderObject.hpp" +#include "cru/ui/render/RenderObject.hpp" namespace cru::ui::controls { Container::Container() { render_object_ = std::make_unique<render::BorderRenderObject>(); render_object_->SetBorderEnabled(false); + render_object_->SetAttachedControl(this); + SetContainerRenderObject(render_object_.get()); } Container::~Container() = default; -void Container::OnChildChanged(Control*, Control* new_child) { - render_object_->RemoveChild(0); - render_object_->AddChild(new_child->GetRenderObject(), 0); +render::RenderObject* Container::GetRenderObject() const { + return render_object_.get(); } } // namespace cru::ui::controls diff --git a/src/ui/controls/ContentControl.cpp b/src/ui/controls/ContentControl.cpp new file mode 100644 index 00000000..8c6f0b00 --- /dev/null +++ b/src/ui/controls/ContentControl.cpp @@ -0,0 +1,31 @@ +#include "cru/ui/controls/ContentControl.hpp" + +namespace cru::ui::controls { +Control* ContentControl::GetChild() const { + if (GetChildren().empty()) return nullptr; + return GetChildren()[0]; +} + +void ContentControl::SetChild(Control* child) { + Control* old_child = nullptr; + if (!GetChildren().empty()) { + old_child = GetChildren()[0]; + this->RemoveChild(0); + } + if (child) { + this->AddChild(child, 0); + } + OnChildChanged(old_child, child); +} + +void ContentControl::OnChildChanged(Control* old_child, Control* new_child) { + if (container_render_object_) { + if (old_child) { + container_render_object_->RemoveChild(0); + } + if (new_child) { + container_render_object_->AddChild(new_child->GetRenderObject(), 0); + } + } +} +} // namespace cru::ui::controls diff --git a/src/ui/controls/Control.cpp b/src/ui/controls/Control.cpp new file mode 100644 index 00000000..29c2c46a --- /dev/null +++ b/src/ui/controls/Control.cpp @@ -0,0 +1,166 @@ +#include "cru/ui/controls/Control.hpp" + +#include "cru/common/Base.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/ui/Base.hpp" +#include "cru/ui/host/WindowHost.hpp" +#include "cru/ui/render/RenderObject.hpp" +#include "cru/ui/style/StyleRuleSet.hpp" + +#include <memory> + +namespace cru::ui::controls { +using platform::gui::ICursor; +using platform::gui::IUiApplication; +using platform::gui::SystemCursorType; + +Control::Control() { + style_rule_set_ = std::make_unique<style::StyleRuleSet>(); + style_rule_set_bind_ = + std::make_unique<style::StyleRuleSetBind>(this, style_rule_set_.get()); + + MouseEnterEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) { + this->is_mouse_over_ = true; + this->OnMouseHoverChange(true); + }); + + MouseLeaveEvent()->Direct()->AddHandler([this](event::MouseEventArgs&) { + this->is_mouse_over_ = false; + this->OnMouseHoverChange(true); + }); +} + +Control::~Control() { + for (const auto child : children_) delete child; +} + +host::WindowHost* Control::GetWindowHost() const { return window_host_; } + +void Control::TraverseDescendants( + const std::function<void(Control*)>& predicate) { + predicate(this); + for (auto c : GetChildren()) c->TraverseDescendants(predicate); +} + +bool Control::HasFocus() { + auto host = GetWindowHost(); + if (host == nullptr) return false; + + return host->GetFocusControl() == this; +} + +bool Control::CaptureMouse() { + auto host = GetWindowHost(); + if (host == nullptr) return false; + + return host->CaptureMouseFor(this); +} + +void Control::SetFocus() { + auto host = GetWindowHost(); + if (host == nullptr) return; + + host->SetFocusControl(this); +} + +bool Control::ReleaseMouse() { + auto host = GetWindowHost(); + if (host == nullptr) return false; + + return host->CaptureMouseFor(nullptr); +} + +bool Control::IsMouseCaptured() { + auto host = GetWindowHost(); + if (host == nullptr) return false; + + return host->GetMouseCaptureControl() == this; +} + +std::shared_ptr<ICursor> Control::GetCursor() { return cursor_; } + +std::shared_ptr<ICursor> Control::GetInheritedCursor() { + Control* control = this; + while (control != nullptr) { + const auto cursor = control->GetCursor(); + if (cursor != nullptr) return cursor; + control = control->GetParent(); + } + return IUiApplication::GetInstance()->GetCursorManager()->GetSystemCursor( + SystemCursorType::Arrow); +} + +void Control::SetCursor(std::shared_ptr<ICursor> cursor) { + cursor_ = std::move(cursor); + const auto host = GetWindowHost(); + if (host != nullptr) { + host->UpdateCursor(); + } +} + +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. + Expects(position >= 0); + Expects(position <= static_cast<Index>( + children_.size())); // The position is out of range. + + children_.insert(children_.cbegin() + position, control); + + const auto old_parent = control->parent_; + control->parent_ = this; + + OnAddChild(control, position); + control->OnParentChanged(old_parent, this); + + if (window_host_) + control->TraverseDescendants([this](Control* control) { + control->window_host_ = window_host_; + control->OnAttachToHost(window_host_); + }); +} + +void Control::RemoveChild(const Index position) { + Expects(position >= 0); + Expects(position < static_cast<Index>( + children_.size())); // The position is out of range. + + const auto i = children_.cbegin() + position; + const auto control = *i; + + children_.erase(i); + control->parent_ = nullptr; + + OnRemoveChild(control, position); + control->OnParentChanged(this, nullptr); + + if (window_host_) + control->TraverseDescendants([this](Control* control) { + control->window_host_ = nullptr; + control->OnDetachFromHost(window_host_); + }); +} + +void Control::OnAddChild(Control* child, Index position) { + CRU_UNUSED(child) + CRU_UNUSED(position) +} +void Control::OnRemoveChild(Control* child, Index position) { + CRU_UNUSED(child) + CRU_UNUSED(position) +} + +void Control::OnParentChanged(Control* old_parent, Control* new_parent) { + CRU_UNUSED(old_parent) + CRU_UNUSED(new_parent) +} + +void Control::OnAttachToHost(host::WindowHost* host) { CRU_UNUSED(host) } + +void Control::OnDetachFromHost(host::WindowHost* host) { CRU_UNUSED(host) } +} // namespace cru::ui::controls diff --git a/src/ui/controls/FlexLayout.cpp b/src/ui/controls/FlexLayout.cpp index b7f350dc..e390241f 100644 --- a/src/ui/controls/FlexLayout.cpp +++ b/src/ui/controls/FlexLayout.cpp @@ -8,6 +8,7 @@ using render::FlexLayoutRenderObject; FlexLayout::FlexLayout() { render_object_.reset(new FlexLayoutRenderObject()); render_object_->SetAttachedControl(this); + SetContainerRenderObject(render_object_.get()); } FlexLayout::~FlexLayout() = default; @@ -60,13 +61,12 @@ void FlexLayout::SetFlexDirection(FlexDirection direction) { render_object_->SetFlexDirection(direction); } -void FlexLayout::OnAddChild(Control* child, const Index position) { - render_object_->AddChild(child->GetRenderObject(), position); +FlexCrossAlignment FlexLayout::GetItemCrossAlign() const { + return render_object_->GetItemCrossAlign(); } -void FlexLayout::OnRemoveChild(Control* child, const Index position) { - CRU_UNUSED(child) - - render_object_->RemoveChild(position); +void FlexLayout::SetItemCrossAlign(FlexCrossAlignment alignment) { + if (alignment == GetItemCrossAlign()) return; + render_object_->SetItemCrossAlign(alignment); } } // namespace cru::ui::controls diff --git a/src/ui/controls/LayoutControl.cpp b/src/ui/controls/LayoutControl.cpp new file mode 100644 index 00000000..5954853e --- /dev/null +++ b/src/ui/controls/LayoutControl.cpp @@ -0,0 +1,18 @@ +#include "cru/ui/controls/LayoutControl.hpp" + +#include "cru/ui/render/RenderObject.hpp" + +namespace cru::ui::controls { +void LayoutControl::OnAddChild(Control* child, Index position) { + if (container_render_object_ != nullptr) { + container_render_object_->AddChild(child->GetRenderObject(), position); + } +} + +void LayoutControl::OnRemoveChild(Control* child, Index position) { + CRU_UNUSED(child) + if (container_render_object_ != nullptr) { + container_render_object_->RemoveChild(position); + } +} +} // namespace cru::ui::controls diff --git a/src/ui/controls/NoChildControl.cpp b/src/ui/controls/NoChildControl.cpp new file mode 100644 index 00000000..c62c5819 --- /dev/null +++ b/src/ui/controls/NoChildControl.cpp @@ -0,0 +1,3 @@ +#include "cru/ui/controls/NoChildControl.hpp" + +namespace cru::ui::controls {} diff --git a/src/ui/controls/Popup.cpp b/src/ui/controls/Popup.cpp new file mode 100644 index 00000000..bc217bf5 --- /dev/null +++ b/src/ui/controls/Popup.cpp @@ -0,0 +1,22 @@ +#include "cru/ui/controls/Popup.hpp" + +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/ui/controls/RootControl.hpp" +#include "cru/ui/host/WindowHost.hpp" +#include "cru/ui/render/StackLayoutRenderObject.hpp" + +#include <memory> + +namespace cru::ui::controls { +Popup::Popup(Control* attached_control) : RootControl(attached_control) {} + +Popup::~Popup() = default; + +gsl::not_null<platform::gui::INativeWindow*> Popup::CreateNativeWindow( + gsl::not_null<host::WindowHost*> host, + platform::gui::INativeWindow* parent) { + return host->CreateNativeWindow( + {parent, platform::gui::CreateWindowFlags::NoCaptionAndBorder}); +} + +} // namespace cru::ui::controls diff --git a/src/ui/controls/RootControl.cpp b/src/ui/controls/RootControl.cpp new file mode 100644 index 00000000..015703c3 --- /dev/null +++ b/src/ui/controls/RootControl.cpp @@ -0,0 +1,53 @@ +#include "cru/ui/controls/RootControl.hpp" + +#include "cru/common/Base.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/ui/host/WindowHost.hpp" +#include "cru/ui/render/Base.hpp" +#include "cru/ui/render/StackLayoutRenderObject.hpp" +#include "gsl/pointers" + +#include <memory> + +namespace cru::ui::controls { +RootControl::RootControl(Control* attached_control) + : attached_control_(attached_control) { + render_object_ = std::make_unique<render::StackLayoutRenderObject>(); + render_object_->SetAttachedControl(this); + SetContainerRenderObject(render_object_.get()); + window_host_ = std::make_unique<host::WindowHost>(this); +} + +RootControl::~RootControl() {} + +render::RenderObject* RootControl::GetRenderObject() const { + return render_object_.get(); +} + +void RootControl::EnsureWindowCreated() { this->GetNativeWindow(true); } + +Rect RootControl::GetRect() { return window_host_->GetWindowRect(); } + +void RootControl::SetRect(const Rect& rect) { + window_host_->SetWindowRect(rect); +} + +void RootControl::Show(bool create) { + platform::gui::INativeWindow* native_window = GetNativeWindow(create); + if (!native_window) return; + native_window->SetVisible(true); +} + +platform::gui::INativeWindow* RootControl::GetNativeWindow(bool create) { + const auto host = GetWindowHost(); + platform::gui::INativeWindow* native_window = host->GetNativeWindow(); + if (!create) return native_window; + if (!native_window) { + native_window = this->CreateNativeWindow( + host, attached_control_ + ? attached_control_->GetWindowHost()->GetNativeWindow() + : nullptr); + } + return native_window; +} +} // namespace cru::ui::controls diff --git a/src/ui/controls/StackLayout.cpp b/src/ui/controls/StackLayout.cpp index ce500b79..89968571 100644 --- a/src/ui/controls/StackLayout.cpp +++ b/src/ui/controls/StackLayout.cpp @@ -1,12 +1,15 @@ #include "cru/ui/controls/StackLayout.hpp" +#include <memory> #include "cru/ui/render/StackLayoutRenderObject.hpp" namespace cru::ui::controls { using render::StackLayoutRenderObject; -StackLayout::StackLayout() : render_object_(new StackLayoutRenderObject()) { +StackLayout::StackLayout() { + render_object_ = std::make_unique<StackLayoutRenderObject>(); render_object_->SetAttachedControl(this); + SetContainerRenderObject(render_object_.get()); } StackLayout::~StackLayout() = default; @@ -14,14 +17,4 @@ StackLayout::~StackLayout() = default; render::RenderObject* StackLayout::GetRenderObject() const { return render_object_.get(); } - -void StackLayout::OnAddChild(Control* child, const Index position) { - render_object_->AddChild(child->GetRenderObject(), position); -} - -void StackLayout::OnRemoveChild(Control* child, const Index position) { - CRU_UNUSED(child) - - render_object_->RemoveChild(position); -} } // namespace cru::ui::controls diff --git a/src/ui/controls/TextBlock.cpp b/src/ui/controls/TextBlock.cpp index 9ce99ab6..0724edcf 100644 --- a/src/ui/controls/TextBlock.cpp +++ b/src/ui/controls/TextBlock.cpp @@ -1,16 +1,22 @@ #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" #include "cru/ui/render/TextRenderObject.hpp" namespace cru::ui::controls { -using render::CanvasRenderObject; -using render::StackLayoutRenderObject; using render::TextRenderObject; +TextBlock* TextBlock::Create() { return new TextBlock(); } + +TextBlock* TextBlock::Create(std::u16string text, bool selectable) { + auto c = new TextBlock(); + c->SetText(text); + c->SetSelectable(selectable); + return c; +} + TextBlock::TextBlock() { const auto theme_resources = UiManager::GetInstance()->GetThemeResources(); @@ -20,8 +26,10 @@ TextBlock::TextBlock() { text_render_object_->SetAttachedControl(this); - service_ = std::make_unique<TextControlService<TextBlock>>(this); - service_->SetEnabled(true); + service_ = std::make_unique<TextHostControlService>(this); + + service_->SetEnabled(false); + service_->SetEditable(false); } TextBlock::~TextBlock() = default; @@ -36,6 +44,10 @@ void TextBlock::SetText(std::u16string text) { service_->SetText(std::move(text)); } +bool TextBlock::IsSelectable() const { return service_->IsEnabled(); } + +void TextBlock::SetSelectable(bool value) { service_->SetEnabled(value); } + gsl::not_null<render::TextRenderObject*> TextBlock::GetTextRenderObject() { return text_render_object_.get(); } diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp index 4a8d6658..bfc98c06 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" @@ -10,9 +9,7 @@ namespace cru::ui::controls { using render::BorderRenderObject; -using render::CanvasRenderObject; using render::ScrollRenderObject; -using render::StackLayoutRenderObject; using render::TextRenderObject; TextBox::TextBox() @@ -20,8 +17,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<TextRenderObject>( theme_resources->text_brush, theme_resources->default_font, theme_resources->text_selection_brush, theme_resources->caret_brush); @@ -33,24 +28,15 @@ 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<TextControlService<TextBox>>(this); + service_ = std::make_unique<TextHostControlService>(this); service_->SetEnabled(true); - service_->SetCaretVisible(true); 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()->SetParent(&theme_resources->text_box_style); } TextBox::~TextBox() {} @@ -67,19 +53,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/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp deleted file mode 100644 index 5d8d4645..00000000 --- a/src/ui/controls/TextControlService.hpp +++ /dev/null @@ -1,403 +0,0 @@ -#pragma once -#include "../Helper.hpp" -#include "cru/common/Logger.hpp" -#include "cru/common/StringUtil.hpp" -#include "cru/platform/graph/Font.hpp" -#include "cru/platform/graph/Painter.hpp" -#include "cru/platform/native/InputMethod.hpp" -#include "cru/platform/native/UiApplication.hpp" -#include "cru/platform/native/Window.hpp" -#include "cru/ui/Control.hpp" -#include "cru/ui/UiEvent.hpp" -#include "cru/ui/UiHost.hpp" -#include "cru/ui/render/CanvasRenderObject.hpp" -#include "cru/ui/render/ScrollRenderObject.hpp" -#include "cru/ui/render/TextRenderObject.hpp" - -namespace cru::ui::controls { -constexpr int k_default_caret_blink_duration = 500; - -// TControl should inherits `Control` and has following methods: -// ``` -// gsl::not_null<render::TextRenderObject*> GetTextRenderObject(); -// render::ScrollRenderObject* GetScrollRenderObject(); -// ``` -template <typename TControl> -class TextControlService : public Object { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::controls::TextControlService") - - public: - TextControlService(gsl::not_null<TControl*> control) : control_(control) {} - - CRU_DELETE_COPY(TextControlService) - CRU_DELETE_MOVE(TextControlService) - - ~TextControlService() override { - const auto application = GetUiApplication(); - // Don't call TearDownCaret, because it use text render object of control, - // which may be destroyed already. - application->CancelTimer(this->caret_timer_id_); - } - - 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(); - } - } else { - this->AbortSelection(); - this->event_revoker_guards_.clear(); - this->TearDownCaret(); - } - } - - bool IsEditable() { return this->editable_; } - - void SetEditable(bool editable) { - this->editable_ = editable; - this->input_method_context_.reset(); - } - - 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); - if (stop_composition && this->input_method_context_) { - this->input_method_context_->CancelComposition(); - } - CoerceSelection(); - SyncTextRenderObject(); - } - - std::optional<platform::native::CompositionText> GetCompositionInfo() { - if (this->input_method_context_ == nullptr) return std::nullopt; - auto composition_info = this->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<render::TextRenderObject*> 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) { - if (const auto scroll_render_object = this->GetScrollRenderObject()) { - const auto caret_rect = this->GetTextRenderObject()->GetCaretRect(); - // TODO: Wait a tick for layout completed. - this->GetScrollRenderObject()->ScrollToContain(caret_rect, - Thickness{5.f}); - } - } - } - - void DeleteSelectedText() { - auto selection = GetSelection().Normalize(); - if (selection.count == 0) return; - this->text_.erase(this->text_.cbegin() + selection.GetStart(), - this->text_.cbegin() + selection.GetEnd()); - SetSelection(selection.GetStart()); - } - - 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(); - - // Cancel first anyhow for safety. - application->CancelTimer(this->caret_timer_id_); - - this->GetTextRenderObject()->SetDrawCaret(true); - this->caret_timer_id_ = application->SetInterval( - std::chrono::milliseconds(this->caret_blink_duration_), - [this] { this->GetTextRenderObject()->ToggleDrawCaret(); }); - } - - void TearDownCaret() { - const auto application = GetUiApplication(); - application->CancelTimer(this->caret_timer_id_); - 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()); - } - } - - template <typename TArgs> - void SetupOneHandler(event::RoutedEvent<TArgs>* (Control::*event)(), - void (TextControlService::*handler)( - typename event::RoutedEvent<TArgs>::EventArgs)) { - this->event_revoker_guards_.push_back( - EventRevokerGuard{(this->control_->*event)()->Direct()->AddHandler( - std::bind(handler, this, std::placeholders::_1))}); - } - - void StartSelection(Index start) { - SetSelection(start); - 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); - log::TagDebug(log_tag, u"Text selection updated, range: {}, {}.", - selection.GetStart(), selection.GetEnd()); - } - - void SetupHandlers() { - Expects(event_revoker_guards_.empty()); - - SetupOneHandler(&Control::MouseMoveEvent, - &TextControlService::MouseMoveHandler); - SetupOneHandler(&Control::MouseDownEvent, - &TextControlService::MouseDownHandler); - SetupOneHandler(&Control::MouseUpEvent, - &TextControlService::MouseUpHandler); - SetupOneHandler(&Control::KeyDownEvent, - &TextControlService::KeyDownHandler); - SetupOneHandler(&Control::KeyUpEvent, &TextControlService::KeyUpHandler); - SetupOneHandler(&Control::GainFocusEvent, - &TextControlService::GainFocusHandler); - SetupOneHandler(&Control::LoseFocusEvent, - &TextControlService::LoseFocusHandler); - } - - 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) { - this->control_->RequestFocus(); - if (this->select_down_button_.has_value()) { - return; - } else { - if (!this->control_->CaptureMouse()) return; - if (!this->control_->RequestFocus()) return; - const auto text_render_object = this->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::native::KeyCode; - using cru::platform::native::KeyModifiers; - - switch (key_code) { - case KeyCode::Backspace: { - if (!IsEditable()) return; - const auto selection = GetSelection(); - if (selection.count == 0) { - const auto text = this->GetTextView(); - const auto caret_position = GetCaretPosition(); - if (caret_position == 0) return; - gsl::index new_position; - Utf16PreviousCodePoint(text, caret_position, &new_position); - text_.erase(text_.cbegin() + new_position, - text_.cbegin() + caret_position); - SetSelection(new_position); - } else { - this->DeleteSelectedText(); - } - } break; - case KeyCode::Delete: { - if (!IsEditable()) return; - const auto selection = GetSelection(); - if (selection.count == 0) { - const auto text = this->GetTextView(); - const auto caret_position = GetCaretPosition(); - if (caret_position == static_cast<gsl::index>(text.size())) return; - gsl::index new_position; - Utf16NextCodePoint(text, caret_position, &new_position); - text_.erase(text_.cbegin() + caret_position, - text_.cbegin() + new_position); - SyncTextRenderObject(); - } 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; - } - } - - void KeyUpHandler(event::KeyEventArgs& args) { CRU_UNUSED(args); } - - void GainFocusHandler(event::FocusChangeEventArgs& args) { - CRU_UNUSED(args); - if (editable_) { - UiHost* ui_host = this->control_->GetUiHost(); - auto window = ui_host->GetNativeWindowResolver()->Resolve(); - if (window == nullptr) return; - input_method_context_ = - GetUiApplication()->GetInputMethodManager()->GetContext(window); - input_method_context_->EnableIME(); - auto sync = [this](std::nullptr_t) { this->SyncTextRenderObject(); }; - input_method_context_->CompositionStartEvent()->AddHandler( - [this](std::nullptr_t) { this->DeleteSelectedText(); }); - input_method_context_->CompositionEvent()->AddHandler(sync); - input_method_context_->CompositionEndEvent()->AddHandler(sync); - input_method_context_->TextEvent()->AddHandler( - [this](const std::u16string_view& text) { - if (text == u"\b") return; - this->text_.insert(GetCaretPosition(), text); - this->SetSelection(GetCaretPosition() + text.size()); - }); - } - } - - void LoseFocusHandler(event::FocusChangeEventArgs& args) { - if (!args.IsWindow()) this->AbortSelection(); - if (input_method_context_) { - input_method_context_->DisableIME(); - input_method_context_.reset(); - } - SyncTextRenderObject(); - } - - private: - gsl::not_null<TControl*> control_; - std::vector<EventRevokerGuard> event_revoker_guards_; - - std::u16string text_; - TextRange selection_; - - bool enable_ = false; - bool editable_ = false; - - bool caret_visible_ = false; - long long caret_timer_id_ = -1; - int caret_blink_duration_ = k_default_caret_blink_duration; - - // nullopt means not selecting - std::optional<MouseButton> select_down_button_; - - std::unique_ptr<platform::native::IInputMethodContext> input_method_context_; -}; // namespace cru::ui::controls -} // namespace cru::ui::controls diff --git a/src/ui/controls/TextHostControlService.cpp b/src/ui/controls/TextHostControlService.cpp new file mode 100644 index 00000000..07b4f1e8 --- /dev/null +++ b/src/ui/controls/TextHostControlService.cpp @@ -0,0 +1,469 @@ +#include "cru/ui/controls/TextHostControlService.hpp" + +#include "../Helper.hpp" +#include "cru/common/Logger.hpp" +#include "cru/common/StringUtil.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/platform/gui/InputMethod.hpp" +#include "cru/platform/gui/Keyboard.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/platform/gui/Window.hpp" +#include "cru/ui/Base.hpp" +#include "cru/ui/DebugFlags.hpp" +#include "cru/ui/events/UiEvent.hpp" +#include "cru/ui/helper/ShortcutHub.hpp" +#include "cru/ui/host/WindowHost.hpp" +#include "cru/ui/render/ScrollRenderObject.hpp" +#include "cru/ui/render/TextRenderObject.hpp" + +namespace cru::ui::controls { +TextHostControlService::TextHostControlService(gsl::not_null<Control*> control) + : control_(control), + text_host_control_(dynamic_cast<ITextHostControl*>(control.get())) { + SetUpShortcuts(); + + SetupOneHandler(&Control::MouseMoveEvent, + &TextHostControlService::MouseMoveHandler); + SetupOneHandler(&Control::MouseDownEvent, + &TextHostControlService::MouseDownHandler); + SetupOneHandler(&Control::MouseUpEvent, + &TextHostControlService::MouseUpHandler); + SetupOneHandler(&Control::GainFocusEvent, + &TextHostControlService::GainFocusHandler); + SetupOneHandler(&Control::LoseFocusEvent, + &TextHostControlService::LoseFocusHandler); + + shortcut_hub_.Install(control_); +} + +void TextHostControlService::SetEnabled(bool enable) { + if (enable == this->enable_) return; + this->enable_ = enable; + if (enable) { + if (this->caret_visible_) { + this->SetupCaret(); + } + this->control_->SetCursor( + GetUiApplication()->GetCursorManager()->GetSystemCursor( + platform::gui::SystemCursorType::IBeam)); + } else { + this->AbortSelection(); + this->TearDownCaret(); + this->control_->SetCursor(nullptr); + } +} + +void TextHostControlService::SetEditable(bool editable) { + this->editable_ = editable; + if (!editable) CancelComposition(); +} + +void TextHostControlService::SetText(std::u16string text, + bool stop_composition) { + this->text_ = std::move(text); + CoerceSelection(); + if (stop_composition) { + CancelComposition(); + } + SyncTextRenderObject(); +} + +void TextHostControlService::InsertText(gsl::index position, + std::u16string_view text, + bool stop_composition) { + if (!Utf16IsValidInsertPosition(this->text_, position)) { + log::TagError(log_tag, u"Invalid text insert position."); + return; + } + this->text_.insert(this->text_.cbegin() + position, text.begin(), text.end()); + if (stop_composition) { + CancelComposition(); + } + SyncTextRenderObject(); +} + +void TextHostControlService::DeleteChar(gsl::index position, + bool stop_composition) { + if (!Utf16IsValidInsertPosition(this->text_, position)) { + log::TagError(log_tag, u"Invalid text delete position."); + return; + } + if (position == static_cast<gsl::index>(this->text_.size())) return; + Index next; + Utf16NextCodePoint(this->text_, position, &next); + this->DeleteText(TextRange::FromTwoSides(position, next), stop_composition); +} + +// Return the position of deleted character. +gsl::index TextHostControlService::DeleteCharPrevious(gsl::index position, + bool stop_composition) { + if (!Utf16IsValidInsertPosition(this->text_, position)) { + log::TagError(log_tag, u"Invalid text delete position."); + return 0; + } + if (position == 0) return 0; + Index previous; + Utf16PreviousCodePoint(this->text_, position, &previous); + this->DeleteText(TextRange::FromTwoSides(previous, position), + stop_composition); + return previous; +} + +void TextHostControlService::DeleteText(TextRange range, + bool stop_composition) { + if (range.count == 0) return; + range = range.Normalize(); + if (!Utf16IsValidInsertPosition(this->text_, range.GetStart())) { + log::TagError(log_tag, u"Invalid text delete start position."); + return; + } + if (!Utf16IsValidInsertPosition(this->text_, range.GetStart())) { + log::TagError(log_tag, u"Invalid text delete end position."); + return; + } + this->text_.erase(this->text_.cbegin() + range.GetStart(), + this->text_.cbegin() + range.GetEnd()); + this->CoerceSelection(); + if (stop_composition) { + CancelComposition(); + } + this->SyncTextRenderObject(); +} + +platform::gui::IInputMethodContext* +TextHostControlService ::GetInputMethodContext() { + host::WindowHost* host = this->control_->GetWindowHost(); + if (!host) return nullptr; + platform::gui::INativeWindow* native_window = host->GetNativeWindow(); + if (!native_window) return nullptr; + return native_window->GetInputMethodContext(); +} + +void TextHostControlService::CancelComposition() { + auto input_method_context = GetInputMethodContext(); + if (input_method_context == nullptr) return; + input_method_context->CancelComposition(); +} + +std::optional<platform::gui::CompositionText> +TextHostControlService::GetCompositionInfo() { + auto input_method_context = GetInputMethodContext(); + if (input_method_context == nullptr) return std::nullopt; + auto composition_info = input_method_context->GetCompositionText(); + if (composition_info.text.empty()) return std::nullopt; + return composition_info; +} + +void TextHostControlService::SetCaretVisible(bool visible) { + if (visible == this->caret_visible_) return; + + this->caret_visible_ = visible; + + if (this->enable_) { + if (visible) { + this->SetupCaret(); + } else { + this->TearDownCaret(); + } + } +} + +void TextHostControlService::SetCaretBlinkDuration(int milliseconds) { + if (this->caret_blink_duration_ == milliseconds) return; + + if (this->enable_ && this->caret_visible_) { + this->TearDownCaret(); + this->SetupCaret(); + } +} + +void TextHostControlService::ScrollToCaret() { + if (const auto scroll_render_object = this->GetScrollRenderObject()) { + this->control_->GetWindowHost()->RunAfterLayoutStable( + [this, scroll_render_object]() { + const auto caret_rect = this->GetTextRenderObject()->GetCaretRect(); + scroll_render_object->ScrollToContain(caret_rect, Thickness{5.f}); + }); + } +} + +gsl::not_null<render::TextRenderObject*> +TextHostControlService::GetTextRenderObject() { + return this->text_host_control_->GetTextRenderObject(); +} + +render::ScrollRenderObject* TextHostControlService::GetScrollRenderObject() { + return this->text_host_control_->GetScrollRenderObject(); +} + +void TextHostControlService::SetSelection(gsl::index caret_position) { + this->SetSelection(TextRange{caret_position, 0}); +} + +void TextHostControlService::SetSelection(TextRange selection, + bool scroll_to_caret) { + this->selection_ = selection; + CoerceSelection(); + SyncTextRenderObject(); + if (scroll_to_caret) { + this->ScrollToCaret(); + } +} + +void TextHostControlService::ChangeSelectionEnd(Index new_end) { + auto selection = GetSelection(); + selection.ChangeEnd(new_end); + this->SetSelection(selection); +} + +void TextHostControlService::AbortSelection() { + if (this->mouse_move_selecting_) { + this->control_->ReleaseMouse(); + this->mouse_move_selecting_ = false; + } + SetSelection(GetCaretPosition()); +} + +void TextHostControlService::ReplaceSelectedText(std::u16string_view text) { + DeleteSelectedText(); + InsertText(GetSelection().GetStart(), text); + SetSelection(GetSelection().GetStart() + text.size()); +} + +void TextHostControlService::DeleteSelectedText() { + this->DeleteText(GetSelection()); + SetSelection(GetSelection().Normalize().GetStart()); +} + +void TextHostControlService::SetupCaret() { + const auto application = GetUiApplication(); + this->GetTextRenderObject()->SetDrawCaret(true); + this->caret_timer_canceler_.Reset(application->SetInterval( + std::chrono::milliseconds(this->caret_blink_duration_), + [this] { this->GetTextRenderObject()->ToggleDrawCaret(); })); +} + +void TextHostControlService::TearDownCaret() { + this->caret_timer_canceler_.Reset(); + this->GetTextRenderObject()->SetDrawCaret(false); +} + +void TextHostControlService::CoerceSelection() { + this->selection_ = this->selection_.CoerceInto(0, text_.size()); +} + +void TextHostControlService::SyncTextRenderObject() { + const auto text_render_object = this->GetTextRenderObject(); + const auto composition_info = this->GetCompositionInfo(); + if (composition_info) { + const auto caret_position = GetCaretPosition(); + auto text = this->text_; + text.insert(caret_position, composition_info->text); + text_render_object->SetText(text); + text_render_object->SetCaretPosition(caret_position + + composition_info->selection.GetEnd()); + auto selection = composition_info->selection; + selection.position += caret_position; + text_render_object->SetSelectionRange(selection); + } else { + text_render_object->SetText(this->text_); + text_render_object->SetCaretPosition(this->GetCaretPosition()); + text_render_object->SetSelectionRange(this->GetSelection()); + } +} + +void TextHostControlService::UpdateInputMethodPosition() { + if (auto input_method_context = this->GetInputMethodContext()) { + Point right_bottom = + this->GetTextRenderObject()->GetTotalOffset() + + this->GetTextRenderObject()->GetCaretRect().GetRightBottom(); + right_bottom.x += 5; + right_bottom.y += 5; + + if constexpr (debug_flags::text_service) { + log::TagDebug(log_tag, + u"Calculate input method candidate window position: {}.", + right_bottom.ToDebugString()); + } + + input_method_context->SetCandidateWindowPosition(right_bottom); + } +} + +void TextHostControlService::MouseDownHandler( + event::MouseButtonEventArgs& args) { + if (IsEnabled()) { + this->control_->SetFocus(); + if (args.GetButton() == mouse_buttons::left && + !this->mouse_move_selecting_) { + if (!this->control_->CaptureMouse()) return; + this->mouse_move_selecting_ = true; + const auto text_render_object = this->GetTextRenderObject(); + const auto result = text_render_object->TextHitTest( + args.GetPointToContent(text_render_object)); + const auto position = result.position + (result.trailing ? 1 : 0); + SetSelection(position); + } + } +} + +void TextHostControlService::MouseUpHandler(event::MouseButtonEventArgs& args) { + if (args.GetButton() == mouse_buttons::left && mouse_move_selecting_) { + this->control_->ReleaseMouse(); + this->mouse_move_selecting_ = false; + } +} + +void TextHostControlService::MouseMoveHandler(event::MouseEventArgs& args) { + if (this->mouse_move_selecting_) { + const auto text_render_object = this->GetTextRenderObject(); + const auto result = text_render_object->TextHitTest( + args.GetPointToContent(text_render_object)); + const auto position = result.position + (result.trailing ? 1 : 0); + ChangeSelectionEnd(position); + } +} + +void TextHostControlService::GainFocusHandler( + event::FocusChangeEventArgs& args) { + CRU_UNUSED(args); + if (editable_) { + auto input_method_context = GetInputMethodContext(); + if (input_method_context == nullptr) return; + input_method_context->EnableIME(); + auto sync = [this](std::nullptr_t) { + this->SyncTextRenderObject(); + ScrollToCaret(); + }; + input_method_context_event_guard_ += + input_method_context->CompositionStartEvent()->AddHandler( + [this](std::nullptr_t) { this->DeleteSelectedText(); }); + input_method_context_event_guard_ += + input_method_context->CompositionEvent()->AddHandler(sync); + input_method_context_event_guard_ += + input_method_context->CompositionEndEvent()->AddHandler(sync); + input_method_context_event_guard_ += + input_method_context->TextEvent()->AddHandler( + [this](const std::u16string_view& text) { + this->ReplaceSelectedText(text); + }); + + host::WindowHost* window_host = control_->GetWindowHost(); + if (window_host) + input_method_context_event_guard_ += + window_host->AfterLayoutEvent()->AddHandler( + [this](auto) { this->UpdateInputMethodPosition(); }); + SetCaretVisible(true); + } +} + +void TextHostControlService::LoseFocusHandler( + event::FocusChangeEventArgs& args) { + if (!args.IsWindow()) this->AbortSelection(); + input_method_context_event_guard_.Clear(); + auto input_method_context = GetInputMethodContext(); + if (input_method_context) { + input_method_context->DisableIME(); + } + SetCaretVisible(false); + SyncTextRenderObject(); +} + +void TextHostControlService::SetUpShortcuts() { + using platform::gui::KeyCode; + using platform::gui::KeyModifiers; + + shortcut_hub_.RegisterShortcut(u"Backspace", KeyCode::Backspace, [this] { + if (!IsEnabled()) return false; + if (!IsEditable()) return false; + const auto selection = GetSelection(); + if (selection.count == 0) { + SetSelection(DeleteCharPrevious(GetCaretPosition())); + } else { + this->DeleteSelectedText(); + } + return true; + }); + + shortcut_hub_.RegisterShortcut(u"Delete", KeyCode::Delete, [this] { + if (!IsEnabled()) return false; + if (!IsEditable()) return false; + const auto selection = GetSelection(); + if (selection.count == 0) { + DeleteChar(GetCaretPosition()); + } else { + this->DeleteSelectedText(); + } + return true; + }); + + shortcut_hub_.RegisterShortcut(u"Left", KeyCode::Left, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + Utf16PreviousCodePoint(text, caret, &caret); + this->SetSelection(caret); + return true; + }); + + shortcut_hub_.RegisterShortcut(u"ShiftLeft", + {KeyCode::Left, KeyModifiers::shift}, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + Utf16PreviousCodePoint(text, caret, &caret); + this->ChangeSelectionEnd(caret); + return true; + }); + + shortcut_hub_.RegisterShortcut( + u"CtrlLeft", {KeyCode::Left, KeyModifiers::ctrl}, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + this->SetSelection(Utf16PreviousWord(text, caret)); + return true; + }); + + shortcut_hub_.RegisterShortcut( + u"CtrlShiftLeft", + {KeyCode::Left, KeyModifiers::ctrl | KeyModifiers::shift}, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + this->ChangeSelectionEnd(Utf16PreviousWord(text, caret)); + return true; + }); + + shortcut_hub_.RegisterShortcut(u"Right", KeyCode::Right, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + Utf16NextCodePoint(text, caret, &caret); + this->SetSelection(caret); + return true; + }); + + shortcut_hub_.RegisterShortcut(u"ShiftRight", + {KeyCode::Right, KeyModifiers::shift}, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + Utf16NextCodePoint(text, caret, &caret); + this->ChangeSelectionEnd(caret); + return true; + }); + + shortcut_hub_.RegisterShortcut( + u"CtrlRight", {KeyCode::Right, KeyModifiers::ctrl}, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + this->SetSelection(Utf16NextWord(text, caret)); + return true; + }); + + shortcut_hub_.RegisterShortcut( + u"CtrlShiftRight", + {KeyCode::Right, KeyModifiers::ctrl | KeyModifiers::shift}, [this] { + auto text = this->GetTextView(); + auto caret = this->GetCaretPosition(); + this->ChangeSelectionEnd(Utf16NextWord(text, caret)); + return true; + }); +} +} // namespace cru::ui::controls diff --git a/src/ui/controls/Window.cpp b/src/ui/controls/Window.cpp new file mode 100644 index 00000000..ba66f42e --- /dev/null +++ b/src/ui/controls/Window.cpp @@ -0,0 +1,24 @@ +#include "cru/ui/controls/Window.hpp" + +#include "cru/common/Base.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/ui/controls/RootControl.hpp" +#include "cru/ui/host/WindowHost.hpp" +#include "cru/ui/render/Base.hpp" +#include "cru/ui/render/StackLayoutRenderObject.hpp" + +namespace cru::ui::controls { +Window* Window::Create(Control* attached_control) { + return new Window(attached_control); +} + +Window::Window(Control* attached_control) : RootControl(attached_control) {} + +Window::~Window() {} + +gsl::not_null<platform::gui::INativeWindow*> Window::CreateNativeWindow( + gsl::not_null<host::WindowHost*> host, + platform::gui::INativeWindow* parent) { + return host->CreateNativeWindow({parent}); +} +} // namespace cru::ui::controls |