diff options
40 files changed, 727 insertions, 169 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d745ba51..c2a624e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: - name: Install Libraries run: | sudo apt update - sudo apt install -y libpng-dev libcairo2-dev libpango1.0-dev libxcb1-dev + sudo apt install -y libpng-dev libcairo2-dev libpango1.0-dev libxcb1-dev libxcb-cursor-dev - name: Build run: | @@ -1,6 +1,7 @@ .DS_Store .vs +.vscode .cache diff --git a/CMakeLists.txt b/CMakeLists.txt index 64b0b936..6da50c5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,19 @@ if (MSVC) endif() set(CRU_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include) +set(CRU_ASSETS_DIR ${PROJECT_SOURCE_DIR}/assets) + +function(target_add_resources target res_dir) + message("Add resources for target ${target} with files ${res_dir}.") + + file(GLOB_RECURSE RES_SOURCES "${CRU_ASSETS_DIR}/${res_dir}/*") + target_sources(${target} PUBLIC ${RES_SOURCES}) + foreach (RES_FILE ${RES_SOURCES}) + file(RELATIVE_PATH RES_PATH ${CRU_ASSETS_DIR} ${RES_FILE}) + cmake_path(GET RES_PATH PARENT_PATH RES_PATH) + set_property(SOURCE ${RES_FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${RES_PATH}") + endforeach(RES_FILE) +endfunction() # We don't use meson at present. Enable it in future. # include(scripts/cmake/meson-projects.cmake) diff --git a/demos/CMakeLists.txt b/demos/CMakeLists.txt index b38bc7a8..46eda9c4 100644 --- a/demos/CMakeLists.txt +++ b/demos/CMakeLists.txt @@ -2,7 +2,7 @@ add_library(CruDemoBase INTERFACE) target_link_libraries(CruDemoBase INTERFACE CruPlatformBootstrap) -add_subdirectory(Graphics) +add_subdirectory(platform) if(WIN32) add_subdirectory(main) @@ -13,6 +13,8 @@ elseif(APPLE) add_subdirectory(ScrollView) add_subdirectory(InputMethod) elseif(UNIX) + add_subdirectory(main) + add_subdirectory(ScrollView) add_subdirectory(xcb) endif() diff --git a/demos/Graphics/CMakeLists.txt b/demos/Graphics/CMakeLists.txt deleted file mode 100644 index 64982a19..00000000 --- a/demos/Graphics/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -add_library(CruDemoGraphicsBase INTERFACE) -target_link_libraries(CruDemoGraphicsBase INTERFACE CruPlatformGraphicsBootstrap) - -add_executable(CruDemoGraphicsDrawCircle DrawCircle.cpp) -target_link_libraries(CruDemoGraphicsDrawCircle PRIVATE CruDemoGraphicsBase) - -add_executable(CruDemoGraphicsSvgPath SvgPath.cpp) -target_link_libraries(CruDemoGraphicsSvgPath PRIVATE CruDemoGraphicsBase) diff --git a/demos/Graphics/DrawCircle.cpp b/demos/Graphics/DrawCircle.cpp deleted file mode 100644 index db63c7dc..00000000 --- a/demos/Graphics/DrawCircle.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "cru/base/io/CFileStream.h" -#include "cru/platform/Color.h" -#include "cru/platform/bootstrap/GraphicsBootstrap.h" -#include "cru/platform/graphics/Factory.h" -#include "cru/platform/graphics/ImageFactory.h" -#include "cru/platform/graphics/Painter.h" - -#include <memory> - -int main() { - std::unique_ptr<cru::platform::graphics::IGraphicsFactory> graphics_factory( - cru::platform::bootstrap::CreateGraphicsFactory()); - - auto image = graphics_factory->GetImageFactory()->CreateBitmap(500, 500); - - { - auto brush = - graphics_factory->CreateSolidColorBrush(cru::platform::colors::skyblue); - auto painter = image->CreatePainter(); - painter->FillEllipse(cru::platform::Rect{200, 200, 100, 100}, brush.get()); - painter->EndDraw(); - } - - cru::io::CFileStream file_stream("draw-circle-demo.png", "w"); - - graphics_factory->GetImageFactory()->EncodeToStream( - image.get(), &file_stream, cru::platform::graphics::ImageFormat::Png, - 1.0f); - - return 0; -} diff --git a/demos/Graphics/SvgPath.cpp b/demos/Graphics/SvgPath.cpp deleted file mode 100644 index 44524d77..00000000 --- a/demos/Graphics/SvgPath.cpp +++ /dev/null @@ -1,41 +0,0 @@ - -#include "cru/base/io/CFileStream.h" -#include "cru/platform/Color.h" -#include "cru/platform/Matrix.h" -#include "cru/platform/bootstrap/GraphicsBootstrap.h" -#include "cru/platform/graphics/Factory.h" -#include "cru/platform/graphics/ImageFactory.h" -#include "cru/platform/graphics/Painter.h" - -#include <memory> - -int main() { - std::unique_ptr<cru::platform::graphics::IGraphicsFactory> graphics_factory( - cru::platform::bootstrap::CreateGraphicsFactory()); - - auto brush = - graphics_factory->CreateSolidColorBrush(cru::platform::colors::black); - - auto geometry_builder = graphics_factory->CreateGeometryBuilder(); - geometry_builder->ParseAndApplySvgPathData( - uR"( -M8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5z -M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1h-3zm1.038 3.018a6.093 6.093 0 0 1 .924 0 6 6 0 1 1-.924 0zM0 3.5c0 .753.333 1.429.86 1.887A8.035 8.035 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5zM13.5 1c-.753 0-1.429.333-1.887.86a8.035 8.035 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1z - )"); - auto geometry = geometry_builder->Build(); - - auto image = graphics_factory->GetImageFactory()->CreateBitmap(160, 160); - auto painter = image->CreatePainter(); - - painter->ConcatTransform(cru::platform::Matrix::Scale(10, 10)); - painter->FillGeometry(geometry.get(), brush.get()); - painter->EndDraw(); - - cru::io::CFileStream file_stream("./svg-path-demo.png", "w"); - - graphics_factory->GetImageFactory()->EncodeToStream( - image.get(), &file_stream, cru::platform::graphics::ImageFormat::Png, - 1.0f); - - return 0; -} diff --git a/demos/ScrollView/CMakeLists.txt b/demos/ScrollView/CMakeLists.txt index cfa99ebd..8b34f5d8 100644 --- a/demos/ScrollView/CMakeLists.txt +++ b/demos/ScrollView/CMakeLists.txt @@ -8,4 +8,5 @@ if(APPLE) ) endif() +target_add_resources(CruDemoScrollView cru/ui) target_link_libraries(CruDemoScrollView PRIVATE CruDemoBase CruUi) diff --git a/demos/main/CMakeLists.txt b/demos/main/CMakeLists.txt index 9a30e2f4..2da90405 100644 --- a/demos/main/CMakeLists.txt +++ b/demos/main/CMakeLists.txt @@ -8,4 +8,5 @@ if(APPLE) ) endif() +target_add_resources(CruDemoMain cru/ui) target_link_libraries(CruDemoMain PRIVATE CruDemoBase CruUi) diff --git a/demos/platform/CMakeLists.txt b/demos/platform/CMakeLists.txt new file mode 100644 index 00000000..36673acd --- /dev/null +++ b/demos/platform/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(graphics) +add_subdirectory(gui) diff --git a/demos/platform/graphics/Base.cpp b/demos/platform/graphics/Base.cpp new file mode 100644 index 00000000..be39f7e0 --- /dev/null +++ b/demos/platform/graphics/Base.cpp @@ -0,0 +1,37 @@ +#include "Base.h" +#include <cru/base/io/CFileStream.h> +#include <cru/platform/bootstrap/GraphicsBootstrap.h> +#include <cru/platform/graphics/Factory.h> +#include <cru/platform/graphics/Image.h> +#include <cru/platform/graphics/ImageFactory.h> + +CruPlatformGraphicsDemo::CruPlatformGraphicsDemo(std::string file_name, + int width, int height) + : file_name_(std::move(file_name)) { + factory_.reset(cru::platform::bootstrap::CreateGraphicsFactory()); + image_ = factory_->GetImageFactory()->CreateBitmap(width, height); + painter_ = image_->CreatePainter(); +} + +CruPlatformGraphicsDemo::~CruPlatformGraphicsDemo() { + painter_->EndDraw(); + + cru::io::CFileStream file_stream(file_name_.c_str(), "wb"); + + factory_->GetImageFactory()->EncodeToStream( + image_.get(), &file_stream, cru::platform::graphics::ImageFormat::Png, + 1.0f); +} + +cru::platform::graphics::IGraphicsFactory* +CruPlatformGraphicsDemo::GetFactory() { + return factory_.get(); +} + +cru::platform::graphics::IImage* CruPlatformGraphicsDemo::GetImage() { + return image_.get(); +} + +cru::platform::graphics::IPainter* CruPlatformGraphicsDemo::GetPainter() { + return painter_.get(); +} diff --git a/demos/platform/graphics/Base.h b/demos/platform/graphics/Base.h new file mode 100644 index 00000000..6f4f4929 --- /dev/null +++ b/demos/platform/graphics/Base.h @@ -0,0 +1,25 @@ +#pragma once + +#include <cru/base/Base.h> +#include <cru/platform/graphics/Factory.h> +#include <cru/platform/graphics/Image.h> +#include <cru/platform/graphics/Painter.h> + +#include <memory> +#include <string> + +class CruPlatformGraphicsDemo : public cru::Object2 { + public: + CruPlatformGraphicsDemo(std::string file_name, int width, int height); + ~CruPlatformGraphicsDemo() override; + + cru::platform::graphics::IGraphicsFactory* GetFactory(); + cru::platform::graphics::IImage* GetImage(); + cru::platform::graphics::IPainter* GetPainter(); + + private: + std::string file_name_; + std::unique_ptr<cru::platform::graphics::IGraphicsFactory> factory_; + std::unique_ptr<cru::platform::graphics::IImage> image_; + std::unique_ptr<cru::platform::graphics::IPainter> painter_; +}; diff --git a/demos/platform/graphics/CMakeLists.txt b/demos/platform/graphics/CMakeLists.txt new file mode 100644 index 00000000..07758ee6 --- /dev/null +++ b/demos/platform/graphics/CMakeLists.txt @@ -0,0 +1,12 @@ +add_library(CruDemoPlatformGraphicsBase INTERFACE) +target_link_libraries(CruDemoPlatformGraphicsBase INTERFACE CruPlatformGraphicsBootstrap) +target_sources(CruDemoPlatformGraphicsBase INTERFACE Base.cpp) + +add_executable(CruDemoPlatformGraphicsDrawCircle DrawCircle.cpp) +target_link_libraries(CruDemoPlatformGraphicsDrawCircle PRIVATE CruDemoPlatformGraphicsBase) + +add_executable(CruDemoPlatformGraphicsDrawText DrawText.cpp) +target_link_libraries(CruDemoPlatformGraphicsDrawText PRIVATE CruDemoPlatformGraphicsBase) + +add_executable(CruDemoPlatformGraphicsSvgPath SvgPath.cpp) +target_link_libraries(CruDemoPlatformGraphicsSvgPath PRIVATE CruDemoPlatformGraphicsBase) diff --git a/demos/platform/graphics/DrawCircle.cpp b/demos/platform/graphics/DrawCircle.cpp new file mode 100644 index 00000000..76855e0a --- /dev/null +++ b/demos/platform/graphics/DrawCircle.cpp @@ -0,0 +1,17 @@ +#include "Base.h" +#include "cru/platform/Color.h" +#include "cru/platform/graphics/Factory.h" +#include "cru/platform/graphics/Painter.h" + +#include <memory> + +int main() { + CruPlatformGraphicsDemo demo("draw-circle-demo.png", 500, 500); + + auto brush = + demo.GetFactory()->CreateSolidColorBrush(cru::platform::colors::skyblue); + demo.GetPainter()->FillEllipse(cru::platform::Rect{200, 200, 100, 100}, + brush.get()); + + return 0; +} diff --git a/demos/platform/graphics/DrawText.cpp b/demos/platform/graphics/DrawText.cpp new file mode 100644 index 00000000..dfea18cd --- /dev/null +++ b/demos/platform/graphics/DrawText.cpp @@ -0,0 +1,27 @@ +#include "Base.h" +#include "cru/platform/Color.h" +#include "cru/platform/graphics/Factory.h" +#include "cru/platform/graphics/Font.h" +#include "cru/platform/graphics/Painter.h" + +#include <iostream> +#include <memory> + +int main() { + CruPlatformGraphicsDemo demo("draw-text-demo.png", 500, 200); + + auto brush = + demo.GetFactory()->CreateSolidColorBrush(cru::platform::colors::skyblue); + + std::shared_ptr<cru::platform::graphics::IFont> font( + demo.GetFactory()->CreateFont(u"", 24)); + auto text_layout = demo.GetFactory()->CreateTextLayout(font, u"Hello world!"); + demo.GetPainter()->DrawText({0, 0}, text_layout.get(), brush.get()); + + auto bounds = text_layout->GetTextBounds(); + std::cout << "Bounds of text:\n\tx: " << bounds.left + << "\n\ty: " << bounds.top << "\n\twidth: " << bounds.width + << "\n\theight: " << bounds.height << std::endl; + + return 0; +} diff --git a/demos/platform/graphics/SvgPath.cpp b/demos/platform/graphics/SvgPath.cpp new file mode 100644 index 00000000..d0827878 --- /dev/null +++ b/demos/platform/graphics/SvgPath.cpp @@ -0,0 +1,28 @@ + +#include "Base.h" +#include "cru/platform/Color.h" +#include "cru/platform/Matrix.h" +#include "cru/platform/graphics/Factory.h" +#include "cru/platform/graphics/Painter.h" + +#include <memory> + +int main() { + CruPlatformGraphicsDemo demo("svg-path-demo.png", 160, 160); + + auto brush = + demo.GetFactory()->CreateSolidColorBrush(cru::platform::colors::black); + + auto geometry_builder = demo.GetFactory()->CreateGeometryBuilder(); + geometry_builder->ParseAndApplySvgPathData( + uR"( +M8.5 5.5a.5.5 0 0 0-1 0v3.362l-1.429 2.38a.5.5 0 1 0 .858.515l1.5-2.5A.5.5 0 0 0 8.5 9V5.5z +M6.5 0a.5.5 0 0 0 0 1H7v1.07a7.001 7.001 0 0 0-3.273 12.474l-.602.602a.5.5 0 0 0 .707.708l.746-.746A6.97 6.97 0 0 0 8 16a6.97 6.97 0 0 0 3.422-.892l.746.746a.5.5 0 0 0 .707-.708l-.601-.602A7.001 7.001 0 0 0 9 2.07V1h.5a.5.5 0 0 0 0-1h-3zm1.038 3.018a6.093 6.093 0 0 1 .924 0 6 6 0 1 1-.924 0zM0 3.5c0 .753.333 1.429.86 1.887A8.035 8.035 0 0 1 4.387 1.86 2.5 2.5 0 0 0 0 3.5zM13.5 1c-.753 0-1.429.333-1.887.86a8.035 8.035 0 0 1 3.527 3.527A2.5 2.5 0 0 0 13.5 1z + )"); + auto geometry = geometry_builder->Build(); + + demo.GetPainter()->ConcatTransform(cru::platform::Matrix::Scale(10, 10)); + demo.GetPainter()->FillGeometry(geometry.get(), brush.get()); + + return 0; +} diff --git a/demos/platform/gui/CMakeLists.txt b/demos/platform/gui/CMakeLists.txt new file mode 100644 index 00000000..b179fbc6 --- /dev/null +++ b/demos/platform/gui/CMakeLists.txt @@ -0,0 +1,5 @@ +add_library(CruDemoPlatformGuiBase INTERFACE) +target_link_libraries(CruDemoPlatformGuiBase INTERFACE CruPlatformBootstrap) + +add_executable(CruDemoPlatformGuiEmptyWindow EmptyWindow.cpp) +target_link_libraries(CruDemoPlatformGuiEmptyWindow PRIVATE CruDemoPlatformGuiBase) diff --git a/demos/platform/gui/EmptyWindow.cpp b/demos/platform/gui/EmptyWindow.cpp new file mode 100644 index 00000000..299ddd17 --- /dev/null +++ b/demos/platform/gui/EmptyWindow.cpp @@ -0,0 +1,25 @@ +#include "cru/platform/bootstrap/Bootstrap.h" +#include "cru/platform/gui/Base.h" +#include "cru/platform/gui/UiApplication.h" +#include "cru/platform/gui/Window.h" + +using cru::platform::gui::INativeWindow; +using cru::platform::gui::IUiApplication; +using cru::platform::gui::WindowVisibilityType; + +int main() { + std::unique_ptr<IUiApplication> application( + cru::platform::bootstrap::CreateUiApplication()); + + application->SetQuitOnAllWindowClosed(true); + + std::unique_ptr<INativeWindow> window1(application->CreateWindow()); + window1->SetVisibility(WindowVisibilityType::Show); + window1->SetToForeground(); + + std::unique_ptr<INativeWindow> window2(application->CreateWindow()); + window2->SetVisibility(WindowVisibilityType::Show); + window2->SetToForeground(); + + return application->Run(); +} diff --git a/include/cru/base/Guard.h b/include/cru/base/Guard.h index 5a9f9c5d..6b6cf851 100644 --- a/include/cru/base/Guard.h +++ b/include/cru/base/Guard.h @@ -1,5 +1,6 @@ #pragma once +#include <cstdlib> #include <functional> namespace cru { @@ -23,4 +24,29 @@ struct Guard { ExitFunc on_exit; }; + +template <typename T> +struct FreeLater { + FreeLater(T* ptr) : ptr(ptr) {} + ~FreeLater() { ::free(ptr); } + + FreeLater(const FreeLater& other) = delete; + FreeLater& operator=(const FreeLater& other) = delete; + + FreeLater(FreeLater&& other) : ptr(other.ptr) { other.ptr = nullptr; } + FreeLater& operator=(FreeLater&& other) { + if (this != &other) { + ::free(ptr); + ptr = other.ptr; + other.ptr = nullptr; + } + return *this; + } + + operator T*() const { return ptr; } + T* operator->() { return ptr; } + + T* ptr; +}; + } // namespace cru diff --git a/include/cru/platform/GraphicsBase.h b/include/cru/platform/GraphicsBase.h index b0f653ef..d5936476 100644 --- a/include/cru/platform/GraphicsBase.h +++ b/include/cru/platform/GraphicsBase.h @@ -206,10 +206,22 @@ struct Rect final { height + thickness.GetVerticalTotal()); } - constexpr Rect Shrink(const Thickness& thickness) const { - return Rect(left + thickness.left, top + thickness.top, + constexpr Rect Shrink(const Thickness& thickness, + bool normalize = false) const { + Rect result(left + thickness.left, top + thickness.top, width - thickness.GetHorizontalTotal(), height - thickness.GetVerticalTotal()); + + if (normalize) { + if (result.width < 0) { + result.width = 0; + } + if (result.height < 0) { + result.height = 0; + } + } + + return result; } constexpr bool IsPointInside(const Point& point) const { diff --git a/include/cru/platform/gui/Window.h b/include/cru/platform/gui/Window.h index 742ef798..885c1250 100644 --- a/include/cru/platform/gui/Window.h +++ b/include/cru/platform/gui/Window.h @@ -44,6 +44,7 @@ struct NativeKeyEventArgs { // Represents a native window, which exposes some low-level events and // operations. struct INativeWindow : virtual IPlatformResource { + virtual bool IsCreated(); virtual void Close() = 0; virtual INativeWindow* GetParent() = 0; @@ -72,6 +73,8 @@ struct INativeWindow : virtual IPlatformResource { // The lefttop of the rect is relative to screen lefttop. virtual void SetWindowRect(const Rect& rect) = 0; + // Return true if window gained the focus. But the return value should be + // ignored, since it does not guarantee anything. virtual bool RequestFocus() = 0; // Relative to client lefttop. diff --git a/include/cru/platform/gui/xcb/Base.h b/include/cru/platform/gui/xcb/Base.h index f3bcfd01..ad571a40 100644 --- a/include/cru/platform/gui/xcb/Base.h +++ b/include/cru/platform/gui/xcb/Base.h @@ -7,13 +7,13 @@ namespace cru::platform::gui::xcb { class XcbResource : public Object, public virtual IPlatformResource { public: - static String kPlatformId; + static constexpr const char16_t* kPlatformId = u"XCB"; protected: XcbResource() = default; public: - String GetPlatformId() const final { return kPlatformId; } + String GetPlatformId() const final { return String(kPlatformId); } }; class XcbException : public PlatformException { diff --git a/include/cru/platform/gui/xcb/Cursor.h b/include/cru/platform/gui/xcb/Cursor.h new file mode 100644 index 00000000..02ede7dd --- /dev/null +++ b/include/cru/platform/gui/xcb/Cursor.h @@ -0,0 +1,44 @@ +#pragma once + +#include <cru/base/io/Stream.h> +#include "../Cursor.h" +#include "Base.h" + +#include <xcb/xcb.h> +#include <xcb/xcb_cursor.h> +#include <memory> +#include <string_view> +#include <unordered_map> + +namespace cru::platform::gui::xcb { +class XcbUiApplication; + +class XcbCursor : public XcbResource, public virtual ICursor { + public: + XcbCursor(XcbUiApplication* application, xcb_cursor_t cursor, bool auto_free); + ~XcbCursor() override; + + xcb_cursor_t GetXcbCursor(); + + private: + XcbUiApplication* application_; + xcb_cursor_t cursor_; + bool auto_free_; +}; + +class XcbCursorManager : public XcbResource, public virtual ICursorManager { + public: + explicit XcbCursorManager(XcbUiApplication* application); + ~XcbCursorManager() override; + + std::shared_ptr<ICursor> GetSystemCursor(SystemCursorType type) override; + + private: + std::shared_ptr<XcbCursor> LoadXCursor(std::string_view name); + + private: + XcbUiApplication* application_; + xcb_cursor_context_t* xcb_cursor_context_; + std::unordered_map<SystemCursorType, std::shared_ptr<XcbCursor>> cursors_; +}; +} // namespace cru::platform::gui::xcb diff --git a/include/cru/platform/gui/xcb/UiApplication.h b/include/cru/platform/gui/xcb/UiApplication.h index 6063a8ab..b8de86f2 100644 --- a/include/cru/platform/gui/xcb/UiApplication.h +++ b/include/cru/platform/gui/xcb/UiApplication.h @@ -12,6 +12,7 @@ namespace cru::platform::gui::xcb { class XcbWindow; +class XcbCursorManager; class XcbUiApplication : public XcbResource, public virtual IUiApplication { friend XcbWindow; @@ -26,6 +27,7 @@ class XcbUiApplication : public XcbResource, public virtual IUiApplication { void CheckXcbConnectionError(); xcb_connection_t* GetXcbConnection(); + void XcbFlush(); // This API is weird, but before we have correct screen API, we still use it. xcb_screen_t* GetFirstXcbScreen(); @@ -41,6 +43,9 @@ class XcbUiApplication : public XcbResource, public virtual IUiApplication { CRU_XCB_UI_APPLICATION_DEFINE_XCB_ATOM(_NET_WM_WINDOW_TYPE) CRU_XCB_UI_APPLICATION_DEFINE_XCB_ATOM(_NET_WM_WINDOW_TYPE_NORMAL) CRU_XCB_UI_APPLICATION_DEFINE_XCB_ATOM(_NET_WM_WINDOW_TYPE_UTILITY) + CRU_XCB_UI_APPLICATION_DEFINE_XCB_ATOM(_NET_FRAME_EXTENTS) + CRU_XCB_UI_APPLICATION_DEFINE_XCB_ATOM(WM_PROTOCOLS) + CRU_XCB_UI_APPLICATION_DEFINE_XCB_ATOM(WM_DELETE_WINDOW) #undef CRU_XCB_UI_APPLICATION_DEFINE_XCB_ATOM @@ -61,29 +66,18 @@ class XcbUiApplication : public XcbResource, public virtual IUiApplication { std::function<void()> action) override; void CancelTimer(long long id) override; - virtual std::vector<INativeWindow*> GetAllWindow() = 0; + std::vector<INativeWindow*> GetAllWindow() override; - virtual INativeWindow* CreateWindow() = 0; + INativeWindow* CreateWindow() override; - virtual cru::platform::graphics::IGraphicsFactory* GetGraphicsFactory() = 0; + cru::platform::graphics::IGraphicsFactory* GetGraphicsFactory() override; - virtual ICursorManager* GetCursorManager() = 0; + ICursorManager* GetCursorManager() override; - virtual IClipboard* GetClipboard() = 0; + IClipboard* GetClipboard() override; // If return nullptr, it means the menu is not supported. - virtual IMenu* GetApplicationMenu(); - - /** - * \todo Implement on Windows. - */ - virtual std::optional<String> ShowSaveDialog(SaveDialogOptions options); - - /** - * \todo Implement on Windows. - */ - virtual std::optional<std::vector<String>> ShowOpenDialog( - OpenDialogOptions options); + IMenu* GetApplicationMenu() override; private: void HandleXEvents(); @@ -104,5 +98,7 @@ class XcbUiApplication : public XcbResource, public virtual IUiApplication { bool is_quit_on_all_window_closed_; std::vector<XcbWindow*> windows_; + + XcbCursorManager* cursor_manager_; }; } // namespace cru::platform::gui::xcb diff --git a/include/cru/platform/gui/xcb/Window.h b/include/cru/platform/gui/xcb/Window.h index 6d923666..61e4b616 100644 --- a/include/cru/platform/gui/xcb/Window.h +++ b/include/cru/platform/gui/xcb/Window.h @@ -1,5 +1,6 @@ #pragma once +#include "../../GraphicsBase.h" #include "../Window.h" #include "Base.h" @@ -10,6 +11,7 @@ namespace cru::platform::gui::xcb { class XcbUiApplication; +class XcbCursor; class XcbWindow : public XcbResource, public virtual INativeWindow { friend XcbUiApplication; @@ -18,6 +20,7 @@ class XcbWindow : public XcbResource, public virtual INativeWindow { explicit XcbWindow(XcbUiApplication* application); ~XcbWindow() override; + bool IsCreated() override; void Close() override; INativeWindow* GetParent() override; @@ -32,33 +35,27 @@ class XcbWindow : public XcbResource, public virtual INativeWindow { WindowVisibilityType GetVisibility() override; void SetVisibility(WindowVisibilityType visibility) override; - virtual Size GetClientSize() = 0; - virtual void SetClientSize(const Size& size) = 0; + Size GetClientSize() override; + void SetClientSize(const Size& size) override; - virtual Rect GetClientRect() = 0; - virtual void SetClientRect(const Rect& rect) = 0; + Rect GetClientRect() override; + void SetClientRect(const Rect& rect) override; - // Get the rect of the window containing frame. - // The lefttop of the rect is relative to screen lefttop. - virtual Rect GetWindowRect() = 0; + Rect GetWindowRect() override; + void SetWindowRect(const Rect& rect) override; - // Set the rect of the window containing frame. - // The lefttop of the rect is relative to screen lefttop. - virtual void SetWindowRect(const Rect& rect) = 0; + bool RequestFocus() override; - virtual bool RequestFocus() = 0; + Point GetMousePosition() override; - // Relative to client lefttop. - virtual Point GetMousePosition() = 0; + bool CaptureMouse() override; + bool ReleaseMouse() override; - virtual bool CaptureMouse() = 0; - virtual bool ReleaseMouse() = 0; + void SetCursor(std::shared_ptr<ICursor> cursor) override; - virtual void SetCursor(std::shared_ptr<ICursor> cursor) = 0; + void SetToForeground() override; - virtual void SetToForeground() = 0; - - virtual void RequestRepaint() = 0; + void RequestRepaint() override; std::unique_ptr<graphics::IPainter> BeginPaint() override; @@ -78,7 +75,7 @@ class XcbWindow : public XcbResource, public virtual INativeWindow { IEvent<NativeKeyEventArgs>* KeyDownEvent() override; IEvent<NativeKeyEventArgs>* KeyUpEvent() override; - virtual IInputMethodContext* GetInputMethodContext() = 0; + IInputMethodContext* GetInputMethodContext() override; public: std::optional<xcb_window_t> GetXcbWindow(); @@ -91,12 +88,19 @@ class XcbWindow : public XcbResource, public virtual INativeWindow { void DoSetParent(xcb_window_t window); void DoSetStyleFlags(xcb_window_t window); void DoSetTitle(xcb_window_t window); + void DoSetClientRect(xcb_window_t window, const Rect& rect); + void DoSetCursor(xcb_window_t window, XcbCursor* cursor); void* XcbGetProperty(xcb_window_t window, xcb_atom_t property, xcb_atom_t type, std::uint32_t offset, std::uint32_t length, std::uint32_t* out_length = nullptr); + // Relative to screen lefttop. + Point GetXcbWindowPosition(xcb_window_t window); + + std::optional<Thickness> Get_NET_FRAME_EXTENTS(xcb_window_t window); + private: XcbUiApplication* application_; std::optional<xcb_window_t> xcb_window_; @@ -105,6 +109,7 @@ class XcbWindow : public XcbResource, public virtual INativeWindow { WindowStyleFlag style_; std::string title_; bool mapped_; + std::shared_ptr<XcbCursor> cursor_; XcbWindow* parent_; diff --git a/src/ThemeBuilder/CMakeLists.txt b/src/ThemeBuilder/CMakeLists.txt index 5e49cf78..93b24e85 100644 --- a/src/ThemeBuilder/CMakeLists.txt +++ b/src/ThemeBuilder/CMakeLists.txt @@ -40,4 +40,6 @@ if(APPLE) ) endif() +target_add_resources(CruThemeBuilder cru/ui) +target_add_resources(CruThemeBuilder cru/theme_builder) target_link_libraries(CruThemeBuilder PRIVATE CruPlatformBootstrap CruUi) diff --git a/src/base/io/Resource.cpp b/src/base/io/Resource.cpp index d369b5f5..e599f8a9 100644 --- a/src/base/io/Resource.cpp +++ b/src/base/io/Resource.cpp @@ -1,19 +1,20 @@ #include "cru/base/io/Resource.h" +#include "cru/base/Base.h" #include "cru/base/Exception.h" -#include "cru/base/log/Logger.h" #if defined(CRU_PLATFORM_OSX) #include <CoreFoundation/CoreFoundation.h> #elif defined(CRU_PLATFORM_WINDOWS) #include <Windows.h> +#elif defined(__linux) +#include <fstream> +#include <sstream> #endif #include <filesystem> namespace cru::io { std::filesystem::path GetResourceDir() { - constexpr auto kLogTag = u"GetResourceDir"; - #if defined(CRU_PLATFORM_OSX) CFBundleRef main_bundle = CFBundleGetMainBundle(); CFURLRef bundle_url = CFBundleCopyBundleURL(main_bundle); @@ -26,10 +27,18 @@ std::filesystem::path GetResourceDir() { CFRelease(cf_string_ref); return bundle_path / "Contents/Resources"; -#elif defined(CRU_PLATFORM_WINDOWS) +#elif defined(_WIN32) || defined(__linux) +#if defined(_WIN32) wchar_t buffer[MAX_PATH]; DWORD size = ::GetModuleFileNameW(nullptr, buffer, MAX_PATH); std::filesystem::path module_path(buffer, buffer + size); +#else // linux + std::ifstream file("/proc/self/cmdline"); + std::stringstream buffer; + buffer << file.rdbuf(); + auto str = buffer.str(); + std::filesystem::path module_path(str.substr(0, str.find('\0'))); +#endif auto p = module_path; while (p.has_parent_path()) { p = p.parent_path(); @@ -41,8 +50,9 @@ std::filesystem::path GetResourceDir() { } throw Exception(u"Failed to find resource directory."); -#else - throw Exception(u"Not implemented."); + #endif + + NotImplemented(); } } // namespace cru::io diff --git a/src/base/platform/unix/EventLoop.cpp b/src/base/platform/unix/EventLoop.cpp index 7c475697..6e8dc16e 100644 --- a/src/base/platform/unix/EventLoop.cpp +++ b/src/base/platform/unix/EventLoop.cpp @@ -34,15 +34,15 @@ int UnixEventLoop::Run() { while (!exit_code_) { int poll_timeout = -1; - while (CheckPoll()) { + if (CheckPoll()) { continue; } - while (CheckTimer()) { + if (CheckTimer()) { continue; } - while (CheckActionPipe()) { + if (CheckActionPipe()) { continue; } diff --git a/src/platform/bootstrap/Bootstrap.cpp b/src/platform/bootstrap/Bootstrap.cpp index e9183550..bcf12613 100644 --- a/src/platform/bootstrap/Bootstrap.cpp +++ b/src/platform/bootstrap/Bootstrap.cpp @@ -1,10 +1,11 @@ #include "cru/platform/bootstrap/Bootstrap.h" -#include "cru/base/Base.h" #if defined(_WIN32) #include "cru/platform/gui/win/UiApplication.h" #elif defined(__APPLE__) #include "cru/platform/gui/osx/UiApplication.h" +#elif defined(__unix) +#include "cru/platform/gui/xcb/UiApplication.h" #else #endif @@ -15,7 +16,8 @@ cru::platform::gui::IUiApplication* CreateUiApplication() { #elif defined(__APPLE__) return new cru::platform::gui::osx::OsxUiApplication(); #else - NotImplemented(); + return new cru::platform::gui::xcb::XcbUiApplication(); #endif + NotImplemented(); } } // namespace cru::platform::bootstrap diff --git a/src/platform/bootstrap/CMakeLists.txt b/src/platform/bootstrap/CMakeLists.txt index 41f1e3d3..24cdff2b 100644 --- a/src/platform/bootstrap/CMakeLists.txt +++ b/src/platform/bootstrap/CMakeLists.txt @@ -17,10 +17,10 @@ if(WIN32) elseif(APPLE) target_link_libraries(CruPlatformGraphicsBootstrap PUBLIC CruPlatformGraphicsQuartz) target_link_libraries(CruPlatformBootstrap PUBLIC CruPlatformGuiOsx) -elseif(EMSCRIPTEN) - target_link_libraries(CruPlatformGraphicsBootstrap PUBLIC CruBase) - target_link_libraries(CruPlatformBootstrap PUBLIC CruBase) # TODO: Remember to change this. -else() +elseif(UNIX) target_link_libraries(CruPlatformGraphicsBootstrap PUBLIC CruPlatformGraphicsCairo) - target_link_libraries(CruPlatformBootstrap PUBLIC CruPlatformGraphicsCairo) + target_link_libraries(CruPlatformBootstrap PUBLIC CruPlatformGuiXcb) +else() + target_link_libraries(CruPlatformGraphicsBootstrap PUBLIC CruBase) + target_link_libraries(CruPlatformBootstrap PUBLIC CruBase) endif() diff --git a/src/platform/graphics/cairo/CairoPainter.cpp b/src/platform/graphics/cairo/CairoPainter.cpp index b9ab50c4..8dd214cc 100644 --- a/src/platform/graphics/cairo/CairoPainter.cpp +++ b/src/platform/graphics/cairo/CairoPainter.cpp @@ -186,6 +186,8 @@ void CairoPainter::DrawText(const Point& offset, ITextLayout* text_layout, cairo_save(cairo_); cairo_set_source(cairo_, cairo_pattern); + cairo_move_to(cairo_, offset.x, offset.y); + pango_cairo_update_layout(cairo_, pango_text_layout->GetPangoLayout()); pango_cairo_show_layout(cairo_, pango_text_layout->GetPangoLayout()); cairo_restore(cairo_); } diff --git a/src/platform/graphics/cairo/PangoFont.cpp b/src/platform/graphics/cairo/PangoFont.cpp index 0de17add..d5c1ad0b 100644 --- a/src/platform/graphics/cairo/PangoFont.cpp +++ b/src/platform/graphics/cairo/PangoFont.cpp @@ -10,7 +10,7 @@ PangoFont::PangoFont(CairoGraphicsFactory* factory, String font_family, auto font_family_str = font_family_.ToUtf8(); pango_font_description_set_family(pango_font_description_, font_family_str.c_str()); - pango_font_description_set_size(pango_font_description_, font_size); + pango_font_description_set_size(pango_font_description_, font_size * PANGO_SCALE); } PangoFont::~PangoFont() { diff --git a/src/platform/graphics/cairo/PangoTextLayout.cpp b/src/platform/graphics/cairo/PangoTextLayout.cpp index 1033ce9e..0ba7c806 100644 --- a/src/platform/graphics/cairo/PangoTextLayout.cpp +++ b/src/platform/graphics/cairo/PangoTextLayout.cpp @@ -6,13 +6,26 @@ #include "cru/platform/graphics/cairo/CairoGraphicsFactory.h" #include "cru/platform/graphics/cairo/PangoFont.h" +#include <pango/pangocairo.h> + namespace cru::platform::graphics::cairo { +namespace { +Rect ConvertFromPango(const Rect& rect) { + auto result = rect; + result.left /= PANGO_SCALE; + result.top /= PANGO_SCALE; + result.width /= PANGO_SCALE; + result.height /= PANGO_SCALE; + return result; +} +} // namespace + PangoTextLayout::PangoTextLayout(CairoGraphicsFactory* factory, std::shared_ptr<IFont> font) : CairoResource(factory) { Expects(font); font_ = CheckPlatform<PangoFont>(font, GetPlatformId()); - pango_layout_ = pango_layout_new(factory->GetDefaultPangoContext()); + pango_layout_ = pango_cairo_create_layout(factory->GetDefaultCairo()); pango_layout_set_font_description(pango_layout_, font_->GetPangoFontDescription()); }; @@ -37,11 +50,11 @@ void PangoTextLayout::SetFont(std::shared_ptr<IFont> font) { } void PangoTextLayout::SetMaxWidth(float max_width) { - return pango_layout_set_width(pango_layout_, max_width); + return pango_layout_set_width(pango_layout_, max_width * PANGO_SCALE); } void PangoTextLayout::SetMaxHeight(float max_height) { - return pango_layout_set_height(pango_layout_, max_height); + return pango_layout_set_height(pango_layout_, max_height * PANGO_SCALE); } bool PangoTextLayout::IsEditMode() { return edit_mode_; } @@ -64,7 +77,7 @@ float PangoTextLayout::GetLineHeight(Index line_index) { auto line = pango_layout_get_line_readonly(pango_layout_, line_index); int height; pango_layout_line_get_height(line, &height); - return height; + return static_cast<float>(height) / PANGO_SCALE; } Index PangoTextLayout::FromUtf8IndexToUtf16Index(Index index) { @@ -90,8 +103,9 @@ Index PangoTextLayout::FromUtf16IndexToUtf8Index(Index index) { Rect PangoTextLayout::GetTextBounds(bool includingTrailingSpace) { PangoRectangle rectangle; - pango_layout_get_extents(pango_layout_, nullptr, &rectangle); - return Rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height); + pango_layout_get_extents(pango_layout_, &rectangle, nullptr); + return ConvertFromPango( + Rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height)); } std::vector<Rect> PangoTextLayout::TextRangeRect(const TextRange& text_range) { @@ -108,32 +122,36 @@ std::vector<Rect> PangoTextLayout::TextRangeRect(const TextRange& text_range) { if (start_line_index == end_line_index) { auto line = pango_layout_get_line(pango_layout_, start_line_index); PangoRectangle rectangle; - pango_layout_line_get_extents(line, nullptr, &rectangle); - return {Rect(rectangle.x + start_x_pos, rectangle.y, - end_x_pos - start_x_pos, rectangle.height)}; + pango_layout_line_get_extents(line, &rectangle, nullptr); + return {ConvertFromPango(Rect(rectangle.x + start_x_pos, rectangle.y, + end_x_pos - start_x_pos, rectangle.height))}; } else { std::vector<Rect> result; PangoRectangle rectangle; auto start_line = pango_layout_get_line(pango_layout_, start_line_index); - pango_layout_line_get_extents(start_line, nullptr, &rectangle); + pango_layout_line_get_extents(start_line, &rectangle, nullptr); result.push_back(Rect(rectangle.x + start_x_pos, rectangle.y, rectangle.width - start_x_pos, rectangle.height)); for (int line_index = start_line_index + 1; line_index < end_line_index; line_index++) { auto line = pango_layout_get_line(pango_layout_, line_index); - pango_layout_line_get_extents(line, nullptr, &rectangle); + pango_layout_line_get_extents(line, &rectangle, nullptr); result.push_back( Rect(rectangle.x, rectangle.y, rectangle.width, rectangle.height)); } auto end_line = pango_layout_get_line(pango_layout_, end_line_index); - pango_layout_line_get_extents(end_line, nullptr, &rectangle); + pango_layout_line_get_extents(end_line, &rectangle, nullptr); result.push_back( Rect(rectangle.x, rectangle.y, end_x_pos, rectangle.height)); + for (auto& r : result) { + r = ConvertFromPango(r); + } + return result; } } @@ -146,15 +164,17 @@ Rect PangoTextLayout::TextSinglePoint(Index position, bool trailing) { auto line = pango_layout_get_line(pango_layout_, line_index); PangoRectangle rectangle; - pango_layout_line_get_extents(line, nullptr, &rectangle); + pango_layout_line_get_extents(line, &rectangle, nullptr); - return Rect(rectangle.x + x_pos, rectangle.y, 0, rectangle.height); + return ConvertFromPango( + Rect(rectangle.x + x_pos, rectangle.y, 0, rectangle.height)); } TextHitTestResult PangoTextLayout::HitTest(const Point& point) { int index, trailing; - auto inside_text = pango_layout_xy_to_index(pango_layout_, point.x, point.y, - &index, &trailing); + auto inside_text = + pango_layout_xy_to_index(pango_layout_, point.x * PANGO_SCALE, + point.y * PANGO_SCALE, &index, &trailing); return TextHitTestResult{FromUtf8IndexToUtf16Index(index), trailing != 0, inside_text != 0}; } diff --git a/src/platform/gui/CMakeLists.txt b/src/platform/gui/CMakeLists.txt index 808caf9c..b541428e 100644 --- a/src/platform/gui/CMakeLists.txt +++ b/src/platform/gui/CMakeLists.txt @@ -2,6 +2,7 @@ add_library(CruPlatformGui Keyboard.cpp Menu.cpp UiApplication.cpp + Window.cpp ) target_link_libraries(CruPlatformGui PUBLIC CruPlatformGraphics) target_compile_definitions(CruPlatformGui PRIVATE CRU_PLATFORM_GUI_EXPORT_API) diff --git a/src/platform/gui/Window.cpp b/src/platform/gui/Window.cpp new file mode 100644 index 00000000..fdcfbae4 --- /dev/null +++ b/src/platform/gui/Window.cpp @@ -0,0 +1,6 @@ +#include "cru/platform/gui/Window.h" +#include "cru/base/Base.h" + +namespace cru::platform::gui { +bool INativeWindow::IsCreated() { NotImplemented(); } +} // namespace cru::platform::gui diff --git a/src/platform/gui/xcb/CMakeLists.txt b/src/platform/gui/xcb/CMakeLists.txt index aa6116e4..fa5cc0bf 100644 --- a/src/platform/gui/xcb/CMakeLists.txt +++ b/src/platform/gui/xcb/CMakeLists.txt @@ -1,11 +1,13 @@ find_library(LIBRARY_CAIRO cairo REQUIRED) find_library(LIBRARY_XCB xcb REQUIRED) -add_library(CruPlatformGuiX11 +find_library(LIBRARY_XCB_CURSOR xcb-cursor REQUIRED) +add_library(CruPlatformGuiXcb + Cursor.cpp Keyboard.cpp UiApplication.cpp Window.cpp ) -target_link_libraries(CruPlatformGuiX11 PUBLIC +target_link_libraries(CruPlatformGuiXcb PUBLIC CruPlatformGui CruPlatformGraphicsCairo - ${LIBRARY_XCB} ${LIBRARY_CAIRO} + ${LIBRARY_XCB} ${LIBRARY_XCB_CURSOR} ${LIBRARY_CAIRO} ) diff --git a/src/platform/gui/xcb/Cursor.cpp b/src/platform/gui/xcb/Cursor.cpp new file mode 100644 index 00000000..5582c6a6 --- /dev/null +++ b/src/platform/gui/xcb/Cursor.cpp @@ -0,0 +1,54 @@ +#include "cru/platform/gui/xcb/Cursor.h" +#include "cru/base/Exception.h" +#include "cru/platform/gui/Cursor.h" +#include "cru/platform/gui/xcb/UiApplication.h" + +#include <xcb/xcb.h> +#include <xcb/xcb_cursor.h> +#include <memory> + +namespace cru::platform::gui::xcb { +XcbCursor::XcbCursor(XcbUiApplication* application, xcb_cursor_t cursor, + bool auto_free) + : application_(application), cursor_(cursor), auto_free_(auto_free) {} + +XcbCursor::~XcbCursor() { + if (auto_free_) { + xcb_free_cursor(application_->GetXcbConnection(), cursor_); + } +} + +xcb_cursor_t XcbCursor::GetXcbCursor() { return cursor_; } + +XcbCursorManager::XcbCursorManager(XcbUiApplication* application) + : application_(application) { + auto code = xcb_cursor_context_new(application->GetXcbConnection(), + application->GetFirstXcbScreen(), + &xcb_cursor_context_); + if (code != 0) { + throw PlatformException("Failed to call xcb_cursor_context_new."); + } + + cursors_[SystemCursorType::Arrow] = + std::make_shared<XcbCursor>(application_, XCB_CURSOR_NONE, false); + cursors_[SystemCursorType::Hand] = LoadXCursor("pointer"); + cursors_[SystemCursorType::IBeam] = LoadXCursor("ibeam"); +} + +XcbCursorManager::~XcbCursorManager() { + xcb_cursor_context_free(xcb_cursor_context_); +} + +std::shared_ptr<ICursor> XcbCursorManager::GetSystemCursor( + SystemCursorType type) { + return cursors_[type]; +} + +std::shared_ptr<XcbCursor> XcbCursorManager::LoadXCursor( + std::string_view name) { + return std::make_shared<XcbCursor>( + application_, xcb_cursor_load_cursor(xcb_cursor_context_, name.data()), + true); +} + +} // namespace cru::platform::gui::xcb diff --git a/src/platform/gui/xcb/Keyboard.cpp b/src/platform/gui/xcb/Keyboard.cpp index d7559062..e5f6da8e 100644 --- a/src/platform/gui/xcb/Keyboard.cpp +++ b/src/platform/gui/xcb/Keyboard.cpp @@ -1,4 +1,5 @@ #include "cru/platform/gui/xcb/Keyboard.h" +#include "cru/base/Guard.h" #include "cru/platform/gui/Keyboard.h" #include "cru/platform/gui/xcb/UiApplication.h" @@ -82,8 +83,8 @@ KeyCode XorgKeycodeToCruKeyCode(XcbUiApplication *application, // Get keyboard mapping auto mapping_cookie = xcb_get_keyboard_mapping(connection, keycode, 1); - auto *mapping_reply = - xcb_get_keyboard_mapping_reply(connection, mapping_cookie, NULL); + auto mapping_reply = FreeLater( + xcb_get_keyboard_mapping_reply(connection, mapping_cookie, NULL)); if (!mapping_reply) { throw XcbException("Cannot get keyboard mapping."); @@ -107,7 +108,8 @@ using KeymapBitset = KeymapBitset GetXorgKeymap(xcb_connection_t *connection) { auto keymap_cookie = xcb_query_keymap(connection); - auto keymap_reply = xcb_query_keymap_reply(connection, keymap_cookie, NULL); + auto keymap_reply = + FreeLater(xcb_query_keymap_reply(connection, keymap_cookie, NULL)); if (!keymap_reply) { throw XcbException("Cannot get keymap."); @@ -136,8 +138,8 @@ std::unordered_map<KeyCode, bool> GetKeyboardState( // Get keyboard mapping auto mapping_cookie = xcb_get_keyboard_mapping(connection, min_keycode, max_keycode - min_keycode + 1); - auto *mapping_reply = - xcb_get_keyboard_mapping_reply(connection, mapping_cookie, NULL); + auto mapping_reply = FreeLater( + xcb_get_keyboard_mapping_reply(connection, mapping_cookie, NULL)); if (!mapping_reply) { throw XcbException("Cannot get keyboard mapping."); diff --git a/src/platform/gui/xcb/UiApplication.cpp b/src/platform/gui/xcb/UiApplication.cpp index 0a6ab07f..1e5613b4 100644 --- a/src/platform/gui/xcb/UiApplication.cpp +++ b/src/platform/gui/xcb/UiApplication.cpp @@ -1,10 +1,15 @@ #include "cru/platform/gui/xcb/UiApplication.h" +#include "cru/base/Base.h" +#include "cru/base/Guard.h" #include "cru/platform/graphics/cairo/CairoGraphicsFactory.h" +#include "cru/platform/gui/Window.h" +#include "cru/platform/gui/xcb/Cursor.h" #include "cru/platform/gui/xcb/Window.h" #include <poll.h> #include <xcb/xcb.h> +#include <algorithm> namespace cru::platform::gui::xcb { XcbUiApplication::XcbUiApplication( @@ -20,6 +25,7 @@ XcbUiApplication::XcbUiApplication( int screen_num; xcb_connection_t *connection = xcb_connect(NULL, &screen_num); + xcb_connection_ = connection; this->CheckXcbConnectionError(); event_loop_.SetPoll(xcb_get_file_descriptor(connection), POLLIN, @@ -28,15 +34,23 @@ XcbUiApplication::XcbUiApplication( const xcb_setup_t *setup = xcb_get_setup(connection); xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); this->screen_ = iter.data; + + cursor_manager_ = new XcbCursorManager(this); } XcbUiApplication::~XcbUiApplication() { + delete cursor_manager_; + xcb_disconnect(this->xcb_connection_); if (release_cairo_factory_) { delete cairo_factory_; } } +graphics::cairo::CairoGraphicsFactory *XcbUiApplication::GetCairoFactory() { + return cairo_factory_; +} + void XcbUiApplication::CheckXcbConnectionError() { if (xcb_connection_has_error(this->xcb_connection_)) { throw XcbException("xcb_connection_has_error returned non-zero."); @@ -47,6 +61,8 @@ xcb_connection_t *XcbUiApplication::GetXcbConnection() { return xcb_connection_; } +void XcbUiApplication::XcbFlush() { xcb_flush(xcb_connection_); } + xcb_screen_t *XcbUiApplication::GetFirstXcbScreen() { return screen_; } xcb_atom_t XcbUiApplication::GetOrCreateXcbAtom(std::string name) { @@ -57,7 +73,8 @@ xcb_atom_t XcbUiApplication::GetOrCreateXcbAtom(std::string name) { auto cookie = xcb_intern_atom(xcb_connection_, false, name.size(), name.data()); - auto reply = xcb_intern_atom_reply(xcb_connection_, cookie, nullptr); + auto reply = + FreeLater(xcb_intern_atom_reply(xcb_connection_, cookie, nullptr)); auto atom = reply->atom; xcb_atom_.emplace(std::move(name), atom); return atom; @@ -122,6 +139,25 @@ void XcbUiApplication::HandleXEvents() { } } +std::vector<INativeWindow *> XcbUiApplication::GetAllWindow() { + std::vector<INativeWindow *> windows(windows_.size()); + std::ranges::copy(windows_, windows.begin()); + return windows; +} + +INativeWindow *XcbUiApplication::CreateWindow() { return new XcbWindow(this); } + +cru::platform::graphics::IGraphicsFactory * +XcbUiApplication::GetGraphicsFactory() { + return cairo_factory_; +} + +ICursorManager *XcbUiApplication::GetCursorManager() { return cursor_manager_; } + +IClipboard *XcbUiApplication::GetClipboard() { NotImplemented(); } + +IMenu *XcbUiApplication::GetApplicationMenu() { return nullptr; } + void XcbUiApplication::RegisterWindow(XcbWindow *window) { windows_.push_back(window); } diff --git a/src/platform/gui/xcb/Window.cpp b/src/platform/gui/xcb/Window.cpp index 188f62b8..e6e92fbc 100644 --- a/src/platform/gui/xcb/Window.cpp +++ b/src/platform/gui/xcb/Window.cpp @@ -1,17 +1,23 @@ #include "cru/platform/gui/xcb/Window.h" #include "cru/base/Base.h" +#include "cru/base/Guard.h" #include "cru/platform/Check.h" +#include "cru/platform/GraphicsBase.h" +#include "cru/platform/graphics/NullPainter.h" #include "cru/platform/graphics/Painter.h" #include "cru/platform/graphics/cairo/CairoPainter.h" #include "cru/platform/gui/Base.h" #include "cru/platform/gui/Keyboard.h" #include "cru/platform/gui/Window.h" +#include "cru/platform/gui/xcb/Cursor.h" #include "cru/platform/gui/xcb/Keyboard.h" #include "cru/platform/gui/xcb/UiApplication.h" #include <cairo-xcb.h> #include <cairo.h> #include <xcb/xcb.h> +#include <xcb/xproto.h> +#include <algorithm> #include <cassert> #include <cinttypes> #include <cstdint> @@ -66,9 +72,12 @@ XcbWindow::XcbWindow(XcbUiApplication *application) XcbWindow::~XcbWindow() { application_->UnregisterWindow(this); } +bool XcbWindow::IsCreated() { return xcb_window_.has_value(); } + void XcbWindow::Close() { if (xcb_window_) { xcb_destroy_window(application_->GetXcbConnection(), *xcb_window_); + application_->XcbFlush(); } } @@ -79,6 +88,7 @@ void XcbWindow::SetParent(INativeWindow *parent) { if (xcb_window_) { DoSetParent(*xcb_window_); } + application_->XcbFlush(); } WindowStyleFlag XcbWindow::GetStyleFlag() { return style_; } @@ -88,6 +98,7 @@ void XcbWindow::SetStyleFlag(WindowStyleFlag flag) { if (xcb_window_) { DoSetStyleFlags(*xcb_window_); } + application_->XcbFlush(); } String XcbWindow::GetTitle() { return String::FromUtf8(title_); } @@ -97,6 +108,7 @@ void XcbWindow::SetTitle(String title) { if (xcb_window_) { DoSetTitle(*xcb_window_); } + application_->XcbFlush(); } namespace { @@ -140,7 +152,6 @@ void XcbWindow::SetVisibility(WindowVisibilityType visibility) { auto old_value = static_cast<std::uint32_t *>( XcbGetProperty(*xcb_window_, atom, atom, 0, 2)); if (old_value) value[1] = old_value[1]; - UnreachableCode(); xcb_change_property(application_->GetXcbConnection(), XCB_PROP_MODE_REPLACE, window, atom, atom, 32, sizeof(value) / sizeof(*value), @@ -175,10 +186,125 @@ void XcbWindow::SetVisibility(WindowVisibilityType visibility) { default: UnreachableCode(); } + + application_->XcbFlush(); +} + +Size XcbWindow::GetClientSize() { return GetClientRect().GetSize(); } + +void XcbWindow::SetClientSize(const Size &size) { + auto rect = GetClientRect(); + SetClientRect(Rect(rect.GetLeftTop(), size)); +} + +Rect XcbWindow::GetClientRect() { + if (!xcb_window_) { + return Rect{}; + } + + auto window = *xcb_window_; + + auto cookie = xcb_get_geometry(application_->GetXcbConnection(), window); + auto reply = FreeLater(xcb_get_geometry_reply( + application_->GetXcbConnection(), cookie, nullptr)); + auto position = GetXcbWindowPosition(window); + + return Rect(position.x, position.y, reply->width, reply->height); +} + +void XcbWindow::SetClientRect(const Rect &rect) { + if (!xcb_window_) return; + DoSetClientRect(*xcb_window_, rect); + application_->XcbFlush(); +} + +Rect XcbWindow::GetWindowRect() { + if (!xcb_window_) return {}; + + auto client_rect = GetClientRect(); + auto frame_properties = Get_NET_FRAME_EXTENTS(*xcb_window_); + + if (frame_properties.has_value()) { + return client_rect.Expand(*frame_properties); + } + + return client_rect; +} + +void XcbWindow::SetWindowRect(const Rect &rect) { + if (!xcb_window_) return; + + auto real_rect = rect; + auto frame_properties = Get_NET_FRAME_EXTENTS(*xcb_window_); + + if (frame_properties.has_value()) { + real_rect = real_rect.Shrink(*frame_properties, true); + } + + SetClientRect(real_rect); +} + +bool XcbWindow::RequestFocus() { + if (!xcb_window_) return false; + xcb_set_input_focus(application_->GetXcbConnection(), XCB_NONE, *xcb_window_, + XCB_CURRENT_TIME); + application_->XcbFlush(); + return true; +} + +Point XcbWindow::GetMousePosition() { + auto window = xcb_window_.value_or(application_->GetFirstXcbScreen()->root); + auto cookie = xcb_query_pointer(application_->GetXcbConnection(), window); + auto reply = FreeLater(xcb_query_pointer_reply( + application_->GetXcbConnection(), cookie, nullptr)); + return Point(reply->win_x, reply->win_y); +} + +bool XcbWindow::CaptureMouse() { + if (!xcb_window_) return false; + // KDE handles grabbing automatically. + // + // xcb_grab_pointer(application_->GetXcbConnection(), TRUE, *xcb_window_, 0, + // XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, XCB_WINDOW_NONE, + // XCB_CURSOR_NONE, XCB_CURRENT_TIME); + // application_->XcbFlush(); + return true; +} + +bool XcbWindow::ReleaseMouse() { + if (!xcb_window_) return false; + // KDE handles grabbing automatically. + // + // xcb_ungrab_pointer(application_->xcb_connection_, XCB_CURRENT_TIME); + // application_->XcbFlush(); + return true; +} + +void XcbWindow::SetCursor(std::shared_ptr<ICursor> cursor) { + if (!xcb_window_) return; + auto xcb_cursor = CheckPlatform<XcbCursor>(cursor, GetPlatformId()); + cursor_ = xcb_cursor; + DoSetCursor(*xcb_window_, xcb_cursor.get()); +} + +void XcbWindow::SetToForeground() { + SetVisibility(WindowVisibilityType::Show); + assert(xcb_window_.has_value()); + const static uint32_t values[] = {XCB_STACK_MODE_ABOVE}; + xcb_configure_window(application_->GetXcbConnection(), *xcb_window_, + XCB_CONFIG_WINDOW_STACK_MODE, values); + application_->XcbFlush(); +} + +void XcbWindow::RequestRepaint() { + // TODO: true throttle + paint_event_.Raise(nullptr); } std::unique_ptr<graphics::IPainter> XcbWindow::BeginPaint() { - assert(cairo_surface_); + if (!xcb_window_.has_value()) { + return std::make_unique<graphics::NullPainter>(); + } auto factory = application_->GetCairoFactory(); cairo_t *cairo = cairo_create(cairo_surface_); @@ -225,6 +351,8 @@ IEvent<NativeKeyEventArgs> *XcbWindow::KeyDownEvent() { IEvent<NativeKeyEventArgs> *XcbWindow::KeyUpEvent() { return &key_up_event_; } +IInputMethodContext *XcbWindow::GetInputMethodContext() { return nullptr; } + std::optional<xcb_window_t> XcbWindow::GetXcbWindow() { return xcb_window_; } xcb_window_t XcbWindow::DoCreateWindow() { @@ -253,8 +381,16 @@ xcb_window_t XcbWindow::DoCreateWindow() { xcb_window_ = window; + std::vector<xcb_atom_t> wm_protocols{ + application_->GetXcbAtomWM_DELETE_WINDOW()}; + xcb_change_property(application_->GetXcbConnection(), XCB_PROP_MODE_REPLACE, + window, application_->GetXcbAtomWM_PROTOCOLS(), + XCB_ATOM_ATOM, 32, wm_protocols.size(), + wm_protocols.data()); + DoSetStyleFlags(window); DoSetParent(window); + DoSetCursor(window, cursor_.get()); xcb_visualtype_t *visual_type; @@ -291,6 +427,14 @@ void XcbWindow::HandleEvent(xcb_generic_event_t *event) { cairo_surface_destroy(cairo_surface_); cairo_surface_ = nullptr; xcb_window_ = std::nullopt; + + if (application_->IsQuitOnAllWindowClosed() && + std::ranges::none_of( + application_->GetAllWindow(), + [](INativeWindow *window) { return window->IsCreated(); })) { + application_->RequestQuit(0); + } + break; } case XCB_CONFIGURE_NOTIFY: { @@ -388,6 +532,16 @@ void XcbWindow::HandleEvent(xcb_generic_event_t *event) { key_up_event_.Raise(std::move(args)); break; } + case XCB_CLIENT_MESSAGE: { + xcb_client_message_event_t *cm = (xcb_client_message_event_t *)event; + if (cm->data.data32[0] == application_->GetXcbAtomWM_DELETE_WINDOW() && + xcb_window_.has_value()) { + xcb_destroy_window(application_->GetXcbConnection(), + xcb_window_.value()); + xcb_flush(application_->GetXcbConnection()); + } + break; + } default: /* Unknown event type, ignore it */ printf("Unknown event: %" PRIu8 "\n", event->response_type); @@ -455,6 +609,10 @@ std::optional<xcb_window_t> XcbWindow::GetEventWindow( xcb_key_release_event_t *kr = (xcb_key_release_event_t *)event; return kr->event; } + case XCB_CLIENT_MESSAGE: { + xcb_client_message_event_t *cm = (xcb_client_message_event_t *)event; + return cm->window; + } default: return std::nullopt; } @@ -493,14 +651,38 @@ void XcbWindow::DoSetTitle(xcb_window_t window) { } } +void XcbWindow::DoSetClientRect(xcb_window_t window, const Rect &rect) { + auto tree_cookie = xcb_query_tree(application_->GetXcbConnection(), window); + auto tree_reply = FreeLater(xcb_query_tree_reply( + application_->GetXcbConnection(), tree_cookie, nullptr)); + auto parent_position = GetXcbWindowPosition(tree_reply->parent); + + std::uint32_t values[4]{ + static_cast<std::uint32_t>(rect.left - parent_position.x), + static_cast<std::uint32_t>(rect.top - parent_position.y), + static_cast<std::uint32_t>(rect.width), + static_cast<std::uint32_t>(rect.height)}; + xcb_configure_window(application_->GetXcbConnection(), window, + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT, + values); +} + +void XcbWindow::DoSetCursor(xcb_window_t window, XcbCursor *cursor) { + std::uint32_t values[]{cursor ? cursor->GetXcbCursor() : XCB_CURSOR_NONE}; + xcb_change_window_attributes(application_->GetXcbConnection(), window, + XCB_CW_CURSOR, values); + application_->XcbFlush(); +} + void *XcbWindow::XcbGetProperty(xcb_window_t window, xcb_atom_t property, xcb_atom_t type, std::uint32_t offset, std::uint32_t length, std::uint32_t *out_length) { auto cookie = xcb_get_property(application_->GetXcbConnection(), false, window, property, type, offset, length); - auto reply = - xcb_get_property_reply(application_->GetXcbConnection(), cookie, NULL); + auto reply = FreeLater( + xcb_get_property_reply(application_->GetXcbConnection(), cookie, NULL)); if (reply->type == XCB_ATOM_NONE) { return nullptr; } @@ -509,4 +691,41 @@ void *XcbWindow::XcbGetProperty(xcb_window_t window, xcb_atom_t property, } return xcb_get_property_value(reply); } + +std::optional<Thickness> XcbWindow::Get_NET_FRAME_EXTENTS(xcb_window_t window) { + auto frame_properties = static_cast<std::uint32_t *>( + XcbGetProperty(window, application_->GetXcbAtom_NET_FRAME_EXTENTS(), + XCB_ATOM_CARDINAL, 0, 4)); + + if (frame_properties == nullptr) { + return std::nullopt; + } + + return Thickness(frame_properties[0], frame_properties[2], + frame_properties[1], frame_properties[3]); +} + +Point XcbWindow::GetXcbWindowPosition(xcb_window_t window) { + Point result; + + while (true) { + auto cookie = xcb_get_geometry(application_->GetXcbConnection(), window); + auto reply = FreeLater(xcb_get_geometry_reply( + application_->GetXcbConnection(), cookie, nullptr)); + result.x += reply->x; + result.y += reply->y; + + auto tree_cookie = xcb_query_tree(application_->GetXcbConnection(), window); + auto tree_reply = FreeLater(xcb_query_tree_reply( + application_->GetXcbConnection(), tree_cookie, nullptr)); + window = tree_reply->parent; + // TODO: Multi-screen offset? + if (tree_reply->root == window || window == XCB_WINDOW_NONE) { + break; + } + } + + return result; +} + } // namespace cru::platform::gui::xcb |