diff options
Diffstat (limited to 'CruUI-Generate/cru_ui.cpp')
-rw-r--r-- | CruUI-Generate/cru_ui.cpp | 3934 |
1 files changed, 3934 insertions, 0 deletions
diff --git a/CruUI-Generate/cru_ui.cpp b/CruUI-Generate/cru_ui.cpp new file mode 100644 index 00000000..52e8aec3 --- /dev/null +++ b/CruUI-Generate/cru_ui.cpp @@ -0,0 +1,3934 @@ +#include "cru_ui.hpp" +//-------------------------------------------------------- +//-------begin of file: ..\..\src\any_map.cpp +//-------------------------------------------------------- + +namespace cru +{ + AnyMap::ListenerToken AnyMap::RegisterValueChangeListener(const String& key, const Listener& listener) + { + const auto token = current_listener_token_++; + map_[key].second.push_back(token); + listeners_.emplace(token, listener); + return token; + } + + void AnyMap::UnregisterValueChangeListener(const ListenerToken token) + { + const auto find_result = listeners_.find(token); + if (find_result != listeners_.cend()) + listeners_.erase(find_result); + } + + void AnyMap::InvokeListeners(std::list<ListenerToken>& listener_list, const std::any& value) + { + auto i = listener_list.cbegin(); + while (i != listener_list.cend()) + { + auto current_i = i++; + const auto find_result = listeners_.find(*current_i); + if (find_result != listeners_.cend()) + find_result->second(value); + else + listener_list.erase(current_i); // otherwise remove the invalid listener token. + } + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\any_map.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\application.cpp +//-------------------------------------------------------- + + +namespace cru { + constexpr auto god_window_class_name = L"GodWindowClass"; + constexpr int invoke_later_message_id = WM_USER + 2000; + + + LRESULT CALLBACK GodWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + const auto app = Application::GetInstance(); + + if (app) + { + const auto result = app->GetGodWindow()->HandleGodWindowMessage(hWnd, uMsg, wParam, lParam); + if (result.has_value()) + return result.value(); + else + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + else + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + + GodWindow::GodWindow(Application* application) + { + const auto h_instance = application->GetInstanceHandle(); + + god_window_class_ = std::make_unique<ui::WindowClass>(god_window_class_name, GodWndProc, h_instance); + + hwnd_ = CreateWindowEx(0, + god_window_class_name, + L"", 0, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + HWND_MESSAGE, nullptr, h_instance, nullptr + ); + + if (hwnd_ == nullptr) + throw std::runtime_error("Failed to create window."); + } + + GodWindow::~GodWindow() + { + ::DestroyWindow(hwnd_); + } + + std::optional<LRESULT> GodWindow::HandleGodWindowMessage(HWND hwnd, int msg, WPARAM w_param, LPARAM l_param) + { + switch (msg) + { + case invoke_later_message_id: + { + const auto p_action = reinterpret_cast<std::function<void()>*>(w_param); + (*p_action)(); + delete p_action; + return 0; + } + case WM_TIMER: + { + const auto id = static_cast<UINT_PTR>(w_param); + const auto action = TimerManager::GetInstance()->GetAction(id); + if (action.has_value()) + { + (action.value().second)(); + if (!action.value().first) + TimerManager::GetInstance()->KillTimer(id); + return 0; + } + break; + } + default: + return std::nullopt; + } + return std::nullopt; + } + + + + Application* Application::instance_ = nullptr; + + Application * Application::GetInstance() { + return instance_; + } + + namespace + { + void LoadSystemCursor(HINSTANCE h_instance) + { + ui::cursors::arrow = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_ARROW), false); + ui::cursors::hand = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_HAND), false); + ui::cursors::i_beam = std::make_shared<ui::Cursor>(::LoadCursorW(nullptr, IDC_IBEAM), false); + } + } + + Application::Application(HINSTANCE h_instance) + : h_instance_(h_instance) { + + if (instance_) + throw std::runtime_error("A application instance already exists."); + + instance_ = this; + + god_window_ = std::make_unique<GodWindow>(this); + + LoadSystemCursor(h_instance); + } + + Application::~Application() + { + for (auto i = singleton_list_.crbegin(); i != singleton_list_.crend(); ++i) + delete *i; + instance_ = nullptr; + } + + int Application::Run() + { + MSG msg; + + while (GetMessage(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return static_cast<int>(msg.wParam); + } + + void Application::Quit(const int quit_code) { + ::PostQuitMessage(quit_code); + } + + void InvokeLater(const std::function<void()>& action) { + //copy the action to a safe place + auto p_action_copy = new std::function<void()>(action); + + if (PostMessageW(Application::GetInstance()->GetGodWindow()->GetHandle(), invoke_later_message_id, reinterpret_cast<WPARAM>(p_action_copy), 0) == 0) + throw Win32Error(::GetLastError(), "InvokeLater failed to post message."); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\application.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\base.cpp +//-------------------------------------------------------- + + +namespace cru +{ + MultiByteString ToUtf8String(const StringView& string) + { + if (string.empty()) + return MultiByteString(); + + const auto length = ::WideCharToMultiByte(CP_UTF8, 0, string.data(), -1, nullptr, 0, nullptr, nullptr); + MultiByteString result; + result.reserve(length); + if (::WideCharToMultiByte(CP_UTF8, 0, string.data(), -1, result.data(), static_cast<int>(result.capacity()), nullptr, nullptr) == 0) + throw Win32Error(::GetLastError(), "Failed to convert wide string to UTF-8."); + return result; + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\base.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\cru_debug.cpp +//-------------------------------------------------------- + + +namespace cru::debug +{ + void DebugMessage(const StringView& message) + { + ::OutputDebugStringW(message.data()); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\cru_debug.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\exception.cpp +//-------------------------------------------------------- + + +namespace cru +{ + inline std::string HResultMakeMessage(HRESULT h_result, std::optional<MultiByteStringView> message) + { + char buffer[10]; + sprintf_s(buffer, "%#08x", h_result); + + if (message.has_value()) + return Format("An HResultError is thrown. HRESULT: {}.\nAdditional message: {}\n", buffer, message.value()); + else + return Format("An HResultError is thrown. HRESULT: {}.\n", buffer); + } + + HResultError::HResultError(HRESULT h_result, std::optional<MultiByteStringView> additional_message) + : runtime_error(HResultMakeMessage(h_result, std::nullopt)), h_result_(h_result) + { + + } + + inline std::string Win32MakeMessage(DWORD error_code, std::optional<MultiByteStringView> message) + { + char buffer[10]; + sprintf_s(buffer, "%#04x", error_code); + + if (message.has_value()) + return Format("Last error code: {}.\nAdditional message: {}\n", buffer, message.value()); + else + return Format("Last error code: {}.\n", buffer); + } + + Win32Error::Win32Error(DWORD error_code, std::optional<MultiByteStringView> additional_message) + : runtime_error(Win32MakeMessage(error_code, std::nullopt)), error_code_(error_code) + { + + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\exception.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\main.cpp +//-------------------------------------------------------- + +using cru::String; +using cru::StringView; +using cru::Application; +using cru::ui::Rect; +using cru::ui::Window; +using cru::ui::Alignment; +using cru::ui::LayoutSideParams; +using cru::ui::Thickness; +using cru::ui::ControlList; +using cru::ui::CreateWithLayout; +using cru::ui::controls::LinearLayout; +using cru::ui::controls::TextBlock; +using cru::ui::controls::ToggleButton; +using cru::ui::controls::Button; +using cru::ui::controls::TextBox; +using cru::ui::controls::ListItem; + +int APIENTRY wWinMain( + HINSTANCE hInstance, + HINSTANCE hPrevInstance, + LPWSTR lpCmdLine, + int nCmdShow) { + + Application application(hInstance); + + const auto window = Window::CreateOverlapped(); + /* + window.native_message_event.AddHandler([](cru::ui::events::WindowNativeMessageEventArgs& args) + { + if (args.GetWindowMessage().msg == WM_PAINT) + { + OutputDebugStringW(L"Paint!\n"); + //args.SetResult(0); + } + }); + */ + /* + // test1 + cru::ui::controls::TextBlock text_block; + text_block.SetText(L"Hello world!"); + text_block.SetSize(cru::ui::Size(200, 30)); + window.AddChild(&text_block); + + std::array<D2D_COLOR_F, 4> colors = + { + D2D1::ColorF(D2D1::ColorF::Blue), + D2D1::ColorF(D2D1::ColorF::Yellow), + D2D1::ColorF(D2D1::ColorF::Green), + D2D1::ColorF(D2D1::ColorF::Red) + }; + + std::random_device rd; // only used once to initialise (seed) engine + std::mt19937 rng(rd()); // random-number engine used (Mersenne-Twister in this case) + std::uniform_int_distribution<decltype(colors.size())> uni(0, colors.size() - 1); // guaranteed unbiased + + + window.draw_event.AddHandler([&](cru::ui::events::DrawEventArgs& args) { + auto device_context = args.GetDeviceContext(); + + ID2D1SolidColorBrush* brush; + device_context->CreateSolidColorBrush(colors[uni(rng)], &brush); + + device_context->FillRectangle(D2D1::RectF(100.0f, 100.0f, 300.0f, 200.0f), brush); + + brush->Release(); + }); + + cru::SetTimeout(2.0, [&window]() { + window.Repaint(); + + auto task = cru::SetInterval(0.5, [&window]() { + window.Repaint(); + }); + + cru::SetTimeout(4, [task]() { + task->Cancel(); + task->Cancel(); // test for idempotency. + }); + }); + */ + + + //test 2 + const auto layout = CreateWithLayout<LinearLayout>(LayoutSideParams::Exactly(500), LayoutSideParams::Content()); + + layout->mouse_click_event.AddHandler([layout](cru::ui::events::MouseButtonEventArgs& args) + { + if (args.GetSender() == args.GetOriginalSender()) + layout->AddChild(TextBlock::Create(L"Layout is clicked!")); + }); + + { + const auto inner_layout = CreateWithLayout<LinearLayout>(LayoutSideParams::Content(Alignment::End), LayoutSideParams::Content(), LinearLayout::Orientation::Horizontal); + + inner_layout->AddChild(TextBlock::Create(L"Toggle debug border")); + + const auto toggle_button = ToggleButton::Create(); +#ifdef CRU_DEBUG_LAYOUT + toggle_button->toggle_event.AddHandler([&window](cru::ui::events::ToggleEventArgs& args) + { + window->SetDebugLayout(args.GetNewState()); + }); +#endif + inner_layout->AddChild(toggle_button); + layout->AddChild(inner_layout); + } + + { + const auto button = Button::Create(); + button->GetLayoutParams()->padding = Thickness(20, 5); + button->AddChild(TextBlock::Create(L"Show popup window parenting this.")); + button->mouse_click_event.AddHandler([window, button](auto) + { + std::vector<cru::ui::controls::MenuItemInfo> items; + items.emplace_back(L"Hello world!", []{}); + items.emplace_back(L"Item 2", []{}); + items.emplace_back(L"Close parent window.", [window]{ window->Close(); }); + + cru::ui::controls::CreatePopupMenu(window->PointToScreen(button->GetPositionAbsolute()), items, window)->Show(); + }); + layout->AddChild(button); + } + + { + const auto button = Button::Create(); + button->GetLayoutParams()->padding = Thickness(20, 5); + button->AddChild(TextBlock::Create(L"Show popup window parenting null.")); + button->mouse_click_event.AddHandler([](auto) + { + auto popup = Window::CreatePopup(nullptr); + popup->SetWindowRect(Rect(100, 100, 300, 300)); + popup->Show(); + }); + layout->AddChild(button); + } + + { + const auto button = Button::Create(); + button->GetLayoutParams()->padding = Thickness(20, 5); + button->AddChild(TextBlock::Create(L"Show popup window with caption.")); + button->mouse_click_event.AddHandler([](auto) + { + auto popup = Window::CreatePopup(nullptr, true); + popup->SetWindowRect(Rect(100, 100, 300, 300)); + popup->Show(); + }); + layout->AddChild(button); + } + + { + const auto text_block = CreateWithLayout<TextBlock>(LayoutSideParams::Exactly(200), LayoutSideParams::Exactly(80), L"Hello World!!!"); + + text_block->mouse_click_event.AddHandler([layout](cru::ui::events::MouseButtonEventArgs& args) + { + layout->AddChild(TextBlock::Create(L"Hello world is clicked!")); + }); + + layout->AddChild(text_block); + } + + { + const auto text_box = TextBox::Create(); + text_box->GetLayoutParams()->width.min = 50.0f; + text_box->GetLayoutParams()->width.max = 100.0f; + layout->AddChild(text_box); + } + + { + const auto text_block = CreateWithLayout<TextBlock>(LayoutSideParams::Stretch(), LayoutSideParams::Stretch(), L"This is a very very very very very long sentence!!!"); + text_block->SetSelectable(true); + layout->AddChild(text_block); + } + + layout->AddChild(CreateWithLayout<TextBlock>(LayoutSideParams::Content(Alignment::Start), LayoutSideParams::Content(), L"This is a little short sentence!!!")); + layout->AddChild(CreateWithLayout<TextBlock>(LayoutSideParams::Content(Alignment::End), LayoutSideParams::Stretch(), L"By crupest!!!")); + + + window->AddChild(layout); + + /* + window.AddChild( + CreateWithLayout<Border>(LayoutSideParams::Exactly(200), LayoutSideParams::Content(), + std::initializer_list<cru::ui::Control*>{ + CreateWithLayout<TextBox>(LayoutSideParams::Stretch(), LayoutSideParams::Content()) + } + )); + */ + + /* test 3 + const auto linear_layout = CreateWithLayout<LinearLayout>(Thickness(50, 50), Thickness(50, 50), LinearLayout::Orientation::Vertical, ControlList{ + Button::Create({ + TextBlock::Create(L"Button") + }), + CreateWithLayout<TextBox>(Thickness(30), Thickness(20)) + }); + + linear_layout->SetBordered(true); + + window.AddChild(linear_layout); + */ + + + window->Show(); + + return application.Run(); +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\main.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\timer.cpp +//-------------------------------------------------------- + + +namespace cru +{ + TimerManager* TimerManager::GetInstance() + { + return Application::GetInstance()->ResolveSingleton<TimerManager>([](auto) + { + return new TimerManager{}; + }); + } + + UINT_PTR TimerManager::CreateTimer(const UINT milliseconds, const bool loop, const TimerAction& action) + { + const auto id = current_count_++; + ::SetTimer(Application::GetInstance()->GetGodWindow()->GetHandle(), id, milliseconds, nullptr); + map_.emplace(id, std::make_pair(loop, action)); + return id; + } + + void TimerManager::KillTimer(const UINT_PTR id) + { + const auto find_result = map_.find(id); + if (find_result != map_.cend()) + { + ::KillTimer(Application::GetInstance()->GetGodWindow()->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; + } + + TimerTask::TimerTask(const UINT_PTR id) + : id_(id) + { + + } + + void TimerTask::Cancel() const + { + TimerManager::GetInstance()->KillTimer(id_); + } + + TimerTask SetTimeout(std::chrono::milliseconds milliseconds, const TimerAction& action) + { + const auto id = TimerManager::GetInstance()->CreateTimer(static_cast<UINT>(milliseconds.count()), false, action); + return TimerTask(id); + } + + TimerTask SetInterval(std::chrono::milliseconds milliseconds, const TimerAction& action) + { + const auto id = TimerManager::GetInstance()->CreateTimer(static_cast<UINT>(milliseconds.count()), true, action); + return TimerTask(id); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\timer.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\graph\graph.cpp +//-------------------------------------------------------- + + +namespace cru::graph +{ + using Microsoft::WRL::ComPtr; + + WindowRenderTarget::WindowRenderTarget(GraphManager* graph_manager, HWND hwnd) + { + this->graph_manager_ = graph_manager; + + const auto d3d11_device = graph_manager->GetD3D11Device(); + const auto dxgi_factory = graph_manager->GetDxgiFactory(); + + // Allocate a descriptor. + DXGI_SWAP_CHAIN_DESC1 swap_chain_desc = { 0 }; + swap_chain_desc.Width = 0; // use automatic sizing + swap_chain_desc.Height = 0; + swap_chain_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // this is the most common swapchain format + swap_chain_desc.Stereo = false; + swap_chain_desc.SampleDesc.Count = 1; // don't use multi-sampling + swap_chain_desc.SampleDesc.Quality = 0; + swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + swap_chain_desc.BufferCount = 2; // use double buffering to enable flip + swap_chain_desc.Scaling = DXGI_SCALING_NONE; + swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; // all apps must use this SwapEffect + swap_chain_desc.Flags = 0; + + + + // Get the final swap chain for this window from the DXGI factory. + ThrowIfFailed( + dxgi_factory->CreateSwapChainForHwnd( + d3d11_device.Get(), + hwnd, + &swap_chain_desc, + nullptr, + nullptr, + &dxgi_swap_chain_ + ) + ); + + CreateTargetBitmap(); + } + + WindowRenderTarget::~WindowRenderTarget() + { + + } + + void WindowRenderTarget::ResizeBuffer(const int width, const int height) + { + const auto graph_manager = graph_manager_; + const auto d2d1_device_context = graph_manager->GetD2D1DeviceContext(); + + ComPtr<ID2D1Image> old_target; + d2d1_device_context->GetTarget(&old_target); + const auto target_this = old_target == this->target_bitmap_; + if (target_this) + d2d1_device_context->SetTarget(nullptr); + + old_target = nullptr; + target_bitmap_ = nullptr; + + ThrowIfFailed( + dxgi_swap_chain_->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0) + ); + + CreateTargetBitmap(); + + if (target_this) + d2d1_device_context->SetTarget(target_bitmap_.Get()); + } + + void WindowRenderTarget::SetAsTarget() + { + GetD2DDeviceContext()->SetTarget(target_bitmap_.Get()); + } + + void WindowRenderTarget::Present() + { + ThrowIfFailed( + dxgi_swap_chain_->Present(1, 0) + ); + } + + void WindowRenderTarget::CreateTargetBitmap() + { + // Direct2D needs the dxgi version of the backbuffer surface pointer. + ComPtr<IDXGISurface> dxgiBackBuffer; + ThrowIfFailed( + dxgi_swap_chain_->GetBuffer(0, IID_PPV_ARGS(&dxgiBackBuffer)) + ); + + const auto dpi = graph_manager_->GetDpi(); + + 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 + ); + + // Get a D2D surface from the DXGI back buffer to use as the D2D render target. + ThrowIfFailed( + graph_manager_->GetD2D1DeviceContext()->CreateBitmapFromDxgiSurface( + dxgiBackBuffer.Get(), + &bitmap_properties, + &target_bitmap_ + ) + ); + } + + GraphManager* GraphManager::GetInstance() + { + return Application::GetInstance()->ResolveSingleton<GraphManager>([](auto) + { + return new GraphManager{}; + }); + } + + GraphManager::GraphManager() + { + UINT creation_flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + +#ifdef CRU_DEBUG + creation_flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + const D3D_FEATURE_LEVEL feature_levels[] = + { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1 + }; + + + ThrowIfFailed(D3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_HARDWARE, + nullptr, + creation_flags, + feature_levels, + ARRAYSIZE(feature_levels), + D3D11_SDK_VERSION, + &d3d11_device_, + nullptr, + &d3d11_device_context_ + )); + + Microsoft::WRL::ComPtr<IDXGIDevice> dxgi_device; + + ThrowIfFailed(d3d11_device_.As(&dxgi_device)); + + ThrowIfFailed(D2D1CreateFactory( + D2D1_FACTORY_TYPE_SINGLE_THREADED, + __uuidof(ID2D1Factory1), + &d2d1_factory_ + )); + + ThrowIfFailed(d2d1_factory_->CreateDevice(dxgi_device.Get(), &d2d1_device_)); + + ThrowIfFailed(d2d1_device_->CreateDeviceContext( + D2D1_DEVICE_CONTEXT_OPTIONS_NONE, + &d2d1_device_context_ + )); + + // Identify the physical adapter (GPU or card) this device is runs on. + ComPtr<IDXGIAdapter> dxgi_adapter; + ThrowIfFailed( + dxgi_device->GetAdapter(&dxgi_adapter) + ); + + // Get the factory object that created the DXGI device. + ThrowIfFailed( + dxgi_adapter->GetParent(IID_PPV_ARGS(&dxgi_factory_)) + ); + + + ThrowIfFailed(DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + __uuidof(IDWriteFactory), + reinterpret_cast<IUnknown**>(dwrite_factory_.GetAddressOf()) + )); + + dwrite_factory_->GetSystemFontCollection(&dwrite_system_font_collection_); + } + + GraphManager::~GraphManager() + { + + } + + std::shared_ptr<WindowRenderTarget> GraphManager::CreateWindowRenderTarget(HWND hwnd) + { + return std::make_shared<WindowRenderTarget>(this, hwnd); + } + + Dpi GraphManager::GetDpi() const + { + Dpi dpi; + d2d1_factory_->GetDesktopDpi(&dpi.x, &dpi.y); + return dpi; + } + + void GraphManager::ReloadSystemMetrics() + { + ThrowIfFailed( + d2d1_factory_->ReloadSystemMetrics() + ); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\graph\graph.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\border_property.cpp +//-------------------------------------------------------- + + +namespace cru::ui +{ + BorderProperty::BorderProperty(): BorderProperty(UiManager::GetInstance()->GetPredefineResources()->border_property_brush) + { + + } + + BorderProperty::BorderProperty(Microsoft::WRL::ComPtr<ID2D1Brush> brush): brush_(std::move(brush)) + { + + } + + BorderProperty::BorderProperty(Microsoft::WRL::ComPtr<ID2D1Brush> brush, const float width, const float radius_x, + const float radius_y, Microsoft::WRL::ComPtr<ID2D1StrokeStyle> stroke_style) : + brush_(std::move(brush)), stroke_width_(width), radius_x_(radius_x), radius_y_(radius_y), stroke_style_(std::move(stroke_style)) + { + + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\border_property.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\control.cpp +//-------------------------------------------------------- + +#include <algorithm> + + +#ifdef CRU_DEBUG_LAYOUT +#endif + +namespace cru::ui +{ + using namespace events; + + Control::Control(const bool container) : + is_container_(container) + { + + } + + Control::Control(WindowConstructorTag, Window* window) : Control(true) + { + window_ = window; + } + + Control::~Control() + { + for (auto control: GetChildren()) + { + delete control; + } + } + + void AddChildCheck(Control* control) + { + if (control->GetParent() != nullptr) + throw std::invalid_argument("The control already has a parent."); + + if (dynamic_cast<Window*>(control)) + throw std::invalid_argument("Can't add a window as child."); + } + + const std::vector<Control*>& Control::GetChildren() const + { + return children_; + } + + void Control::AddChild(Control* control) + { + ThrowIfNotContainer(); + AddChildCheck(control); + + this->children_.push_back(control); + + control->parent_ = this; + + this->OnAddChild(control); + } + + void Control::AddChild(Control* control, int position) + { + ThrowIfNotContainer(); + AddChildCheck(control); + + if (position < 0 || static_cast<decltype(this->children_.size())>(position) > this->children_.size()) + throw std::invalid_argument("The position is out of range."); + + this->children_.insert(this->children_.cbegin() + position, control); + + control->parent_ = this; + + this->OnAddChild(this); + } + + void Control::RemoveChild(Control* child) + { + ThrowIfNotContainer(); + const auto i = std::find(this->children_.cbegin(), this->children_.cend(), child); + if (i == this->children_.cend()) + throw std::invalid_argument("The argument child is not a child of this control."); + + this->children_.erase(i); + + child->parent_ = nullptr; + + this->OnRemoveChild(this); + } + + void Control::RemoveChild(const int position) + { + ThrowIfNotContainer(); + if (position < 0 || static_cast<decltype(this->children_.size())>(position) >= this->children_.size()) + throw std::invalid_argument("The position is out of range."); + + const auto p = children_.cbegin() + position; + const auto child = *p; + children_.erase(p); + + child->parent_ = nullptr; + + this->OnRemoveChild(child); + } + + Control* Control::GetAncestor() + { + // if attached to window, the window is the ancestor. + if (window_) + return window_; + + // otherwise find the ancestor + auto ancestor = this; + while (const auto parent = ancestor->GetParent()) + ancestor = parent; + return ancestor; + } + + void TraverseDescendantsInternal(Control* control, const std::function<void(Control*)>& predicate) + { + predicate(control); + if (control->IsContainer()) + for (auto c: control->GetChildren()) + { + TraverseDescendantsInternal(c, predicate); + } + } + + void Control::TraverseDescendants(const std::function<void(Control*)>& predicate) + { + if (is_container_) + TraverseDescendantsInternal(this, predicate); + else + predicate(this); + } + + Point Control::GetPositionRelative() + { + return position_; + } + + void Control::SetPositionRelative(const Point & position) + { + if (position != position_) + { + if (old_position_ == position) // if cache has been refreshed and no pending notify + old_position_ = position_; + position_ = position; + LayoutManager::GetInstance()->InvalidateControlPositionCache(this); + if (auto window = GetWindow()) + { + window->Repaint(); + } + } + } + + Size Control::GetSize() + { + return size_; + } + + void Control::SetSize(const Size & size) + { + const auto old_size = size_; + size_ = size; + SizeChangedEventArgs args(this, this, old_size, size); + RaiseSizeChangedEvent(args); + if (auto window = GetWindow()) + window->Repaint(); + } + + Point Control::GetPositionAbsolute() const + { + return position_cache_.lefttop_position_absolute; + } + + Point Control::ControlToWindow(const Point& point) const + { + return Point(point.x + position_cache_.lefttop_position_absolute.x, + point.y + position_cache_.lefttop_position_absolute.y); + } + + Point Control::WindowToControl(const Point & point) const + { + return Point(point.x - position_cache_.lefttop_position_absolute.x, + point.y - position_cache_.lefttop_position_absolute.y); + } + + bool Control::IsPointInside(const Point & point) + { + const auto size = GetSize(); + return point.x >= 0.0f && point.x < size.width && point.y >= 0.0f && point.y < size.height; + } + + void Control::Draw(ID2D1DeviceContext* device_context) + { + D2D1::Matrix3x2F old_transform; + device_context->GetTransform(&old_transform); + + const auto position = GetPositionRelative(); + device_context->SetTransform(old_transform * D2D1::Matrix3x2F::Translation(position.x, position.y)); + + OnDrawCore(device_context); + + for (auto child : GetChildren()) + child->Draw(device_context); + + device_context->SetTransform(old_transform); + } + + void Control::Repaint() + { + if (window_ != nullptr) + window_->Repaint(); + } + + bool Control::RequestFocus() + { + auto window = GetWindow(); + if (window == nullptr) + return false; + + return window->RequestFocusFor(this); + } + + bool Control::HasFocus() + { + auto window = GetWindow(); + if (window == nullptr) + return false; + + return window->GetFocusControl() == this; + } + + void Control::InvalidateLayout() + { + if (const auto window = GetWindow()) + window->WindowInvalidateLayout(); + } + + void Control::Measure(const Size& available_size) + { + SetDesiredSize(OnMeasureCore(available_size)); + } + + void Control::Layout(const Rect& rect) + { + SetPositionRelative(rect.GetLeftTop()); + SetSize(rect.GetSize()); + OnLayoutCore(Rect(Point::Zero(), rect.GetSize())); + } + + Size Control::GetDesiredSize() const + { + return desired_size_; + } + + void Control::SetDesiredSize(const Size& desired_size) + { + desired_size_ = desired_size; + } + + inline void Shrink(Rect& rect, const Thickness& thickness) + { + rect.left += thickness.left; + rect.top += thickness.top; + rect.width -= thickness.GetHorizontalTotal(); + rect.height -= thickness.GetVerticalTotal(); + } + + Rect Control::GetRect(const RectRange range) + { + if (GetSize() == Size::Zero()) + return Rect(); + + const auto layout_params = GetLayoutParams(); + + auto result = Rect(Point::Zero(), GetSize()); + + if (range == RectRange::Margin) + return result; + + Shrink(result, layout_params->margin); + + if (range == RectRange::FullBorder) + return result; + + if (is_bordered_) + Shrink(result, Thickness(GetBorderProperty().GetStrokeWidth() / 2.0f)); + + if (range == RectRange::HalfBorder) + return result; + + if (is_bordered_) + Shrink(result, Thickness(GetBorderProperty().GetStrokeWidth() / 2.0f)); + + if (range == RectRange::Padding) + return result; + + Shrink(result, layout_params->padding); + + return result; + } + + Point Control::TransformPoint(const Point& point, const RectRange from, const RectRange to) + { + const auto rect_from = GetRect(from); + const auto rect_to = GetRect(to); + auto p = point; + p.x += rect_from.left; + p.y += rect_from.top; + p.x -= rect_to.left; + p.y -= rect_to.top; + return p; + } + + void Control::InvalidateBorder() + { + InvalidateLayout(); + Repaint(); + } + + void Control::SetBordered(const bool bordered) + { + if (bordered != is_bordered_) + { + is_bordered_ = bordered; + InvalidateBorder(); + } + } + + void Control::SetCursor(const Cursor::Ptr& cursor) + { + if (cursor != cursor_) + { + cursor_ = cursor; + const auto window = GetWindow(); + if (window && window->GetMouseHoverControl() == this) + window->UpdateCursor(); + } + } + + void Control::OnAddChild(Control* child) + { + if (auto window = GetWindow()) + { + child->TraverseDescendants([window](Control* control) { + control->OnAttachToWindow(window); + }); + window->RefreshControlList(); + InvalidateLayout(); + } + } + + void Control::OnRemoveChild(Control* child) + { + if (auto window = GetWindow()) + { + child->TraverseDescendants([window](Control* control) { + control->OnDetachToWindow(window); + }); + window->RefreshControlList(); + InvalidateLayout(); + } + } + + void Control::OnAttachToWindow(Window* window) + { + window_ = window; + } + + void Control::OnDetachToWindow(Window * window) + { + window_ = nullptr; + } + + void Control::OnDrawCore(ID2D1DeviceContext* device_context) + { + #ifdef CRU_DEBUG_LAYOUT + if (GetWindow()->IsDebugLayout()) + { + if (padding_geometry_ != nullptr) + device_context->FillGeometry(padding_geometry_.Get(), UiManager::GetInstance()->GetPredefineResources()->debug_layout_padding_brush.Get()); + if (margin_geometry_ != nullptr) + device_context->FillGeometry(margin_geometry_.Get(), UiManager::GetInstance()->GetPredefineResources()->debug_layout_margin_brush.Get()); + device_context->DrawRectangle(Convert(GetRect(RectRange::Margin)), UiManager::GetInstance()->GetPredefineResources()->debug_layout_out_border_brush.Get()); + } +#endif + + if (is_bordered_) + { + const auto border_rect = GetRect(RectRange::HalfBorder); + device_context->DrawRoundedRectangle( + D2D1::RoundedRect( + Convert(border_rect), + GetBorderProperty().GetRadiusX(), + GetBorderProperty().GetRadiusY() + ), + GetBorderProperty().GetBrush().Get(), + GetBorderProperty().GetStrokeWidth(), + GetBorderProperty().GetStrokeStyle().Get() + ); + } + + //draw background. + const auto padding_rect = GetRect(RectRange::Padding); + graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(padding_rect.left, padding_rect.top), + [this](ID2D1DeviceContext* device_context) + { + OnDrawBackground(device_context); + DrawEventArgs args(this, this, device_context); + draw_background_event.Raise(args); + }); + + + const auto rect = GetRect(RectRange::Content); + graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(rect.left, rect.top), + [this](ID2D1DeviceContext* device_context) + { + OnDrawContent(device_context); + DrawEventArgs args(this, this, device_context); + draw_content_event.Raise(args); + }); + + graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(padding_rect.left, padding_rect.top), + [this](ID2D1DeviceContext* device_context) + { + OnDrawForeground(device_context); + DrawEventArgs args(this, this, device_context); + draw_foreground_event.Raise(args); + }); + } + + void Control::OnDrawContent(ID2D1DeviceContext * device_context) + { + + } + + void Control::OnDrawForeground(ID2D1DeviceContext* device_context) + { + + } + + void Control::OnDrawBackground(ID2D1DeviceContext* device_context) + { + + } + + void Control::OnPositionChanged(PositionChangedEventArgs & args) + { + + } + + void Control::OnSizeChanged(SizeChangedEventArgs & args) + { + } + + void Control::OnPositionChangedCore(PositionChangedEventArgs & args) + { + + } + + namespace + { +#ifdef CRU_DEBUG_LAYOUT + Microsoft::WRL::ComPtr<ID2D1Geometry> CalculateSquareRingGeometry(const Rect& out, const Rect& in) + { + const auto d2d1_factory = graph::GraphManager::GetInstance()->GetD2D1Factory(); + Microsoft::WRL::ComPtr<ID2D1RectangleGeometry> out_geometry; + ThrowIfFailed(d2d1_factory->CreateRectangleGeometry(Convert(out), &out_geometry)); + Microsoft::WRL::ComPtr<ID2D1RectangleGeometry> in_geometry; + ThrowIfFailed(d2d1_factory->CreateRectangleGeometry(Convert(in), &in_geometry)); + Microsoft::WRL::ComPtr<ID2D1PathGeometry> result_geometry; + ThrowIfFailed(d2d1_factory->CreatePathGeometry(&result_geometry)); + Microsoft::WRL::ComPtr<ID2D1GeometrySink> sink; + ThrowIfFailed(result_geometry->Open(&sink)); + ThrowIfFailed(out_geometry->CombineWithGeometry(in_geometry.Get(), D2D1_COMBINE_MODE_EXCLUDE, D2D1::Matrix3x2F::Identity(), sink.Get())); + ThrowIfFailed(sink->Close()); + return result_geometry; + } +#endif + } + + void Control::OnSizeChangedCore(SizeChangedEventArgs & args) + { +#ifdef CRU_DEBUG_LAYOUT + margin_geometry_ = CalculateSquareRingGeometry(GetRect(RectRange::Margin), GetRect(RectRange::FullBorder)); + padding_geometry_ = CalculateSquareRingGeometry(GetRect(RectRange::Padding), GetRect(RectRange::Content)); +#endif + } + + void Control::RaisePositionChangedEvent(PositionChangedEventArgs& args) + { + OnPositionChangedCore(args); + OnPositionChanged(args); + position_changed_event.Raise(args); + } + + void Control::RaiseSizeChangedEvent(SizeChangedEventArgs& args) + { + OnSizeChangedCore(args); + OnSizeChanged(args); + size_changed_event.Raise(args); + } + + void Control::OnMouseEnter(MouseEventArgs & args) + { + } + + void Control::OnMouseLeave(MouseEventArgs & args) + { + } + + void Control::OnMouseMove(MouseEventArgs & args) + { + } + + void Control::OnMouseDown(MouseButtonEventArgs & args) + { + } + + void Control::OnMouseUp(MouseButtonEventArgs & args) + { + } + + void Control::OnMouseClick(MouseButtonEventArgs& args) + { + + } + + void Control::OnMouseEnterCore(MouseEventArgs & args) + { + is_mouse_inside_ = true; + } + + void Control::OnMouseLeaveCore(MouseEventArgs & args) + { + is_mouse_inside_ = false; + for (auto& is_mouse_click_valid : is_mouse_click_valid_map_) + { + if (is_mouse_click_valid.second) + { + is_mouse_click_valid.second = false; + OnMouseClickEnd(is_mouse_click_valid.first); + } + } + } + + void Control::OnMouseMoveCore(MouseEventArgs & args) + { + + } + + void Control::OnMouseDownCore(MouseButtonEventArgs & args) + { + if (is_focus_on_pressed_ && args.GetSender() == args.GetOriginalSender()) + RequestFocus(); + is_mouse_click_valid_map_[args.GetMouseButton()] = true; + OnMouseClickBegin(args.GetMouseButton()); + } + + void Control::OnMouseUpCore(MouseButtonEventArgs & args) + { + if (is_mouse_click_valid_map_[args.GetMouseButton()]) + { + is_mouse_click_valid_map_[args.GetMouseButton()] = false; + RaiseMouseClickEvent(args); + OnMouseClickEnd(args.GetMouseButton()); + } + } + + void Control::OnMouseClickCore(MouseButtonEventArgs& args) + { + + } + + void Control::RaiseMouseEnterEvent(MouseEventArgs& args) + { + OnMouseEnterCore(args); + OnMouseEnter(args); + mouse_enter_event.Raise(args); + } + + void Control::RaiseMouseLeaveEvent(MouseEventArgs& args) + { + OnMouseLeaveCore(args); + OnMouseLeave(args); + mouse_leave_event.Raise(args); + } + + void Control::RaiseMouseMoveEvent(MouseEventArgs& args) + { + OnMouseMoveCore(args); + OnMouseMove(args); + mouse_move_event.Raise(args); + } + + void Control::RaiseMouseDownEvent(MouseButtonEventArgs& args) + { + OnMouseDownCore(args); + OnMouseDown(args); + mouse_down_event.Raise(args); + } + + void Control::RaiseMouseUpEvent(MouseButtonEventArgs& args) + { + OnMouseUpCore(args); + OnMouseUp(args); + mouse_up_event.Raise(args); + } + + void Control::RaiseMouseClickEvent(MouseButtonEventArgs& args) + { + OnMouseClickCore(args); + OnMouseClick(args); + mouse_click_event.Raise(args); + } + + void Control::OnMouseClickBegin(MouseButton button) + { + + } + + void Control::OnMouseClickEnd(MouseButton button) + { + + } + + void Control::OnKeyDown(KeyEventArgs& args) + { + } + + void Control::OnKeyUp(KeyEventArgs& args) + { + } + + void Control::OnChar(CharEventArgs& args) + { + } + + void Control::OnKeyDownCore(KeyEventArgs& args) + { + } + + void Control::OnKeyUpCore(KeyEventArgs& args) + { + } + + void Control::OnCharCore(CharEventArgs& args) + { + } + + void Control::RaiseKeyDownEvent(KeyEventArgs& args) + { + OnKeyDownCore(args); + OnKeyDown(args); + key_down_event.Raise(args); + } + + void Control::RaiseKeyUpEvent(KeyEventArgs& args) + { + OnKeyUpCore(args); + OnKeyUp(args); + key_up_event.Raise(args); + } + + void Control::RaiseCharEvent(CharEventArgs& args) + { + OnCharCore(args); + OnChar(args); + char_event.Raise(args); + } + + void Control::OnGetFocus(FocusChangeEventArgs& args) + { + + } + + void Control::OnLoseFocus(FocusChangeEventArgs& args) + { + + } + + void Control::OnGetFocusCore(FocusChangeEventArgs& args) + { + + } + + void Control::OnLoseFocusCore(FocusChangeEventArgs& args) + { + + } + + void Control::RaiseGetFocusEvent(FocusChangeEventArgs& args) + { + OnGetFocusCore(args); + OnGetFocus(args); + get_focus_event.Raise(args); + } + + void Control::RaiseLoseFocusEvent(FocusChangeEventArgs& args) + { + OnLoseFocusCore(args); + OnLoseFocus(args); + lose_focus_event.Raise(args); + } + + inline Size ThicknessToSize(const Thickness& thickness) + { + return Size(thickness.left + thickness.right, thickness.top + thickness.bottom); + } + + inline float AtLeast0(const float value) + { + return value < 0 ? 0 : value; + } + + Size Control::OnMeasureCore(const Size& available_size) + { + const auto layout_params = GetLayoutParams(); + + if (!layout_params->Validate()) + throw std::runtime_error("LayoutParams is not valid. Please check it."); + + auto border_size = Size::Zero(); + if (is_bordered_) + { + const auto border_width = GetBorderProperty().GetStrokeWidth(); + border_size = Size(border_width * 2.0f, border_width * 2.0f); + } + + // the total size of padding, border and margin + const auto outer_size = ThicknessToSize(layout_params->padding) + + ThicknessToSize(layout_params->margin) + border_size; + + + auto&& get_content_measure_length = [](const LayoutSideParams& layout_length, const float available_length, const float outer_length) -> float + { + float length; + if (layout_length.mode == MeasureMode::Exactly) + length = layout_length.length; + else if (available_length > outer_length) + length = available_length - outer_length; + else + length = 0; + return Coerce(length, layout_length.min, layout_length.max); + }; + + // if padding, margin and border exceeded, then content size is 0. + const auto content_measure_size = Size( + get_content_measure_length(layout_params->width, available_size.width, outer_size.width), + get_content_measure_length(layout_params->height, available_size.height, outer_size.height) + ); + + const auto content_actual_size = OnMeasureContent(content_measure_size); + + auto stretch_width = false; + auto stretch_width_determined = true; + auto stretch_height = false; + auto stretch_height_determined = true; + + // if it is stretch, init is stretch, and undetermined. + if (layout_params->width.mode == MeasureMode::Stretch) + { + stretch_width = true; + stretch_width_determined = false; + } + if (layout_params->height.mode == MeasureMode::Stretch) + { + stretch_height = true; + stretch_width_determined = false; + } + + if (!stretch_width_determined || !stretch_height_determined) + { + auto parent = GetParent(); + while (parent != nullptr) + { + auto lp = parent->GetLayoutParams(); + + if (!stretch_width_determined) + { + if (lp->width.mode == MeasureMode::Content) // if the first ancestor that is not stretch is content, then it can't stretch. + { + stretch_width = false; + stretch_width_determined = true; + } + if (lp->width.mode == MeasureMode::Exactly) // if the first ancestor that is not stretch is content, then it must be stretch. + { + stretch_width = true; + stretch_width_determined = true; + } + } + + if (!stretch_height_determined) // the same as width + { + if (lp->height.mode == MeasureMode::Content) // if the first ancestor that is not stretch is content, then it can't stretch. + { + stretch_height = false; + stretch_height_determined = true; + } + if (lp->height.mode == MeasureMode::Exactly) // if the first ancestor that is not stretch is content, then it must be stretch. + { + stretch_height = true; + stretch_height_determined = true; + } + } + + if (stretch_width_determined && stretch_height_determined) // if both are determined. + break; + + parent = GetParent(); + } + } + + auto&& calculate_final_length = [](const bool stretch, const std::optional<float> min_length, const float measure_length, const float actual_length) -> float + { + // only use measure length when stretch and actual length is smaller than measure length, that is "stretch" + if (stretch && actual_length < measure_length) + return measure_length; + return Coerce(actual_length, min_length, std::nullopt); + }; + + const auto final_size = Size( + calculate_final_length(stretch_width, layout_params->width.min, content_measure_size.width, content_actual_size.width), + calculate_final_length(stretch_height, layout_params->height.min, content_measure_size.height, content_actual_size.height) + ) + outer_size; + + return final_size; + } + + void Control::OnLayoutCore(const Rect& rect) + { + const auto layout_params = GetLayoutParams(); + + auto border_width = 0.0f; + if (is_bordered_) + { + border_width = GetBorderProperty().GetStrokeWidth(); + } + + const Rect content_rect( + rect.left + layout_params->padding.left + layout_params->margin.right + border_width, + rect.top + layout_params->padding.top + layout_params->margin.top + border_width, + rect.width - layout_params->padding.GetHorizontalTotal() - layout_params->margin.GetHorizontalTotal() - border_width * 2.0f, + rect.height - layout_params->padding.GetVerticalTotal() - layout_params->margin.GetVerticalTotal() - border_width * 2.0f + ); + + if (content_rect.width < 0.0) + throw std::runtime_error(Format("Width to layout must sufficient. But in {}, width for content is {}.", ToUtf8String(GetControlType()), content_rect.width)); + if (content_rect.height < 0.0) + throw std::runtime_error(Format("Height to layout must sufficient. But in {}, height for content is {}.", ToUtf8String(GetControlType()), content_rect.height)); + + OnLayoutContent(content_rect); + } + + Size Control::OnMeasureContent(const Size& available_size) + { + auto max_child_size = Size::Zero(); + for (auto control: GetChildren()) + { + control->Measure(available_size); + const auto&& size = control->GetDesiredSize(); + if (max_child_size.width < size.width) + max_child_size.width = size.width; + if (max_child_size.height < size.height) + max_child_size.height = size.height; + } + + // coerce size fro stretch. + for (auto control: GetChildren()) + { + auto size = control->GetDesiredSize(); + const auto layout_params = control->GetLayoutParams(); + if (layout_params->width.mode == MeasureMode::Stretch) + size.width = max_child_size.width; + if (layout_params->height.mode == MeasureMode::Stretch) + size.height = max_child_size.height; + control->SetDesiredSize(size); + } + + return max_child_size; + } + + void Control::OnLayoutContent(const Rect& rect) + { + for (auto control: GetChildren()) + { + const auto layout_params = control->GetLayoutParams(); + const auto size = control->GetDesiredSize(); + + auto&& calculate_anchor = [](const float anchor, const Alignment alignment, const float layout_length, const float control_length) -> float + { + switch (alignment) + { + case Alignment::Center: + return anchor + (layout_length - control_length) / 2; + case Alignment::Start: + return anchor; + case Alignment::End: + return anchor + layout_length - control_length; + default: + UnreachableCode(); + } + }; + + control->Layout(Rect(Point( + calculate_anchor(rect.left, layout_params->width.alignment, rect.width, size.width), + calculate_anchor(rect.top, layout_params->height.alignment, rect.height, size.height) + ), size)); + } + } + + void Control::CheckAndNotifyPositionChanged() + { + if (this->old_position_ != this->position_) + { + PositionChangedEventArgs args(this, this, this->old_position_, this->position_); + this->RaisePositionChangedEvent(args); + this->old_position_ = this->position_; + } + } + + 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; + } + } + + Control * IsAncestorOrDescendant(Control * left, Control * right) + { + //Search up along the trunk from "left". Return if find "right". + auto control = left; + while (control != nullptr) + { + if (control == right) + return control; + control = control->GetParent(); + } + //Search up along the trunk from "right". Return if find "left". + control = right; + while (control != nullptr) + { + if (control == left) + return control; + control = control->GetParent(); + } + return nullptr; + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\control.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\cursor.cpp +//-------------------------------------------------------- + + +namespace cru::ui +{ + Cursor::Cursor(HCURSOR handle, const bool auto_release) + : handle_(handle), auto_release_(auto_release) + { + + } + + Cursor::~Cursor() + { + if (auto_release_) + ::DestroyCursor(handle_); + } + + namespace cursors + { + Cursor::Ptr arrow{}; + Cursor::Ptr hand{}; + Cursor::Ptr i_beam{}; + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\cursor.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\layout_base.cpp +//-------------------------------------------------------- + + +namespace cru::ui +{ + LayoutManager* LayoutManager::GetInstance() + { + return Application::GetInstance()->ResolveSingleton<LayoutManager>([](auto) + { + return new LayoutManager{}; + }); + } + + void LayoutManager::InvalidateControlPositionCache(Control * control) + { + if (cache_invalid_controls_.count(control) == 1) + return; + + // find descendant then erase it; find ancestor then just return. + auto i = cache_invalid_controls_.cbegin(); + while (i != cache_invalid_controls_.cend()) + { + auto current_i = i++; + const auto result = IsAncestorOrDescendant(*current_i, control); + if (result == control) + cache_invalid_controls_.erase(current_i); + else if (result != nullptr) + return; // find a ancestor of "control", just return + } + + cache_invalid_controls_.insert(control); + + if (cache_invalid_controls_.size() == 1) // when insert just now and not repeat to "InvokeLater". + { + InvokeLater([this] { + for (const auto i : cache_invalid_controls_) + RefreshControlPositionCache(i); + for (const auto i : cache_invalid_controls_) // traverse all descendants of position-invalid controls and notify position change event + i->TraverseDescendants([](Control* control) + { + control->CheckAndNotifyPositionChanged(); + }); + cache_invalid_controls_.clear(); // after update and notify, clear the set. + + }); + } + } + + void LayoutManager::RefreshInvalidControlPositionCache() + { + for (const auto i : cache_invalid_controls_) + RefreshControlPositionCache(i); + cache_invalid_controls_.clear(); + } + + void LayoutManager::RefreshControlPositionCache(Control * control) + { + auto point = Point::Zero(); + auto parent = control; + while ((parent = parent->GetParent())) { + const auto p = parent->GetPositionRelative(); + point.x += p.x; + point.y += p.y; + } + RefreshControlPositionCacheInternal(control, point); + } + + void LayoutManager::RefreshControlPositionCacheInternal(Control * control, const Point & parent_lefttop_absolute) + { + const auto position = control->GetPositionRelative(); + const Point lefttop( + parent_lefttop_absolute.x + position.x, + parent_lefttop_absolute.y + position.y + ); + control->position_cache_.lefttop_position_absolute = lefttop; + for(auto c : control->GetChildren()) + { + RefreshControlPositionCacheInternal(c, lefttop); + } + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\layout_base.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\ui_base.cpp +//-------------------------------------------------------- + + +namespace cru::ui +{ + bool IsKeyDown(const int virtual_code) + { + const auto result = ::GetKeyState(virtual_code); + return (static_cast<unsigned short>(result) & 0x8000) != 0; + } + + bool IsKeyToggled(const int virtual_code) + { + const auto result = ::GetKeyState(virtual_code); + return (static_cast<unsigned short>(result) & 1) != 0; + } + + bool IsAnyMouseButtonDown() + { + return IsKeyDown(VK_LBUTTON) || IsKeyDown(VK_RBUTTON) || IsKeyDown(VK_MBUTTON); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\ui_base.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\ui_manager.cpp +//-------------------------------------------------------- + + + +namespace cru::ui +{ + namespace + { + void GetSystemCaretInfo(CaretInfo* caret_info) + { + caret_info->caret_blink_duration = std::chrono::milliseconds(::GetCaretBlinkTime()); + DWORD caret_width; + if (!::SystemParametersInfoW(SPI_GETCARETWIDTH, 0 , &caret_width, 0)) + throw Win32Error(::GetLastError(), "Failed to get system caret width."); + caret_info->half_caret_width = caret_width / 2.0f; + } + + Microsoft::WRL::ComPtr<ID2D1Brush> CreateSolidBrush(graph::GraphManager* graph_manager, const D2D1_COLOR_F& color) + { + const auto device_context = graph_manager->GetD2D1DeviceContext(); + Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> solid_color_brush; + device_context->CreateSolidColorBrush(color, &solid_color_brush); + return solid_color_brush; + } + + Microsoft::WRL::ComPtr<IDWriteTextFormat> CreateDefaultTextFormat(graph::GraphManager* graph_manager) + { + const auto dwrite_factory = graph_manager->GetDWriteFactory(); + + Microsoft::WRL::ComPtr<IDWriteTextFormat> text_format; + + ThrowIfFailed(dwrite_factory->CreateTextFormat( + L"ç‰çº¿", nullptr, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + 24.0, L"zh-cn", + &text_format + )); + + ThrowIfFailed(text_format->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER)); + ThrowIfFailed(text_format->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER)); + + return text_format; + } + } + + + //!!! never use default constructor of border at here, because it will recursively call this method! + PredefineResources::PredefineResources(graph::GraphManager* graph_manager) : + border_property_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Black))}, + + button_normal_border {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::RoyalBlue)), 2, 6, 6}, + button_press_border {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Blue)), 2, 6, 6}, + + text_control_selection_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::LightSkyBlue))}, + + text_box_border {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Black))}, + text_box_text_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Black))}, + text_box_text_format {CreateDefaultTextFormat(graph_manager)}, + text_box_caret_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Black))}, + + text_block_text_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Black))}, + text_block_text_format {CreateDefaultTextFormat(graph_manager)}, + + toggle_button_on_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::DeepSkyBlue))}, + toggle_button_off_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::LightGray))}, + + list_item_normal_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::White, 0))}, + list_item_normal_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::White, 0))}, + list_item_hover_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue))}, + list_item_hover_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))}, + list_item_select_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::MediumBlue))}, + list_item_select_fill_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.3f))} + +#ifdef CRU_DEBUG_LAYOUT + , + debug_layout_out_border_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::Crimson))}, + debug_layout_margin_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::LightCoral, 0.25f))}, + debug_layout_padding_brush {CreateSolidBrush(graph_manager, D2D1::ColorF(D2D1::ColorF::SkyBlue, 0.25f))} +#endif + { + + } + + UiManager* UiManager::GetInstance() + { + return Application::GetInstance()->ResolveSingleton<UiManager>([](auto) + { + return new UiManager{}; + }); + } + + UiManager::UiManager() + : predefine_resources_(graph::GraphManager::GetInstance()) + { + GetSystemCaretInfo(&caret_info_); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\ui_manager.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\window.cpp +//-------------------------------------------------------- + + +namespace cru::ui +{ + WindowClass::WindowClass(const String& name, WNDPROC window_proc, HINSTANCE h_instance) + : name_(name) + { + WNDCLASSEX window_class; + window_class.cbSize = sizeof(WNDCLASSEX); + + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = window_proc; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = h_instance; + window_class.hIcon = LoadIcon(NULL, IDI_APPLICATION); + window_class.hCursor = LoadCursor(NULL, IDC_ARROW); + window_class.hbrBackground = GetSysColorBrush(COLOR_BTNFACE); + window_class.lpszMenuName = NULL; + window_class.lpszClassName = name.c_str(); + window_class.hIconSm = NULL; + + atom_ = RegisterClassEx(&window_class); + if (atom_ == 0) + throw std::runtime_error("Failed to create window class."); + } + + LRESULT __stdcall GeneralWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) { + auto window = WindowManager::GetInstance()->FromHandle(hWnd); + + LRESULT result; + if (window != nullptr && window->HandleWindowMessage(hWnd, Msg, wParam, lParam, result)) + return result; + + return DefWindowProc(hWnd, Msg, wParam, lParam); + } + + WindowManager* WindowManager::GetInstance() + { + return Application::GetInstance()->ResolveSingleton<WindowManager>([](auto) + { + return new WindowManager{}; + }); + } + + WindowManager::WindowManager() { + general_window_class_ = std::make_unique<WindowClass>( + L"CruUIWindowClass", + GeneralWndProc, + Application::GetInstance()->GetInstanceHandle() + ); + } + + void WindowManager::RegisterWindow(HWND hwnd, Window * window) { + const auto find_result = window_map_.find(hwnd); + if (find_result != window_map_.end()) + throw std::runtime_error("The hwnd is already in the map."); + + window_map_.emplace(hwnd, window); + } + + void WindowManager::UnregisterWindow(HWND hwnd) { + const auto find_result = window_map_.find(hwnd); + if (find_result == window_map_.end()) + throw std::runtime_error("The hwnd is not in the map."); + window_map_.erase(find_result); + + if (window_map_.empty()) + Application::GetInstance()->Quit(0); + } + + Window* WindowManager::FromHandle(HWND hwnd) { + const auto find_result = window_map_.find(hwnd); + if (find_result == window_map_.end()) + return nullptr; + else + return find_result->second; + } + + std::vector<Window*> WindowManager::GetAllWindows() const + { + std::vector<Window*> windows; + for (auto [key, value] : window_map_) + windows.push_back(value); + return windows; + } + + inline Point PiToDip(const POINT& pi_point) + { + return Point( + graph::PixelToDipX(pi_point.x), + graph::PixelToDipY(pi_point.y) + ); + } + + inline POINT DipToPi(const Point& dip_point) + { + POINT result; + result.x = graph::DipToPixelX(dip_point.x); + result.y = graph::DipToPixelY(dip_point.y); + return result; + } + + + namespace + { + Cursor::Ptr GetCursorInherit(Control* control) + { + while (control != nullptr) + { + const auto cursor = control->GetCursor(); + if (cursor != nullptr) + return cursor; + control = control->GetParent(); + } + return cursors::arrow; + } + } + + Window* Window::CreateOverlapped() + { + return new Window(tag_overlapped_constructor{}); + } + + Window* Window::CreatePopup(Window* parent, const bool caption) + { + return new Window(tag_popup_constructor{}, parent, caption); + } + + Window::Window(tag_overlapped_constructor) : Control(WindowConstructorTag{}, this), control_list_({ this }) { + const auto window_manager = WindowManager::GetInstance(); + + hwnd_ = CreateWindowEx(0, + window_manager->GetGeneralWindowClass()->GetName(), + L"", WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + nullptr, nullptr, Application::GetInstance()->GetInstanceHandle(), nullptr + ); + + if (hwnd_ == nullptr) + throw std::runtime_error("Failed to create window."); + + AfterCreateHwnd(window_manager); + } + + Window::Window(tag_popup_constructor, Window* parent, const bool caption) : Control(WindowConstructorTag{}, this), control_list_({ this }) + { + if (parent != nullptr && !parent->IsWindowValid()) + throw std::runtime_error("Parent window is not valid."); + + parent_window_ = parent; + + const auto window_manager = WindowManager::GetInstance(); + + hwnd_ = CreateWindowEx(0, + window_manager->GetGeneralWindowClass()->GetName(), + L"", caption ? (WS_POPUPWINDOW | WS_CAPTION) : WS_POPUP, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + parent == nullptr ? nullptr : parent->GetWindowHandle(), + nullptr, Application::GetInstance()->GetInstanceHandle(), nullptr + ); + + if (hwnd_ == nullptr) + throw std::runtime_error("Failed to create window."); + + AfterCreateHwnd(window_manager); + } + + void Window::AfterCreateHwnd(WindowManager* window_manager) + { + window_manager->RegisterWindow(hwnd_, this); + + render_target_ = graph::GraphManager::GetInstance()->CreateWindowRenderTarget(hwnd_); + + SetCursor(cursors::arrow); + } + + Window::~Window() { + if (IsWindowValid()) + { + SetDeleteThisOnDestroy(false); // avoid double delete. + Close(); + } + TraverseDescendants([this](Control* control) { + control->OnDetachToWindow(this); + }); + } + + StringView Window::GetControlType() const + { + return control_type; + } + + void Window::SetDeleteThisOnDestroy(bool value) + { + delete_this_on_destroy_ = value; + } + + void Window::Close() { + if (IsWindowValid()) + DestroyWindow(hwnd_); + } + + void Window::Repaint() { + if (IsWindowValid()) { + InvalidateRect(hwnd_, nullptr, false); + } + } + + void Window::Show() { + if (IsWindowValid()) { + ShowWindow(hwnd_, SW_SHOWNORMAL); + } + } + + void Window::Hide() { + if (IsWindowValid()) { + ShowWindow(hwnd_, SW_HIDE); + } + } + + Size Window::GetClientSize() { + if (!IsWindowValid()) + return Size(); + + const auto pixel_rect = GetClientRectPixel(); + return Size( + graph::PixelToDipX(pixel_rect.right), + graph::PixelToDipY(pixel_rect.bottom) + ); + } + + void Window::SetClientSize(const Size & size) { + if (IsWindowValid()) { + const auto window_style = static_cast<DWORD>(GetWindowLongPtr(hwnd_, GWL_STYLE)); + const auto window_ex_style = static_cast<DWORD>(GetWindowLongPtr(hwnd_, GWL_EXSTYLE)); + + RECT rect; + rect.left = 0; + rect.top = 0; + rect.right = graph::DipToPixelX(size.width); + rect.bottom = graph::DipToPixelY(size.height); + AdjustWindowRectEx(&rect, window_style, FALSE, window_ex_style); + + SetWindowPos( + hwnd_, nullptr, 0, 0, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_NOZORDER | SWP_NOMOVE + ); + } + } + + Rect Window::GetWindowRect() { + if (!IsWindowValid()) + return Rect(); + + RECT rect; + ::GetWindowRect(hwnd_, &rect); + + return Rect::FromVertices( + graph::PixelToDipX(rect.left), + graph::PixelToDipY(rect.top), + graph::PixelToDipX(rect.right), + graph::PixelToDipY(rect.bottom) + ); + } + + void Window::SetWindowRect(const Rect & rect) { + if (IsWindowValid()) { + SetWindowPos( + hwnd_, nullptr, + graph::DipToPixelX(rect.left), + graph::DipToPixelY(rect.top), + graph::DipToPixelX(rect.GetRight()), + graph::DipToPixelY(rect.GetBottom()), + SWP_NOZORDER + ); + } + } + + void Window::SetWindowPosition(const Point& position) + { + if (IsWindowValid()) { + SetWindowPos( + hwnd_, nullptr, + graph::DipToPixelX(position.x), + graph::DipToPixelY(position.y), + 0, 0, + SWP_NOZORDER | SWP_NOSIZE + ); + } + } + + Point Window::PointToScreen(const Point& point) + { + if (!IsWindowValid()) + return Point::Zero(); + + auto p = DipToPi(point); + if (::ClientToScreen(GetWindowHandle(), &p) == 0) + throw Win32Error(::GetLastError(), "Failed transform point from window to screen."); + return PiToDip(p); + } + + Point Window::PointFromScreen(const Point& point) + { + if (!IsWindowValid()) + return Point::Zero(); + + auto p = DipToPi(point); + if (::ScreenToClient(GetWindowHandle(), &p) == 0) + throw Win32Error(::GetLastError(), "Failed transform point from screen to window."); + return PiToDip(p); + } + + bool Window::HandleWindowMessage(HWND hwnd, int msg, WPARAM w_param, LPARAM l_param, LRESULT & result) { + + if (!native_message_event.IsNoHandler()) + { + events::WindowNativeMessageEventArgs args(this, this, {hwnd, msg, w_param, l_param}); + native_message_event.Raise(args); + if (args.GetResult().has_value()) + { + result = args.GetResult().value(); + return true; + } + } + + switch (msg) { + case WM_PAINT: + OnPaintInternal(); + result = 0; + return true; + case WM_ERASEBKGND: + result = 1; + return true; + case WM_SETFOCUS: + OnSetFocusInternal(); + result = 0; + return true; + case WM_KILLFOCUS: + OnKillFocusInternal(); + result = 0; + return true; + case WM_MOUSEMOVE: + { + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + OnMouseMoveInternal(point); + result = 0; + return true; + } + case WM_LBUTTONDOWN: + { + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + OnMouseDownInternal(MouseButton::Left, point); + result = 0; + return true; + } + case WM_LBUTTONUP: + { + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + OnMouseUpInternal(MouseButton::Left, point); + result = 0; + return true; + } + case WM_RBUTTONDOWN: + { + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + OnMouseDownInternal(MouseButton::Right, point); + result = 0; + return true; + } + case WM_RBUTTONUP: + { + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + OnMouseUpInternal(MouseButton::Right, point); + result = 0; + return true; + } + case WM_MBUTTONDOWN: + { + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + OnMouseDownInternal(MouseButton::Middle, point); + result = 0; + return true; + } + case WM_MBUTTONUP: + { + POINT point; + point.x = GET_X_LPARAM(l_param); + point.y = GET_Y_LPARAM(l_param); + OnMouseUpInternal(MouseButton::Middle, point); + result = 0; + return true; + } + case WM_KEYDOWN: + OnKeyDownInternal(static_cast<int>(w_param)); + result = 0; + return true; + case WM_KEYUP: + OnKeyUpInternal(static_cast<int>(w_param)); + result = 0; + return true; + case WM_CHAR: + OnCharInternal(static_cast<wchar_t>(w_param)); + result = 0; + return true; + case WM_SIZE: + OnResizeInternal(LOWORD(l_param), HIWORD(l_param)); + result = 0; + return true; + case WM_ACTIVATE: + if (w_param == WA_ACTIVE || w_param == WA_CLICKACTIVE) + OnActivatedInternal(); + else if (w_param == WA_INACTIVE) + OnDeactivatedInternal(); + result = 0; + return true; + case WM_DESTROY: + OnDestroyInternal(); + result = 0; + return true; + default: + return false; + } + } + + Point Window::GetMousePosition() + { + if (!IsWindowValid()) + return Point::Zero(); + POINT point; + ::GetCursorPos(&point); + ::ScreenToClient(hwnd_, &point); + return PiToDip(point); + } + + Point Window::GetPositionRelative() + { + return Point(); + } + + void Window::SetPositionRelative(const Point & position) + { + + } + + Size Window::GetSize() + { + return GetClientSize(); + } + + void Window::SetSize(const Size & size) + { + + } + + void Window::WindowInvalidateLayout() + { + if (is_layout_invalid_) + return; + + is_layout_invalid_ = true; + InvokeLater([this] + { + if (is_layout_invalid_) + Relayout(); + }); + } + + void Window::Relayout() + { + Measure(GetSize()); + OnLayoutCore(Rect(Point::Zero(), GetSize())); + is_layout_invalid_ = false; + } + + void Window::SetSizeFitContent(const Size& max_size) + { + Measure(max_size); + SetClientSize(GetDesiredSize()); + OnLayoutCore(Rect(Point::Zero(), GetSize())); + is_layout_invalid_ = false; + } + + void Window::RefreshControlList() { + control_list_.clear(); + TraverseDescendants([this](Control* control) { + this->control_list_.push_back(control); + }); + } + + Control * Window::HitTest(const Point & point) + { + for (auto i = control_list_.crbegin(); i != control_list_.crend(); ++i) { + auto control = *i; + if (control->IsPointInside(control->WindowToControl(point))) { + return control; + } + } + return nullptr; + } + + bool Window::RequestFocusFor(Control * control) + { + if (control == nullptr) + throw std::invalid_argument("The control to request focus can't be null. You can set it as the window."); + + if (!IsWindowValid()) + return false; + + if (!window_focus_) + { + focus_control_ = control; + ::SetFocus(hwnd_); + return true; // event dispatch will be done in window message handling function "OnSetFocusInternal". + } + + if (focus_control_ == control) + return true; + + DispatchEvent(focus_control_, &Control::RaiseLoseFocusEvent, nullptr, false); + + focus_control_ = control; + + DispatchEvent(control, &Control::RaiseGetFocusEvent, nullptr, false); + + return true; + } + + Control* Window::GetFocusControl() + { + return focus_control_; + } + + Control* Window::CaptureMouseFor(Control* control) + { + if (control != nullptr) + { + ::SetCapture(hwnd_); + std::swap(mouse_capture_control_, control); + DispatchMouseHoverControlChangeEvent(control ? control : mouse_hover_control_, mouse_capture_control_, GetMousePosition()); + return control; + } + else + { + return ReleaseCurrentMouseCapture(); + } + } + + Control* Window::ReleaseCurrentMouseCapture() + { + if (mouse_capture_control_) + { + const auto previous = mouse_capture_control_; + mouse_capture_control_ = nullptr; + ::ReleaseCapture(); + DispatchMouseHoverControlChangeEvent(previous, mouse_hover_control_, GetMousePosition()); + return previous; + } + else + { + return nullptr; + } + } + + void Window::UpdateCursor() + { + if (IsWindowValid() && mouse_hover_control_ != nullptr) + { + SetCursorInternal(GetCursorInherit(mouse_hover_control_)->GetHandle()); + } + } + +#ifdef CRU_DEBUG_LAYOUT + void Window::SetDebugLayout(const bool value) + { + if (debug_layout_ != value) + { + debug_layout_ = value; + Repaint(); + } + } +#endif + + RECT Window::GetClientRectPixel() { + RECT rect{ }; + GetClientRect(hwnd_, &rect); + return rect; + } + + bool Window::IsMessageInQueue(UINT message) + { + MSG msg; + return ::PeekMessageW(&msg, hwnd_, message, message, PM_NOREMOVE) != 0; + } + + void Window::SetCursorInternal(HCURSOR cursor) + { + if (IsWindowValid()) + { + ::SetClassLongPtrW(GetWindowHandle(), GCLP_HCURSOR, reinterpret_cast<LONG_PTR>(cursor)); + if (mouse_hover_control_ != nullptr) + ::SetCursor(cursor); + } + } + + void Window::OnDestroyInternal() { + WindowManager::GetInstance()->UnregisterWindow(hwnd_); + hwnd_ = nullptr; + if (delete_this_on_destroy_) + InvokeLater([this]{ delete this; }); + } + + void Window::OnPaintInternal() { + render_target_->SetAsTarget(); + + auto device_context = render_target_->GetD2DDeviceContext(); + + device_context->BeginDraw(); + + //Clear the background. + device_context->Clear(D2D1::ColorF(D2D1::ColorF::White)); + + Draw(device_context.Get()); + + ThrowIfFailed( + device_context->EndDraw(), "Failed to draw window." + ); + + render_target_->Present(); + + ValidateRect(hwnd_, nullptr); + } + + void Window::OnResizeInternal(const int new_width, const int new_height) { + render_target_->ResizeBuffer(new_width, new_height); + if (!(new_width == 0 && new_height == 0)) + WindowInvalidateLayout(); + } + + void Window::OnSetFocusInternal() + { + window_focus_ = true; + DispatchEvent(focus_control_, &Control::RaiseGetFocusEvent, nullptr, true); + } + + void Window::OnKillFocusInternal() + { + window_focus_ = false; + DispatchEvent(focus_control_, &Control::RaiseLoseFocusEvent, nullptr, true); + } + + void Window::OnMouseMoveInternal(const POINT point) + { + const auto dip_point = PiToDip(point); + + //when mouse was previous outside the window + if (mouse_hover_control_ == nullptr) { + //invoke TrackMouseEvent to have WM_MOUSELEAVE sent. + TRACKMOUSEEVENT tme; + tme.cbSize = sizeof tme; + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = hwnd_; + + TrackMouseEvent(&tme); + } + + //Find the first control that hit test succeed. + const auto new_control_mouse_hover = HitTest(dip_point); + const auto old_control_mouse_hover = mouse_hover_control_; + mouse_hover_control_ = new_control_mouse_hover; + + if (mouse_capture_control_) // if mouse is captured + { + DispatchEvent(mouse_capture_control_, &Control::RaiseMouseMoveEvent, nullptr, dip_point); + } + else + { + DispatchMouseHoverControlChangeEvent(old_control_mouse_hover, new_control_mouse_hover, dip_point); + DispatchEvent(new_control_mouse_hover, &Control::RaiseMouseMoveEvent, nullptr, dip_point); + } + } + + void Window::OnMouseLeaveInternal() + { + DispatchEvent(mouse_hover_control_, &Control::RaiseMouseLeaveEvent, nullptr); + mouse_hover_control_ = nullptr; + } + + void Window::OnMouseDownInternal(MouseButton button, POINT point) + { + const auto dip_point = PiToDip(point); + + Control* control; + + if (mouse_capture_control_) + control = mouse_capture_control_; + else + control = HitTest(dip_point); + + DispatchEvent(control, &Control::RaiseMouseDownEvent, nullptr, dip_point, button); + } + + void Window::OnMouseUpInternal(MouseButton button, POINT point) + { + const auto dip_point = PiToDip(point); + + Control* control; + + if (mouse_capture_control_) + control = mouse_capture_control_; + else + control = HitTest(dip_point); + + DispatchEvent(control, &Control::RaiseMouseUpEvent, nullptr, dip_point, button); + } + + void Window::OnKeyDownInternal(int virtual_code) + { + DispatchEvent(focus_control_, &Control::RaiseKeyDownEvent, nullptr, virtual_code); + } + + void Window::OnKeyUpInternal(int virtual_code) + { + DispatchEvent(focus_control_, &Control::RaiseKeyUpEvent, nullptr, virtual_code); + } + + void Window::OnCharInternal(wchar_t c) + { + DispatchEvent(focus_control_, &Control::RaiseCharEvent, nullptr, c); + } + + void Window::OnActivatedInternal() + { + events::UiEventArgs args(this, this); + activated_event.Raise(args); + } + + void Window::OnDeactivatedInternal() + { + events::UiEventArgs args(this, this); + deactivated_event.Raise(args); + } + + void Window::DispatchMouseHoverControlChangeEvent(Control* old_control, Control* new_control, const Point& point) + { + if (new_control != old_control) //if the mouse-hover-on control changed + { + const auto lowest_common_ancestor = FindLowestCommonAncestor(old_control, new_control); + if (old_control != nullptr) // if last mouse-hover-on control exists + DispatchEvent(old_control, &Control::RaiseMouseLeaveEvent, lowest_common_ancestor); // dispatch mouse leave event. + if (new_control != nullptr) + { + DispatchEvent(new_control, &Control::RaiseMouseEnterEvent, lowest_common_ancestor, point); // dispatch mouse enter event. + UpdateCursor(); + } + } + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\window.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\animations\animation.cpp +//-------------------------------------------------------- + +#include <utility> + + +namespace cru::ui::animations +{ + namespace details + { + class AnimationDelegateImpl; + constexpr double frame_rate = 60; + constexpr AnimationTimeUnit frame_step_time = AnimationTimeUnit(1) / frame_rate; + + + class AnimationDelegateImpl : public virtual IAnimationDelegate + { + public: + explicit AnimationDelegateImpl(String tag) + : tag_(std::move(tag)) + { + + } + AnimationDelegateImpl(const AnimationDelegateImpl& other) = delete; + AnimationDelegateImpl(AnimationDelegateImpl&& other) = delete; + AnimationDelegateImpl& operator=(const AnimationDelegateImpl& other) = delete; + AnimationDelegateImpl& operator=(AnimationDelegateImpl&& other) = delete; + ~AnimationDelegateImpl() override = default; + + void Cancel() override + { + AnimationManager::GetInstance()->RemoveAnimation(tag_); + } + + private: + String tag_; + }; + + + class Animation : public Object + { + public: + Animation(AnimationInfo info, AnimationDelegatePtr delegate) + : info_(std::move(info)), delegate_(std::move(delegate)) + { + + } + + Animation(const Animation& other) = delete; + Animation(Animation&& other) = delete; + Animation& operator=(const Animation& other) = delete; + Animation& operator=(Animation&& other) = delete; + ~Animation() override; + + + // If finish or invalid, return false. + bool Step(AnimationTimeUnit time); + + String GetTag() const + { + return info_.tag; + } + + private: + const AnimationInfo info_; + const AnimationDelegatePtr delegate_; + + AnimationTimeUnit current_time_ = AnimationTimeUnit::zero(); + }; + + AnimationManager* AnimationManager::GetInstance() + { + return Application::GetInstance()->ResolveSingleton<AnimationManager>([](auto) + { + return new AnimationManager{}; + }); + } + + AnimationManager::AnimationManager() + { + + } + + AnimationManager::~AnimationManager() + { + KillTimer(); + } + + AnimationDelegatePtr AnimationManager::CreateAnimation(AnimationInfo info) + { + if (animations_.empty()) + SetTimer(); + + const auto tag = info.tag; + auto delegate = std::make_shared<AnimationDelegateImpl>(tag); + animations_[tag] = std::make_unique<Animation>(std::move(info), delegate); + + return delegate; + } + + void AnimationManager::RemoveAnimation(const String& tag) + { + const auto find_result = animations_.find(tag); + if (find_result != animations_.cend()) + animations_.erase(find_result); + + if (animations_.empty()) + KillTimer(); + } + + void AnimationManager::SetTimer() + { + if (!timer_.has_value()) + timer_ = SetInterval(std::chrono::duration_cast<std::chrono::milliseconds>(frame_step_time), [this]() + { + auto i = animations_.cbegin(); + while (i != animations_.cend()) + { + auto current_i = i++; + if (current_i->second->Step(frame_step_time)) + animations_.erase(current_i); + } + + if (animations_.empty()) + KillTimer(); + }); + } + + void AnimationManager::KillTimer() + { + if (timer_.has_value()) + { + timer_.value().Cancel(); + timer_ = std::nullopt; + } + } + + Animation::~Animation() + { + if (current_time_ < info_.duration) + for (const auto& handler : info_.cancel_handlers) + handler(); + } + + bool Animation::Step(const AnimationTimeUnit time) + { + current_time_ += time; + if (current_time_ > info_.duration) + { + for (const auto& handler : info_.step_handlers) + handler(delegate_, 1); + for (const auto& handler : info_.finish_handlers) + handler(); + return true; + } + else + { + for (const auto& handler : info_.step_handlers) + handler(delegate_, current_time_ / info_.duration); + return false; + } + } + + } + + AnimationDelegatePtr AnimationBuilder::Start() + { + CheckValid(); + valid_ = false; + return details::AnimationManager::GetInstance()->CreateAnimation(std::move(info_)); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\animations\animation.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\button.cpp +//-------------------------------------------------------- + + +namespace cru::ui::controls +{ + Button::Button() : Control(true), + normal_border_{UiManager::GetInstance()->GetPredefineResources()->button_normal_border}, + pressed_border_{UiManager::GetInstance()->GetPredefineResources()->button_press_border} + { + SetBordered(true); + GetBorderProperty() = normal_border_; + + SetCursor(cursors::hand); + } + + StringView Button::GetControlType() const + { + return control_type; + } + + void Button::OnMouseClickBegin(MouseButton button) + { + GetBorderProperty() = pressed_border_; + InvalidateBorder(); + } + + void Button::OnMouseClickEnd(MouseButton button) + { + GetBorderProperty() = normal_border_; + InvalidateBorder(); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\button.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\linear_layout.cpp +//-------------------------------------------------------- + +#include <algorithm> + +namespace cru::ui::controls +{ + LinearLayout::LinearLayout(const Orientation orientation) + : Control(true), orientation_(orientation) + { + + } + + inline float AtLeast0(const float value) + { + return value < 0 ? 0 : value; + } + + inline Size AtLeast0(const Size& size) + { + return Size(AtLeast0(size.width), AtLeast0(size.height)); + } + + StringView LinearLayout::GetControlType() const + { + return control_type; + } + + Size LinearLayout::OnMeasureContent(const Size& available_size) + { + auto actual_size_for_children = Size::Zero(); + + float secondary_side_child_max_length = 0; + + std::list<Control*> stretch_control_list; + + // First measure Content and Exactly and count Stretch. + if (orientation_ == Orientation::Horizontal) + for(auto control: GetChildren()) + { + const auto mode = control->GetLayoutParams()->width.mode; + if (mode == MeasureMode::Content || mode == MeasureMode::Exactly) + { + Size current_available_size(AtLeast0(available_size.width - actual_size_for_children.width), available_size.height); + control->Measure(current_available_size); + const auto size = control->GetDesiredSize(); + actual_size_for_children.width += size.width; + secondary_side_child_max_length = std::max(size.height, secondary_side_child_max_length); + } + else + stretch_control_list.push_back(control); + } + else + for(auto control: GetChildren()) + { + const auto mode = control->GetLayoutParams()->height.mode; + if (mode == MeasureMode::Content || mode == MeasureMode::Exactly) + { + Size current_available_size(available_size.width, AtLeast0(available_size.height - actual_size_for_children.height)); + control->Measure(current_available_size); + const auto size = control->GetDesiredSize(); + actual_size_for_children.height += size.height; + secondary_side_child_max_length = std::max(size.width, secondary_side_child_max_length); + } + else + stretch_control_list.push_back(control); + } + + if (orientation_ == Orientation::Horizontal) + { + const auto available_width = AtLeast0(available_size.width - actual_size_for_children.width) / stretch_control_list.size(); + for (const auto control : stretch_control_list) + { + control->Measure(Size(available_width, available_size.height)); + const auto size = control->GetDesiredSize(); + actual_size_for_children.width += size.width; + secondary_side_child_max_length = std::max(size.height, secondary_side_child_max_length); + } + } + else + { + const auto available_height = AtLeast0(available_size.height - actual_size_for_children.height) / stretch_control_list.size(); + for (const auto control : stretch_control_list) + { + control->Measure(Size(available_size.width, available_height)); + const auto size = control->GetDesiredSize(); + actual_size_for_children.height += size.height; + secondary_side_child_max_length = std::max(size.width, secondary_side_child_max_length); + } + } + + if (orientation_ == Orientation::Horizontal) + { + for (auto control : GetChildren()) + { + if (control->GetLayoutParams()->height.mode == MeasureMode::Stretch) + { + control->SetDesiredSize(Size(control->GetDesiredSize().width, secondary_side_child_max_length)); + } + } + actual_size_for_children.height = secondary_side_child_max_length; + } + else + { + for (auto control : GetChildren()) + { + if (control->GetLayoutParams()->width.mode == MeasureMode::Stretch) + { + control->SetDesiredSize(Size(secondary_side_child_max_length, control->GetDesiredSize().height)); + } + } + + actual_size_for_children.width = secondary_side_child_max_length; + } + + return actual_size_for_children; + } + + void LinearLayout::OnLayoutContent(const Rect& rect) + { + float current_main_side_anchor = 0; + for(auto control: GetChildren()) + { + const auto layout_params = control->GetLayoutParams(); + const auto size = control->GetDesiredSize(); + const auto alignment = orientation_ == Orientation::Horizontal ? layout_params->height.alignment : layout_params->width.alignment; + + auto&& calculate_secondary_side_anchor = [alignment](const float layout_length, const float control_length) -> float + { + switch (alignment) + { + case Alignment::Center: + return (layout_length - control_length) / 2; + case Alignment::Start: + return 0; + case Alignment::End: + return layout_length - control_length; + default: + UnreachableCode(); + } + }; + + auto&& calculate_rect = [rect, size](const float anchor_left, const float anchor_top) + { + return Rect(Point(rect.left + anchor_left, rect.top + anchor_top), size); + }; + + if (orientation_ == Orientation::Horizontal) + { + control->Layout(calculate_rect(current_main_side_anchor, calculate_secondary_side_anchor(rect.height, size.height))); + current_main_side_anchor += size.width; + } + else + { + control->Layout(calculate_rect(calculate_secondary_side_anchor(rect.width, size.width), current_main_side_anchor)); + current_main_side_anchor += size.height; + } + } + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\linear_layout.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\list_item.cpp +//-------------------------------------------------------- + + +namespace cru::ui::controls +{ + ListItem::ListItem() : Control(true) + { + const auto predefine_resources = UiManager::GetInstance()->GetPredefineResources(); + + brushes_[State::Normal].border_brush = predefine_resources->list_item_normal_border_brush; + brushes_[State::Normal].fill_brush = predefine_resources->list_item_normal_fill_brush; + brushes_[State::Hover] .border_brush = predefine_resources->list_item_hover_border_brush; + brushes_[State::Hover] .fill_brush = predefine_resources->list_item_hover_fill_brush; + brushes_[State::Select].border_brush = predefine_resources->list_item_select_border_brush; + brushes_[State::Select].fill_brush = predefine_resources->list_item_select_fill_brush; + } + + StringView ListItem::GetControlType() const + { + return control_type; + } + + void ListItem::SetState(const State state) + { + state_ = state; + Repaint(); + } + + void ListItem::OnDrawForeground(ID2D1DeviceContext* device_context) + { + const auto rect = Rect(Point::Zero(), GetRect(RectRange::Padding).GetSize()); + device_context->FillRectangle(Convert(rect), brushes_[state_].fill_brush.Get()); + device_context->DrawRectangle(Convert(rect.Shrink(Thickness(0.5))), brushes_[state_].border_brush.Get(), 1); + } + + void ListItem::OnMouseEnterCore(events::MouseEventArgs& args) + { + if (GetState() == State::Select) + return; + + if (IsAnyMouseButtonDown()) + return; + + SetState(State::Hover); + } + + void ListItem::OnMouseLeaveCore(events::MouseEventArgs& args) + { + if (GetState() == State::Select) + return; + + SetState(State::Normal); + } + + void ListItem::OnMouseClickCore(events::MouseButtonEventArgs& args) + { + if (args.GetMouseButton() == MouseButton::Left) + SetState(State::Select); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\list_item.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\popup_menu.cpp +//-------------------------------------------------------- + + +namespace cru::ui::controls +{ + Window* CreatePopupMenu(const Point& anchor, const std::vector<MenuItemInfo>& items, Window* parent) + { + const auto popup = Window::CreatePopup(parent); + + popup->lose_focus_event.AddHandler([popup](events::FocusChangeEventArgs& args) + { + if (args.IsWindow()) + popup->Close(); + }); + + const auto create_menu_item = [popup](const String& text, const std::function<void()>& action) -> ListItem* + { + auto text_block = TextBlock::Create(text); + text_block->GetLayoutParams()->width.alignment = Alignment::Start; + + auto list_item = CreateWithLayout<ListItem>( + LayoutSideParams::Stretch(Alignment::Center), + LayoutSideParams::Content(Alignment::Start), + ControlList{ text_block } + ); + + list_item->mouse_click_event.AddHandler([popup, action](events::MouseButtonEventArgs& args) + { + if (args.GetMouseButton() == MouseButton::Left) + { + action(); + popup->Close(); + } + }); + + return list_item; + }; + + const auto menu = LinearLayout::Create(LinearLayout::Orientation::Vertical); + + menu->SetBordered(true); + + for (const auto& item : items) + menu->AddChild(create_menu_item(item.first, item.second)); + + popup->AddChild(menu); + + popup->SetSizeFitContent(); + popup->SetWindowPosition(anchor); + + return popup; + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\popup_menu.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\text_block.cpp +//-------------------------------------------------------- + + +namespace cru::ui::controls +{ + TextBlock::TextBlock() : TextControl( + UiManager::GetInstance()->GetPredefineResources()->text_block_text_format, + UiManager::GetInstance()->GetPredefineResources()->text_block_text_brush + ) + { + + } + + StringView TextBlock::GetControlType() const + { + return control_type; + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\text_block.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\text_box.cpp +//-------------------------------------------------------- + +#include <cwctype> +#include <cassert> + + +namespace cru::ui::controls +{ + TextBox::TextBox() : TextControl( + UiManager::GetInstance()->GetPredefineResources()->text_box_text_format, + UiManager::GetInstance()->GetPredefineResources()->text_box_text_brush + ) + { + SetSelectable(true); + + caret_brush_ = UiManager::GetInstance()->GetPredefineResources()->text_box_caret_brush; + + GetBorderProperty() = UiManager::GetInstance()->GetPredefineResources()->text_box_border; + SetBordered(true); + } + + TextBox::~TextBox() = default; + + StringView TextBox::GetControlType() const + { + return control_type; + } + + void TextBox::OnDrawContent(ID2D1DeviceContext* device_context) + { + TextControl::OnDrawContent(device_context); + if (is_caret_show_) + { + const auto caret_half_width = UiManager::GetInstance()->GetCaretInfo().half_caret_width; + FLOAT x, y; + DWRITE_HIT_TEST_METRICS metrics{}; + ThrowIfFailed(text_layout_->HitTestTextPosition(caret_position_, FALSE, &x, &y, &metrics)); + device_context->FillRectangle(D2D1::RectF(metrics.left - caret_half_width, metrics.top, metrics.left + caret_half_width, metrics.top + metrics.height), caret_brush_.Get()); + } + } + + void TextBox::OnGetFocusCore(events::FocusChangeEventArgs& args) + { + TextControl::OnGetFocusCore(args); + assert(!caret_timer_.has_value()); + is_caret_show_ = true; + caret_timer_ = SetInterval(UiManager::GetInstance()->GetCaretInfo().caret_blink_duration, [this] + { + is_caret_show_ = !is_caret_show_; + Repaint(); + }); + } + + void TextBox::OnLoseFocusCore(events::FocusChangeEventArgs& args) + { + Control::OnLoseFocusCore(args); + assert(caret_timer_.has_value()); + caret_timer_->Cancel(); + caret_timer_ = std::nullopt; + is_caret_show_ = false; + } + + void TextBox::OnKeyDownCore(events::KeyEventArgs& args) + { + Control::OnKeyDownCore(args); + if (args.GetVirtualCode() == VK_LEFT && caret_position_ > 0) + { + if (IsKeyDown(VK_SHIFT)) + { + if (GetCaretSelectionSide()) + ShiftLeftSelectionRange(-1); + else + ShiftRightSelectionRange(-1); + } + else + { + const auto selection = GetSelectedRange(); + if (selection.has_value()) + { + ClearSelection(); + caret_position_ = selection.value().position; + } + else + caret_position_--; + } + Repaint(); + } + + if (args.GetVirtualCode() == VK_RIGHT && caret_position_ < GetText().size()) + { + if (IsKeyDown(VK_SHIFT)) + { + if (GetCaretSelectionSide()) + ShiftLeftSelectionRange(1); + else + ShiftRightSelectionRange(1); + } + else + { + const auto selection = GetSelectedRange(); + if (selection.has_value()) + { + ClearSelection(); + caret_position_ = selection.value().position + selection.value().count; + } + else + caret_position_++; + } + } + } + + void TextBox::OnCharCore(events::CharEventArgs& args) + { + Control::OnCharCore(args); + if (args.GetChar() == L'\b') + { + if (GetSelectedRange().has_value()) + { + const auto selection_range = GetSelectedRange().value(); + auto text = GetText(); + text.erase(text.cbegin() + selection_range.position, text.cbegin() + selection_range.position + selection_range.count); + SetText(text); + caret_position_ = selection_range.position; + ClearSelection(); + } + else + { + if (caret_position_ > 0) + { + auto text = GetText(); + if (!text.empty()) + { + const auto position = --caret_position_; + text.erase(text.cbegin() + position); + SetText(text); + } + } + } + return; + } + + if (std::iswprint(args.GetChar())) + { + if (GetSelectedRange().has_value()) + { + const auto selection_range = GetSelectedRange().value(); + auto text = GetText(); + text.erase(selection_range.position, selection_range.count); + text.insert(text.cbegin() + selection_range.position, args.GetChar()); + SetText(text); + caret_position_ = selection_range.position + 1; + ClearSelection(); + } + else + { + ClearSelection(); + const auto position = caret_position_++; + auto text = GetText(); + text.insert(text.cbegin() + position, { args.GetChar() }); + SetText(text); + } + } + } + + void TextBox::RequestChangeCaretPosition(const unsigned position) + { + caret_position_ = position; + Repaint(); + } + + bool TextBox::GetCaretSelectionSide() const + { + const auto selection = TextRange::ToTwoSides(GetSelectedRange(), caret_position_); + if (selection.first == caret_position_) + return true; + if (selection.second == caret_position_) + return false; + assert(false); + return true; + } + + void TextBox::ShiftLeftSelectionRange(const int count) + { + const auto selection_range_side = TextRange::ToTwoSides(GetSelectedRange(), caret_position_); + int new_left = selection_range_side.first + count; + new_left = new_left < 0 ? 0 : new_left; // at least 0 + caret_position_ = new_left; + SetSelectedRange(TextRange::FromTwoSides(static_cast<unsigned>(new_left), selection_range_side.second)); + } + + void TextBox::ShiftRightSelectionRange(const int count) + { + const auto selection_range_side = TextRange::ToTwoSides(GetSelectedRange(), caret_position_); + int new_right = selection_range_side.second + count; + new_right = new_right < 0 ? 0 : new_right; // at least 0 + caret_position_ = new_right; + SetSelectedRange(TextRange::FromTwoSides(selection_range_side.first, static_cast<unsigned>(new_right))); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\text_box.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\text_control.cpp +//-------------------------------------------------------- + +#include <cassert> + + +namespace cru::ui::controls +{ + TextControl::TextControl(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& init_text_format, + const Microsoft::WRL::ComPtr<ID2D1Brush>& init_brush) : Control(false) + { + text_format_ = init_text_format; + + RecreateTextLayout(); + + brush_ = init_brush; + + selection_brush_ = UiManager::GetInstance()->GetPredefineResources()->text_control_selection_brush; + } + + + void TextControl::SetText(const String& text) + { + if (text_ != text) + { + const auto old_text = text_; + text_ = text; + OnTextChangedCore(old_text, text); + } + } + + void TextControl::SetBrush(const Microsoft::WRL::ComPtr<ID2D1Brush>& brush) + { + brush_ = brush; + Repaint(); + } + + void TextControl::SetTextFormat(const Microsoft::WRL::ComPtr<IDWriteTextFormat>& text_format) + { + text_format_ = text_format; + RecreateTextLayout(); + Repaint(); + } + + void TextControl::SetSelectable(const bool is_selectable) + { + if (is_selectable_ != is_selectable) + { + if (!is_selectable) + { + if (is_selecting_) + { + is_selecting_ = false; + GetWindow()->ReleaseCurrentMouseCapture(); + } + selected_range_ = std::nullopt; + Repaint(); + } + is_selectable_ = is_selectable; + UpdateCursor(std::nullopt); + } + } + + void TextControl::SetSelectedRange(std::optional<TextRange> text_range) + { + if (is_selectable_) + { + selected_range_ = text_range; + Repaint(); + } + } + + void TextControl::OnSizeChangedCore(events::SizeChangedEventArgs& args) + { + Control::OnSizeChangedCore(args); + const auto content = GetRect(RectRange::Content); + ThrowIfFailed(text_layout_->SetMaxWidth(content.width)); + ThrowIfFailed(text_layout_->SetMaxHeight(content.height)); + Repaint(); + } + + namespace + { + unsigned TextLayoutHitTest(IDWriteTextLayout* text_layout, const Point& point) + { + BOOL is_trailing, is_inside; + DWRITE_HIT_TEST_METRICS metrics{}; + text_layout->HitTestPoint(point.x, point.y, &is_trailing, &is_inside, &metrics); + return is_trailing == 0 ? metrics.textPosition : metrics.textPosition + 1; + } + + void DrawSelectionRect(ID2D1DeviceContext* device_context, IDWriteTextLayout* layout, ID2D1Brush* brush, const std::optional<TextRange> range) + { + if (range.has_value()) + { + DWRITE_TEXT_METRICS text_metrics{}; + ThrowIfFailed(layout->GetMetrics(&text_metrics)); + const auto metrics_count = text_metrics.lineCount * text_metrics.maxBidiReorderingDepth; + + std::vector<DWRITE_HIT_TEST_METRICS> hit_test_metrics(metrics_count); + UINT32 actual_count; + layout->HitTestTextRange( + range.value().position, range.value().count, + 0, 0, + hit_test_metrics.data(), metrics_count, &actual_count + ); + + hit_test_metrics.erase(hit_test_metrics.cbegin() + actual_count, hit_test_metrics.cend()); + + for (const auto& metrics : hit_test_metrics) + device_context->FillRoundedRectangle(D2D1::RoundedRect(D2D1::RectF(metrics.left, metrics.top, metrics.left + metrics.width, metrics.top + metrics.height), 3, 3), brush); + } + } + } + void TextControl::OnDrawContent(ID2D1DeviceContext* device_context) + { + Control::OnDrawContent(device_context); + DrawSelectionRect(device_context, text_layout_.Get(), selection_brush_.Get(), selected_range_); + device_context->DrawTextLayout(D2D1::Point2F(), text_layout_.Get(), brush_.Get()); + } + + void TextControl::OnMouseDownCore(events::MouseButtonEventArgs& args) + { + Control::OnMouseDownCore(args); + if (is_selectable_ && args.GetMouseButton() == MouseButton::Left && GetRect(RectRange::Padding).IsPointInside(args.GetPoint(this, RectRange::Margin))) + { + selected_range_ = std::nullopt; + const auto hit_test_result = TextLayoutHitTest(text_layout_.Get(), args.GetPoint(this)); + RequestChangeCaretPosition(hit_test_result); + mouse_down_position_ = hit_test_result; + is_selecting_ = true; + GetWindow()->CaptureMouseFor(this); + Repaint(); + } + } + + void TextControl::OnMouseMoveCore(events::MouseEventArgs& args) + { + Control::OnMouseMoveCore(args); + if (is_selecting_) + { + const auto hit_test_result = TextLayoutHitTest(text_layout_.Get(), args.GetPoint(this)); + RequestChangeCaretPosition(hit_test_result); + selected_range_ = TextRange::FromTwoSides(hit_test_result, mouse_down_position_); + Repaint(); + } + UpdateCursor(args.GetPoint(this, RectRange::Margin)); + } + + void TextControl::OnMouseUpCore(events::MouseButtonEventArgs& args) + { + Control::OnMouseUpCore(args); + if (args.GetMouseButton() == MouseButton::Left) + { + if (is_selecting_) + { + is_selecting_ = false; + GetWindow()->ReleaseCurrentMouseCapture(); + } + } + } + + void TextControl::OnLoseFocusCore(events::FocusChangeEventArgs& args) + { + Control::OnLoseFocusCore(args); + if (is_selecting_) + { + is_selecting_ = false; + GetWindow()->ReleaseCurrentMouseCapture(); + } + if (!args.IsWindow()) // If the focus lose is triggered window-wide, then save the selection state. Otherwise, clear selection. + { + selected_range_ = std::nullopt; + Repaint(); + } + } + + + Size TextControl::OnMeasureContent(const Size& available_size) + { + ThrowIfFailed(text_layout_->SetMaxWidth(available_size.width)); + ThrowIfFailed(text_layout_->SetMaxHeight(available_size.height)); + + DWRITE_TEXT_METRICS metrics{}; + + ThrowIfFailed(text_layout_->GetMetrics(&metrics)); + + const Size measure_result(metrics.width, metrics.height); + + return measure_result; + } + + void TextControl::RequestChangeCaretPosition(unsigned position) + { + + } + + void TextControl::OnTextChangedCore(const String& old_text, const String& new_text) + { + RecreateTextLayout(); + InvalidateLayout(); + Repaint(); + } + + void TextControl::RecreateTextLayout() + { + assert(text_format_ != nullptr); + + text_layout_ = nullptr; + + const auto dwrite_factory = graph::GraphManager::GetInstance()->GetDWriteFactory(); + + const auto&& size = GetSize(); + + ThrowIfFailed(dwrite_factory->CreateTextLayout( + text_.c_str(), static_cast<UINT32>(text_.size()), + text_format_.Get(), + size.width, size.height, + &text_layout_ + )); + } + + void TextControl::UpdateCursor(const std::optional<Point>& point) + { + if (!is_selectable_) + { + SetCursor(nullptr); + return; + } + + const auto window = GetWindow(); + if (window == nullptr) + { + SetCursor(nullptr); + return; + } + + if (is_selecting_) + { + SetCursor(cursors::i_beam); + return; + } + + const auto p = point.value_or(WindowToControl(window->GetMousePosition())); + if (GetRect(RectRange::Padding).IsPointInside(p)) + SetCursor(cursors::i_beam); + else + SetCursor(nullptr); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\text_control.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\controls\toggle_button.cpp +//-------------------------------------------------------- + + +namespace cru::ui::controls +{ + using animations::AnimationBuilder; + + // ui length parameters of toggle button. + constexpr float half_height = 15; + constexpr float half_width = half_height * 2; + constexpr float stroke_width = 3; + constexpr float inner_circle_radius = half_height - stroke_width; + constexpr float inner_circle_x = half_width - half_height; + + ToggleButton::ToggleButton() : current_circle_position_(-inner_circle_x) + { + graph::GraphManager::GetInstance()->GetD2D1Factory()->CreateRoundedRectangleGeometry(D2D1::RoundedRect(D2D1::RectF(-half_width, -half_height, half_width, half_height), half_height, half_height), &frame_path_); + + on_brush_ = UiManager::GetInstance()->GetPredefineResources()->toggle_button_on_brush; + off_brush_ = UiManager::GetInstance()->GetPredefineResources()->toggle_button_off_brush; + } + + inline D2D1_POINT_2F ConvertPoint(const Point& point) + { + return D2D1::Point2F(point.x, point.y); + } + + StringView ToggleButton::GetControlType() const + { + return control_type; + } + + bool ToggleButton::IsPointInside(const Point& point) + { + const auto size = GetSize(); + const auto transform = D2D1::Matrix3x2F::Translation(size.width / 2, size.height / 2); + BOOL contains; + frame_path_->FillContainsPoint(ConvertPoint(point), transform, &contains); + if (!contains) + frame_path_->StrokeContainsPoint(ConvertPoint(point), stroke_width, nullptr, transform, &contains); + return contains != 0; + } + + void ToggleButton::SetState(const bool state) + { + if (state != state_) + { + state_ = state; + float destination_x; + + if (state) + destination_x = inner_circle_x; + else + destination_x = -inner_circle_x; + + const auto previous_position = current_circle_position_; + const auto delta = destination_x - current_circle_position_; + + constexpr auto total_time = FloatSecond(0.2); + + const auto time = total_time * (std::abs(delta) / (inner_circle_x * 2)); + + // ReSharper disable once CppExpressionWithoutSideEffects + AnimationBuilder(Format(L"ToggleButton {}", reinterpret_cast<size_t>(this)), time) + .AddStepHandler([=](auto, const double percentage) + { + current_circle_position_ = static_cast<float>(previous_position + delta * percentage); + Repaint(); + }) + .Start(); + + RaiseToggleEvent(state); + Repaint(); + } + } + + void ToggleButton::Toggle() + { + SetState(!GetState()); + } + + void ToggleButton::OnToggle(events::ToggleEventArgs& args) + { + + } + + void ToggleButton::OnDrawContent(ID2D1DeviceContext* device_context) + { + Control::OnDrawContent(device_context); + const auto size = GetSize(); + graph::WithTransform(device_context, D2D1::Matrix3x2F::Translation(size.width / 2, size.height / 2), [this](ID2D1DeviceContext* device_context) + { + if (state_) + { + device_context->DrawGeometry(frame_path_.Get(), on_brush_.Get(), stroke_width); + device_context->FillEllipse(D2D1::Ellipse(D2D1::Point2F(current_circle_position_, 0), inner_circle_radius, inner_circle_radius), on_brush_.Get()); + } + else + { + device_context->DrawGeometry(frame_path_.Get(), off_brush_.Get(), stroke_width); + device_context->FillEllipse(D2D1::Ellipse(D2D1::Point2F(current_circle_position_, 0), inner_circle_radius, inner_circle_radius), off_brush_.Get()); + } + }); + } + + void ToggleButton::OnMouseClickCore(events::MouseButtonEventArgs& args) + { + Control::OnMouseClickCore(args); + Toggle(); + } + + Size ToggleButton::OnMeasureContent(const Size& available_size) + { + const Size result_size( + half_width * 2 + stroke_width, + half_height * 2 + stroke_width + ); + + return result_size; + } + + void ToggleButton::RaiseToggleEvent(bool new_state) + { + events::ToggleEventArgs args(this, this, new_state); + OnToggle(args); + toggle_event.Raise(args); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\controls\toggle_button.cpp +//-------------------------------------------------------- +//-------------------------------------------------------- +//-------begin of file: ..\..\src\ui\events\ui_event.cpp +//-------------------------------------------------------- + + +namespace cru::ui::events +{ + Point MouseEventArgs::GetPoint(Control* control, const RectRange range) const + { + if (point_.has_value()) + return control->TransformPoint(control->WindowToControl(point_.value()), RectRange::Margin, range); + return Point(); + } +} +//-------------------------------------------------------- +//-------end of file: ..\..\src\ui\events\ui_event.cpp +//-------------------------------------------------------- |