diff --git a/CMakeLists.txt b/CMakeLists.txt index 95a029c..5689f27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -325,6 +325,15 @@ else() set(EUI_APP_MAIN_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/core/app/glfw_app_main.cpp") endif() +if(UNIX AND NOT APPLE) + # window_backend.cpp's dragWindow() speaks _NET_WM_MOVERESIZE directly, + # which needs libX11. Neither GLFW nor SDL2 forwards X11 to the app. + find_package(X11 QUIET) + if(X11_FOUND) + target_link_libraries(eui_neo PRIVATE X11::X11) + endif() +endif() + target_link_libraries(eui_neo PUBLIC Freetype::Freetype) # ----------------------------------------------------------------------------- @@ -787,6 +796,12 @@ if(EUI_NEO_WINDOW_BACKEND STREQUAL "sdl2") find_dependency(SDL2 CONFIG) endif() +if(NOT APPLE AND NOT WIN32) + # window_backend.cpp's dragWindow()/startWindowResize() speak X11 directly, + # so eui_neo carries X11 symbols and the consumer must link libX11 too. + find_dependency(X11) +endif() + if(NOT WIN32) find_dependency(CURL) endif() diff --git a/components/components.h b/components/components.h index abddd41..1367e27 100644 --- a/components/components.h +++ b/components/components.h @@ -24,6 +24,7 @@ #include "components/dialog.h" #include "components/sidebar.h" #include "components/navbar.h" +#include "components/titlebar.h" #include "components/toast.h" #include "components/tooltip.h" #include "components/contextmenu.h" diff --git a/components/titlebar.h b/components/titlebar.h new file mode 100644 index 0000000..2b75625 --- /dev/null +++ b/components/titlebar.h @@ -0,0 +1,215 @@ +#pragma once + +#include "components/button.h" +#include "components/mousearea.h" +#include "eui/app.h" +#include "core/dsl.h" + +#include +#include +#include +#include + +namespace components { + +// Custom window decoration for a frameless window (see DslAppConfig::frameless). +// Covers the WHOLE window: a title strip at the top (title + minimize/maximize/ +// close buttons, drag-to-move) plus optional resize handles along the four +// edges and corners. Pass size() the full window dimensions and titleHeight() +// the strip height. +// +// By default the controls drive the running DSL app's window through +// app::minimizeWindow() / app::toggleMaximizeWindow() / app::requestWindowClose() +// / app::dragWindow() / app::startWindowResize(edge). Every action can be +// overridden with the on*() setters. +class TitleBarBuilder { +public: + TitleBarBuilder(core::dsl::Ui& ui, std::string id) + : ui_(ui), id_(std::move(id)) {} + + TitleBarBuilder& size(float width, float height) { + width_ = width; + height_ = height; + return *this; + } + TitleBarBuilder& titleHeight(float value) { titleHeight_ = value; return *this; } + TitleBarBuilder& title(std::string value) { title_ = std::move(value); return *this; } + TitleBarBuilder& fontSize(float value) { fontSize_ = value; return *this; } + TitleBarBuilder& background(const core::Color& value) { background_ = value; return *this; } + TitleBarBuilder& titleColor(const core::Color& value) { titleColor_ = value; return *this; } + TitleBarBuilder& buttonHover(const core::Color& value) { buttonHover_ = value; return *this; } + TitleBarBuilder& closeHover(const core::Color& value) { closeHover_ = value; return *this; } + TitleBarBuilder& showMinimize(bool value = true) { showMinimize_ = value; return *this; } + TitleBarBuilder& showMaximize(bool value = true) { showMaximize_ = value; return *this; } + TitleBarBuilder& showClose(bool value = true) { showClose_ = value; return *this; } + TitleBarBuilder& draggable(bool value = true) { draggable_ = value; return *this; } + TitleBarBuilder& resizable(bool value = true) { resizable_ = value; return *this; } + TitleBarBuilder& resizeMargin(float value) { resizeMargin_ = value; return *this; } + TitleBarBuilder& onMinimize(std::function callback) { onMinimize_ = std::move(callback); return *this; } + TitleBarBuilder& onMaximize(std::function callback) { onMaximize_ = std::move(callback); return *this; } + TitleBarBuilder& onClose(std::function callback) { onClose_ = std::move(callback); return *this; } + TitleBarBuilder& onDrag(std::function callback) { onDrag_ = std::move(callback); return *this; } + // 自定义标题栏内容:传入后完全接管标题栏内容区(取代默认的 title + 三按钮), + // 背景拖动 / resize 手柄仍自动提供。回调里用 ui.text / components::button 等自由搭建。 + TitleBarBuilder& content(std::function callback) { content_ = std::move(callback); return *this; } + + void build() { + const float w = width_ > 0.0f ? width_ : 400.0f; + const float h = height_ > 0.0f ? height_ : 300.0f; + const float th = titleHeight_ > 0.0f ? titleHeight_ : 48.0f; + const float m = resizable_ ? std::max(2.0f, resizeMargin_) : 0.0f; + const float font = fontSize_ > 0.0f ? fontSize_ : th * 0.40f; + const float buttonSize = th * 0.88f; + const float buttonGap = th * 0.12f; + const float pad = th * 0.35f; + + const std::function minimize = onMinimize_ ? onMinimize_ : [] { app::minimizeWindow(); }; + const std::function maximize = onMaximize_ ? onMaximize_ : [] { app::toggleMaximizeWindow(); }; + const std::function close = onClose_ ? onClose_ : [] { app::requestWindowClose(); }; + const std::function drag = onDrag_ ? onDrag_ : [] { app::dragWindow(); }; + + int controlCount = 0; + if (showMinimize_) { ++controlCount; } + if (showMaximize_) { ++controlCount; } + if (showClose_) { ++controlCount; } + const float buttonsWidth = controlCount > 0 + ? static_cast(controlCount) * buttonSize + static_cast(controlCount - 1) * buttonGap + : 0.0f; + + ui_.stack(id_) + .size(w, h) + .content([&] { + // Title strip background. The top `m` pixels are covered by the + // resize handle (higher z-index), so the draggable region is + // effectively y in [m, th]. + auto background = ui_.rect(id_ + ".bg") + .size(w, th) + .color(background_); + if (draggable_) { + background.onPress([drag](const core::PointerEvent&, const core::Rect&) { drag(); }); + } + background.build(); + + if (content_) { + // 用户自定义标题栏内容:stack 容器,可用 row/column/x/y 自由布局 + ui_.stack(id_ + ".content") + .size(w, th) + .content(content_) + .build(); + } else { + // 默认布局:标题靠左,窗口控制按钮靠右 + if (!title_.empty()) { + ui_.text(id_ + ".title") + .x(pad) + .size(std::max(0.0f, w - pad - buttonsWidth - buttonGap - pad), th) + .text(title_) + .fontSize(font) + .color(titleColor_) + .horizontalAlign(core::HorizontalAlign::Left) + .verticalAlign(core::VerticalAlign::Center) + .build(); + } + + if (controlCount > 0) { + ui_.row(id_ + ".buttons") + .x(w - pad - buttonsWidth) + .y((th - buttonSize) * 0.5f) + .size(buttonsWidth, buttonSize) + .gap(buttonGap) + .content([&] { + if (showMinimize_) { + controlButton(id_ + ".min", 0xF2D1, buttonSize, background_, buttonHover_, minimize); + } + if (showMaximize_) { + // 全屏/还原图标随窗口状态切换:未全屏显示全屏图标, + // 已全屏显示还原图标 + controlButton(id_ + ".max", + app::isWindowMaximized() ? 0xF2D2 : 0xF2D0, + buttonSize, background_, buttonHover_, maximize); + } + if (showClose_) { + controlButton(id_ + ".close", 0xF00D, buttonSize, background_, closeHover_, close); + } + }) + .build(); + } + } + + if (resizable_) { + resizeHandle(id_ + ".rs.tl", 0.0f, 0.0f, m, m, eui::window::ResizeEdge::TopLeft); + resizeHandle(id_ + ".rs.t", m, 0.0f, w - 2.0f * m, m, eui::window::ResizeEdge::Top); + resizeHandle(id_ + ".rs.tr", w - m, 0.0f, m, m, eui::window::ResizeEdge::TopRight); + resizeHandle(id_ + ".rs.r", w - m, m, m, h - 2.0f * m, eui::window::ResizeEdge::Right); + resizeHandle(id_ + ".rs.br", w - m, h - m, m, m, eui::window::ResizeEdge::BottomRight); + resizeHandle(id_ + ".rs.b", m, h - m, w - 2.0f * m, m, eui::window::ResizeEdge::Bottom); + resizeHandle(id_ + ".rs.bl", 0.0f, h - m, m, m, eui::window::ResizeEdge::BottomLeft); + resizeHandle(id_ + ".rs.l", 0.0f, m, m, h - 2.0f * m, eui::window::ResizeEdge::Left); + } + }) + .build(); + } + +private: + void controlButton(const std::string& buttonId, + unsigned int codepoint, + float buttonSize, + const core::Color& normal, + const core::Color& hover, + const std::function& action) { + components::button(ui_, buttonId) + .size(buttonSize, buttonSize) + .text("") // 清掉默认 "Button" 文字,只保留图标 + .icon(codepoint) + .iconSize(buttonSize * 0.46f) + .colors(normal, hover, hover) + .iconColor(titleColor_) + .radius(buttonSize * 0.22f) + .shadow(0.0f, 0.0f, 0.0f, core::Color{0.0f, 0.0f, 0.0f, 0.0f}) + .onClick(action) + .build(); + } + + void resizeHandle(const std::string& handleId, + float x, + float y, + float width, + float height, + eui::window::ResizeEdge edge) { + components::mouseArea(ui_, handleId) + .position(x, y) + .size(width, height) + .zIndex(100) + .color(core::Color{0.0f, 0.0f, 0.0f, 0.0f}) + .onPress([edge](const MouseEvent&) { app::startWindowResize(edge); }) + .build(); + } + + core::dsl::Ui& ui_; + std::string id_; + float width_ = 0.0f; + float height_ = 0.0f; + float titleHeight_ = 0.0f; + float fontSize_ = 0.0f; + float resizeMargin_ = 6.0f; + std::string title_; + core::Color background_ = {0.12f, 0.13f, 0.15f, 1.0f}; + core::Color titleColor_ = {0.92f, 0.94f, 0.97f, 1.0f}; + core::Color buttonHover_ = {1.0f, 1.0f, 1.0f, 0.12f}; + core::Color closeHover_ = {0.90f, 0.25f, 0.23f, 1.0f}; + bool showMinimize_ = true; + bool showMaximize_ = true; + bool showClose_ = true; + bool draggable_ = true; + bool resizable_ = true; + std::function onMinimize_; + std::function onMaximize_; + std::function onClose_; + std::function onDrag_; + std::function content_; +}; + +inline TitleBarBuilder titlebar(core::dsl::Ui& ui, const std::string& id) { + return TitleBarBuilder(ui, id); +} + +} // namespace components diff --git a/core/app/glfw_app_main.cpp b/core/app/glfw_app_main.cpp index 709e30b..59358c2 100644 --- a/core/app/glfw_app_main.cpp +++ b/core/app/glfw_app_main.cpp @@ -409,6 +409,7 @@ int main() { windowRequest.height = app::initialWindowHeight(); windowRequest.title = app::windowTitle(); windowRequest.renderApi = core::render::windowRenderApi(); + windowRequest.decorated = !app::frameless(); GLFWwindow* window = static_cast(core::window::createWindow(windowRequest)); if (!window) { glfwTerminate(); diff --git a/core/app/sdl2_app_main.cpp b/core/app/sdl2_app_main.cpp index c669fbf..a3385e4 100644 --- a/core/app/sdl2_app_main.cpp +++ b/core/app/sdl2_app_main.cpp @@ -506,6 +506,7 @@ int main() { windowRequest.height = app::initialWindowHeight(); windowRequest.title = app::windowTitle(); windowRequest.renderApi = core::render::windowRenderApi(); + windowRequest.decorated = !app::frameless(); SDL_Window* window = static_cast(core::window::createWindow(windowRequest)); if (window == nullptr) { SDL_Quit(); diff --git a/core/window/window_backend.cpp b/core/window/window_backend.cpp index e1db9fe..81da902 100644 --- a/core/window/window_backend.cpp +++ b/core/window/window_backend.cpp @@ -10,6 +10,8 @@ #if defined(__linux__) && !defined(__ANDROID__) && defined(SDL_VIDEO_DRIVER_X11) #include #include +#include +#include #endif #ifdef None #undef None @@ -345,6 +347,9 @@ Handle createWindow(const WindowCreateRequest& request) { if (request.resizable) { flags |= SDL_WINDOW_RESIZABLE; } + if (!request.decorated) { + flags |= SDL_WINDOW_BORDERLESS; + } flags |= request.renderApi == RenderApi::Vulkan ? SDL_WINDOW_VULKAN : SDL_WINDOW_OPENGL; SDL_Window* window = SDL_CreateWindow( @@ -506,10 +511,177 @@ void setImeCursorRect(Handle window, float x, float y, float width, float height #endif } +void minimizeWindow(Handle window) { + if (window != nullptr) { + SDL_MinimizeWindow(static_cast(window)); + } +} + +void maximizeWindow(Handle window) { + if (window != nullptr) { + SDL_MaximizeWindow(static_cast(window)); + } +} + +void restoreWindow(Handle window) { + if (window != nullptr) { + SDL_RestoreWindow(static_cast(window)); + } +} + +bool isWindowMaximized(Handle window) { + if (window == nullptr) { + return false; + } + return (SDL_GetWindowFlags(static_cast(window)) & SDL_WINDOW_MAXIMIZED) != 0; +} + +void requestWindowClose(Handle window) { + if (window == nullptr) { + return; + } + SDL_Event event{}; + event.type = SDL_QUIT; + SDL_PushEvent(&event); +} + +bool dragWindow(Handle window) { + if (window == nullptr) { + return false; + } +#if defined(_WIN32) + HWND hwnd = hwndForSdlWindow(static_cast(window)); + if (hwnd == nullptr) { + return false; + } + ReleaseCapture(); + SendMessageW(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); + return true; +#elif defined(__linux__) && !defined(__ANDROID__) && defined(SDL_VIDEO_DRIVER_X11) + SDL_SysWMinfo info{}; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(static_cast(window), &info) != SDL_TRUE || + info.subsystem != SDL_SYSWM_X11) { + return false; + } + Display* display = info.info.x11.display; + const Window xwindow = info.info.x11.window; + if (display == nullptr || xwindow == 0) { + return false; + } + // The WM grabs the pointer from the root position we report; pass the + // cursor's current root coordinates so the window doesn't jump to (0,0). + int rootX = 0; + int rootY = 0; + { + Window rootRet = 0; + Window childRet = 0; + int winX = 0; + int winY = 0; + unsigned int mask = 0; + if (XQueryPointer(display, xwindow, &rootRet, &childRet, + &rootX, &rootY, &winX, &winY, &mask) == False) { + rootX = 0; + rootY = 0; + } + } + XEvent event{}; + event.xclient.type = ClientMessage; + event.xclient.message_type = XInternAtom(display, "_NET_WM_MOVERESIZE", False); + event.xclient.display = display; + event.xclient.window = xwindow; + event.xclient.format = 32; + event.xclient.data.l[0] = rootX; /* cursor root x */ + event.xclient.data.l[1] = rootY; /* cursor root y */ + event.xclient.data.l[2] = 8; /* _NET_WM_MOVERESIZE_MOVE */ + event.xclient.data.l[3] = Button1; + event.xclient.data.l[4] = 0; + XSendEvent(display, DefaultRootWindow(display), False, + SubstructureRedirectMask | SubstructureNotifyMask, &event); + XFlush(display); + return true; +#else + return false; +#endif +} + +bool startWindowResize(Handle window, ResizeEdge edge) { + if (window == nullptr) { + return false; + } +#if defined(_WIN32) + HWND hwnd = hwndForSdlWindow(static_cast(window)); + if (hwnd == nullptr) { + return false; + } + static const int hitTests[] = { + HTTOPLEFT, HTTOP, HTTOPRIGHT, HTRIGHT, + HTBOTTOMRIGHT, HTBOTTOM, HTBOTTOMLEFT, HTLEFT + }; + ReleaseCapture(); + SendMessageW(hwnd, WM_NCLBUTTONDOWN, hitTests[static_cast(edge)], 0); + return true; +#elif defined(__linux__) && !defined(__ANDROID__) && defined(SDL_VIDEO_DRIVER_X11) + SDL_SysWMinfo info{}; + SDL_VERSION(&info.version); + if (SDL_GetWindowWMInfo(static_cast(window), &info) != SDL_TRUE || + info.subsystem != SDL_SYSWM_X11) { + return false; + } + Display* display = info.info.x11.display; + const Window xwindow = info.info.x11.window; + if (display == nullptr || xwindow == 0) { + return false; + } + // ResizeEdge enum order matches _NET_WM_MOVERESIZE directions + // (SIZE_TOPLEFT=0 .. SIZE_LEFT=7). + const int direction = static_cast(edge); + int rootX = 0; + int rootY = 0; + { + Window rootRet = 0; + Window childRet = 0; + int winX = 0; + int winY = 0; + unsigned int mask = 0; + if (XQueryPointer(display, xwindow, &rootRet, &childRet, + &rootX, &rootY, &winX, &winY, &mask) == False) { + rootX = 0; + rootY = 0; + } + } + XEvent event{}; + event.xclient.type = ClientMessage; + event.xclient.message_type = XInternAtom(display, "_NET_WM_MOVERESIZE", False); + event.xclient.display = display; + event.xclient.window = xwindow; + event.xclient.format = 32; + event.xclient.data.l[0] = rootX; + event.xclient.data.l[1] = rootY; + event.xclient.data.l[2] = direction; + event.xclient.data.l[3] = Button1; + event.xclient.data.l[4] = 0; + XSendEvent(display, DefaultRootWindow(display), False, + SubstructureRedirectMask | SubstructureNotifyMask, &event); + XFlush(display); + return true; +#else + return false; +#endif +} + } // namespace core::window #else +#if defined(_WIN32) +#define GLFW_EXPOSE_NATIVE_WIN32 +#elif defined(__linux__) && !defined(__ANDROID__) +#define GLFW_EXPOSE_NATIVE_X11 +#elif defined(__APPLE__) +#define GLFW_EXPOSE_NATIVE_COCOA +#endif + #ifndef GLFW_INCLUDE_NONE #define GLFW_INCLUDE_NONE #endif @@ -517,6 +689,36 @@ void setImeCursorRect(Handle window, float x, float y, float width, float height #include "core/platform/ime_bridge.h" +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#elif defined(__linux__) && !defined(__ANDROID__) +#include +#include +#include +// X11 headers (including the ones glfw3native.h pulls in) use the Bool/Status +// macros below; undef only after they are all included to avoid polluting the +// C++ code that follows. +#ifdef None +#undef None +#endif +#ifdef Bool +#undef Bool +#endif +#ifdef Status +#undef Status +#endif +#ifdef Success +#undef Success +#endif +#endif + namespace core::window { namespace { @@ -546,6 +748,7 @@ Handle createWindow(const WindowCreateRequest& request) { shareContext = static_cast(request.parent); } glfwWindowHint(GLFW_RESIZABLE, request.resizable ? GLFW_TRUE : GLFW_FALSE); + glfwWindowHint(GLFW_DECORATED, request.decorated ? GLFW_TRUE : GLFW_FALSE); return glfwCreateWindow( request.width, @@ -564,6 +767,17 @@ void destroyWindow(Handle window) { NativeWindowInfo nativeWindowInfo(Handle window) { NativeWindowInfo result; result.handle = window; + if (window == nullptr) { + return result; + } +#if defined(_WIN32) + result.platformWindow = glfwGetWin32Window(static_cast(window)); +#elif defined(__APPLE__) + result.platformWindow = glfwGetCocoaWindow(static_cast(window)); +#endif + // X11's Window is an XID (unsigned long), not a pointer, so platformWindow + // stays null there — matching the SDL2 backend. dragWindow() reads the X11 + // window directly via glfwGetX11Window() instead. return result; } @@ -616,6 +830,150 @@ void setImeCursorRect(Handle window, float x, float y, float width, float height eui_ime_set_cursor_rect_with_font(static_cast(window), x, y, width, height, height); } +void minimizeWindow(Handle window) { + if (window != nullptr) { + glfwIconifyWindow(static_cast(window)); + } +} + +void maximizeWindow(Handle window) { + if (window != nullptr) { + glfwMaximizeWindow(static_cast(window)); + } +} + +void restoreWindow(Handle window) { + if (window != nullptr) { + glfwRestoreWindow(static_cast(window)); + } +} + +bool isWindowMaximized(Handle window) { + if (window == nullptr) { + return false; + } + return glfwGetWindowAttrib(static_cast(window), GLFW_MAXIMIZED) == GLFW_TRUE; +} + +void requestWindowClose(Handle window) { + if (window != nullptr) { + glfwSetWindowShouldClose(static_cast(window), GLFW_TRUE); + } +} + +bool dragWindow(Handle window) { + if (window == nullptr) { + return false; + } +#if defined(_WIN32) + HWND hwnd = glfwGetWin32Window(static_cast(window)); + if (hwnd == nullptr) { + return false; + } + ReleaseCapture(); + SendMessageW(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); + return true; +#elif defined(__linux__) && !defined(__ANDROID__) + Display* display = glfwGetX11Display(); + const Window xwindow = glfwGetX11Window(static_cast(window)); + if (display == nullptr || xwindow == 0) { + return false; + } + // The WM grabs the pointer from the root position we report; pass the + // cursor's current root coordinates so the window doesn't jump to (0,0). + int rootX = 0; + int rootY = 0; + { + Window rootRet = 0; + Window childRet = 0; + int winX = 0; + int winY = 0; + unsigned int mask = 0; + if (XQueryPointer(display, xwindow, &rootRet, &childRet, + &rootX, &rootY, &winX, &winY, &mask) == False) { + rootX = 0; + rootY = 0; + } + } + XEvent event{}; + event.xclient.type = ClientMessage; + event.xclient.message_type = XInternAtom(display, "_NET_WM_MOVERESIZE", False); + event.xclient.display = display; + event.xclient.window = xwindow; + event.xclient.format = 32; + event.xclient.data.l[0] = rootX; /* cursor root x */ + event.xclient.data.l[1] = rootY; /* cursor root y */ + event.xclient.data.l[2] = 8; /* _NET_WM_MOVERESIZE_MOVE */ + event.xclient.data.l[3] = Button1; + event.xclient.data.l[4] = 0; + XSendEvent(display, DefaultRootWindow(display), False, + SubstructureRedirectMask | SubstructureNotifyMask, &event); + XFlush(display); + return true; +#else + return false; +#endif +} + +bool startWindowResize(Handle window, ResizeEdge edge) { + if (window == nullptr) { + return false; + } +#if defined(_WIN32) + HWND hwnd = glfwGetWin32Window(static_cast(window)); + if (hwnd == nullptr) { + return false; + } + static const int hitTests[] = { + HTTOPLEFT, HTTOP, HTTOPRIGHT, HTRIGHT, + HTBOTTOMRIGHT, HTBOTTOM, HTBOTTOMLEFT, HTLEFT + }; + ReleaseCapture(); + SendMessageW(hwnd, WM_NCLBUTTONDOWN, hitTests[static_cast(edge)], 0); + return true; +#elif defined(__linux__) && !defined(__ANDROID__) + Display* display = glfwGetX11Display(); + const Window xwindow = glfwGetX11Window(static_cast(window)); + if (display == nullptr || xwindow == 0) { + return false; + } + // ResizeEdge enum order matches _NET_WM_MOVERESIZE directions + // (SIZE_TOPLEFT=0 .. SIZE_LEFT=7). + const int direction = static_cast(edge); + int rootX = 0; + int rootY = 0; + { + Window rootRet = 0; + Window childRet = 0; + int winX = 0; + int winY = 0; + unsigned int mask = 0; + if (XQueryPointer(display, xwindow, &rootRet, &childRet, + &rootX, &rootY, &winX, &winY, &mask) == False) { + rootX = 0; + rootY = 0; + } + } + XEvent event{}; + event.xclient.type = ClientMessage; + event.xclient.message_type = XInternAtom(display, "_NET_WM_MOVERESIZE", False); + event.xclient.display = display; + event.xclient.window = xwindow; + event.xclient.format = 32; + event.xclient.data.l[0] = rootX; + event.xclient.data.l[1] = rootY; + event.xclient.data.l[2] = direction; + event.xclient.data.l[3] = Button1; + event.xclient.data.l[4] = 0; + XSendEvent(display, DefaultRootWindow(display), False, + SubstructureRedirectMask | SubstructureNotifyMask, &event); + XFlush(display); + return true; +#else + return false; +#endif +} + } // namespace core::window #endif diff --git a/core/window/window_backend.h b/core/window/window_backend.h index d16a550..6571c39 100644 --- a/core/window/window_backend.h +++ b/core/window/window_backend.h @@ -34,4 +34,17 @@ void installInputCallbacks(Handle window); void uninstallInputCallbacks(Handle window); bool queryImeComposition(Handle window, std::string& text, bool& composing); +// Window controls used by a custom (frameless) title bar. All are no-ops on a +// null handle. minimize/maximize/restore/close are cross-platform in both +// backends; dragWindow/startWindowResize need a per-platform bridge (see the +// .cpp) and return false when the platform can't synthesize the move/resize +// (e.g. Wayland). +void minimizeWindow(Handle window); +void maximizeWindow(Handle window); +void restoreWindow(Handle window); +bool isWindowMaximized(Handle window); +void requestWindowClose(Handle window); +bool dragWindow(Handle window); +bool startWindowResize(Handle window, ResizeEdge edge); + } // namespace core::window diff --git a/core/window/window_types.h b/core/window/window_types.h index 76a001c..0c76a4a 100644 --- a/core/window/window_types.h +++ b/core/window/window_types.h @@ -16,6 +16,17 @@ enum class RenderApi { Vulkan }; +enum class ResizeEdge { + TopLeft, + Top, + TopRight, + Right, + BottomRight, + Bottom, + BottomLeft, + Left +}; + struct WindowCreateRequest { int width = 0; int height = 0; @@ -23,6 +34,7 @@ struct WindowCreateRequest { bool resizable = true; bool highDpi = true; bool modal = false; + bool decorated = true; Handle parent = nullptr; RenderApi renderApi = RenderApi::OpenGL; }; diff --git a/examples/titlebar_custom_demo.cpp b/examples/titlebar_custom_demo.cpp new file mode 100644 index 0000000..467a78d --- /dev/null +++ b/examples/titlebar_custom_demo.cpp @@ -0,0 +1,100 @@ +// 无边框窗口 + 自定义标题栏 demo(自定义内容版)。 +// 与 titlebar_demo 的区别:用 titlebar 的 content() 回调完全自定义标题栏内容, +// 展示"想填什么自己填"的用法。背景拖动 / 四边四角 resize 仍由组件自动提供。 +#include "eui_neo.h" + +#include + +namespace app { + +const DslAppConfig& dslAppConfig() { + static const DslAppConfig config = DslAppConfig{} + .title("Frameless Custom TitleBar Demo") + .windowSize(800, 560) + .fps(60.0) + .frameless(true); + return config; +} + +void compose(eui::Ui& ui, const eui::Screen& screen) { + const float titleHeight = 48.0f; + + ui.stack("root") + .size(screen.width, screen.height) + .content([&] { + ui.column("content") + .y(titleHeight) + .size(screen.width, screen.height - titleHeight) + .padding(32.0f) + .gap(16.0f) + .content([&] { + ui.text("hint") + .text("这是自定义标题栏版:标题栏内容完全用 content() 回调自由搭建。\n" + "左侧是应用图标 + 标题,右侧是自定义设置按钮 + 最小化 + 关闭。") + .fontSize(16.0f) + .wrap(true) + .build(); + + components::button(ui, "my_button") + .text("点我") + .onClick([] { + std::cout << "按钮被点击了!" << std::endl; + }) + .build(); + }) + .build(); + + // 自定义标题栏:content() 回调完全接管内容区 + components::titlebar(ui, "titlebar") + .size(screen.width, screen.height) + .titleHeight(titleHeight) + .content([&] { + const float pad = 14.0f; + const float btn = 34.0f; + // 左侧:应用图标 + 标题 + ui.text("tb.icon") + .x(pad) + .size(20.0f, titleHeight) + .icon(0xF015) + .fontSize(16.0f) + .color(core::Color{0.90f, 0.94f, 1.0f, 1.0f}) + .horizontalAlign(core::HorizontalAlign::Left) + .verticalAlign(core::VerticalAlign::Center) + .build(); + ui.text("tb.title") + .x(pad + 24.0f) + .size(screen.width - 260.0f, titleHeight) + .text("我的自定义应用") + .fontSize(16.0f) + .color(core::Color{0.92f, 0.94f, 0.97f, 1.0f}) + .horizontalAlign(core::HorizontalAlign::Left) + .verticalAlign(core::VerticalAlign::Center) + .build(); + // 右侧:自定义按钮 + 窗口控制按钮 + ui.row("tb.buttons") + .x(screen.width - pad - btn * 3.0f - 12.0f) + .y((titleHeight - btn) * 0.5f) + .size(btn * 3.0f + 12.0f, btn) + .gap(6.0f) + .content([&] { + components::button(ui, "tb.settings") + .size(btn, btn).text("").icon(0xF013) + .onClick([] { std::cout << "设置按钮" << std::endl; }) + .build(); + components::button(ui, "tb.min") + .size(btn, btn).text("").icon(0xF2D1) + .onClick([] { app::minimizeWindow(); }) + .build(); + components::button(ui, "tb.close") + .size(btn, btn).text("").icon(0xF00D) + .onClick([] { app::requestWindowClose(); }) + .build(); + }) + .build(); + }) + .build(); + }) + .build(); +} + +} // namespace app diff --git a/examples/titlebar_demo.cpp b/examples/titlebar_demo.cpp new file mode 100644 index 0000000..3fc559d --- /dev/null +++ b/examples/titlebar_demo.cpp @@ -0,0 +1,62 @@ +// 无边框窗口 + 自定义标题栏 demo。 +// 行为: +// - frameless(true) 移除系统标题栏 +// - 拖动顶部标题栏移动窗口 +// - 标题栏右侧按钮控制最小化 / 最大化 / 关闭 +// - 拖动窗口四边 / 四角缩放窗口 +#include "eui_neo.h" + +#include + +namespace app { + +const DslAppConfig& dslAppConfig() { + static const DslAppConfig config = DslAppConfig{} + .title("Frameless TitleBar Demo") + .windowSize(800, 560) + .fps(60.0) + .frameless(true); // 无边框窗口,标题栏由 titlebar 组件自绘 + return config; +} + +void compose(eui::Ui& ui, const eui::Screen& screen) { + const float titleHeight = 48.0f; + + ui.stack("root") + .size(screen.width, screen.height) + .content([&] { + ui.column("content") + .y(titleHeight) + .size(screen.width, screen.height - titleHeight) + .padding(32.0f) + .gap(16.0f) + .content([&] { + ui.text("hint") + .text("这是一个无边框窗口,顶部是自定义标题栏。\n" + "拖动标题栏移动窗口,拖动四边/四角缩放窗口,右侧按钮控制最小化/最大化/关闭。") + .fontSize(16.0f) + .wrap(true) + .build(); + + components::button(ui, "my_button") + .text("点我") + .onClick([] { + std::cout << "按钮被点击了!" << std::endl; + }) + .build(); + }) + .build(); + + // 覆盖整个窗口:标题栏 + 四边四角 resize 手柄。 + // 默认布局(不传 content):左上角标题 + 右上角三按钮(最小化/全屏/关闭), + // 全屏后图标自动切换为"还原"。 + components::titlebar(ui, "titlebar") + .size(screen.width, screen.height) + .titleHeight(titleHeight) + .title("EUI-NEO 无边框标题栏 Demo") + .build(); + }) + .build(); +} + +} // namespace app diff --git a/include/eui/app.h b/include/eui/app.h index af4f1e0..dc67bf8 100644 --- a/include/eui/app.h +++ b/include/eui/app.h @@ -31,6 +31,7 @@ float uiScale(); bool trayEnabled(); const char* trayTitle(); const char* trayIconPath(); +bool frameless(); void requestUpdate(); bool initialize(eui::window::Handle window); bool update(eui::window::Handle window, float deltaSeconds, int windowWidth, int windowHeight, float dpiScale, float pointerScale); @@ -42,6 +43,17 @@ void releaseGraphicsResources(); void shutdown(); std::vector consumeWindowRequests(); +// Window controls for a custom (frameless) title bar. Each operates on the +// window backing the running DSL app, so they only have an effect after +// initialize() has captured the window handle. Safe no-ops otherwise. +void minimizeWindow(); +void maximizeWindow(); +void toggleMaximizeWindow(); +bool isWindowMaximized(); +void requestWindowClose(); +bool dragWindow(); +bool startWindowResize(eui::window::ResizeEdge edge); + namespace detail { void requestFullPaint(); } diff --git a/include/eui/detail/dsl_app_impl.h b/include/eui/detail/dsl_app_impl.h index 22a87af..a541d53 100644 --- a/include/eui/detail/dsl_app_impl.h +++ b/include/eui/detail/dsl_app_impl.h @@ -52,6 +52,15 @@ inline DslAppState& dslAppState() { return state; } +// The window currently backing the running DSL app. Set on initialize() and +// refreshed on every update(), so the window-control helpers below always act +// on the most recently driven window (the main window in the single-window +// case, which is what a custom title bar targets). +inline core::window::Handle& windowHandle() { + static core::window::Handle handle = nullptr; + return handle; +} + inline std::string resolveIconPath(const std::string& iconPath) { if (iconPath.empty()) { return {}; @@ -201,10 +210,47 @@ const char* trayIconPath() { return (config.trayIconPathValue.empty() ? config.iconPathValue : config.trayIconPathValue).c_str(); } +bool frameless() { + return dslAppConfig().framelessValue; +} + void requestUpdate() { core::platform::requestUiUpdate(); } +void minimizeWindow() { + core::window::minimizeWindow(detail::windowHandle()); +} + +void maximizeWindow() { + core::window::maximizeWindow(detail::windowHandle()); +} + +void toggleMaximizeWindow() { + const core::window::Handle window = detail::windowHandle(); + if (core::window::isWindowMaximized(window)) { + core::window::restoreWindow(window); + } else { + core::window::maximizeWindow(window); + } +} + +bool isWindowMaximized() { + return core::window::isWindowMaximized(detail::windowHandle()); +} + +void requestWindowClose() { + core::window::requestWindowClose(detail::windowHandle()); +} + +bool dragWindow() { + return core::window::dragWindow(detail::windowHandle()); +} + +bool startWindowResize(eui::window::ResizeEdge edge) { + return core::window::startWindowResize(detail::windowHandle(), edge); +} + namespace detail { void requestFullPaint() { @@ -223,6 +269,7 @@ bool initialize(core::window::Handle window) { detail::applyWindowIcon(window); state.iconApplied = true; } + detail::windowHandle() = window; return detail::dslRuntime().initialize(window); } @@ -241,6 +288,7 @@ bool update(core::window::Handle window, float deltaSeconds, int windowWidth, in return false; } + detail::windowHandle() = window; const DslAppConfig& config = dslAppConfig(); const float effectiveScale = dpiScale * uiScale(); const float logicalWidth = static_cast(windowWidth) / effectiveScale; diff --git a/include/eui/dsl_app.h b/include/eui/dsl_app.h index 6a10f3c..536d6ad 100644 --- a/include/eui/dsl_app.h +++ b/include/eui/dsl_app.h @@ -27,6 +27,7 @@ struct DslAppConfig { bool trayEnabledValue = false; std::string trayTitleValue; std::string trayIconPathValue; + bool framelessValue = false; DslAppConfig& title(std::string value) { titleValue = std::move(value); return *this; } DslAppConfig& pageId(std::string value) { pageIdValue = std::move(value); return *this; } @@ -68,6 +69,10 @@ struct DslAppConfig { trayIconPathValue = std::move(value); return *this; } + DslAppConfig& frameless(bool value = true) { + framelessValue = value; + return *this; + } }; struct DslWindowConfig { diff --git a/include/eui/window.h b/include/eui/window.h index e570d13..336e68c 100644 --- a/include/eui/window.h +++ b/include/eui/window.h @@ -8,5 +8,6 @@ using ContextKey = core::window::ContextKey; using CursorHandle = core::window::CursorHandle; using CursorType = core::window::CursorType; using Handle = core::window::Handle; +using ResizeEdge = core::window::ResizeEdge; } // namespace eui::window diff --git a/xmake.lua b/xmake.lua index 67e8243..5181869 100644 --- a/xmake.lua +++ b/xmake.lua @@ -344,6 +344,11 @@ target("eui_neo") elseif window_backend == "sdl2" then add_packages("libsdl2", {public = true}) end + if is_plat("linux") then + -- window_backend.cpp's dragWindow() speaks _NET_WM_MOVERESIZE directly, + -- which needs libX11. Neither GLFW nor SDL2 forwards X11 to the app. + add_syslinks("X11", {public = true}) + end add_packages("libcurl", {public = true, optional = true}) if not is_plat("windows", "mingw") and has_package("libcurl") then add_defines("EUI_HAS_CURL=1", {public = true})