diff options
author | crupest <crupest@outlook.com> | 2021-03-24 19:14:19 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2021-03-24 19:14:19 +0800 |
commit | 7f15a1ff9a2007e119798053083a0a87d042990a (patch) | |
tree | cb35c01a7eaee867376d959b96c9bbd15df939e5 | |
parent | 74956951ee663012df0c3fe4ebe29799cb2f7732 (diff) | |
parent | 7703063a5816b089483e78ccd74bb9902ccfbea8 (diff) | |
download | cru-7f15a1ff9a2007e119798053083a0a87d042990a.tar.gz cru-7f15a1ff9a2007e119798053083a0a87d042990a.tar.bz2 cru-7f15a1ff9a2007e119798053083a0a87d042990a.zip |
Merge branch 'master' of https://github.com/crupest/CruUI
203 files changed, 6208 insertions, 2892 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa5d19e3..3aa088a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: submodules: true - name: Restore artifacts, or run vcpkg, build and cache artifacts - uses: lukka/run-vcpkg@v3 + uses: lukka/run-vcpkg@v6 id: runvcpkg with: vcpkgArguments: "${{ env.vcpkgPackages }}" diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 90a89d15..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,39 +0,0 @@ -trigger: -- master - -jobs: - - job: Windows_Build - pool: - vmImage: 'windows-2019' - strategy: - matrix: - x86_debug: - buildPlatform: 'x86' - buildConfiguration: 'Debug' - x86_release: - buildPlatform: 'x86' - buildConfiguration: 'Release' - x64_debug: - buildPlatform: 'x64' - buildConfiguration: 'Debug' - x64_release: - buildPlatform: 'x64' - buildConfiguration: 'Release' - - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.x' - addToPath: true - - - script: python tools\win_build.py -a $(buildPlatform) -c $(buildConfiguration) - - - task: CopyFiles@2 - inputs: - contents: '**\$(buildConfiguration)\**\?(*.exe|*.dll|*.pdb)' - targetFolder: '$(build.artifactStagingDirectory)' - - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: '$(build.artifactStagingDirectory)' - artifactName: 'executables_$(buildPlatform)_$(buildConfiguration)' diff --git a/demos/CMakeLists.txt b/demos/CMakeLists.txt index 64612bf9..16159d08 100644 --- a/demos/CMakeLists.txt +++ b/demos/CMakeLists.txt @@ -1,7 +1,7 @@ add_library(cru_demo_base INTERFACE) if(WIN32) - target_link_libraries(cru_demo_base INTERFACE cru_win_native) + target_link_libraries(cru_demo_base INTERFACE cru_win_gui) endif() add_subdirectory(main) diff --git a/demos/input_method/main.cpp b/demos/input_method/main.cpp index bff0e1d3..5a6dc942 100644 --- a/demos/input_method/main.cpp +++ b/demos/input_method/main.cpp @@ -1,25 +1,22 @@ -#include "cru/platform/graph/Factory.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/platform/graphics/Factory.hpp" +#include "cru/platform/graphics/Font.hpp" +#include "cru/platform/graphics/Painter.hpp" +#include "cru/platform/gui/InputMethod.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/platform/gui/Window.hpp" int main() { using namespace cru::platform; - using namespace cru::platform::graph; - using namespace cru::platform::native; + using namespace cru::platform::graphics; + using namespace cru::platform::gui; auto application = CreateUiApplication(); auto graph_factory = application->GetGraphFactory(); - auto window_resolver = application->CreateWindow(nullptr); + auto window = application->CreateWindow(nullptr); - auto window = window_resolver->Resolve(); - - auto input_method_context = - application->GetInputMethodManager()->GetContext(window); + auto input_method_context = window->GetInputMethodContext(); auto brush = graph_factory->CreateSolidColorBrush(); brush->SetColor(colors::black); diff --git a/demos/main/main.cpp b/demos/main/main.cpp index 82038731..541a2320 100644 --- a/demos/main/main.cpp +++ b/demos/main/main.cpp @@ -1,23 +1,20 @@ #include "cru/platform/HeapDebug.hpp" -#include "cru/platform/native/UiApplication.hpp" -#include "cru/platform/native/Window.hpp" -#include "cru/ui/UiHost.hpp" -#include "cru/ui/Window.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/platform/gui/Window.hpp" +#include "cru/ui/Base.hpp" #include "cru/ui/controls/Button.hpp" #include "cru/ui/controls/FlexLayout.hpp" -#include "cru/ui/controls/StackLayout.hpp" #include "cru/ui/controls/TextBlock.hpp" #include "cru/ui/controls/TextBox.hpp" +#include "cru/ui/controls/Window.hpp" +#include "cru/ui/host/WindowHost.hpp" -using cru::platform::native::CreateUiApplication; -using cru::ui::Rect; -using cru::ui::Thickness; -using cru::ui::Window; +using cru::platform::gui::CreateUiApplication; using cru::ui::controls::Button; using cru::ui::controls::FlexLayout; -using cru::ui::controls::StackLayout; using cru::ui::controls::TextBlock; using cru::ui::controls::TextBox; +using cru::ui::controls::Window; int main() { #ifdef CRU_DEBUG @@ -26,37 +23,27 @@ int main() { auto application = CreateUiApplication(); - const auto window = Window::CreateOverlapped(); + const auto window = Window::Create(); const auto flex_layout = FlexLayout::Create(); + flex_layout->SetFlexDirection(cru::ui::FlexDirection::Vertical); + flex_layout->SetContentMainAlign(cru::ui::FlexCrossAlignment::Center); + flex_layout->SetItemCrossAlign(cru::ui::FlexCrossAlignment::Center); - window->SetChild(flex_layout); + window->AddChild(flex_layout, 0); - const auto button = Button::Create(); - const auto text_block1 = TextBlock::Create(); - text_block1->SetText(u"Hello World!"); - button->SetChild(text_block1); - flex_layout->AddChild(button, 0); - - const auto text_block2 = TextBlock::Create(); - text_block2->SetText(u"Hello World!"); - - const auto text_block3 = TextBlock::Create(); - text_block3->SetText(u"Overlapped text"); + const auto text_block = TextBlock::Create(u"Hello World from CruUI!", true); + flex_layout->AddChild(text_block, 0); - const auto stack_layout = StackLayout::Create(); - stack_layout->AddChild(text_block2, 0); - stack_layout->AddChild(text_block3, 1); - flex_layout->AddChild(stack_layout, 1); - - const auto text_block4 = TextBlock::Create(); - text_block4->SetText(u"Hello World!!!"); - flex_layout->AddChild(text_block4, 2); + const auto button_text_block = TextBlock::Create(u"OK"); + const auto button = Button::Create(); + button->SetChild(button_text_block); + flex_layout->AddChild(button, 1); const auto text_box = TextBox::Create(); - flex_layout->AddChild(text_box, 3); + flex_layout->AddChild(text_box, 2); - window->GetUiHost()->GetNativeWindowResolver()->Resolve()->SetVisible(true); + window->Show(); return application->Run(); } diff --git a/include/cru/common/Base.hpp b/include/cru/common/Base.hpp index a5a9421d..560f83bb 100644 --- a/include/cru/common/Base.hpp +++ b/include/cru/common/Base.hpp @@ -1,8 +1,8 @@ #pragma once #include "PreConfig.hpp" +#include <exception> #include <gsl/gsl> - #include <stdexcept> #define CRU_UNUSED(entity) static_cast<void>(entity); @@ -42,12 +42,17 @@ struct Interface { virtual ~Interface() = default; }; -[[noreturn]] inline void UnreachableCode() { - throw std::runtime_error("Unreachable code."); -} +[[noreturn]] inline void UnreachableCode() { std::terminate(); } using Index = gsl::index; +// https://www.boost.org/doc/libs/1_54_0/doc/html/hash/reference.html#boost.hash_combine +template <class T> +inline void hash_combine(std::size_t& s, const T& v) { + std::hash<T> h; + s ^= h(v) + 0x9e3779b9 + (s << 6) + (s >> 2); +} + #define CRU_DEFINE_CLASS_LOG_TAG(tag) \ private: \ constexpr static std::u16string_view log_tag = tag; diff --git a/include/cru/common/ClonablePtr.hpp b/include/cru/common/ClonablePtr.hpp new file mode 100644 index 00000000..5e4b80c9 --- /dev/null +++ b/include/cru/common/ClonablePtr.hpp @@ -0,0 +1,204 @@ +#pragma once + +#include <cstddef> +#include <functional> +#include <memory> +#include <type_traits> + +namespace cru { +template <typename TClonable> +class ClonablePtr { + template <typename T> + friend class ClonablePtr; + + public: + using element_type = typename std::unique_ptr<TClonable>::element_type; + using pointer = typename std::unique_ptr<TClonable>::pointer; + + ClonablePtr() = default; + ClonablePtr(std::nullptr_t) noexcept : ptr_(nullptr) {} + explicit ClonablePtr(pointer p) noexcept : ptr_(p) {} + ClonablePtr(std::unique_ptr<element_type>&& p) noexcept + : ptr_(std::move(p)) {} + template <typename O, + std::enable_if_t<std::is_convertible_v< + typename ClonablePtr<O>::pointer, pointer>, + int> = 0> + ClonablePtr(std::unique_ptr<O>&& p) : ptr_(std::move(p)) {} + ClonablePtr(const ClonablePtr& other) : ptr_(other.ptr_->Clone()) {} + ClonablePtr(ClonablePtr&& other) = default; + template <typename O, + std::enable_if_t<std::is_convertible_v< + typename ClonablePtr<O>::pointer, pointer>, + int> = 0> + ClonablePtr(const ClonablePtr<O>& other) : ptr_(other.ptr_->Clone()) {} + template <typename O, + std::enable_if_t<std::is_convertible_v< + typename ClonablePtr<O>::pointer, pointer>, + int> = 0> + ClonablePtr(ClonablePtr<O>&& other) noexcept : ptr_(std::move(other.ptr_)) {} + ClonablePtr& operator=(std::nullptr_t) noexcept { + ptr_ = nullptr; + return *this; + } + ClonablePtr& operator=(std::unique_ptr<element_type>&& other) noexcept { + ptr_ = std::move(other); + return *this; + } + template <typename O, + std::enable_if_t<std::is_convertible_v< + typename ClonablePtr<O>::pointer, pointer>, + int> = 0> + ClonablePtr& operator=(std::unique_ptr<O>&& p) noexcept { + ptr_ = std::move(p); + return *this; + } + ClonablePtr& operator=(const ClonablePtr& other) { + if (this != &other) { + ptr_ = std::unique_ptr<element_type>(other.ptr_->Clone()); + } + return *this; + } + ClonablePtr& operator=(ClonablePtr&& other) = default; + template <typename O, + std::enable_if_t<std::is_convertible_v< + typename ClonablePtr<O>::pointer, pointer>, + int> = 0> + ClonablePtr& operator=(const ClonablePtr<O>& other) noexcept { + if (this != &other) { + ptr_ = std::unique_ptr<element_type>(other.ptr_->Clone()); + } + return *this; + } + template <typename O, + std::enable_if_t<std::is_convertible_v< + typename ClonablePtr<O>::pointer, pointer>, + int> = 0> + ClonablePtr& operator=(ClonablePtr<O>&& other) noexcept { + ptr_ = std::move(other.ptr_); + } + + ~ClonablePtr() = default; + + public: + pointer release() noexcept { return ptr_.release(); } + void reset(pointer p = pointer()) noexcept { ptr_.reset(p); } + void swap(ClonablePtr& other) noexcept { ptr_.swap(other.ptr_); } + + public: + pointer get() const noexcept { return ptr_.get(); } + + operator bool() const noexcept { return ptr_; } + + element_type& operator*() const noexcept { return *ptr_; } + pointer operator->() const noexcept { return ptr_.get(); } + + private: + std::unique_ptr<element_type> ptr_; +}; + +template <typename T> +void swap(ClonablePtr<T>& left, ClonablePtr<T>& right) noexcept { + left.swap(right); +} + +template <typename T> +bool operator==(const ClonablePtr<T>& left, const ClonablePtr<T>& right) { + return left.get() == right.get(); +} + +template <typename T> +bool operator!=(const ClonablePtr<T>& left, const ClonablePtr<T>& right) { + return left.get() != right.get(); +} + +template <typename T> +bool operator<(const ClonablePtr<T>& left, const ClonablePtr<T>& right) { + return left.get() < right.get(); +} + +template <typename T> +bool operator<=(const ClonablePtr<T>& left, const ClonablePtr<T>& right) { + return left.get() <= right.get(); +} + +template <typename T> +bool operator>(const ClonablePtr<T>& left, const ClonablePtr<T>& right) { + return left.get() > right.get(); +} + +template <typename T> +bool operator>=(const ClonablePtr<T>& left, const ClonablePtr<T>& right) { + return left.get() >= right.get(); +} + +template <typename T> +bool operator==(const ClonablePtr<T>& left, std::nullptr_t) { + return left.get() == nullptr; +} + +template <typename T> +bool operator!=(const ClonablePtr<T>& left, std::nullptr_t) { + return left.get() != nullptr; +} + +template <typename T> +bool operator<(const ClonablePtr<T>& left, std::nullptr_t) { + return left.get() < nullptr; +} + +template <typename T> +bool operator<=(const ClonablePtr<T>& left, std::nullptr_t) { + return left.get() <= nullptr; +} + +template <typename T> +bool operator>(const ClonablePtr<T>& left, std::nullptr_t) { + return left.get() > nullptr; +} + +template <typename T> +bool operator>=(const ClonablePtr<T>& left, std::nullptr_t) { + return left.get() >= nullptr; +} + +template <typename T> +bool operator==(std::nullptr_t, const ClonablePtr<T>& right) { + return nullptr == right.get(); +} + +template <typename T> +bool operator!=(std::nullptr_t, const ClonablePtr<T>& right) { + return nullptr != right.get(); +} + +template <typename T> +bool operator<(std::nullptr_t, const ClonablePtr<T>& right) { + return nullptr < right.get(); +} + +template <typename T> +bool operator<=(std::nullptr_t, const ClonablePtr<T>& right) { + return nullptr <= right.get(); +} + +template <typename T> +bool operator>(std::nullptr_t, const ClonablePtr<T>& right) { + return nullptr > right.get(); +} + +template <typename T> +bool operator>=(std::nullptr_t, const ClonablePtr<T>& right) { + return nullptr >= right.get(); +} + +} // namespace cru + +namespace std { +template <typename T> +struct hash<cru::ClonablePtr<T>> { + std::size_t operator()(const cru::ClonablePtr<T>& p) const { + return std::hash<typename cru::ClonablePtr<T>::pointer>(p.get()); + } +}; +} // namespace std diff --git a/include/cru/common/Event.hpp b/include/cru/common/Event.hpp index 377ca7f3..b6999aa4 100644 --- a/include/cru/common/Event.hpp +++ b/include/cru/common/Event.hpp @@ -5,14 +5,19 @@ #include <algorithm> #include <functional> +#include <initializer_list> #include <memory> #include <utility> +#include <variant> #include <vector> namespace cru { class EventRevoker; namespace details { +template <class> +inline constexpr bool always_false_v = false; + // Base class of all Event<T...>. // It erases event args types and provides a // unified form to create event revoker and @@ -24,10 +29,8 @@ class EventBase : public SelfResolvable<EventBase> { using EventHandlerToken = long; EventBase() {} - EventBase(const EventBase& other) = delete; - EventBase(EventBase&& other) = delete; - EventBase& operator=(const EventBase& other) = delete; - EventBase& operator=(EventBase&& other) = delete; + CRU_DELETE_COPY(EventBase) + CRU_DEFAULT_MOVE(EventBase) virtual ~EventBase() = default; // Remove the handler with the given token. If the token @@ -84,78 +87,114 @@ using DeducedEventArgs = std::conditional_t< std::is_lvalue_reference_v<TRaw>, TRaw, std::conditional_t<std::is_scalar_v<TRaw>, TRaw, const TRaw&>>; +struct IBaseEvent { + protected: + IBaseEvent() = default; + CRU_DELETE_COPY(IBaseEvent) + CRU_DEFAULT_MOVE(IBaseEvent) + ~IBaseEvent() = default; // Note that user can't destroy a Event via IEvent. + // So destructor should be protected. + + using SpyOnlyHandler = std::function<void()>; + + public: + virtual EventRevoker AddSpyOnlyHandler(SpyOnlyHandler handler) = 0; +}; + // Provides an interface of event. // IEvent only allow to add handler but not to raise the event. You may // want to create an Event object and expose IEvent only so users won't // be able to emit the event. template <typename TEventArgs> -struct IEvent { +struct IEvent : virtual IBaseEvent { public: using EventArgs = DeducedEventArgs<TEventArgs>; using EventHandler = std::function<void(EventArgs)>; + using ShortCircuitHandler = std::function<bool(EventArgs)>; protected: IEvent() = default; - IEvent(const IEvent& other) = delete; - IEvent(IEvent&& other) = delete; - IEvent& operator=(const IEvent& other) = delete; - IEvent& operator=(IEvent&& other) = delete; + CRU_DELETE_COPY(IEvent) + CRU_DEFAULT_MOVE(IEvent) ~IEvent() = default; // Note that user can't destroy a Event via IEvent. So // destructor should be protected. public: - virtual EventRevoker AddHandler(const EventHandler& handler) = 0; - virtual EventRevoker AddHandler(EventHandler&& handler) = 0; + virtual EventRevoker AddHandler(EventHandler handler) = 0; + virtual EventRevoker AddShortCircuitHandler(ShortCircuitHandler handler) = 0; + virtual EventRevoker PrependShortCircuitHandler( + ShortCircuitHandler handler) = 0; }; // A non-copyable non-movable Event class. // It stores a list of event handlers. template <typename TEventArgs> class Event : public details::EventBase, public IEvent<TEventArgs> { + using typename IEvent<TEventArgs>::EventArgs; + + using typename IBaseEvent::SpyOnlyHandler; using typename IEvent<TEventArgs>::EventHandler; + using typename IEvent<TEventArgs>::ShortCircuitHandler; private: struct HandlerData { - HandlerData(EventHandlerToken token, EventHandler handler) - : token(token), handler(handler) {} + HandlerData(EventHandlerToken token, ShortCircuitHandler handler) + : token(token), handler(std::move(handler)) {} EventHandlerToken token; - EventHandler handler; + ShortCircuitHandler handler; }; public: Event() = default; - Event(const Event&) = delete; - Event& operator=(const Event&) = delete; - Event(Event&&) = delete; - Event& operator=(Event&&) = delete; + CRU_DELETE_COPY(Event) + CRU_DEFAULT_MOVE(Event) ~Event() = default; - EventRevoker AddHandler(const EventHandler& handler) override { + EventRevoker AddSpyOnlyHandler(SpyOnlyHandler handler) override { + return AddShortCircuitHandler([handler = std::move(handler)](EventArgs) { + handler(); + return false; + }); + } + + EventRevoker AddHandler(EventHandler handler) override { + return AddShortCircuitHandler( + [handler = std::move(handler)](EventArgs args) { + handler(args); + return false; + }); + } + + // Handler return true to short circuit following handlers. + EventRevoker AddShortCircuitHandler(ShortCircuitHandler handler) override { const auto token = current_token_++; - this->handler_data_list_.emplace_back(token, handler); + this->handler_data_list_.emplace_back(token, std::move(handler)); return CreateRevoker(token); } - EventRevoker AddHandler(EventHandler&& handler) override { + // Handler return true to short circuit following handlers. + EventRevoker PrependShortCircuitHandler( + ShortCircuitHandler handler) override { const auto token = current_token_++; - this->handler_data_list_.emplace_back(token, std::move(handler)); + this->handler_data_list_.emplace(this->handler_data_list_.cbegin(), token, + std::move(handler)); return CreateRevoker(token); } // This method will make a copy of all handlers. Because user might delete a - // handler in a handler, which may lead to seg fault as the handler is deleted - // while being executed. - // Thanks to this behavior, all handlers will be taken a snapshot when Raise - // is called, so even if you delete a handler during this period, all handlers - // in the snapshot will be executed. - void Raise(typename IEvent<TEventArgs>::EventArgs args) { - std::vector<EventHandler> handlers; + // handler in a handler, which may lead to seg fault as the handler is + // deleted while being executed. Thanks to this behavior, all handlers will + // be taken a snapshot when Raise is called, so even if you delete a handler + // during this period, all handlers in the snapshot will be executed. + void Raise(EventArgs args) { + std::vector<ShortCircuitHandler> handlers; handlers.reserve(this->handler_data_list_.size()); for (const auto& data : this->handler_data_list_) { handlers.push_back(data.handler); } for (const auto& handler : handlers) { - handler(args); + auto short_circuit = handler(args); + if (short_circuit) return; } } @@ -183,6 +222,7 @@ struct EventRevokerDestroyer { }; } // namespace details +// A guard class for event revoker. Automatically revoke it when destroyed. class EventRevokerGuard { public: EventRevokerGuard() = default; @@ -201,7 +241,9 @@ class EventRevokerGuard { return *revoker_; } - void Release() { revoker_.release(); } + EventRevoker Release() { return std::move(*revoker_.release()); } + + void Reset() { revoker_.reset(); } void Reset(EventRevoker&& revoker) { revoker_.reset(new EventRevoker(std::move(revoker))); @@ -209,5 +251,32 @@ class EventRevokerGuard { private: std::unique_ptr<EventRevoker, details::EventRevokerDestroyer> revoker_; -}; // namespace cru +}; + +class EventRevokerListGuard { + public: + EventRevokerListGuard() = default; + EventRevokerListGuard(const EventRevokerListGuard& other) = delete; + EventRevokerListGuard(EventRevokerListGuard&& other) = default; + EventRevokerListGuard& operator=(const EventRevokerListGuard& other) = delete; + EventRevokerListGuard& operator=(EventRevokerListGuard&& other) = default; + ~EventRevokerListGuard() = default; + + public: + void Add(EventRevoker&& revoker) { + event_revoker_guard_list_.push_back(EventRevokerGuard(std::move(revoker))); + } + + EventRevokerListGuard& operator+=(EventRevoker&& revoker) { + this->Add(std::move(revoker)); + return *this; + } + + void Clear() { event_revoker_guard_list_.clear(); } + + bool IsEmpty() const { return event_revoker_guard_list_.empty(); } + + private: + std::vector<EventRevokerGuard> event_revoker_guard_list_; +}; } // namespace cru diff --git a/include/cru/common/Format.hpp b/include/cru/common/Format.hpp new file mode 100644 index 00000000..59f34036 --- /dev/null +++ b/include/cru/common/Format.hpp @@ -0,0 +1,23 @@ +#pragma once +#include "Base.hpp" + +#include "StringUtil.hpp" + +#include <array> +#include <charconv> +#include <string> +#include <string_view> +#include <system_error> +#include <type_traits> + +namespace cru { +template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> +std::u16string ToUtf16String(T number) { + std::array<char, 40> buffer; + auto result = + std::to_chars(buffer.data(), buffer.data() + buffer.size(), number); + Ensures(result.ec == std::errc()); + std::string_view utf8_result(buffer.data(), result.ptr - buffer.data()); + return ToUtf16(utf8_result); +} +} // namespace cru diff --git a/include/cru/common/HandlerRegistry.hpp b/include/cru/common/HandlerRegistry.hpp new file mode 100644 index 00000000..bd74a9e0 --- /dev/null +++ b/include/cru/common/HandlerRegistry.hpp @@ -0,0 +1,86 @@ +#pragma once +#include "Base.hpp" + +#include <algorithm> +#include <functional> +#include <utility> +#include <vector> + +namespace cru { + +template <typename T> +class HandlerRegistryIterator { + public: + using RawIterator = + typename std::vector<std::pair<int, std::function<T>>>::const_iterator; + + explicit HandlerRegistryIterator(RawIterator raw) : raw_(std::move(raw)) {} + + CRU_DELETE_COPY(HandlerRegistryIterator) + CRU_DELETE_MOVE(HandlerRegistryIterator) + + ~HandlerRegistryIterator() = default; + + const std::function<T>& operator*() const { return raw_->second; } + const std::function<T>* operator->() const { return &raw_->second; } + + HandlerRegistryIterator& operator++() { + ++raw_; + return *this; + } + + HandlerRegistryIterator operator++(int) { + auto c = *this; + this->operator++(); + return c; + } + + bool operator==(const HandlerRegistryIterator<T>& other) const { + return this->raw_ == other.raw_; + } + + bool operator!=(const HandlerRegistryIterator<T>& other) const { + return !this->operator==(other); + } + + private: + RawIterator raw_; +}; + +template <typename T> +class HandlerRegistry final { + public: + HandlerRegistry() = default; + CRU_DEFAULT_COPY(HandlerRegistry) + CRU_DEFAULT_MOVE(HandlerRegistry) + ~HandlerRegistry() = default; + + public: + int AddHandler(std::function<T> handler) { + auto id = current_id_++; + handler_list_.push_back({id, std::move(handler)}); + } + + void RemoveHandler(int id) { + auto result = std::find_if(handler_list_.cbegin(), handler_list_.cend(), + [id](const std::pair<int, std::function<T>>& d) { + return d.first == id; + }); + if (result != handler_list_.cend()) { + handler_list_.erase(result); + } + } + + HandlerRegistryIterator<T> begin() const { + return HandlerRegistryIterator<T>(handler_list_.begin()); + } + + HandlerRegistryIterator<T> end() const { + return HandlerRegistryIterator<T>(handler_list_.begin()); + } + + private: + int current_id_ = 1; + std::vector<std::pair<int, std::function<T>>> handler_list_; +}; +} // namespace cru diff --git a/include/cru/common/Logger.hpp b/include/cru/common/Logger.hpp index 4ea17c09..daf2e7d2 100644 --- a/include/cru/common/Logger.hpp +++ b/include/cru/common/Logger.hpp @@ -40,6 +40,7 @@ class Logger : public Object { std::list<std::unique_ptr<ILogSource>> sources_; }; +// TODO: Remove argument evaluation in Debug. template <typename... TArgs> void Debug([[maybe_unused]] TArgs&&... args) { #ifdef CRU_DEBUG @@ -66,6 +67,7 @@ void Error(TArgs&&... args) { fmt::format(std::forward<TArgs>(args)...)); } +// TODO: Remove argument evaluation in Debug. template <typename... TArgs> void TagDebug([[maybe_unused]] std::u16string_view tag, [[maybe_unused]] TArgs&&... args) { diff --git a/include/cru/common/SelfResolvable.hpp b/include/cru/common/SelfResolvable.hpp index 94f3ae87..eaa4ce34 100644 --- a/include/cru/common/SelfResolvable.hpp +++ b/include/cru/common/SelfResolvable.hpp @@ -39,9 +39,27 @@ class SelfResolvable { SelfResolvable() : resolver_(new T*(static_cast<T*>(this))) {} SelfResolvable(const SelfResolvable&) = delete; SelfResolvable& operator=(const SelfResolvable&) = delete; - SelfResolvable(SelfResolvable&&) = delete; - SelfResolvable& operator=(SelfResolvable&&) = delete; - virtual ~SelfResolvable() { (*resolver_) = nullptr; } + + // Resolvers to old object will resolve to new object. + SelfResolvable(SelfResolvable&& other) + : resolver_(std::move(other.resolver_)) { + (*resolver_) = static_cast<T*>(this); + } + + // Old resolvers for this object will resolve to nullptr. + // Other's resolvers will now resolve to this. + SelfResolvable& operator=(SelfResolvable&& other) { + if (this != &other) { + (*resolver_) = nullptr; + resolver_ = std::move(other.resolver_); + (*resolver_) = static_cast<T*>(this); + } + return *this; + } + + virtual ~SelfResolvable() { + if (resolver_ != nullptr) (*resolver_) = nullptr; + } ObjectResolver<T> CreateResolver() { return ObjectResolver<T>(resolver_); } diff --git a/include/cru/common/StringUtil.hpp b/include/cru/common/StringUtil.hpp index 5dacfa12..62999d53 100644 --- a/include/cru/common/StringUtil.hpp +++ b/include/cru/common/StringUtil.hpp @@ -1,6 +1,10 @@ #pragma once #include "Base.hpp" +#include <functional> +#include <string> +#include <string_view> + namespace cru { using CodePoint = std::int32_t; constexpr CodePoint k_invalid_code_point = -1; @@ -124,4 +128,21 @@ void Utf16EncodeCodePointAppend(CodePoint code_point, std::u16string& str); std::string ToUtf8(std::u16string_view s); std::u16string ToUtf16(std::string_view s); + +// If given s is not a valid utf16 string, return value is UD. +bool Utf16IsValidInsertPosition(std::u16string_view s, gsl::index position); + +// Return position after the character making predicate returns true or 0 if no +// character doing so. +gsl::index Utf16BackwardUntil(std::u16string_view str, gsl::index position, + const std::function<bool(CodePoint)>& predicate); +// Return position before the character making predicate returns true or +// str.size() if no character doing so. +gsl::index Utf16ForwardUntil(std::u16string_view str, gsl::index position, + const std::function<bool(CodePoint)>& predicate); + +gsl::index Utf16PreviousWord(std::u16string_view str, gsl::index position, + bool* is_space = nullptr); +gsl::index Utf16NextWord(std::u16string_view str, gsl::index position, + bool* is_space = nullptr); } // namespace cru diff --git a/include/cru/platform/GraphBase.hpp b/include/cru/platform/GraphBase.hpp index 186ee9d0..6bf2736f 100644 --- a/include/cru/platform/GraphBase.hpp +++ b/include/cru/platform/GraphBase.hpp @@ -1,9 +1,13 @@ #pragma once #include "cru/common/Base.hpp" +#include "cru/common/Format.hpp" + +#include <fmt/core.h> #include <cstdint> #include <limits> #include <optional> +#include <string> #include <utility> namespace cru::platform { @@ -14,6 +18,16 @@ struct Point final { constexpr Point(const float x, const float y) : x(x), y(y) {} explicit constexpr Point(const Size& size); + std::u16string ToDebugString() const { + return fmt::format(u"({}, {})", ToUtf16String(x), ToUtf16String(y)); + } + + constexpr Point& operator+=(const Point& other) { + this->x += other.x; + this->y += other.y; + return *this; + } + float x = 0; float y = 0; }; @@ -46,6 +60,11 @@ struct Size final { std::numeric_limits<float>::max()}; } + std::u16string ToDebugString() const { + return fmt::format(u"({}, {})", ToUtf16String(width), + ToUtf16String(height)); + } + float width = 0; float height = 0; }; @@ -258,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; @@ -297,6 +316,12 @@ struct Color { (hex >> 24) & mask); } + constexpr Color WithAlpha(std::uint8_t new_alpha) const { + auto result = *this; + result.alpha = new_alpha; + return result; + } + std::uint8_t red; std::uint8_t green; std::uint8_t blue; diff --git a/include/cru/platform/Matrix.hpp b/include/cru/platform/Matrix.hpp index e702df90..8ec5faaa 100644 --- a/include/cru/platform/Matrix.hpp +++ b/include/cru/platform/Matrix.hpp @@ -50,10 +50,15 @@ struct Matrix { return Matrix{1.0f, 0.0f, 0.0f, 1.0f, x, y}; } + static Matrix Translation(const Point& point) { + return Translation(point.x, point.y); + } + static Matrix Scale(float sx, float sy) { return Matrix{sx, 0.0f, 0.0f, sy, 0.0f, 0.0f}; } + // Clockwise. static Matrix Rotation(float angle) { float r = AngleToRadian(angle); float s = std::sin(r); diff --git a/include/cru/platform/graph/Base.hpp b/include/cru/platform/graphics/Base.hpp index 61cfc5ef..e751ebdb 100644 --- a/include/cru/platform/graph/Base.hpp +++ b/include/cru/platform/graphics/Base.hpp @@ -5,7 +5,7 @@ #include <memory> -namespace cru::platform::graph { +namespace cru::platform::graphics { // forward declarations struct IGraphFactory; struct IBrush; diff --git a/include/cru/platform/graph/Brush.hpp b/include/cru/platform/graphics/Brush.hpp index e67384de..10c666b5 100644 --- a/include/cru/platform/graph/Brush.hpp +++ b/include/cru/platform/graphics/Brush.hpp @@ -1,7 +1,7 @@ #pragma once #include "Resource.hpp" -namespace cru::platform::graph { +namespace cru::platform::graphics { struct IBrush : virtual IGraphResource {}; struct ISolidColorBrush : virtual IBrush { diff --git a/include/cru/platform/graph/Factory.hpp b/include/cru/platform/graphics/Factory.hpp index b4e68f12..f9018e13 100644 --- a/include/cru/platform/graph/Factory.hpp +++ b/include/cru/platform/graphics/Factory.hpp @@ -9,7 +9,7 @@ #include <string> #include <string_view> -namespace cru::platform::graph { +namespace cru::platform::graphics { // Entry point of the graph module. struct IGraphFactory : virtual INativeResource { virtual std::unique_ptr<ISolidColorBrush> CreateSolidColorBrush() = 0; @@ -21,5 +21,11 @@ struct IGraphFactory : virtual INativeResource { virtual std::unique_ptr<ITextLayout> CreateTextLayout( std::shared_ptr<IFont> font, std::u16string text) = 0; + + std::unique_ptr<ISolidColorBrush> CreateSolidColorBrush(const Color& color) { + std::unique_ptr<ISolidColorBrush> brush = CreateSolidColorBrush(); + brush->SetColor(color); + return brush; + } }; -} // namespace cru::platform::graph +} // namespace cru::platform::graphics diff --git a/include/cru/platform/graph/Font.hpp b/include/cru/platform/graphics/Font.hpp index 182cc15b..70392a69 100644 --- a/include/cru/platform/graph/Font.hpp +++ b/include/cru/platform/graphics/Font.hpp @@ -1,7 +1,7 @@ #pragma once #include "Resource.hpp" -namespace cru::platform::graph { +namespace cru::platform::graphics { struct IFont : virtual IGraphResource { virtual float GetFontSize() = 0; }; diff --git a/include/cru/platform/graph/Geometry.hpp b/include/cru/platform/graphics/Geometry.hpp index 354efd97..b0ce6ad9 100644 --- a/include/cru/platform/graph/Geometry.hpp +++ b/include/cru/platform/graphics/Geometry.hpp @@ -1,7 +1,7 @@ #pragma once #include "Resource.hpp" -namespace cru::platform::graph { +namespace cru::platform::graphics { struct IGeometry : virtual IGraphResource { virtual bool FillContains(const Point& point) = 0; }; diff --git a/include/cru/platform/graph/Painter.hpp b/include/cru/platform/graphics/Painter.hpp index 27ae420b..f75ea52b 100644 --- a/include/cru/platform/graph/Painter.hpp +++ b/include/cru/platform/graphics/Painter.hpp @@ -1,7 +1,7 @@ #pragma once #include "Resource.hpp" -namespace cru::platform::graph { +namespace cru::platform::graphics { struct IPainter : virtual INativeResource { virtual Matrix GetTransform() = 0; @@ -9,6 +9,8 @@ struct IPainter : virtual INativeResource { virtual void Clear(const Color& color) = 0; + virtual void DrawLine(const Point& start, const Point& end, IBrush* brush, + float width) = 0; virtual void StrokeRectangle(const Rect& rectangle, IBrush* brush, float width) = 0; virtual void FillRectangle(const Rect& rectangle, IBrush* brush) = 0; @@ -26,4 +28,4 @@ struct IPainter : virtual INativeResource { virtual void EndDraw() = 0; }; -} // namespace cru::platform::graph +} // namespace cru::platform::graphics diff --git a/include/cru/platform/graph/Resource.hpp b/include/cru/platform/graphics/Resource.hpp index 8859360c..a1625ce4 100644 --- a/include/cru/platform/graph/Resource.hpp +++ b/include/cru/platform/graphics/Resource.hpp @@ -1,7 +1,7 @@ #pragma once #include "Base.hpp" -namespace cru::platform::graph { +namespace cru::platform::graphics { struct IGraphFactory; struct IGraphResource : virtual INativeResource { diff --git a/include/cru/platform/graph/TextLayout.hpp b/include/cru/platform/graphics/TextLayout.hpp index a101983f..b363fb77 100644 --- a/include/cru/platform/graph/TextLayout.hpp +++ b/include/cru/platform/graphics/TextLayout.hpp @@ -4,7 +4,7 @@ #include <string> #include <vector> -namespace cru::platform::graph { +namespace cru::platform::graphics { struct ITextLayout : virtual IGraphResource { virtual std::u16string GetText() = 0; virtual std::u16string_view GetTextView() = 0; @@ -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<Rect> 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/platform/graph/util/Painter.hpp b/include/cru/platform/graphics/util/Painter.hpp index f9aec027..90457cf4 100644 --- a/include/cru/platform/graph/util/Painter.hpp +++ b/include/cru/platform/graphics/util/Painter.hpp @@ -4,14 +4,14 @@ #include <functional> #include <type_traits> -namespace cru::platform::graph::util { +namespace cru::platform::graphics::util { template <typename Fn> void WithTransform(IPainter* painter, const Matrix& matrix, const Fn& action) { static_assert(std::is_invocable_v<decltype(action), IPainter*>, "Action must can be be invoked with painter."); const auto old = painter->GetTransform(); - painter->SetTransform(old * matrix); + painter->SetTransform(matrix * old); action(painter); painter->SetTransform(old); } -} // namespace cru::platform::graph::util +} // namespace cru::platform::graphics::util diff --git a/include/cru/platform/native/Base.hpp b/include/cru/platform/gui/Base.hpp index bba7b960..7a9d1889 100644 --- a/include/cru/platform/native/Base.hpp +++ b/include/cru/platform/gui/Base.hpp @@ -1,23 +1,18 @@ #pragma once +#include "Keyboard.hpp" #include "cru/common/Base.hpp" #include "cru/common/Bitmask.hpp" -#include "cru/platform/graph/Base.hpp" -#include "Keyboard.hpp" +#include "cru/platform/graphics/Base.hpp" -namespace cru::platform::native { +#include "../Resource.hpp" + +namespace cru::platform::gui { struct ICursor; struct ICursorManager; struct IUiApplication; struct INativeWindow; -struct INativeWindowResolver; -struct IInputMethodManager; struct IInputMethodContext; -struct Dpi { - float x; - float y; -}; - namespace details { struct TagMouseButton {}; } // namespace details @@ -30,11 +25,6 @@ constexpr MouseButton middle{0b10}; constexpr MouseButton right{0b100}; } // namespace mouse_buttons -enum class SystemCursorType { - Arrow, - Hand, -}; - struct NativeMouseButtonEventArgs { MouseButton button; Point point; @@ -49,4 +39,4 @@ struct NativeKeyEventArgs { enum class FocusChangeType { Gain, Lost }; enum class MouseEnterLeaveType { Enter, Leave }; -} // namespace cru::platform::native +} // namespace cru::platform::gui diff --git a/include/cru/platform/native/Cursor.hpp b/include/cru/platform/gui/Cursor.hpp index 6c8f8068..316496a0 100644 --- a/include/cru/platform/native/Cursor.hpp +++ b/include/cru/platform/gui/Cursor.hpp @@ -1,10 +1,11 @@ #pragma once -#include "../Resource.hpp" #include "Base.hpp" #include <memory> -namespace cru::platform::native { +namespace cru::platform::gui { +enum class SystemCursorType { Arrow, Hand, IBeam }; + struct ICursor : virtual INativeResource {}; struct ICursorManager : virtual INativeResource { @@ -12,4 +13,4 @@ struct ICursorManager : virtual INativeResource { // TODO: Add method to create cursor. }; -} // namespace cru::platform::native +} // namespace cru::platform::gui diff --git a/include/cru/platform/gui/DebugFlags.hpp b/include/cru/platform/gui/DebugFlags.hpp new file mode 100644 index 00000000..2b7c7c19 --- /dev/null +++ b/include/cru/platform/gui/DebugFlags.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace cru::platform::gui { +struct DebugFlags { + static constexpr int paint = 0; + static constexpr int input_method = 0; +}; +} // namespace cru::platform::gui diff --git a/include/cru/platform/native/InputMethod.hpp b/include/cru/platform/gui/InputMethod.hpp index 6f222a43..9d090eab 100644 --- a/include/cru/platform/native/InputMethod.hpp +++ b/include/cru/platform/gui/InputMethod.hpp @@ -1,5 +1,4 @@ #pragma once -#include "../Resource.hpp" #include "Base.hpp" #include "cru/common/Event.hpp" @@ -8,7 +7,7 @@ #include <memory> #include <vector> -namespace cru::platform::native { +namespace cru::platform::gui { struct CompositionClause { int start; int end; @@ -38,7 +37,8 @@ struct IInputMethodContext : virtual INativeResource { virtual CompositionText GetCompositionText() = 0; - // Set the candidate window lefttop. Use this method to prepare typing. + // Set the candidate window lefttop. Relative to window lefttop. Use this + // method to prepare typing. virtual void SetCandidateWindowPosition(const Point& point) = 0; // Triggered when user starts composition. @@ -52,22 +52,17 @@ struct IInputMethodContext : virtual INativeResource { virtual IEvent<std::u16string_view>* TextEvent() = 0; }; - -struct IInputMethodManager : virtual INativeResource { - virtual std::unique_ptr<IInputMethodContext> GetContext( - INativeWindow* window) = 0; -}; -} // namespace cru::platform::native +} // namespace cru::platform::gui template <> -struct fmt::formatter<cru::platform::native::CompositionText, char16_t> +struct fmt::formatter<cru::platform::gui::CompositionText, char16_t> : fmt::formatter<std::u16string_view, char16_t> { auto parse(fmt::basic_format_parse_context<char16_t>& ctx) { return fmt::formatter<std::u16string_view, char16_t>::parse(ctx); } template <typename FormatContext> - auto format(const cru::platform::native::CompositionText& ct, + auto format(const cru::platform::gui::CompositionText& ct, FormatContext& ctx) { auto output = ctx.out(); output = format_to(output, u"text: {}\n", ct.text); diff --git a/include/cru/platform/native/Keyboard.hpp b/include/cru/platform/gui/Keyboard.hpp index 83c61bcc..6c29239b 100644 --- a/include/cru/platform/native/Keyboard.hpp +++ b/include/cru/platform/gui/Keyboard.hpp @@ -1,7 +1,10 @@ #pragma once #include "cru/common/Bitmask.hpp" -namespace cru::platform::native { +#include <string> +#include <string_view> + +namespace cru::platform::gui { // Because of the complexity of keyboard layout, I only add code in US keyboard // layout, the most widely used layout in China. We should try to make it easy // to add new keyboard layout. @@ -113,8 +116,13 @@ struct TagKeyModifier {}; using KeyModifier = Bitmask<details::TagKeyModifier>; struct KeyModifiers { + static constexpr KeyModifier none{0}; static constexpr KeyModifier shift{0b1}; static constexpr KeyModifier ctrl{0b10}; static constexpr KeyModifier alt{0b100}; }; -} // namespace cru::platform::native + +std::u16string_view ToString(KeyCode key_code); +std::u16string ToString(KeyModifier key_modifier, + std::u16string_view separator = u"+"); +} // namespace cru::platform::gui diff --git a/include/cru/platform/gui/UiApplication.hpp b/include/cru/platform/gui/UiApplication.hpp new file mode 100644 index 00000000..5a5b0b13 --- /dev/null +++ b/include/cru/platform/gui/UiApplication.hpp @@ -0,0 +1,135 @@ +#pragma once +#include "Base.hpp" + +#include "cru/common/Bitmask.hpp" + +#include <chrono> +#include <functional> +#include <memory> +#include <vector> + +namespace cru::platform::gui { +namespace details { +struct CreateWindowFlagTag; +} + +using CreateWindowFlag = Bitmask<details::CreateWindowFlagTag>; + +struct CreateWindowFlags { + static constexpr CreateWindowFlag NoCaptionAndBorder{0b1}; +}; + +// The entry point of a ui application. +struct IUiApplication : public virtual INativeResource { + public: + static IUiApplication* GetInstance() { return instance; } + + private: + static IUiApplication* instance; + + protected: + IUiApplication(); + + public: + ~IUiApplication() override; + + // Block current thread and run the message loop. Return the exit code when + // message loop gets a quit message (possibly posted by method RequestQuit). + virtual int Run() = 0; + + // Post a quit message with given quit code. + virtual void RequestQuit(int quit_code) = 0; + + virtual void AddOnQuitHandler(std::function<void()> handler) = 0; + + // Timer id should always be positive (not 0) and never the same. So it's ok + // to use negative value (or 0) to represent no timer. + virtual long long SetImmediate(std::function<void()> action) = 0; + virtual long long SetTimeout(std::chrono::milliseconds milliseconds, + std::function<void()> action) = 0; + virtual long long SetInterval(std::chrono::milliseconds milliseconds, + std::function<void()> action) = 0; + // Implementation should guarantee calls on timer id already canceled have no + // effects and do not crash. Also canceling negative id or 0 should always + // result in no-op. + virtual void CancelTimer(long long id) = 0; + + virtual std::vector<INativeWindow*> GetAllWindow() = 0; + + INativeWindow* CreateWindow(INativeWindow* parent) { + return this->CreateWindow(parent, CreateWindowFlag(0)); + }; + virtual INativeWindow* CreateWindow(INativeWindow* parent, + CreateWindowFlag flags) = 0; + + virtual cru::platform::graphics::IGraphFactory* GetGraphFactory() = 0; + + virtual ICursorManager* GetCursorManager() = 0; +}; + +class TimerAutoCanceler { + public: + TimerAutoCanceler() : id_(0) {} + explicit TimerAutoCanceler(long long id) : id_(id) {} + + CRU_DELETE_COPY(TimerAutoCanceler) + + TimerAutoCanceler(TimerAutoCanceler&& other) : id_(other.id_) { + other.id_ = 0; + } + + TimerAutoCanceler& operator=(TimerAutoCanceler&& other) { + if (&other == this) { + return *this; + } + Reset(other.id_); + other.id_ = 0; + return *this; + } + + TimerAutoCanceler& operator=(long long other) { + return this->operator=(TimerAutoCanceler(other)); + } + + ~TimerAutoCanceler() { Reset(); } + + long long Release() { + auto temp = id_; + id_ = 0; + return temp; + } + + void Reset(long long id = 0) { + if (id_ > 0) IUiApplication::GetInstance()->CancelTimer(id_); + id_ = id; + } + + explicit operator bool() const { return id_; } + + private: + long long id_; +}; + +class TimerListAutoCanceler { + public: + TimerListAutoCanceler() = default; + CRU_DELETE_COPY(TimerListAutoCanceler) + CRU_DEFAULT_MOVE(TimerListAutoCanceler) + ~TimerListAutoCanceler() = default; + + TimerListAutoCanceler& operator+=(long long id) { + list_.push_back(TimerAutoCanceler(id)); + return *this; + } + + void Clear() { list_.clear(); } + + bool IsEmpty() const { return list_.empty(); } + + private: + std::vector<TimerAutoCanceler> list_; +}; + +// Bootstrap from this. +std::unique_ptr<IUiApplication> CreateUiApplication(); +} // namespace cru::platform::gui diff --git a/include/cru/platform/native/Window.hpp b/include/cru/platform/gui/Window.hpp index 1fcac1fc..26d1a476 100644 --- a/include/cru/platform/native/Window.hpp +++ b/include/cru/platform/gui/Window.hpp @@ -1,21 +1,14 @@ #pragma once -#include "../Resource.hpp" #include "Base.hpp" + #include "cru/common/Event.hpp" #include <string_view> -namespace cru::platform::native { +namespace cru::platform::gui { // Represents a native window, which exposes some low-level events and // operations. -// -// Usually you save an INativeWindowResolver after creating a window. Because -// window may be destroyed when user do certain actions like click the close -// button. Then the INativeWindow instance is destroyed and -// INativeWindowResolver::Resolve return nullptr to indicate the fact. struct INativeWindow : virtual INativeResource { - virtual std::shared_ptr<INativeWindowResolver> GetResolver() = 0; - virtual void Close() = 0; virtual INativeWindow* GetParent() = 0; @@ -45,8 +38,9 @@ struct INativeWindow : virtual INativeResource { virtual void RequestRepaint() = 0; // Remember to call EndDraw on return value and destroy it. - virtual std::unique_ptr<graph::IPainter> BeginPaint() = 0; + virtual std::unique_ptr<graphics::IPainter> BeginPaint() = 0; + // Don't use this instance after receive this event. virtual IEvent<std::nullptr_t>* DestroyEvent() = 0; virtual IEvent<std::nullptr_t>* PaintEvent() = 0; virtual IEvent<Size>* ResizeEvent() = 0; @@ -57,11 +51,7 @@ struct INativeWindow : virtual INativeResource { virtual IEvent<NativeMouseButtonEventArgs>* MouseUpEvent() = 0; virtual IEvent<NativeKeyEventArgs>* KeyDownEvent() = 0; virtual IEvent<NativeKeyEventArgs>* KeyUpEvent() = 0; -}; -// See INativeWindow for more info. -struct INativeWindowResolver : virtual INativeResource { - // Think twice before you save the return value. - virtual INativeWindow* Resolve() = 0; + virtual IInputMethodContext* GetInputMethodContext() = 0; }; -} // namespace cru::platform::native +} // namespace cru::platform::gui diff --git a/include/cru/platform/native/UiApplication.hpp b/include/cru/platform/native/UiApplication.hpp deleted file mode 100644 index 1aa4df57..00000000 --- a/include/cru/platform/native/UiApplication.hpp +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once -#include "../Resource.hpp" -#include "Base.hpp" - -#include <chrono> -#include <functional> -#include <memory> -#include <vector> - -namespace cru::platform::native { -// The entry point of a ui application. -struct IUiApplication : public virtual INativeResource { - public: - static IUiApplication* GetInstance() { return instance; } - - private: - static IUiApplication* instance; - - protected: - IUiApplication(); - - public: - ~IUiApplication() override; - - // Block current thread and run the message loop. Return the exit code when - // message loop gets a quit message (possibly posted by method RequestQuit). - virtual int Run() = 0; - - // Post a quit message with given quit code. - virtual void RequestQuit(int quit_code) = 0; - - virtual void AddOnQuitHandler(std::function<void()> handler) = 0; - - virtual void InvokeLater(std::function<void()> action) = 0; - // Timer id should always be positive and never the same. So it's ok to use - // negative value to represent no timer. - virtual long long SetTimeout(std::chrono::milliseconds milliseconds, - std::function<void()> action) = 0; - virtual long long SetInterval(std::chrono::milliseconds milliseconds, - std::function<void()> action) = 0; - // Implementation should guarantee calls on timer id already canceled have no - // effects and do not crash. Also canceling negative id should always result - // in no-op. - virtual void CancelTimer(long long id) = 0; - - virtual std::vector<INativeWindow*> GetAllWindow() = 0; - virtual std::shared_ptr<INativeWindowResolver> CreateWindow( - INativeWindow* parent) = 0; - - virtual cru::platform::graph::IGraphFactory* GetGraphFactory() = 0; - - virtual ICursorManager* GetCursorManager() = 0; - virtual IInputMethodManager* GetInputMethodManager() = 0; -}; - -// Bootstrap from this. -std::unique_ptr<IUiApplication> CreateUiApplication(); -} // namespace cru::platform::native diff --git a/include/cru/ui/Base.hpp b/include/cru/ui/Base.hpp index 6be359ab..fbdfec77 100644 --- a/include/cru/ui/Base.hpp +++ b/include/cru/ui/Base.hpp @@ -1,7 +1,7 @@ #pragma once #include "cru/common/Base.hpp" -#include "cru/platform/graph/Base.hpp" -#include "cru/platform/native/Base.hpp" +#include "cru/platform/graphics/Base.hpp" +#include "cru/platform/gui/Base.hpp" #include <functional> #include <memory> @@ -19,23 +19,35 @@ using cru::platform::RoundedRect; using cru::platform::Size; using cru::platform::TextRange; using cru::platform::Thickness; -using cru::platform::native::MouseButton; +using cru::platform::gui::MouseButton; -namespace mouse_buttons = cru::platform::native::mouse_buttons; +namespace mouse_buttons = cru::platform::gui::mouse_buttons; namespace colors = cru::platform::colors; //-------------------- region: forward declaration -------------------- + +namespace controls { class Window; class Control; -class ClickDetector; -class UiHost; +} // namespace controls + +namespace host { +class WindowHost; +} namespace render { class RenderObject; } +namespace style { +class StyleRuleSet; +class StyleRuleSetBind; +} // namespace style + //-------------------- region: basic types -------------------- +enum class Direction { Horizontal, Vertical }; + namespace internal { constexpr int align_start = 0; constexpr int align_end = align_start + 1; @@ -82,28 +94,20 @@ inline bool operator!=(const CornerRadius& left, const CornerRadius& right) { return !(left == right); } -struct BorderStyle { - std::shared_ptr<platform::graph::IBrush> border_brush; - Thickness border_thickness; - CornerRadius border_radius; - std::shared_ptr<platform::graph::IBrush> foreground_brush; - std::shared_ptr<platform::graph::IBrush> background_brush; -}; - class CanvasPaintEventArgs { public: - CanvasPaintEventArgs(platform::graph::IPainter* painter, + CanvasPaintEventArgs(platform::graphics::IPainter* painter, const Size& paint_size) : painter_(painter), paint_size_(paint_size) {} CRU_DEFAULT_COPY(CanvasPaintEventArgs) CRU_DEFAULT_MOVE(CanvasPaintEventArgs) ~CanvasPaintEventArgs() = default; - platform::graph::IPainter* GetPainter() const { return painter_; } + platform::graphics::IPainter* GetPainter() const { return painter_; } Size GetPaintSize() const { return paint_size_; } private: - platform::graph::IPainter* painter_; + platform::graphics::IPainter* painter_; Size paint_size_; }; diff --git a/include/cru/ui/ContentControl.hpp b/include/cru/ui/ContentControl.hpp deleted file mode 100644 index 19f13a1d..00000000 --- a/include/cru/ui/ContentControl.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include "Control.hpp" - -namespace cru::ui { -class ContentControl : public Control { - protected: - ContentControl(); - - public: - ContentControl(const ContentControl& other) = delete; - ContentControl(ContentControl&& other) = delete; - ContentControl& operator=(const ContentControl& other) = delete; - ContentControl& operator=(ContentControl&& other) = delete; - ~ContentControl() override; - - const std::vector<Control*>& GetChildren() const override final { - return child_vector_; - } - Control* GetChild() const { return child_; } - void SetChild(Control* child); - - protected: - virtual void OnChildChanged(Control* old_child, Control* new_child); - - private: - std::vector<Control*> child_vector_; - Control*& child_; -}; -} // namespace cru::ui diff --git a/include/cru/ui/DebugFlags.hpp b/include/cru/ui/DebugFlags.hpp new file mode 100644 index 00000000..51482135 --- /dev/null +++ b/include/cru/ui/DebugFlags.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace cru::ui::debug_flags { +constexpr bool routed_event = false; +constexpr bool layout = false; +constexpr bool shortcut = false; +constexpr bool text_service = false; +constexpr int click_detector = 0; +} // namespace cru::ui::debug_flags diff --git a/include/cru/ui/LayoutControl.hpp b/include/cru/ui/LayoutControl.hpp deleted file mode 100644 index 7997b37e..00000000 --- a/include/cru/ui/LayoutControl.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include "Control.hpp" - -namespace cru::ui { -class LayoutControl : public Control { - protected: - LayoutControl() = default; - - public: - LayoutControl(const LayoutControl& other) = delete; - LayoutControl(LayoutControl&& other) = delete; - LayoutControl& operator=(const LayoutControl& other) = delete; - LayoutControl& operator=(LayoutControl&& other) = delete; - ~LayoutControl() override; - - const std::vector<Control*>& GetChildren() const override final { - return children_; - } - - void AddChild(Control* control, Index position); - - void RemoveChild(Index position); - - protected: - virtual void OnAddChild(Control* child, Index position); - virtual void OnRemoveChild(Control* child, Index position); - - private: - std::vector<Control*> children_; -}; -} // namespace cru::ui diff --git a/include/cru/ui/UiHost.hpp b/include/cru/ui/UiHost.hpp deleted file mode 100644 index b1658ef6..00000000 --- a/include/cru/ui/UiHost.hpp +++ /dev/null @@ -1,172 +0,0 @@ -#pragma once -#include "Base.hpp" - -#include "cru/common/Event.hpp" -#include "cru/common/SelfResolvable.hpp" -#include "render/Base.hpp" - -namespace cru::ui { -struct AfterLayoutEventArgs {}; - -// The host of all controls and render objects. -// -// 3 situations on destroy: -// 1. Native window destroyed, IsRetainAfterDestroy: false: -// OnNativeDestroy(set native_window_destroyed_ to true, call ~Window due to -// deleting_ is false and IsRetainAfterDestroy is false) -> ~Window -> -// ~UiHost(not destroy native window repeatedly due to native_window_destroyed_ -// is true) -// 2. Native window destroyed, IsRetainAfterDestroy: true: -// OnNativeDestroy(set native_window_destroyed_ to true, not call ~Window -// because deleting_ is false and IsRetainAfterDestroy is true) -// then, ~Window -> ~UiHost(not destroy native window repeatedly due to -// native_window_destroyed_ is true) -// 3. Native window not destroyed, ~Window is called: -// ~Window -> ~UiHost(set deleting_ to true, destroy native window -// due to native_window_destroyed is false) -> OnNativeDestroy(not call ~Window -// due to deleting_ is true and IsRetainAfterDestroy is whatever) -// In conclusion: -// 1. Set native_window_destroyed_ to true at the beginning of OnNativeDestroy. -// 2. Set deleting_ to true at the beginning of ~UiHost. -// 3. Destroy native window when native_window_destroy_ is false in ~Window. -// 4. Delete Window when deleting_ is false and IsRetainAfterDestroy is false in -// OnNativeDestroy. -class UiHost : public Object, public SelfResolvable<UiHost> { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::UiHost") - - public: - // This will create root window render object and attach it to window. - // It will also create and manage a native window. - UiHost(Window* window); - - CRU_DELETE_COPY(UiHost) - CRU_DELETE_MOVE(UiHost) - - ~UiHost() override; - - public: - // Mark the layout as invalid, and arrange a re-layout later. - // This method could be called more than one times in a message cycle. But - // layout only takes place once. - void InvalidateLayout(); - - // Mark the paint as invalid, and arrange a re-paint later. - // This method could be called more than one times in a message cycle. But - // paint only takes place once. - void InvalidatePaint(); - - IEvent<AfterLayoutEventArgs>* AfterLayoutEvent() { - return &after_layout_event_; - } - - void Relayout(); - - // Get current control that mouse hovers on. This ignores the mouse-capture - // control. Even when mouse is captured by another control, this function - // return the control under cursor. You can use `GetMouseCaptureControl` to - // get more info. - Control* GetMouseHoverControl() const { return mouse_hover_control_; } - - //*************** region: focus *************** - - // Request focus for specified control. - bool RequestFocusFor(Control* control); - - // Get the control that has focus. - Control* GetFocusControl(); - - //*************** region: focus *************** - - // Pass nullptr to release capture. If mouse is already capture by a control, - // this capture will fail and return false. If control is identical to the - // capturing control, capture is not changed and this function will return - // true. - // - // When capturing control changes, - // appropriate event will be sent. If mouse is not on the capturing control - // and capture is released, mouse enter event will be sent to the mouse-hover - // control. If mouse is not on the capturing control and capture is set, mouse - // leave event will be sent to the mouse-hover control. - bool CaptureMouseFor(Control* control); - - // Return null if not captured. - Control* GetMouseCaptureControl(); - - Control* HitTest(const Point& point); - - void UpdateCursor(); - - std::shared_ptr<platform::native::INativeWindowResolver> - GetNativeWindowResolver() { - return native_window_resolver_; - } - - bool IsRetainAfterDestroy() { return retain_after_destroy_; } - - void SetRetainAfterDestroy(bool destroy) { retain_after_destroy_ = destroy; } - - private: - //*************** region: native messages *************** - void OnNativeDestroy(platform::native::INativeWindow* window, std::nullptr_t); - void OnNativePaint(platform::native::INativeWindow* window, std::nullptr_t); - void OnNativeResize(platform::native::INativeWindow* window, - const Size& size); - - void OnNativeFocus(platform::native::INativeWindow* window, - cru::platform::native::FocusChangeType focus); - - void OnNativeMouseEnterLeave( - platform::native::INativeWindow* window, - cru::platform::native::MouseEnterLeaveType enter); - void OnNativeMouseMove(platform::native::INativeWindow* window, - const Point& point); - void OnNativeMouseDown( - platform::native::INativeWindow* window, - const platform::native::NativeMouseButtonEventArgs& args); - void OnNativeMouseUp( - platform::native::INativeWindow* window, - const platform::native::NativeMouseButtonEventArgs& args); - - void OnNativeKeyDown(platform::native::INativeWindow* window, - const platform::native::NativeKeyEventArgs& args); - void OnNativeKeyUp(platform::native::INativeWindow* window, - const platform::native::NativeKeyEventArgs& args); - - //*************** region: event dispatcher helper *************** - - void DispatchMouseHoverControlChangeEvent(Control* old_control, - Control* new_control, - const Point& point, bool no_leave, - bool no_enter); - - private: - bool need_layout_ = false; - - Event<AfterLayoutEventArgs> after_layout_event_; - - std::shared_ptr<platform::native::INativeWindowResolver> - native_window_resolver_; - - // See remarks of UiHost. - bool retain_after_destroy_ = false; - // See remarks of UiHost. - bool deleting_ = false; - - // We need this because calling Resolve on resolver in handler of destroy - // event is bad and will always get the dying window. But we need to label the - // window as destroyed so the destructor will not destroy native window - // repeatedly. See remarks of UiHost. - bool native_window_destroyed_ = false; - - std::vector<EventRevokerGuard> event_revoker_guards_; - - Window* window_control_; - std::unique_ptr<render::WindowRenderObject> root_render_object_; - - Control* mouse_hover_control_; - - Control* focus_control_; // "focus_control_" can't be nullptr - - Control* mouse_captured_control_; -}; -} // namespace cru::ui diff --git a/include/cru/ui/UiManager.hpp b/include/cru/ui/UiManager.hpp index e6facdbd..6c0d9500 100644 --- a/include/cru/ui/UiManager.hpp +++ b/include/cru/ui/UiManager.hpp @@ -2,15 +2,22 @@ #include "Base.hpp" #include "controls/Base.hpp" +#include "style/StyleRuleSet.hpp" + +#include <memory> +#include <string> namespace cru::ui { struct ThemeResources { - std::shared_ptr<platform::graph::IFont> default_font; - std::shared_ptr<platform::graph::IBrush> text_brush; - std::shared_ptr<platform::graph::IBrush> text_selection_brush; - std::shared_ptr<platform::graph::IBrush> caret_brush; - controls::ButtonStyle button_style; - controls::TextBoxBorderStyle text_box_border_style; + std::u16string default_font_family; + std::shared_ptr<platform::graphics::IFont> default_font; + std::shared_ptr<platform::graphics::IBrush> text_brush; + std::shared_ptr<platform::graphics::IBrush> text_selection_brush; + std::shared_ptr<platform::graphics::IBrush> caret_brush; + style::StyleRuleSet button_style; + style::StyleRuleSet text_box_style; + + style::StyleRuleSet menu_item_style; }; class UiManager : public Object { diff --git a/include/cru/ui/Window.hpp b/include/cru/ui/Window.hpp deleted file mode 100644 index 450ea97b..00000000 --- a/include/cru/ui/Window.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once -#include "ContentControl.hpp" - -namespace cru::ui { -class Window final : public ContentControl { - friend UiHost; - - public: - static constexpr std::u16string_view control_type = u"Window"; - - public: - static Window* CreateOverlapped(); - - private: - struct tag_overlapped_constructor {}; - - explicit Window(tag_overlapped_constructor); - - public: - Window(const Window& other) = delete; - Window(Window&& other) = delete; - Window& operator=(const Window& other) = delete; - Window& operator=(Window&& other) = delete; - ~Window() override; - - public: - std::u16string_view GetControlType() const final; - - render::RenderObject* GetRenderObject() const override; - - protected: - void OnChildChanged(Control* old_child, Control* new_child) override; - - private: - std::unique_ptr<UiHost> managed_ui_host_; - - // UiHost is responsible to take care of lifetime of this. - render::WindowRenderObject* render_object_; -}; -} // namespace cru::ui diff --git a/include/cru/ui/components/Component.hpp b/include/cru/ui/components/Component.hpp new file mode 100644 index 00000000..0dfc587b --- /dev/null +++ b/include/cru/ui/components/Component.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "../Base.hpp" + +namespace cru::ui::components { +// In destructor, component should check all owned controls whether it is +// attached to window, if not, destroy them, otherwise it is host's duty to +// destroy them. +class Component : public Object { + public: + Component() = default; + + CRU_DELETE_COPY(Component) + CRU_DELETE_MOVE(Component) + + ~Component() = default; + + virtual controls::Control* GetRootControl() = 0; +}; +} // namespace cru::ui::components diff --git a/include/cru/ui/components/Menu.hpp b/include/cru/ui/components/Menu.hpp new file mode 100644 index 00000000..dedf2bd5 --- /dev/null +++ b/include/cru/ui/components/Menu.hpp @@ -0,0 +1,60 @@ +#pragma once +#include "Component.hpp" +#include "cru/common/Base.hpp" +#include "cru/ui/controls/Button.hpp" +#include "cru/ui/controls/Control.hpp" +#include "cru/ui/controls/FlexLayout.hpp" +#include "cru/ui/controls/TextBlock.hpp" + +#include <string> +#include <vector> + +namespace cru::ui::components { +class MenuItem : public Component { + public: + MenuItem(); + explicit MenuItem(std::u16string text); + + CRU_DELETE_COPY(MenuItem) + CRU_DELETE_MOVE(MenuItem) + + ~MenuItem(); + + public: + controls::Control* GetRootControl() override { return container_; } + + void SetText(std::u16string text); + + private: + controls::Button* container_; + controls::TextBlock* text_; +}; + +class Menu : public Component { + public: + Menu(); + + CRU_DELETE_COPY(Menu) + CRU_DELETE_MOVE(Menu) + + ~Menu(); + + public: + gsl::index GetItemCount() const { + return static_cast<gsl::index>(items_.size()); + } + + void AddItem(Component* component) { AddItem(component, GetItemCount()); } + void AddItem(Component* component, gsl::index index); + Component* RemoveItem(gsl::index index); + + void AddTextItem(std::u16string text) { + AddTextItem(std::move(text), GetItemCount()); + } + void AddTextItem(std::u16string text, gsl::index index); + + private: + controls::FlexLayout* container_; + std::vector<Component*> items_; +}; +} // namespace cru::ui::components diff --git a/include/cru/ui/controls/Base.hpp b/include/cru/ui/controls/Base.hpp index b550601b..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 = 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 { - BorderStyle normal; - BorderStyle hover; - BorderStyle focus; - 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 a4f727d6..1c9b1216 100644 --- a/include/cru/ui/controls/Button.hpp +++ b/include/cru/ui/controls/Button.hpp @@ -1,11 +1,16 @@ #pragma once -#include "../ContentControl.hpp" -#include "Base.hpp" +#include "ContentControl.hpp" -#include "../ClickDetector.hpp" +#include "../helper/ClickDetector.hpp" +#include "IBorderControl.hpp" +#include "IClickableControl.hpp" +#include "cru/common/Event.hpp" +#include "cru/ui/style/ApplyBorderStyleInfo.hpp" namespace cru::ui::controls { -class Button : public ContentControl { +class Button : public ContentControl, + public virtual IClickableControl, + public virtual IBorderControl { public: static constexpr std::u16string_view control_type = u"Button"; @@ -26,17 +31,19 @@ class Button : public ContentControl { render::RenderObject* GetRenderObject() const override; public: - const ButtonStyle& GetStyle() const { return style_; } - void SetStyle(ButtonStyle style); + helper::ClickState GetClickState() override { + return click_detector_.GetState(); + } - protected: - void OnChildChanged(Control* old_child, Control* new_child) override; + IEvent<helper::ClickState>* ClickStateChangeEvent() override { + return click_detector_.StateChangeEvent(); + } + + void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) override; private: std::unique_ptr<render::BorderRenderObject> render_object_{}; - ButtonStyle style_; - - ClickDetector click_detector_; + helper::ClickDetector click_detector_; }; } // namespace cru::ui::controls diff --git a/include/cru/ui/controls/Container.hpp b/include/cru/ui/controls/Container.hpp index 304d402c..18958837 100644 --- a/include/cru/ui/controls/Container.hpp +++ b/include/cru/ui/controls/Container.hpp @@ -1,5 +1,5 @@ #pragma once -#include "../ContentControl.hpp" +#include "ContentControl.hpp" namespace cru::ui::controls { class Container : public ContentControl { @@ -19,9 +19,6 @@ class Container : public ContentControl { render::RenderObject* GetRenderObject() const override; - protected: - void OnChildChanged(Control* old_child, Control* new_child) override; - private: std::unique_ptr<render::BorderRenderObject> render_object_; }; diff --git a/include/cru/ui/controls/ContentControl.hpp b/include/cru/ui/controls/ContentControl.hpp new file mode 100644 index 00000000..1bdaf7e4 --- /dev/null +++ b/include/cru/ui/controls/ContentControl.hpp @@ -0,0 +1,38 @@ +#pragma once +#include "Control.hpp" + +#include "cru/ui/render/RenderObject.hpp" + +namespace cru::ui::controls { +class ContentControl : public Control { + protected: + ContentControl() = default; + + public: + ContentControl(const ContentControl& other) = delete; + ContentControl(ContentControl&& other) = delete; + ContentControl& operator=(const ContentControl& other) = delete; + ContentControl& operator=(ContentControl&& other) = delete; + ~ContentControl() override = default; + + Control* GetChild() const; + void SetChild(Control* child); + + protected: + virtual void OnChildChanged(Control* old_child, Control* new_child); + + render::RenderObject* GetContainerRenderObject() const { + return container_render_object_; + } + void SetContainerRenderObject(render::RenderObject* ro) { + container_render_object_ = ro; + } + + private: + using Control::AddChild; + using Control::RemoveChild; + + private: + render::RenderObject* container_render_object_ = nullptr; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/Control.hpp b/include/cru/ui/controls/Control.hpp index bd86bc2f..341b6ef2 100644 --- a/include/cru/ui/Control.hpp +++ b/include/cru/ui/controls/Control.hpp @@ -1,15 +1,15 @@ #pragma once #include "Base.hpp" +#include "../events/UiEvent.hpp" +#include "../render/Base.hpp" #include "cru/common/Event.hpp" -#include "render/Base.hpp" -#include "UiEvent.hpp" #include <string_view> -namespace cru::ui { +namespace cru::ui::controls { class Control : public Object { - friend UiHost; + friend host::WindowHost; protected: Control(); @@ -19,39 +19,31 @@ class Control : public Object { Control(Control&& other) = delete; Control& operator=(const Control& other) = delete; Control& operator=(Control&& other) = delete; - ~Control() override = default; + ~Control() override; public: virtual std::u16string_view GetControlType() const = 0; //*************** region: tree *************** public: - // Get the ui host if attached, otherwise, return nullptr. - UiHost* GetUiHost() const { return ui_host_; } + host::WindowHost* GetWindowHost() const; Control* GetParent() const { return parent_; } - virtual const std::vector<Control*>& GetChildren() const = 0; + const std::vector<Control*>& GetChildren() const { return children_; } // Traverse the tree rooted the control including itself. void TraverseDescendants(const std::function<void(Control*)>& predicate); - void _SetParent(Control* parent); - void _SetDescendantUiHost(UiHost* host); - - private: - static void _TraverseDescendants( - Control* control, const std::function<void(Control*)>& predicate); - public: virtual render::RenderObject* GetRenderObject() const = 0; //*************** region: focus *************** public: - bool RequestFocus(); - bool HasFocus(); + void SetFocus(); + //*************** region: mouse *************** public: bool IsMouseOver() const { return is_mouse_over_; } @@ -66,13 +58,16 @@ class Control : public Object { // Cursor is inherited from parent recursively if not set. public: // null for not set - std::shared_ptr<platform::native::ICursor> GetCursor(); + std::shared_ptr<platform::gui::ICursor> GetCursor(); // will not return nullptr - std::shared_ptr<platform::native::ICursor> GetInheritedCursor(); + std::shared_ptr<platform::gui::ICursor> GetInheritedCursor(); // null to unset - void SetCursor(std::shared_ptr<platform::native::ICursor> cursor); + void SetCursor(std::shared_ptr<platform::gui::ICursor> cursor); + + public: + style::StyleRuleSet* GetStyleRuleSet(); //*************** region: events *************** public: @@ -134,19 +129,29 @@ class Control : public Object { //*************** region: tree *************** protected: + void AddChild(Control* control, Index position); + void RemoveChild(Index position); + virtual void OnAddChild(Control* child, Index position); + virtual void OnRemoveChild(Control* child, Index position); virtual void OnParentChanged(Control* old_parent, Control* new_parent); - virtual void OnAttachToHost(UiHost* host); - virtual void OnDetachFromHost(UiHost* host); + virtual void OnAttachToHost(host::WindowHost* host); + virtual void OnDetachFromHost(host::WindowHost* host); + protected: virtual void OnMouseHoverChange(bool newHover) { CRU_UNUSED(newHover) } private: - UiHost* ui_host_ = nullptr; Control* parent_ = nullptr; + std::vector<Control*> children_; + + host::WindowHost* window_host_ = nullptr; private: bool is_mouse_over_ = false; - std::shared_ptr<platform::native::ICursor> cursor_ = nullptr; + std::shared_ptr<platform::gui::ICursor> cursor_ = nullptr; + + std::unique_ptr<style::StyleRuleSet> style_rule_set_; + std::unique_ptr<style::StyleRuleSetBind> style_rule_set_bind_; }; -} // namespace cru::ui +} // namespace cru::ui::controls diff --git a/include/cru/ui/controls/FlexLayout.hpp b/include/cru/ui/controls/FlexLayout.hpp index 87162569..4f6abfdb 100644 --- a/include/cru/ui/controls/FlexLayout.hpp +++ b/include/cru/ui/controls/FlexLayout.hpp @@ -1,5 +1,5 @@ #pragma once -#include "../LayoutControl.hpp" +#include "LayoutControl.hpp" namespace cru::ui::controls { class FlexLayout : public LayoutControl { @@ -28,13 +28,12 @@ class FlexLayout : public LayoutControl { FlexDirection GetFlexDirection() const; void SetFlexDirection(FlexDirection direction); + FlexCrossAlignment GetItemCrossAlign() const; + void SetItemCrossAlign(FlexCrossAlignment alignment); + FlexChildLayoutData GetChildLayoutData(Control* control); void SetChildLayoutData(Control* control, FlexChildLayoutData data); - protected: - void OnAddChild(Control* child, Index position) override; - void OnRemoveChild(Control* child, Index position) override; - private: std::shared_ptr<render::FlexLayoutRenderObject> render_object_; }; diff --git a/include/cru/ui/controls/IBorderControl.hpp b/include/cru/ui/controls/IBorderControl.hpp new file mode 100644 index 00000000..817305ef --- /dev/null +++ b/include/cru/ui/controls/IBorderControl.hpp @@ -0,0 +1,10 @@ +#pragma once +#include "../style/ApplyBorderStyleInfo.hpp" +#include "Base.hpp" +#include "cru/common/Base.hpp" + +namespace cru::ui::controls { +struct IBorderControl : virtual Interface { + virtual void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) = 0; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/controls/IClickableControl.hpp b/include/cru/ui/controls/IClickableControl.hpp new file mode 100644 index 00000000..aa7f13ab --- /dev/null +++ b/include/cru/ui/controls/IClickableControl.hpp @@ -0,0 +1,12 @@ +#pragma once +#include "Base.hpp" + +#include "cru/common/Event.hpp" +#include "cru/ui/helper/ClickDetector.hpp" + +namespace cru::ui::controls { +struct IClickableControl : virtual Interface { + virtual helper::ClickState GetClickState() = 0; + virtual IEvent<helper::ClickState>* ClickStateChangeEvent() = 0; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/controls/LayoutControl.hpp b/include/cru/ui/controls/LayoutControl.hpp new file mode 100644 index 00000000..106dd94d --- /dev/null +++ b/include/cru/ui/controls/LayoutControl.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "Control.hpp" + +namespace cru::ui::controls { +class LayoutControl : public Control { + protected: + LayoutControl() = default; + explicit LayoutControl(render::RenderObject* container_render_object) + : container_render_object_(container_render_object) {} + + public: + LayoutControl(const LayoutControl& other) = delete; + LayoutControl(LayoutControl&& other) = delete; + LayoutControl& operator=(const LayoutControl& other) = delete; + LayoutControl& operator=(LayoutControl&& other) = delete; + ~LayoutControl() override = default; + + using Control::AddChild; + using Control::RemoveChild; + + protected: + // If container render object is not null. Render object of added or removed + // child control will automatically sync to the container render object. + render::RenderObject* GetContainerRenderObject() const; + void SetContainerRenderObject(render::RenderObject* ro) { + container_render_object_ = ro; + } + + void OnAddChild(Control* child, Index position) override; + void OnRemoveChild(Control* child, Index position) override; + + private: + render::RenderObject* container_render_object_ = nullptr; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/NoChildControl.hpp b/include/cru/ui/controls/NoChildControl.hpp index 1a31ae7e..562137f1 100644 --- a/include/cru/ui/NoChildControl.hpp +++ b/include/cru/ui/controls/NoChildControl.hpp @@ -1,11 +1,8 @@ #pragma once #include "Control.hpp" -namespace cru::ui { +namespace cru::ui::controls { class NoChildControl : public Control { - private: - static const std::vector<Control*> empty_control_vector; - protected: NoChildControl() = default; @@ -16,9 +13,8 @@ class NoChildControl : public Control { NoChildControl& operator=(NoChildControl&& other) = delete; ~NoChildControl() override = default; - protected: - const std::vector<Control*>& GetChildren() const override final { - return empty_control_vector; - } + private: + using Control::AddChild; + using Control::RemoveChild; }; } // namespace cru::ui diff --git a/include/cru/ui/controls/Popup.hpp b/include/cru/ui/controls/Popup.hpp new file mode 100644 index 00000000..d76e1211 --- /dev/null +++ b/include/cru/ui/controls/Popup.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "RootControl.hpp" + +#include "cru/ui/Base.hpp" +#include "cru/platform/gui/Base.hpp" + +#include <memory> + +namespace cru::ui::controls { +class Popup : public RootControl { + public: + explicit Popup(Control* attached_control = nullptr); + + CRU_DELETE_COPY(Popup) + CRU_DELETE_MOVE(Popup) + + ~Popup() override; + + protected: + gsl::not_null<platform::gui::INativeWindow*> CreateNativeWindow( + gsl::not_null<host::WindowHost*> host, + platform::gui::INativeWindow* parent) override; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/controls/RootControl.hpp b/include/cru/ui/controls/RootControl.hpp new file mode 100644 index 00000000..53e69e7c --- /dev/null +++ b/include/cru/ui/controls/RootControl.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "LayoutControl.hpp" + +#include "cru/common/Base.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/ui/Base.hpp" + +namespace cru::ui::controls { +class RootControl : public LayoutControl { + protected: + explicit RootControl(Control* attached_control); + + public: + CRU_DELETE_COPY(RootControl) + CRU_DELETE_MOVE(RootControl) + ~RootControl() override; + + public: + render::RenderObject* GetRenderObject() const override; + + void EnsureWindowCreated(); + + // If create is false and native window is not create, it will not be created + // and shown. + void Show(bool create = true); + + Rect GetRect(); + void SetRect(const Rect& rect); + + protected: + virtual gsl::not_null<platform::gui::INativeWindow*> CreateNativeWindow( + gsl::not_null<host::WindowHost*> host, + platform::gui::INativeWindow* parent) = 0; + + private: + platform::gui::INativeWindow* GetNativeWindow(bool create); + + private: + std::unique_ptr<host::WindowHost> window_host_; + + std::unique_ptr<render::StackLayoutRenderObject> render_object_; + + Control* attached_control_; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/controls/StackLayout.hpp b/include/cru/ui/controls/StackLayout.hpp index c0b95044..aa9440c2 100644 --- a/include/cru/ui/controls/StackLayout.hpp +++ b/include/cru/ui/controls/StackLayout.hpp @@ -1,5 +1,5 @@ #pragma once -#include "../LayoutControl.hpp" +#include "LayoutControl.hpp" namespace cru::ui::controls { class StackLayout : public LayoutControl { @@ -21,10 +21,6 @@ class StackLayout : public LayoutControl { render::RenderObject* GetRenderObject() const override; - protected: - void OnAddChild(Control* child, Index position) override; - void OnRemoveChild(Control* child, Index position) override; - private: std::shared_ptr<render::StackLayoutRenderObject> render_object_; }; diff --git a/include/cru/ui/controls/TextBlock.hpp b/include/cru/ui/controls/TextBlock.hpp index 8a9a3bff..be31816c 100644 --- a/include/cru/ui/controls/TextBlock.hpp +++ b/include/cru/ui/controls/TextBlock.hpp @@ -1,15 +1,15 @@ #pragma once -#include "../NoChildControl.hpp" +#include "NoChildControl.hpp" -namespace cru::ui::controls { -template <typename TControl> -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"; - static TextBlock* Create() { return new TextBlock(); } + static TextBlock* Create(); + static TextBlock* Create(std::u16string text, bool selectable = false); protected: TextBlock(); @@ -28,12 +28,17 @@ class TextBlock : public NoChildControl { std::u16string GetText() const; void SetText(std::u16string text); - gsl::not_null<render::TextRenderObject*> GetTextRenderObject(); - render::ScrollRenderObject* GetScrollRenderObject() { return nullptr; } + bool IsSelectable() const; + void SetSelectable(bool value); + + gsl::not_null<render::TextRenderObject*> GetTextRenderObject() override; + render::ScrollRenderObject* GetScrollRenderObject() override { + return nullptr; + } private: std::unique_ptr<render::TextRenderObject> text_render_object_; - std::unique_ptr<TextControlService<TextBlock>> service_; + std::unique_ptr<TextHostControlService> service_; }; } // namespace cru::ui::controls diff --git a/include/cru/ui/controls/TextBox.hpp b/include/cru/ui/controls/TextBox.hpp index 5976f6da..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 "Base.hpp" +#include "NoChildControl.hpp" + +#include "IBorderControl.hpp" +#include "TextHostControlService.hpp" #include <memory> @@ -8,7 +10,9 @@ namespace cru::ui::controls { template <typename TControl> class TextControlService; -class TextBox : public NoChildControl { +class TextBox : public NoChildControl, + public virtual IBorderControl, + public virtual ITextHostControl { public: static constexpr std::u16string_view control_type = u"TextBox"; @@ -27,25 +31,16 @@ class TextBox : public NoChildControl { render::RenderObject* GetRenderObject() const override; - gsl::not_null<render::TextRenderObject*> GetTextRenderObject(); - render::ScrollRenderObject* GetScrollRenderObject(); - - const TextBoxBorderStyle& GetBorderStyle(); - void SetBorderStyle(TextBoxBorderStyle border_style); - - protected: - void OnMouseHoverChange(bool newHover) override; + gsl::not_null<render::TextRenderObject*> GetTextRenderObject() override; + render::ScrollRenderObject* GetScrollRenderObject() override; - private: - void UpdateBorderStyle(); + void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style) override; private: std::unique_ptr<render::BorderRenderObject> border_render_object_; std::unique_ptr<render::ScrollRenderObject> scroll_render_object_; std::unique_ptr<render::TextRenderObject> text_render_object_; - TextBoxBorderStyle border_style_; - - std::unique_ptr<TextControlService<TextBox>> service_; + std::unique_ptr<TextHostControlService> 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..340228fe --- /dev/null +++ b/include/cru/ui/controls/TextHostControlService.hpp @@ -0,0 +1,137 @@ +#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 <functional> +#include <string> + +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<render::TextRenderObject*> 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*> 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<platform::gui::CompositionText> 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 ChangeSelectionEnd(gsl::index new_end); + void AbortSelection(); + + 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<render::TextRenderObject*> GetTextRenderObject(); + render::ScrollRenderObject* GetScrollRenderObject(); + + // May return nullptr. + platform::gui::IInputMethodContext* GetInputMethodContext(); + + void CoerceSelection(); + + void SetupCaret(); + void TearDownCaret(); + + void SyncTextRenderObject(); + + void UpdateInputMethodPosition(); + + template <typename TArgs> + void SetupOneHandler(event::RoutedEvent<TArgs>* (Control::*event)(), + void (TextHostControlService::*handler)( + typename event::RoutedEvent<TArgs>::EventArgs)) { + this->event_guard_ += (this->control_->*event)()->Bubble()->AddHandler( + std::bind(handler, this, std::placeholders::_1)); + } + + void MouseMoveHandler(event::MouseEventArgs& args); + void MouseDownHandler(event::MouseButtonEventArgs& args); + void MouseUpHandler(event::MouseButtonEventArgs& args); + void GainFocusHandler(event::FocusChangeEventArgs& args); + void LoseFocusHandler(event::FocusChangeEventArgs& args); + + void SetUpShortcuts(); + + private: + gsl::not_null<Control*> control_; + gsl::not_null<ITextHostControl*> 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_ = false; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/controls/Window.hpp b/include/cru/ui/controls/Window.hpp new file mode 100644 index 00000000..cca56b64 --- /dev/null +++ b/include/cru/ui/controls/Window.hpp @@ -0,0 +1,32 @@ +#pragma once +#include "cru/platform/gui/Base.hpp" +#include "cru/ui/controls/RootControl.hpp" + +#include "cru/common/Base.hpp" + +namespace cru::ui::controls { +class Window final : public RootControl { + public: + static constexpr std::u16string_view control_type = u"Window"; + + public: + static Window* Create(Control* attached_control = nullptr); + + private: + explicit Window(Control* attached_control); + + public: + CRU_DELETE_COPY(Window) + CRU_DELETE_MOVE(Window) + + ~Window() override; + + public: + std::u16string_view GetControlType() const final { return control_type; } + + protected: + gsl::not_null<platform::gui::INativeWindow*> CreateNativeWindow( + gsl::not_null<host::WindowHost*> host, + platform::gui::INativeWindow* parent) override; +}; +} // namespace cru::ui::controls diff --git a/include/cru/ui/UiEvent.hpp b/include/cru/ui/events/UiEvent.hpp index 5adace8a..22ad0150 100644 --- a/include/cru/ui/UiEvent.hpp +++ b/include/cru/ui/events/UiEvent.hpp @@ -1,15 +1,15 @@ #pragma once -#include "Base.hpp" +#include "../Base.hpp" #include "cru/common/Event.hpp" -#include "cru/platform/native/Keyboard.hpp" +#include "cru/platform/gui/Keyboard.hpp" #include <memory> #include <optional> #include <string> #include <type_traits> -namespace cru::platform::graph { +namespace cru::platform::graphics { struct IPainter; } @@ -84,6 +84,7 @@ class MouseEventArgs : public UiEventArgs { // This point is relative to window client lefttop. Point GetPoint() const { return point_.value_or(Point{}); } + Point GetPoint(render::RenderObject* render_target) const; Point GetPointToContent(render::RenderObject* render_target) const; private: @@ -94,13 +95,13 @@ class MouseButtonEventArgs : public MouseEventArgs { public: MouseButtonEventArgs(Object* sender, Object* original_sender, const Point& point, const MouseButton button, - platform::native::KeyModifier key_modifier) + platform::gui::KeyModifier key_modifier) : MouseEventArgs(sender, original_sender, point), button_(button), key_modifier_(key_modifier) {} MouseButtonEventArgs(Object* sender, Object* original_sender, const MouseButton button, - platform::native::KeyModifier key_modifier) + platform::gui::KeyModifier key_modifier) : MouseEventArgs(sender, original_sender), button_(button), key_modifier_(key_modifier) {} @@ -111,11 +112,11 @@ class MouseButtonEventArgs : public MouseEventArgs { ~MouseButtonEventArgs() override = default; MouseButton GetButton() const { return button_; } - platform::native::KeyModifier GetKeyModifier() const { return key_modifier_; } + platform::gui::KeyModifier GetKeyModifier() const { return key_modifier_; } private: MouseButton button_; - platform::native::KeyModifier key_modifier_; + platform::gui::KeyModifier key_modifier_; }; class MouseWheelEventArgs : public MouseEventArgs { @@ -138,7 +139,7 @@ class MouseWheelEventArgs : public MouseEventArgs { class PaintEventArgs : public UiEventArgs { public: PaintEventArgs(Object* sender, Object* original_sender, - platform::graph::IPainter* painter) + platform::graphics::IPainter* painter) : UiEventArgs(sender, original_sender), painter_(painter) {} PaintEventArgs(const PaintEventArgs& other) = default; PaintEventArgs(PaintEventArgs&& other) = default; @@ -146,10 +147,10 @@ class PaintEventArgs : public UiEventArgs { PaintEventArgs& operator=(PaintEventArgs&& other) = default; ~PaintEventArgs() = default; - platform::graph::IPainter* GetPainter() const { return painter_; } + platform::graphics::IPainter* GetPainter() const { return painter_; } private: - platform::graph::IPainter* painter_; + platform::graphics::IPainter* painter_; }; class FocusChangeEventArgs : public UiEventArgs { @@ -191,8 +192,8 @@ class ToggleEventArgs : public UiEventArgs { class KeyEventArgs : public UiEventArgs { public: KeyEventArgs(Object* sender, Object* original_sender, - platform::native::KeyCode key_code, - platform::native::KeyModifier key_modifier) + platform::gui::KeyCode key_code, + platform::gui::KeyModifier key_modifier) : UiEventArgs(sender, original_sender), key_code_(key_code), key_modifier_(key_modifier) {} @@ -202,12 +203,12 @@ class KeyEventArgs : public UiEventArgs { KeyEventArgs& operator=(KeyEventArgs&& other) = default; ~KeyEventArgs() override = default; - platform::native::KeyCode GetKeyCode() const { return key_code_; } - platform::native::KeyModifier GetKeyModifier() const { return key_modifier_; } + platform::gui::KeyCode GetKeyCode() const { return key_code_; } + platform::gui::KeyModifier GetKeyModifier() const { return key_modifier_; } private: - platform::native::KeyCode key_code_; - platform::native::KeyModifier key_modifier_; + platform::gui::KeyCode key_code_; + platform::gui::KeyModifier key_modifier_; }; class CharEventArgs : public UiEventArgs { diff --git a/include/cru/ui/ClickDetector.hpp b/include/cru/ui/helper/ClickDetector.hpp index 4ffe5d05..b58297b1 100644 --- a/include/cru/ui/ClickDetector.hpp +++ b/include/cru/ui/helper/ClickDetector.hpp @@ -1,10 +1,10 @@ #pragma once -#include "Control.hpp" +#include "../controls/Control.hpp" -namespace cru::ui { +namespace cru::ui::helper { class ClickEventArgs : Object { public: - ClickEventArgs(Control* sender, const Point& down_point, + ClickEventArgs(controls::Control* sender, const Point& down_point, const Point& up_point, MouseButton button) : sender_(sender), down_point_(down_point), @@ -16,13 +16,13 @@ class ClickEventArgs : Object { ~ClickEventArgs() override = default; - Control* GetSender() const { return sender_; } + controls::Control* GetSender() const { return sender_; } Point GetDownPoint() const { return down_point_; } Point GetUpPoint() const { return up_point_; } MouseButton GetButton() const { return button_; } private: - Control* sender_; + controls::Control* sender_; Point down_point_; Point up_point_; MouseButton button_; @@ -39,14 +39,14 @@ class ClickDetector : public Object { CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::ClickDetector") public: - explicit ClickDetector(Control* control); + explicit ClickDetector(controls::Control* control); CRU_DELETE_COPY(ClickDetector) CRU_DELETE_MOVE(ClickDetector) ~ClickDetector() override = default; - Control* GetControl() const { return control_; } + controls::Control* GetControl() const { return control_; } ClickState GetState() const { return state_; } @@ -69,9 +69,9 @@ class ClickDetector : public Object { void SetState(ClickState state); private: - Control* control_; + controls::Control* control_; - ClickState state_; + ClickState state_ = ClickState::None; bool enable_ = true; MouseButton trigger_button_ = mouse_buttons::left | mouse_buttons::right; @@ -84,4 +84,4 @@ class ClickDetector : public Object { Point down_point_; MouseButton button_; }; -} // namespace cru::ui +} // namespace cru::ui::helper diff --git a/include/cru/ui/helper/ShortcutHub.hpp b/include/cru/ui/helper/ShortcutHub.hpp new file mode 100644 index 00000000..fe3414fe --- /dev/null +++ b/include/cru/ui/helper/ShortcutHub.hpp @@ -0,0 +1,134 @@ +#pragma once +#include "../Base.hpp" + +#include "../events/UiEvent.hpp" +#include "cru/common/Event.hpp" +#include "cru/platform/gui/Keyboard.hpp" + +#include <cstddef> +#include <functional> +#include <memory> +#include <optional> +#include <string> +#include <string_view> +#include <type_traits> +#include <unordered_map> +#include <vector> + +namespace cru::ui::helper { + +class ShortcutKeyBind { + public: + ShortcutKeyBind( + platform::gui::KeyCode key, + platform::gui::KeyModifier modifier = platform::gui::KeyModifiers::none) + : key_(key), modifier_(modifier) {} + + CRU_DEFAULT_COPY(ShortcutKeyBind) + CRU_DEFAULT_MOVE(ShortcutKeyBind) + + ~ShortcutKeyBind() = default; + + platform::gui::KeyCode GetKey() const { return key_; } + platform::gui::KeyModifier GetModifier() const { return modifier_; } + + bool Is(platform::gui::KeyCode key, + platform::gui::KeyModifier modifier) const { + return key == key_ && modifier == modifier_; + } + + bool operator==(const ShortcutKeyBind& other) const { + return this->key_ == other.key_ && this->modifier_ == other.modifier_; + } + + bool operator!=(const ShortcutKeyBind& other) const { + return !this->operator==(other); + } + + std::u16string ToString() { + std::u16string result = u"("; + result += platform::gui::ToString(modifier_); + result += u")"; + result += platform::gui::ToString(key_); + return result; + } + + private: + platform::gui::KeyCode key_; + platform::gui::KeyModifier modifier_; +}; +} // namespace cru::ui::helper + +namespace std { +template <> +struct hash<cru::ui::helper::ShortcutKeyBind> { + std::size_t operator()(const cru::ui::helper::ShortcutKeyBind& value) const { + std::size_t result = 0; + cru::hash_combine(result, static_cast<int>(value.GetKey())); + cru::hash_combine(result, static_cast<int>(value.GetModifier())); + return result; + } +}; +} // namespace std + +namespace cru::ui::helper { +struct Shortcut { + // Just for debug. + std::u16string name; + ShortcutKeyBind key_bind; + // Return true if it consumes the shortcut. Or return false if it does not + // handle the shortcut. + std::function<bool()> handler; +}; + +struct ShortcutInfo { + int id; + std::u16string name; + ShortcutKeyBind key_bind; + std::function<bool()> handler; +}; + +class ShortcutHub : public Object { + public: + ShortcutHub() = default; + + CRU_DELETE_COPY(ShortcutHub) + CRU_DELETE_MOVE(ShortcutHub) + + ~ShortcutHub() override = default; + + int RegisterShortcut(std::u16string name, ShortcutKeyBind bind, + std::function<bool()> handler) { + return RegisterShortcut({std::move(name), bind, std::move(handler)}); + } + + // Return an id used for unregistering. + int RegisterShortcut(Shortcut shortcut); + + void UnregisterShortcut(int id); + + std::vector<ShortcutInfo> GetAllShortcuts() const; + std::optional<ShortcutInfo> GetShortcut(int id) const; + const std::vector<ShortcutInfo>& GetShortcutByKeyBind( + const ShortcutKeyBind& key_bind) const; + + IEvent<event::KeyEventArgs&>* FallbackKeyEvent() { return &fallback_event_; } + + void Install(controls::Control* control); + void Uninstall(); + + private: + void OnKeyDown(event::KeyEventArgs& event); + + private: + std::unordered_map<ShortcutKeyBind, std::vector<ShortcutInfo>> map_; + + const std::vector<ShortcutInfo> empty_list_; + + int current_id_ = 1; + + Event<event::KeyEventArgs&> fallback_event_; + + EventRevokerListGuard event_guard_; +}; +} // namespace cru::ui::helper diff --git a/include/cru/ui/host/LayoutPaintCycler.hpp b/include/cru/ui/host/LayoutPaintCycler.hpp new file mode 100644 index 00000000..ed543afa --- /dev/null +++ b/include/cru/ui/host/LayoutPaintCycler.hpp @@ -0,0 +1,39 @@ +#pragma once +#include "../Base.hpp" + +#include "cru/platform/gui/UiApplication.hpp" + +#include <chrono> + +namespace cru::ui::host { +class LayoutPaintCycler { + public: + explicit LayoutPaintCycler(WindowHost* host); + + CRU_DELETE_COPY(LayoutPaintCycler) + CRU_DELETE_MOVE(LayoutPaintCycler) + + ~LayoutPaintCycler(); + + public: + void InvalidateLayout(); + void InvalidatePaint(); + + bool IsLayoutDirty() { return layout_dirty_; } + + private: + void OnCycle(); + + private: + WindowHost* host_; + + platform::gui::TimerAutoCanceler timer_canceler_; + + bool layout_dirty_ = true; + bool paint_dirty_ = true; + + std::chrono::steady_clock::time_point last_cycle_time_; + std::chrono::steady_clock::duration cycle_threshold_ = + std::chrono::milliseconds(1000) / 144; +}; +} // namespace cru::ui::host diff --git a/include/cru/ui/host/WindowHost.hpp b/include/cru/ui/host/WindowHost.hpp new file mode 100644 index 00000000..bd2f7c16 --- /dev/null +++ b/include/cru/ui/host/WindowHost.hpp @@ -0,0 +1,176 @@ +#pragma once +#include "../Base.hpp" + +#include "../render/Base.hpp" +#include "cru/common/Event.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/platform/gui/Window.hpp" + +#include <functional> +#include <memory> +#include <optional> + +namespace cru::ui::host { +class LayoutPaintCycler; + +struct AfterLayoutEventArgs {}; + +struct CreateWindowParams { + CreateWindowParams(platform::gui::INativeWindow* parent = nullptr, + platform::gui::CreateWindowFlag flag = {}) + : parent(parent), flag(flag) {} + + platform::gui::INativeWindow* parent; + platform::gui::CreateWindowFlag flag; +}; + +// The bridge between control tree and native window. +class WindowHost : public Object { + CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::host::WindowHost") + + public: + WindowHost(controls::Control* root_control); + + CRU_DELETE_COPY(WindowHost) + CRU_DELETE_MOVE(WindowHost) + + ~WindowHost() override; + + public: + platform::gui::INativeWindow* GetNativeWindow() { return native_window_; } + + // Do nothing if native window is already created. + gsl::not_null<platform::gui::INativeWindow*> CreateNativeWindow( + CreateWindowParams create_window_params = {}); + + // Mark the layout as invalid, and arrange a re-layout later. + // This method could be called more than one times in a message cycle. But + // layout only takes place once. + void InvalidateLayout(); + + // Mark the paint as invalid, and arrange a re-paint later. + // This method could be called more than one times in a message cycle. But + // paint only takes place once. + void InvalidatePaint(); + + IEvent<AfterLayoutEventArgs>* AfterLayoutEvent() { + return &after_layout_event_; + } + + void Relayout(); + void Relayout(const Size& available_size); + + void Repaint(); + + // Is layout is invalid, wait for relayout and then run the action. Otherwist + // run it right now. + void RunAfterLayoutStable(std::function<void()> action); + + // If true, preferred size of root render object is set to window size when + // measure. Default is true. + bool IsLayoutPreferToFillWindow() const; + void SetLayoutPreferToFillWindow(bool value); + + // Get current control that mouse hovers on. This ignores the mouse-capture + // control. Even when mouse is captured by another control, this function + // return the control under cursor. You can use `GetMouseCaptureControl` to + // get more info. + controls::Control* GetMouseHoverControl() const { + return mouse_hover_control_; + } + + //*************** region: focus *************** + + controls::Control* GetFocusControl(); + + void SetFocusControl(controls::Control* control); + + //*************** region: focus *************** + + // Pass nullptr to release capture. If mouse is already capture by a control, + // this capture will fail and return false. If control is identical to the + // capturing control, capture is not changed and this function will return + // true. + // + // When capturing control changes, + // appropriate event will be sent. If mouse is not on the capturing control + // and capture is released, mouse enter event will be sent to the mouse-hover + // control. If mouse is not on the capturing control and capture is set, mouse + // leave event will be sent to the mouse-hover control. + bool CaptureMouseFor(controls::Control* control); + + // Return null if not captured. + controls::Control* GetMouseCaptureControl(); + + controls::Control* HitTest(const Point& point); + + void UpdateCursor(); + + IEvent<platform::gui::INativeWindow*>* NativeWindowChangeEvent() { + return &native_window_change_event_; + } + + // If window exist, return window actual size. Otherwise if saved rect exists, + // return it. Otherwise return 0. + Rect GetWindowRect(); + + void SetSavedWindowRect(std::optional<Rect> rect); + + void SetWindowRect(const Rect& rect); + + private: + //*************** region: native messages *************** + void OnNativeDestroy(platform::gui::INativeWindow* window, std::nullptr_t); + void OnNativePaint(platform::gui::INativeWindow* window, std::nullptr_t); + void OnNativeResize(platform::gui::INativeWindow* window, const Size& size); + + void OnNativeFocus(platform::gui::INativeWindow* window, + cru::platform::gui::FocusChangeType focus); + + void OnNativeMouseEnterLeave(platform::gui::INativeWindow* window, + cru::platform::gui::MouseEnterLeaveType enter); + void OnNativeMouseMove(platform::gui::INativeWindow* window, + const Point& point); + void OnNativeMouseDown(platform::gui::INativeWindow* window, + const platform::gui::NativeMouseButtonEventArgs& args); + void OnNativeMouseUp(platform::gui::INativeWindow* window, + const platform::gui::NativeMouseButtonEventArgs& args); + + void OnNativeKeyDown(platform::gui::INativeWindow* window, + const platform::gui::NativeKeyEventArgs& args); + void OnNativeKeyUp(platform::gui::INativeWindow* window, + const platform::gui::NativeKeyEventArgs& args); + + //*************** region: event dispatcher helper *************** + + void DispatchMouseHoverControlChangeEvent(controls::Control* old_control, + controls::Control* new_control, + const Point& point, bool no_leave, + bool no_enter); + + private: + controls::Control* root_control_ = nullptr; + render::RenderObject* root_render_object_ = nullptr; + + platform::gui::INativeWindow* native_window_ = nullptr; + + std::unique_ptr<LayoutPaintCycler> layout_paint_cycler_; + + Event<AfterLayoutEventArgs> after_layout_event_; + std::vector<std::function<void()> > after_layout_stable_action_; + + std::vector<EventRevokerGuard> event_revoker_guards_; + + controls::Control* mouse_hover_control_ = nullptr; + + controls::Control* focus_control_; + + controls::Control* mouse_captured_control_ = nullptr; + + bool layout_prefer_to_fill_window_ = true; + + Event<platform::gui::INativeWindow*> native_window_change_event_; + + std::optional<Rect> saved_rect_; +}; +} // namespace cru::ui::host diff --git a/include/cru/ui/render/Base.hpp b/include/cru/ui/render/Base.hpp index 801d58bd..ac67349e 100644 --- a/include/cru/ui/render/Base.hpp +++ b/include/cru/ui/render/Base.hpp @@ -9,5 +9,4 @@ class FlexLayoutRenderObject; class ScrollRenderObject; class StackLayoutRenderObject; class TextRenderObject; -class WindowRenderObject; } // namespace cru::ui::render diff --git a/include/cru/ui/render/BorderRenderObject.hpp b/include/cru/ui/render/BorderRenderObject.hpp index 587f051a..3d4f4dad 100644 --- a/include/cru/ui/render/BorderRenderObject.hpp +++ b/include/cru/ui/render/BorderRenderObject.hpp @@ -1,5 +1,7 @@ #pragma once +#include "../style/ApplyBorderStyleInfo.hpp" #include "RenderObject.hpp" +#include "cru/ui/Base.hpp" namespace cru::ui::render { class BorderRenderObject : public RenderObject { @@ -16,11 +18,11 @@ class BorderRenderObject : public RenderObject { bool IsBorderEnabled() const { return is_border_enabled_; } void SetBorderEnabled(bool enabled) { is_border_enabled_ = enabled; } - std::shared_ptr<platform::graph::IBrush> GetBorderBrush() { + std::shared_ptr<platform::graphics::IBrush> GetBorderBrush() { return border_brush_; } - void SetBorderBrush(std::shared_ptr<platform::graph::IBrush> brush) { + void SetBorderBrush(std::shared_ptr<platform::graphics::IBrush> brush) { if (brush == border_brush_) return; border_brush_ = std::move(brush); InvalidatePaint(); @@ -42,32 +44,32 @@ class BorderRenderObject : public RenderObject { RecreateGeometry(); } - std::shared_ptr<platform::graph::IBrush> GetForegroundBrush() { + std::shared_ptr<platform::graphics::IBrush> GetForegroundBrush() { return foreground_brush_; } - void SetForegroundBrush(std::shared_ptr<platform::graph::IBrush> brush) { + void SetForegroundBrush(std::shared_ptr<platform::graphics::IBrush> brush) { if (brush == foreground_brush_) return; foreground_brush_ = std::move(brush); InvalidatePaint(); } - std::shared_ptr<platform::graph::IBrush> GetBackgroundBrush() { + std::shared_ptr<platform::graphics::IBrush> GetBackgroundBrush() { return background_brush_; } - void SetBackgroundBrush(std::shared_ptr<platform::graph::IBrush> brush) { + void SetBackgroundBrush(std::shared_ptr<platform::graphics::IBrush> brush) { if (brush == background_brush_) return; background_brush_ = std::move(brush); InvalidatePaint(); } - void SetBorderStyle(const BorderStyle& style); + void ApplyBorderStyle(const style::ApplyBorderStyleInfo& style); RenderObject* HitTest(const Point& point) override; protected: - void OnDrawCore(platform::graph::IPainter* painter) override; + void OnDrawCore(platform::graphics::IPainter* painter) override; Size OnMeasureCore(const MeasureRequirement& requirement, const MeasureSize& preferred_size) override; @@ -87,19 +89,19 @@ class BorderRenderObject : public RenderObject { private: bool is_border_enabled_ = false; - std::shared_ptr<platform::graph::IBrush> border_brush_; + std::shared_ptr<platform::graphics::IBrush> border_brush_; Thickness border_thickness_; CornerRadius border_radius_; - std::shared_ptr<platform::graph::IBrush> foreground_brush_; - std::shared_ptr<platform::graph::IBrush> background_brush_; + std::shared_ptr<platform::graphics::IBrush> foreground_brush_; + std::shared_ptr<platform::graphics::IBrush> background_brush_; // The ring. Used for painting. - std::unique_ptr<platform::graph::IGeometry> geometry_; + std::unique_ptr<platform::graphics::IGeometry> geometry_; // Area including inner area of the border. Used for painting foreground and // background. - std::unique_ptr<platform::graph::IGeometry> border_inner_geometry_; + std::unique_ptr<platform::graphics::IGeometry> border_inner_geometry_; // Area including border ring and inner area. Used for hit test. - std::unique_ptr<platform::graph::IGeometry> border_outer_geometry_; + std::unique_ptr<platform::graphics::IGeometry> border_outer_geometry_; }; } // namespace cru::ui::render diff --git a/include/cru/ui/render/CanvasRenderObject.hpp b/include/cru/ui/render/CanvasRenderObject.hpp index 3216f08c..58fee59c 100644 --- a/include/cru/ui/render/CanvasRenderObject.hpp +++ b/include/cru/ui/render/CanvasRenderObject.hpp @@ -22,7 +22,7 @@ class CanvasRenderObject : public RenderObject { IEvent<CanvasPaintEventArgs>* PaintEvent() { return &paint_event_; } protected: - void OnDrawContent(platform::graph::IPainter* painter) override; + void OnDrawContent(platform::graphics::IPainter* painter) override; Size OnMeasureContent(const MeasureRequirement& requirement, const MeasureSize& preferred_size) override; diff --git a/include/cru/ui/render/FlexLayoutRenderObject.hpp b/include/cru/ui/render/FlexLayoutRenderObject.hpp index ee29d1e4..a8154487 100644 --- a/include/cru/ui/render/FlexLayoutRenderObject.hpp +++ b/include/cru/ui/render/FlexLayoutRenderObject.hpp @@ -1,6 +1,8 @@ #pragma once #include "LayoutRenderObject.hpp" +#include <string_view> + namespace cru::ui::render { // Measure Logic (v0.1): // Cross axis measure logic is the same as stack layout. @@ -85,6 +87,8 @@ class FlexLayoutRenderObject : public LayoutRenderObject<FlexChildLayoutData> { FlexLayoutRenderObject& operator=(FlexLayoutRenderObject&& other) = delete; ~FlexLayoutRenderObject() override = default; + std::u16string_view GetName() const override; + FlexDirection GetFlexDirection() const { return direction_; } void SetFlexDirection(FlexDirection direction) { direction_ = direction; diff --git a/include/cru/ui/render/LayoutRenderObject.hpp b/include/cru/ui/render/LayoutRenderObject.hpp index b46ba0d0..732031a1 100644 --- a/include/cru/ui/render/LayoutRenderObject.hpp +++ b/include/cru/ui/render/LayoutRenderObject.hpp @@ -1,7 +1,7 @@ #pragma once #include "RenderObject.hpp" -#include "cru/platform/graph/util/Painter.hpp" +#include "cru/platform/graphics/util/Painter.hpp" namespace cru::ui::render { template <typename TChildLayoutData> diff --git a/include/cru/ui/render/MeasureRequirement.hpp b/include/cru/ui/render/MeasureRequirement.hpp index 2be159f8..6a0c6952 100644 --- a/include/cru/ui/render/MeasureRequirement.hpp +++ b/include/cru/ui/render/MeasureRequirement.hpp @@ -1,8 +1,12 @@ #pragma once #include "Base.hpp" +#include "cru/common/Format.hpp" + +#include <fmt/core.h> #include <algorithm> #include <limits> +#include <string> namespace cru::ui::render { constexpr Size Min(const Size& left, const Size& right) { @@ -112,6 +116,11 @@ class MeasureLength final { } } + std::u16string ToDebugString() const { + return IsSpecified() ? ToUtf16String(GetLengthOrUndefined()) + : u"UNSPECIFIED"; + } + private: // -1 for not specify float length_; @@ -160,6 +169,11 @@ struct MeasureSize { }; } + std::u16string ToDebugString() const { + return fmt::format(u"({}, {})", width.ToDebugString(), + height.ToDebugString()); + } + constexpr static MeasureSize NotSpecified() { return MeasureSize{MeasureLength::NotSpecified(), MeasureLength::NotSpecified()}; @@ -187,10 +201,11 @@ struct MeasureRequirement { : max(max), min(min) {} constexpr bool Satisfy(const Size& size) const { - return max.width.GetLengthOrMax() >= size.width && - max.height.GetLengthOrMax() >= size.height && - min.width.GetLengthOr0() <= size.width && - min.height.GetLengthOr0() <= size.height; + auto normalized = Normalize(); + return normalized.max.width.GetLengthOrMax() >= size.width && + normalized.max.height.GetLengthOrMax() >= size.height && + normalized.min.width.GetLengthOr0() <= size.width && + normalized.min.height.GetLengthOr0() <= size.height; } constexpr MeasureRequirement Normalize() const { @@ -225,6 +240,11 @@ struct MeasureRequirement { return result; } + std::u16string ToDebugString() const { + return fmt::format(u"{{min: {}, max: {}}}", min.ToDebugString(), + max.ToDebugString()); + } + constexpr static MeasureRequirement Merge(const MeasureRequirement& left, const MeasureRequirement& right) { return MeasureRequirement{MeasureSize::Min(left.max, right.max), diff --git a/include/cru/ui/render/RenderObject.hpp b/include/cru/ui/render/RenderObject.hpp index f820f029..8bcd4c62 100644 --- a/include/cru/ui/render/RenderObject.hpp +++ b/include/cru/ui/render/RenderObject.hpp @@ -2,10 +2,15 @@ #include "Base.hpp" #include "MeasureRequirement.hpp" +#include "cru/common/Base.hpp" #include "cru/common/Event.hpp" +#include "cru/ui/Base.hpp" -namespace cru::ui::render { +#include <cstddef> +#include <string> +#include <string_view> +namespace cru::ui::render { // Render object will not destroy its children when destroyed. Control must // manage lifecycle of its render objects. Since control will destroy its // children when destroyed, render objects will be destroyed along with it. @@ -29,13 +34,13 @@ namespace cru::ui::render { // // To write a custom RenderObject, override following methods: // public: -// void Draw(platform::graph::IPainter* painter) override; +// void Draw(platform::graphics::IPainter* painter) override; // RenderObject* HitTest(const Point& point) override; // protected: // Size OnMeasureContent(const MeasureRequirement& requirement) override; // void OnLayoutContent(const Rect& content_rect) override; class RenderObject : public Object { - friend WindowRenderObject; + friend host::WindowHost; CRU_DEFINE_CLASS_LOG_TAG(u"cru::ui::render::RenderObject") @@ -58,10 +63,10 @@ class RenderObject : public Object { RenderObject& operator=(RenderObject&& other) = delete; ~RenderObject() override = default; - Control* GetAttachedControl() const { return control_; } - void SetAttachedControl(Control* new_control) { control_ = new_control; } + controls::Control* GetAttachedControl() const { return control_; } + void SetAttachedControl(controls::Control* new_control); - UiHost* GetUiHost() const { return ui_host_; } + host::WindowHost* GetWindowHost() const { return window_host_; } RenderObject* GetParent() const { return parent_; } @@ -70,6 +75,9 @@ class RenderObject : public Object { void AddChild(RenderObject* render_object, Index position); void RemoveChild(Index position); + RenderObject* GetFirstChild() const; + void TraverseDescendants(const std::function<void(RenderObject*)>& action); + // Offset from parent's lefttop to lefttop of this render object. Margin is // accounted for. Point GetOffset() const { return offset_; } @@ -123,16 +131,30 @@ class RenderObject : public Object { // This will set offset of this render object and call OnLayoutCore. void Layout(const Point& offset); - void Draw(platform::graph::IPainter* painter); + virtual Rect GetPaddingRect() const; + virtual Rect GetContentRect() const; + + void Draw(platform::graphics::IPainter* painter); // Param point must be relative the lefttop of render object including margin. // Add offset before pass point to children. virtual RenderObject* HitTest(const Point& point) = 0; + IEvent<host::WindowHost*>* AttachToHostEvent() { + return &attach_to_host_event_; + } + IEvent<std::nullptr_t>* DetachFromHostEvent() { + return &detach_from_host_event_; + } + public: void InvalidateLayout(); void InvalidatePaint(); + public: + virtual std::u16string_view GetName() const; + std::u16string GetDebugPathInTree() const; + protected: void SetChildMode(ChildMode mode) { child_mode_ = mode; } @@ -148,15 +170,15 @@ class RenderObject : public Object { virtual void OnRemoveChild(RenderObject* removed_child, Index position); // Draw all children with offset. - void DefaultDrawChildren(platform::graph::IPainter* painter); + void DefaultDrawChildren(platform::graphics::IPainter* painter); // Draw all children with translation of content rect lefttop. - void DefaultDrawContent(platform::graph::IPainter* painter); + void DefaultDrawContent(platform::graphics::IPainter* painter); // Call DefaultDrawContent. Then call DefaultDrawChildren. - virtual void OnDrawCore(platform::graph::IPainter* painter); + virtual void OnDrawCore(platform::graphics::IPainter* painter); - virtual void OnDrawContent(platform::graph::IPainter* painter); + virtual void OnDrawContent(platform::graphics::IPainter* painter); // Size measure including margin and padding. Please reduce margin and padding // or other custom things and pass the result content measure requirement and @@ -182,20 +204,20 @@ class RenderObject : public Object { // Lefttop of content_rect should be added when calculated children's offset. virtual void OnLayoutContent(const Rect& content_rect) = 0; - virtual void OnAfterLayout(); - static void NotifyAfterLayoutRecursive(RenderObject* render_object); + virtual void OnAttachedControlChanged(controls::Control* control) { + CRU_UNUSED(control) + } - virtual Rect GetPaddingRect() const; - virtual Rect GetContentRect() const; + virtual void OnAfterLayout(); private: void SetParent(RenderObject* new_parent); - void SetRenderHostRecursive(UiHost* host); + void SetWindowHostRecursive(host::WindowHost* host); private: - Control* control_ = nullptr; - UiHost* ui_host_ = nullptr; + controls::Control* control_ = nullptr; + host::WindowHost* window_host_ = nullptr; RenderObject* parent_ = nullptr; std::vector<RenderObject*> children_{}; @@ -210,5 +232,8 @@ class RenderObject : public Object { Thickness margin_{}; Thickness padding_{}; + + Event<host::WindowHost*> attach_to_host_event_; + Event<std::nullptr_t> detach_from_host_event_; }; } // namespace cru::ui::render diff --git a/include/cru/ui/render/ScrollBar.hpp b/include/cru/ui/render/ScrollBar.hpp new file mode 100644 index 00000000..3293e9d0 --- /dev/null +++ b/include/cru/ui/render/ScrollBar.hpp @@ -0,0 +1,218 @@ +#pragma once +#include "Base.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/Event.hpp" +#include "cru/platform/graphics/Base.hpp" +#include "cru/platform/graphics/Geometry.hpp" +#include "cru/platform/graphics/Painter.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/ui/Base.hpp" +#include "cru/ui/controls/Control.hpp" + +#include <gsl/pointers> +#include <memory> +#include <optional> + +namespace cru::ui::render { +class ScrollRenderObject; + +enum class ScrollKind { Absolute, Relative, Page, Line }; + +struct Scroll { + Direction direction; + ScrollKind kind; + // For absolute, the new scroll position. Otherwise, offset. + float value; +}; + +enum class ScrollBarAreaKind { + UpArrow, // Line up + DownArrow, // Line down + UpSlot, // Page up + DownSlot, // Page down + Thumb +}; + +class ScrollBar : public Object { + public: + ScrollBar(gsl::not_null<ScrollRenderObject*> render_object, + Direction direction); + + CRU_DELETE_COPY(ScrollBar) + CRU_DELETE_MOVE(ScrollBar) + + ~ScrollBar() override; + + public: + Direction GetDirection() const { return direction_; } + + bool IsEnabled() const { return is_enabled_; } + void SetEnabled(bool value); + + bool IsExpanded() const { return is_expanded_; } + void SetExpanded(bool value); + + void Draw(platform::graphics::IPainter* painter); + + IEvent<Scroll>* ScrollAttemptEvent() { return &scroll_attempt_event_; } + + void InstallHandlers(controls::Control* control); + void UninstallHandlers() { InstallHandlers(nullptr); } + + gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> + GetCollapsedThumbBrush() const; + gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> + GetExpandedThumbBrush() const; + gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> + GetExpandedSlotBrush() const; + gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> + GetExpandedArrowBrush() const; + gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> + GetExpandedArrowBackgroundBrush() const; + + protected: + void OnDraw(platform::graphics::IPainter* painter, bool expand); + + virtual void DrawUpArrow(platform::graphics::IPainter* painter, + const Rect& area) = 0; + virtual void DrawDownArrow(platform::graphics::IPainter* painter, + const Rect& area) = 0; + + std::optional<ScrollBarAreaKind> ExpandedHitTest(const Point& point); + + virtual bool IsShowBar() = 0; + + virtual std::optional<Rect> GetExpandedAreaRect( + ScrollBarAreaKind area_kind) = 0; + virtual std::optional<Rect> GetCollapsedTriggerExpandAreaRect() = 0; + virtual std::optional<Rect> GetCollapsedThumbRect() = 0; + + virtual float CalculateNewScrollPosition(const Rect& thumb_original_rect, + const Point& mouse_offset) = 0; + + private: + void SetCursor(); + void RestoreCursor(); + + void BeginAutoCollapseTimer(); + void StopAutoCollapseTimer(); + + void OnMouseLeave(); + + protected: + gsl::not_null<ScrollRenderObject*> render_object_; + + std::unique_ptr<platform::graphics::IGeometry> arrow_geometry_; + + private: + Direction direction_; + + bool is_enabled_ = true; + + bool is_expanded_ = false; + + std::shared_ptr<platform::graphics::IBrush> collapsed_thumb_brush_; + std::shared_ptr<platform::graphics::IBrush> expanded_thumb_brush_; + std::shared_ptr<platform::graphics::IBrush> expanded_slot_brush_; + std::shared_ptr<platform::graphics::IBrush> expanded_arrow_brush_; + std::shared_ptr<platform::graphics::IBrush> expanded_arrow_background_brush_; + + Rect move_thumb_thumb_original_rect_; + std::optional<Point> move_thumb_start_; + + EventRevokerListGuard event_guard_; + + Event<Scroll> scroll_attempt_event_; + + std::optional<std::shared_ptr<platform::gui::ICursor>> old_cursor_; + + platform::gui::TimerAutoCanceler auto_collapse_timer_canceler_; +}; + +class HorizontalScrollBar : public ScrollBar { + public: + explicit HorizontalScrollBar( + gsl::not_null<ScrollRenderObject*> render_object); + + CRU_DELETE_COPY(HorizontalScrollBar) + CRU_DELETE_MOVE(HorizontalScrollBar) + + ~HorizontalScrollBar() override = default; + + protected: + void DrawUpArrow(platform::graphics::IPainter* painter, + const Rect& area) override; + void DrawDownArrow(platform::graphics::IPainter* painter, + const Rect& area) override; + + bool IsShowBar() override; + + std::optional<Rect> GetExpandedAreaRect(ScrollBarAreaKind area_kind) override; + std::optional<Rect> GetCollapsedTriggerExpandAreaRect() override; + std::optional<Rect> GetCollapsedThumbRect() override; + + float CalculateNewScrollPosition(const Rect& thumb_original_rect, + const Point& mouse_offset) override; +}; + +class VerticalScrollBar : public ScrollBar { + public: + explicit VerticalScrollBar(gsl::not_null<ScrollRenderObject*> render_object); + + CRU_DELETE_COPY(VerticalScrollBar) + CRU_DELETE_MOVE(VerticalScrollBar) + + ~VerticalScrollBar() override = default; + + protected: + void DrawUpArrow(platform::graphics::IPainter* painter, + const Rect& area) override; + void DrawDownArrow(platform::graphics::IPainter* painter, + const Rect& area) override; + + bool IsShowBar() override; + + std::optional<Rect> GetExpandedAreaRect(ScrollBarAreaKind area_kind) override; + std::optional<Rect> GetCollapsedTriggerExpandAreaRect() override; + std::optional<Rect> GetCollapsedThumbRect() override; + + float CalculateNewScrollPosition(const Rect& thumb_original_rect, + const Point& mouse_offset) override; +}; + +// A delegate to draw scrollbar and register related events. +class ScrollBarDelegate : public Object { + public: + explicit ScrollBarDelegate(gsl::not_null<ScrollRenderObject*> render_object); + + CRU_DELETE_COPY(ScrollBarDelegate) + CRU_DELETE_MOVE(ScrollBarDelegate) + + ~ScrollBarDelegate() override = default; + + public: + bool IsHorizontalBarEnabled() const { return horizontal_bar_.IsEnabled(); } + void SetHorizontalBarEnabled(bool value) { + horizontal_bar_.SetEnabled(value); + } + + bool IsVerticalBarEnabled() const { return horizontal_bar_.IsEnabled(); } + void SetVerticalBarEnabled(bool value) { horizontal_bar_.SetEnabled(value); } + + IEvent<Scroll>* ScrollAttemptEvent() { return &scroll_attempt_event_; } + + void DrawScrollBar(platform::graphics::IPainter* painter); + + void InstallHandlers(controls::Control* control); + void UninstallHandlers() { InstallHandlers(nullptr); } + + private: + gsl::not_null<ScrollRenderObject*> render_object_; + + HorizontalScrollBar horizontal_bar_; + VerticalScrollBar vertical_bar_; + + Event<Scroll> scroll_attempt_event_; +}; +} // namespace cru::ui::render diff --git a/include/cru/ui/render/ScrollRenderObject.hpp b/include/cru/ui/render/ScrollRenderObject.hpp index 9b0cbf9a..aed25f8e 100644 --- a/include/cru/ui/render/ScrollRenderObject.hpp +++ b/include/cru/ui/render/ScrollRenderObject.hpp @@ -1,8 +1,11 @@ #pragma once #include "RenderObject.hpp" -#include "cru/platform/graph/util/Painter.hpp" +#include "cru/platform/graphics/util/Painter.hpp" +#include "cru/ui/Base.hpp" +#include "cru/ui/render/ScrollBar.hpp" +#include <memory> #include <optional> namespace cru::ui::render { @@ -16,7 +19,7 @@ namespace cru::ui::render { // Or layout by scroll state. class ScrollRenderObject : public RenderObject { public: - ScrollRenderObject() : RenderObject(ChildMode::Single) {} + ScrollRenderObject(); CRU_DELETE_COPY(ScrollRenderObject) CRU_DELETE_MOVE(ScrollRenderObject) @@ -27,8 +30,22 @@ class ScrollRenderObject : public RenderObject { // Return the coerced scroll offset. Point GetScrollOffset(); + float GetScrollOffset(Direction direction) { + return direction == Direction::Horizontal ? GetScrollOffset().x + : GetScrollOffset().y; + } void SetScrollOffset(const Point& offset); void SetScrollOffset(std::optional<float> x, std::optional<float> y); + void SetScrollOffset(Direction direction, std::optional<float> value) { + if (direction == Direction::Horizontal) { + SetScrollOffset(value, std::nullopt); + } else { + SetScrollOffset(std::nullopt, value); + } + } + + void Scroll(const Scroll& scroll); + Point GetRawScrollOffset() const { return scroll_offset_; } // Return the viewable area rect. @@ -44,7 +61,7 @@ class ScrollRenderObject : public RenderObject { void ScrollToContain(const Rect& rect, const Thickness& margin = Thickness{}); protected: - void OnDrawCore(platform::graph::IPainter* painter) override; + void OnDrawCore(platform::graphics::IPainter* painter) override; // Logic: // If available size is bigger than child's preferred size, then child's @@ -54,7 +71,11 @@ class ScrollRenderObject : public RenderObject { const MeasureSize& preferred_size) override; void OnLayoutContent(const Rect& content_rect) override; + void OnAttachedControlChanged(controls::Control* control) override; + private: Point scroll_offset_; + + std::unique_ptr<ScrollBarDelegate> scroll_bar_delegate_; }; } // namespace cru::ui::render diff --git a/include/cru/ui/render/TextRenderObject.hpp b/include/cru/ui/render/TextRenderObject.hpp index 3be42bbb..bdec18d1 100644 --- a/include/cru/ui/render/TextRenderObject.hpp +++ b/include/cru/ui/render/TextRenderObject.hpp @@ -24,10 +24,10 @@ class TextRenderObject : public RenderObject { constexpr static float default_caret_width = 2; public: - TextRenderObject(std::shared_ptr<platform::graph::IBrush> brush, - std::shared_ptr<platform::graph::IFont> font, - std::shared_ptr<platform::graph::IBrush> selection_brush, - std::shared_ptr<platform::graph::IBrush> caret_brush); + TextRenderObject(std::shared_ptr<platform::graphics::IBrush> brush, + std::shared_ptr<platform::graphics::IFont> font, + std::shared_ptr<platform::graphics::IBrush> selection_brush, + std::shared_ptr<platform::graphics::IBrush> caret_brush); TextRenderObject(const TextRenderObject& other) = delete; TextRenderObject(TextRenderObject&& other) = delete; TextRenderObject& operator=(const TextRenderObject& other) = delete; @@ -38,25 +38,27 @@ class TextRenderObject : public RenderObject { std::u16string_view GetTextView() const; void SetText(std::u16string new_text); - std::shared_ptr<platform::graph::IBrush> GetBrush() const { return brush_; } - void SetBrush(std::shared_ptr<platform::graph::IBrush> new_brush); + std::shared_ptr<platform::graphics::IBrush> GetBrush() const { + return brush_; + } + void SetBrush(std::shared_ptr<platform::graphics::IBrush> new_brush); - std::shared_ptr<platform::graph::IFont> GetFont() const; - void SetFont(std::shared_ptr<platform::graph::IFont> font); + std::shared_ptr<platform::graphics::IFont> GetFont() const; + void SetFont(std::shared_ptr<platform::graphics::IFont> font); std::vector<Rect> TextRangeRect(const TextRange& text_range); Point TextSinglePoint(gsl::index position, bool trailing); - platform::graph::TextHitTestResult TextHitTest(const Point& point); + platform::graphics::TextHitTestResult TextHitTest(const Point& point); std::optional<TextRange> GetSelectionRange() const { return selection_range_; } void SetSelectionRange(std::optional<TextRange> new_range); - std::shared_ptr<platform::graph::IBrush> GetSelectionBrush() const { + std::shared_ptr<platform::graphics::IBrush> GetSelectionBrush() const { return selection_brush_; } - void SetSelectionBrush(std::shared_ptr<platform::graph::IBrush> new_brush); + void SetSelectionBrush(std::shared_ptr<platform::graphics::IBrush> new_brush); bool IsDrawCaret() const { return draw_caret_; } void SetDrawCaret(bool draw_caret); @@ -72,18 +74,23 @@ class TextRenderObject : public RenderObject { // Lefttop relative to render object lefttop. Rect GetCaretRect(); - std::shared_ptr<platform::graph::IBrush> GetCaretBrush() const { + std::shared_ptr<platform::graphics::IBrush> GetCaretBrush() const { return caret_brush_; } - void GetCaretBrush(std::shared_ptr<platform::graph::IBrush> brush); + void GetCaretBrush(std::shared_ptr<platform::graphics::IBrush> brush); 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: - void OnDrawContent(platform::graph::IPainter* painter) override; + void OnDrawContent(platform::graphics::IPainter* painter) override; // See remarks of this class. Size OnMeasureContent(const MeasureRequirement& requirement, @@ -93,16 +100,18 @@ class TextRenderObject : public RenderObject { void OnAfterLayout() override; private: - std::shared_ptr<platform::graph::IBrush> brush_; - std::shared_ptr<platform::graph::IFont> font_; - std::unique_ptr<platform::graph::ITextLayout> text_layout_; + std::shared_ptr<platform::graphics::IBrush> brush_; + std::shared_ptr<platform::graphics::IFont> font_; + std::unique_ptr<platform::graphics::ITextLayout> text_layout_; std::optional<TextRange> selection_range_ = std::nullopt; - std::shared_ptr<platform::graph::IBrush> selection_brush_; + std::shared_ptr<platform::graphics::IBrush> selection_brush_; bool draw_caret_ = false; gsl::index caret_position_ = 0; - std::shared_ptr<platform::graph::IBrush> caret_brush_; + std::shared_ptr<platform::graphics::IBrush> caret_brush_; float caret_width_ = default_caret_width; + + bool is_measure_including_trailing_space_ = false; }; } // namespace cru::ui::render diff --git a/include/cru/ui/render/WindowRenderObject.hpp b/include/cru/ui/render/WindowRenderObject.hpp deleted file mode 100644 index 4c254f42..00000000 --- a/include/cru/ui/render/WindowRenderObject.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once -#include "RenderObject.hpp" - -namespace cru::ui::render { -class WindowRenderObject : public RenderObject { - public: - WindowRenderObject(UiHost* host); - WindowRenderObject(const WindowRenderObject& other) = delete; - WindowRenderObject(WindowRenderObject&& other) = delete; - WindowRenderObject& operator=(const WindowRenderObject& other) = delete; - WindowRenderObject& operator=(WindowRenderObject&& other) = delete; - ~WindowRenderObject() override = default; - - RenderObject* HitTest(const Point& point) override; - - protected: - Size OnMeasureContent(const MeasureRequirement& requirement, - const MeasureSize& preferred_size) override; - void OnLayoutContent(const Rect& content_rect) override; - - private: - RenderObject* GetChild() const { - return GetChildren().empty() ? nullptr : GetChildren()[0]; - } - - private: - EventRevokerGuard after_layout_event_guard_; -}; -} // namespace cru::ui::render diff --git a/include/cru/ui/style/ApplyBorderStyleInfo.hpp b/include/cru/ui/style/ApplyBorderStyleInfo.hpp new file mode 100644 index 00000000..5058b51f --- /dev/null +++ b/include/cru/ui/style/ApplyBorderStyleInfo.hpp @@ -0,0 +1,28 @@ +#pragma once +#include <optional> +#include "../Base.hpp" + +namespace cru::ui::style { +struct ApplyBorderStyleInfo { + explicit ApplyBorderStyleInfo( + std::optional<std::shared_ptr<platform::graphics::IBrush>> border_brush = + std::nullopt, + std::optional<Thickness> border_thickness = std::nullopt, + std::optional<CornerRadius> border_radius = std::nullopt, + std::optional<std::shared_ptr<platform::graphics::IBrush>> + foreground_brush = std::nullopt, + std::optional<std::shared_ptr<platform::graphics::IBrush>> + background_brush = std::nullopt) + : border_brush(std::move(border_brush)), + border_thickness(std::move(border_thickness)), + border_radius(std::move(border_radius)), + foreground_brush(std::move(foreground_brush)), + background_brush(std::move(background_brush)) {} + + std::optional<std::shared_ptr<platform::graphics::IBrush>> border_brush; + std::optional<Thickness> border_thickness; + std::optional<CornerRadius> border_radius; + std::optional<std::shared_ptr<platform::graphics::IBrush>> foreground_brush; + std::optional<std::shared_ptr<platform::graphics::IBrush>> background_brush; +}; +} // namespace cru::ui::style diff --git a/include/cru/ui/style/Condition.hpp b/include/cru/ui/style/Condition.hpp new file mode 100644 index 00000000..d5cf16f2 --- /dev/null +++ b/include/cru/ui/style/Condition.hpp @@ -0,0 +1,123 @@ +#pragma once +#include "../Base.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/ClonablePtr.hpp" +#include "cru/common/Event.hpp" +#include "cru/ui/controls/IClickableControl.hpp" +#include "cru/ui/helper/ClickDetector.hpp" + +#include <memory> +#include <type_traits> +#include <utility> +#include <vector> + +namespace cru::ui::style { +class Condition : public Object { + public: + virtual std::vector<IBaseEvent*> ChangeOn( + controls::Control* control) const = 0; + virtual bool Judge(controls::Control* control) const = 0; + + virtual Condition* Clone() const = 0; +}; + +class NoCondition : public Condition { + public: + static ClonablePtr<NoCondition> Create() { + return ClonablePtr<NoCondition>(new NoCondition); + }; + + std::vector<IBaseEvent*> 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<ClonablePtr<Condition>> conditions); + + std::vector<IBaseEvent*> ChangeOn(controls::Control* control) const override; + + protected: + std::vector<ClonablePtr<Condition>> conditions_; +}; + +class AndCondition : public CompoundCondition { + public: + using CompoundCondition::CompoundCondition; + + bool Judge(controls::Control* control) const override; + + AndCondition* Clone() const override { return new AndCondition(conditions_); } +}; + +class OrCondition : public CompoundCondition { + public: + using CompoundCondition::CompoundCondition; + + bool Judge(controls::Control* control) const override; + + OrCondition* Clone() const override { return new OrCondition(conditions_); } +}; + +class FocusCondition : public Condition { + public: + static ClonablePtr<FocusCondition> Create(bool has_focus) { + return ClonablePtr<FocusCondition>(new FocusCondition(has_focus)); + } + + explicit FocusCondition(bool has_focus); + + std::vector<IBaseEvent*> ChangeOn(controls::Control* control) const override; + bool Judge(controls::Control* control) const override; + + FocusCondition* Clone() const override { + return new FocusCondition(has_focus_); + } + + private: + bool has_focus_; +}; + +class HoverCondition : public Condition { + public: + static ClonablePtr<HoverCondition> Create(bool hover) { + return ClonablePtr<HoverCondition>(new HoverCondition(hover)); + } + + explicit HoverCondition(bool hover) : hover_(hover) {} + + std::vector<IBaseEvent*> 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<ClickStateCondition> Create( + helper::ClickState click_state) { + return ClonablePtr<ClickStateCondition>( + new ClickStateCondition(click_state)); + } + + explicit ClickStateCondition(helper::ClickState click_state); + + std::vector<IBaseEvent*> ChangeOn(controls::Control* control) const override; + bool Judge(controls::Control* control) const override; + + ClickStateCondition* Clone() const override { + return new ClickStateCondition(click_state_); + } + + private: + helper::ClickState click_state_; +}; +} // namespace cru::ui::style diff --git a/include/cru/ui/style/StyleRule.hpp b/include/cru/ui/style/StyleRule.hpp new file mode 100644 index 00000000..8ac42cd0 --- /dev/null +++ b/include/cru/ui/style/StyleRule.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "Condition.hpp" +#include "Styler.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/ClonablePtr.hpp" +#include "cru/ui/Base.hpp" + +#include <memory> +#include <string> +#include <vector> + +namespace cru::ui::style { +class StyleRule : public Object { + public: + StyleRule(ClonablePtr<Condition> condition, ClonablePtr<Styler> styler, + std::u16string name = {}); + + CRU_DEFAULT_COPY(StyleRule) + CRU_DEFAULT_MOVE(StyleRule) + + ~StyleRule() override = default; + + public: + const std::u16string& GetName() const { return name_; } + Condition* GetCondition() const { return condition_.get(); } + Styler* GetStyler() const { return styler_.get(); } + + StyleRule WithNewCondition(ClonablePtr<Condition> condition, + std::u16string name = {}) const { + return StyleRule{std::move(condition), styler_, std::move(name)}; + } + + StyleRule WithNewStyler(ClonablePtr<Styler> styler, + std::u16string name = {}) const { + return StyleRule{condition_, std::move(styler), std::move(name)}; + } + + bool CheckAndApply(controls::Control* control) const; + + private: + ClonablePtr<Condition> condition_; + ClonablePtr<Styler> styler_; + std::u16string name_; +}; +} // namespace cru::ui::style diff --git a/include/cru/ui/style/StyleRuleSet.hpp b/include/cru/ui/style/StyleRuleSet.hpp new file mode 100644 index 00000000..e62dd2de --- /dev/null +++ b/include/cru/ui/style/StyleRuleSet.hpp @@ -0,0 +1,87 @@ +#pragma once +#include "StyleRule.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/Event.hpp" + +#include <cstddef> + +namespace cru::ui::style { +class StyleRuleSet : public Object { + public: + StyleRuleSet() = default; + explicit StyleRuleSet(StyleRuleSet* parent); + + CRU_DELETE_COPY(StyleRuleSet) + CRU_DELETE_MOVE(StyleRuleSet) + + ~StyleRuleSet() override = default; + + public: + StyleRuleSet* GetParent() const { return parent_; } + void SetParent(StyleRuleSet* parent); + + gsl::index GetSize() const { return static_cast<gsl::index>(rules_.size()); } + const std::vector<StyleRule>& GetRules() const { return rules_; } + + void AddStyleRule(StyleRule rule) { + AddStyleRule(std::move(rule), GetSize()); + } + + void AddStyleRule(StyleRule rule, gsl::index index); + + template <typename Iter> + 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, bool set_parent = false); + + 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<std::nullptr_t>* ChangeEvent() { return &change_event_; } + + private: + void RaiseChangeEvent() { change_event_.Raise(nullptr); } + + private: + Event<std::nullptr_t> change_event_; + + StyleRuleSet* parent_ = nullptr; + EventRevokerGuard parent_change_event_guard_; + + std::vector<StyleRule> 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_; + + // child first, parent last. + std::vector<StyleRuleSet*> ruleset_chain_cache_; + + EventRevokerListGuard guard_; +}; +} // namespace cru::ui::style diff --git a/include/cru/ui/style/Styler.hpp b/include/cru/ui/style/Styler.hpp new file mode 100644 index 00000000..865cbbaf --- /dev/null +++ b/include/cru/ui/style/Styler.hpp @@ -0,0 +1,80 @@ +#pragma once +#include "../Base.hpp" +#include "ApplyBorderStyleInfo.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/ClonablePtr.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/ui/controls/Control.hpp" + +#include <memory> +#include <vector> + +namespace cru::ui::style { +class Styler : public Object { + public: + virtual void Apply(controls::Control* control) const = 0; + + virtual Styler* Clone() const = 0; +}; + +class CompoundStyler : public Styler { + public: + template <typename... S> + static ClonablePtr<CompoundStyler> Create(ClonablePtr<S>... s) { + return ClonablePtr<CompoundStyler>( + new CompoundStyler(std::vector<ClonablePtr<Styler>>{std::move(s)...})); + } + + explicit CompoundStyler(std::vector<ClonablePtr<Styler>> stylers) + : stylers_(std::move(stylers)) {} + + void Apply(controls::Control* control) const override { + for (const auto& styler : stylers_) { + styler->Apply(control); + } + } + + virtual CompoundStyler* Clone() const override { + return new CompoundStyler(stylers_); + } + + private: + std::vector<ClonablePtr<Styler>> stylers_; +}; + +class BorderStyler : public Styler { + public: + static ClonablePtr<BorderStyler> Create(ApplyBorderStyleInfo style) { + return ClonablePtr<BorderStyler>(new BorderStyler(std::move(style))); + } + + explicit BorderStyler(ApplyBorderStyleInfo style); + + void Apply(controls::Control* control) const override; + + BorderStyler* Clone() const override { return new BorderStyler(style_); } + + private: + ApplyBorderStyleInfo style_; +}; + +class CursorStyler : public Styler { + public: + static ClonablePtr<CursorStyler> Create( + std::shared_ptr<platform::gui::ICursor> cursor) { + return ClonablePtr<CursorStyler>(new CursorStyler(std::move(cursor))); + } + + static ClonablePtr<CursorStyler> Create(platform::gui::SystemCursorType type); + + explicit CursorStyler(std::shared_ptr<platform::gui::ICursor> cursor) + : cursor_(std::move(cursor)) {} + + void Apply(controls::Control* control) const override; + + CursorStyler* Clone() const override { return new CursorStyler(cursor_); } + + private: + std::shared_ptr<platform::gui::ICursor> cursor_; +}; +} // namespace cru::ui::style diff --git a/include/cru/win/graph/direct/Brush.hpp b/include/cru/win/graphics/direct/Brush.hpp index df1debe3..fbff83b5 100644 --- a/include/cru/win/graph/direct/Brush.hpp +++ b/include/cru/win/graphics/direct/Brush.hpp @@ -2,9 +2,9 @@ #include "ComResource.hpp" #include "Resource.hpp" -#include "cru/platform/graph/Brush.hpp" +#include "cru/platform/graphics/Brush.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { struct ID2DBrush : virtual IBrush { virtual ID2D1Brush* GetD2DBrushInterface() const = 0; }; @@ -36,4 +36,4 @@ class D2DSolidColorBrush : public DirectGraphResource, Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush_; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/ComResource.hpp b/include/cru/win/graphics/direct/ComResource.hpp index 2ac332cd..34ea39ed 100644 --- a/include/cru/win/graph/direct/ComResource.hpp +++ b/include/cru/win/graphics/direct/ComResource.hpp @@ -3,9 +3,9 @@ #include "cru/common/Base.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { template <typename TInterface> struct IComResource : virtual Interface { virtual TInterface* GetComInterface() const = 0; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/ConvertUtil.hpp b/include/cru/win/graphics/direct/ConvertUtil.hpp index 12a04c7b..0d8da8a1 100644 --- a/include/cru/win/graph/direct/ConvertUtil.hpp +++ b/include/cru/win/graphics/direct/ConvertUtil.hpp @@ -1,9 +1,9 @@ #pragma once #include "../../WinPreConfig.hpp" -#include "cru/platform/graph/Base.hpp" +#include "cru/platform/graphics/Base.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { inline D2D1_MATRIX_3X2_F Convert(const platform::Matrix& matrix) { D2D1_MATRIX_3X2_F m; m._11 = matrix.m11; @@ -104,4 +104,4 @@ inline bool operator==(const D2D1_ELLIPSE& left, const D2D1_ELLIPSE& right) { inline bool operator!=(const D2D1_ELLIPSE& left, const D2D1_ELLIPSE& right) { return !(left == right); } -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/Exception.hpp b/include/cru/win/graphics/direct/Exception.hpp index 8b62e8fa..72493f2f 100644 --- a/include/cru/win/graph/direct/Exception.hpp +++ b/include/cru/win/graphics/direct/Exception.hpp @@ -1,7 +1,7 @@ #pragma once #include "../../Exception.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { using platform::win::HResultError; using platform::win::ThrowIfFailed; -} // namespace cru::platform::graph::win::direct
\ No newline at end of file +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/Factory.hpp b/include/cru/win/graphics/direct/Factory.hpp index e70454f5..70f3ede1 100644 --- a/include/cru/win/graph/direct/Factory.hpp +++ b/include/cru/win/graphics/direct/Factory.hpp @@ -1,9 +1,9 @@ #pragma once #include "Resource.hpp" -#include "cru/platform/graph/Factory.hpp" +#include "cru/platform/graphics/Factory.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { class DirectGraphFactory : public DirectResource, public virtual IGraphFactory { public: DirectGraphFactory(); @@ -55,4 +55,4 @@ class DirectGraphFactory : public DirectResource, public virtual IGraphFactory { Microsoft::WRL::ComPtr<IDWriteFactory> dwrite_factory_; Microsoft::WRL::ComPtr<IDWriteFontCollection> dwrite_system_font_collection_; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/Font.hpp b/include/cru/win/graphics/direct/Font.hpp index 2195f3e4..fd3921a3 100644 --- a/include/cru/win/graph/direct/Font.hpp +++ b/include/cru/win/graphics/direct/Font.hpp @@ -2,11 +2,11 @@ #include "ComResource.hpp" #include "Resource.hpp" -#include "cru/platform/graph/Font.hpp" +#include "cru/platform/graphics/Font.hpp" #include <string_view> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { class DWriteFont : public DirectGraphResource, public virtual IFont, public virtual IComResource<IDWriteTextFormat> { @@ -30,4 +30,4 @@ class DWriteFont : public DirectGraphResource, std::u16string font_family_; Microsoft::WRL::ComPtr<IDWriteTextFormat> text_format_; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/Geometry.hpp b/include/cru/win/graphics/direct/Geometry.hpp index 87987d3e..edfec590 100644 --- a/include/cru/win/graph/direct/Geometry.hpp +++ b/include/cru/win/graphics/direct/Geometry.hpp @@ -2,9 +2,9 @@ #include "ComResource.hpp" #include "Resource.hpp" -#include "cru/platform/graph/Geometry.hpp" +#include "cru/platform/graphics/Geometry.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { class D2DGeometryBuilder : public DirectGraphResource, public virtual IGeometryBuilder { public: @@ -54,4 +54,4 @@ class D2DGeometry : public DirectGraphResource, private: Microsoft::WRL::ComPtr<ID2D1PathGeometry> geometry_; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/Painter.hpp b/include/cru/win/graphics/direct/Painter.hpp index a50f962d..b34c1563 100644 --- a/include/cru/win/graph/direct/Painter.hpp +++ b/include/cru/win/graphics/direct/Painter.hpp @@ -2,11 +2,11 @@ #include "ComResource.hpp" #include "Resource.hpp" -#include "cru/platform/graph/Painter.hpp" +#include "cru/platform/graphics/Painter.hpp" #include <vector> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { class D2DPainter : public DirectResource, public virtual IPainter, public virtual IComResource<ID2D1RenderTarget> { @@ -27,6 +27,8 @@ class D2DPainter : public DirectResource, void Clear(const Color& color) override; + void DrawLine(const Point& start, const Point& end, IBrush* brush, + float width) override; void StrokeRectangle(const Rect& rectangle, IBrush* brush, float width) override; void FillRectangle(const Rect& rectangle, IBrush* brush) override; @@ -57,4 +59,4 @@ class D2DPainter : public DirectResource, bool is_drawing_ = true; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/Resource.hpp b/include/cru/win/graphics/direct/Resource.hpp index 6162ebd8..f60f373e 100644 --- a/include/cru/win/graph/direct/Resource.hpp +++ b/include/cru/win/graphics/direct/Resource.hpp @@ -1,11 +1,11 @@ #pragma once #include "../../WinPreConfig.hpp" -#include "cru/platform/graph/Resource.hpp" +#include "cru/platform/graphics/Resource.hpp" #include <string_view> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { class DirectGraphFactory; class DirectResource : public Object, public virtual INativeResource { @@ -46,4 +46,4 @@ class DirectGraphResource : public DirectResource, private: DirectGraphFactory* factory_; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graph/direct/TextLayout.hpp b/include/cru/win/graphics/direct/TextLayout.hpp index 016009ab..aa040278 100644 --- a/include/cru/win/graph/direct/TextLayout.hpp +++ b/include/cru/win/graphics/direct/TextLayout.hpp @@ -2,12 +2,12 @@ #include "ComResource.hpp" #include "Resource.hpp" -#include "cru/platform/graph/TextLayout.hpp" +#include "cru/platform/graphics/TextLayout.hpp" #include <limits> #include <memory> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { class DWriteFont; class DWriteTextLayout : public DirectGraphResource, @@ -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<Rect> TextRangeRect(const TextRange& text_range) override; @@ -52,4 +52,4 @@ class DWriteTextLayout : public DirectGraphResource, float max_height_ = std::numeric_limits<float>::max(); Microsoft::WRL::ComPtr<IDWriteTextLayout> text_layout_; }; -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graphics/direct/WindowPainter.hpp b/include/cru/win/graphics/direct/WindowPainter.hpp new file mode 100644 index 00000000..b5faf7b5 --- /dev/null +++ b/include/cru/win/graphics/direct/WindowPainter.hpp @@ -0,0 +1,21 @@ +#pragma once +#include "Painter.hpp" +#include "WindowRenderTarget.hpp" + +namespace cru::platform::graphics::win::direct { +class D2DWindowPainter : public graphics::win::direct::D2DPainter { + public: + explicit D2DWindowPainter(D2DWindowRenderTarget* window); + + CRU_DELETE_COPY(D2DWindowPainter) + CRU_DELETE_MOVE(D2DWindowPainter) + + ~D2DWindowPainter() override; + + protected: + void DoEndDraw() override; + + private: + D2DWindowRenderTarget* render_target_; +}; +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/graphics/direct/WindowRenderTarget.hpp b/include/cru/win/graphics/direct/WindowRenderTarget.hpp new file mode 100644 index 00000000..75b1bf20 --- /dev/null +++ b/include/cru/win/graphics/direct/WindowRenderTarget.hpp @@ -0,0 +1,42 @@ +#pragma once +#include "Factory.hpp" + +namespace cru::platform::graphics::win::direct { +// Represents a window render target. +class D2DWindowRenderTarget : public Object { + public: + D2DWindowRenderTarget(gsl::not_null<DirectGraphFactory*> factory, HWND hwnd); + + CRU_DELETE_COPY(D2DWindowRenderTarget) + CRU_DELETE_MOVE(D2DWindowRenderTarget) + + ~D2DWindowRenderTarget() override = default; + + public: + graphics::win::direct::DirectGraphFactory* GetDirectFactory() const { + return factory_; + } + + ID2D1DeviceContext* GetD2D1DeviceContext() { + return d2d1_device_context_.Get(); + } + + void SetDpi(float x, float y); + + // Resize the underlying buffer. + void ResizeBuffer(int width, int height); + + // Present the data of the underlying buffer to the window. + void Present(); + + private: + void CreateTargetBitmap(); + + private: + DirectGraphFactory* factory_; + HWND hwnd_; + Microsoft::WRL::ComPtr<ID2D1DeviceContext> d2d1_device_context_; + Microsoft::WRL::ComPtr<IDXGISwapChain1> dxgi_swap_chain_; + Microsoft::WRL::ComPtr<ID2D1Bitmap1> target_bitmap_; +}; +} // namespace cru::platform::graphics::win::direct diff --git a/include/cru/win/native/Base.hpp b/include/cru/win/gui/Base.hpp index a50c6dd1..00782663 100644 --- a/include/cru/win/native/Base.hpp +++ b/include/cru/win/gui/Base.hpp @@ -3,17 +3,14 @@ #include "cru/common/Base.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class GodWindow; class TimerManager; class WinCursor; class WinCursorManager; class WindowClass; class WindowManager; -class WindowRenderTarget; class WinNativeWindow; -class WinNativeWindowResolver; class WinUiApplication; -class WinInputMethodManager; -class WinInputMethodContextRef; -} // namespace cru::platform::native::win +class WinInputMethodContext; +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/Cursor.hpp b/include/cru/win/gui/Cursor.hpp index 373b9170..e7c76879 100644 --- a/include/cru/win/native/Cursor.hpp +++ b/include/cru/win/gui/Cursor.hpp @@ -1,13 +1,13 @@ #pragma once #include "Resource.hpp" -#include "cru/platform/native/Cursor.hpp" +#include "cru/platform/gui/Cursor.hpp" #include <memory> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class WinCursor : public WinNativeResource, public virtual ICursor { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::native::win::WinCursor") + CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::gui::win::WinCursor") public: WinCursor(HCURSOR handle, bool auto_destroy); @@ -45,5 +45,6 @@ class WinCursorManager : public WinNativeResource, private: std::shared_ptr<WinCursor> sys_arrow_; std::shared_ptr<WinCursor> sys_hand_; + std::shared_ptr<WinCursor> sys_ibeam_; }; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/Exception.hpp b/include/cru/win/gui/Exception.hpp index 6a5265c1..895e6c14 100644 --- a/include/cru/win/native/Exception.hpp +++ b/include/cru/win/gui/Exception.hpp @@ -1,7 +1,7 @@ #pragma once #include "../Exception.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { using platform::win::Win32Error; using platform::win::HResultError; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/GodWindow.hpp b/include/cru/win/gui/GodWindow.hpp index 8b20e01f..0343b159 100644 --- a/include/cru/win/native/GodWindow.hpp +++ b/include/cru/win/gui/GodWindow.hpp @@ -1,11 +1,14 @@ #pragma once #include "Base.hpp" +#include "WindowNativeMessageEventArgs.hpp" +#include "cru/common/Event.hpp" + #include <memory> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class GodWindow : public Object { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::native::win::GodWindow") + CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::gui::win::GodWindow") public: explicit GodWindow(WinUiApplication* application); @@ -20,10 +23,16 @@ class GodWindow : public Object { bool HandleGodWindowMessage(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param, LRESULT* result); + IEvent<WindowNativeMessageEventArgs&>* MessageEvent() { + return &message_event_; + } + private: WinUiApplication* application_; std::unique_ptr<WindowClass> god_window_class_; HWND hwnd_; + + Event<WindowNativeMessageEventArgs&> message_event_; }; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/InputMethod.hpp b/include/cru/win/gui/InputMethod.hpp index 113f460d..51a007d8 100644 --- a/include/cru/win/native/InputMethod.hpp +++ b/include/cru/win/gui/InputMethod.hpp @@ -6,13 +6,13 @@ #include "Resource.hpp" #include "WindowNativeMessageEventArgs.hpp" -#include "cru/platform/native/InputMethod.hpp" +#include "cru/platform/gui/InputMethod.hpp" #include <imm.h> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class AutoHIMC : public Object { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::native::win::AutoHIMC") + CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::gui::win::AutoHIMC") public: explicit AutoHIMC(HWND hwnd); @@ -35,7 +35,7 @@ class AutoHIMC : public Object { class WinInputMethodContext : public WinNativeResource, public virtual IInputMethodContext { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::native::win::WinInputMethodContext") + CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::gui::win::WinInputMethodContext") public: WinInputMethodContext(gsl::not_null<WinNativeWindow*> window); @@ -72,31 +72,16 @@ class WinInputMethodContext : public WinNativeResource, std::u16string GetResultString(); - std::optional<AutoHIMC> TryGetHIMC(); + AutoHIMC GetHIMC(); private: - std::shared_ptr<INativeWindowResolver> native_window_resolver_; + WinNativeWindow* native_window_; - std::vector<EventRevokerGuard> event_revoker_guards_; + EventRevokerListGuard event_guard_; Event<std::nullptr_t> composition_start_event_; Event<std::nullptr_t> composition_end_event_; Event<std::nullptr_t> composition_event_; Event<std::u16string_view> text_event_; }; - -class WinInputMethodManager : public WinNativeResource, - public virtual IInputMethodManager { - public: - WinInputMethodManager(WinUiApplication* application); - - CRU_DELETE_COPY(WinInputMethodManager) - CRU_DELETE_MOVE(WinInputMethodManager) - - ~WinInputMethodManager() override; - - public: - std::unique_ptr<IInputMethodContext> GetContext( - INativeWindow* window) override; -}; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/gui/Keyboard.hpp b/include/cru/win/gui/Keyboard.hpp new file mode 100644 index 00000000..5b98833c --- /dev/null +++ b/include/cru/win/gui/Keyboard.hpp @@ -0,0 +1,9 @@ +#pragma once +#include "Base.hpp" + +#include "cru/platform/gui/Keyboard.hpp" + +namespace cru::platform::gui::win { +KeyCode VirtualKeyToKeyCode(int virtual_key); +KeyModifier RetrieveKeyMofifier(); +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/Resource.hpp b/include/cru/win/gui/Resource.hpp index 0de0e1a8..1f6f0a4a 100644 --- a/include/cru/win/native/Resource.hpp +++ b/include/cru/win/gui/Resource.hpp @@ -3,7 +3,7 @@ #include "cru/platform/Resource.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class WinNativeResource : public Object, public virtual INativeResource { public: static constexpr std::u16string_view k_platform_id = u"Windows"; @@ -20,4 +20,4 @@ class WinNativeResource : public Object, public virtual INativeResource { public: std::u16string_view GetPlatformId() const final { return k_platform_id; } }; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/UiApplication.hpp b/include/cru/win/gui/UiApplication.hpp index cbc08af7..4cf46858 100644 --- a/include/cru/win/native/UiApplication.hpp +++ b/include/cru/win/gui/UiApplication.hpp @@ -1,15 +1,16 @@ #pragma once #include "Resource.hpp" -#include "cru/platform/native/UiApplication.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/platform/gui/UiApplication.hpp" #include <memory> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { class DirectGraphFactory; } -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class WinUiApplication : public WinNativeResource, public virtual IUiApplication { public: @@ -32,7 +33,7 @@ class WinUiApplication : public WinNativeResource, void AddOnQuitHandler(std::function<void()> handler) override; - void InvokeLater(std::function<void()> action) override; + long long SetImmediate(std::function<void()> action) override; long long SetTimeout(std::chrono::milliseconds milliseconds, std::function<void()> action) override; long long SetInterval(std::chrono::milliseconds milliseconds, @@ -40,17 +41,15 @@ class WinUiApplication : public WinNativeResource, void CancelTimer(long long id) override; std::vector<INativeWindow*> GetAllWindow() override; - std::shared_ptr<INativeWindowResolver> CreateWindow( - INativeWindow* parent) override; + INativeWindow* CreateWindow(INativeWindow* parent, CreateWindowFlag flag) override; - cru::platform::graph::IGraphFactory* GetGraphFactory() override; + cru::platform::graphics::IGraphFactory* GetGraphFactory() override; - cru::platform::graph::win::direct::DirectGraphFactory* GetDirectFactory() { + cru::platform::graphics::win::direct::DirectGraphFactory* GetDirectFactory() { return graph_factory_.get(); } ICursorManager* GetCursorManager() override; - IInputMethodManager* GetInputMethodManager() override; HINSTANCE GetInstanceHandle() const { return instance_handle_; } @@ -61,7 +60,7 @@ class WinUiApplication : public WinNativeResource, private: HINSTANCE instance_handle_; - std::unique_ptr<cru::platform::graph::win::direct::DirectGraphFactory> + std::unique_ptr<cru::platform::graphics::win::direct::DirectGraphFactory> graph_factory_; std::unique_ptr<GodWindow> god_window_; @@ -69,8 +68,7 @@ class WinUiApplication : public WinNativeResource, std::unique_ptr<WindowManager> window_manager_; std::unique_ptr<WinCursorManager> cursor_manager_; - std::unique_ptr<WinInputMethodManager> input_method_manager_; std::vector<std::function<void()>> quit_handlers_; }; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/Window.hpp b/include/cru/win/gui/Window.hpp index 3e0b11cd..3ba9ef68 100644 --- a/include/cru/win/native/Window.hpp +++ b/include/cru/win/gui/Window.hpp @@ -2,13 +2,16 @@ #include "Resource.hpp" #include "WindowNativeMessageEventArgs.hpp" -#include "cru/platform/native/Window.hpp" +#include "cru/platform/GraphBase.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/platform/gui/Window.hpp" +#include "cru/win/graphics/direct/WindowRenderTarget.hpp" #include <memory> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class WinNativeWindow : public WinNativeResource, public virtual INativeWindow { - CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::native::win::WinNativeWindow") + CRU_DEFINE_CLASS_LOG_TAG(u"cru::platform::gui::win::WinNativeWindow") public: WinNativeWindow(WinUiApplication* application, WindowClass* window_class, @@ -20,10 +23,6 @@ class WinNativeWindow : public WinNativeResource, public virtual INativeWindow { ~WinNativeWindow() override; public: - std::shared_ptr<INativeWindowResolver> GetResolver() override { - return std::static_pointer_cast<INativeWindowResolver>(resolver_); - } - void Close() override; WinNativeWindow* GetParent() override { return parent_window_; } @@ -48,7 +47,7 @@ class WinNativeWindow : public WinNativeResource, public virtual INativeWindow { bool ReleaseMouse() override; void RequestRepaint() override; - std::unique_ptr<graph::IPainter> BeginPaint() override; + std::unique_ptr<graphics::IPainter> BeginPaint() override; void SetCursor(std::shared_ptr<ICursor> cursor) override; @@ -60,18 +59,18 @@ class WinNativeWindow : public WinNativeResource, public virtual INativeWindow { return &mouse_enter_leave_event_; } IEvent<Point>* MouseMoveEvent() override { return &mouse_move_event_; } - IEvent<platform::native::NativeMouseButtonEventArgs>* MouseDownEvent() + IEvent<platform::gui::NativeMouseButtonEventArgs>* MouseDownEvent() override { return &mouse_down_event_; } - IEvent<platform::native::NativeMouseButtonEventArgs>* MouseUpEvent() + IEvent<platform::gui::NativeMouseButtonEventArgs>* MouseUpEvent() override { return &mouse_up_event_; } - IEvent<platform::native::NativeKeyEventArgs>* KeyDownEvent() override { + IEvent<platform::gui::NativeKeyEventArgs>* KeyDownEvent() override { return &key_down_event_; } - IEvent<platform::native::NativeKeyEventArgs>* KeyUpEvent() override { + IEvent<platform::gui::NativeKeyEventArgs>* KeyUpEvent() override { return &key_up_event_; } @@ -79,16 +78,40 @@ class WinNativeWindow : public WinNativeResource, public virtual INativeWindow { return &native_message_event_; } + IInputMethodContext* GetInputMethodContext() override; + // Get the handle of the window. Return null if window is invalid. HWND GetWindowHandle() const { return hwnd_; } bool HandleNativeWindowMessage(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param, LRESULT* result); - WindowRenderTarget* GetWindowRenderTarget() const { + graphics::win::direct::D2DWindowRenderTarget* GetWindowRenderTarget() const { return window_render_target_.get(); } + //*************** region: dpi *************** + float GetDpi() const { return dpi_; } + + inline int DipToPixel(const float dip) { + return static_cast<int>(dip * GetDpi() / 96.0f); + } + + inline POINT DipToPixel(const Point& dip_point) { + POINT result; + result.x = DipToPixel(dip_point.x); + result.y = DipToPixel(dip_point.y); + return result; + } + + inline float PixelToDip(const int pixel) { + return static_cast<float>(pixel) * 96.0f / GetDpi(); + } + + inline Point PixelToDip(const POINT& pi_point) { + return Point(PixelToDip(pi_point.x), PixelToDip(pi_point.y)); + } + private: // Get the client rect in pixel. RECT GetClientRectPixel(); @@ -104,8 +127,8 @@ class WinNativeWindow : public WinNativeResource, public virtual INativeWindow { void OnMouseMoveInternal(POINT point); void OnMouseLeaveInternal(); - void OnMouseDownInternal(platform::native::MouseButton button, POINT point); - void OnMouseUpInternal(platform::native::MouseButton button, POINT point); + void OnMouseDownInternal(platform::gui::MouseButton button, POINT point); + void OnMouseUpInternal(platform::gui::MouseButton button, POINT point); void OnMouseWheelInternal(short delta, POINT point); void OnKeyDownInternal(int virtual_code); @@ -124,53 +147,32 @@ class WinNativeWindow : public WinNativeResource, public virtual INativeWindow { // again. bool sync_flag_ = false; - std::shared_ptr<WinNativeWindowResolver> resolver_; - HWND hwnd_; WinNativeWindow* parent_window_; + float dpi_; + bool has_focus_ = false; bool is_mouse_in_ = false; - std::unique_ptr<WindowRenderTarget> window_render_target_; + std::unique_ptr<graphics::win::direct::D2DWindowRenderTarget> + window_render_target_; std::shared_ptr<WinCursor> cursor_; + std::unique_ptr<WinInputMethodContext> input_method_context_; + Event<std::nullptr_t> destroy_event_; Event<std::nullptr_t> paint_event_; Event<Size> resize_event_; Event<FocusChangeType> focus_event_; Event<MouseEnterLeaveType> mouse_enter_leave_event_; Event<Point> mouse_move_event_; - Event<platform::native::NativeMouseButtonEventArgs> mouse_down_event_; - Event<platform::native::NativeMouseButtonEventArgs> mouse_up_event_; - Event<platform::native::NativeKeyEventArgs> key_down_event_; - Event<platform::native::NativeKeyEventArgs> key_up_event_; + Event<platform::gui::NativeMouseButtonEventArgs> mouse_down_event_; + Event<platform::gui::NativeMouseButtonEventArgs> mouse_up_event_; + Event<platform::gui::NativeKeyEventArgs> key_down_event_; + Event<platform::gui::NativeKeyEventArgs> key_up_event_; Event<WindowNativeMessageEventArgs&> native_message_event_; }; - -class WinNativeWindowResolver : public WinNativeResource, - public virtual INativeWindowResolver { - friend WinNativeWindow::~WinNativeWindow(); - - public: - WinNativeWindowResolver(WinNativeWindow* window) : window_(window) {} - - CRU_DELETE_COPY(WinNativeWindowResolver) - CRU_DELETE_MOVE(WinNativeWindowResolver) - - ~WinNativeWindowResolver() override = default; - - public: - INativeWindow* Resolve() override { return window_; } - - private: - void Reset(); - - private: - WinNativeWindow* window_; -}; - -WinNativeWindow* Resolve(gsl::not_null<INativeWindowResolver*> resolver); -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/WindowClass.hpp b/include/cru/win/gui/WindowClass.hpp index fdd55065..2c07b68f 100644 --- a/include/cru/win/native/WindowClass.hpp +++ b/include/cru/win/gui/WindowClass.hpp @@ -3,7 +3,7 @@ #include <string> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class WindowClass : public Object { public: WindowClass(std::wstring name, WNDPROC window_proc, HINSTANCE h_instance); @@ -21,4 +21,4 @@ class WindowClass : public Object { std::wstring name_; ATOM atom_; }; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/WindowNativeMessageEventArgs.hpp b/include/cru/win/gui/WindowNativeMessageEventArgs.hpp index 84a7a123..834ba3c2 100644 --- a/include/cru/win/native/WindowNativeMessageEventArgs.hpp +++ b/include/cru/win/gui/WindowNativeMessageEventArgs.hpp @@ -3,7 +3,7 @@ #include "cru/common/Base.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { struct WindowNativeMessage { HWND hwnd; UINT msg; @@ -37,4 +37,4 @@ class WindowNativeMessageEventArgs : public Object { LRESULT result_; bool handled_ = false; }; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/include/cru/win/native/Keyboard.hpp b/include/cru/win/native/Keyboard.hpp deleted file mode 100644 index 790e0015..00000000 --- a/include/cru/win/native/Keyboard.hpp +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once -#include "Base.hpp" - -#include "cru/platform/native/Keyboard.hpp" - -namespace cru::platform::native::win { -KeyCode VirtualKeyToKeyCode(int virtual_key); -KeyModifier RetrieveKeyMofifier(); -} // namespace cru::platform::native::win diff --git a/include/cru/win/native/WindowRenderTarget.hpp b/include/cru/win/native/WindowRenderTarget.hpp deleted file mode 100644 index 83ac1e03..00000000 --- a/include/cru/win/native/WindowRenderTarget.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once -#include "Base.hpp" - -namespace cru::platform::graph::win::direct { -class DirectGraphFactory; -} - -namespace cru::platform::native::win { -// Represents a window render target. -class WindowRenderTarget : public Object { - public: - WindowRenderTarget(graph::win::direct::DirectGraphFactory* factory, - HWND hwnd); - - CRU_DELETE_COPY(WindowRenderTarget) - CRU_DELETE_MOVE(WindowRenderTarget) - - ~WindowRenderTarget() override = default; - - public: - graph::win::direct::DirectGraphFactory* GetDirectFactory() const { - return factory_; - } - - ID2D1DeviceContext* GetD2D1DeviceContext() { - return d2d1_device_context_.Get(); - } - - // Resize the underlying buffer. - void ResizeBuffer(int width, int height); - - // Set this render target as the d2d device context's target. - void SetAsTarget(); - - // Present the data of the underlying buffer to the window. - void Present(); - - private: - void CreateTargetBitmap(); - - private: - graph::win::direct::DirectGraphFactory* factory_; - Microsoft::WRL::ComPtr<ID2D1DeviceContext> d2d1_device_context_; - Microsoft::WRL::ComPtr<IDXGISwapChain1> dxgi_swap_chain_; - Microsoft::WRL::ComPtr<ID2D1Bitmap1> target_bitmap_; -}; -} // namespace cru::platform::native::win diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 6a18ef2b..73ad9456 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(cru_base PUBLIC ${CRU_BASE_INCLUDE_DIR}/Base.hpp ${CRU_BASE_INCLUDE_DIR}/Bitmask.hpp ${CRU_BASE_INCLUDE_DIR}/Event.hpp + ${CRU_BASE_INCLUDE_DIR}/Format.hpp ${CRU_BASE_INCLUDE_DIR}/Logger.hpp ${CRU_BASE_INCLUDE_DIR}/PreConfig.hpp ${CRU_BASE_INCLUDE_DIR}/SelfResolvable.hpp diff --git a/src/common/Logger.cpp b/src/common/Logger.cpp index dfa25347..af1dd692 100644 --- a/src/common/Logger.cpp +++ b/src/common/Logger.cpp @@ -73,7 +73,6 @@ void Logger::Log(LogLevel level, std::u16string_view tag, } #endif for (const auto &source : sources_) { - auto now = std::time(nullptr); source->Write(level, fmt::format(u"[{}] {} {}: {}\n", GetLogTime(), LogLevelToString(level), tag, s)); } diff --git a/src/common/StringUtil.cpp b/src/common/StringUtil.cpp index fc6d6349..0cadc545 100644 --- a/src/common/StringUtil.cpp +++ b/src/common/StringUtil.cpp @@ -1,4 +1,6 @@ #include "cru/common/StringUtil.hpp" +#include "cru/common/Base.hpp" +#include "gsl/gsl_util" namespace cru { namespace { @@ -191,8 +193,8 @@ void Utf8EncodeCodePointAppend(CodePoint code_point, std::string& str) { } void Utf16EncodeCodePointAppend(CodePoint code_point, std::u16string& str) { - if (code_point >= 0 && code_point <= 0xD7FF || - code_point >= 0xE000 && code_point <= 0xFFFF) { + if ((code_point >= 0 && code_point <= 0xD7FF) || + (code_point >= 0xE000 && code_point <= 0xFFFF)) { str.push_back(static_cast<char16_t>(code_point)); } else if (code_point >= 0x10000 && code_point <= 0x10FFFF) { std::uint32_t u = code_point - 0x10000; @@ -220,4 +222,66 @@ std::u16string ToUtf16(std::string_view s) { } return result; } + +bool Utf16IsValidInsertPosition(std::u16string_view s, gsl::index position) { + if (position < 0) return false; + if (position > static_cast<gsl::index>(s.size())) return false; + if (position == 0) return true; + if (position == static_cast<gsl::index>(s.size())) return true; + return !IsUtf16SurrogatePairTrailing(s[position]); +} + +gsl::index Utf16BackwardUntil(std::u16string_view str, gsl::index position, + const std::function<bool(CodePoint)>& predicate) { + if (position <= 0) return position; + while (true) { + gsl::index p = position; + auto c = Utf16PreviousCodePoint(str, p, &position); + if (predicate(c)) return p; + if (c == k_invalid_code_point) return p; + } + UnreachableCode(); +} + +gsl::index Utf16ForwardUntil(std::u16string_view str, gsl::index position, + const std::function<bool(CodePoint)>& predicate) { + if (position >= static_cast<gsl::index>(str.size())) return position; + while (true) { + gsl::index p = position; + auto c = Utf16NextCodePoint(str, p, &position); + if (predicate(c)) return p; + if (c == k_invalid_code_point) return p; + } + UnreachableCode(); +} + +inline bool IsSpace(CodePoint c) { return c == 0x20 || c == 0xA; } + +gsl::index Utf16PreviousWord(std::u16string_view str, gsl::index position, + bool* is_space) { + if (position <= 0) return position; + auto c = Utf16PreviousCodePoint(str, position, nullptr); + if (IsSpace(c)) { // TODO: Currently only test against 0x20(space). + if (is_space) *is_space = true; + return Utf16BackwardUntil(str, position, + [](CodePoint c) { return !IsSpace(c); }); + } else { + if (is_space) *is_space = false; + return Utf16BackwardUntil(str, position, IsSpace); + } +} + +gsl::index Utf16NextWord(std::u16string_view str, gsl::index position, + bool* is_space) { + if (position >= static_cast<gsl::index>(str.size())) return position; + auto c = Utf16NextCodePoint(str, position, nullptr); + if (IsSpace(c)) { // TODO: Currently only test against 0x20(space). + if (is_space) *is_space = true; + return Utf16ForwardUntil(str, position, + [](CodePoint c) { return !IsSpace(c); }); + } else { + if (is_space) *is_space = false; + return Utf16ForwardUntil(str, position, IsSpace); + } +} } // namespace cru diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt index 51253b56..623ec08f 100644 --- a/src/platform/CMakeLists.txt +++ b/src/platform/CMakeLists.txt @@ -10,5 +10,5 @@ target_sources(cru_platform_base INTERFACE ) target_link_libraries(cru_platform_base INTERFACE cru_base) -add_subdirectory(graph) -add_subdirectory(native) +add_subdirectory(graphics) +add_subdirectory(gui) diff --git a/src/platform/graph/CMakeLists.txt b/src/platform/graph/CMakeLists.txt deleted file mode 100644 index 3bf11e8d..00000000 --- a/src/platform/graph/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -set(CRU_PLATFORM_GRAPH_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/platform/graph) -add_library(cru_platform_graph INTERFACE) -target_sources(cru_platform_graph INTERFACE - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/Base.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/Brush.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/Font.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/Geometry.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/Factory.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/Resource.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/Painter.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/TextLayout.hpp - ${CRU_PLATFORM_GRAPH_INCLUDE_DIR}/util/Painter.hpp -) -target_link_libraries(cru_platform_graph INTERFACE cru_platform_base) diff --git a/src/platform/graphics/CMakeLists.txt b/src/platform/graphics/CMakeLists.txt new file mode 100644 index 00000000..5f841267 --- /dev/null +++ b/src/platform/graphics/CMakeLists.txt @@ -0,0 +1,14 @@ +set(CRU_PLATFORM_GRAPHICS_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/platform/graphics) +add_library(cru_platform_graphics INTERFACE) +target_sources(cru_platform_graphics INTERFACE + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/Base.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/Brush.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/Font.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/Geometry.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/Factory.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/Resource.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/Painter.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/TextLayout.hpp + ${CRU_PLATFORM_GRAPHICS_INCLUDE_DIR}/util/Painter.hpp +) +target_link_libraries(cru_platform_graphics INTERFACE cru_platform_base) diff --git a/src/platform/gui/CMakeLists.txt b/src/platform/gui/CMakeLists.txt new file mode 100644 index 00000000..aca7620c --- /dev/null +++ b/src/platform/gui/CMakeLists.txt @@ -0,0 +1,14 @@ +set(CRU_PLATFORM_GUI_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/platform/gui) +add_library(cru_platform_gui STATIC + Keyboard.cpp + UiApplication.cpp +) +target_sources(cru_platform_gui PUBLIC + ${CRU_PLATFORM_GUI_INCLUDE_DIR}/Base.hpp + ${CRU_PLATFORM_GUI_INCLUDE_DIR}/Cursor.hpp + ${CRU_PLATFORM_GUI_INCLUDE_DIR}/InputMethod.hpp + ${CRU_PLATFORM_GUI_INCLUDE_DIR}/Keyboard.hpp + ${CRU_PLATFORM_GUI_INCLUDE_DIR}/Window.hpp + ${CRU_PLATFORM_GUI_INCLUDE_DIR}/UiApplication.hpp +) +target_link_libraries(cru_platform_gui PUBLIC cru_platform_graphics) diff --git a/src/platform/gui/Keyboard.cpp b/src/platform/gui/Keyboard.cpp new file mode 100644 index 00000000..24880e00 --- /dev/null +++ b/src/platform/gui/Keyboard.cpp @@ -0,0 +1,142 @@ +#include "cru/platform/gui/Keyboard.hpp" + +#include <array> +#include <string> +#include <string_view> + +namespace cru::platform::gui { +constexpr std::array<std::u16string_view, + static_cast<int>(KeyCode::NumPad9) + 1> + key_code_string_list{u"Unknown", + u"LeftButton", + u"MiddleButton", + u"RightButton", + u"Escape", + u"F1", + u"F2", + u"F3", + u"F4", + u"F5", + u"F6", + u"F7", + u"F8", + u"F9", + u"F10", + u"F11", + u"F12", + u"N0", + u"N1", + u"N2", + u"N3", + u"N4", + u"N5", + u"N6", + u"N7", + u"N8", + u"N9", + u"A", + u"B", + u"C", + u"D", + u"E", + u"F", + u"G", + u"H", + u"I", + u"J", + u"K", + u"L", + u"M", + u"N", + u"O", + u"P", + u"Q", + u"R", + u"S", + u"T", + u"U", + u"V", + u"W", + u"X", + u"Y", + u"Z", + u"GraveAccent", + u"Tab", + u"CapsLock", + u"LeftShift", + u"LeftCtrl", + u"LeftSuper", + u"LeftAlt", + u"Minus", + u"Equal", + u"Backspace", + u"LeftSquareBracket", + u"RightSquareBracket", + u"BackSlash", + u"Semicolon", + u"Quote", + u"Comma", + u"Period", + u"Slash", + u"RightShift", + u"RightCtrl", + u"RightSuper", + u"RightAlt", + u"Insert", + u"Delete", + u"Home", + u"End", + u"PageUp", + u"PageDown", + u"Up", + u"Left", + u"Down", + u"Right", + u"PrintScreen", + u"ScrollLock", + u"Pause", + u"NumPad0", + u"NumPad1", + u"NumPad2", + u"NumPad3", + u"NumPad4", + u"NumPad5", + u"NumPad6", + u"NumPad7", + u"NumPad8", + u"NumPad9"}; + +std::u16string_view ToString(KeyCode key_code) { + if (static_cast<int>(key_code) < 0 || + static_cast<int>(key_code) >= + static_cast<int>(key_code_string_list.size())) + return u"UNKNOWN_KEYCODENAME"; + + return key_code_string_list[static_cast<int>(key_code)]; +} + +std::u16string ToString(KeyModifier key_modifier, + std::u16string_view separator) { + std::vector<std::u16string> list; + if (key_modifier & KeyModifiers::shift) { + list.push_back(u"Shift"); + } + + if (key_modifier & KeyModifiers::ctrl) { + list.push_back(u"Ctrl"); + } + + if (key_modifier & KeyModifiers::alt) { + list.push_back(u"Shift"); + } + + if (list.empty()) return u""; + std::u16string result = list.front(); + for (auto iter = list.cbegin() + 1; iter != list.cend(); ++iter) { + result += separator; + result += *iter; + } + + return result; +} +} // namespace cru::platform::gui diff --git a/src/platform/native/UiApplication.cpp b/src/platform/gui/UiApplication.cpp index 200b10e0..f095361e 100644 --- a/src/platform/native/UiApplication.cpp +++ b/src/platform/gui/UiApplication.cpp @@ -1,6 +1,6 @@ -#include "cru/platform/native/UiApplication.hpp" +#include "cru/platform/gui/UiApplication.hpp" -namespace cru::platform::native { +namespace cru::platform::gui { IUiApplication* IUiApplication::instance = nullptr; IUiApplication::IUiApplication() { @@ -12,4 +12,4 @@ IUiApplication::IUiApplication() { } IUiApplication::~IUiApplication() { instance = nullptr; } -} // namespace cru::platform::native +} // namespace cru::platform::gui diff --git a/src/platform/native/CMakeLists.txt b/src/platform/native/CMakeLists.txt deleted file mode 100644 index c68a0958..00000000 --- a/src/platform/native/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -set(CRU_PLATFORM_NATIVE_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/platform/native) -add_library(cru_platform_native STATIC - UiApplication.cpp -) -target_sources(cru_platform_native PUBLIC - ${CRU_PLATFORM_NATIVE_INCLUDE_DIR}/Base.hpp - ${CRU_PLATFORM_NATIVE_INCLUDE_DIR}/Cursor.hpp - ${CRU_PLATFORM_NATIVE_INCLUDE_DIR}/InputMethod.hpp - ${CRU_PLATFORM_NATIVE_INCLUDE_DIR}/Keyboard.hpp - ${CRU_PLATFORM_NATIVE_INCLUDE_DIR}/Window.hpp - ${CRU_PLATFORM_NATIVE_INCLUDE_DIR}/UiApplication.hpp -) -target_link_libraries(cru_platform_native PUBLIC cru_platform_graph) diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 6c50ec57..7d2792d6 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -2,53 +2,74 @@ set(CRU_UI_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/ui) add_library(cru_ui STATIC Helper.hpp - RoutedEventDispatch.hpp + host/RoutedEventDispatch.hpp - ClickDetector.cpp - ContentControl.cpp - Control.cpp Helper.cpp - LayoutControl.cpp - NoChildControl.cpp - UiEvent.cpp - UiHost.cpp UiManager.cpp - Window.cpp + components/Component.cpp + components/Menu.cpp controls/Button.cpp controls/Container.cpp + controls/ContentControl.cpp + controls/Control.cpp controls/FlexLayout.cpp + controls/LayoutControl.cpp + controls/NoChildControl.cpp + controls/Popup.cpp + controls/RootControl.cpp controls/StackLayout.cpp controls/TextBlock.cpp controls/TextBox.cpp - controls/TextControlService.hpp + controls/TextHostControlService.cpp + controls/Window.cpp + events/UiEvent.cpp + helper/BorderStyle.cpp + helper/ClickDetector.cpp + helper/ShortcutHub.cpp + host/LayoutPaintCycler.cpp + host/WindowHost.cpp render/BorderRenderObject.cpp render/CanvasRenderObject.cpp render/FlexLayoutRenderObject.cpp render/LayoutHelper.cpp render/RenderObject.cpp + render/ScrollBar.cpp render/ScrollRenderObject.cpp render/StackLayoutRenderObject.cpp render/TextRenderObject.cpp - render/WindowRenderObject.cpp + style/Condition.cpp + style/Styler.cpp + style/StyleRule.cpp + style/StyleRuleSet.cpp ) target_sources(cru_ui PUBLIC ${CRU_UI_INCLUDE_DIR}/Base.hpp - ${CRU_UI_INCLUDE_DIR}/ClickDetector.hpp - ${CRU_UI_INCLUDE_DIR}/ContentControl.hpp - ${CRU_UI_INCLUDE_DIR}/Control.hpp - ${CRU_UI_INCLUDE_DIR}/LayoutControl.hpp - ${CRU_UI_INCLUDE_DIR}/NoChildControl.hpp - ${CRU_UI_INCLUDE_DIR}/UiEvent.hpp - ${CRU_UI_INCLUDE_DIR}/UiHost.hpp + ${CRU_UI_INCLUDE_DIR}/DebugFlags.hpp ${CRU_UI_INCLUDE_DIR}/UiManager.hpp - ${CRU_UI_INCLUDE_DIR}/Window.hpp + ${CRU_UI_INCLUDE_DIR}/components/Component.hpp + ${CRU_UI_INCLUDE_DIR}/components/Menu.hpp ${CRU_UI_INCLUDE_DIR}/controls/Base.hpp ${CRU_UI_INCLUDE_DIR}/controls/Button.hpp ${CRU_UI_INCLUDE_DIR}/controls/Container.hpp + ${CRU_UI_INCLUDE_DIR}/controls/ContentControl.hpp + ${CRU_UI_INCLUDE_DIR}/controls/Control.hpp ${CRU_UI_INCLUDE_DIR}/controls/FlexLayout.hpp + ${CRU_UI_INCLUDE_DIR}/controls/IBorderControl.hpp + ${CRU_UI_INCLUDE_DIR}/controls/IClickableControl.hpp + ${CRU_UI_INCLUDE_DIR}/controls/LayoutControl.hpp + ${CRU_UI_INCLUDE_DIR}/controls/NoChildControl.hpp + ${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 + ${CRU_UI_INCLUDE_DIR}/helper/ShortcutHub.hpp + ${CRU_UI_INCLUDE_DIR}/host/LayoutPaintCycler.hpp + ${CRU_UI_INCLUDE_DIR}/host/WindowHost.hpp ${CRU_UI_INCLUDE_DIR}/render/Base.hpp ${CRU_UI_INCLUDE_DIR}/render/BorderRenderObject.hpp ${CRU_UI_INCLUDE_DIR}/render/CanvasRenderObject.hpp @@ -57,9 +78,14 @@ target_sources(cru_ui PUBLIC ${CRU_UI_INCLUDE_DIR}/render/LayoutRenderObject.hpp ${CRU_UI_INCLUDE_DIR}/render/MeasureRequirement.hpp ${CRU_UI_INCLUDE_DIR}/render/RenderObject.hpp + ${CRU_UI_INCLUDE_DIR}/render/ScrollBar.hpp ${CRU_UI_INCLUDE_DIR}/render/ScrollRenderObject.hpp ${CRU_UI_INCLUDE_DIR}/render/StackLayoutRenderObject.hpp ${CRU_UI_INCLUDE_DIR}/render/TextRenderObject.hpp - ${CRU_UI_INCLUDE_DIR}/render/WindowRenderObject.hpp + ${CRU_UI_INCLUDE_DIR}/style/ApplyBorderStyleInfo.hpp + ${CRU_UI_INCLUDE_DIR}/style/Condition.hpp + ${CRU_UI_INCLUDE_DIR}/style/Styler.hpp + ${CRU_UI_INCLUDE_DIR}/style/StyleRule.hpp + ${CRU_UI_INCLUDE_DIR}/style/StyleRuleSet.hpp ) -target_link_libraries(cru_ui PUBLIC cru_platform_native) +target_link_libraries(cru_ui PUBLIC cru_platform_gui) diff --git a/src/ui/ContentControl.cpp b/src/ui/ContentControl.cpp deleted file mode 100644 index 8d1a17d2..00000000 --- a/src/ui/ContentControl.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "cru/ui/ContentControl.hpp" - -#include "cru/ui/Window.hpp" - -namespace cru::ui { -ContentControl::ContentControl() - : child_vector_{nullptr}, child_(child_vector_[0]) {} - -ContentControl::~ContentControl() { delete child_; } - -void ContentControl::SetChild(Control* child) { - Expects(!dynamic_cast<Window*>(child)); // Can't add a window as child. - if (child == child_) return; - - const auto host = GetUiHost(); - const auto old_child = child_; - child_ = child; - if (old_child) { - old_child->_SetParent(nullptr); - old_child->_SetDescendantUiHost(nullptr); - } - if (child) { - child->_SetParent(this); - child->_SetDescendantUiHost(host); - } - OnChildChanged(old_child, child); -} - -void ContentControl::OnChildChanged(Control* old_child, Control* new_child) { - CRU_UNUSED(old_child) - CRU_UNUSED(new_child) -} -} // namespace cru::ui diff --git a/src/ui/Control.cpp b/src/ui/Control.cpp deleted file mode 100644 index cd1367fe..00000000 --- a/src/ui/Control.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#include "cru/ui/Control.hpp" - -#include "cru/platform/native/Cursor.hpp" -#include "cru/platform/native/UiApplication.hpp" -#include "cru/ui/Base.hpp" -#include "cru/ui/UiHost.hpp" -#include "RoutedEventDispatch.hpp" - -#include <memory> - -namespace cru::ui { -using platform::native::ICursor; -using platform::native::IUiApplication; -using platform::native::SystemCursorType; - -Control::Control() { - 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); - }); -} - -void Control::_SetParent(Control* parent) { - const auto old_parent = GetParent(); - parent_ = parent; - const auto new_parent = GetParent(); - if (old_parent != new_parent) OnParentChanged(old_parent, new_parent); -} - -void Control::_SetDescendantUiHost(UiHost* host) { - if (host == nullptr && ui_host_ == nullptr) return; - - // You can only attach or detach window. - Expects((host != nullptr && ui_host_ == nullptr) || - (host == nullptr && ui_host_ != nullptr)); - - if (host == nullptr) { - const auto old = ui_host_; - TraverseDescendants([old](Control* control) { - control->ui_host_ = nullptr; - control->OnDetachFromHost(old); - }); - } else - TraverseDescendants([host](Control* control) { - control->ui_host_ = host; - control->OnAttachToHost(host); - }); -} - -void Control::TraverseDescendants( - const std::function<void(Control*)>& predicate) { - _TraverseDescendants(this, predicate); -} - -void Control::_TraverseDescendants( - Control* control, const std::function<void(Control*)>& predicate) { - predicate(control); - for (auto c : control->GetChildren()) _TraverseDescendants(c, predicate); -} - -bool Control::RequestFocus() { - auto host = GetUiHost(); - if (host == nullptr) return false; - - return host->RequestFocusFor(this); -} - -bool Control::HasFocus() { - auto host = GetUiHost(); - if (host == nullptr) return false; - - return host->GetFocusControl() == this; -} - -bool Control::CaptureMouse() { - auto host = GetUiHost(); - if (host == nullptr) return false; - - return host->CaptureMouseFor(this); -} - -bool Control::ReleaseMouse() { - auto host = GetUiHost(); - if (host == nullptr) return false; - - return host->CaptureMouseFor(nullptr); -} - -bool Control::IsMouseCaptured() { - auto host = GetUiHost(); - 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 = GetUiHost(); - if (host != nullptr) { - host->UpdateCursor(); - } -} - -void Control::OnParentChanged(Control* old_parent, Control* new_parent) { - CRU_UNUSED(old_parent) - CRU_UNUSED(new_parent) -} - -void Control::OnAttachToHost(UiHost* host) { CRU_UNUSED(host) } - -void Control::OnDetachFromHost(UiHost* host) { CRU_UNUSED(host) } -} // namespace cru::ui diff --git a/src/ui/Helper.cpp b/src/ui/Helper.cpp index 6f67e701..88ead993 100644 --- a/src/ui/Helper.cpp +++ b/src/ui/Helper.cpp @@ -1,11 +1,11 @@ #include "Helper.hpp" -#include "cru/platform/graph/Factory.hpp" -#include "cru/platform/native/UiApplication.hpp" +#include "cru/platform/graphics/Factory.hpp" +#include "cru/platform/gui/UiApplication.hpp" namespace cru::ui { -using cru::platform::graph::IGraphFactory; -using cru::platform::native::IUiApplication; +using cru::platform::graphics::IGraphFactory; +using cru::platform::gui::IUiApplication; IGraphFactory* GetGraphFactory() { return IUiApplication::GetInstance()->GetGraphFactory(); diff --git a/src/ui/Helper.hpp b/src/ui/Helper.hpp index 6923852f..327f91ff 100644 --- a/src/ui/Helper.hpp +++ b/src/ui/Helper.hpp @@ -12,6 +12,6 @@ struct IUiApplication; } // namespace cru::platform namespace cru::ui { -cru::platform::graph::IGraphFactory* GetGraphFactory(); -cru::platform::native::IUiApplication* GetUiApplication(); +cru::platform::graphics::IGraphFactory* GetGraphFactory(); +cru::platform::gui::IUiApplication* GetUiApplication(); } // namespace cru::ui diff --git a/src/ui/LayoutControl.cpp b/src/ui/LayoutControl.cpp deleted file mode 100644 index 4813566b..00000000 --- a/src/ui/LayoutControl.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "cru/ui/LayoutControl.hpp" - -#include "cru/ui/Window.hpp" - -namespace cru::ui { -LayoutControl::~LayoutControl() { - for (const auto child : children_) delete child; -} - -void LayoutControl::AddChild(Control* control, const Index position) { - Expects(control->GetParent() == - nullptr); // The control already has a parent. - Expects(!dynamic_cast<Window*>(control)); // Can't add a window as child. - Expects(position >= 0); - Expects(position <= - static_cast<Index>( - this->children_.size())); // The position is out of range. - - children_.insert(this->children_.cbegin() + position, control); - - control->_SetParent(this); - control->_SetDescendantUiHost(GetUiHost()); - - OnAddChild(control, position); -} - -void LayoutControl::RemoveChild(const Index position) { - Expects(position >= 0); - Expects(position < - static_cast<Index>( - this->children_.size())); // The position is out of range. - - const auto i = children_.cbegin() + position; - const auto child = *i; - - children_.erase(i); - - child->_SetParent(nullptr); - child->_SetDescendantUiHost(nullptr); - - OnRemoveChild(child, position); -} - -void LayoutControl::OnAddChild(Control* child, const Index position) { - CRU_UNUSED(child) - CRU_UNUSED(position) -} - -void LayoutControl::OnRemoveChild(Control* child, const Index position) { - CRU_UNUSED(child) - CRU_UNUSED(position) -} -} // namespace cru::ui diff --git a/src/ui/NoChildControl.cpp b/src/ui/NoChildControl.cpp deleted file mode 100644 index 86861049..00000000 --- a/src/ui/NoChildControl.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include "cru/ui/NoChildControl.hpp" - -namespace cru::ui { -const std::vector<Control*> NoChildControl::empty_control_vector{}; -} diff --git a/src/ui/UiHost.cpp b/src/ui/UiHost.cpp deleted file mode 100644 index d8dcb6da..00000000 --- a/src/ui/UiHost.cpp +++ /dev/null @@ -1,388 +0,0 @@ -#include "cru/ui/UiHost.hpp" - -#include "RoutedEventDispatch.hpp" -#include "cru/common/Logger.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/Window.hpp" -#include "cru/ui/render/WindowRenderObject.hpp" - -namespace cru::ui { -using platform::native::INativeWindow; -using platform::native::IUiApplication; - -namespace event_names { -#ifdef CRU_DEBUG -// clang-format off -#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = CRU_MAKE_UNICODE_LITERAL(name); -// clang-format on -#else -#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = u""; -#endif - -CRU_DEFINE_EVENT_NAME(LoseFocus) -CRU_DEFINE_EVENT_NAME(GainFocus) -CRU_DEFINE_EVENT_NAME(MouseEnter) -CRU_DEFINE_EVENT_NAME(MouseLeave) -CRU_DEFINE_EVENT_NAME(MouseMove) -CRU_DEFINE_EVENT_NAME(MouseDown) -CRU_DEFINE_EVENT_NAME(MouseUp) -CRU_DEFINE_EVENT_NAME(KeyDown) -CRU_DEFINE_EVENT_NAME(KeyUp) - -#undef CRU_DEFINE_EVENT_NAME -} // namespace event_names - -namespace { -bool IsAncestor(Control* control, Control* ancestor) { - while (control != nullptr) { - if (control == ancestor) return true; - control = control->GetParent(); - } - return false; -} - -std::list<Control*> GetAncestorList(Control* control) { - std::list<Control*> l; - while (control != nullptr) { - l.push_front(control); - control = control->GetParent(); - } - return l; -} - -Control* FindLowestCommonAncestor(Control* left, Control* right) { - if (left == nullptr || right == nullptr) return nullptr; - - auto&& left_list = GetAncestorList(left); - auto&& right_list = GetAncestorList(right); - - // the root is different - if (left_list.front() != right_list.front()) return nullptr; - - // find the last same control or the last control (one is ancestor of the - // other) - auto left_i = left_list.cbegin(); - auto right_i = right_list.cbegin(); - - while (true) { - if (left_i == left_list.cend()) { - return *(--left_i); - } - if (right_i == right_list.cend()) { - return *(--right_i); - } - if (*left_i != *right_i) { - return *(--left_i); - } - ++left_i; - ++right_i; - } -} -} // namespace - -namespace { -template <typename T> -inline void BindNativeEvent( - UiHost* host, INativeWindow* native_window, IEvent<T>* event, - void (UiHost::*handler)(INativeWindow*, typename IEvent<T>::EventArgs), - std::vector<EventRevokerGuard>& guard_pool) { - guard_pool.push_back(EventRevokerGuard(event->AddHandler( - std::bind(handler, host, native_window, std::placeholders::_1)))); -} -} // namespace - -UiHost::UiHost(Window* window) - : window_control_(window), - mouse_hover_control_(nullptr), - focus_control_(window), - mouse_captured_control_(nullptr) { - const auto ui_application = IUiApplication::GetInstance(); - native_window_resolver_ = ui_application->CreateWindow(nullptr); - - const auto native_window = native_window_resolver_->Resolve(); - - auto input_method_context = - ui_application->GetInputMethodManager()->GetContext(native_window); - input_method_context->DisableIME(); - - window->ui_host_ = this; - - root_render_object_ = std::make_unique<render::WindowRenderObject>(this); - root_render_object_->SetAttachedControl(window); - window->render_object_ = root_render_object_.get(); - - BindNativeEvent(this, native_window, native_window->DestroyEvent(), - &UiHost::OnNativeDestroy, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->PaintEvent(), - &UiHost::OnNativePaint, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->ResizeEvent(), - &UiHost::OnNativeResize, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->FocusEvent(), - &UiHost::OnNativeFocus, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->MouseEnterLeaveEvent(), - &UiHost::OnNativeMouseEnterLeave, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->MouseMoveEvent(), - &UiHost::OnNativeMouseMove, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->MouseDownEvent(), - &UiHost::OnNativeMouseDown, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->MouseUpEvent(), - &UiHost::OnNativeMouseUp, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->KeyDownEvent(), - &UiHost::OnNativeKeyDown, event_revoker_guards_); - BindNativeEvent(this, native_window, native_window->KeyUpEvent(), - &UiHost::OnNativeKeyUp, event_revoker_guards_); -} - -UiHost::~UiHost() { - deleting_ = true; - window_control_->TraverseDescendants( - [this](Control* control) { control->OnDetachFromHost(this); }); - if (!native_window_destroyed_) { - const auto native_window = native_window_resolver_->Resolve(); - if (native_window) { - native_window->Close(); - } - } -} - -void UiHost::InvalidatePaint() { - if (const auto native_window = native_window_resolver_->Resolve()) - native_window->RequestRepaint(); -} - -void UiHost::InvalidateLayout() { - log::TagDebug(log_tag, u"A relayout is requested."); - if (!need_layout_) { - platform::native::IUiApplication::GetInstance()->InvokeLater( - [resolver = this->CreateResolver()] { - if (const auto host = resolver.Resolve()) { - host->Relayout(); - host->need_layout_ = false; - host->InvalidatePaint(); - } - }); - need_layout_ = true; - } -} - -void UiHost::Relayout() { - const auto native_window = native_window_resolver_->Resolve(); - const auto client_size = native_window - ? native_window->GetClientSize() - : Size{100, 100}; // a reasonable assumed size - root_render_object_->Measure( - render::MeasureRequirement{client_size, - render::MeasureSize::NotSpecified()}, - render::MeasureSize::NotSpecified()); - root_render_object_->Layout(Point{}); - after_layout_event_.Raise(AfterLayoutEventArgs{}); - log::TagDebug(log_tag, u"A relayout is finished."); -} - -bool UiHost::RequestFocusFor(Control* control) { - Expects(control != nullptr); // The control to request focus can't be null. - // You can set it as the window. - - if (focus_control_ == control) return true; - - const auto old_focus_control = focus_control_; - - focus_control_ = control; - - DispatchEvent(event_names::LoseFocus, old_focus_control, - &Control::LoseFocusEvent, nullptr, false); - - DispatchEvent(event_names::GainFocus, control, &Control::GainFocusEvent, - nullptr, false); - - return true; -} - -Control* UiHost::GetFocusControl() { return focus_control_; } - -bool UiHost::CaptureMouseFor(Control* control) { - const auto native_window = native_window_resolver_->Resolve(); - if (!native_window) return false; - - if (control == mouse_captured_control_) return true; - - if (control == nullptr) { - const auto old_capture_control = mouse_captured_control_; - mouse_captured_control_ = - nullptr; // update this in case this is used in event handlers - if (old_capture_control != mouse_hover_control_) { - DispatchMouseHoverControlChangeEvent( - old_capture_control, mouse_hover_control_, - native_window->GetMousePosition(), true, false); - } - UpdateCursor(); - return true; - } - - if (mouse_captured_control_) return false; - - mouse_captured_control_ = control; - DispatchMouseHoverControlChangeEvent( - mouse_hover_control_, mouse_captured_control_, - native_window->GetMousePosition(), false, true); - UpdateCursor(); - return true; -} - -Control* UiHost::GetMouseCaptureControl() { return mouse_captured_control_; } - -void UiHost::OnNativeDestroy(INativeWindow* window, std::nullptr_t) { - CRU_UNUSED(window) - native_window_destroyed_ = true; - if (!deleting_ && !retain_after_destroy_) delete window_control_; -} - -void UiHost::OnNativePaint(INativeWindow* window, std::nullptr_t) { - auto painter = window->BeginPaint(); - painter->Clear(colors::white); - root_render_object_->Draw(painter.get()); - painter->EndDraw(); -} - -void UiHost::OnNativeResize(INativeWindow* window, const Size& size) { - CRU_UNUSED(window) - CRU_UNUSED(size) - - InvalidateLayout(); -} - -void UiHost::OnNativeFocus(INativeWindow* window, - platform::native::FocusChangeType focus) { - CRU_UNUSED(window) - - focus == platform::native::FocusChangeType::Gain - ? DispatchEvent(event_names::GainFocus, focus_control_, - &Control::GainFocusEvent, nullptr, true) - : DispatchEvent(event_names::LoseFocus, focus_control_, - &Control::LoseFocusEvent, nullptr, true); -} - -void UiHost::OnNativeMouseEnterLeave( - INativeWindow* window, platform::native::MouseEnterLeaveType type) { - CRU_UNUSED(window) - - if (type == platform::native::MouseEnterLeaveType::Leave) { - DispatchEvent(event_names::MouseLeave, mouse_hover_control_, - &Control::MouseLeaveEvent, nullptr); - mouse_hover_control_ = nullptr; - } -} - -void UiHost::OnNativeMouseMove(INativeWindow* window, const Point& point) { - CRU_UNUSED(window) - - // Find the first control that hit test succeed. - const auto new_mouse_hover_control = HitTest(point); - const auto old_mouse_hover_control = mouse_hover_control_; - mouse_hover_control_ = new_mouse_hover_control; - - if (mouse_captured_control_) { - const auto n = FindLowestCommonAncestor(new_mouse_hover_control, - mouse_captured_control_); - const auto o = FindLowestCommonAncestor(old_mouse_hover_control, - mouse_captured_control_); - bool a = IsAncestor(o, n); - if (a) { - DispatchEvent(event_names::MouseLeave, o, &Control::MouseLeaveEvent, n); - } else { - DispatchEvent(event_names::MouseEnter, n, &Control::MouseEnterEvent, o, - point); - } - DispatchEvent(event_names::MouseMove, mouse_captured_control_, - &Control::MouseMoveEvent, nullptr, point); - UpdateCursor(); - return; - } - - DispatchMouseHoverControlChangeEvent( - old_mouse_hover_control, new_mouse_hover_control, point, false, false); - DispatchEvent(event_names::MouseMove, new_mouse_hover_control, - &Control::MouseMoveEvent, nullptr, point); - UpdateCursor(); -} - -void UiHost::OnNativeMouseDown( - INativeWindow* window, - const platform::native::NativeMouseButtonEventArgs& args) { - CRU_UNUSED(window) - - Control* control = - mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point); - DispatchEvent(event_names::MouseDown, control, &Control::MouseDownEvent, - nullptr, args.point, args.button, args.modifier); -} - -void UiHost::OnNativeMouseUp( - INativeWindow* window, - const platform::native::NativeMouseButtonEventArgs& args) { - CRU_UNUSED(window) - - Control* control = - mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point); - DispatchEvent(event_names::MouseUp, control, &Control::MouseUpEvent, nullptr, - args.point, args.button, args.modifier); -} - -void UiHost::OnNativeKeyDown(INativeWindow* window, - const platform::native::NativeKeyEventArgs& args) { - CRU_UNUSED(window) - - DispatchEvent(event_names::KeyDown, focus_control_, &Control::KeyDownEvent, - nullptr, args.key, args.modifier); -} - -void UiHost::OnNativeKeyUp(INativeWindow* window, - const platform::native::NativeKeyEventArgs& args) { - CRU_UNUSED(window) - - DispatchEvent(event_names::KeyUp, focus_control_, &Control::KeyUpEvent, - nullptr, args.key, args.modifier); -} - -void UiHost::DispatchMouseHoverControlChangeEvent(Control* old_control, - Control* new_control, - const Point& point, - bool no_leave, - bool no_enter) { - if (new_control != old_control) // if the mouse-hover-on control changed - { - const auto lowest_common_ancestor = - FindLowestCommonAncestor(old_control, new_control); - if (!no_leave && old_control != nullptr) - DispatchEvent(event_names::MouseLeave, old_control, - &Control::MouseLeaveEvent, - lowest_common_ancestor); // dispatch mouse leave event. - if (!no_enter && new_control != nullptr) { - DispatchEvent(event_names::MouseEnter, new_control, - &Control::MouseEnterEvent, lowest_common_ancestor, - point); // dispatch mouse enter event. - } - } -} - -void UiHost::UpdateCursor() { - if (const auto native_window = native_window_resolver_->Resolve()) { - const auto capture = GetMouseCaptureControl(); - native_window->SetCursor( - (capture ? capture : GetMouseHoverControl())->GetInheritedCursor()); - } -} - -Control* UiHost::HitTest(const Point& point) { - const auto render_object = root_render_object_->HitTest(point); - if (render_object) { - const auto control = render_object->GetAttachedControl(); - Ensures(control); - return control; - } - return window_control_; -} -} // namespace cru::ui diff --git a/src/ui/UiManager.cpp b/src/ui/UiManager.cpp index 4cd38efa..7981aa86 100644 --- a/src/ui/UiManager.cpp +++ b/src/ui/UiManager.cpp @@ -1,13 +1,22 @@ #include "cru/ui/UiManager.hpp" +#include <optional> #include "Helper.hpp" -#include "cru/platform/graph/Brush.hpp" -#include "cru/platform/graph/Factory.hpp" -#include "cru/platform/graph/Font.hpp" -#include "cru/platform/native/UiApplication.hpp" +#include "cru/platform/GraphBase.hpp" +#include "cru/platform/graphics/Brush.hpp" +#include "cru/platform/graphics/Factory.hpp" +#include "cru/platform/graphics/Font.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/ui/Base.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::graph; +using namespace cru::platform::graphics; +using namespace cru::ui::style; +using namespace cru::ui::helper; namespace { std::unique_ptr<ISolidColorBrush> CreateSolidColorBrush(IGraphFactory* factory, @@ -30,51 +39,80 @@ UiManager* UiManager::GetInstance() { UiManager::UiManager() { const auto factory = GetGraphFactory(); - theme_resource_.default_font = factory->CreateFont(u"ç‰çº¿", 24.0f); + theme_resource_.default_font_family = u"ç‰çº¿"; - const auto black_brush = std::shared_ptr<platform::graph::ISolidColorBrush>( - CreateSolidColorBrush(factory, colors::black)); + theme_resource_.default_font = + factory->CreateFont(theme_resource_.default_font_family, 24.0f); + + const auto black_brush = + std::shared_ptr<platform::graphics::ISolidColorBrush>( + 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_.button_style.AddStyleRule( + {NoCondition::Create(), + BorderStyler::Create(ApplyBorderStyleInfo{std::nullopt, Thickness(3), + CornerRadius(5), std::nullopt, + std::nullopt}), + u"DefaultButton"}); + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::None), + CompoundStyler::Create( + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x00bfff))}), + CursorStyler::Create(platform::gui::SystemCursorType::Arrow)), + u"DefaultButtonNormal"}); + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::Hover), + CompoundStyler::Create( + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x47d1ff))}), + CursorStyler::Create(platform::gui::SystemCursorType::Hand)), + u"DefaultButtonHover"}); + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::Press), + CompoundStyler::Create( + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff))}), + CursorStyler::Create(platform::gui::SystemCursorType::Hand)), + u"DefaultButtonPress"}); + theme_resource_.button_style.AddStyleRule( + {ClickStateCondition::Create(ClickState::PressInactive), + CompoundStyler::Create( + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x91e4ff))}), + CursorStyler::Create(platform::gui::SystemCursorType::Arrow)), + u"DefaultButtonPressInactive"}); - 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_style.AddStyleRule( + {NoCondition::Create(), + BorderStyler::Create( + ApplyBorderStyleInfo{std::nullopt, Thickness{1}, CornerRadius{5}}), + u"DefaultTextBox"}); + theme_resource_.text_box_style.AddStyleRule( + {HoverCondition::Create(false), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0xced4da))}), + u"DefaultTextBoxNormal"}); + theme_resource_.text_box_style.AddStyleRule( + {HoverCondition::Create(true), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0xced4da))}), + u"DefaultTextBoxHover"}); + theme_resource_.text_box_style.AddStyleRule( + {FocusCondition::Create(true), + BorderStyler::Create(ApplyBorderStyleInfo{ + CreateSolidColorBrush(factory, Color::FromHex(0x495057))}), + u"DefaultTextBoxFocus"}); - theme_resource_.text_box_border_style.focus_hover = - theme_resource_.text_box_border_style.focus; + theme_resource_.menu_item_style.AddStyleRule( + {NoCondition::Create(), + BorderStyler::Create( + ApplyBorderStyleInfo{std::nullopt, Thickness{0}, CornerRadius{0}}), + u"DefaultMenuItem"}); } UiManager::~UiManager() = default; diff --git a/src/ui/Window.cpp b/src/ui/Window.cpp deleted file mode 100644 index dca95ebb..00000000 --- a/src/ui/Window.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "cru/ui/Window.hpp" - -#include "cru/ui/render/WindowRenderObject.hpp" -#include "cru/ui/UiHost.hpp" - -namespace cru::ui { -Window* Window::CreateOverlapped() { - return new Window(tag_overlapped_constructor{}); -} - -Window::Window(tag_overlapped_constructor) { - managed_ui_host_ = std::make_unique<UiHost>(this); -} - -Window::~Window() { - // explicit destroy ui host first. - managed_ui_host_.reset(); -} - -std::u16string_view Window::GetControlType() const { return control_type; } - -render::RenderObject* Window::GetRenderObject() const { return render_object_; } - -void Window::OnChildChanged(Control* old_child, Control* new_child) { - if (old_child) render_object_->RemoveChild(0); - if (new_child) render_object_->AddChild(new_child->GetRenderObject(), 0); -} -} // namespace cru::ui diff --git a/src/ui/components/Component.cpp b/src/ui/components/Component.cpp new file mode 100644 index 00000000..5b62ffc9 --- /dev/null +++ b/src/ui/components/Component.cpp @@ -0,0 +1,5 @@ +#include "cru/ui/components/Component.hpp" + +namespace cru::ui::components { + +} diff --git a/src/ui/components/Menu.cpp b/src/ui/components/Menu.cpp new file mode 100644 index 00000000..d45bc44f --- /dev/null +++ b/src/ui/components/Menu.cpp @@ -0,0 +1,61 @@ +#include "cru/ui/components/Menu.hpp" +#include "cru/ui/UiManager.hpp" +#include "cru/ui/controls/Button.hpp" +#include "cru/ui/controls/FlexLayout.hpp" +#include "cru/ui/controls/TextBlock.hpp" +#include "cru/ui/style/StyleRuleSet.hpp" + +#include <string> + +namespace cru::ui::components { +MenuItem::MenuItem() { + container_ = controls::Button::Create(); + text_ = controls::TextBlock::Create(); + container_->SetChild(text_); + container_->GetStyleRuleSet()->SetParent( + &UiManager::GetInstance()->GetThemeResources()->menu_item_style); +} + +MenuItem::MenuItem(std::u16string text) : MenuItem() { + SetText(std::move(text)); +} + +MenuItem::~MenuItem() { + if (!container_->GetWindowHost()) { + delete container_; + delete text_; + } +} + +void MenuItem::SetText(std::u16string text) { text_->SetText(std::move(text)); } + +Menu::Menu() { container_ = controls::FlexLayout::Create(); } + +Menu::~Menu() { + if (!container_->GetWindowHost()) { + delete container_; + } + + for (auto item : items_) { + delete item; + } +} + +void Menu::AddItem(Component* item, gsl::index index) { + Expects(index >= 0 && index <= GetItemCount()); + + items_.insert(items_.cbegin() + index, item); + container_->AddChild(item->GetRootControl(), index); +} + +Component* Menu::RemoveItem(gsl::index index) { + Expects(index >= 0 && index < GetItemCount()); + + Component* item = items_[index]; + + items_.erase(items_.cbegin() + index); + container_->RemoveChild(index); + + return item; +} +} // namespace cru::ui::components 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 diff --git a/src/ui/UiEvent.cpp b/src/ui/events/UiEvent.cpp index 74dd54dc..4c75f690 100644 --- a/src/ui/UiEvent.cpp +++ b/src/ui/events/UiEvent.cpp @@ -1,8 +1,12 @@ -#include "cru/ui/UiEvent.hpp" +#include "cru/ui/events/UiEvent.hpp" #include "cru/ui/render/RenderObject.hpp" namespace cru::ui::event { +Point MouseEventArgs::GetPoint(render::RenderObject* render_object) const { + return GetPoint() - render_object->GetTotalOffset(); +} + Point MouseEventArgs::GetPointToContent( render::RenderObject* render_object) const { return render_object->FromRootToContent(GetPoint()); diff --git a/src/ui/helper/BorderStyle.cpp b/src/ui/helper/BorderStyle.cpp new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/ui/helper/BorderStyle.cpp diff --git a/src/ui/ClickDetector.cpp b/src/ui/helper/ClickDetector.cpp index 09f208cd..309685d3 100644 --- a/src/ui/ClickDetector.cpp +++ b/src/ui/helper/ClickDetector.cpp @@ -1,11 +1,12 @@ -#include "cru/ui/ClickDetector.hpp" +#include "cru/ui/helper/ClickDetector.hpp" #include "cru/common/Logger.hpp" +#include "cru/ui/DebugFlags.hpp" #include <optional> -namespace cru::ui { -ClickDetector::ClickDetector(Control* control) { +namespace cru::ui::helper { +ClickDetector::ClickDetector(controls::Control* control) { Expects(control); control_ = control; @@ -44,8 +45,10 @@ ClickDetector::ClickDetector(Control* control) { if (this->enable_ && (button & this->trigger_button_) && this->state_ == ClickState::Hover) { if (!this->control_->CaptureMouse()) { - log::TagDebug(log_tag, - u"Failed to capture mouse when begin click."); + if constexpr (debug_flags::click_detector) { + log::TagDebug(log_tag, + u"Failed to capture mouse when begin click."); + } return; } this->down_point_ = args.GetPoint(); @@ -106,26 +109,26 @@ void ClickDetector::SetTriggerButton(MouseButton trigger_button) { } void ClickDetector::SetState(ClickState state) { -#ifdef CRU_DEBUG - auto to_string = [](ClickState state) -> std::u16string_view { - switch (state) { - case ClickState::None: - return u"None"; - case ClickState::Hover: - return u"Hover"; - case ClickState::Press: - return u"Press"; - case ClickState::PressInactive: - return u"PressInvactive"; - default: - UnreachableCode(); - } - }; - log::TagDebug(log_tag, u"Click state changed, new state: {}.", - to_string(state)); -#endif + if constexpr (debug_flags::click_detector) { + auto to_string = [](ClickState state) -> std::u16string_view { + switch (state) { + case ClickState::None: + return u"None"; + case ClickState::Hover: + return u"Hover"; + case ClickState::Press: + return u"Press"; + case ClickState::PressInactive: + return u"PressInvactive"; + default: + UnreachableCode(); + } + }; + log::TagDebug(log_tag, u"Click state changed, new state: {}.", + to_string(state)); + } state_ = state; state_change_event_.Raise(state); } -} // namespace cru::ui +} // namespace cru::ui::helper diff --git a/src/ui/helper/ShortcutHub.cpp b/src/ui/helper/ShortcutHub.cpp new file mode 100644 index 00000000..f35ad0ef --- /dev/null +++ b/src/ui/helper/ShortcutHub.cpp @@ -0,0 +1,131 @@ +#include "cru/ui/helper/ShortcutHub.hpp" + +#include "cru/common/Logger.hpp" +#include "cru/ui/DebugFlags.hpp" +#include "cru/ui/controls/Control.hpp" + +#include <algorithm> +#include <functional> +#include <iterator> +#include <optional> + +namespace cru::ui::helper { +int ShortcutHub::RegisterShortcut(Shortcut shortcut) { + const int id = current_id_++; + map_[shortcut.key_bind].push_back({id, std::move(shortcut.name), + shortcut.key_bind, + std::move(shortcut.handler)}); + return id; +} + +void ShortcutHub::UnregisterShortcut(int id) { + if (id <= 0) return; + for (auto& pair : map_) { + auto& list = pair.second; + auto result = + std::find_if(list.cbegin(), list.cend(), + [id](const ShortcutInfo& info) { return info.id == id; }); + if (result != list.cend()) { + list.erase(result); + } + } +} + +std::vector<ShortcutInfo> ShortcutHub::GetAllShortcuts() const { + std::vector<ShortcutInfo> result; + + for (const auto& pair : map_) { + std::copy(pair.second.cbegin(), pair.second.cend(), + std::back_inserter(result)); + } + + return result; +} + +std::optional<ShortcutInfo> ShortcutHub::GetShortcut(int id) const { + for (auto& pair : map_) { + auto& list = pair.second; + auto result = + std::find_if(list.cbegin(), list.cend(), + [id](const ShortcutInfo& info) { return info.id == id; }); + if (result != list.cend()) { + return *result; + } + } + return std::nullopt; +} + +const std::vector<ShortcutInfo>& ShortcutHub::GetShortcutByKeyBind( + const ShortcutKeyBind& key_bind) const { + auto result = map_.find(key_bind); + if (result != map_.cend()) return result->second; + return empty_list_; +} + +void ShortcutHub::Install(controls::Control* control) { + if (!event_guard_.IsEmpty()) { + log::Error(u"Shortcut hub is already installed. Failed to install."); + return; + } + + event_guard_ += control->KeyDownEvent()->Bubble()->AddHandler( + std::bind(&ShortcutHub::OnKeyDown, this, std::placeholders::_1)); +} + +void ShortcutHub::Uninstall() { + if (event_guard_.IsEmpty()) { + log::Warn(u"Shortcut hub is not installed. Failed to uninstall."); + return; + } + + event_guard_.Clear(); +} + +void ShortcutHub::OnKeyDown(event::KeyEventArgs& event) { + ShortcutKeyBind key_bind(event.GetKeyCode(), event.GetKeyModifier()); + const auto& shortcut_list = this->GetShortcutByKeyBind(key_bind); + + bool handled = false; + + if constexpr (debug_flags::shortcut) { + if (shortcut_list.empty()) { + log::Debug(u"No shortcut for key bind {}.", key_bind.ToString()); + } + log::Debug(u"Begin to handle shortcut for key bind {}.", + key_bind.ToString()); + } + + for (const auto& shortcut : shortcut_list) { + auto is_handled = shortcut.handler(); + if (is_handled) { + if constexpr (debug_flags::shortcut) { + log::Debug(u"Handle {} handled it.", shortcut.name); + } + + handled = true; + event.SetHandled(); + + break; + } else { + if constexpr (debug_flags::shortcut) { + log::Debug(u"Handle {} didn't handle it.", shortcut.name); + } + } + } + + if constexpr (debug_flags::shortcut) { + if (!shortcut_list.empty()) { + log::Debug(u"End handling shortcut for key bind {}.", + key_bind.ToString()); + } + } + + if (!handled) { + if constexpr (debug_flags::shortcut) { + log::Debug(u"Raise fallback event for unhandled shortcut of key bind {}.", + key_bind.ToString()); + } + fallback_event_.Raise(event); + } +} +} // namespace cru::ui::helper diff --git a/src/ui/host/LayoutPaintCycler.cpp b/src/ui/host/LayoutPaintCycler.cpp new file mode 100644 index 00000000..fd581e00 --- /dev/null +++ b/src/ui/host/LayoutPaintCycler.cpp @@ -0,0 +1,35 @@ +#include "cru/ui/host/LayoutPaintCycler.hpp" +#include <chrono> + +#include "../Helper.hpp" +#include "cru/ui/Base.hpp" +#include "cru/ui/host/WindowHost.hpp" + +namespace cru::ui::host { +LayoutPaintCycler::LayoutPaintCycler(WindowHost* host) : host_(host) { + timer_canceler_ = GetUiApplication()->SetInterval( + std::chrono::duration_cast<std::chrono::milliseconds>( + this->cycle_threshold_), + [this] { OnCycle(); }); +} + +LayoutPaintCycler::~LayoutPaintCycler() = default; + +void LayoutPaintCycler::InvalidateLayout() { layout_dirty_ = true; } + +void LayoutPaintCycler::InvalidatePaint() { paint_dirty_ = true; } + +void LayoutPaintCycler::OnCycle() { + last_cycle_time_ = std::chrono::steady_clock::now(); + if (layout_dirty_) { + host_->Relayout(); + host_->Repaint(); + } else { + if (paint_dirty_) { + host_->Repaint(); + } + } + layout_dirty_ = false; + paint_dirty_ = false; +} +} // namespace cru::ui::host diff --git a/src/ui/RoutedEventDispatch.hpp b/src/ui/host/RoutedEventDispatch.hpp index 9337e9ec..52507fc7 100644 --- a/src/ui/RoutedEventDispatch.hpp +++ b/src/ui/host/RoutedEventDispatch.hpp @@ -1,9 +1,9 @@ #pragma once -#include "cru/ui/Control.hpp" - #include "cru/common/Logger.hpp" +#include "cru/ui/DebugFlags.hpp" +#include "cru/ui/controls/Control.hpp" -#include <list> +#include <vector> namespace cru::ui { // Dispatch the event. @@ -20,33 +20,23 @@ namespace cru::ui { // "original_sender", which is unchanged. And "args" will be perfectly forwarded // as the rest arguments. template <typename EventArgs, typename... Args> -void DispatchEvent(const std::u16string_view& event_name, - Control* const original_sender, - event::RoutedEvent<EventArgs>* (Control::*event_ptr)(), - Control* const last_receiver, Args&&... args) { -#ifndef CRU_DEBUG +void DispatchEvent( + const std::u16string_view& event_name, + controls::Control* const original_sender, + event::RoutedEvent<EventArgs>* (controls::Control::*event_ptr)(), + controls::Control* const last_receiver, Args&&... args) { CRU_UNUSED(event_name) -#endif - -#ifdef CRU_DEBUG - bool do_log = true; - if (event_name == u"MouseMove") do_log = false; -#endif if (original_sender == last_receiver) { - /* - #ifdef CRU_DEBUG - if (do_log) - log::Debug( - "Routed event {} no need to dispatch (original_sender == " - "last_receiver). Original sender is {}.", - event_name, original_sender->GetControlType()); - #endif - */ + if constexpr (debug_flags::routed_event) + log::Debug( + "Routed event {} no need to dispatch (original_sender == " + "last_receiver). Original sender is {}.", + event_name, original_sender->GetControlType()); return; } - std::list<Control*> receive_list; + std::vector<controls::Control*> receive_list; auto parent = original_sender; while (parent != last_receiver) { @@ -54,8 +44,7 @@ void DispatchEvent(const std::u16string_view& event_name, parent = parent->GetParent(); } -#ifdef CRU_DEBUG - if (do_log) { + if constexpr (debug_flags::routed_event) { std::u16string log = u"Dispatch routed event "; log += event_name; log += u". Path (parent first): "; @@ -68,31 +57,24 @@ void DispatchEvent(const std::u16string_view& event_name, log += (*i)->GetControlType(); log::Debug(log); } -#endif auto handled = false; -#ifdef CRU_DEBUG int count = 0; -#endif // tunnel for (auto i = receive_list.crbegin(); i != receive_list.crend(); ++i) { -#ifdef CRU_DEBUG count++; -#endif EventArgs event_args(*i, original_sender, std::forward<Args>(args)...); static_cast<Event<EventArgs&>*>(((*i)->*event_ptr)()->Tunnel()) ->Raise(event_args); if (event_args.IsHandled()) { handled = true; -#ifdef CRU_DEBUG - if (do_log) + if constexpr (debug_flags::routed_event) log::Debug( u"Routed event is short-circuit in TUNNEL at {}-st control (count " u"from parent).", count); -#endif break; } } @@ -100,20 +82,16 @@ void DispatchEvent(const std::u16string_view& event_name, // bubble if (!handled) { for (auto i : receive_list) { -#ifdef CRU_DEBUG count--; -#endif EventArgs event_args(i, original_sender, std::forward<Args>(args)...); static_cast<Event<EventArgs&>*>((i->*event_ptr)()->Bubble()) ->Raise(event_args); if (event_args.IsHandled()) { -#ifdef CRU_DEBUG - if (do_log) + if constexpr (debug_flags::routed_event) log::Debug( u"Routed event is short-circuit in BUBBLE at {}-st control " u"(count from parent).", count); -#endif break; } } @@ -126,8 +104,7 @@ void DispatchEvent(const std::u16string_view& event_name, ->Raise(event_args); } -#ifdef CRU_DEBUG - if (do_log) log::Debug(u"Routed event dispatch finished."); -#endif + if constexpr (debug_flags::routed_event) + log::Debug(u"Routed event dispatch finished."); } } // namespace cru::ui diff --git a/src/ui/host/WindowHost.cpp b/src/ui/host/WindowHost.cpp new file mode 100644 index 00000000..5e107733 --- /dev/null +++ b/src/ui/host/WindowHost.cpp @@ -0,0 +1,440 @@ +#include "cru/ui/host/WindowHost.hpp" + +#include "RoutedEventDispatch.hpp" +#include "cru/common/Base.hpp" +#include "cru/common/Logger.hpp" +#include "cru/platform/graphics/Painter.hpp" +#include "cru/platform/gui/InputMethod.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/platform/gui/Window.hpp" +#include "cru/ui/DebugFlags.hpp" +#include "cru/ui/controls/Window.hpp" +#include "cru/ui/host/LayoutPaintCycler.hpp" +#include "cru/ui/render/MeasureRequirement.hpp" +#include "cru/ui/render/RenderObject.hpp" + +#include <cstddef> +#include <memory> + +namespace cru::ui::host { +using platform::gui::INativeWindow; +using platform::gui::IUiApplication; + +namespace event_names { +#ifdef CRU_DEBUG +// clang-format off +#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = CRU_MAKE_UNICODE_LITERAL(name); +// clang-format on +#else +#define CRU_DEFINE_EVENT_NAME(name) constexpr const char16_t* name = u""; +#endif + +CRU_DEFINE_EVENT_NAME(LoseFocus) +CRU_DEFINE_EVENT_NAME(GainFocus) +CRU_DEFINE_EVENT_NAME(MouseEnter) +CRU_DEFINE_EVENT_NAME(MouseLeave) +CRU_DEFINE_EVENT_NAME(MouseMove) +CRU_DEFINE_EVENT_NAME(MouseDown) +CRU_DEFINE_EVENT_NAME(MouseUp) +CRU_DEFINE_EVENT_NAME(KeyDown) +CRU_DEFINE_EVENT_NAME(KeyUp) + +#undef CRU_DEFINE_EVENT_NAME +} // namespace event_names + +namespace { +bool IsAncestor(controls::Control* control, controls::Control* ancestor) { + while (control != nullptr) { + if (control == ancestor) return true; + control = control->GetParent(); + } + return false; +} + +// Ancestor at last. +std::vector<controls::Control*> GetAncestorList(controls::Control* control) { + std::vector<controls::Control*> l; + while (control != nullptr) { + l.push_back(control); + control = control->GetParent(); + } + return l; +} + +controls::Control* FindLowestCommonAncestor(controls::Control* left, + controls::Control* right) { + if (left == nullptr || right == nullptr) return nullptr; + + auto&& left_list = GetAncestorList(left); + auto&& right_list = GetAncestorList(right); + + // the root is different + if (left_list.back() != right_list.back()) return nullptr; + + // find the last same control or the last control (one is ancestor of the + // other) + auto left_iter = left_list.crbegin(); + auto right_iter = right_list.crbegin(); + + while (true) { + if (left_iter == left_list.crend()) { + return left_list.front(); + } + if (right_iter == right_list.crend()) { + return right_list.front(); + } + if (*left_iter != *right_iter) { + return *(--left_iter); + } + ++left_iter; + ++right_iter; + } +} +} // namespace + +namespace { +template <typename T> +inline void BindNativeEvent( + WindowHost* host, INativeWindow* native_window, IEvent<T>* event, + void (WindowHost::*handler)(INativeWindow*, typename IEvent<T>::EventArgs), + std::vector<EventRevokerGuard>& guard_pool) { + guard_pool.push_back(EventRevokerGuard(event->AddHandler( + std::bind(handler, host, native_window, std::placeholders::_1)))); +} +} // namespace + +WindowHost::WindowHost(controls::Control* root_control) + : root_control_(root_control), focus_control_(root_control) { + root_control_->TraverseDescendants([this](controls::Control* control) { + control->window_host_ = this; + control->OnAttachToHost(this); + }); + + root_render_object_ = root_control->GetRenderObject(); + root_render_object_->SetWindowHostRecursive(this); + + this->layout_paint_cycler_ = std::make_unique<LayoutPaintCycler>(this); +} + +WindowHost::~WindowHost() {} + +gsl::not_null<platform::gui::INativeWindow*> WindowHost::CreateNativeWindow( + CreateWindowParams create_window_params) { + if (native_window_ != nullptr) return native_window_; + + const auto ui_application = IUiApplication::GetInstance(); + + auto native_window = ui_application->CreateWindow(create_window_params.parent, + create_window_params.flag); + + native_window_ = native_window; + + BindNativeEvent(this, native_window, native_window->DestroyEvent(), + &WindowHost::OnNativeDestroy, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->PaintEvent(), + &WindowHost::OnNativePaint, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->ResizeEvent(), + &WindowHost::OnNativeResize, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->FocusEvent(), + &WindowHost::OnNativeFocus, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->MouseEnterLeaveEvent(), + &WindowHost::OnNativeMouseEnterLeave, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->MouseMoveEvent(), + &WindowHost::OnNativeMouseMove, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->MouseDownEvent(), + &WindowHost::OnNativeMouseDown, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->MouseUpEvent(), + &WindowHost::OnNativeMouseUp, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->KeyDownEvent(), + &WindowHost::OnNativeKeyDown, event_revoker_guards_); + BindNativeEvent(this, native_window, native_window->KeyUpEvent(), + &WindowHost::OnNativeKeyUp, event_revoker_guards_); + + if (saved_rect_) { + native_window->SetWindowRect(saved_rect_.value()); + } + + native_window_change_event_.Raise(native_window); + + return native_window_; +} + +void WindowHost::InvalidatePaint() { layout_paint_cycler_->InvalidatePaint(); } + +void WindowHost::InvalidateLayout() { + layout_paint_cycler_->InvalidateLayout(); +} + +bool WindowHost::IsLayoutPreferToFillWindow() const { + return layout_prefer_to_fill_window_; +} + +void WindowHost::SetLayoutPreferToFillWindow(bool value) { + if (value == layout_prefer_to_fill_window_) return; + layout_prefer_to_fill_window_ = value; + InvalidateLayout(); +} + +void WindowHost::Relayout() { + const auto available_size = + native_window_ ? native_window_->GetClientSize() + : Size{100, 100}; // a reasonable assumed size + Relayout(available_size); +} + +void WindowHost::Relayout(const Size& available_size) { + root_render_object_->Measure( + render::MeasureRequirement{available_size, + IsLayoutPreferToFillWindow() + ? render::MeasureSize(available_size) + : render::MeasureSize::NotSpecified()}, + render::MeasureSize::NotSpecified()); + root_render_object_->Layout(Point{}); + for (auto& action : after_layout_stable_action_) action(); + after_layout_event_.Raise(AfterLayoutEventArgs{}); + root_render_object_->TraverseDescendants( + [](render::RenderObject* render_object) { + render_object->OnAfterLayout(); + }); + after_layout_stable_action_.clear(); + if constexpr (debug_flags::layout) + log::TagDebug(log_tag, u"A relayout is finished."); +} + +void WindowHost::Repaint() { + auto painter = native_window_->BeginPaint(); + painter->Clear(colors::white); + root_render_object_->Draw(painter.get()); + painter->EndDraw(); +} + +controls::Control* WindowHost::GetFocusControl() { return focus_control_; } + +void WindowHost::SetFocusControl(controls::Control* control) { + if (focus_control_ == control) return; + if (control == nullptr) control = root_control_; + + const auto old_focus_control = focus_control_; + + focus_control_ = control; + + DispatchEvent(event_names::LoseFocus, old_focus_control, + &controls::Control::LoseFocusEvent, nullptr, false); + + DispatchEvent(event_names::GainFocus, control, + &controls::Control::GainFocusEvent, nullptr, false); +} + +bool WindowHost::CaptureMouseFor(controls::Control* control) { + if (!native_window_) return false; + if (!native_window_->CaptureMouse()) return false; + + if (control == mouse_captured_control_) return true; + + if (control == nullptr) { + native_window_->ReleaseMouse(); + const auto old_capture_control = mouse_captured_control_; + mouse_captured_control_ = + nullptr; // update this in case this is used in event handlers + if (old_capture_control != mouse_hover_control_) { + DispatchMouseHoverControlChangeEvent( + old_capture_control, mouse_hover_control_, + native_window_->GetMousePosition(), true, false); + } + UpdateCursor(); + return true; + } + + if (mouse_captured_control_) return false; + + mouse_captured_control_ = control; + DispatchMouseHoverControlChangeEvent( + mouse_hover_control_, mouse_captured_control_, + native_window_->GetMousePosition(), false, true); + UpdateCursor(); + return true; +} + +controls::Control* WindowHost::GetMouseCaptureControl() { + return mouse_captured_control_; +} + +void WindowHost::RunAfterLayoutStable(std::function<void()> action) { + if (layout_paint_cycler_->IsLayoutDirty()) { + after_layout_stable_action_.push_back(std::move(action)); + } else { + action(); + } +} + +Rect WindowHost::GetWindowRect() { + if (native_window_) return native_window_->GetWindowRect(); + return saved_rect_.value_or(Rect{}); +} + +void WindowHost::SetSavedWindowRect(std::optional<Rect> rect) { + saved_rect_ = std::move(rect); +} + +void WindowHost::SetWindowRect(const Rect& rect) { + SetSavedWindowRect(rect); + if (native_window_) native_window_->SetWindowRect(rect); +} + +void WindowHost::OnNativeDestroy(INativeWindow* window, std::nullptr_t) { + CRU_UNUSED(window) + + saved_rect_ = this->native_window_->GetWindowRect(); + + this->native_window_ = nullptr; + event_revoker_guards_.clear(); + + native_window_change_event_.Raise(nullptr); +} + +void WindowHost::OnNativePaint(INativeWindow* window, std::nullptr_t) { + CRU_UNUSED(window) + layout_paint_cycler_->InvalidatePaint(); +} + +void WindowHost::OnNativeResize(INativeWindow* window, const Size& size) { + CRU_UNUSED(window) + CRU_UNUSED(size) + + InvalidateLayout(); +} + +void WindowHost::OnNativeFocus(INativeWindow* window, + platform::gui::FocusChangeType focus) { + CRU_UNUSED(window) + + focus == platform::gui::FocusChangeType::Gain + ? DispatchEvent(event_names::GainFocus, focus_control_, + &controls::Control::GainFocusEvent, nullptr, true) + : DispatchEvent(event_names::LoseFocus, focus_control_, + &controls::Control::LoseFocusEvent, nullptr, true); +} + +void WindowHost::OnNativeMouseEnterLeave( + INativeWindow* window, platform::gui::MouseEnterLeaveType type) { + CRU_UNUSED(window) + + if (type == platform::gui::MouseEnterLeaveType::Leave) { + DispatchEvent(event_names::MouseLeave, mouse_hover_control_, + &controls::Control::MouseLeaveEvent, nullptr); + mouse_hover_control_ = nullptr; + } +} + +void WindowHost::OnNativeMouseMove(INativeWindow* window, const Point& point) { + CRU_UNUSED(window) + + // Find the first control that hit test succeed. + const auto new_mouse_hover_control = HitTest(point); + const auto old_mouse_hover_control = mouse_hover_control_; + mouse_hover_control_ = new_mouse_hover_control; + + if (mouse_captured_control_) { + const auto n = FindLowestCommonAncestor(new_mouse_hover_control, + mouse_captured_control_); + const auto o = FindLowestCommonAncestor(old_mouse_hover_control, + mouse_captured_control_); + bool a = IsAncestor(o, n); + if (a) { + DispatchEvent(event_names::MouseLeave, o, + &controls::Control::MouseLeaveEvent, n); + } else { + DispatchEvent(event_names::MouseEnter, n, + &controls::Control::MouseEnterEvent, o, point); + } + DispatchEvent(event_names::MouseMove, mouse_captured_control_, + &controls::Control::MouseMoveEvent, nullptr, point); + UpdateCursor(); + return; + } + + DispatchMouseHoverControlChangeEvent( + old_mouse_hover_control, new_mouse_hover_control, point, false, false); + DispatchEvent(event_names::MouseMove, new_mouse_hover_control, + &controls::Control::MouseMoveEvent, nullptr, point); + UpdateCursor(); +} + +void WindowHost::OnNativeMouseDown( + INativeWindow* window, + const platform::gui::NativeMouseButtonEventArgs& args) { + CRU_UNUSED(window) + + controls::Control* control = + mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point); + DispatchEvent(event_names::MouseDown, control, + &controls::Control::MouseDownEvent, nullptr, args.point, + args.button, args.modifier); +} + +void WindowHost::OnNativeMouseUp( + INativeWindow* window, + const platform::gui::NativeMouseButtonEventArgs& args) { + CRU_UNUSED(window) + + controls::Control* control = + mouse_captured_control_ ? mouse_captured_control_ : HitTest(args.point); + DispatchEvent(event_names::MouseUp, control, &controls::Control::MouseUpEvent, + nullptr, args.point, args.button, args.modifier); +} + +void WindowHost::OnNativeKeyDown( + INativeWindow* window, const platform::gui::NativeKeyEventArgs& args) { + CRU_UNUSED(window) + + DispatchEvent(event_names::KeyDown, focus_control_, + &controls::Control::KeyDownEvent, nullptr, args.key, + args.modifier); +} + +void WindowHost::OnNativeKeyUp(INativeWindow* window, + const platform::gui::NativeKeyEventArgs& args) { + CRU_UNUSED(window) + + DispatchEvent(event_names::KeyUp, focus_control_, + &controls::Control::KeyUpEvent, nullptr, args.key, + args.modifier); +} + +void WindowHost::DispatchMouseHoverControlChangeEvent( + controls::Control* old_control, controls::Control* new_control, + const Point& point, bool no_leave, bool no_enter) { + if (new_control != old_control) // if the mouse-hover-on control changed + { + const auto lowest_common_ancestor = + FindLowestCommonAncestor(old_control, new_control); + if (!no_leave && old_control != nullptr) + DispatchEvent(event_names::MouseLeave, old_control, + &controls::Control::MouseLeaveEvent, + lowest_common_ancestor); // dispatch mouse leave event. + if (!no_enter && new_control != nullptr) { + DispatchEvent(event_names::MouseEnter, new_control, + &controls::Control::MouseEnterEvent, lowest_common_ancestor, + point); // dispatch mouse enter event. + } + } +} + +void WindowHost::UpdateCursor() { + if (native_window_) { + const auto capture = GetMouseCaptureControl(); + native_window_->SetCursor( + (capture ? capture : GetMouseHoverControl())->GetInheritedCursor()); + } +} + +controls::Control* WindowHost::HitTest(const Point& point) { + const auto render_object = root_render_object_->HitTest(point); + if (render_object) { + const auto control = render_object->GetAttachedControl(); + Ensures(control); + return control; + } + return root_control_; +} +} // namespace cru::ui::host diff --git a/src/ui/render/BorderRenderObject.cpp b/src/ui/render/BorderRenderObject.cpp index b7e1e709..e2c40f0c 100644 --- a/src/ui/render/BorderRenderObject.cpp +++ b/src/ui/render/BorderRenderObject.cpp @@ -2,9 +2,11 @@ #include "../Helper.hpp" #include "cru/common/Logger.hpp" -#include "cru/platform/graph/Factory.hpp" -#include "cru/platform/graph/Geometry.hpp" -#include "cru/platform/graph/util/Painter.hpp" +#include "cru/platform/graphics/Factory.hpp" +#include "cru/platform/graphics/Geometry.hpp" +#include "cru/platform/graphics/util/Painter.hpp" +#include "cru/ui/style/ApplyBorderStyleInfo.hpp" +#include "gsl/gsl_assert" #include <algorithm> @@ -16,12 +18,13 @@ 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; +void BorderRenderObject::ApplyBorderStyle( + const style::ApplyBorderStyleInfo& style) { + if (style.border_brush) border_brush_ = *style.border_brush; + if (style.border_thickness) border_thickness_ = *style.border_thickness; + if (style.border_radius) border_radius_ = *style.border_radius; + if (style.foreground_brush) foreground_brush_ = *style.foreground_brush; + if (style.background_brush) background_brush_ = *style.background_brush; InvalidateLayout(); } @@ -51,7 +54,7 @@ RenderObject* BorderRenderObject::HitTest(const Point& point) { } } -void BorderRenderObject::OnDrawCore(platform::graph::IPainter* painter) { +void BorderRenderObject::OnDrawCore(platform::graphics::IPainter* painter) { if (background_brush_ != nullptr) painter->FillGeometry(border_inner_geometry_.get(), background_brush_.get()); @@ -109,9 +112,10 @@ Size BorderRenderObject::OnMeasureCore(const MeasureRequirement& requirement, if (!requirement.max.height.IsNotSpecified()) { const auto max_height = requirement.max.height.GetLengthOrMax(); if (coerced_space_size.height > max_height) { - log::TagWarn(log_tag, - u"(Measure) Vertical length of padding, border and margin is " - u"bigger than required max length."); + log::TagWarn( + log_tag, + u"(Measure) Vertical length of padding, border and margin is " + u"bigger than required max length."); coerced_space_size.height = max_height; } content_requirement.max.height = max_height - coerced_space_size.height; @@ -235,7 +239,7 @@ void BorderRenderObject::RecreateGeometry() { r.left_bottom - Point{t.left, t.bottom}, r.right_bottom - Point{t.right, t.bottom}); - auto f = [](platform::graph::IGeometryBuilder* builder, const Rect& rect, + auto f = [](platform::graphics::IGeometryBuilder* builder, const Rect& rect, const CornerRadius& corner) { builder->BeginFigure(Point(rect.left + corner.left_top.x, rect.top)); builder->LineTo(Point(rect.GetRight() - corner.right_top.x, rect.top)); @@ -263,7 +267,7 @@ void BorderRenderObject::RecreateGeometry() { size.width - margin.GetHorizontalTotal(), size.height - margin.GetVerticalTotal()}; const auto graph_factory = GetGraphFactory(); - std::unique_ptr<platform::graph::IGeometryBuilder> builder{ + std::unique_ptr<platform::graphics::IGeometryBuilder> builder{ graph_factory->CreateGeometryBuilder()}; f(builder.get(), outer_rect, outer_radius); border_outer_geometry_ = builder->Build(); diff --git a/src/ui/render/CanvasRenderObject.cpp b/src/ui/render/CanvasRenderObject.cpp index 967fdcec..bf1155e1 100644 --- a/src/ui/render/CanvasRenderObject.cpp +++ b/src/ui/render/CanvasRenderObject.cpp @@ -10,7 +10,7 @@ RenderObject* CanvasRenderObject::HitTest(const Point& point) { return padding_rect.IsPointInside(point) ? this : nullptr; } -void CanvasRenderObject::OnDrawContent(platform::graph::IPainter* painter) { +void CanvasRenderObject::OnDrawContent(platform::graphics::IPainter* painter) { const auto rect = GetContentRect(); CanvasPaintEventArgs args{painter, rect.GetSize()}; paint_event_.Raise(args); diff --git a/src/ui/render/FlexLayoutRenderObject.cpp b/src/ui/render/FlexLayoutRenderObject.cpp index ade230b5..b1ef69ee 100644 --- a/src/ui/render/FlexLayoutRenderObject.cpp +++ b/src/ui/render/FlexLayoutRenderObject.cpp @@ -1,7 +1,7 @@ #include "cru/ui/render/FlexLayoutRenderObject.hpp" #include "cru/common/Logger.hpp" -#include "cru/platform/graph/util/Painter.hpp" +#include "cru/platform/graphics/util/Painter.hpp" #include "cru/ui/render/LayoutHelper.hpp" #include <algorithm> @@ -10,6 +10,10 @@ namespace cru::ui::render { +std::u16string_view FlexLayoutRenderObject::GetName() const { + return u"FlexLayoutRenderObject"; +} + struct tag_horizontal_t {}; struct tag_vertical_t {}; @@ -64,7 +68,7 @@ template <typename TSize> constexpr TSize CreateTSize(decltype(std::declval<TSize>().width) main, decltype(std::declval<TSize>().height) cross, tag_vertical_t) { - return TSize{main, cross}; + return TSize{cross, main}; } enum class FlexLayoutAdjustType { None, Expand, Shrink }; @@ -387,10 +391,11 @@ void FlexLayoutRenderObject::OnLayoutContent(const Rect& content_rect) { const auto cross_align = GetChildLayoutDataList()[i].cross_alignment.value_or( GetItemCrossAlign()); - child->Layout( - Point{content_rect.top + current_main_offset, - CalculateAnchorByAlignment(cross_align, content_rect.left, - content_rect.width, size.width)}); + child->Layout(Point{ + CalculateAnchorByAlignment(cross_align, content_rect.left, + content_rect.width, size.width), + content_rect.top + current_main_offset, + }); current_main_offset += size.height; } } else { @@ -402,9 +407,9 @@ void FlexLayoutRenderObject::OnLayoutContent(const Rect& content_rect) { GetChildLayoutDataList()[i].cross_alignment.value_or( GetItemCrossAlign()); child->Layout( - Point{content_rect.GetBottom() - current_main_offset, - CalculateAnchorByAlignment(cross_align, content_rect.left, - content_rect.width, size.width)}); + Point{CalculateAnchorByAlignment(cross_align, content_rect.left, + content_rect.width, size.width), + content_rect.GetBottom() - current_main_offset}); current_main_offset += size.height; } } diff --git a/src/ui/render/RenderObject.cpp b/src/ui/render/RenderObject.cpp index 30433868..7cf750cd 100644 --- a/src/ui/render/RenderObject.cpp +++ b/src/ui/render/RenderObject.cpp @@ -1,12 +1,21 @@ #include "cru/ui/render/RenderObject.hpp" #include "cru/common/Logger.hpp" -#include "cru/platform/graph/util/Painter.hpp" -#include "cru/ui/UiHost.hpp" +#include "cru/platform/graphics/util/Painter.hpp" +#include "cru/ui/DebugFlags.hpp" +#include "cru/ui/host/WindowHost.hpp" #include <algorithm> +#include <string> +#include <string_view> +#include <vector> namespace cru::ui::render { +void RenderObject::SetAttachedControl(controls::Control* new_control) { + control_ = new_control; + OnAttachedControlChanged(new_control); +} + void RenderObject::AddChild(RenderObject* render_object, const Index position) { Expects(child_mode_ != ChildMode::None); Expects(!(child_mode_ == ChildMode::Single && children_.size() > 0)); @@ -20,7 +29,7 @@ void RenderObject::AddChild(RenderObject* render_object, const Index position) { children_.insert(children_.cbegin() + position, render_object); render_object->SetParent(this); - render_object->SetRenderHostRecursive(GetUiHost()); + render_object->SetWindowHostRecursive(GetWindowHost()); OnAddChild(render_object, position); } @@ -33,10 +42,25 @@ void RenderObject::RemoveChild(const Index position) { const auto removed_child = *i; children_.erase(i); removed_child->SetParent(nullptr); - removed_child->SetRenderHostRecursive(nullptr); + removed_child->SetWindowHostRecursive(nullptr); OnRemoveChild(removed_child, position); } +RenderObject* RenderObject::GetFirstChild() const { + const auto& children = GetChildren(); + if (children.empty()) { + return nullptr; + } else { + return children.front(); + } +} + +void RenderObject::TraverseDescendants( + const std::function<void(RenderObject*)>& action) { + action(this); + for (auto child : children_) child->TraverseDescendants(action); +} + Point RenderObject::GetTotalOffset() const { Point result{}; const RenderObject* render_object = this; @@ -66,18 +90,34 @@ void RenderObject::Measure(const MeasureRequirement& requirement, MeasureSize merged_preferred_size = preferred_size.OverrideBy(preferred_size_); + if constexpr (cru::ui::debug_flags::layout) { + log::Debug(u"{} Measure begins :\nrequirement: {}\npreferred size: {}", + this->GetDebugPathInTree(), requirement.ToDebugString(), + preferred_size.ToDebugString()); + } + size_ = OnMeasureCore(merged_requirement, merged_preferred_size); + + if constexpr (cru::ui::debug_flags::layout) { + log::Debug(u"{} Measure ends :\nresult size: {}", + this->GetDebugPathInTree(), size_.ToDebugString()); + } + Ensures(size_.width >= 0); Ensures(size_.height >= 0); Ensures(requirement.Satisfy(size_)); } void RenderObject::Layout(const Point& offset) { + if constexpr (cru::ui::debug_flags::layout) { + log::Debug(u"{} Layout :\noffset: {}", this->GetDebugPathInTree(), + offset.ToDebugString()); + } offset_ = offset; OnLayoutCore(); } -void RenderObject::Draw(platform::graph::IPainter* painter) { +void RenderObject::Draw(platform::graphics::IPainter* painter) { OnDrawCore(painter); } @@ -112,29 +152,29 @@ void RenderObject::OnRemoveChild(RenderObject* removed_child, Index position) { InvalidatePaint(); } -void RenderObject::DefaultDrawChildren(platform::graph::IPainter* painter) { +void RenderObject::DefaultDrawChildren(platform::graphics::IPainter* painter) { for (const auto child : GetChildren()) { auto offset = child->GetOffset(); - platform::graph::util::WithTransform( + platform::graphics::util::WithTransform( painter, platform::Matrix::Translation(offset.x, offset.y), [child](auto p) { child->Draw(p); }); } } -void RenderObject::DefaultDrawContent(platform::graph::IPainter* painter) { +void RenderObject::DefaultDrawContent(platform::graphics::IPainter* painter) { const auto content_rect = GetContentRect(); - platform::graph::util::WithTransform( + platform::graphics::util::WithTransform( painter, Matrix::Translation(content_rect.left, content_rect.top), [this](auto p) { this->OnDrawContent(p); }); } -void RenderObject::OnDrawCore(platform::graph::IPainter* painter) { +void RenderObject::OnDrawCore(platform::graphics::IPainter* painter) { DefaultDrawContent(painter); DefaultDrawChildren(painter); } -void RenderObject::OnDrawContent(platform::graph::IPainter* painter) { +void RenderObject::OnDrawContent(platform::graphics::IPainter* painter) { CRU_UNUSED(painter); } @@ -249,24 +289,44 @@ void RenderObject::SetParent(RenderObject* new_parent) { } void RenderObject::InvalidateLayout() { - if (ui_host_ != nullptr) ui_host_->InvalidateLayout(); + if (window_host_ != nullptr) window_host_->InvalidateLayout(); } void RenderObject::InvalidatePaint() { - if (ui_host_ != nullptr) ui_host_->InvalidatePaint(); + if (window_host_ != nullptr) window_host_->InvalidatePaint(); } -void RenderObject::NotifyAfterLayoutRecursive(RenderObject* render_object) { - render_object->OnAfterLayout(); - for (const auto o : render_object->GetChildren()) { - NotifyAfterLayoutRecursive(o); +constexpr std::u16string_view kUnamedName(u"UNNAMED"); + +std::u16string_view RenderObject::GetName() const { return kUnamedName; } + +std::u16string RenderObject::GetDebugPathInTree() const { + std::vector<std::u16string_view> chain; + const RenderObject* parent = this; + while (parent != nullptr) { + chain.push_back(parent->GetName()); + parent = parent->GetParent(); } + + std::u16string result(chain.back()); + for (auto iter = chain.crbegin() + 1; iter != chain.crend(); ++iter) { + result += u" -> "; + result += *iter; + } + + return result; } -void RenderObject::SetRenderHostRecursive(UiHost* host) { - ui_host_ = host; +void RenderObject::SetWindowHostRecursive(host::WindowHost* host) { + if (window_host_ != nullptr) { + detach_from_host_event_.Raise(nullptr); + } + window_host_ = host; + if (host != nullptr) { + attach_to_host_event_.Raise(nullptr); + } for (const auto child : GetChildren()) { - child->SetRenderHostRecursive(host); + child->SetWindowHostRecursive(host); } } } // namespace cru::ui::render diff --git a/src/ui/render/ScrollBar.cpp b/src/ui/render/ScrollBar.cpp new file mode 100644 index 00000000..7f69c1e2 --- /dev/null +++ b/src/ui/render/ScrollBar.cpp @@ -0,0 +1,622 @@ +#include "cru/ui/render/ScrollBar.hpp" + +#include "../Helper.hpp" +#include "cru/common/Base.hpp" +#include "cru/platform/GraphBase.hpp" +#include "cru/platform/graphics/Factory.hpp" +#include "cru/platform/graphics/Geometry.hpp" +#include "cru/platform/graphics/Painter.hpp" +#include "cru/platform/graphics/util/Painter.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/ui/Base.hpp" +#include "cru/ui/events/UiEvent.hpp" +#include "cru/ui/render/ScrollRenderObject.hpp" +#include "gsl/gsl_assert" + +#include <algorithm> +#include <cassert> +#include <chrono> +#include <gsl/pointers> +#include <memory> +#include <optional> +#include <stdexcept> + +namespace cru::ui::render { +using namespace std::chrono_literals; +constexpr float kScrollBarCollapseThumbWidth = 2; +constexpr float kScrollBarCollapsedTriggerExpandAreaWidth = 5; +constexpr float kScrollBarExpandWidth = 10; +constexpr float kScrollBarArrowHeight = 3.5; +constexpr auto kScrollBarAutoCollapseDelay = 1500ms; + +constexpr std::array<ScrollBarAreaKind, 5> kScrollBarAreaKindList{ + ScrollBarAreaKind::UpArrow, ScrollBarAreaKind::DownArrow, + ScrollBarAreaKind::UpSlot, ScrollBarAreaKind::DownSlot, + ScrollBarAreaKind::Thumb}; + +namespace { +std::unique_ptr<platform::graphics::IGeometry> CreateScrollBarArrowGeometry() { + auto geometry_builder = GetGraphFactory()->CreateGeometryBuilder(); + geometry_builder->BeginFigure({-kScrollBarArrowHeight / 2, 0}); + geometry_builder->LineTo({kScrollBarArrowHeight / 2, kScrollBarArrowHeight}); + geometry_builder->LineTo({kScrollBarArrowHeight / 2, -kScrollBarArrowHeight}); + geometry_builder->CloseFigure(true); + return geometry_builder->Build(); +} +} // namespace + +ScrollBar::ScrollBar(gsl::not_null<ScrollRenderObject*> render_object, + Direction direction) + : render_object_(render_object), direction_(direction) { + // TODO: Use theme resource and delete this. + + auto graphics_factory = GetUiApplication()->GetInstance()->GetGraphFactory(); + + collapsed_thumb_brush_ = + graphics_factory->CreateSolidColorBrush(colors::gray.WithAlpha(128)); + expanded_thumb_brush_ = graphics_factory->CreateSolidColorBrush(colors::gray); + expanded_slot_brush_ = + graphics_factory->CreateSolidColorBrush(colors::seashell); + expanded_arrow_brush_ = graphics_factory->CreateSolidColorBrush(colors::gray); + expanded_arrow_background_brush_ = + graphics_factory->CreateSolidColorBrush(colors::seashell); + + arrow_geometry_ = CreateScrollBarArrowGeometry(); +} + +ScrollBar::~ScrollBar() { RestoreCursor(); } + +void ScrollBar::SetEnabled(bool value) { + if (value == is_enabled_) return; + if (!value) { + SetExpanded(false); + if (move_thumb_start_) { + if (const auto control = this->render_object_->GetAttachedControl()) { + control->ReleaseMouse(); + } + move_thumb_start_ = std::nullopt; + } + } +} + +void ScrollBar::SetExpanded(bool value) { + if (is_expanded_ == value) return; + is_expanded_ = value; + render_object_->InvalidatePaint(); +} + +void ScrollBar::Draw(platform::graphics::IPainter* painter) { + if (is_enabled_) { + OnDraw(painter, is_expanded_); + } +} + +void ScrollBar::InstallHandlers(controls::Control* control) { + event_guard_.Clear(); + if (control != nullptr) { + event_guard_ += + control->MouseDownEvent()->Bubble()->PrependShortCircuitHandler( + [control, this](event::MouseButtonEventArgs& event) { + if (event.GetButton() == mouse_buttons::left && IsEnabled() && + IsExpanded()) { + auto hit_test_result = + ExpandedHitTest(event.GetPoint(render_object_)); + if (!hit_test_result) return false; + + switch (*hit_test_result) { + case ScrollBarAreaKind::UpArrow: + this->scroll_attempt_event_.Raise( + {GetDirection(), ScrollKind::Line, -1}); + event.SetHandled(); + return true; + case ScrollBarAreaKind::DownArrow: + this->scroll_attempt_event_.Raise( + {GetDirection(), ScrollKind::Line, 1}); + event.SetHandled(); + return true; + case ScrollBarAreaKind::UpSlot: + this->scroll_attempt_event_.Raise( + {GetDirection(), ScrollKind::Page, -1}); + event.SetHandled(); + return true; + case ScrollBarAreaKind::DownSlot: + this->scroll_attempt_event_.Raise( + {GetDirection(), ScrollKind::Page, 1}); + event.SetHandled(); + return true; + case ScrollBarAreaKind::Thumb: { + auto thumb_rect = + GetExpandedAreaRect(ScrollBarAreaKind::Thumb); + assert(thumb_rect); + + if (!control->CaptureMouse()) break; + move_thumb_thumb_original_rect_ = *thumb_rect; + move_thumb_start_ = event.GetPoint(); + event.SetHandled(); + return true; + } + default: + break; + } + } + + return false; + }); + + event_guard_ += + control->MouseUpEvent()->Bubble()->PrependShortCircuitHandler( + [control, this](event::MouseButtonEventArgs& event) { + if (event.GetButton() == mouse_buttons::left && + move_thumb_start_) { + move_thumb_start_ = std::nullopt; + + auto hit_test_result = + ExpandedHitTest(event.GetPoint(this->render_object_)); + if (!hit_test_result) { + OnMouseLeave(); + } + + control->ReleaseMouse(); + event.SetHandled(); + return true; + } + return false; + }); + + event_guard_ += + control->MouseMoveEvent()->Bubble()->PrependShortCircuitHandler( + [this](event::MouseEventArgs& event) { + if (move_thumb_start_) { + auto new_scroll_position = CalculateNewScrollPosition( + move_thumb_thumb_original_rect_, + event.GetPoint() - *move_thumb_start_); + + this->scroll_attempt_event_.Raise({GetDirection(), + ScrollKind::Absolute, + new_scroll_position}); + event.SetHandled(); + return true; + } + + if (IsEnabled()) { + if (IsExpanded()) { + auto hit_test_result = + ExpandedHitTest(event.GetPoint(this->render_object_)); + if (hit_test_result) { + SetCursor(); + StopAutoCollapseTimer(); + } else { + OnMouseLeave(); + } + } else { + auto trigger_expand_area = + GetCollapsedTriggerExpandAreaRect(); + if (trigger_expand_area && + trigger_expand_area->IsPointInside( + event.GetPoint(this->render_object_))) { + SetExpanded(true); + SetCursor(); + event.SetHandled(); + return true; + } + } + } + + return false; + }); + + event_guard_ += + control->MouseLeaveEvent()->Bubble()->PrependShortCircuitHandler( + [this](event::MouseEventArgs&) { + if (IsExpanded() && !move_thumb_start_) { + OnMouseLeave(); + } + return false; + }); + } +} + +gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> +ScrollBar::GetCollapsedThumbBrush() const { + // TODO: Read theme resource. + return collapsed_thumb_brush_; +} + +gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> +ScrollBar::GetExpandedThumbBrush() const { + // TODO: Read theme resource. + return expanded_thumb_brush_; +} + +gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> +ScrollBar::GetExpandedSlotBrush() const { + // TODO: Read theme resource. + return expanded_slot_brush_; +} + +gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> +ScrollBar::GetExpandedArrowBrush() const { + // TODO: Read theme resource. + return expanded_arrow_brush_; +} + +gsl::not_null<std::shared_ptr<platform::graphics::IBrush>> +ScrollBar::GetExpandedArrowBackgroundBrush() const { + // TODO: Read theme resource. + return expanded_arrow_background_brush_; +} + +void ScrollBar::OnDraw(platform::graphics::IPainter* painter, + bool is_expanded) { + if (is_expanded) { + auto thumb_rect = GetExpandedAreaRect(ScrollBarAreaKind::Thumb); + if (thumb_rect) + painter->FillRectangle(*thumb_rect, GetExpandedThumbBrush().get().get()); + + auto slot_brush = GetExpandedSlotBrush().get().get(); + + auto up_slot_rect = GetExpandedAreaRect(ScrollBarAreaKind::UpSlot); + if (up_slot_rect) painter->FillRectangle(*up_slot_rect, slot_brush); + + auto down_slot_rect = GetExpandedAreaRect(ScrollBarAreaKind::DownSlot); + if (down_slot_rect) painter->FillRectangle(*down_slot_rect, slot_brush); + + auto up_arrow = GetExpandedAreaRect(ScrollBarAreaKind::UpArrow); + if (up_arrow) this->DrawUpArrow(painter, *up_arrow); + + auto down_arrow = GetExpandedAreaRect(ScrollBarAreaKind::DownArrow); + if (down_arrow) this->DrawDownArrow(painter, *down_arrow); + } else { + auto optional_rect = GetCollapsedThumbRect(); + if (optional_rect) { + painter->FillRectangle(*optional_rect, + GetCollapsedThumbBrush().get().get()); + } + } +} + +void ScrollBar::SetCursor() { + if (!old_cursor_) { + if (const auto control = render_object_->GetAttachedControl()) { + old_cursor_ = control->GetCursor(); + control->SetCursor( + GetUiApplication()->GetCursorManager()->GetSystemCursor( + platform::gui::SystemCursorType::Arrow)); + } + } +} + +void ScrollBar::RestoreCursor() { + if (old_cursor_) { + if (const auto control = render_object_->GetAttachedControl()) { + control->SetCursor(*old_cursor_); + } + old_cursor_ = std::nullopt; + } +} + +void ScrollBar::BeginAutoCollapseTimer() { + if (!auto_collapse_timer_canceler_ && IsExpanded()) { + auto_collapse_timer_canceler_ = GetUiApplication()->SetTimeout( + kScrollBarAutoCollapseDelay, [this] { this->SetExpanded(false); }); + } +} + +void ScrollBar::StopAutoCollapseTimer() { + auto_collapse_timer_canceler_.Reset(); +} + +void ScrollBar::OnMouseLeave() { + RestoreCursor(); + BeginAutoCollapseTimer(); +} + +std::optional<ScrollBarAreaKind> ScrollBar::ExpandedHitTest( + const Point& point) { + for (auto kind : kScrollBarAreaKindList) { + auto rect = this->GetExpandedAreaRect(kind); + if (rect) { + if (rect->IsPointInside(point)) return kind; + } + } + return std::nullopt; +} + +HorizontalScrollBar::HorizontalScrollBar( + gsl::not_null<ScrollRenderObject*> render_object) + : ScrollBar(render_object, Direction::Horizontal) {} + +void HorizontalScrollBar::DrawUpArrow(platform::graphics::IPainter* painter, + const Rect& area) { + painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get()); + + platform::graphics::util::WithTransform( + painter, Matrix::Translation(area.GetCenter()), + [this](platform::graphics::IPainter* painter) { + painter->FillGeometry(arrow_geometry_.get(), + GetExpandedArrowBrush().get().get()); + }); +} + +void HorizontalScrollBar::DrawDownArrow(platform::graphics::IPainter* painter, + const Rect& area) { + painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get()); + + platform::graphics::util::WithTransform( + painter, Matrix::Rotation(180) * Matrix::Translation(area.GetCenter()), + [this](platform::graphics::IPainter* painter) { + painter->FillGeometry(arrow_geometry_.get(), + GetExpandedArrowBrush().get().get()); + }); +} + +bool HorizontalScrollBar::IsShowBar() { + const auto child = render_object_->GetFirstChild(); + if (child == nullptr) return false; + + const auto view_rect = render_object_->GetViewRect(); + const auto child_size = child->GetSize(); + + if (view_rect.width >= child_size.width) return false; + + return true; +} + +std::optional<Rect> HorizontalScrollBar::GetExpandedAreaRect( + ScrollBarAreaKind area_kind) { + auto show = IsShowBar(); + if (!show) return std::nullopt; + + const auto padding_rect = render_object_->GetPaddingRect(); + + const auto child = render_object_->GetFirstChild(); + + const auto view_rect = render_object_->GetViewRect(); + const auto child_size = child->GetSize(); + + const float start_percentage = view_rect.left / child_size.width; + const float length_percentage = view_rect.width / child_size.width; + const float end_percentage = start_percentage + length_percentage; + + const float top = padding_rect.GetBottom() - kScrollBarExpandWidth; + const float height = kScrollBarExpandWidth; + + // Without arrow. + const float bar_area_length = padding_rect.width - 3 * kScrollBarExpandWidth; + const float bar_area_start = padding_rect.left + kScrollBarExpandWidth; + + switch (area_kind) { + case ScrollBarAreaKind::UpArrow: + return Rect{padding_rect.left, top, kScrollBarExpandWidth, height}; + case ScrollBarAreaKind::DownArrow: + return Rect{padding_rect.GetRight() - 2 * kScrollBarExpandWidth, top, + kScrollBarExpandWidth, height}; + case ScrollBarAreaKind::UpSlot: + return Rect{bar_area_start, top, bar_area_length * start_percentage, + height}; + case ScrollBarAreaKind::DownSlot: + return Rect{bar_area_start + bar_area_length * end_percentage, top, + bar_area_length * (1 - end_percentage), height}; + case ScrollBarAreaKind::Thumb: + return Rect{bar_area_start + bar_area_length * start_percentage, top, + bar_area_length * length_percentage, height}; + default: + throw std::invalid_argument("Unsupported scroll area kind."); + } +} + +std::optional<Rect> HorizontalScrollBar::GetCollapsedTriggerExpandAreaRect() { + auto show = IsShowBar(); + if (!show) return std::nullopt; + + const auto padding_rect = render_object_->GetPaddingRect(); + + return Rect{ + padding_rect.left, + padding_rect.GetBottom() - kScrollBarCollapsedTriggerExpandAreaWidth, + padding_rect.width, kScrollBarCollapseThumbWidth}; +} + +std::optional<Rect> HorizontalScrollBar::GetCollapsedThumbRect() { + auto show = IsShowBar(); + if (!show) return std::nullopt; + + const auto child = render_object_->GetFirstChild(); + + const auto view_rect = render_object_->GetViewRect(); + const auto child_size = child->GetSize(); + + const float start_percentage = view_rect.left / child_size.width; + const float length_percentage = view_rect.width / child_size.width; + // const float end_percentage = start_percentage + length_percentage; + + const auto padding_rect = render_object_->GetPaddingRect(); + + return Rect{padding_rect.left + padding_rect.width * start_percentage, + padding_rect.GetBottom() - kScrollBarCollapseThumbWidth, + padding_rect.width * length_percentage, + kScrollBarCollapseThumbWidth}; +} + +float HorizontalScrollBar::CalculateNewScrollPosition( + const Rect& thumb_original_rect, const Point& mouse_offset) { + auto new_thumb_start = thumb_original_rect.left + mouse_offset.x; + + const auto padding_rect = render_object_->GetPaddingRect(); + + auto scroll_area_start = padding_rect.left + kScrollBarExpandWidth; + auto scroll_area_end = padding_rect.GetRight() - 2 * kScrollBarExpandWidth; + + auto thumb_head_end = scroll_area_end - thumb_original_rect.width; + + const auto child = render_object_->GetFirstChild(); + const auto child_size = child->GetSize(); + + new_thumb_start = + std::clamp(new_thumb_start, scroll_area_start, thumb_head_end); + + auto offset = (new_thumb_start - scroll_area_start) / + (scroll_area_end - scroll_area_start) * child_size.width; + + return offset; +} + +VerticalScrollBar::VerticalScrollBar( + gsl::not_null<ScrollRenderObject*> render_object) + : ScrollBar(render_object, Direction::Vertical) {} + +void VerticalScrollBar::DrawUpArrow(platform::graphics::IPainter* painter, + const Rect& area) { + painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get()); + + platform::graphics::util::WithTransform( + painter, Matrix::Rotation(90) * Matrix::Translation(area.GetCenter()), + [this](platform::graphics::IPainter* painter) { + painter->FillGeometry(arrow_geometry_.get(), + GetExpandedArrowBrush().get().get()); + }); +} + +void VerticalScrollBar::DrawDownArrow(platform::graphics::IPainter* painter, + const Rect& area) { + painter->FillRectangle(area, GetExpandedArrowBackgroundBrush().get().get()); + + platform::graphics::util::WithTransform( + painter, Matrix::Rotation(270) * Matrix::Translation(area.GetCenter()), + [this](platform::graphics::IPainter* painter) { + painter->FillGeometry(arrow_geometry_.get(), + GetExpandedArrowBrush().get().get()); + }); +} + +bool VerticalScrollBar::IsShowBar() { + const auto child = render_object_->GetFirstChild(); + if (child == nullptr) return false; + + const auto view_rect = render_object_->GetViewRect(); + const auto child_size = child->GetSize(); + + if (view_rect.height >= child_size.height) return false; + + return true; +} + +std::optional<Rect> VerticalScrollBar::GetExpandedAreaRect( + ScrollBarAreaKind area_kind) { + auto show = IsShowBar(); + if (!show) return std::nullopt; + + const auto padding_rect = render_object_->GetPaddingRect(); + + const auto child = render_object_->GetFirstChild(); + + const auto view_rect = render_object_->GetViewRect(); + const auto child_size = child->GetSize(); + + const float start_percentage = view_rect.top / child_size.height; + const float length_percentage = view_rect.height / child_size.height; + const float end_percentage = start_percentage + length_percentage; + + const float left = padding_rect.GetRight() - kScrollBarExpandWidth; + const float width = kScrollBarExpandWidth; + + // Without arrow. + const float bar_area_length = padding_rect.height - 3 * kScrollBarExpandWidth; + const float bar_area_start = padding_rect.top + kScrollBarExpandWidth; + + switch (area_kind) { + case ScrollBarAreaKind::UpArrow: + return Rect{left, padding_rect.top, width, kScrollBarExpandWidth}; + case ScrollBarAreaKind::DownArrow: + return Rect{left, padding_rect.GetBottom() - 2 * kScrollBarExpandWidth, + width, kScrollBarExpandWidth}; + case ScrollBarAreaKind::UpSlot: + return Rect{left, bar_area_start, width, + bar_area_length * start_percentage}; + case ScrollBarAreaKind::DownSlot: + return Rect{left, bar_area_start + bar_area_length * end_percentage, + width, bar_area_length * (1 - end_percentage)}; + case ScrollBarAreaKind::Thumb: + return Rect{left, bar_area_start + bar_area_length * start_percentage, + width, bar_area_length * length_percentage}; + default: + throw std::invalid_argument("Unsupported scroll area kind."); + } +} + +std::optional<Rect> VerticalScrollBar::GetCollapsedTriggerExpandAreaRect() { + auto show = IsShowBar(); + if (!show) return std::nullopt; + + const auto padding_rect = render_object_->GetPaddingRect(); + + return Rect{ + padding_rect.GetRight() - kScrollBarCollapsedTriggerExpandAreaWidth, + padding_rect.top, kScrollBarCollapseThumbWidth, padding_rect.height}; +} + +std::optional<Rect> VerticalScrollBar::GetCollapsedThumbRect() { + const auto child = render_object_->GetFirstChild(); + if (child == nullptr) return std::nullopt; + + const auto view_rect = render_object_->GetViewRect(); + const auto padding_rect = render_object_->GetPaddingRect(); + const auto child_size = child->GetSize(); + + if (view_rect.height >= child_size.height) return std::nullopt; + + const float start_percentage = view_rect.top / child_size.height; + const float length_percentage = view_rect.height / child_size.height; + // const float end_percentage = start_percentage + length_percentage; + + return Rect{padding_rect.GetRight() - kScrollBarCollapseThumbWidth, + padding_rect.top + padding_rect.height * start_percentage, + kScrollBarCollapseThumbWidth, + padding_rect.height * length_percentage}; +} + +float VerticalScrollBar::CalculateNewScrollPosition( + const Rect& thumb_original_rect, const Point& mouse_offset) { + auto new_thumb_start = thumb_original_rect.top + mouse_offset.y; + + const auto padding_rect = render_object_->GetPaddingRect(); + + auto scroll_area_start = padding_rect.top + kScrollBarExpandWidth; + auto scroll_area_end = padding_rect.GetBottom() - 2 * kScrollBarExpandWidth; + + auto thumb_head_end = scroll_area_end - thumb_original_rect.height; + + const auto child = render_object_->GetFirstChild(); + const auto child_size = child->GetSize(); + + new_thumb_start = + std::clamp(new_thumb_start, scroll_area_start, thumb_head_end); + + auto offset = (new_thumb_start - scroll_area_start) / + (scroll_area_end - scroll_area_start) * child_size.width; + + return offset; +} + +ScrollBarDelegate::ScrollBarDelegate( + gsl::not_null<ScrollRenderObject*> render_object) + : render_object_(render_object), + horizontal_bar_(render_object), + vertical_bar_(render_object) { + horizontal_bar_.ScrollAttemptEvent()->AddHandler( + [this](auto scroll) { this->scroll_attempt_event_.Raise(scroll); }); + vertical_bar_.ScrollAttemptEvent()->AddHandler( + [this](auto scroll) { this->scroll_attempt_event_.Raise(scroll); }); +} + +void ScrollBarDelegate::DrawScrollBar(platform::graphics::IPainter* painter) { + horizontal_bar_.Draw(painter); + vertical_bar_.Draw(painter); +} + +void ScrollBarDelegate::InstallHandlers(controls::Control* control) { + horizontal_bar_.InstallHandlers(control); + vertical_bar_.InstallHandlers(control); +} +} // namespace cru::ui::render diff --git a/src/ui/render/ScrollRenderObject.cpp b/src/ui/render/ScrollRenderObject.cpp index 08ce744b..fd5143ff 100644 --- a/src/ui/render/ScrollRenderObject.cpp +++ b/src/ui/render/ScrollRenderObject.cpp @@ -1,11 +1,18 @@ #include "cru/ui/render/ScrollRenderObject.hpp" -#include "cru/platform/graph/Painter.hpp" -#include "cru/platform/graph/util/Painter.hpp" +#include "cru/platform/graphics/Painter.hpp" +#include "cru/platform/graphics/util/Painter.hpp" +#include "cru/ui/Base.hpp" +#include "cru/ui/controls/Control.hpp" +#include "cru/ui/render/ScrollBar.hpp" #include <algorithm> +#include <memory> +#include <optional> namespace cru::ui::render { +constexpr float kLineHeight = 16; + namespace { // This method assumes margin offset is already considered. // It promises that it won't return negetive value. @@ -24,13 +31,46 @@ Point CoerceScroll(const Point& scroll_offset, const Size& content_size, n = max; }; - coerce(result.x, scroll_offset.x); - coerce(result.y, scroll_offset.y); + coerce(result.x, max_scroll.x); + coerce(result.y, max_scroll.y); return result; } } // namespace +ScrollRenderObject::ScrollRenderObject() : RenderObject(ChildMode::Single) { + scroll_bar_delegate_ = std::make_unique<ScrollBarDelegate>(this); + scroll_bar_delegate_->ScrollAttemptEvent()->AddHandler( + [this](const struct Scroll& scroll) { this->Scroll(scroll); }); +} + +void ScrollRenderObject::Scroll(const struct Scroll& scroll) { + auto direction = scroll.direction; + + switch (scroll.kind) { + case ScrollKind::Absolute: + SetScrollOffset(direction, scroll.value); + break; + case ScrollKind::Relative: + SetScrollOffset(direction, + GetScrollOffset(scroll.direction) + scroll.value); + break; + case ScrollKind::Page: + SetScrollOffset(direction, GetScrollOffset(direction) + + (direction == Direction::Horizontal + ? GetViewRect().width + : GetViewRect().height) * + scroll.value); + break; + case ScrollKind::Line: + SetScrollOffset(direction, + GetScrollOffset(direction) + kLineHeight * scroll.value); + break; + default: + break; + } +} + RenderObject* ScrollRenderObject::HitTest(const Point& point) { if (const auto child = GetSingleChild()) { const auto offset = child->GetOffset(); @@ -42,16 +82,17 @@ RenderObject* ScrollRenderObject::HitTest(const Point& point) { return rect.IsPointInside(point) ? this : nullptr; } // namespace cru::ui::render -void ScrollRenderObject::OnDrawCore(platform::graph::IPainter* painter) { +void ScrollRenderObject::OnDrawCore(platform::graphics::IPainter* painter) { DefaultDrawContent(painter); if (const auto child = GetSingleChild()) { - painter->PushLayer(this->GetPaddingRect()); + painter->PushLayer(this->GetContentRect()); const auto offset = child->GetOffset(); - platform::graph::util::WithTransform( + platform::graphics::util::WithTransform( painter, Matrix::Translation(offset.x, offset.y), - [child](platform::graph::IPainter* p) { child->Draw(p); }); + [child](platform::graphics::IPainter* p) { child->Draw(p); }); painter->PopLayer(); } + scroll_bar_delegate_->DrawScrollBar(painter); } Point ScrollRenderObject::GetScrollOffset() { @@ -138,8 +179,15 @@ Size ScrollRenderObject::OnMeasureContent(const MeasureRequirement& requirement, void ScrollRenderObject::OnLayoutContent(const Rect& content_rect) { if (const auto child = GetSingleChild()) { - const auto child_size = child->GetSize(); child->Layout(content_rect.GetLeftTop() - GetScrollOffset()); } } + +void ScrollRenderObject::OnAttachedControlChanged(controls::Control* control) { + if (control) { + scroll_bar_delegate_->InstallHandlers(control); + } else { + scroll_bar_delegate_->UninstallHandlers(); + } +} } // namespace cru::ui::render diff --git a/src/ui/render/TextRenderObject.cpp b/src/ui/render/TextRenderObject.cpp index cecbe1f3..06092d52 100644 --- a/src/ui/render/TextRenderObject.cpp +++ b/src/ui/render/TextRenderObject.cpp @@ -2,19 +2,19 @@ #include "../Helper.hpp" #include "cru/common/Logger.hpp" -#include "cru/platform/graph/Factory.hpp" -#include "cru/platform/graph/TextLayout.hpp" -#include "cru/platform/graph/util/Painter.hpp" +#include "cru/platform/graphics/Factory.hpp" +#include "cru/platform/graphics/TextLayout.hpp" +#include "cru/platform/graphics/util/Painter.hpp" #include <algorithm> #include <limits> namespace cru::ui::render { TextRenderObject::TextRenderObject( - std::shared_ptr<platform::graph::IBrush> brush, - std::shared_ptr<platform::graph::IFont> font, - std::shared_ptr<platform::graph::IBrush> selection_brush, - std::shared_ptr<platform::graph::IBrush> caret_brush) { + std::shared_ptr<platform::graphics::IBrush> brush, + std::shared_ptr<platform::graphics::IFont> font, + std::shared_ptr<platform::graphics::IBrush> selection_brush, + std::shared_ptr<platform::graphics::IBrush> caret_brush) { Expects(brush); Expects(font); Expects(selection_brush); @@ -43,20 +43,22 @@ std::u16string_view TextRenderObject::GetTextView() const { void TextRenderObject::SetText(std::u16string new_text) { text_layout_->SetText(std::move(new_text)); + InvalidateLayout(); } void TextRenderObject::SetBrush( - std::shared_ptr<platform::graph::IBrush> new_brush) { + std::shared_ptr<platform::graphics::IBrush> new_brush) { Expects(new_brush); new_brush.swap(brush_); InvalidatePaint(); } -std::shared_ptr<platform::graph::IFont> TextRenderObject::GetFont() const { +std::shared_ptr<platform::graphics::IFont> TextRenderObject::GetFont() const { return text_layout_->GetFont(); } -void TextRenderObject::SetFont(std::shared_ptr<platform::graph::IFont> font) { +void TextRenderObject::SetFont( + std::shared_ptr<platform::graphics::IFont> font) { Expects(font); text_layout_->SetFont(std::move(font)); } @@ -69,7 +71,7 @@ Point TextRenderObject::TextSinglePoint(gsl::index position, bool trailing) { return text_layout_->TextSinglePoint(position, trailing); } -platform::graph::TextHitTestResult TextRenderObject::TextHitTest( +platform::graphics::TextHitTestResult TextRenderObject::TextHitTest( const Point& point) { return text_layout_->HitTest(point); } @@ -80,7 +82,7 @@ void TextRenderObject::SetSelectionRange(std::optional<TextRange> new_range) { } void TextRenderObject::SetSelectionBrush( - std::shared_ptr<platform::graph::IBrush> new_brush) { + std::shared_ptr<platform::graphics::IBrush> new_brush) { Expects(new_brush); new_brush.swap(selection_brush_); if (selection_range_ && selection_range_->count) { @@ -105,7 +107,7 @@ void TextRenderObject::SetCaretPosition(gsl::index position) { } void TextRenderObject::GetCaretBrush( - std::shared_ptr<platform::graph::IBrush> brush) { + std::shared_ptr<platform::graphics::IBrush> brush) { Expects(brush); brush.swap(caret_brush_); if (draw_caret_) { @@ -153,12 +155,18 @@ 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; } -void TextRenderObject::OnDrawContent(platform::graph::IPainter* painter) { +void TextRenderObject::OnDrawContent(platform::graphics::IPainter* painter) { if (this->selection_range_.has_value()) { const auto&& rects = text_layout_->TextRangeRect(this->selection_range_.value()); @@ -184,7 +192,9 @@ Size TextRenderObject::OnMeasureContent(const MeasureRequirement& requirement, text_layout_->SetMaxWidth(measure_width); text_layout_->SetMaxHeight(std::numeric_limits<float>::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/ui/render/WindowRenderObject.cpp b/src/ui/render/WindowRenderObject.cpp deleted file mode 100644 index 4adf559e..00000000 --- a/src/ui/render/WindowRenderObject.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "cru/ui/render/WindowRenderObject.hpp" - -#include "../Helper.hpp" -#include "cru/platform/graph/util/Painter.hpp" -#include "cru/ui/UiHost.hpp" - -namespace cru::ui::render { -WindowRenderObject::WindowRenderObject(UiHost* host) { - SetChildMode(ChildMode::Single); - ui_host_ = host; - after_layout_event_guard_.Reset(host->AfterLayoutEvent()->AddHandler( - [this](auto) { NotifyAfterLayoutRecursive(this); })); -} - -RenderObject* WindowRenderObject::HitTest(const Point& point) { - if (const auto child = GetChild()) { - auto offset = child->GetOffset(); - Point p{point.x - offset.x, point.y - offset.y}; - const auto result = child->HitTest(p); - if (result != nullptr) { - return result; - } - } - return Rect{Point{}, GetSize()}.IsPointInside(point) ? this : nullptr; -} - -Size WindowRenderObject::OnMeasureContent(const MeasureRequirement& requirement, - const MeasureSize& preferred_size) { - if (const auto child = GetChild()) { - child->Measure(requirement, preferred_size); - return child->GetSize(); - } else { - return Size{}; - } -} - -void WindowRenderObject::OnLayoutContent(const Rect& content_rect) { - if (const auto child = GetChild()) child->Layout(content_rect.GetLeftTop()); -} -} // namespace cru::ui::render diff --git a/src/ui/style/Condition.cpp b/src/ui/style/Condition.cpp new file mode 100644 index 00000000..f4866c04 --- /dev/null +++ b/src/ui/style/Condition.cpp @@ -0,0 +1,84 @@ +#include "cru/ui/style/Condition.hpp" +#include <memory> + +#include "cru/common/ClonablePtr.hpp" +#include "cru/common/Event.hpp" +#include "cru/ui/controls/Control.hpp" +#include "cru/ui/controls/IClickableControl.hpp" +#include "cru/ui/helper/ClickDetector.hpp" + +namespace cru::ui::style { +CompoundCondition::CompoundCondition( + std::vector<ClonablePtr<Condition>> conditions) + : conditions_(std::move(conditions)) {} + +std::vector<IBaseEvent*> CompoundCondition::ChangeOn( + controls::Control* control) const { + std::vector<IBaseEvent*> result; + + for (auto condition : conditions_) { + for (auto e : condition->ChangeOn(control)) { + result.push_back(e); + } + } + + return result; +} + +bool AndCondition::Judge(controls::Control* control) const { + for (auto condition : conditions_) { + if (!condition->Judge(control)) return false; + } + return true; +} + +bool OrCondition::Judge(controls::Control* control) const { + for (auto condition : conditions_) { + if (condition->Judge(control)) return true; + } + return false; +} + +FocusCondition::FocusCondition(bool has_focus) : has_focus_(has_focus) {} + +std::vector<IBaseEvent*> FocusCondition::ChangeOn( + controls::Control* control) const { + return {control->GainFocusEvent()->Direct(), + control->LoseFocusEvent()->Direct()}; +} + +bool FocusCondition::Judge(controls::Control* control) const { + return control->HasFocus() == has_focus_; +} + +std::vector<IBaseEvent*> 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) {} + +std::vector<IBaseEvent*> ClickStateCondition::ChangeOn( + controls::Control* control) const { + auto clickable_control = dynamic_cast<controls::IClickableControl*>(control); + if (clickable_control) { + return {clickable_control->ClickStateChangeEvent()}; + } else { + return {}; + } +} + +bool ClickStateCondition::Judge(controls::Control* control) const { + auto clickable_control = dynamic_cast<controls::IClickableControl*>(control); + if (clickable_control) { + return clickable_control->GetClickState() == click_state_; + } + return false; +} +} // namespace cru::ui::style diff --git a/src/ui/style/StyleRule.cpp b/src/ui/style/StyleRule.cpp new file mode 100644 index 00000000..1a72a970 --- /dev/null +++ b/src/ui/style/StyleRule.cpp @@ -0,0 +1,17 @@ +#include "cru/ui/style/StyleRule.hpp" + +namespace cru::ui::style { +StyleRule::StyleRule(ClonablePtr<Condition> condition, + ClonablePtr<Styler> styler, std::u16string name) + : condition_(std::move(condition)), + styler_(std::move(styler)), + name_(std::move(name)) {} + +bool StyleRule::CheckAndApply(controls::Control *control) const { + auto active = condition_->Judge(control); + if (active) { + styler_->Apply(control); + } + return active; +} +} // namespace cru::ui::style diff --git a/src/ui/style/StyleRuleSet.cpp b/src/ui/style/StyleRuleSet.cpp new file mode 100644 index 00000000..537d1956 --- /dev/null +++ b/src/ui/style/StyleRuleSet.cpp @@ -0,0 +1,97 @@ +#include "cru/ui/style/StyleRuleSet.hpp" +#include "cru/common/Event.hpp" +#include "cru/ui/controls/Control.hpp" +#include "gsl/gsl_assert" + +#include <unordered_set> + +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)); + + RaiseChangeEvent(); +} + +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); + + RaiseChangeEvent(); +} + +void StyleRuleSet::Set(const StyleRuleSet& other, bool set_parent) { + rules_ = other.rules_; + if (set_parent) parent_ = other.parent_; + + RaiseChangeEvent(); +} + +StyleRuleSetBind::StyleRuleSetBind(controls::Control* control, + StyleRuleSet* ruleset) + : control_(control), ruleset_(ruleset) { + Expects(control); + Expects(ruleset); + + ruleset->ChangeEvent()->AddSpyOnlyHandler([this] { + UpdateRuleSetChainCache(); + UpdateChangeListener(); + UpdateStyle(); + }); +} + +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<IBaseEvent*> events; + + // 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) { + guard_ += e->AddSpyOnlyHandler([this] { this->UpdateStyle(); }); + } +} + +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 diff --git a/src/ui/style/Styler.cpp b/src/ui/style/Styler.cpp new file mode 100644 index 00000000..da3a2247 --- /dev/null +++ b/src/ui/style/Styler.cpp @@ -0,0 +1,29 @@ +#include "cru/ui/style/Styler.hpp" + +#include "../Helper.hpp" +#include "cru/common/ClonablePtr.hpp" +#include "cru/platform/gui/Cursor.hpp" +#include "cru/platform/gui/UiApplication.hpp" +#include "cru/ui/controls/Control.hpp" +#include "cru/ui/controls/IBorderControl.hpp" +#include "cru/ui/style/ApplyBorderStyleInfo.hpp" + +namespace cru::ui::style { +BorderStyler::BorderStyler(ApplyBorderStyleInfo style) + : style_(std::move(style)) {} + +void BorderStyler::Apply(controls::Control *control) const { + if (auto border_control = dynamic_cast<controls::IBorderControl *>(control)) { + border_control->ApplyBorderStyle(style_); + } +} + +ClonablePtr<CursorStyler> CursorStyler::Create( + platform::gui::SystemCursorType type) { + return Create(GetUiApplication()->GetCursorManager()->GetSystemCursor(type)); +} + +void CursorStyler::Apply(controls::Control *control) const { + control->SetCursor(cursor_); +} +} // namespace cru::ui::style diff --git a/src/win/CMakeLists.txt b/src/win/CMakeLists.txt index 75b0a7ca..bf8de863 100644 --- a/src/win/CMakeLists.txt +++ b/src/win/CMakeLists.txt @@ -2,6 +2,7 @@ set(CRU_WIN_BASE_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/win/) add_library(cru_win_base STATIC DebugLogger.hpp + StdOutLogger.hpp Exception.cpp HeapDebug.cpp @@ -13,5 +14,5 @@ target_sources(cru_win_base PUBLIC target_compile_definitions(cru_win_base PUBLIC UNICODE _UNICODE) # use unicode target_link_libraries(cru_win_base PUBLIC cru_base) -add_subdirectory(graph) -add_subdirectory(native) +add_subdirectory(graphics) +add_subdirectory(gui) diff --git a/src/win/StdOutLogger.hpp b/src/win/StdOutLogger.hpp new file mode 100644 index 00000000..bff8b30e --- /dev/null +++ b/src/win/StdOutLogger.hpp @@ -0,0 +1,24 @@ +#include "cru/common/Base.hpp" +#include "cru/win/WinPreConfig.hpp" + +#include "cru/common/Logger.hpp" + +#include <cwchar> + +namespace cru::platform::win { +class WinStdOutLoggerSource : public ::cru::log::ILogSource { + public: + WinStdOutLoggerSource() = default; + + CRU_DELETE_COPY(WinStdOutLoggerSource) + CRU_DELETE_MOVE(WinStdOutLoggerSource) + + ~WinStdOutLoggerSource() = default; + + void Write(::cru::log::LogLevel level, const std::u16string& s) override { + CRU_UNUSED(level) + + std::fputws(reinterpret_cast<const wchar_t*>(s.c_str()), stdout); + } +}; +} // namespace cru::platform::win diff --git a/src/win/graph/direct/CMakeLists.txt b/src/win/graph/direct/CMakeLists.txt deleted file mode 100644 index 5505b0b5..00000000 --- a/src/win/graph/direct/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -set(CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/win/graph/direct) - -add_library(cru_win_graph_direct STATIC - Brush.cpp - Font.cpp - Geometry.cpp - Factory.cpp - Painter.cpp - Resource.cpp - TextLayout.cpp -) -target_sources(cru_win_graph_direct PUBLIC - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/Brush.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/ComResource.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/ConvertUtil.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/Exception.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/Font.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/Geometry.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/Factory.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/Painter.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/Resource.hpp - ${CRU_WIN_GRAPH_DIRECT_INCLUDE_DIR}/TextLayout.hpp -) -target_link_libraries(cru_win_graph_direct PUBLIC D3D11 D2d1 DWrite) -target_link_libraries(cru_win_graph_direct PUBLIC cru_win_base cru_platform_graph) diff --git a/src/win/graph/direct/Resource.cpp b/src/win/graph/direct/Resource.cpp deleted file mode 100644 index 772bb74b..00000000 --- a/src/win/graph/direct/Resource.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "cru/win/graph/direct/Resource.hpp" - -#include "cru/win/graph/direct/Factory.hpp" - -namespace cru::platform::graph::win::direct { -DirectGraphResource::DirectGraphResource(DirectGraphFactory* factory) - : factory_(factory) { - Expects(factory); -} - -IGraphFactory* DirectGraphResource::GetGraphFactory() { return factory_; } -} // namespace cru::platform::graph::win::direct diff --git a/src/win/graph/CMakeLists.txt b/src/win/graphics/CMakeLists.txt index c90537ac..c90537ac 100644 --- a/src/win/graph/CMakeLists.txt +++ b/src/win/graphics/CMakeLists.txt diff --git a/src/win/graph/direct/Brush.cpp b/src/win/graphics/direct/Brush.cpp index 2b9d4ac3..b7842b97 100644 --- a/src/win/graph/direct/Brush.cpp +++ b/src/win/graphics/direct/Brush.cpp @@ -1,10 +1,10 @@ -#include "cru/win/graph/direct/Brush.hpp" +#include "cru/win/graphics/direct/Brush.hpp" -#include "cru/win/graph/direct/ConvertUtil.hpp" -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Factory.hpp" +#include "cru/win/graphics/direct/ConvertUtil.hpp" +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Factory.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { D2DSolidColorBrush::D2DSolidColorBrush(DirectGraphFactory* factory) : DirectGraphResource(factory) { ThrowIfFailed(factory->GetDefaultD2D1DeviceContext()->CreateSolidColorBrush( @@ -14,4 +14,4 @@ D2DSolidColorBrush::D2DSolidColorBrush(DirectGraphFactory* factory) void D2DSolidColorBrush::SetColor(const Color& color) { brush_->SetColor(Convert(color)); } -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/graphics/direct/CMakeLists.txt b/src/win/graphics/direct/CMakeLists.txt new file mode 100644 index 00000000..d4c96a65 --- /dev/null +++ b/src/win/graphics/direct/CMakeLists.txt @@ -0,0 +1,29 @@ +set(CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/win/graphics/direct) + +add_library(cru_win_graphics_direct STATIC + Brush.cpp + Font.cpp + Geometry.cpp + Factory.cpp + Painter.cpp + Resource.cpp + TextLayout.cpp + WindowPainter.cpp + WindowRenderTarget.cpp +) +target_sources(cru_win_graphics_direct PUBLIC + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/Brush.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/ComResource.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/ConvertUtil.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/Exception.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/Font.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/Geometry.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/Factory.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/Painter.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/Resource.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/TextLayout.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/WindowPainter.hpp + ${CRU_WIN_GRAPHICS_DIRECT_INCLUDE_DIR}/WindowRenderTarget.hpp +) +target_link_libraries(cru_win_graphics_direct PUBLIC D3D11 D2d1 DWrite) +target_link_libraries(cru_win_graphics_direct PUBLIC cru_win_base cru_platform_graphics) diff --git a/src/win/graph/direct/Factory.cpp b/src/win/graphics/direct/Factory.cpp index 03e64e13..6694801f 100644 --- a/src/win/graph/direct/Factory.cpp +++ b/src/win/graphics/direct/Factory.cpp @@ -1,16 +1,16 @@ -#include "cru/win/graph/direct/Factory.hpp" +#include "cru/win/graphics/direct/Factory.hpp" #include "cru/common/Logger.hpp" -#include "cru/win/graph/direct/Brush.hpp" -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Font.hpp" -#include "cru/win/graph/direct/Geometry.hpp" -#include "cru/win/graph/direct/TextLayout.hpp" +#include "cru/win/graphics/direct/Brush.hpp" +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Font.hpp" +#include "cru/win/graphics/direct/Geometry.hpp" +#include "cru/win/graphics/direct/TextLayout.hpp" #include <cstdlib> #include <utility> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { namespace { void InitializeCom() { const auto hresult = ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); @@ -104,4 +104,4 @@ std::unique_ptr<ITextLayout> DirectGraphFactory::CreateTextLayout( return std::make_unique<DWriteTextLayout>(this, std::move(font), std::move(text)); } -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/graph/direct/Font.cpp b/src/win/graphics/direct/Font.cpp index 34de5b71..1d6a5c88 100644 --- a/src/win/graph/direct/Font.cpp +++ b/src/win/graphics/direct/Font.cpp @@ -1,12 +1,12 @@ -#include "cru/win/graph/direct/Font.hpp" +#include "cru/win/graphics/direct/Font.hpp" -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Factory.hpp" +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Factory.hpp" #include <array> #include <utility> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { DWriteFont::DWriteFont(DirectGraphFactory* factory, std::u16string font_family, float font_size) : DirectGraphResource(factory), font_family_(std::move(font_family)) { @@ -28,4 +28,4 @@ DWriteFont::DWriteFont(DirectGraphFactory* factory, std::u16string font_family, } float DWriteFont::GetFontSize() { return text_format_->GetFontSize(); } -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/graph/direct/Geometry.cpp b/src/win/graphics/direct/Geometry.cpp index e77b4749..8aa961b2 100644 --- a/src/win/graph/direct/Geometry.cpp +++ b/src/win/graphics/direct/Geometry.cpp @@ -1,10 +1,10 @@ -#include "cru/win/graph/direct/Geometry.hpp" +#include "cru/win/graphics/direct/Geometry.hpp" -#include "cru/win/graph/direct/ConvertUtil.hpp" -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Factory.hpp" +#include "cru/win/graphics/direct/ConvertUtil.hpp" +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Factory.hpp" -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { D2DGeometryBuilder::D2DGeometryBuilder(DirectGraphFactory* factory) : DirectGraphResource(factory) { ThrowIfFailed(factory->GetD2D1Factory()->CreatePathGeometry(&geometry_)); @@ -59,4 +59,4 @@ bool D2DGeometry::FillContains(const Point& point) { Convert(point), D2D1::Matrix3x2F::Identity(), &result)); return result != 0; } -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/graph/direct/Painter.cpp b/src/win/graphics/direct/Painter.cpp index 3ffb5208..d6999cfa 100644 --- a/src/win/graph/direct/Painter.cpp +++ b/src/win/graphics/direct/Painter.cpp @@ -1,15 +1,15 @@ -#include "cru/win/graph/direct/Painter.hpp" +#include "cru/win/graphics/direct/Painter.hpp" #include "cru/platform/Check.hpp" -#include "cru/win/graph/direct/Brush.hpp" -#include "cru/win/graph/direct/ConvertUtil.hpp" -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Geometry.hpp" -#include "cru/win/graph/direct/TextLayout.hpp" +#include "cru/win/graphics/direct/Brush.hpp" +#include "cru/win/graphics/direct/ConvertUtil.hpp" +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Geometry.hpp" +#include "cru/win/graphics/direct/TextLayout.hpp" #include <type_traits> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { D2DPainter::D2DPainter(ID2D1RenderTarget* render_target) { Expects(render_target); render_target_ = render_target; @@ -32,6 +32,14 @@ void D2DPainter::Clear(const Color& color) { render_target_->Clear(Convert(color)); } +void D2DPainter::DrawLine(const Point& start, const Point& end, IBrush* brush, + float width) { + CheckValidation(); + const auto b = CheckPlatform<ID2DBrush>(brush, GetPlatformId()); + render_target_->DrawLine(Convert(start), Convert(end), + b->GetD2DBrushInterface(), width); +} + void D2DPainter::StrokeRectangle(const Rect& rectangle, IBrush* brush, float width) { CheckValidation(); @@ -101,4 +109,4 @@ void D2DPainter::CheckValidation() { "Can't do that on painter after end drawing."); } } -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/graphics/direct/Resource.cpp b/src/win/graphics/direct/Resource.cpp new file mode 100644 index 00000000..2b4a0772 --- /dev/null +++ b/src/win/graphics/direct/Resource.cpp @@ -0,0 +1,12 @@ +#include "cru/win/graphics/direct/Resource.hpp" + +#include "cru/win/graphics/direct/Factory.hpp" + +namespace cru::platform::graphics::win::direct { +DirectGraphResource::DirectGraphResource(DirectGraphFactory* factory) + : factory_(factory) { + Expects(factory); +} + +IGraphFactory* DirectGraphResource::GetGraphFactory() { return factory_; } +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/graph/direct/TextLayout.cpp b/src/win/graphics/direct/TextLayout.cpp index 0d4a6392..0b3c68ca 100644 --- a/src/win/graph/direct/TextLayout.cpp +++ b/src/win/graphics/direct/TextLayout.cpp @@ -1,14 +1,14 @@ -#include "cru/win/graph/direct/TextLayout.hpp" +#include "cru/win/graphics/direct/TextLayout.hpp" #include "cru/common/Logger.hpp" #include "cru/platform/Check.hpp" -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Factory.hpp" -#include "cru/win/graph/direct/Font.hpp" +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Factory.hpp" +#include "cru/win/graphics/direct/Font.hpp" #include <utility> -namespace cru::platform::graph::win::direct { +namespace cru::platform::graphics::win::direct { DWriteTextLayout::DWriteTextLayout(DirectGraphFactory* factory, std::shared_ptr<IFont> font, std::u16string text) @@ -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<Rect> DWriteTextLayout::TextRangeRect( @@ -121,4 +124,4 @@ TextHitTestResult DWriteTextLayout::HitTest(const Point& point) { result.insideText = inside != 0; return result; } -} // namespace cru::platform::graph::win::direct +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/graphics/direct/WindowPainter.cpp b/src/win/graphics/direct/WindowPainter.cpp new file mode 100644 index 00000000..c88667b6 --- /dev/null +++ b/src/win/graphics/direct/WindowPainter.cpp @@ -0,0 +1,20 @@ +#include "cru/win/graphics/direct/WindowPainter.hpp" + +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Factory.hpp" +#include "cru/win/graphics/direct/WindowRenderTarget.hpp" + +namespace cru::platform::graphics::win::direct { +D2DWindowPainter::D2DWindowPainter(D2DWindowRenderTarget* render_target) + : D2DPainter(render_target->GetD2D1DeviceContext()), + render_target_(render_target) { + render_target_->GetD2D1DeviceContext()->BeginDraw(); +} + +D2DWindowPainter::~D2DWindowPainter() { EndDraw(); } + +void D2DWindowPainter::DoEndDraw() { + ThrowIfFailed(render_target_->GetD2D1DeviceContext()->EndDraw()); + render_target_->Present(); +} +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/native/WindowRenderTarget.cpp b/src/win/graphics/direct/WindowRenderTarget.cpp index 4a114ebf..7479ae24 100644 --- a/src/win/native/WindowRenderTarget.cpp +++ b/src/win/graphics/direct/WindowRenderTarget.cpp @@ -1,19 +1,17 @@ -#include "cru/win/native/WindowRenderTarget.hpp" +#include "cru/win/graphics/direct/WindowRenderTarget.hpp" -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Factory.hpp" -#include "DpiUtil.hpp" - -namespace cru::platform::native::win { -using namespace cru::platform::graph::win::direct; -WindowRenderTarget::WindowRenderTarget(DirectGraphFactory* factory, HWND hwnd) - : factory_(factory) { - Expects(factory); +#include "cru/win/graphics/direct/Exception.hpp" +#include "cru/win/graphics/direct/Factory.hpp" +namespace cru::platform::graphics::win::direct { +D2DWindowRenderTarget::D2DWindowRenderTarget( + gsl::not_null<DirectGraphFactory*> factory, HWND hwnd) + : factory_(factory), hwnd_(hwnd) { const auto d3d11_device = factory->GetD3D11Device(); const auto dxgi_factory = factory->GetDxgiFactory(); d2d1_device_context_ = factory->CreateD2D1DeviceContext(); + d2d1_device_context_->SetUnitMode(D2D1_UNIT_MODE_DIPS); // Allocate a descriptor. DXGI_SWAP_CHAIN_DESC1 swap_chain_desc; @@ -40,7 +38,11 @@ WindowRenderTarget::WindowRenderTarget(DirectGraphFactory* factory, HWND hwnd) CreateTargetBitmap(); } -void WindowRenderTarget::ResizeBuffer(const int width, const int height) { +void D2DWindowRenderTarget::SetDpi(float x, float y) { + d2d1_device_context_->SetDpi(x, y); +} + +void D2DWindowRenderTarget::ResizeBuffer(const int width, const int height) { // In order to resize buffer, we need to untarget the buffer first. d2d1_device_context_->SetTarget(nullptr); target_bitmap_ = nullptr; @@ -49,11 +51,11 @@ void WindowRenderTarget::ResizeBuffer(const int width, const int height) { CreateTargetBitmap(); } -void WindowRenderTarget::Present() { +void D2DWindowRenderTarget::Present() { ThrowIfFailed(dxgi_swap_chain_->Present(1, 0)); } -void WindowRenderTarget::CreateTargetBitmap() { +void D2DWindowRenderTarget::CreateTargetBitmap() { Expects(target_bitmap_ == nullptr); // target bitmap must not exist. // Direct2D needs the dxgi version of the backbuffer surface pointer. @@ -61,12 +63,13 @@ void WindowRenderTarget::CreateTargetBitmap() { ThrowIfFailed( dxgi_swap_chain_->GetBuffer(0, IID_PPV_ARGS(&dxgi_back_buffer))); - const auto dpi = GetDpi(); // TODO! DPI awareness. + float dpi_x, dpi_y; + d2d1_device_context_->GetDpi(&dpi_x, &dpi_y); auto bitmap_properties = D2D1::BitmapProperties1( D2D1_BITMAP_OPTIONS_TARGET | D2D1_BITMAP_OPTIONS_CANNOT_DRAW, D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE), - dpi.x, dpi.y); + dpi_x, dpi_y); // Get a D2D surface from the DXGI back buffer to use as the D2D render // target. @@ -75,4 +78,4 @@ void WindowRenderTarget::CreateTargetBitmap() { d2d1_device_context_->SetTarget(target_bitmap_.Get()); } -} // namespace cru::platform::native::win +} // namespace cru::platform::graphics::win::direct diff --git a/src/win/gui/CMakeLists.txt b/src/win/gui/CMakeLists.txt new file mode 100644 index 00000000..48bed00d --- /dev/null +++ b/src/win/gui/CMakeLists.txt @@ -0,0 +1,31 @@ +set(CRU_WIN_GUI_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/win/gui) + +add_library(cru_win_gui STATIC + TimerManager.hpp + WindowManager.hpp + + Cursor.cpp + GodWindow.cpp + InputMethod.cpp + Keyboard.cpp + TimerManager.cpp + UiApplication.cpp + Window.cpp + WindowClass.cpp + WindowManager.cpp +) +target_sources(cru_win_gui PUBLIC + ${CRU_WIN_GUI_INCLUDE_DIR}/Cursor.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/Exception.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/Base.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/GodWindow.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/InputMethod.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/Keyboard.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/Resource.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/UiApplication.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/Window.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/WindowClass.hpp + ${CRU_WIN_GUI_INCLUDE_DIR}/WindowNativeMessageEventArgs.hpp +) +target_link_libraries(cru_win_gui PUBLIC imm32) +target_link_libraries(cru_win_gui PUBLIC cru_win_graphics_direct cru_platform_gui) diff --git a/src/win/native/Cursor.cpp b/src/win/gui/Cursor.cpp index 429f6e7c..80e8a749 100644 --- a/src/win/native/Cursor.cpp +++ b/src/win/gui/Cursor.cpp @@ -1,11 +1,11 @@ -#include "cru/win/native/Cursor.hpp" +#include "cru/win/gui/Cursor.hpp" #include "cru/common/Logger.hpp" -#include "cru/win/native/Exception.hpp" +#include "cru/win/gui/Exception.hpp" #include <stdexcept> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { WinCursor::WinCursor(HCURSOR handle, bool auto_destroy) { handle_ = handle; auto_destroy_ = auto_destroy; @@ -35,7 +35,8 @@ WinCursor* LoadWinCursor(const wchar_t* name) { WinCursorManager::WinCursorManager() : sys_arrow_(LoadWinCursor(IDC_ARROW)), - sys_hand_(LoadWinCursor(IDC_HAND)) {} + sys_hand_(LoadWinCursor(IDC_HAND)), + sys_ibeam_(LoadWinCursor(IDC_IBEAM)) {} std::shared_ptr<WinCursor> WinCursorManager::GetSystemWinCursor( SystemCursorType type) { @@ -44,8 +45,10 @@ std::shared_ptr<WinCursor> WinCursorManager::GetSystemWinCursor( return sys_arrow_; case SystemCursorType::Hand: return sys_hand_; + case SystemCursorType::IBeam: + return sys_ibeam_; default: throw std::runtime_error("Unknown system cursor value."); } } -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/GodWindow.cpp b/src/win/gui/GodWindow.cpp index b1e7275e..7bce83a3 100644 --- a/src/win/native/GodWindow.cpp +++ b/src/win/gui/GodWindow.cpp @@ -1,13 +1,11 @@ -#include "cru/win/native/GodWindow.hpp" +#include "cru/win/gui/GodWindow.hpp" -#include "GodWindowMessage.hpp" -#include "Timer.hpp" #include "cru/common/Logger.hpp" -#include "cru/win/native/Exception.hpp" -#include "cru/win/native/UiApplication.hpp" -#include "cru/win/native/WindowClass.hpp" +#include "cru/win/gui/Exception.hpp" +#include "cru/win/gui/UiApplication.hpp" +#include "cru/win/gui/WindowClass.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { constexpr auto god_window_class_name = L"GodWindowClass"; LRESULT CALLBACK GodWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, @@ -16,14 +14,14 @@ LRESULT CALLBACK GodWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, if (app) { LRESULT result; - const auto handled = app->GetGodWindow()->HandleGodWindowMessage( - hWnd, uMsg, wParam, lParam, &result); - if (handled) - return result; - else - return DefWindowProcW(hWnd, uMsg, wParam, lParam); - } else - return DefWindowProcW(hWnd, uMsg, wParam, lParam); + auto god_window = app->GetGodWindow(); + if (god_window != nullptr) { + const auto handled = god_window->HandleGodWindowMessage( + hWnd, uMsg, wParam, lParam, &result); + if (handled) return result; + } + } + return DefWindowProcW(hWnd, uMsg, wParam, lParam); } GodWindow::GodWindow(WinUiApplication* application) { @@ -51,32 +49,15 @@ GodWindow::~GodWindow() { bool GodWindow::HandleGodWindowMessage(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param, LRESULT* result) { - CRU_UNUSED(hwnd) - CRU_UNUSED(l_param) + WindowNativeMessageEventArgs args( + WindowNativeMessage{hwnd, msg, w_param, l_param}); + message_event_.Raise(args); - switch (msg) { - case invoke_later_message_id: { - const auto p_action = reinterpret_cast<std::function<void()>*>(w_param); - (*p_action)(); - delete p_action; - *result = 0; - return true; - } - case WM_TIMER: { - const auto id = static_cast<UINT_PTR>(w_param); - const auto action = application_->GetTimerManager()->GetAction(id); - if (action.has_value()) { - (action.value().second)(); - if (!action.value().first) - application_->GetTimerManager()->KillTimer(id); - result = 0; - return true; - } - break; - } - default: - return false; + if (args.IsHandled()) { + *result = args.GetResult(); + return true; } + return false; } -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/InputMethod.cpp b/src/win/gui/InputMethod.cpp index 21681de2..cc237e88 100644 --- a/src/win/native/InputMethod.cpp +++ b/src/win/gui/InputMethod.cpp @@ -1,15 +1,15 @@ -#include "cru/win/native/InputMethod.hpp" +#include "cru/win/gui/InputMethod.hpp" -#include "DpiUtil.hpp" #include "cru/common/Logger.hpp" #include "cru/common/StringUtil.hpp" #include "cru/platform/Check.hpp" +#include "cru/platform/gui/DebugFlags.hpp" #include "cru/win/Exception.hpp" -#include "cru/win/native/Window.hpp" +#include "cru/win/gui/Window.hpp" #include <vector> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { AutoHIMC::AutoHIMC(HWND hwnd) : hwnd_(hwnd) { Expects(hwnd); handle_ = ::ImmGetContext(hwnd); @@ -146,36 +146,26 @@ CompositionText GetCompositionInfo(HIMC imm_context) { WinInputMethodContext::WinInputMethodContext( gsl::not_null<WinNativeWindow*> window) - : native_window_resolver_(window->GetResolver()) { - event_revoker_guards_.push_back( - EventRevokerGuard(window->NativeMessageEvent()->AddHandler( - std::bind(&WinInputMethodContext::OnWindowNativeMessage, this, - std::placeholders::_1)))); + : native_window_(window) { + event_guard_ += window->NativeMessageEvent()->AddHandler( + std::bind(&WinInputMethodContext::OnWindowNativeMessage, this, + std::placeholders::_1)); } WinInputMethodContext::~WinInputMethodContext() {} void WinInputMethodContext::EnableIME() { - const auto native_window = Resolve(native_window_resolver_.get()); - if (native_window == nullptr) return; - const auto hwnd = native_window->GetWindowHandle(); - + const auto hwnd = native_window_->GetWindowHandle(); if (::ImmAssociateContextEx(hwnd, nullptr, IACE_DEFAULT) == FALSE) { log::TagWarn(log_tag, u"Failed to enable ime."); } } void WinInputMethodContext::DisableIME() { - const auto native_window = Resolve(native_window_resolver_.get()); - if (native_window == nullptr) return; - const auto hwnd = native_window->GetWindowHandle(); - + const auto hwnd = native_window_->GetWindowHandle(); AutoHIMC himc{hwnd}; - if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0)) { - log::TagWarn(log_tag, - u"Failed to complete composition before disable ime."); - } + ::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0); if (::ImmAssociateContextEx(hwnd, nullptr, 0) == FALSE) { log::TagWarn(log_tag, u"Failed to disable ime."); @@ -183,42 +173,32 @@ void WinInputMethodContext::DisableIME() { } void WinInputMethodContext::CompleteComposition() { - auto optional_himc = TryGetHIMC(); - if (!optional_himc.has_value()) return; - auto himc = *std::move(optional_himc); - + auto himc = GetHIMC(); if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0)) { log::TagWarn(log_tag, u"Failed to complete composition."); } } void WinInputMethodContext::CancelComposition() { - auto optional_himc = TryGetHIMC(); - if (!optional_himc.has_value()) return; - auto himc = *std::move(optional_himc); - + auto himc = GetHIMC(); if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0)) { log::TagWarn(log_tag, u"Failed to complete composition."); } } CompositionText WinInputMethodContext::GetCompositionText() { - auto optional_himc = TryGetHIMC(); - if (!optional_himc.has_value()) return CompositionText{}; - auto himc = *std::move(optional_himc); - + auto himc = GetHIMC(); return GetCompositionInfo(himc.Get()); } void WinInputMethodContext::SetCandidateWindowPosition(const Point& point) { - auto optional_himc = TryGetHIMC(); - if (!optional_himc.has_value()) return; - auto himc = *std::move(optional_himc); + auto himc = GetHIMC(); ::CANDIDATEFORM form; - form.dwIndex = 1; + form.dwIndex = 0; form.dwStyle = CFS_CANDIDATEPOS; - form.ptCurrentPos = DipToPi(point); + + form.ptCurrentPos = native_window_->DipToPixel(point); if (!::ImmSetCandidateWindow(himc.Get(), &form)) log::TagDebug(log_tag, @@ -246,7 +226,7 @@ void WinInputMethodContext::OnWindowNativeMessage( const auto& message = args.GetWindowMessage(); switch (message.msg) { case WM_CHAR: { - const auto c = static_cast<char16_t>(message.w_param); + auto c = static_cast<char16_t>(message.w_param); if (IsUtf16SurrogatePairCodeUnit(c)) { // I don't think this will happen because normal key strike without ime // should only trigger ascci character. If it is a charater from @@ -255,8 +235,12 @@ void WinInputMethodContext::OnWindowNativeMessage( u"A WM_CHAR message for character from supplementary " u"planes is ignored."); } else { - char16_t s[1] = {c}; - text_event_.Raise({s, 1}); + if (c != '\b') { // ignore backspace + if (c == '\r') c = '\n'; // Change \r to \n + + char16_t s[1] = {c}; + text_event_.Raise({s, 1}); + } } args.HandleWithResult(0); break; @@ -264,8 +248,10 @@ void WinInputMethodContext::OnWindowNativeMessage( case WM_IME_COMPOSITION: { composition_event_.Raise(nullptr); auto composition_text = GetCompositionText(); - log::TagDebug(log_tag, u"WM_IME_COMPOSITION composition text:\n{}", - composition_text); + if constexpr (DebugFlags::input_method) { + log::TagDebug(log_tag, u"WM_IME_COMPOSITION composition text:\n{}", + composition_text); + } if (message.l_param & GCS_RESULTSTR) { auto result_string = GetResultString(); text_event_.Raise(result_string); @@ -273,10 +259,16 @@ void WinInputMethodContext::OnWindowNativeMessage( break; } case WM_IME_STARTCOMPOSITION: { + if constexpr (DebugFlags::input_method) { + log::TagDebug(log_tag, u"WM_IME_STARTCOMPOSITION received."); + } composition_start_event_.Raise(nullptr); break; } case WM_IME_ENDCOMPOSITION: { + if constexpr (DebugFlags::input_method) { + log::TagDebug(log_tag, u"WM_IME_ENDCOMPOSITION received."); + } composition_end_event_.Raise(nullptr); break; } @@ -284,29 +276,13 @@ void WinInputMethodContext::OnWindowNativeMessage( } std::u16string WinInputMethodContext::GetResultString() { - auto optional_himc = TryGetHIMC(); - if (!optional_himc.has_value()) return u""; - auto himc = *std::move(optional_himc); - + auto himc = GetHIMC(); auto result = win::GetResultString(himc.Get()); return result; } -std::optional<AutoHIMC> WinInputMethodContext::TryGetHIMC() { - const auto native_window = Resolve(native_window_resolver_.get()); - if (native_window == nullptr) return std::nullopt; - const auto hwnd = native_window->GetWindowHandle(); +AutoHIMC WinInputMethodContext::GetHIMC() { + const auto hwnd = native_window_->GetWindowHandle(); return AutoHIMC{hwnd}; } - -WinInputMethodManager::WinInputMethodManager(WinUiApplication*) {} - -WinInputMethodManager::~WinInputMethodManager() {} - -std::unique_ptr<IInputMethodContext> WinInputMethodManager::GetContext( - INativeWindow* window) { - Expects(window); - const auto w = CheckPlatform<WinNativeWindow>(window, GetPlatformId()); - return std::make_unique<WinInputMethodContext>(w); -} -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/Keyboard.cpp b/src/win/gui/Keyboard.cpp index 929ca737..b706b240 100644 --- a/src/win/native/Keyboard.cpp +++ b/src/win/gui/Keyboard.cpp @@ -1,6 +1,6 @@ -#include "cru/win/native/Keyboard.hpp" +#include "cru/win/gui/Keyboard.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { KeyCode VirtualKeyToKeyCode(int virtual_key) { if (virtual_key >= 0x30 && virtual_key <= 0x39) { return KeyCode{static_cast<int>(KeyCode::N0) + (virtual_key - 0x30)}; @@ -71,4 +71,4 @@ KeyModifier RetrieveKeyMofifier() { if (::GetKeyState(VK_MENU) < 0) result |= KeyModifiers::alt; return result; } -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/gui/TimerManager.cpp b/src/win/gui/TimerManager.cpp new file mode 100644 index 00000000..fc26b6c4 --- /dev/null +++ b/src/win/gui/TimerManager.cpp @@ -0,0 +1,100 @@ +#include "TimerManager.hpp" + +#include "cru/win/gui/Base.hpp" +#include "cru/win/gui/Exception.hpp" +#include "gsl/gsl_util" + +#include <functional> +#include <type_traits> + +namespace cru::platform::gui::win { +constexpr int kSetImmediateWindowMessageId = WM_USER + 2000; + +TimerManager::TimerManager(GodWindow* god_window) { + god_window_ = god_window; + event_guard_ += god_window->MessageEvent()->AddHandler(std::bind( + &TimerManager::HandleGodWindowMessage, this, std::placeholders::_1)); +} + +long long TimerManager::SetTimer(TimerType type, int period, + std::function<void()> action) { + auto id = next_id_++; + TimerInfo timer_info{id, type, type == TimerType::Immediate ? 0 : period, + std::move(action)}; + if (type == TimerType::Immediate) { + if (!::PostMessageW(god_window_->GetHandle(), kSetImmediateWindowMessageId, + gsl::narrow<UINT_PTR>(id), 0)) { + throw Win32Error( + ::GetLastError(), + "Failed to post window message to god window for set immediate."); + } + } else { + CreateNativeTimer(&timer_info); + } + + info_map_.emplace(id, std::move(timer_info)); + return id; +} + +void TimerManager::CancelTimer(long long id) { + if (id <= 0) return; + auto find_result = this->info_map_.find(id); + if (find_result != info_map_.cend()) { + auto& info = find_result->second; + KillNativeTimer(&info); + this->info_map_.erase(find_result); + } +} + +void TimerManager::CreateNativeTimer(TimerInfo* info) { + info->native_timer_id = gsl::narrow<UINT_PTR>(info->id); + ::SetTimer(god_window_->GetHandle(), info->native_timer_id, info->period, + nullptr); +} + +void TimerManager::KillNativeTimer(TimerInfo* info) { + if (info->native_timer_id == 0) return; + ::KillTimer(god_window_->GetHandle(), info->native_timer_id); + info->native_timer_id = 0; +} + +void TimerManager::HandleGodWindowMessage(WindowNativeMessageEventArgs& args) { + const auto& message = args.GetWindowMessage(); + + switch (message.msg) { + case kSetImmediateWindowMessageId: { + auto find_result = + this->info_map_.find(static_cast<long long>(message.w_param)); + if (find_result != info_map_.cend()) { + auto& info = find_result->second; + info.action(); + info_map_.erase(find_result); + } + args.SetResult(0); + args.SetHandled(true); + return; + } + case WM_TIMER: { + auto find_result = + this->info_map_.find(static_cast<long long>(message.w_param)); + if (find_result != info_map_.cend()) { + auto& info = find_result->second; + if (info.type == TimerType::Interval) { + info.action(); + args.SetResult(0); + args.SetHandled(true); + } else if (info.type == TimerType::Timeout) { + info.action(); + KillNativeTimer(&info); + info_map_.erase(find_result); + args.SetResult(0); + args.SetHandled(true); + } + } + return; + } + default: + return; + } +} +} // namespace cru::platform::gui::win diff --git a/src/win/gui/TimerManager.hpp b/src/win/gui/TimerManager.hpp new file mode 100644 index 00000000..a8db1075 --- /dev/null +++ b/src/win/gui/TimerManager.hpp @@ -0,0 +1,61 @@ +#pragma once +#include "cru/common/Event.hpp" +#include "cru/win/WinPreConfig.hpp" + +#include "cru/common/Base.hpp" +#include "cru/win/gui/GodWindow.hpp" +#include "cru/win/gui/WindowNativeMessageEventArgs.hpp" + +#include <chrono> +#include <functional> +#include <optional> +#include <unordered_map> + +namespace cru::platform::gui::win { +enum class TimerType { Immediate, Timeout, Interval }; + +struct TimerInfo { + TimerInfo(long long id, TimerType type, int period, + std::function<void()> action, UINT_PTR native_timer_id = 0) + : id(id), + type(type), + period(period), + action(std::move(action)), + native_timer_id(native_timer_id) {} + + long long id; + TimerType type; + int period; // in milliseconds + std::function<void()> action; + UINT_PTR native_timer_id; +}; + +class TimerManager : public Object { + public: + TimerManager(GodWindow* god_window); + + CRU_DELETE_COPY(TimerManager) + CRU_DELETE_MOVE(TimerManager) + + ~TimerManager() override = default; + + // Period is in milliseconds. When type is immediate, it is not checked and + // used. + long long SetTimer(TimerType type, int period, std::function<void()> action); + void CancelTimer(long long id); + + private: + void HandleGodWindowMessage(WindowNativeMessageEventArgs& args); + + void CreateNativeTimer(TimerInfo* info); + void KillNativeTimer(TimerInfo* info); + + private: + GodWindow* god_window_; + + EventRevokerListGuard event_guard_; + + long long next_id_ = 1; + std::unordered_map<long long, TimerInfo> info_map_; +}; +} // namespace cru::platform::gui::win diff --git a/src/win/native/UiApplication.cpp b/src/win/gui/UiApplication.cpp index dbf52e05..f4541dd0 100644 --- a/src/win/native/UiApplication.cpp +++ b/src/win/gui/UiApplication.cpp @@ -1,25 +1,25 @@ -#include "cru/win/native/UiApplication.hpp" +#include "cru/win/gui/UiApplication.hpp" #include "../DebugLogger.hpp" +#include "../StdOutLogger.hpp" +#include "TimerManager.hpp" +#include "WindowManager.hpp" #include "cru/common/Logger.hpp" #include "cru/platform/Check.hpp" -#include "cru/win/graph/direct/Factory.hpp" -#include "cru/win/native/Cursor.hpp" -#include "cru/win/native/Exception.hpp" -#include "cru/win/native/GodWindow.hpp" -#include "cru/win/native/InputMethod.hpp" -#include "cru/win/native/Window.hpp" -#include "GodWindowMessage.hpp" -#include "Timer.hpp" -#include "WindowManager.hpp" - -namespace cru::platform::native { +#include "cru/win/graphics/direct/Factory.hpp" +#include "cru/win/gui/Cursor.hpp" +#include "cru/win/gui/Exception.hpp" +#include "cru/win/gui/GodWindow.hpp" +#include "cru/win/gui/InputMethod.hpp" +#include "cru/win/gui/Window.hpp" + +namespace cru::platform::gui { std::unique_ptr<IUiApplication> CreateUiApplication() { return std::make_unique<win::WinUiApplication>(); } -} // namespace cru::platform::native +} // namespace cru::platform::gui -namespace cru::platform::native::win { +namespace cru::platform::gui::win { WinUiApplication* WinUiApplication::instance = nullptr; WinUiApplication::WinUiApplication() { @@ -29,17 +29,20 @@ WinUiApplication::WinUiApplication() { if (!instance_handle_) throw Win32Error("Failed to get module(instance) handle."); + ::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + log::Logger::GetInstance()->AddSource( std::make_unique<::cru::platform::win::WinDebugLoggerSource>()); + log::Logger::GetInstance()->AddSource( + std::make_unique<::cru::platform::win::WinStdOutLoggerSource>()); - graph_factory_ = - std::make_unique<cru::platform::graph::win::direct::DirectGraphFactory>(); + graph_factory_ = std::make_unique< + cru::platform::graphics::win::direct::DirectGraphFactory>(); god_window_ = std::make_unique<GodWindow>(this); timer_manager_ = std::make_unique<TimerManager>(god_window_.get()); window_manager_ = std::make_unique<WindowManager>(this); cursor_manager_ = std::make_unique<WinCursorManager>(); - input_method_manager_ = std::make_unique<WinInputMethodManager>(this); } WinUiApplication::~WinUiApplication() { instance = nullptr; } @@ -64,30 +67,27 @@ void WinUiApplication::AddOnQuitHandler(std::function<void()> handler) { quit_handlers_.push_back(std::move(handler)); } -void WinUiApplication::InvokeLater(std::function<void()> action) { - // copy the action to a safe place - auto p_action_copy = new std::function<void()>(std::move(action)); - - if (::PostMessageW(GetGodWindow()->GetHandle(), invoke_later_message_id, - reinterpret_cast<WPARAM>(p_action_copy), 0) == 0) - throw Win32Error(::GetLastError(), "InvokeLater failed to post message."); +long long WinUiApplication::SetImmediate(std::function<void()> action) { + return this->timer_manager_->SetTimer(TimerType::Immediate, 0, + std::move(action)); } long long WinUiApplication::SetTimeout(std::chrono::milliseconds milliseconds, std::function<void()> action) { - return gsl::narrow<long long>(timer_manager_->CreateTimer( - static_cast<UINT>(milliseconds.count()), false, std::move(action))); + return this->timer_manager_->SetTimer(TimerType::Timeout, + gsl::narrow<int>(milliseconds.count()), + std::move(action)); } long long WinUiApplication::SetInterval(std::chrono::milliseconds milliseconds, std::function<void()> action) { - return gsl::narrow<long long>(timer_manager_->CreateTimer( - static_cast<UINT>(milliseconds.count()), true, std::move(action))); + return this->timer_manager_->SetTimer(TimerType::Interval, + gsl::narrow<int>(milliseconds.count()), + std::move(action)); } void WinUiApplication::CancelTimer(long long id) { - if (id < 0) return; - timer_manager_->KillTimer(static_cast<UINT_PTR>(id)); + timer_manager_->CancelTimer(id); } std::vector<INativeWindow*> WinUiApplication::GetAllWindow() { @@ -99,26 +99,24 @@ std::vector<INativeWindow*> WinUiApplication::GetAllWindow() { return result; } -std::shared_ptr<INativeWindowResolver> WinUiApplication::CreateWindow( - INativeWindow* parent) { +INativeWindow* WinUiApplication::CreateWindow(INativeWindow* parent, + CreateWindowFlag flag) { WinNativeWindow* p = nullptr; if (parent != nullptr) { p = CheckPlatform<WinNativeWindow>(parent, GetPlatformId()); } - return (new WinNativeWindow(this, window_manager_->GetGeneralWindowClass(), - WS_OVERLAPPEDWINDOW, p)) - ->GetResolver(); + return new WinNativeWindow(this, window_manager_->GetGeneralWindowClass(), + flag & CreateWindowFlags::NoCaptionAndBorder + ? WS_POPUP + : WS_OVERLAPPEDWINDOW, + p); } -cru::platform::graph::IGraphFactory* WinUiApplication::GetGraphFactory() { +cru::platform::graphics::IGraphFactory* WinUiApplication::GetGraphFactory() { return graph_factory_.get(); } ICursorManager* WinUiApplication::GetCursorManager() { return cursor_manager_.get(); } - -IInputMethodManager* WinUiApplication::GetInputMethodManager() { - return input_method_manager_.get(); -} -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/Window.cpp b/src/win/gui/Window.cpp index 81642451..efd3bfcc 100644 --- a/src/win/native/Window.cpp +++ b/src/win/gui/Window.cpp @@ -1,27 +1,27 @@ -#include "cru/win/native/Window.hpp" +#include "cru/win/gui/Window.hpp" -#include "DpiUtil.hpp" -#include "WindowD2DPainter.hpp" #include "WindowManager.hpp" #include "cru/common/Logger.hpp" #include "cru/platform/Check.hpp" -#include "cru/win/native/Cursor.hpp" -#include "cru/win/native/Exception.hpp" -#include "cru/win/native/Keyboard.hpp" -#include "cru/win/native/UiApplication.hpp" -#include "cru/win/native/WindowClass.hpp" -#include "cru/win/native/WindowRenderTarget.hpp" +#include "cru/platform/gui/Base.hpp" +#include "cru/platform/gui/DebugFlags.hpp" +#include "cru/win/graphics/direct/WindowPainter.hpp" +#include "cru/win/gui/Cursor.hpp" +#include "cru/win/gui/Exception.hpp" +#include "cru/win/gui/InputMethod.hpp" +#include "cru/win/gui/Keyboard.hpp" +#include "cru/win/gui/UiApplication.hpp" +#include "cru/win/gui/WindowClass.hpp" #include <imm.h> #include <windowsx.h> +#include <memory> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { WinNativeWindow::WinNativeWindow(WinUiApplication* application, WindowClass* window_class, DWORD window_style, WinNativeWindow* parent) - : application_(application), - resolver_(std::make_shared<WinNativeWindowResolver>(this)), - parent_window_(parent) { + : application_(application), parent_window_(parent) { Expects(application); // application can't be null. if (parent != nullptr) { @@ -39,13 +39,24 @@ WinNativeWindow::WinNativeWindow(WinUiApplication* application, if (hwnd_ == nullptr) throw Win32Error(::GetLastError(), "Failed to create window."); + auto dpi = ::GetDpiForWindow(hwnd_); + if (dpi == 0) + throw Win32Error(::GetLastError(), "Failed to get dpi of window."); + dpi_ = static_cast<float>(dpi); + log::Debug(u"Dpi of window is {}.", dpi_); + window_manager->RegisterWindow(hwnd_, this); SetCursor(application->GetCursorManager()->GetSystemCursor( - cru::platform::native::SystemCursorType::Arrow)); + cru::platform::gui::SystemCursorType::Arrow)); + + window_render_target_ = + std::make_unique<graphics::win::direct::D2DWindowRenderTarget>( + application->GetDirectFactory(), hwnd_); + window_render_target_->SetDpi(dpi_, dpi_); - window_render_target_ = std::make_unique<WindowRenderTarget>( - application->GetDirectFactory(), hwnd_); + input_method_context_ = std::make_unique<WinInputMethodContext>(this); + input_method_context_->DisableIME(); } WinNativeWindow::~WinNativeWindow() { @@ -53,7 +64,6 @@ WinNativeWindow::~WinNativeWindow() { sync_flag_ = true; Close(); } - resolver_->Reset(); } void WinNativeWindow::Close() { ::DestroyWindow(hwnd_); } @@ -65,7 +75,7 @@ void WinNativeWindow::SetVisible(bool is_visible) { } Size WinNativeWindow::GetClientSize() { const auto pixel_rect = GetClientRectPixel(); - return Size(PixelToDipX(pixel_rect.right), PixelToDipY(pixel_rect.bottom)); + return Size(PixelToDip(pixel_rect.right), PixelToDip(pixel_rect.bottom)); } void WinNativeWindow::SetClientSize(const Size& size) { @@ -77,8 +87,8 @@ void WinNativeWindow::SetClientSize(const Size& size) { RECT rect; rect.left = 0; rect.top = 0; - rect.right = DipToPixelX(size.width); - rect.bottom = DipToPixelY(size.height); + rect.right = DipToPixel(size.width); + rect.bottom = DipToPixel(size.height); if (!AdjustWindowRectEx(&rect, window_style, FALSE, window_ex_style)) throw Win32Error(::GetLastError(), "Failed to invoke AdjustWindowRectEx."); @@ -92,14 +102,14 @@ Rect WinNativeWindow::GetWindowRect() { if (!::GetWindowRect(hwnd_, &rect)) throw Win32Error(::GetLastError(), "Failed to invoke GetWindowRect."); - return Rect::FromVertices(PixelToDipX(rect.left), PixelToDipY(rect.top), - PixelToDipX(rect.right), PixelToDipY(rect.bottom)); + return Rect::FromVertices(PixelToDip(rect.left), PixelToDip(rect.top), + PixelToDip(rect.right), PixelToDip(rect.bottom)); } void WinNativeWindow::SetWindowRect(const Rect& rect) { - if (!SetWindowPos(hwnd_, nullptr, DipToPixelX(rect.left), - DipToPixelY(rect.top), DipToPixelX(rect.GetRight()), - DipToPixelY(rect.GetBottom()), SWP_NOZORDER)) + if (!SetWindowPos(hwnd_, nullptr, DipToPixel(rect.left), DipToPixel(rect.top), + DipToPixel(rect.GetRight()), DipToPixel(rect.GetBottom()), + SWP_NOZORDER)) throw Win32Error(::GetLastError(), "Failed to invoke SetWindowPos."); } @@ -109,7 +119,7 @@ Point WinNativeWindow::GetMousePosition() { throw Win32Error(::GetLastError(), "Failed to get cursor position."); if (!::ScreenToClient(hwnd_, &p)) throw Win32Error(::GetLastError(), "Failed to call ScreenToClient."); - return PiToDip(p); + return PixelToDip(p); } bool WinNativeWindow::CaptureMouse() { @@ -123,15 +133,18 @@ bool WinNativeWindow::ReleaseMouse() { } void WinNativeWindow::RequestRepaint() { - log::TagDebug(log_tag, u"A repaint is requested."); + if constexpr (DebugFlags::paint) { + log::TagDebug(log_tag, u"A repaint is requested."); + } if (!::InvalidateRect(hwnd_, nullptr, FALSE)) throw Win32Error(::GetLastError(), "Failed to invalidate window."); if (!::UpdateWindow(hwnd_)) throw Win32Error(::GetLastError(), "Failed to update window."); } -std::unique_ptr<graph::IPainter> WinNativeWindow::BeginPaint() { - return std::make_unique<WindowD2DPainter>(window_render_target_.get()); +std::unique_ptr<graphics::IPainter> WinNativeWindow::BeginPaint() { + return std::make_unique<graphics::win::direct::D2DWindowPainter>( + window_render_target_.get()); } void WinNativeWindow::SetCursor(std::shared_ptr<ICursor> cursor) { @@ -190,6 +203,10 @@ void WinNativeWindow::SetCursor(std::shared_ptr<ICursor> cursor) { } } +IInputMethodContext* WinNativeWindow::GetInputMethodContext() { + return static_cast<IInputMethodContext*>(input_method_context_.get()); +} + bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, WPARAM w_param, LPARAM l_param, LRESULT* result) { @@ -229,7 +246,7 @@ bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, POINT point; point.x = GET_X_LPARAM(l_param); point.y = GET_Y_LPARAM(l_param); - OnMouseDownInternal(platform::native::mouse_buttons::left, point); + OnMouseDownInternal(platform::gui::mouse_buttons::left, point); *result = 0; return true; } @@ -237,7 +254,7 @@ bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, POINT point; point.x = GET_X_LPARAM(l_param); point.y = GET_Y_LPARAM(l_param); - OnMouseUpInternal(platform::native::mouse_buttons::left, point); + OnMouseUpInternal(platform::gui::mouse_buttons::left, point); *result = 0; return true; } @@ -245,7 +262,7 @@ bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, POINT point; point.x = GET_X_LPARAM(l_param); point.y = GET_Y_LPARAM(l_param); - OnMouseDownInternal(platform::native::mouse_buttons::right, point); + OnMouseDownInternal(platform::gui::mouse_buttons::right, point); *result = 0; return true; } @@ -253,7 +270,7 @@ bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, POINT point; point.x = GET_X_LPARAM(l_param); point.y = GET_Y_LPARAM(l_param); - OnMouseUpInternal(platform::native::mouse_buttons::right, point); + OnMouseUpInternal(platform::gui::mouse_buttons::right, point); *result = 0; return true; } @@ -261,7 +278,7 @@ bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, POINT point; point.x = GET_X_LPARAM(l_param); point.y = GET_Y_LPARAM(l_param); - OnMouseDownInternal(platform::native::mouse_buttons::middle, point); + OnMouseDownInternal(platform::gui::mouse_buttons::middle, point); *result = 0; return true; } @@ -269,7 +286,7 @@ bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, POINT point; point.x = GET_X_LPARAM(l_param); point.y = GET_Y_LPARAM(l_param); - OnMouseUpInternal(platform::native::mouse_buttons::middle, point); + OnMouseUpInternal(platform::gui::mouse_buttons::middle, point); *result = 0; return true; } @@ -328,6 +345,15 @@ bool WinNativeWindow::HandleNativeWindowMessage(HWND hwnd, UINT msg, case WM_IME_COMPOSITION: *result = 0; return true; + case WM_DPICHANGED: { + dpi_ = static_cast<float>(LOWORD(w_param)); + const RECT* suggest_rect = reinterpret_cast<const RECT*>(l_param); + window_render_target_->SetDpi(dpi_, dpi_); + SetWindowPos(hwnd_, NULL, suggest_rect->left, suggest_rect->top, + suggest_rect->right - suggest_rect->left, + suggest_rect->bottom - suggest_rect->top, + SWP_NOZORDER | SWP_NOACTIVATE); + } default: return false; } @@ -341,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; @@ -353,14 +379,16 @@ void WinNativeWindow::OnDestroyInternal() { void WinNativeWindow::OnPaintInternal() { paint_event_.Raise(nullptr); ValidateRect(hwnd_, nullptr); - log::TagDebug(log_tag, u"A repaint is finished."); + if constexpr (DebugFlags::paint) { + log::TagDebug(log_tag, u"A repaint is finished."); + } } void WinNativeWindow::OnResizeInternal(const int new_width, const int new_height) { if (!(new_width == 0 && new_height == 0)) { window_render_target_->ResizeBuffer(new_width, new_height); - resize_event_.Raise(Size{PixelToDipX(new_width), PixelToDipY(new_height)}); + resize_event_.Raise(Size{PixelToDip(new_width), PixelToDip(new_height)}); } } @@ -389,7 +417,7 @@ void WinNativeWindow::OnMouseMoveInternal(const POINT point) { mouse_enter_leave_event_.Raise(MouseEnterLeaveType::Enter); } - mouse_move_event_.Raise(PiToDip(point)); + mouse_move_event_.Raise(PixelToDip(point)); } void WinNativeWindow::OnMouseLeaveInternal() { @@ -397,15 +425,15 @@ void WinNativeWindow::OnMouseLeaveInternal() { mouse_enter_leave_event_.Raise(MouseEnterLeaveType::Leave); } -void WinNativeWindow::OnMouseDownInternal(platform::native::MouseButton button, +void WinNativeWindow::OnMouseDownInternal(platform::gui::MouseButton button, POINT point) { - const auto dip_point = PiToDip(point); + const auto dip_point = PixelToDip(point); mouse_down_event_.Raise({button, dip_point, RetrieveKeyMofifier()}); } -void WinNativeWindow::OnMouseUpInternal(platform::native::MouseButton button, +void WinNativeWindow::OnMouseUpInternal(platform::gui::MouseButton button, POINT point) { - const auto dip_point = PiToDip(point); + const auto dip_point = PixelToDip(point); mouse_up_event_.Raise({button, dip_point, RetrieveKeyMofifier()}); } @@ -427,16 +455,4 @@ void WinNativeWindow::OnKeyUpInternal(int virtual_code) { void WinNativeWindow::OnActivatedInternal() {} void WinNativeWindow::OnDeactivatedInternal() {} - -void WinNativeWindowResolver::Reset() { - Expects(window_); // already reset, can't reset again - window_ = nullptr; -} - -WinNativeWindow* Resolve(gsl::not_null<INativeWindowResolver*> resolver) { - const auto window = resolver->Resolve(); - return window == nullptr ? nullptr - : CheckPlatform<WinNativeWindow>( - window, WinNativeResource::k_platform_id); -} // namespace cru::platform::native::win -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/WindowClass.cpp b/src/win/gui/WindowClass.cpp index 2e74606e..a033d091 100644 --- a/src/win/native/WindowClass.cpp +++ b/src/win/gui/WindowClass.cpp @@ -1,8 +1,8 @@ -#include "cru/win/native/WindowClass.hpp" +#include "cru/win/gui/WindowClass.hpp" -#include "cru/win/native/Exception.hpp" +#include "cru/win/gui/Exception.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { WindowClass::WindowClass(std::wstring name, WNDPROC window_proc, HINSTANCE h_instance) : name_(std::move(name)) { @@ -25,4 +25,4 @@ WindowClass::WindowClass(std::wstring name, WNDPROC window_proc, if (atom_ == 0) throw Win32Error(::GetLastError(), "Failed to create window class."); } -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/WindowManager.cpp b/src/win/gui/WindowManager.cpp index 56cc8981..4e84e967 100644 --- a/src/win/native/WindowManager.cpp +++ b/src/win/gui/WindowManager.cpp @@ -1,10 +1,10 @@ #include "WindowManager.hpp" -#include "cru/win/native/UiApplication.hpp" -#include "cru/win/native/Window.hpp" -#include "cru/win/native/WindowClass.hpp" +#include "cru/win/gui/UiApplication.hpp" +#include "cru/win/gui/Window.hpp" +#include "cru/win/gui/WindowClass.hpp" -namespace cru::platform::native::win { +namespace cru::platform::gui::win { LRESULT __stdcall GeneralWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { auto window = @@ -53,4 +53,4 @@ std::vector<WinNativeWindow*> WindowManager::GetAllWindows() const { for (const auto& [key, value] : window_map_) windows.push_back(value); return windows; } -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/WindowManager.hpp b/src/win/gui/WindowManager.hpp index 3f6387f7..3b037f89 100644 --- a/src/win/native/WindowManager.hpp +++ b/src/win/gui/WindowManager.hpp @@ -7,7 +7,7 @@ #include <memory> #include <vector> -namespace cru::platform::native::win { +namespace cru::platform::gui::win { class WinUiApplication; class WinNativeWindow; class WindowClass; @@ -48,4 +48,4 @@ class WindowManager : public Object { std::unique_ptr<WindowClass> general_window_class_; std::map<HWND, WinNativeWindow*> window_map_; }; -} // namespace cru::platform::native::win +} // namespace cru::platform::gui::win diff --git a/src/win/native/CMakeLists.txt b/src/win/native/CMakeLists.txt deleted file mode 100644 index f1b167d2..00000000 --- a/src/win/native/CMakeLists.txt +++ /dev/null @@ -1,37 +0,0 @@ -set(CRU_WIN_NATIVE_INCLUDE_DIR ${CRU_INCLUDE_DIR}/cru/win/native) - -add_library(cru_win_native STATIC - DpiUtil.hpp - GodWindowMessage.hpp - Timer.hpp - WindowD2DPainter.hpp - WindowManager.hpp - - Cursor.cpp - GodWindow.cpp - InputMethod.cpp - Keyboard.cpp - Timer.cpp - UiApplication.cpp - Window.cpp - WindowClass.cpp - WindowD2DPainter.cpp - WindowManager.cpp - WindowRenderTarget.cpp -) -target_sources(cru_win_native PUBLIC - ${CRU_WIN_NATIVE_INCLUDE_DIR}/Cursor.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/Exception.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/Base.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/GodWindow.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/InputMethod.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/Keyboard.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/Resource.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/UiApplication.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/Window.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/WindowClass.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/WindowNativeMessageEventArgs.hpp - ${CRU_WIN_NATIVE_INCLUDE_DIR}/WindowRenderTarget.hpp -) -target_link_libraries(cru_win_native PUBLIC imm32) -target_link_libraries(cru_win_native PUBLIC cru_win_graph_direct cru_platform_native) diff --git a/src/win/native/DpiUtil.hpp b/src/win/native/DpiUtil.hpp deleted file mode 100644 index 16ffda25..00000000 --- a/src/win/native/DpiUtil.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include "cru/platform/native/Base.hpp" - -// The dpi awareness needs to be implemented in the future. Currently we use 96 -// as default. - -namespace cru::platform::native::win { -inline platform::native::Dpi GetDpi() { - return platform::native::Dpi{96.0f, 96.0f}; -} - -inline int DipToPixelInternal(const float dip, const float dpi) { - return static_cast<int>(dip * dpi / 96.0f); -} - -inline int DipToPixelX(const float dip_x) { - return DipToPixelInternal(dip_x, GetDpi().x); -} - -inline int DipToPixelY(const float dip_y) { - return DipToPixelInternal(dip_y, GetDpi().y); -} - -inline float DipToPixelInternal(const int pixel, const float dpi) { - return static_cast<float>(pixel) * 96.0f / dpi; -} - -inline float PixelToDipX(const int pixel_x) { - return DipToPixelInternal(pixel_x, GetDpi().x); -} - -inline float PixelToDipY(const int pixel_y) { - return DipToPixelInternal(pixel_y, GetDpi().y); -} - -inline Point PiToDip(const POINT& pi_point) { - return Point(PixelToDipX(pi_point.x), PixelToDipY(pi_point.y)); -} - -inline POINT DipToPi(const Point& dip_point) { - POINT result; - result.x = DipToPixelX(dip_point.x); - result.y = DipToPixelY(dip_point.y); - return result; -} -} // namespace cru::platform::native::win diff --git a/src/win/native/GodWindowMessage.hpp b/src/win/native/GodWindowMessage.hpp deleted file mode 100644 index 9063cb4d..00000000 --- a/src/win/native/GodWindowMessage.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once -#include "cru/win/WinPreConfig.hpp" - -namespace cru::platform::native::win { -constexpr int invoke_later_message_id = WM_USER + 2000; -} diff --git a/src/win/native/Timer.cpp b/src/win/native/Timer.cpp deleted file mode 100644 index 662067fb..00000000 --- a/src/win/native/Timer.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "Timer.hpp" - -namespace cru::platform::native::win { -TimerManager::TimerManager(GodWindow* god_window) { god_window_ = god_window; } - -UINT_PTR TimerManager::CreateTimer(const UINT milliseconds, const bool loop, - TimerAction action) { - const auto id = current_count_++; - ::SetTimer(god_window_->GetHandle(), id, milliseconds, nullptr); - map_.emplace(id, std::make_pair(loop, std::move(action))); - return id; -} - -void TimerManager::KillTimer(const UINT_PTR id) { - const auto find_result = map_.find(id); - if (find_result != map_.cend()) { - ::KillTimer(god_window_->GetHandle(), id); - map_.erase(find_result); - } -} - -std::optional<std::pair<bool, TimerAction>> TimerManager::GetAction( - const UINT_PTR id) { - const auto find_result = map_.find(id); - if (find_result == map_.cend()) return std::nullopt; - return find_result->second; -} -} // namespace cru::platform::native::win diff --git a/src/win/native/Timer.hpp b/src/win/native/Timer.hpp deleted file mode 100644 index 95f186a1..00000000 --- a/src/win/native/Timer.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once -#include "cru/win/WinPreConfig.hpp" - -#include "cru/common/Base.hpp" -#include "cru/win/native/GodWindow.hpp" - -#include <chrono> -#include <functional> -#include <map> -#include <optional> - -namespace cru::platform::native::win { -using TimerAction = std::function<void()>; - -class TimerManager : public Object { - public: - TimerManager(GodWindow* god_window); - - CRU_DELETE_COPY(TimerManager) - CRU_DELETE_MOVE(TimerManager) - - ~TimerManager() override = default; - - UINT_PTR CreateTimer(UINT milliseconds, bool loop, TimerAction action); - void KillTimer(UINT_PTR id); - std::optional<std::pair<bool, TimerAction>> GetAction(UINT_PTR id); - - private: - GodWindow* god_window_; - - std::map<UINT_PTR, std::pair<bool, TimerAction>> map_{}; - UINT_PTR current_count_ = 0; -}; -} // namespace cru::platform::native::win diff --git a/src/win/native/WindowD2DPainter.cpp b/src/win/native/WindowD2DPainter.cpp deleted file mode 100644 index 7a97480b..00000000 --- a/src/win/native/WindowD2DPainter.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "WindowD2DPainter.hpp" - -#include "cru/win/graph/direct/Exception.hpp" -#include "cru/win/graph/direct/Factory.hpp" -#include "cru/win/native/WindowRenderTarget.hpp" - -namespace cru::platform::native::win { -using namespace cru::platform::graph::win::direct; - -WindowD2DPainter::WindowD2DPainter(WindowRenderTarget* render_target) - : D2DPainter(render_target->GetD2D1DeviceContext()), - render_target_(render_target) { - render_target_->GetD2D1DeviceContext()->BeginDraw(); -} - -WindowD2DPainter::~WindowD2DPainter() { EndDraw(); } - -void WindowD2DPainter::DoEndDraw() { - ThrowIfFailed(render_target_->GetD2D1DeviceContext()->EndDraw()); - render_target_->Present(); -} -} // namespace cru::platform::native::win diff --git a/src/win/native/WindowD2DPainter.hpp b/src/win/native/WindowD2DPainter.hpp deleted file mode 100644 index a638b77a..00000000 --- a/src/win/native/WindowD2DPainter.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include "cru/win/graph/direct/Painter.hpp" -#include "cru/win/native/WindowRenderTarget.hpp" - -namespace cru::platform::native::win { -class WindowD2DPainter : public graph::win::direct::D2DPainter { - public: - explicit WindowD2DPainter(WindowRenderTarget* window); - - CRU_DELETE_COPY(WindowD2DPainter) - CRU_DELETE_MOVE(WindowD2DPainter) - - ~WindowD2DPainter() override; - - protected: - void DoEndDraw() override; - - private: - WindowRenderTarget* render_target_; -}; -} // namespace cru::platform::native::win diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c534b909..3b9567cd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,7 +2,12 @@ find_package(GTest CONFIG REQUIRED) include(GoogleTest) -add_subdirectory(common) - add_library(cru_test_base INTERFACE) target_link_libraries(cru_test_base INTERFACE GTest::gtest GTest::gtest_main) + +add_subdirectory(common) +add_subdirectory(platform) + +if(WIN32) + add_subdirectory(win) +endif() diff --git a/test/common/HandlerRegistryTest.cpp b/test/common/HandlerRegistryTest.cpp new file mode 100644 index 00000000..d1792c7c --- /dev/null +++ b/test/common/HandlerRegistryTest.cpp @@ -0,0 +1,36 @@ +#include "cru/common/HandlerRegistry.hpp" + +#include <gtest/gtest.h> +#include <algorithm> + +TEST(HandlerRegistry, Work) { + using namespace cru; + HandlerRegistry<void()> registry; + + int counter = 1; + + auto tag1 = registry.AddHandler([&counter] { counter++; }); + auto tag2 = registry.AddHandler([&counter] { counter++; }); + + for (const auto& handler : registry) { + handler(); + } + + ASSERT_EQ(counter, 3); + + registry.RemoveHandler(tag1); + + for (const auto& handler : registry) { + handler(); + } + + ASSERT_EQ(counter, 4); + + registry.RemoveHandler(tag2); + + for (const auto& handler : registry) { + handler(); + } + + ASSERT_EQ(counter, 4); +} diff --git a/test/platform/CMakeLists.txt b/test/platform/CMakeLists.txt new file mode 100644 index 00000000..9ad8fb51 --- /dev/null +++ b/test/platform/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(cru_platform_base_test + MatrixTest.cpp +) +target_link_libraries(cru_platform_base_test PRIVATE cru_platform_base cru_test_base) + +gtest_discover_tests(cru_platform_base_test) diff --git a/test/platform/MatrixTest.cpp b/test/platform/MatrixTest.cpp new file mode 100644 index 00000000..3b8aab27 --- /dev/null +++ b/test/platform/MatrixTest.cpp @@ -0,0 +1,37 @@ +#include "cru/platform/GraphBase.hpp" +#include "cru/platform/Matrix.hpp" + +#include <gtest/gtest.h> + +using cru::platform::Matrix; +using cru::platform::Point; + +TEST(Matrix, Rotation) { + Point p(1, 1); + + Point p90 = Matrix::Rotation(90).TransformPoint(p); + ASSERT_FLOAT_EQ(p90.x, -1); + ASSERT_FLOAT_EQ(p90.y, 1); + + Point p180 = Matrix::Rotation(180).TransformPoint(p); + ASSERT_FLOAT_EQ(p180.x, -1); + ASSERT_FLOAT_EQ(p180.y, -1); + + Point p270 = Matrix::Rotation(270).TransformPoint(p); + ASSERT_FLOAT_EQ(p270.x, 1); + ASSERT_FLOAT_EQ(p270.y, -1); +} + +TEST(Matrix, TranslationAndRotation) { + Point p = + (Matrix::Translation(1, 1) * Matrix::Rotation(90)).TransformPoint({1, 1}); + ASSERT_FLOAT_EQ(p.x, -2); + ASSERT_FLOAT_EQ(p.y, 2); +} + +TEST(Matrix, RotationAndTranslation) { + Point p = + (Matrix::Rotation(90) * Matrix::Translation(1, 1)).TransformPoint({1, 1}); + ASSERT_FLOAT_EQ(p.x, 0); + ASSERT_FLOAT_EQ(p.y, 2); +} diff --git a/test/win/CMakeLists.txt b/test/win/CMakeLists.txt new file mode 100644 index 00000000..0ebdd7fe --- /dev/null +++ b/test/win/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(graphics) diff --git a/test/win/graphics/CMakeLists.txt b/test/win/graphics/CMakeLists.txt new file mode 100644 index 00000000..c90537ac --- /dev/null +++ b/test/win/graphics/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(direct) diff --git a/test/win/graphics/direct/CMakeLists.txt b/test/win/graphics/direct/CMakeLists.txt new file mode 100644 index 00000000..69e22ef7 --- /dev/null +++ b/test/win/graphics/direct/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(cru_win_graphics_direct_test + ConvertTest.cpp +) +target_link_libraries(cru_win_graphics_direct_test PRIVATE cru_win_graphics_direct cru_test_base) + +gtest_discover_tests(cru_win_graphics_direct_test) diff --git a/test/win/graphics/direct/ConvertTest.cpp b/test/win/graphics/direct/ConvertTest.cpp new file mode 100644 index 00000000..f8f95dac --- /dev/null +++ b/test/win/graphics/direct/ConvertTest.cpp @@ -0,0 +1,29 @@ +#include "cru/platform/Matrix.hpp" +#include "cru/win/graphics/direct/ConvertUtil.hpp" + +#include <gtest/gtest.h> + +using cru::platform::Matrix; +using cru::platform::graphics::win::direct::Convert; + +TEST(MatrixConvert, Rotation) { + auto matrix = Convert(Matrix::Rotation(90)); + + auto m = *D2D1::Matrix3x2F::ReinterpretBaseType(&matrix); + + auto p = m.TransformPoint({1, 1}); + + ASSERT_FLOAT_EQ(p.x, -1); + ASSERT_FLOAT_EQ(p.y, 1); +} + +TEST(MatrixConvert, RotationAndTranslation) { + auto matrix = Convert(Matrix::Rotation(90) * Matrix::Translation(1, 1)); + + auto m = *D2D1::Matrix3x2F::ReinterpretBaseType(&matrix); + + auto p = m.TransformPoint({1, 1}); + + ASSERT_FLOAT_EQ(p.x, 0); + ASSERT_FLOAT_EQ(p.y, 2); +} diff --git a/vcpkg b/vcpkg -Subproject 13590753fec479c5b0a3d48dd553dde8d49615f +Subproject acb6b10e7fdf5e8519c18398d0b069e1d58ca02 |