From 095fe7919ebe563a1985e2262b5ae7d7d2d85e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Thu, 10 Sep 2026 19:27:29 -0300 Subject: [PATCH] feat(ui): add native multi-window application support - let UIApplication create, manage, render, and destroy multiple native windows - add scoped scene and GL window-context switching - route input events to the scene belonging to each native window - share application fonts, themes, icons, styles, and graphics resources safely - add NewInApplicationWindow factories for UIWindow, UIFileDialog, and UIMessageBox - support native parent relationships, modality, minimum sizes, and optional positioning - close child windows when the primary application window closes - defer native context destruction until the end of the application tick - fall back to in-scene windows for Emscripten and terminal rendering - reapply renderer state caches when switching OpenGL contexts - fix SDL2/SDL3 display bounds and multi-window lifecycle handling - add a native file-dialog and message-box multi-window example - add integration coverage for context switching, input, resources, modality, and closing --- .ecode/project_build.json | 6 + include/eepp/graphics/renderer/renderer.hpp | 3 + .../eepp/graphics/renderer/renderergl3.hpp | 2 + .../eepp/graphics/renderer/renderergl3cp.hpp | 2 + .../eepp/graphics/renderer/renderergles2.hpp | 2 + include/eepp/graphics/texturefactory.hpp | 3 + include/eepp/scene/scenemanager.hpp | 19 ++ include/eepp/ui/uiapplication.hpp | 79 ++++++ include/eepp/ui/uifiledialog.hpp | 14 + include/eepp/ui/uiiconthememanager.hpp | 4 + include/eepp/ui/uimessagebox.hpp | 13 +- include/eepp/ui/uiscenenode.hpp | 24 ++ include/eepp/ui/uiwindow.hpp | 29 ++ include/eepp/window/engine.hpp | 31 ++- include/eepp/window/input.hpp | 10 + include/eepp/window/window.hpp | 22 ++ premake4.lua | 6 + premake5.lua | 6 + src/eepp/graphics/renderer/renderer.cpp | 17 ++ src/eepp/graphics/renderer/renderergl3.cpp | 5 + src/eepp/graphics/renderer/renderergl3cp.cpp | 5 + src/eepp/graphics/renderer/renderergles2.cpp | 5 + src/eepp/graphics/texturefactory.cpp | 6 + src/eepp/scene/eventdispatcher.cpp | 17 +- src/eepp/scene/node.cpp | 12 + src/eepp/scene/scenemanager.cpp | 76 +++++- src/eepp/ui/uiapplication.cpp | 248 ++++++++++++++++-- src/eepp/ui/uifiledialog.cpp | 29 ++ src/eepp/ui/uiiconthememanager.cpp | 7 + src/eepp/ui/uimessagebox.cpp | 12 + src/eepp/ui/uinode.cpp | 2 +- src/eepp/ui/uiscenenode.cpp | 36 ++- src/eepp/ui/uiwindow.cpp | 150 +++++++++++ .../backend/SDL2/displaymanagersdl2.cpp | 4 +- src/eepp/window/backend/SDL2/inputsdl2.cpp | 41 +-- src/eepp/window/backend/SDL2/windowsdl2.cpp | 14 +- src/eepp/window/backend/SDL2/windowsdl2.hpp | 6 + .../backend/SDL3/displaymanagersdl3.cpp | 4 +- src/eepp/window/backend/SDL3/inputsdl3.cpp | 33 +-- src/eepp/window/backend/SDL3/windowsdl3.cpp | 19 +- src/eepp/window/backend/SDL3/windowsdl3.hpp | 6 + src/eepp/window/engine.cpp | 58 +++- src/eepp/window/input.cpp | 22 ++ src/eepp/window/window.cpp | 21 +- .../ui_application_multi_window.cpp | 34 +++ src/tests/unit_tests/uiscenenode_tests.cpp | 235 ++++++++++++++++- 46 files changed, 1290 insertions(+), 109 deletions(-) create mode 100644 src/examples/ui_application_multi_window/ui_application_multi_window.cpp diff --git a/.ecode/project_build.json b/.ecode/project_build.json index b5135be70..f17860032 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -411,6 +411,12 @@ "command": "${project_root}/bin/eepp-ui-data-handling-debug", "name": "eepp-ui-data-handling-debug", "working_dir": "${project_root}/bin" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-ui-application-multi-window-debug", + "name": "eepp-ui-application-multi-window-debug", + "working_dir": "${project_root}/bin" } ], "var": { diff --git a/include/eepp/graphics/renderer/renderer.hpp b/include/eepp/graphics/renderer/renderer.hpp index 57659a600..bfe81015e 100644 --- a/include/eepp/graphics/renderer/renderer.hpp +++ b/include/eepp/graphics/renderer/renderer.hpp @@ -170,6 +170,9 @@ class EE_API Renderer { virtual void enable( unsigned int cap ); + /** Reapplies renderer state cached outside OpenGL after changing the current context. */ + virtual void onContextChanged(); + virtual GraphicsLibraryVersion version() = 0; virtual std::string versionStr() = 0; diff --git a/include/eepp/graphics/renderer/renderergl3.hpp b/include/eepp/graphics/renderer/renderergl3.hpp index 772cf35fd..1e84ec01e 100644 --- a/include/eepp/graphics/renderer/renderergl3.hpp +++ b/include/eepp/graphics/renderer/renderergl3.hpp @@ -33,6 +33,8 @@ class EE_API RendererGL3 : public RendererGLShader { void enable( unsigned int cap ); + void onContextChanged(); + void enableClientState( unsigned int array ); void disableClientState( unsigned int array ); diff --git a/include/eepp/graphics/renderer/renderergl3cp.hpp b/include/eepp/graphics/renderer/renderergl3cp.hpp index 54a460490..ae79da103 100644 --- a/include/eepp/graphics/renderer/renderergl3cp.hpp +++ b/include/eepp/graphics/renderer/renderergl3cp.hpp @@ -33,6 +33,8 @@ class EE_API RendererGL3CP : public RendererGLShader { void enable( unsigned int cap ); + void onContextChanged(); + void enableClientState( unsigned int array ); void disableClientState( unsigned int array ); diff --git a/include/eepp/graphics/renderer/renderergles2.hpp b/include/eepp/graphics/renderer/renderergles2.hpp index 7345ed3d3..2298175d1 100644 --- a/include/eepp/graphics/renderer/renderergles2.hpp +++ b/include/eepp/graphics/renderer/renderergles2.hpp @@ -39,6 +39,8 @@ class EE_API RendererGLES2 : public RendererGLShader { void enable( unsigned int cap ); + void onContextChanged(); + void enableClientState( unsigned int array ); void disableClientState( unsigned int array ); diff --git a/include/eepp/graphics/texturefactory.hpp b/include/eepp/graphics/texturefactory.hpp index ed8fe5389..35d8d3f0e 100644 --- a/include/eepp/graphics/texturefactory.hpp +++ b/include/eepp/graphics/texturefactory.hpp @@ -172,6 +172,9 @@ class EE_API TextureFactory : protected Mutex { */ void setCurrentTexture( const int& textureHandle, const Uint32& TextureUnit ); + /** Invalidates cached OpenGL binding state after changing the current graphics context. */ + void invalidateTextureBindings(); + /** Returns the number of currently live textures. */ Uint32 getTextureCount(); diff --git a/include/eepp/scene/scenemanager.hpp b/include/eepp/scene/scenemanager.hpp index 68471dcc0..83f9be9ff 100644 --- a/include/eepp/scene/scenemanager.hpp +++ b/include/eepp/scene/scenemanager.hpp @@ -10,6 +10,9 @@ using namespace EE::System; namespace EE { namespace UI { class UISceneNode; }} // namespace EE::UI +namespace EE { namespace Window { +class Window; +}} // namespace EE::Window using namespace EE::UI; namespace EE { namespace Scene { @@ -33,19 +36,35 @@ class EE_API SceneManager { void draw(); + /** Draws only scene nodes associated with @p window using its graphics context. */ + void draw( EE::Window::Window* window ); + void update( const Time& elapsed ); void update(); UISceneNode* getUISceneNode(); + /** Returns the UI scene associated with @p window, or nullptr if none is registered. */ + UISceneNode* getUISceneNode( EE::Window::Window* window ); + void setCurrentUISceneNode( UISceneNode* uiSceneNode ); + /** Removes and destroys all registered scene nodes associated with @p window. */ + void destroyScenes( EE::Window::Window* window ); + Time getElapsed() const; protected: + friend class EE::UI::UISceneNode; + + /** Installs the ambient UI scene for the current scope and returns the previously scoped scene. + * UI scene and graphics-context switching is restricted to the application's UI thread. */ + UISceneNode* setScopedUISceneNode( UISceneNode* uiSceneNode ); + Clock mClock; UISceneNode* mUISceneNode; + UISceneNode* mScopedUISceneNode; std::vector mSceneNodes; }; diff --git a/include/eepp/ui/uiapplication.hpp b/include/eepp/ui/uiapplication.hpp index bb46a6425..d0a607f7f 100644 --- a/include/eepp/ui/uiapplication.hpp +++ b/include/eepp/ui/uiapplication.hpp @@ -2,10 +2,12 @@ #define EE_UI_UIAPPLICATION #include +#include #include #include #include +#include using namespace EE::Window; @@ -19,6 +21,17 @@ class UIApplicationSystemFontState; class EE_API UIApplication { public: + /** Controls when UIApplication stops its main loop. + * @see setQuitPolicy() */ + enum class QuitPolicy : Uint8 { + /** Stops after the last application-owned window closes. */ + OnLastWindowClosed, + /** Stops when the primary window closes and closes all secondary windows. */ + OnPrimaryWindowClosed, + /** Window closure never stops the loop; requestQuit() must be called explicitly. */ + Explicit + }; + struct EE_API Settings { Settings() {} @@ -35,6 +48,9 @@ class EE_API UIApplication { //! Must be set to true in order to initialize the basic UI resources (font and UI theme). //! Otherwise it will initialize with an empty UI scene node bool loadBaseResources{ true }; + //! Loads the bundled icon fonts and initializes IconManager when base resources are + //! enabled. + bool loadIconResources{ true }; //! The default base font for the UI. If not provided it will load NotoSans-Regular ( will //! look at "assets/fonts/NotoSans-Regular.ttf" ) Font* baseFont{ nullptr }; @@ -73,6 +89,39 @@ class EE_API UIApplication { //! Document UISceneNode* getUI() const; + /** Creates a secondary application-owned native window and its UISceneNode. The new scene + * inherits the primary scene's font, theme, icon theme, stylesheet, and rendering policies. + * The returned scene remains owned by UIApplication. + * @return The new UI scene, or nullptr if native window creation failed. */ + UISceneNode* createWindow( const WindowSettings& windowSettings, + const ContextSettings& contextSettings = ContextSettings() ); + + /** Returns the application-owned UI scene associated with @p window, or nullptr when the + * window is not owned by this application. */ + UISceneNode* getUI( EE::Window::Window* window ) const; + + /** Returns the number of application-owned windows pending or participating in the loop. */ + size_t getWindowCount() const; + + /** Requests destruction of an application-owned window. The native window is hidden + * immediately and its scene and backend resources are destroyed at the safe point at the end + * of the current frame. */ + void closeWindow( EE::Window::Window* window ); + + /** Requests termination of the application loop without changing the open state of its + * windows. Backend resources are released by UIApplication destruction. */ + void requestQuit(); + + /** Returns whether the application main loop is currently running. */ + bool isRunning() const; + + /** Sets the condition under which window closure stops the application loop. The default is + * QuitPolicy::OnPrimaryWindowClosed. */ + void setQuitPolicy( QuitPolicy policy ); + + /** Returns the current window-close policy. */ + QuitPolicy getQuitPolicy() const; + //! Runs the application until window is closed //! @return EXIT_SUCCESS if application run successfully int run(); @@ -91,11 +140,41 @@ class EE_API UIApplication { String::HashType getStyleSheetDefaultMarker() const { return mStyleSheetMarker; } protected: + /** Associates an application-owned native window with its UI scene and deferred-destruction + * state. */ + struct WindowEntry { + EE::Window::Window* window{ nullptr }; + UISceneNode* ui{ nullptr }; + bool primary{ false }; + bool pendingDestroy{ false }; + }; + + /** Shared primary/secondary window creation implementation. */ + WindowEntry* createWindowInternal( const WindowSettings& windowSettings, + const ContextSettings& contextSettings, bool primary ); + + /** Applies application-level resources and rendering policies to a newly created UI scene. */ + void configureUIScene( UISceneNode* ui ); + + /** Processes one application frame across all open windows. */ + void tick(); + + /** Marks windows closed externally for destruction and applies the active quit policy. */ + void processClosedWindows(); + + /** Destroys scenes and native windows previously marked for deferred destruction. */ + void processPendingWindowDestruction(); + UISceneNode* mUISceneNode{ nullptr }; EE::Window::Window* mWindow{ nullptr }; String::HashType mStyleSheetMarker{ 0 }; bool mDidRun{ false }; bool mShowMemoryManagerResult{ false }; + bool mRunning{ false }; + QuitPolicy mQuitPolicy{ QuitPolicy::OnPrimaryWindowClosed }; + std::vector mWindows; + Settings mSettings; + System::Clock mFrameClock; std::unique_ptr mSystemFontState; }; diff --git a/include/eepp/ui/uifiledialog.hpp b/include/eepp/ui/uifiledialog.hpp index e4ee9da7a..6dbc3d8bd 100644 --- a/include/eepp/ui/uifiledialog.hpp +++ b/include/eepp/ui/uifiledialog.hpp @@ -16,6 +16,7 @@ namespace EE { namespace UI { struct NativeFileDialogHandler; +class UIApplication; class EE_API UIFileDialog : public UIWindow { public: @@ -39,6 +40,19 @@ class EE_API UIFileDialog : public UIWindow { const std::string& defaultFilePattern = "*", const std::string& defaultDirectory = Sys::getProcessPath() ); + /** Creates an eepp file dialog that fills a new native UIApplication window. The dialog uses + * the eepp file browser while its host uses the platform window decoration. UseNativeFileDialog + * is ignored by this factory. Closing either dialog closes the application window. Native + * placement is left to the window manager by default. On Emscripten and in terminal runtime it + * creates a decorated in-application dialog instead of a native window. */ + static UIFileDialog* NewInApplicationWindow( + UIApplication& application, const WindowSettings& windowSettings, + Uint32 dialogFlags = UIFileDialog::DefaultFlags, + const std::string& defaultFilePattern = "*", + const std::string& defaultDirectory = Sys::getProcessPath(), + const ContextSettings& contextSettings = ContextSettings(), bool modal = true, + ApplicationWindowPosition position = ApplicationWindowPosition::WindowManager ); + virtual ~UIFileDialog(); virtual Uint32 getType() const; diff --git a/include/eepp/ui/uiiconthememanager.hpp b/include/eepp/ui/uiiconthememanager.hpp index b9028a8ec..266f18e0a 100644 --- a/include/eepp/ui/uiiconthememanager.hpp +++ b/include/eepp/ui/uiiconthememanager.hpp @@ -21,6 +21,10 @@ class EE_API UIIconThemeManager { UIIconTheme* getCurrentTheme() const; + /** Returns a retaining handle to the current icon theme, or an empty handle if no owned theme + * is current. */ + UIIconThemePtr getCurrentThemeHandle() const; + UIIconThemeManager* setCurrentTheme( UIIconThemePtr currentTheme ); UIIconTheme* getFallbackTheme() const; diff --git a/include/eepp/ui/uimessagebox.hpp b/include/eepp/ui/uimessagebox.hpp index 5ceb668e8..d290578cf 100644 --- a/include/eepp/ui/uimessagebox.hpp +++ b/include/eepp/ui/uimessagebox.hpp @@ -2,6 +2,7 @@ #define EE_UICUIMESSAGEBOX_HPP #include +#include namespace EE { namespace UI { @@ -11,6 +12,7 @@ class UILayout; class UIPushButton; class UIDropDownList; class UIComboBox; +class UIApplication; #define UI_MESSAGE_BOX_DEFAULT_FLAGS \ UI_WIN_CLOSE_BUTTON | UI_WIN_USE_DEFAULT_BUTTONS_ACTIONS | UI_WIN_MODAL | \ @@ -23,6 +25,16 @@ class EE_API UIMessageBox : public UIWindow { static UIMessageBox* New( const Type& type, const String& message, const Uint32& windowFlags = UI_MESSAGE_BOX_DEFAULT_FLAGS ); + /** Creates an eepp message box in a separate native UIApplication window. On Emscripten and in + * terminal runtime it creates a decorated in-application message box instead. */ + static UIMessageBox* NewInApplicationWindow( + UIApplication& application, const EE::Window::WindowSettings& windowSettings, + const Type& type, const String& message, + const Uint32& windowFlags = UI_MESSAGE_BOX_DEFAULT_FLAGS, + const EE::Window::ContextSettings& contextSettings = EE::Window::ContextSettings(), + bool modal = true, + ApplicationWindowPosition position = ApplicationWindowPosition::WindowManager ); + virtual ~UIMessageBox(); virtual void setTheme( UITheme* theme ); @@ -69,7 +81,6 @@ class EE_API UIMessageBox : public UIWindow { virtual Uint32 onKeyUp( const KeyEvent& event ); virtual Uint32 onMessage( const NodeMessage* Msg ); - }; }} // namespace EE::UI diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index d023eaf20..548806ae2 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -51,6 +52,29 @@ struct NavigationRequest { class EE_API UISceneNode : public SceneNode { public: + /** Scoped binding of a UI scene and its native graphics context. Restores both previous + * bindings when destroyed. Context objects are movable but cannot be copied. */ + class EE_API Context { + public: + explicit Context( UISceneNode* scene ); + + ~Context(); + + Context( const Context& ) = delete; + Context& operator=( const Context& ) = delete; + + Context( Context&& other ) noexcept; + Context& operator=( Context&& ) = delete; + + private: + UISceneNode* mPreviousScene{ nullptr }; + EE::Window::Engine::WindowContext mWindowContext; + bool mActive{ true }; + }; + + /** Makes this scene and its native window current for the returned scope. */ + Context makeCurrent(); + /** * @brief Creates a new UISceneNode instance. * diff --git a/include/eepp/ui/uiwindow.hpp b/include/eepp/ui/uiwindow.hpp index c8fbdc44a..d384f16cc 100644 --- a/include/eepp/ui/uiwindow.hpp +++ b/include/eepp/ui/uiwindow.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace EE { namespace Graphics { class FrameBuffer; @@ -11,6 +12,7 @@ class FrameBuffer; namespace EE { namespace UI { +class UIApplication; class UITextView; class UISceneNode; @@ -61,6 +63,14 @@ class EE_API UIWindow : public UIWidget { RELATIVE_LAYOUT }; + /** Selects how a native host created by NewInApplicationWindow() is initially positioned. */ + enum class ApplicationWindowPosition { + /** Leaves placement entirely to the desktop window manager. */ + WindowManager, + /** Centers the host over the primary application window and clamps it to usable bounds. */ + CenteredOnPrimary + }; + static UIWindow* NewOpt( WindowBaseContainerType type, const StyleConfig& windowStyleConfig = StyleConfig() ); @@ -72,6 +82,17 @@ class EE_API UIWindow : public UIWidget { static UIWindow* NewRelLay(); + /** Creates a UIWindow in a native application window. On Emscripten and in terminal runtime + * this transparently falls back to a regular in-application UIWindow. Native placement is left + * to the window manager unless CenteredOnPrimary is explicitly requested. */ + static UIWindow* NewInApplicationWindow( + UIApplication& application, const EE::Window::WindowSettings& windowSettings, + WindowBaseContainerType type = SIMPLE_LAYOUT, + const StyleConfig& windowStyleConfig = StyleConfig(), + const EE::Window::ContextSettings& contextSettings = EE::Window::ContextSettings(), + bool modal = false, + ApplicationWindowPosition position = ApplicationWindowPosition::WindowManager ); + virtual ~UIWindow(); virtual Uint32 getType() const; @@ -210,6 +231,13 @@ class EE_API UIWindow : public UIWidget { virtual void unserialize( const nlohmann::json& json ); protected: + /** Shared native-host factory used by UIWindow subclasses. On Emscripten and in terminal + * runtime it creates a regular in-scene window instead. */ + static UIWindow* createInApplicationWindow( UIApplication& application, + const EE::Window::WindowSettings& windowSettings, + const std::function& windowFactory, + const EE::Window::ContextSettings& contextSettings, + bool modal, ApplicationWindowPosition position ); enum UI_RESIZE_TYPE { RESIZE_NONE, RESIZE_LEFT, @@ -256,6 +284,7 @@ class EE_API UIWindow : public UIWidget { KeyBindings mKeyBindings; std::map mKeyBindingCommands; std::function mCheckEphemeralCloseFn; + std::function mApplicationWindowCloseCallback; explicit UIWindow( WindowBaseContainerType type, const StyleConfig& windowStyleConfig ); diff --git a/include/eepp/window/engine.hpp b/include/eepp/window/engine.hpp index d03d97852..c0fc2e2a8 100644 --- a/include/eepp/window/engine.hpp +++ b/include/eepp/window/engine.hpp @@ -28,6 +28,28 @@ class EE_API Engine { SINGLETON_DECLARE_HEADERS( Engine ) public: + /** Scoped binding of the current native window and graphics context. Restores the previous + * binding when destroyed. Context objects are movable but cannot be copied. */ + class EE_API WindowContext { + public: + ~WindowContext(); + + WindowContext( const WindowContext& ) = delete; + WindowContext& operator=( const WindowContext& ) = delete; + + WindowContext( WindowContext&& other ) noexcept; + WindowContext& operator=( WindowContext&& ) = delete; + + private: + friend class Engine; + + WindowContext( Engine* engine, EE::Window::Window* window ); + + Engine* mEngine{ nullptr }; + EE::Window::Window* mPreviousWindow{ nullptr }; + bool mActive{ true }; + }; + ~Engine(); static bool isEngineRunning(); @@ -54,6 +76,9 @@ class EE_API Engine { /** Set the window as the current. */ void setCurrentWindow( EE::Window::Window* window ); + /** Makes @p window and its graphics context current for the returned scope. */ + WindowContext makeWindowCurrent( EE::Window::Window* window ); + /** @return The number of windows created. */ Uint32 getWindowCount() const; @@ -67,6 +92,10 @@ class EE_API Engine { EE::Window::Window* getWindowID( const Uint32& winID ); + /** Begins an input frame for every window, pumps the backend event queue once, routes events by + * window ID, and then completes every input frame. */ + void updateInput(); + /** Constructs WindowSettings from an ini file It will search for the following properties: Width Window width @@ -181,7 +210,7 @@ class EE_API Engine { EE::Window::Window* createSDL2Window( const WindowSettings& Settings, const ContextSettings& Context ); -#ifdef EE_BACKEND_SDL3 +#if defined( EE_BACKEND_SDL3 ) || defined( EE_SDL_VERSION_3 ) Backend::WindowBackendLibrary* createSDL3Backend( const WindowSettings& Settings ); EE::Window::Window* createSDL3Window( const WindowSettings& Settings, diff --git a/include/eepp/window/input.hpp b/include/eepp/window/input.hpp index 96aa722d0..a2799745d 100644 --- a/include/eepp/window/input.hpp +++ b/include/eepp/window/input.hpp @@ -27,6 +27,12 @@ class EE_API Input { /** Update the Input */ virtual void update() = 0; + /** Clears transient input state, advances the event-frame ID, and drains injected events. */ + void beginInputFrame(); + + /** Emits the end-of-event-processing notification for this input frame. */ + void endInputFrame(); + /** If timeout is zero waits indefinitely for the next available event otherwise waits until the * specified timeout for the next available event. */ @@ -251,6 +257,10 @@ class EE_API Input { /** Process an input event. Called by the input update. */ void processEvent( InputEvent* Event ); + /** Routes an input event to the Input instance identified by InputEvent::WinID, then processes + * it. Events without a window ID are processed by this instance. */ + void processEventForWindow( InputEvent* Event ); + /** Queues an event from any producer thread for processing during the next normal update cycle. * If the bounded queue is full, an older mouse-motion event can be discarded to make room. * @return True if the event was queued. diff --git a/include/eepp/window/window.hpp b/include/eepp/window/window.hpp index 5b1c286fd..5816b386d 100644 --- a/include/eepp/window/window.hpp +++ b/include/eepp/window/window.hpp @@ -274,6 +274,14 @@ class EE_API Window { */ virtual void setSize( Uint32 Width, Uint32 Height, bool isWindowed ) = 0; + /** Sets the native minimum client size in screen coordinates. Backends without native support + * may leave the default no-op implementation. */ + virtual void setMinimumSize( Uint32 Width, Uint32 Height ); + + /** Makes this window modal for @p parent. Passing nullptr clears modality where supported. + * @return True when the backend applied the requested relationship. */ + virtual bool setModalFor( Window* parent ); + /** @return The window size in pixels */ virtual Sizei getSize() const; @@ -311,6 +319,12 @@ class EE_API Window { */ virtual void display( bool clear = false ); + /** Presents the rendered frame. + * @param clear Whether to clear the back buffer after presentation. + * @param limitFrameRate Whether to apply this window's frame-rate limiter. Multi-window loops + * disable this per window and throttle once after all windows have been presented. */ + void display( bool clear, bool limitFrameRate ); + /** @return The elapsed time for the last frame rendered */ virtual const System::Time& getElapsed() const; @@ -351,6 +365,13 @@ class EE_API Window { /** Close the window if is running */ virtual void close(); + /** Defers backend resource destruction until the Engine destroys this Window. Intended for + * owners that must release scene resources at a safe point after close(). */ + void setDeferNativeResourceDestructionOnClose( bool defer ); + + /** Returns whether close() leaves native resources alive until Engine destruction. */ + bool getDeferNativeResourceDestructionOnClose() const; + /** Set the current active view * @param view New view to use (pass GetDefaultView() to set the default view) * @param forceRefresh Forces the view refresh even if is the same as the last one. @@ -568,6 +589,7 @@ class EE_API Window { friend class Input; mutable WindowInfo mWindow; + bool mDeferNativeResourceDestructionOnClose{ false }; Clipboard* mClipboard; Input* mInput; CursorManager* mCursorManager; diff --git a/premake4.lua b/premake4.lua index 9c9ef01e6..c77417369 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1737,6 +1737,12 @@ solution "eepp" files { "src/examples/ui_application_hello_world/*.cpp" } build_link_configuration( "eepp-ui-application-hello-world", true ) + project "eepp-ui-application-multi-window" + set_kind() + language "C++" + files { "src/examples/ui_application_multi_window/*.cpp" } + build_link_configuration( "eepp-ui-application-multi-window", true ) + project "eepp-ui-font-picker" set_kind() language "C++" diff --git a/premake5.lua b/premake5.lua index dbe18203b..7c547fa3f 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1771,6 +1771,12 @@ workspace "eepp" files { "src/examples/ui_application_hello_world/*.cpp" } build_link_configuration( "eepp-ui-application-hello-world", true ) + project "eepp-ui-application-multi-window" + set_kind() + language "C++" + files { "src/examples/ui_application_multi_window/*.cpp" } + build_link_configuration( "eepp-ui-application-multi-window", true ) + project "eepp-ui-font-picker" set_kind() language "C++" diff --git a/src/eepp/graphics/renderer/renderer.cpp b/src/eepp/graphics/renderer/renderer.cpp index 7bea0aaac..73d69900b 100644 --- a/src/eepp/graphics/renderer/renderer.cpp +++ b/src/eepp/graphics/renderer/renderer.cpp @@ -1,10 +1,12 @@ #include +#include #include #include #include #include #include #include +#include #include #ifdef EE_GLES1_LATE_INCLUDE @@ -483,6 +485,21 @@ void Renderer::enable( unsigned int cap ) { glEnable( cap ); } +void Renderer::onContextChanged() { + if ( TextureFactory::existsSingleton() ) + TextureFactory::instance()->invalidateTextureBindings(); + + BlendMode::setMode( BlendMode::getPreBlendFunc(), true ); + lineSmooth(); + polygonSmooth(); + polygonMode(); + multisample( isMultisample() ); + colorMask( mColorMask[0], mColorMask[1], mColorMask[2], mColorMask[3] ); + const float lineWidth = mLineWidth; + mLineWidth = -1.f; + this->lineWidth( lineWidth ); +} + const char* Renderer::getString( unsigned int name ) { return (const char*)glGetString( name ); } diff --git a/src/eepp/graphics/renderer/renderergl3.cpp b/src/eepp/graphics/renderer/renderergl3.cpp index 06913833a..d673d99cf 100644 --- a/src/eepp/graphics/renderer/renderergl3.cpp +++ b/src/eepp/graphics/renderer/renderergl3.cpp @@ -115,6 +115,11 @@ void RendererGL3::reloadCurrentShader() { reloadShader( mCurShader ); } +void RendererGL3::onContextChanged() { + Renderer::onContextChanged(); + reloadCurrentShader(); +} + ShaderProgramPtr RendererGL3::createSubpixelDualSourceShader() { std::string vertexShader = mBaseVertexShader; String::replaceAll( vertexShader, "#version 120", "#version 130" ); diff --git a/src/eepp/graphics/renderer/renderergl3cp.cpp b/src/eepp/graphics/renderer/renderergl3cp.cpp index 384892991..721d31f6b 100644 --- a/src/eepp/graphics/renderer/renderergl3cp.cpp +++ b/src/eepp/graphics/renderer/renderergl3cp.cpp @@ -147,6 +147,11 @@ void RendererGL3CP::reloadCurrentShader() { reloadShader( mCurShader ); } +void RendererGL3CP::onContextChanged() { + Renderer::onContextChanged(); + reloadCurrentShader(); +} + ShaderProgramPtr RendererGL3CP::createSubpixelDualSourceShader() { static const char fragmentShader[] = R"(#version 330 uniform sampler2D textureUnit0; diff --git a/src/eepp/graphics/renderer/renderergles2.cpp b/src/eepp/graphics/renderer/renderergles2.cpp index e485ed359..7cbdd7b25 100644 --- a/src/eepp/graphics/renderer/renderergles2.cpp +++ b/src/eepp/graphics/renderer/renderergles2.cpp @@ -148,6 +148,11 @@ void RendererGLES2::reloadCurrentShader() { reloadShader( mCurShader ); } +void RendererGLES2::onContextChanged() { + Renderer::onContextChanged(); + reloadCurrentShader(); +} + ShaderProgramPtr RendererGLES2::createSubpixelDualSourceShader() { #ifdef EE_GLES2 static const char fragmentShader[] = R"(#extension GL_EXT_blend_func_extended : require diff --git a/src/eepp/graphics/texturefactory.cpp b/src/eepp/graphics/texturefactory.cpp index f705c4a55..e70e6b7dc 100644 --- a/src/eepp/graphics/texturefactory.cpp +++ b/src/eepp/graphics/texturefactory.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -221,6 +222,11 @@ void TextureFactory::setCurrentTexture( const int& textureHandle, const Uint32& mCurrentTexture[TextureUnit] = textureHandle; } +void TextureFactory::invalidateTextureBindings() { + std::fill( mCurrentTexture.begin(), mCurrentTexture.end(), -1 ); + mLastCoordinateType = static_cast( -1 ); +} + TextureRegistrySnapshot TextureFactory::snapshotTextures() { struct LockedTextureRecord { LiveTextureRecord record; diff --git a/src/eepp/scene/eventdispatcher.cpp b/src/eepp/scene/eventdispatcher.cpp index f5cc6d032..8d02cc523 100644 --- a/src/eepp/scene/eventdispatcher.cpp +++ b/src/eepp/scene/eventdispatcher.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -24,10 +25,22 @@ EventDispatcher::EventDispatcher( SceneNode* sceneNode ) : mFirstPress( false ), mNodeWasDragging( NULL ), mNodeDragging( NULL ) { - mCbId = mInput->pushCallback( [this]( InputEvent* event ) { inputCallback( event ); } ); + mCbId = mInput->pushCallback( [this]( InputEvent* event ) { + if ( mSceneNode->isUISceneNode() ) { + auto context = mSceneNode->asType()->makeCurrent(); + inputCallback( event ); + } else { + inputCallback( event ); + } + } ); mIMECbId = mWindow->getIME().addTextEditingCb( [this]( const String& text, Int32 start, Int32 length ) { - sendTextEditing( text, start, length ); + if ( mSceneNode->isUISceneNode() ) { + auto context = mSceneNode->asType()->makeCurrent(); + sendTextEditing( text, start, length ); + } else { + sendTextEditing( text, start, length ); + } } ); } diff --git a/src/eepp/scene/node.cpp b/src/eepp/scene/node.cpp index c64398f4d..b4d63ba14 100644 --- a/src/eepp/scene/node.cpp +++ b/src/eepp/scene/node.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include namespace EE { namespace Scene { @@ -227,6 +229,16 @@ void Node::unsubscribeScheduledUpdate() { Node* Node::setParent( Node* parent ) { eeASSERT( NULL != parent ); +#ifdef EE_DEBUG + if ( isUINode() && parent->isUINode() ) { + auto* childScene = asType()->getUISceneNode(); + auto* parentScene = parent->asType()->getUISceneNode(); + // Same-window scene rebinding is an existing supported mechanism for embedded documents. + // Cross-window rebinding cannot safely migrate input and native-window dependencies. + eeASSERT( !childScene || !parentScene || childScene == parentScene || + childScene->getWindow() == parentScene->getWindow() ); + } +#endif if ( parent == mParentNode ) return this; diff --git a/src/eepp/scene/scenemanager.cpp b/src/eepp/scene/scenemanager.cpp index d1062213e..c069ff4f8 100644 --- a/src/eepp/scene/scenemanager.cpp +++ b/src/eepp/scene/scenemanager.cpp @@ -13,7 +13,7 @@ bool SceneManager::isActive() { !SceneManager::isShuttingDown(); } -SceneManager::SceneManager() : mUISceneNode( NULL ) {} +SceneManager::SceneManager() : mUISceneNode( NULL ), mScopedUISceneNode( NULL ) {} SceneManager::~SceneManager() { for ( auto& it : mSceneNodes ) { @@ -32,6 +32,10 @@ SceneNode* SceneManager::add( SceneNode* sceneNode ) { bool SceneManager::remove( SceneNode* sceneNode ) { auto it = std::find( mSceneNodes.begin(), mSceneNodes.end(), sceneNode ); if ( it != mSceneNodes.end() ) { + if ( mUISceneNode == sceneNode ) + mUISceneNode = nullptr; + if ( mScopedUISceneNode == sceneNode ) + mScopedUISceneNode = nullptr; mSceneNodes.erase( it ); return true; } @@ -44,13 +48,39 @@ size_t SceneManager::count() const { void SceneManager::draw() { for ( auto& sceneNode : mSceneNodes ) { - sceneNode->draw(); + if ( sceneNode->isUISceneNode() ) { + auto context = sceneNode->asType()->makeCurrent(); + sceneNode->draw(); + } else { + auto context = Engine::instance()->makeWindowCurrent( sceneNode->getWindow() ); + sceneNode->draw(); + } + } +} + +void SceneManager::draw( EE::Window::Window* window ) { + for ( auto& sceneNode : mSceneNodes ) { + if ( sceneNode->getWindow() != window ) + continue; + if ( sceneNode->isUISceneNode() ) { + auto context = sceneNode->asType()->makeCurrent(); + sceneNode->draw(); + } else { + auto context = Engine::instance()->makeWindowCurrent( window ); + sceneNode->draw(); + } } } void SceneManager::update( const Time& elapsed ) { for ( auto& sceneNode : mSceneNodes ) { - sceneNode->update( elapsed ); + if ( sceneNode->isUISceneNode() ) { + auto context = sceneNode->asType()->makeCurrent(); + sceneNode->update( elapsed ); + } else { + auto context = Engine::instance()->makeWindowCurrent( sceneNode->getWindow() ); + sceneNode->update( elapsed ); + } } } @@ -59,6 +89,16 @@ void SceneManager::update() { } UISceneNode* SceneManager::getUISceneNode() { + if ( mScopedUISceneNode ) + return mScopedUISceneNode; + + if ( Engine::existsSingleton() ) { + if ( mUISceneNode && mUISceneNode->getWindow() == Engine::instance()->getCurrentWindow() ) + return mUISceneNode; + if ( auto* scene = getUISceneNode( Engine::instance()->getCurrentWindow() ) ) + return scene; + } + if ( NULL == mUISceneNode ) { for ( auto& sceneNode : mSceneNodes ) { if ( sceneNode->isUISceneNode() ) { @@ -71,10 +111,40 @@ UISceneNode* SceneManager::getUISceneNode() { return mUISceneNode; } +UISceneNode* SceneManager::getUISceneNode( EE::Window::Window* window ) { + for ( auto& sceneNode : mSceneNodes ) { + if ( sceneNode->isUISceneNode() && sceneNode->getWindow() == window ) + return sceneNode->asType(); + } + return nullptr; +} + void SceneManager::setCurrentUISceneNode( UISceneNode* uiSceneNode ) { mUISceneNode = uiSceneNode; } +UISceneNode* SceneManager::setScopedUISceneNode( UISceneNode* uiSceneNode ) { + UISceneNode* previous = mScopedUISceneNode; + mScopedUISceneNode = uiSceneNode; + return previous; +} + +void SceneManager::destroyScenes( EE::Window::Window* window ) { + for ( auto it = mSceneNodes.begin(); it != mSceneNodes.end(); ) { + SceneNode* sceneNode = *it; + if ( sceneNode->getWindow() != window ) { + ++it; + continue; + } + if ( mUISceneNode == sceneNode ) + mUISceneNode = nullptr; + if ( mScopedUISceneNode == sceneNode ) + mScopedUISceneNode = nullptr; + it = mSceneNodes.erase( it ); + eeSAFE_DELETE( sceneNode ); + } +} + Time SceneManager::getElapsed() const { return mClock.getElapsedTime(); } diff --git a/src/eepp/ui/uiapplication.cpp b/src/eepp/ui/uiapplication.cpp index 6712406a8..d90ab8605 100644 --- a/src/eepp/ui/uiapplication.cpp +++ b/src/eepp/ui/uiapplication.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -17,6 +19,10 @@ #include #include +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN +#include +#endif + using namespace EE::Graphics; using namespace EE::System; using namespace EE::Scene; @@ -40,7 +46,8 @@ class UIApplicationSystemFontState { static std::atomic sSystemFontsEnabledByDefault{ true }; UIApplication::UIApplication( const WindowSettings& windowSettings, const Settings& appSettings, - const ContextSettings& contextSettings ) { + const ContextSettings& contextSettings ) : + mSettings( appSettings ) { const bool enableSystemFonts = appSettings.enableSystemFonts.value_or( sSystemFontsEnabledByDefault.load( std::memory_order_acquire ) ); if ( enableSystemFonts ) { @@ -67,6 +74,7 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin std::cerr << "Could not create window, exiting" << std::endl; return; } + mWindow->setDeferNativeResourceDestructionOnClose( true ); mDidRun = true; @@ -93,11 +101,12 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin defaultFontService.setHinting( appSettings.fontHinting ); defaultFontService.setAntialiasing( appSettings.fontAntialiasing ); - mUISceneNode = UISceneNode::New(); + mUISceneNode = UISceneNode::New( mWindow ); FontService& uiFontService = mUISceneNode->getResourceScope()->getFontService(); uiFontService.setHinting( appSettings.fontHinting ); uiFontService.setAntialiasing( appSettings.fontAntialiasing ); SceneManager::instance()->add( mUISceneNode ); + mWindows.push_back( { mWindow, mUISceneNode, true, false } ); if ( !appSettings.loadBaseResources ) return; @@ -144,6 +153,24 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin mUISceneNode->setStyleSheet( theme->getStyleSheet() ); mUISceneNode->getStyleSheet().setMarker( mStyleSheetMarker ); mUISceneNode->getUIThemeManager()->setDefaultTheme( std::move( theme ) ); + + if ( appSettings.loadIconResources ) { + auto loadIconFont = []( const std::string& name, + const std::string& path ) -> FontTrueType* { + if ( auto font = defaultResourceScope().findFont( name ) ) + return font->getType() == FontType::TTF ? static_cast( font.get() ) + : nullptr; + if ( !FileSystem::fileExists( path ) ) + return nullptr; + return FontTrueType::New( name, path ).get(); + }; + auto* remixIconFont = loadIconFont( "icon", "assets/fonts/remixicon.ttf" ); + auto* noniconsFont = loadIconFont( "nonicons", "assets/fonts/nonicons.ttf" ); + auto* codIconFont = loadIconFont( "codicon", "assets/fonts/codicon.ttf" ); + if ( remixIconFont || noniconsFont || codIconFont ) + mUISceneNode->getUIIconThemeManager()->setCurrentTheme( + IconManager::init( "uiapplication", remixIconFont, noniconsFont, codIconFont ) ); + } } UIApplication::~UIApplication() { @@ -161,31 +188,216 @@ UISceneNode* UIApplication::getUI() const { return mUISceneNode; } +UIApplication::WindowEntry* +UIApplication::createWindowInternal( const WindowSettings& windowSettings, + const ContextSettings& contextSettings, bool primary ) { + auto context = Engine::instance()->makeWindowCurrent( Engine::instance()->getCurrentWindow() ); + auto* window = Engine::instance()->createWindow( windowSettings, contextSettings ); + if ( !window || !window->isOpen() ) + return nullptr; + window->setDeferNativeResourceDestructionOnClose( true ); + + auto* ui = UISceneNode::New( window ); + SceneManager::instance()->add( ui ); + mWindows.push_back( { window, ui, primary, false } ); + configureUIScene( ui ); + return &mWindows.back(); +} + +void UIApplication::configureUIScene( UISceneNode* ui ) { + FontService& uiFontService = ui->getResourceScope()->getFontService(); + uiFontService.setHinting( mSettings.fontHinting ); + uiFontService.setAntialiasing( mSettings.fontAntialiasing ); + if ( !mUISceneNode || ui == mUISceneNode || !mSettings.loadBaseResources ) + return; + + auto* sourceThemeManager = mUISceneNode->getUIThemeManager(); + ui->getUIThemeManager()->setDefaultFont( sourceThemeManager->getDefaultFont() ); + ui->getUIThemeManager()->setDefaultEffectsEnabled( + sourceThemeManager->getDefaultEffectsEnabled() ); + ui->getUIThemeManager()->setDefaultTheme( sourceThemeManager->getDefaultThemeHandle() ); + ui->getUIIconThemeManager()->setCurrentTheme( + mUISceneNode->getUIIconThemeManager()->getCurrentThemeHandle() ); + ui->setStyleSheet( mUISceneNode->getStyleSheet() ); + ui->getStyleSheet().setMarker( mStyleSheetMarker ); + ui->getRoot()->addClass( "appbackground" ); +} + +UISceneNode* UIApplication::createWindow( const WindowSettings& windowSettings, + const ContextSettings& contextSettings ) { + auto* entry = createWindowInternal( windowSettings, contextSettings, false ); + return entry ? entry->ui : nullptr; +} + +UISceneNode* UIApplication::getUI( EE::Window::Window* window ) const { + for ( const auto& entry : mWindows ) { + if ( entry.window == window ) + return entry.ui; + } + return nullptr; +} + +size_t UIApplication::getWindowCount() const { + return mWindows.size(); +} + +void UIApplication::closeWindow( EE::Window::Window* window ) { + if ( nullptr == window ) + return; + + bool closeAllWindows = false; + for ( auto& entry : mWindows ) { + if ( entry.window != window ) + continue; + entry.window->hide(); + entry.window->close(); + entry.pendingDestroy = true; + closeAllWindows = entry.primary && mQuitPolicy == QuitPolicy::OnPrimaryWindowClosed; + break; + } + + if ( closeAllWindows ) { + for ( auto& entry : mWindows ) { + if ( entry.window ) { + entry.window->hide(); + entry.window->close(); + } + entry.pendingDestroy = true; + } + } +} + +void UIApplication::requestQuit() { + mRunning = false; +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + emscripten_cancel_main_loop(); +#endif +} + +bool UIApplication::isRunning() const { + return mRunning; +} + +void UIApplication::setQuitPolicy( QuitPolicy policy ) { + mQuitPolicy = policy; +} + +UIApplication::QuitPolicy UIApplication::getQuitPolicy() const { + return mQuitPolicy; +} + int UIApplication::run() { + if ( !mDidRun ) + return EXIT_FAILURE; // Offscreen SDL windows do not receive an initial expose event. Ensure the first logical // framebuffer is rendered even when the scene was fully laid out before entering the loop. - if ( Runtime::isOffscreen() ) - mUISceneNode->invalidate( nullptr ); + if ( Runtime::isOffscreen() ) { + for ( auto& entry : mWindows ) + entry.ui->invalidate( nullptr ); + } - mWindow->runMainLoop( [this]() { - mWindow->getInput()->update(); - SceneManager::instance()->update(); - if ( mUISceneNode->invalidated() ) { - mWindow->clear(); - - SceneManager::instance()->draw(); - - mWindow->display(); - } else { -#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN - mWindow->getInput()->waitEvent( Milliseconds( mWindow->hasFocus() ? 16 : 100 ) ); + mRunning = true; +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + emscripten_set_main_loop_arg( + []( void* application ) { static_cast( application )->tick(); }, this, + mWindow->getFrameRateLimit(), 1 ); +#else + while ( mRunning ) + tick(); #endif - } - } ); return mDidRun ? EXIT_SUCCESS : EXIT_FAILURE; } +void UIApplication::processClosedWindows() { + bool primaryClosed = false; + for ( auto& entry : mWindows ) { + if ( entry.window && !entry.window->isOpen() ) { + entry.window->hide(); + entry.pendingDestroy = true; + primaryClosed |= entry.primary; + } + } + + if ( primaryClosed && mQuitPolicy == QuitPolicy::OnPrimaryWindowClosed ) { + for ( auto& entry : mWindows ) { + if ( entry.window && entry.window->isOpen() ) { + entry.window->hide(); + entry.window->close(); + } + entry.pendingDestroy = true; + } + } +} + +void UIApplication::processPendingWindowDestruction() { + for ( auto it = mWindows.begin(); it != mWindows.end(); ) { + if ( !it->pendingDestroy ) { + ++it; + continue; + } + auto* window = it->window; + const bool wasPrimary = it->primary; + const bool retainFinalContext = mWindows.size() == 1; + Engine::instance()->setCurrentWindow( window ); + SceneManager::instance()->destroyScenes( window ); + it = mWindows.erase( it ); + // The engine needs one GL context alive while its shared GPU resources are released during + // shutdown. A closed final window no longer participates in the application loop, but its + // native resources remain engine-owned until Engine destruction. + if ( !retainFinalContext ) + Engine::instance()->destroyWindow( window ); + if ( wasPrimary ) { + mWindow = nullptr; + mUISceneNode = nullptr; + if ( mQuitPolicy == QuitPolicy::OnPrimaryWindowClosed ) + mRunning = false; + } + } + if ( mWindows.empty() && mQuitPolicy == QuitPolicy::OnLastWindowClosed ) + mRunning = false; +} + +void UIApplication::tick() { + Engine::instance()->updateInput(); + processClosedWindows(); + SceneManager::instance()->update(); + + bool presented = false; + for ( auto& entry : mWindows ) { + if ( entry.pendingDestroy || !entry.window->isOpen() || !entry.ui->invalidated() ) + continue; + auto context = entry.ui->makeCurrent(); + entry.window->clear(); + SceneManager::instance()->draw( entry.window ); + entry.window->display( false, false ); + presented = true; + } + + processPendingWindowDestruction(); +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + if ( !mRunning ) + emscripten_cancel_main_loop(); +#endif +#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN + if ( mRunning && !mWindows.empty() ) { + auto* window = mWindows.front().window; + Engine::instance()->setCurrentWindow( window ); + Int64 waitMilliseconds = window->hasFocus() ? 16 : 100; + if ( presented && window->getFrameRateLimit() > 0 ) { + waitMilliseconds = eemax( 0, 1000 / window->getFrameRateLimit() - + mFrameClock.getElapsedTime().asMilliseconds() ); + } + if ( waitMilliseconds > 0 ) + window->getInput()->waitEvent( Milliseconds( waitMilliseconds ) ); + mFrameClock.restart(); + } else if ( mRunning ) { + Sys::sleep( Milliseconds( 16 ) ); + mFrameClock.restart(); + } +#endif +} + UIApplication::Settings::Settings( std::optional basePath, std::optional pixelDensity, bool loadBaseResources, Font* baseFont, std::optional baseStyleSheetPath, diff --git a/src/eepp/ui/uifiledialog.cpp b/src/eepp/ui/uifiledialog.cpp index 6e6b0701b..c429fba58 100644 --- a/src/eepp/ui/uifiledialog.cpp +++ b/src/eepp/ui/uifiledialog.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include @@ -46,6 +48,33 @@ UIFileDialog* UIFileDialog::New( Uint32 dialogFlags, const std::string& defaultF return eeNew( UIFileDialog, ( dialogFlags, defaultFilePattern, defaultDirectory ) ); } +UIFileDialog* UIFileDialog::NewInApplicationWindow( + UIApplication& application, const WindowSettings& windowSettings, Uint32 dialogFlags, + const std::string& defaultFilePattern, const std::string& defaultDirectory, + const ContextSettings& contextSettings, bool modal, ApplicationWindowPosition position ) { + dialogFlags &= ~UIFileDialog::UseNativeFileDialog; + WindowSettings dialogWindowSettings( windowSettings ); +#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN + if ( Runtime::mode() != RuntimeMode::Terminal ) { + // WindowSettings dimensions are screen coordinates, while the file dialog minimum is in dp. + // Create the native host at its final minimum size so the window manager never positions a + // smaller window that is immediately enlarged after the dialog has been laid out. + dialogWindowSettings.Width = + eemax( dialogWindowSettings.Width, + static_cast( PixelDensity::dpToPxI( FDLG_MIN_WIDTH ) ) ); + dialogWindowSettings.Height = + eemax( dialogWindowSettings.Height, + static_cast( PixelDensity::dpToPxI( FDLG_MIN_HEIGHT ) ) ); + } +#endif + return static_cast( createInApplicationWindow( + application, dialogWindowSettings, + [dialogFlags, defaultFilePattern, defaultDirectory] { + return New( dialogFlags, defaultFilePattern, defaultDirectory ); + }, + contextSettings, modal, position ) ); +} + UIFileDialog::UIFileDialog( Uint32 dialogFlags, const std::string& defaultFilePattern, const std::string& defaultDirectory ) : UIWindow(), diff --git a/src/eepp/ui/uiiconthememanager.cpp b/src/eepp/ui/uiiconthememanager.cpp index cea69dbe4..36d9ea2eb 100644 --- a/src/eepp/ui/uiiconthememanager.cpp +++ b/src/eepp/ui/uiiconthememanager.cpp @@ -35,6 +35,13 @@ UIIconTheme* UIIconThemeManager::getCurrentTheme() const { return mCurrentTheme; } +UIIconThemePtr UIIconThemeManager::getCurrentThemeHandle() const { + auto it = + std::find_if( mIconThemes.begin(), mIconThemes.end(), + [&]( const UIIconThemePtr& theme ) { return theme.get() == mCurrentTheme; } ); + return it != mIconThemes.end() ? *it : UIIconThemePtr{}; +} + UIIconThemeManager* UIIconThemeManager::setCurrentTheme( UIIconThemePtr currentTheme ) { if ( currentTheme.get() != mCurrentTheme && currentTheme.get() != mFallbackTheme ) { mCurrentTheme = currentTheme.get(); diff --git a/src/eepp/ui/uimessagebox.cpp b/src/eepp/ui/uimessagebox.cpp index 9d0c52548..733d63e82 100644 --- a/src/eepp/ui/uimessagebox.cpp +++ b/src/eepp/ui/uimessagebox.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -18,6 +19,17 @@ UIMessageBox* UIMessageBox::New( const Type& type, const String& message, return eeNew( UIMessageBox, ( type, message, windowFlags ) ); } +UIMessageBox* UIMessageBox::NewInApplicationWindow( + UIApplication& application, const EE::Window::WindowSettings& windowSettings, const Type& type, + const String& message, const Uint32& windowFlags, + const EE::Window::ContextSettings& contextSettings, bool modal, + ApplicationWindowPosition position ) { + return static_cast( createInApplicationWindow( + application, windowSettings, + [type, message, windowFlags] { return New( type, message, windowFlags ); }, contextSettings, + modal, position ) ); +} + UIMessageBox::UIMessageBox( const Type& type, const String& message, const Uint32& windowFlags ) : UIWindow(), mMsgBoxType( type ), mCloseShortcut( KEY_UNKNOWN ) { mVisible = false; diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index 894d5d0dd..da0a8e548 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -44,7 +44,7 @@ UINode::UINode() : mUISceneNode( SceneManager::instance()->getUISceneNode() ) { mNodeFlags |= NODE_FLAG_UINODE | NODE_FLAG_OVER_FIND_ALLOWED; - if ( NULL != mUISceneNode ) + if ( NULL != mUISceneNode && NULL != mUISceneNode->getRoot() ) setParent( (Node*)mUISceneNode->getRoot() ); } diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index ffe6eeb45..1a00927a9 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -133,6 +133,28 @@ UISceneNode* UISceneNode::New( EE::Window::Window* window, bool importDefaultRes return eeNew( UISceneNode, ( window, importDefaultResources ) ); } +UISceneNode::Context::Context( UISceneNode* scene ) : + mPreviousScene( SceneManager::instance()->setScopedUISceneNode( scene ) ), + mWindowContext( + Engine::instance()->makeWindowCurrent( scene ? scene->getWindow() : nullptr ) ) {} + +UISceneNode::Context::~Context() { + if ( !mActive ) + return; + SceneManager::instance()->setScopedUISceneNode( mPreviousScene ); +} + +UISceneNode::Context::Context( Context&& other ) noexcept : + mPreviousScene( other.mPreviousScene ), + mWindowContext( std::move( other.mWindowContext ) ), + mActive( other.mActive ) { + other.mActive = false; +} + +UISceneNode::Context UISceneNode::makeCurrent() { + return Context( this ); +} + UISceneNode::UISceneNode( EE::Window::Window* window, bool importDefaultResources ) : SceneNode( window ), mRoot( NULL ), @@ -146,6 +168,7 @@ UISceneNode::UISceneNode( EE::Window::Window* window, bool importDefaultResource mDrawableResolver( *this ), mWebResourceCache( WebResourceCache::New() ), mKeyBindings( mWindow->getInput() ) { + auto context = makeCurrent(); if ( mImportDefaultResources ) mResourceScope->importCatalog( defaultResourceScope().getLocalCatalog() ); @@ -547,7 +570,7 @@ bool UISceneNode::windowExists( UIWindow* win ) { } SmallVector UISceneNode::loadNode( pugi::xml_node node, Node* parent, - const Uint32& marker ) { + const Uint32& marker ) { Uint32 oldMarker = mCurrentMarker; mCurrentMarker = marker; @@ -641,8 +664,7 @@ SmallVector UISceneNode::loadNode( pugi::xml_node node, Node* pare UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const Uint32& marker ) { Clock clock; - UISceneNode* prevUISceneNode = SceneManager::instance()->getUISceneNode(); - SceneManager::instance()->setCurrentUISceneNode( this ); + auto context = makeCurrent(); std::string id( node.attribute( "id" ).as_string() ); mIsLoading = true; Clock innerClock; @@ -684,8 +706,6 @@ UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const mIsLoading = false; - SceneManager::instance()->setCurrentUISceneNode( prevUISceneNode ); - if ( mVerbose ) { Log::debug( "UISceneNode::loadLayoutNodes loaded in: %.2f ms", clock.getElapsedTime().asMilliseconds() ); @@ -1174,7 +1194,7 @@ void UISceneNode::flushDirtyStyleAndLayout() { } void UISceneNode::update( const Time& elapsed ) { - UISceneNode* uiSceneNode = SceneManager::instance()->getUISceneNode(); + auto context = makeCurrent(); drainAsyncResourceMainThreadQueue(); @@ -1182,8 +1202,6 @@ void UISceneNode::update( const Time& elapsed ) { mClock.restart(); } - SceneManager::instance()->setCurrentUISceneNode( this ); - updateDirtyStyles(); updateDirtyStyleStates(); updateDirtyLayouts(); @@ -1221,8 +1239,6 @@ void UISceneNode::update( const Time& elapsed ) { invalidationDepth--; } - SceneManager::instance()->setCurrentUISceneNode( uiSceneNode ); - if ( mFirstUpdate && mVerbose ) { mFirstUpdate = false; Log::debug( "UISceneNode::update first update took: %.2f ms", diff --git a/src/eepp/ui/uiwindow.cpp b/src/eepp/ui/uiwindow.cpp index a938ef201..f55517ea2 100644 --- a/src/eepp/ui/uiwindow.cpp +++ b/src/eepp/ui/uiwindow.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -17,6 +19,9 @@ #include #include #include +#include +#include +#include #include @@ -46,6 +51,134 @@ UIWindow* UIWindow::NewRelLay() { return eeNew( UIWindow, ( RELATIVE_LAYOUT ) ); } +UIWindow* UIWindow::NewInApplicationWindow( UIApplication& application, + const EE::Window::WindowSettings& windowSettings, + WindowBaseContainerType type, + const StyleConfig& windowStyleConfig, + const EE::Window::ContextSettings& contextSettings, + bool modal, ApplicationWindowPosition position ) { + return createInApplicationWindow( + application, windowSettings, + [type, windowStyleConfig] { return NewOpt( type, windowStyleConfig ); }, contextSettings, + modal, position ); +} + +UIWindow* UIWindow::createInApplicationWindow( UIApplication& application, + const EE::Window::WindowSettings& windowSettings, + const std::function& windowFactory, + const EE::Window::ContextSettings& contextSettings, + bool modal, ApplicationWindowPosition position ) { + bool supportsMultipleNativeWindows = Runtime::mode() != RuntimeMode::Terminal; +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + supportsMultipleNativeWindows = false; +#endif + if ( !supportsMultipleNativeWindows ) { + (void)contextSettings; + (void)position; + auto* ui = application.getUI(); + if ( nullptr == ui ) + return nullptr; + + auto context = ui->makeCurrent(); + auto* uiWindow = windowFactory(); + if ( nullptr == uiWindow ) + return nullptr; + + Uint32 windowFlags = uiWindow->getWinFlags(); + uiWindow->setWindowFlags( modal ? windowFlags | UI_WIN_MODAL + : windowFlags & ~UI_WIN_MODAL ); + if ( !windowSettings.Title.empty() ) + uiWindow->setTitle( windowSettings.Title ); + + const Sizef requestedSize( windowSettings.Width, windowSettings.Height ); + const Sizef minimumSize( uiWindow->getMinWindowSizeWithDecoration() ); + uiWindow->setSizeWithDecoration( + eemax( requestedSize.getWidth(), minimumSize.getWidth() ), + eemax( requestedSize.getHeight(), minimumSize.getHeight() ) ); + if ( !( windowSettings.Style & WindowStyle::Hidden ) ) + uiWindow->showWhenReady(); + return uiWindow; + } + +#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN + const bool showHostWindow = !( windowSettings.Style & WindowStyle::Hidden ); + EE::Window::WindowSettings hiddenWindowSettings( windowSettings ); + hiddenWindowSettings.Style |= WindowStyle::Hidden; + auto* ui = application.createWindow( hiddenWindowSettings, contextSettings ); + if ( nullptr == ui ) + return nullptr; + + auto context = ui->makeCurrent(); + auto* layout = UIRelativeLayout::New(); + layout->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + layout->setParent( ui->getRoot() ); + auto* uiWindow = windowFactory(); + if ( nullptr == uiWindow ) + return nullptr; + uiWindow->setWindowFlags( ( uiWindow->getWinFlags() & ~UI_WIN_MODAL ) | UI_WIN_NO_DECORATION ); + uiWindow->setLayoutSizePolicy( SizePolicy::MatchParent, SizePolicy::MatchParent ); + uiWindow->setPosition( 0, 0 ); + uiWindow->setParent( layout ); + + auto* window = ui->getWindow(); + // The application window provides the decoration. The UIWindow decoration was explicitly + // disabled above, so its border/title sizes must not affect the native client-area minimum. + const Sizef minimumSizePx = PixelDensity::dpToPx( uiWindow->getMinWindowSize() ); + const Float windowScale = window->getScale(); + const Sizei minimumSize( eeceil( minimumSizePx.getWidth() / windowScale ), + eeceil( minimumSizePx.getHeight() / windowScale ) ); + window->setMinimumSize( minimumSize.getWidth(), minimumSize.getHeight() ); + const Sizei windowSize = window->getSizeInScreenCoordinates(); + if ( windowSize.getWidth() < minimumSize.getWidth() || + windowSize.getHeight() < minimumSize.getHeight() ) { + window->setSize( eemax( windowSize.getWidth(), minimumSize.getWidth() ), + eemax( windowSize.getHeight(), minimumSize.getHeight() ) ); + } + auto* primaryWindow = application.getWindow(); + if ( modal && nullptr != primaryWindow && primaryWindow != window && + !window->setModalFor( primaryWindow ) ) + Log::warning( "UIWindow failed to make its application window modal" ); + uiWindow->mApplicationWindowCloseCallback = [&application, window] { + application.closeWindow( window ); + }; + uiWindow->showWhenReady(); + if ( showHostWindow ) + window->show(); + if ( position == ApplicationWindowPosition::CenteredOnPrimary && nullptr != primaryWindow && + primaryWindow != window ) { + const Vector2i primaryPosition = primaryWindow->getPosition(); + const Sizei primarySize = primaryWindow->getSizeInScreenCoordinates(); + const Sizei dialogSize = window->getSizeInScreenCoordinates(); + Vector2i dialogPosition( + primaryPosition.x + ( primarySize.getWidth() - dialogSize.getWidth() ) / 2, + primaryPosition.y + ( primarySize.getHeight() - dialogSize.getHeight() ) / 2 ); + auto* displayManager = Engine::instance()->getDisplayManager(); + const Vector2i primaryCenter( primaryPosition.x + primarySize.getWidth() / 2, + primaryPosition.y + primarySize.getHeight() / 2 ); + for ( int i = 0; i < displayManager->getDisplayCount(); ++i ) { + auto* display = displayManager->getDisplayIndex( i ); + if ( nullptr == display || !display->getBounds().contains( primaryCenter ) ) + continue; + const Rect usableBounds = display->getUsableBounds(); + const Rect border = window->getBorderSize(); + const int minimumX = usableBounds.Left + border.Left; + const int minimumY = usableBounds.Top + border.Top; + const int maximumX = usableBounds.Right - dialogSize.getWidth() - border.Right; + const int maximumY = usableBounds.Bottom - dialogSize.getHeight() - border.Bottom; + dialogPosition.x = + maximumX < minimumX ? minimumX : eeclamp( dialogPosition.x, minimumX, maximumX ); + dialogPosition.y = + maximumY < minimumY ? minimumY : eeclamp( dialogPosition.y, minimumY, maximumY ); + break; + } + window->setPosition( dialogPosition.x, dialogPosition.y ); + } + return uiWindow; +#else + return nullptr; +#endif +} + UIWindow::UIWindow( UIWindow::WindowBaseContainerType type ) : UIWindow( type, StyleConfig() ) {} UIWindow::UIWindow( UIWindow::WindowBaseContainerType type, const StyleConfig& windowStyleConfig ) : @@ -322,6 +455,13 @@ void UIWindow::updateWinFlags() { if ( isModal() && NULL == mModalNode ) { createModalNode(); + } else if ( !isModal() && NULL != mModalNode ) { + // The modal blocker is parented to the scene, not to the window. It must be removed when + // UI_WIN_MODAL is cleared or it will continue winning hit tests over the entire scene. + mModalNode->setEnabled( false ); + mModalNode->setVisible( false ); + mModalNode->close(); + mModalNode = NULL; } if ( needsUpdate ) { @@ -452,6 +592,7 @@ bool UIWindow::isType( const Uint32& type ) const { void UIWindow::closeWindow() { if ( mClosing ) return; + mClosing = true; if ( NULL != mButtonClose ) mButtonClose->setEnabled( false ); @@ -473,8 +614,17 @@ void UIWindow::closeWindow() { } void UIWindow::close() { + mClosing = true; UIWidget::close(); + // An application-hosted UIWindow owns the lifetime of its native host. OnWindowClose is + // emitted from the destructor and is therefore too late for controls such as a message-box + // OK button, which close the UIWindow itself. + if ( mApplicationWindowCloseCallback ) { + auto closeApplicationWindow = std::move( mApplicationWindowCloseCallback ); + closeApplicationWindow(); + } + if ( NULL != mModalNode ) { mModalNode->setEnabled( false ); mModalNode->setVisible( false ); diff --git a/src/eepp/window/backend/SDL2/displaymanagersdl2.cpp b/src/eepp/window/backend/SDL2/displaymanagersdl2.cpp index cb1d4f119..48140db0e 100644 --- a/src/eepp/window/backend/SDL2/displaymanagersdl2.cpp +++ b/src/eepp/window/backend/SDL2/displaymanagersdl2.cpp @@ -35,7 +35,7 @@ std::string DisplaySDL2::getName() const { Rect DisplaySDL2::getBounds() const { SDL_Rect r; if ( SDL_GetDisplayBounds( index, &r ) == 0 ) - return Rect( r.x, r.y, r.w, r.h ); + return Rect( Vector2i( r.x, r.y ), Sizei( r.w, r.h ) ); return Rect(); } @@ -114,7 +114,7 @@ DisplayMode DisplaySDL2::getClosestDisplayMode( DisplayMode wantedMode ) const { Rect DisplaySDL2::getUsableBounds() const { SDL_Rect r; if ( SDL_GetDisplayUsableBounds( index, &r ) == 0 ) - return Rect( r.x, r.y, r.w, r.h ); + return Rect( Vector2i( r.x, r.y ), Sizei( r.w, r.h ) ); return Rect(); } diff --git a/src/eepp/window/backend/SDL2/inputsdl2.cpp b/src/eepp/window/backend/SDL2/inputsdl2.cpp index 5c36dd189..f70fb0467 100644 --- a/src/eepp/window/backend/SDL2/inputsdl2.cpp +++ b/src/eepp/window/backend/SDL2/inputsdl2.cpp @@ -21,14 +21,8 @@ InputSDL::InputSDL( EE::Window::Window* window ) : InputSDL::~InputSDL() {} void InputSDL::update() { + beginInputFrame(); SDL_Event SDLEvent; - cleanStates(); - - ++mEventsSentId; - if ( mEventsSentId == std::numeric_limits::max() ) - mEventsSentId = 0; - - drainQueuedEvents(); if ( !mQueuedEvents.empty() ) { for ( const auto& prevEvent : mQueuedEvents ) @@ -37,9 +31,7 @@ void InputSDL::update() { } while ( SDL_PollEvent( &SDLEvent ) ) sendEvent( SDLEvent ); - InputEvent endProcessingEvent; - endProcessingEvent.Type = InputEvent::EventsSent; - processEvent( &endProcessingEvent ); + endInputFrame(); } void InputSDL::waitEvent( const Time& timeout ) { @@ -244,14 +236,16 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { } case SDL_TEXTINPUT: { String txt = String::fromUtf8( std::string_view{ SDLEvent.text.text } ); + if ( txt.empty() ) + break; event.Type = InputEvent::TextInput; event.text.timestamp = SDLEvent.text.timestamp; event.WinID = SDLEvent.text.windowID; - for ( size_t i = 0; i < txt.size() - 1; i++ ) { - event.text.text = txt[i]; - processEvent( &event ); + for ( const auto& character : txt ) { + event.text.text = character; + processEventForWindow( &event ); } - event.text.text = txt[txt.size() - 1]; + event.Type = InputEvent::NoEvent; break; } case SDL_TEXTEDITING: { @@ -357,11 +351,11 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { event.Type = InputEvent::MouseButtonDown; event.button.state = 1; - processEvent( &event ); + processEventForWindow( &event ); event.Type = InputEvent::MouseButtonUp; event.button.state = 0; - processEvent( &event ); + processEventForWindow( &event ); event.Type = InputEvent::MouseWheel; event.wheel.which = SDLEvent.wheel.which; @@ -376,7 +370,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { event.wheel.x = SDLEvent.wheel.x; event.wheel.y = SDLEvent.wheel.y; #endif - processEvent( &event ); + processEventForWindow( &event ); break; } case SDL_FINGERMOTION: { @@ -496,17 +490,8 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { } } - EE::Window::Window* win; - - if ( InputEvent::NoEvent != event.Type ) { - if ( event.WinID == mWindow->getWindowID() || event.WinID == 0 ) { - processEvent( &event ); - } else if ( ( win = Engine::instance()->getWindowID( event.WinID ) ) ) { - win->getInput()->processEvent( &event ); - } else { - processEvent( &event ); - } - } + if ( InputEvent::NoEvent != event.Type ) + processEventForWindow( &event ); if ( InputEvent::FileDropped == event.Type || InputEvent::TextDropped == event.Type ) SDL_free( SDLEvent.drop.file ); diff --git a/src/eepp/window/backend/SDL2/windowsdl2.cpp b/src/eepp/window/backend/SDL2/windowsdl2.cpp index 1a9b62bda..9f4857f5d 100644 --- a/src/eepp/window/backend/SDL2/windowsdl2.cpp +++ b/src/eepp/window/backend/SDL2/windowsdl2.cpp @@ -316,7 +316,8 @@ void WindowSDL::makeCurrent() { } void WindowSDL::close() { - destroySDLResources(); + if ( !getDeferNativeResourceDestructionOnClose() ) + destroySDLResources(); Window::close(); } @@ -540,6 +541,17 @@ void WindowSDL::setSize( Uint32 width, Uint32 height, bool windowed ) { sendVideoResizeCb(); } +void WindowSDL::setMinimumSize( Uint32 width, Uint32 height ) { + SDL_SetWindowMinimumSize( mSDLWindow, static_cast( width ), static_cast( height ) ); +} + +bool WindowSDL::setModalFor( Window* parent ) { + auto* parentWindow = dynamic_cast( parent ); + if ( nullptr == parentWindow ) + return false; + return 0 == SDL_SetWindowModalFor( mSDLWindow, parentWindow->mSDLWindow ); +} + void WindowSDL::swapBuffers() { SDL_GL_SwapWindow( mSDLWindow ); } diff --git a/src/eepp/window/backend/SDL2/windowsdl2.hpp b/src/eepp/window/backend/SDL2/windowsdl2.hpp index 2fc880929..3894c6676 100644 --- a/src/eepp/window/backend/SDL2/windowsdl2.hpp +++ b/src/eepp/window/backend/SDL2/windowsdl2.hpp @@ -51,6 +51,12 @@ class EE_API WindowSDL : public Window { void setSize( Uint32 width, Uint32 height, bool windowed ); + /** @copydoc EE::Window::Window::setMinimumSize() */ + void setMinimumSize( Uint32 width, Uint32 height ); + + /** @copydoc EE::Window::Window::setModalFor() */ + bool setModalFor( Window* parent ); + std::vector getDisplayModes() const; void setGamma( Float Red, Float Green, Float Blue ); diff --git a/src/eepp/window/backend/SDL3/displaymanagersdl3.cpp b/src/eepp/window/backend/SDL3/displaymanagersdl3.cpp index fb10cea91..92934bf42 100644 --- a/src/eepp/window/backend/SDL3/displaymanagersdl3.cpp +++ b/src/eepp/window/backend/SDL3/displaymanagersdl3.cpp @@ -17,14 +17,14 @@ std::string DisplaySDL3::getName() const { Rect DisplaySDL3::getBounds() const { SDL_Rect r{}; if ( mDisplayId && SDL_GetDisplayBounds( mDisplayId, &r ) == 0 ) - return Rect( r.x, r.y, r.w, r.h ); + return Rect( Vector2i( r.x, r.y ), Sizei( r.w, r.h ) ); return Rect(); } Rect DisplaySDL3::getUsableBounds() const { SDL_Rect r{}; if ( mDisplayId && SDL_GetDisplayUsableBounds( mDisplayId, &r ) == 0 ) - return Rect( r.x, r.y, r.w, r.h ); + return Rect( Vector2i( r.x, r.y ), Sizei( r.w, r.h ) ); return Rect(); } diff --git a/src/eepp/window/backend/SDL3/inputsdl3.cpp b/src/eepp/window/backend/SDL3/inputsdl3.cpp index e8742770b..bfd2e0e80 100644 --- a/src/eepp/window/backend/SDL3/inputsdl3.cpp +++ b/src/eepp/window/backend/SDL3/inputsdl3.cpp @@ -17,14 +17,8 @@ InputSDL::InputSDL( Window* window ) : InputSDL::~InputSDL() {} void InputSDL::update() { + beginInputFrame(); SDL_Event SDLEvent; - cleanStates(); - - ++mEventsSentId; - if ( mEventsSentId == std::numeric_limits::max() ) - mEventsSentId = 0; - - drainQueuedEvents(); if ( !mQueuedEvents.empty() ) { for ( const auto& prevEvent : mQueuedEvents ) @@ -33,9 +27,7 @@ void InputSDL::update() { } while ( SDL_PollEvent( &SDLEvent ) ) sendEvent( SDLEvent ); - InputEvent endProcessingEvent; - endProcessingEvent.Type = InputEvent::EventsSent; - processEvent( &endProcessingEvent ); + endInputFrame(); } void InputSDL::waitEvent( const Time& timeout ) { @@ -237,7 +229,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { event.WinID = SDLEvent.text.windowID; for ( const auto& character : txt ) { event.text.text = character; - processEvent( &event ); + processEventForWindow( &event ); } event.Type = InputEvent::NoEvent; // Already processed all characters break; @@ -334,11 +326,11 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { event.Type = InputEvent::MouseButtonDown; event.button.state = 1; - processEvent( &event ); + processEventForWindow( &event ); event.Type = InputEvent::MouseButtonUp; event.button.state = 0; - processEvent( &event ); + processEventForWindow( &event ); event.Type = InputEvent::MouseWheel; event.wheel.which = SDLEvent.wheel.windowID; @@ -347,7 +339,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { : InputEvent::WheelEvent::Flipped; event.wheel.x = SDLEvent.wheel.x; event.wheel.y = SDLEvent.wheel.y; - processEvent( &event ); + processEventForWindow( &event ); break; } case SDL_EVENT_JOYSTICK_AXIS_MOTION: { @@ -476,17 +468,8 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { } } - EE::Window::Window* win = nullptr; - - if ( InputEvent::NoEvent != event.Type ) { - if ( event.WinID == mWindow->getWindowID() || event.WinID == 0 ) { - processEvent( &event ); - } else if ( ( win = EE::Window::Engine::instance()->getWindowID( event.WinID ) ) ) { - win->getInput()->processEvent( &event ); - } else { - processEvent( &event ); - } - } + if ( InputEvent::NoEvent != event.Type ) + processEventForWindow( &event ); // In SDL3, drop event data is managed by SDL, do not free } diff --git a/src/eepp/window/backend/SDL3/windowsdl3.cpp b/src/eepp/window/backend/SDL3/windowsdl3.cpp index b08e42831..ce4c03e13 100644 --- a/src/eepp/window/backend/SDL3/windowsdl3.cpp +++ b/src/eepp/window/backend/SDL3/windowsdl3.cpp @@ -291,7 +291,8 @@ void WindowSDL::makeCurrent() { } void WindowSDL::close() { - destroySDLResources(); + if ( !getDeferNativeResourceDestructionOnClose() ) + destroySDLResources(); Window::close(); } @@ -513,6 +514,22 @@ void WindowSDL::setSize( Uint32 width, Uint32 height, bool windowed ) { sendVideoResizeCb(); } +void WindowSDL::setMinimumSize( Uint32 width, Uint32 height ) { + SDL_SetWindowMinimumSize( mSDLWindow, static_cast( width ), static_cast( height ) ); +} + +bool WindowSDL::setModalFor( Window* parent ) { + auto* parentWindow = dynamic_cast( parent ); + if ( nullptr == parent ) { + if ( !SDL_SetWindowModal( mSDLWindow, false ) ) + return false; + return SDL_SetWindowParent( mSDLWindow, nullptr ); + } + if ( nullptr == parentWindow || !SDL_SetWindowParent( mSDLWindow, parentWindow->mSDLWindow ) ) + return false; + return SDL_SetWindowModal( mSDLWindow, true ); +} + void WindowSDL::swapBuffers() { SDL_GL_SwapWindow( mSDLWindow ); } diff --git a/src/eepp/window/backend/SDL3/windowsdl3.hpp b/src/eepp/window/backend/SDL3/windowsdl3.hpp index 262fca016..f9e163448 100644 --- a/src/eepp/window/backend/SDL3/windowsdl3.hpp +++ b/src/eepp/window/backend/SDL3/windowsdl3.hpp @@ -45,6 +45,12 @@ class EE_API WindowSDL : public Window { void setSize( Uint32 width, Uint32 height, bool windowed ); + /** @copydoc EE::Window::Window::setMinimumSize() */ + void setMinimumSize( Uint32 width, Uint32 height ); + + /** @copydoc EE::Window::Window::setModalFor() */ + bool setModalFor( Window* parent ); + std::vector getDisplayModes() const; void setGamma( Float Red, Float Green, Float Blue ); diff --git a/src/eepp/window/engine.cpp b/src/eepp/window/engine.cpp index 836aee783..47d7c5af8 100644 --- a/src/eepp/window/engine.cpp +++ b/src/eepp/window/engine.cpp @@ -30,6 +30,7 @@ #include #endif #include +#include #include #include @@ -54,6 +55,21 @@ using namespace EE::Graphics; namespace EE { namespace Window { +Engine::WindowContext::WindowContext( Engine* engine, EE::Window::Window* window ) : + mEngine( engine ), mPreviousWindow( engine->getCurrentWindow() ) { + mEngine->setCurrentWindow( window ); +} + +Engine::WindowContext::~WindowContext() { + if ( mActive ) + mEngine->setCurrentWindow( mPreviousWindow ); +} + +Engine::WindowContext::WindowContext( WindowContext&& other ) noexcept : + mEngine( other.mEngine ), mPreviousWindow( other.mPreviousWindow ), mActive( other.mActive ) { + other.mActive = false; +} + namespace { void configureRuntimeVideoDriver() { @@ -245,6 +261,7 @@ EE::Window::Window* Engine::createDefaultWindow( const WindowSettings& Settings, } EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettings Context ) { + const bool firstWindow = mWindows.empty(); if ( Runtime::mode() == RuntimeMode::Terminal && !mWindows.empty() ) { Log::error( "Terminal runtime currently supports one top-level Window" ); return nullptr; @@ -287,7 +304,7 @@ EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettin mWindows.insert( { mWindow->getWindowID(), mWindow } ); - if ( Settings.PixelDensity > 0 ) + if ( firstWindow && Settings.PixelDensity > 0 ) PixelDensity::setPixelDensity( Settings.PixelDensity ); return window; @@ -297,11 +314,9 @@ void Engine::destroyWindow( EE::Window::Window* window ) { mWindows.erase( window->getWindowID() ); if ( window == mWindow ) { - if ( mWindows.size() > 0 ) { - mWindow = mWindows.begin()->second; - } else { - mWindow = NULL; - } + mWindow = NULL; + if ( !mWindows.empty() ) + setCurrentWindow( mWindows.begin()->second ); } eeSAFE_DELETE( window ); @@ -329,18 +344,41 @@ EE::Window::Window* Engine::getWindowID( const Uint32& winID ) { return nullptr; } +void Engine::updateInput() { + if ( !mWindow ) + return; + for ( auto& window : mWindows ) { + if ( window.second != mWindow ) + window.second->getInput()->beginInputFrame(); + } + mWindow->getInput()->update(); + for ( auto& window : mWindows ) { + if ( window.second != mWindow ) + window.second->getInput()->endInputFrame(); + } +} + EE::Window::Window* Engine::getCurrentWindow() const { return mWindow; } void Engine::setCurrentWindow( EE::Window::Window* window ) { - if ( NULL != window && window != mWindow ) { + if ( window != mWindow ) { mWindow = window; - - mWindow->setCurrent(); + if ( mWindow ) { + mWindow->setCurrent(); + // Renderer state caches are shared by all windows, while the OpenGL bindings and state + // they mirror belong to each context. Reapply that cached state to the new context. + if ( Renderer::existsSingleton() ) + Renderer::instance()->onContextChanged(); + } } } +Engine::WindowContext Engine::makeWindowCurrent( EE::Window::Window* window ) { + return WindowContext( this, window ); +} + Uint32 Engine::getWindowCount() const { return mWindows.size(); } @@ -468,7 +506,7 @@ void Engine::disableSharedGLContext() { } bool Engine::isSharedGLContextEnabled() { - return mSharedGLContext && mWindow->isThreadedGLContext(); + return mSharedGLContext && mWindow && mWindow->isThreadedGLContext(); } bool Engine::isThreaded() { diff --git a/src/eepp/window/input.cpp b/src/eepp/window/input.cpp index def164368..ddde23efd 100644 --- a/src/eepp/window/input.cpp +++ b/src/eepp/window/input.cpp @@ -31,6 +31,18 @@ Input::~Input() { eeSAFE_DELETE( mJoystickManager ); } +void Input::beginInputFrame() { + cleanStates(); + ++mEventsSentId; + drainQueuedEvents(); +} + +void Input::endInputFrame() { + InputEvent endProcessingEvent; + endProcessingEvent.Type = InputEvent::EventsSent; + processEvent( &endProcessingEvent ); +} + void Input::cleanStates() { memset( mScancodeUp, 0, EE_KEYS_SPACE ); @@ -229,6 +241,16 @@ void Input::processEvent( InputEvent* Event ) { sendEvent( Event ); } +void Input::processEventForWindow( InputEvent* Event ) { + if ( Event->WinID == 0 || Event->WinID == mWindow->getWindowID() ) { + processEvent( Event ); + } else if ( auto* window = Engine::instance()->getWindowID( Event->WinID ) ) { + window->getInput()->processEvent( Event ); + } else { + processEvent( Event ); + } +} + bool Input::enqueueEvent( InputEvent event ) { static constexpr size_t MaxInjectedEvents = 4096; std::lock_guard lock( mInjectedEventsMutex ); diff --git a/src/eepp/window/window.cpp b/src/eepp/window/window.cpp index 529a6f9af..b2f5afe4b 100644 --- a/src/eepp/window/window.cpp +++ b/src/eepp/window/window.cpp @@ -75,6 +75,12 @@ void Window::setSize( Uint32 Width, Uint32 Height ) { setSize( Width, Height, isWindowed() ); } +void Window::setMinimumSize( Uint32, Uint32 ) {} + +bool Window::setModalFor( Window* ) { + return false; +} + bool Window::isWindowed() const { return 0 != !( mWindow.WindowConfig.Style & WindowStyle::Fullscreen ); } @@ -390,6 +396,14 @@ void Window::close() { mWindow.Created = false; } +void Window::setDeferNativeResourceDestructionOnClose( bool defer ) { + mDeferNativeResourceDestructionOnClose = defer; +} + +bool Window::getDeferNativeResourceDestructionOnClose() const { + return mDeferNativeResourceDestructionOnClose; +} + void Window::setFrameRateLimit( Uint32 FrameRateLimit ) { if ( FrameRateLimit == ContextSettings::FrameRateLimitScreenRefreshRate ) { Display* currentDisplay = nullptr; @@ -504,6 +518,10 @@ void Window::clear() { } void Window::display( bool clear ) { + display( clear, true ); +} + +void Window::display( bool clear, bool limitFrameRate ) { GlobalBatchRenderer::instance()->draw(); if ( mFramePresenter ) mFramePresenter->present( *this ); @@ -526,7 +544,8 @@ void Window::display( bool clear ) { updateElapsedTime(); - limitFps(); + if ( limitFrameRate ) + limitFps(); calculateFps(); diff --git a/src/examples/ui_application_multi_window/ui_application_multi_window.cpp b/src/examples/ui_application_multi_window/ui_application_multi_window.cpp new file mode 100644 index 000000000..57dbaef60 --- /dev/null +++ b/src/examples/ui_application_multi_window/ui_application_multi_window.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 640, 480, "eepp - UIApplication Multi-window" } ); + auto* layout = app.getUI()->loadLayoutFromString( R"xml( + + + + )xml" ); + + auto* openFile = layout->querySelector( "#open_file" ); + openFile->on( Event::MouseClick, [&app]( const Event* event ) { + if ( !( event->asMouseEvent()->getFlags() & EE_BUTTON_LMASK ) ) + return; + + auto* dialog = UIFileDialog::NewInApplicationWindow( + app, { 640, 400, "Open File" }, UIFileDialog::DefaultFlags, "*", + FileSystem::getCurrentWorkingDirectory() ); + if ( nullptr == dialog ) + return; + + dialog->on( Event::OpenFile, [&app]( const Event* event ) { + const auto path = event->getNode()->asType()->getFullPath(); + UIMessageBox::NewInApplicationWindow( app, { 520, 100, "File Opened" }, + UIMessageBox::OK, + String::format( "File %s opened", path ) ); + } ); + } ); + + return app.run(); +} diff --git a/src/tests/unit_tests/uiscenenode_tests.cpp b/src/tests/unit_tests/uiscenenode_tests.cpp index b253cc3ff..26b9ebfdc 100644 --- a/src/tests/unit_tests/uiscenenode_tests.cpp +++ b/src/tests/unit_tests/uiscenenode_tests.cpp @@ -2,19 +2,25 @@ #include #include +#include #include #include #include #include +#include #include +#include +#include #include #include +#include #include #include #include #include #include #include +#include using namespace EE; using namespace EE::Graphics; @@ -29,6 +35,231 @@ UTEST( UISceneNode, CssPointerCursorUsesHandCursor ) { EXPECT_STREQ( Cursor::toName( Cursor::Arrow ), "arrow" ); } +UTEST( UISceneNode, ScopedContextBindsNodesAndRestoresNestedScene ) { + auto* engine = Engine::instance(); + auto* window = engine->getCurrentWindow(); + if ( !window ) { + window = + engine->createWindow( WindowSettings( 320, 240, "UI Context Test", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + } + + auto* sceneA = UISceneNode::New( window ); + auto* sceneB = UISceneNode::New( window ); + auto* sceneManager = SceneManager::instance(); + sceneManager->add( sceneA ); + sceneManager->add( sceneB ); + sceneManager->setCurrentUISceneNode( sceneA ); + + { + auto contextA = sceneA->makeCurrent(); + EXPECT_EQ( sceneManager->getUISceneNode(), sceneA ); + auto* nodeA = UIWidget::New(); + EXPECT_EQ( nodeA->getUISceneNode(), sceneA ); + + { + auto contextB = sceneB->makeCurrent(); + EXPECT_EQ( sceneManager->getUISceneNode(), sceneB ); + auto* nodeB = UIWidget::New(); + EXPECT_EQ( nodeB->getUISceneNode(), sceneB ); + } + + EXPECT_EQ( sceneManager->getUISceneNode(), sceneA ); + } + + EXPECT_EQ( sceneManager->getUISceneNode(), sceneA ); + sceneManager->remove( sceneB ); + sceneManager->remove( sceneA ); + eeDelete( sceneB ); + eeDelete( sceneA ); +} + +class TestUIApplication : public UIApplication { + public: + using UIApplication::UIApplication; + void tickOnce() { tick(); } +}; + +UTEST( UIApplication, CreatesSecondaryWindowWithoutChangingAmbientScene ) { + TestUIApplication app( + WindowSettings( 320, 240, "Primary UI Context", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.5f, + true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + ASSERT_TRUE( app.getWindow() != nullptr ); + ASSERT_TRUE( app.getUI() != nullptr ); + EXPECT_EQ( PixelDensity::getPixelDensity(), 1.5f ); + ASSERT_TRUE( app.getUI()->getUIIconThemeManager()->getCurrentTheme() != nullptr ); + EXPECT_TRUE( app.getUI()->getUIIconThemeManager()->findIcon( "go-up" ) != nullptr ); + EXPECT_EQ( app.getQuitPolicy(), UIApplication::QuitPolicy::OnPrimaryWindowClosed ); + if ( Runtime::mode() == RuntimeMode::Terminal ) { + auto* inApplicationWindow = UIWindow::NewInApplicationWindow( + app, WindowSettings( 240, 180, "Terminal In-Application Window" ), + UIWindow::RELATIVE_LAYOUT, UIWindow::StyleConfig(), ContextSettings(), true ); + ASSERT_TRUE( inApplicationWindow != nullptr ); + EXPECT_EQ( app.getWindowCount(), 1u ); + EXPECT_EQ( inApplicationWindow->getUISceneNode(), app.getUI() ); + EXPECT_TRUE( inApplicationWindow->isModal() ); + EXPECT_FALSE( inApplicationWindow->getWinFlags() & UI_WIN_NO_DECORATION ); + return; + } + + auto* primaryWindow = app.getWindow(); + auto* secondaryUI = + app.createWindow( WindowSettings( 240, 180, "Secondary UI Context", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + ASSERT_TRUE( secondaryUI != nullptr ); + EXPECT_EQ( PixelDensity::getPixelDensity(), 1.5f ); + EXPECT_EQ( app.getWindowCount(), 2u ); + EXPECT_EQ( app.getUI( secondaryUI->getWindow() ), secondaryUI ); + EXPECT_EQ( secondaryUI->getUIIconThemeManager()->getCurrentTheme(), + app.getUI()->getUIIconThemeManager()->getCurrentTheme() ); + EXPECT_EQ( Engine::instance()->getCurrentWindow(), primaryWindow ); + EXPECT_EQ( SceneManager::instance()->getUISceneNode(), app.getUI() ); + TextureFactory::instance()->setCurrentTexture( 123, 0 ); + Engine::instance()->setCurrentWindow( secondaryUI->getWindow() ); + EXPECT_EQ( TextureFactory::instance()->getCurrentTexture( 0 ), -1 ); + Engine::instance()->setCurrentWindow( primaryWindow ); + + { + auto context = secondaryUI->makeCurrent(); + auto* widget = UIWidget::New(); + EXPECT_EQ( widget->getUISceneNode(), secondaryUI ); + + auto* textInput = UITextInput::New(); + textInput->setParent( secondaryUI->getRoot() ); + textInput->setFocus(); + InputEvent textEvent; + textEvent.Type = InputEvent::TextInput; + textEvent.WinID = secondaryUI->getWindow()->getWindowID(); + textEvent.text.text = 'x'; + textEvent.text.timestamp = 1; + secondaryUI->getWindow()->getInput()->beginInputFrame(); + primaryWindow->getInput()->processEventForWindow( &textEvent ); + secondaryUI->getWindow()->getInput()->endInputFrame(); + EXPECT_STREQ( textInput->getText().toUtf8().c_str(), "x" ); + } + + EXPECT_EQ( SceneManager::instance()->getUISceneNode(), app.getUI() ); + + const Vector2i primaryPosition = primaryWindow->getPosition(); + const Sizei primaryScreenSize = primaryWindow->getSizeInScreenCoordinates(); + auto* fileDialog = UIFileDialog::NewInApplicationWindow( + app, + WindowSettings( 640, 400, "File Dialog UI Context", + WindowStyle::Titlebar | WindowStyle::Resize, WindowBackend::Default, 32, {}, + 1, false, true ), + UIFileDialog::DefaultFlags, "*", FileSystem::getCurrentWorkingDirectory(), + ContextSettings( false, 0, 0, GLv_default, true, false ), true, + UIWindow::ApplicationWindowPosition::CenteredOnPrimary ); + ASSERT_TRUE( fileDialog != nullptr ); + EXPECT_EQ( app.getWindowCount(), 3u ); + EXPECT_NE( fileDialog->getUISceneNode(), app.getUI() ); + EXPECT_NE( fileDialog->getUISceneNode(), secondaryUI ); + EXPECT_TRUE( fileDialog->getWinFlags() & UI_WIN_NO_DECORATION ); + EXPECT_EQ( fileDialog->getLayoutWidthPolicy(), SizePolicy::MatchParent ); + EXPECT_EQ( fileDialog->getLayoutHeightPolicy(), SizePolicy::MatchParent ); + EXPECT_EQ( Engine::instance()->getCurrentWindow(), primaryWindow ); + EXPECT_EQ( SceneManager::instance()->getUISceneNode(), app.getUI() ); + EXPECT_TRUE( fileDialog->getParent()->isType( UI_TYPE_RELATIVE_LAYOUT ) ); + const Sizei dialogWindowSize = + fileDialog->getUISceneNode()->getWindow()->getSizeInScreenCoordinates(); + const Sizef dialogMinimumSizePx = PixelDensity::dpToPx( fileDialog->getMinWindowSize() ); + const Float dialogWindowScale = fileDialog->getUISceneNode()->getWindow()->getScale(); + EXPECT_TRUE( dialogWindowSize.getWidth() >= + eeceil( dialogMinimumSizePx.getWidth() / dialogWindowScale ) ); + EXPECT_TRUE( dialogWindowSize.getHeight() >= + eeceil( dialogMinimumSizePx.getHeight() / dialogWindowScale ) ); + const Vector2i dialogPosition = fileDialog->getUISceneNode()->getWindow()->getPosition(); + const Vector2i primaryCenter( primaryPosition.x + primaryScreenSize.getWidth() / 2, + primaryPosition.y + primaryScreenSize.getHeight() / 2 ); + auto* displayManager = Engine::instance()->getDisplayManager(); + for ( int i = 0; i < displayManager->getDisplayCount(); ++i ) { + auto* display = displayManager->getDisplayIndex( i ); + if ( nullptr == display || !display->getBounds().contains( primaryCenter ) ) + continue; + const Rect usableBounds = display->getUsableBounds(); + const Rect border = fileDialog->getUISceneNode()->getWindow()->getBorderSize(); + if ( dialogWindowSize.getWidth() + border.Left + border.Right <= usableBounds.getWidth() ) { + EXPECT_TRUE( dialogPosition.x - border.Left >= usableBounds.Left ); + EXPECT_TRUE( dialogPosition.x + dialogWindowSize.getWidth() + border.Right <= + usableBounds.Right ); + } + if ( dialogWindowSize.getHeight() + border.Top + border.Bottom <= + usableBounds.getHeight() ) { + EXPECT_TRUE( dialogPosition.y - border.Top >= usableBounds.Top ); + EXPECT_TRUE( dialogPosition.y + dialogWindowSize.getHeight() + border.Bottom <= + usableBounds.Bottom ); + } + break; + } + + auto* primaryFont = app.getUI()->getUIThemeManager()->getDefaultFont(); + ASSERT_TRUE( primaryFont != nullptr ); + fileDialog->getUISceneNode()->getWindow()->close(); + app.tickOnce(); + EXPECT_EQ( app.getWindowCount(), 2u ); + EXPECT_EQ( app.getUI()->getUIThemeManager()->getDefaultFont(), primaryFont ); + auto* genericWindow = UIWindow::NewInApplicationWindow( + app, + WindowSettings( 400, 240, "Generic UI Window", WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + UIWindow::RELATIVE_LAYOUT, UIWindow::StyleConfig(), + ContextSettings( false, 0, 0, GLv_default, true, false ), true ); + ASSERT_TRUE( genericWindow != nullptr ); + EXPECT_EQ( app.getWindowCount(), 3u ); + genericWindow->getUISceneNode()->getWindow()->close(); + app.tickOnce(); + EXPECT_EQ( app.getWindowCount(), 2u ); + auto* messageBox = UIMessageBox::NewInApplicationWindow( + app, + WindowSettings( 400, 180, "Message Box UI Context", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIMessageBox::OK, "Native modal message", UI_MESSAGE_BOX_DEFAULT_FLAGS, + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + ASSERT_TRUE( messageBox != nullptr ); + EXPECT_EQ( app.getWindowCount(), 3u ); + EXPECT_FALSE( messageBox->isModal() ); + EXPECT_TRUE( messageBox->getModalWidget() == nullptr ); + const Sizei messageBoxWindowSize = + messageBox->getUISceneNode()->getWindow()->getSizeInScreenCoordinates(); + const Uint32 messageBoxWindowId = messageBox->getUISceneNode()->getWindow()->getWindowID(); + EXPECT_EQ( messageBoxWindowSize.getWidth(), 400 ); + EXPECT_EQ( messageBoxWindowSize.getHeight(), 180 ); + bool messageBoxConfirmed = false; + messageBox->on( Event::OnConfirm, + [&messageBoxConfirmed]( const Event* ) { messageBoxConfirmed = true; } ); + messageBox->getUISceneNode()->getUIThemeManager()->setDefaultEffectsEnabled( true ); + messageBox->getEventDispatcher()->sendMsg( messageBox->getButtonOK(), NodeMessage::MouseClick, + EE_BUTTON_LMASK ); + EXPECT_TRUE( messageBoxConfirmed ); + EXPECT_FALSE( messageBox->getUISceneNode()->getWindow()->isVisible() ); + for ( int i = 0; i < 20 && app.getWindowCount() == 3u; ++i ) { + Sys::sleep( Milliseconds( 5 ) ); + app.tickOnce(); + } + EXPECT_EQ( app.getWindowCount(), 2u ); + EXPECT_TRUE( Engine::instance()->getWindowID( messageBoxWindowId ) == nullptr ); + + auto* reopenedDialog = UIFileDialog::NewInApplicationWindow( + app, + WindowSettings( 640, 400, "Reopened File Dialog", WindowStyle::Default, + WindowBackend::Default, 32, {}, 1, false, true ), + UIFileDialog::DefaultFlags, "*", FileSystem::getCurrentWorkingDirectory(), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + ASSERT_TRUE( reopenedDialog != nullptr ); + EXPECT_EQ( app.getWindowCount(), 3u ); + EXPECT_EQ( reopenedDialog->getUISceneNode()->getUIThemeManager()->getDefaultFont(), + primaryFont ); + + primaryWindow->close(); + app.tickOnce(); + EXPECT_EQ( app.getWindowCount(), 0u ); +} + UTEST( Node, DescendantWorldBoundsRefreshAfterDirtyAncestorMoves ) { Node* parent = Node::New(); Node* child = Node::New(); @@ -79,9 +310,7 @@ class InvalidationTestSceneNode : public UISceneNode { size_t processedStyleRootCount() const { return mDirtyStylesSnapshot.size(); } - UIWidget* processedStyleRoot( size_t index ) const { - return mDirtyStylesSnapshot[index].first; - } + UIWidget* processedStyleRoot( size_t index ) const { return mDirtyStylesSnapshot[index].first; } bool processedStyleRootDisablesAnimations( size_t index ) const { return mDirtyStylesSnapshot[index].second;