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
This commit is contained in:
Martín Lucas Golini
2026-09-10 19:27:29 -03:00
parent 3493e849a2
commit 095fe7919e
46 changed files with 1290 additions and 109 deletions
+6
View File
@@ -411,6 +411,12 @@
"command": "${project_root}/bin/eepp-ui-data-handling-debug", "command": "${project_root}/bin/eepp-ui-data-handling-debug",
"name": "eepp-ui-data-handling-debug", "name": "eepp-ui-data-handling-debug",
"working_dir": "${project_root}/bin" "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": { "var": {
@@ -170,6 +170,9 @@ class EE_API Renderer {
virtual void enable( unsigned int cap ); 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 GraphicsLibraryVersion version() = 0;
virtual std::string versionStr() = 0; virtual std::string versionStr() = 0;
@@ -33,6 +33,8 @@ class EE_API RendererGL3 : public RendererGLShader {
void enable( unsigned int cap ); void enable( unsigned int cap );
void onContextChanged();
void enableClientState( unsigned int array ); void enableClientState( unsigned int array );
void disableClientState( unsigned int array ); void disableClientState( unsigned int array );
@@ -33,6 +33,8 @@ class EE_API RendererGL3CP : public RendererGLShader {
void enable( unsigned int cap ); void enable( unsigned int cap );
void onContextChanged();
void enableClientState( unsigned int array ); void enableClientState( unsigned int array );
void disableClientState( unsigned int array ); void disableClientState( unsigned int array );
@@ -39,6 +39,8 @@ class EE_API RendererGLES2 : public RendererGLShader {
void enable( unsigned int cap ); void enable( unsigned int cap );
void onContextChanged();
void enableClientState( unsigned int array ); void enableClientState( unsigned int array );
void disableClientState( unsigned int array ); void disableClientState( unsigned int array );
+3
View File
@@ -172,6 +172,9 @@ class EE_API TextureFactory : protected Mutex {
*/ */
void setCurrentTexture( const int& textureHandle, const Uint32& TextureUnit ); 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. */ /** Returns the number of currently live textures. */
Uint32 getTextureCount(); Uint32 getTextureCount();
+19
View File
@@ -10,6 +10,9 @@ using namespace EE::System;
namespace EE { namespace UI { namespace EE { namespace UI {
class UISceneNode; class UISceneNode;
}} // namespace EE::UI }} // namespace EE::UI
namespace EE { namespace Window {
class Window;
}} // namespace EE::Window
using namespace EE::UI; using namespace EE::UI;
namespace EE { namespace Scene { namespace EE { namespace Scene {
@@ -33,19 +36,35 @@ class EE_API SceneManager {
void draw(); 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( const Time& elapsed );
void update(); void update();
UISceneNode* getUISceneNode(); 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 ); void setCurrentUISceneNode( UISceneNode* uiSceneNode );
/** Removes and destroys all registered scene nodes associated with @p window. */
void destroyScenes( EE::Window::Window* window );
Time getElapsed() const; Time getElapsed() const;
protected: 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; Clock mClock;
UISceneNode* mUISceneNode; UISceneNode* mUISceneNode;
UISceneNode* mScopedUISceneNode;
std::vector<SceneNode*> mSceneNodes; std::vector<SceneNode*> mSceneNodes;
}; };
+79
View File
@@ -2,10 +2,12 @@
#define EE_UI_UIAPPLICATION #define EE_UI_UIAPPLICATION
#include <eepp/graphics/font.hpp> #include <eepp/graphics/font.hpp>
#include <eepp/system/clock.hpp>
#include <eepp/window/window.hpp> #include <eepp/window/window.hpp>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <vector>
using namespace EE::Window; using namespace EE::Window;
@@ -19,6 +21,17 @@ class UIApplicationSystemFontState;
class EE_API UIApplication { class EE_API UIApplication {
public: 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 { struct EE_API Settings {
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). //! 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 //! Otherwise it will initialize with an empty UI scene node
bool loadBaseResources{ true }; 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 //! The default base font for the UI. If not provided it will load NotoSans-Regular ( will
//! look at "assets/fonts/NotoSans-Regular.ttf" ) //! look at "assets/fonts/NotoSans-Regular.ttf" )
Font* baseFont{ nullptr }; Font* baseFont{ nullptr };
@@ -73,6 +89,39 @@ class EE_API UIApplication {
//! Document //! Document
UISceneNode* getUI() const; 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 //! Runs the application until window is closed
//! @return EXIT_SUCCESS if application run successfully //! @return EXIT_SUCCESS if application run successfully
int run(); int run();
@@ -91,11 +140,41 @@ class EE_API UIApplication {
String::HashType getStyleSheetDefaultMarker() const { return mStyleSheetMarker; } String::HashType getStyleSheetDefaultMarker() const { return mStyleSheetMarker; }
protected: 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 }; UISceneNode* mUISceneNode{ nullptr };
EE::Window::Window* mWindow{ nullptr }; EE::Window::Window* mWindow{ nullptr };
String::HashType mStyleSheetMarker{ 0 }; String::HashType mStyleSheetMarker{ 0 };
bool mDidRun{ false }; bool mDidRun{ false };
bool mShowMemoryManagerResult{ false }; bool mShowMemoryManagerResult{ false };
bool mRunning{ false };
QuitPolicy mQuitPolicy{ QuitPolicy::OnPrimaryWindowClosed };
std::vector<WindowEntry> mWindows;
Settings mSettings;
System::Clock mFrameClock;
std::unique_ptr<Private::UIApplicationSystemFontState> mSystemFontState; std::unique_ptr<Private::UIApplicationSystemFontState> mSystemFontState;
}; };
+14
View File
@@ -16,6 +16,7 @@
namespace EE { namespace UI { namespace EE { namespace UI {
struct NativeFileDialogHandler; struct NativeFileDialogHandler;
class UIApplication;
class EE_API UIFileDialog : public UIWindow { class EE_API UIFileDialog : public UIWindow {
public: public:
@@ -39,6 +40,19 @@ class EE_API UIFileDialog : public UIWindow {
const std::string& defaultFilePattern = "*", const std::string& defaultFilePattern = "*",
const std::string& defaultDirectory = Sys::getProcessPath() ); 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 ~UIFileDialog();
virtual Uint32 getType() const; virtual Uint32 getType() const;
+4
View File
@@ -21,6 +21,10 @@ class EE_API UIIconThemeManager {
UIIconTheme* getCurrentTheme() const; 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 ); UIIconThemeManager* setCurrentTheme( UIIconThemePtr currentTheme );
UIIconTheme* getFallbackTheme() const; UIIconTheme* getFallbackTheme() const;
+12 -1
View File
@@ -2,6 +2,7 @@
#define EE_UICUIMESSAGEBOX_HPP #define EE_UICUIMESSAGEBOX_HPP
#include <eepp/ui/uiwindow.hpp> #include <eepp/ui/uiwindow.hpp>
#include <eepp/window/window.hpp>
namespace EE { namespace UI { namespace EE { namespace UI {
@@ -11,6 +12,7 @@ class UILayout;
class UIPushButton; class UIPushButton;
class UIDropDownList; class UIDropDownList;
class UIComboBox; class UIComboBox;
class UIApplication;
#define UI_MESSAGE_BOX_DEFAULT_FLAGS \ #define UI_MESSAGE_BOX_DEFAULT_FLAGS \
UI_WIN_CLOSE_BUTTON | UI_WIN_USE_DEFAULT_BUTTONS_ACTIONS | UI_WIN_MODAL | \ 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, static UIMessageBox* New( const Type& type, const String& message,
const Uint32& windowFlags = UI_MESSAGE_BOX_DEFAULT_FLAGS ); 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 ~UIMessageBox();
virtual void setTheme( UITheme* theme ); virtual void setTheme( UITheme* theme );
@@ -69,7 +81,6 @@ class EE_API UIMessageBox : public UIWindow {
virtual Uint32 onKeyUp( const KeyEvent& event ); virtual Uint32 onKeyUp( const KeyEvent& event );
virtual Uint32 onMessage( const NodeMessage* Msg ); virtual Uint32 onMessage( const NodeMessage* Msg );
}; };
}} // namespace EE::UI }} // namespace EE::UI
+24
View File
@@ -14,6 +14,7 @@
#include <eepp/ui/keyboardshortcut.hpp> #include <eepp/ui/keyboardshortcut.hpp>
#include <eepp/ui/layoutinvalidation.hpp> #include <eepp/ui/layoutinvalidation.hpp>
#include <eepp/ui/webresourcecache.hpp> #include <eepp/ui/webresourcecache.hpp>
#include <eepp/window/engine.hpp>
#include <atomic> #include <atomic>
#include <functional> #include <functional>
@@ -51,6 +52,29 @@ struct NavigationRequest {
class EE_API UISceneNode : public SceneNode { class EE_API UISceneNode : public SceneNode {
public: 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. * @brief Creates a new UISceneNode instance.
* *
+29
View File
@@ -4,6 +4,7 @@
#include <eepp/thirdparty/nlohmann/json_fwd.hpp> #include <eepp/thirdparty/nlohmann/json_fwd.hpp>
#include <eepp/ui/keyboardshortcut.hpp> #include <eepp/ui/keyboardshortcut.hpp>
#include <eepp/ui/uiwidget.hpp> #include <eepp/ui/uiwidget.hpp>
#include <eepp/window/window.hpp>
namespace EE { namespace Graphics { namespace EE { namespace Graphics {
class FrameBuffer; class FrameBuffer;
@@ -11,6 +12,7 @@ class FrameBuffer;
namespace EE { namespace UI { namespace EE { namespace UI {
class UIApplication;
class UITextView; class UITextView;
class UISceneNode; class UISceneNode;
@@ -61,6 +63,14 @@ class EE_API UIWindow : public UIWidget {
RELATIVE_LAYOUT 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, static UIWindow* NewOpt( WindowBaseContainerType type,
const StyleConfig& windowStyleConfig = StyleConfig() ); const StyleConfig& windowStyleConfig = StyleConfig() );
@@ -72,6 +82,17 @@ class EE_API UIWindow : public UIWidget {
static UIWindow* NewRelLay(); 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 ~UIWindow();
virtual Uint32 getType() const; virtual Uint32 getType() const;
@@ -210,6 +231,13 @@ class EE_API UIWindow : public UIWidget {
virtual void unserialize( const nlohmann::json& json ); virtual void unserialize( const nlohmann::json& json );
protected: 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<UIWindow*()>& windowFactory,
const EE::Window::ContextSettings& contextSettings,
bool modal, ApplicationWindowPosition position );
enum UI_RESIZE_TYPE { enum UI_RESIZE_TYPE {
RESIZE_NONE, RESIZE_NONE,
RESIZE_LEFT, RESIZE_LEFT,
@@ -256,6 +284,7 @@ class EE_API UIWindow : public UIWidget {
KeyBindings mKeyBindings; KeyBindings mKeyBindings;
std::map<std::string, KeyBindingCommand> mKeyBindingCommands; std::map<std::string, KeyBindingCommand> mKeyBindingCommands;
std::function<bool( Node* focusNode )> mCheckEphemeralCloseFn; std::function<bool( Node* focusNode )> mCheckEphemeralCloseFn;
std::function<void()> mApplicationWindowCloseCallback;
explicit UIWindow( WindowBaseContainerType type, const StyleConfig& windowStyleConfig ); explicit UIWindow( WindowBaseContainerType type, const StyleConfig& windowStyleConfig );
+30 -1
View File
@@ -28,6 +28,28 @@ class EE_API Engine {
SINGLETON_DECLARE_HEADERS( Engine ) SINGLETON_DECLARE_HEADERS( Engine )
public: 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(); ~Engine();
static bool isEngineRunning(); static bool isEngineRunning();
@@ -54,6 +76,9 @@ class EE_API Engine {
/** Set the window as the current. */ /** Set the window as the current. */
void setCurrentWindow( EE::Window::Window* window ); 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. */ /** @return The number of windows created. */
Uint32 getWindowCount() const; Uint32 getWindowCount() const;
@@ -67,6 +92,10 @@ class EE_API Engine {
EE::Window::Window* getWindowID( const Uint32& winID ); 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 /** Constructs WindowSettings from an ini file
It will search for the following properties: It will search for the following properties:
Width Window width Width Window width
@@ -181,7 +210,7 @@ class EE_API Engine {
EE::Window::Window* createSDL2Window( const WindowSettings& Settings, EE::Window::Window* createSDL2Window( const WindowSettings& Settings,
const ContextSettings& Context ); const ContextSettings& Context );
#ifdef EE_BACKEND_SDL3 #if defined( EE_BACKEND_SDL3 ) || defined( EE_SDL_VERSION_3 )
Backend::WindowBackendLibrary* createSDL3Backend( const WindowSettings& Settings ); Backend::WindowBackendLibrary* createSDL3Backend( const WindowSettings& Settings );
EE::Window::Window* createSDL3Window( const WindowSettings& Settings, EE::Window::Window* createSDL3Window( const WindowSettings& Settings,
+10
View File
@@ -27,6 +27,12 @@ class EE_API Input {
/** Update the Input */ /** Update the Input */
virtual void update() = 0; 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 /** If timeout is zero waits indefinitely for the next available event otherwise waits until the
* specified timeout for the next available event. * specified timeout for the next available event.
*/ */
@@ -251,6 +257,10 @@ class EE_API Input {
/** Process an input event. Called by the input update. */ /** Process an input event. Called by the input update. */
void processEvent( InputEvent* Event ); 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. /** 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. * If the bounded queue is full, an older mouse-motion event can be discarded to make room.
* @return True if the event was queued. * @return True if the event was queued.
+22
View File
@@ -274,6 +274,14 @@ class EE_API Window {
*/ */
virtual void setSize( Uint32 Width, Uint32 Height, bool isWindowed ) = 0; 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 */ /** @return The window size in pixels */
virtual Sizei getSize() const; virtual Sizei getSize() const;
@@ -311,6 +319,12 @@ class EE_API Window {
*/ */
virtual void display( bool clear = false ); 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 */ /** @return The elapsed time for the last frame rendered */
virtual const System::Time& getElapsed() const; virtual const System::Time& getElapsed() const;
@@ -351,6 +365,13 @@ class EE_API Window {
/** Close the window if is running */ /** Close the window if is running */
virtual void close(); 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 /** Set the current active view
* @param view New view to use (pass GetDefaultView() to set the default 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. * @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; friend class Input;
mutable WindowInfo mWindow; mutable WindowInfo mWindow;
bool mDeferNativeResourceDestructionOnClose{ false };
Clipboard* mClipboard; Clipboard* mClipboard;
Input* mInput; Input* mInput;
CursorManager* mCursorManager; CursorManager* mCursorManager;
+6
View File
@@ -1737,6 +1737,12 @@ solution "eepp"
files { "src/examples/ui_application_hello_world/*.cpp" } files { "src/examples/ui_application_hello_world/*.cpp" }
build_link_configuration( "eepp-ui-application-hello-world", true ) 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" project "eepp-ui-font-picker"
set_kind() set_kind()
language "C++" language "C++"
+6
View File
@@ -1771,6 +1771,12 @@ workspace "eepp"
files { "src/examples/ui_application_hello_world/*.cpp" } files { "src/examples/ui_application_hello_world/*.cpp" }
build_link_configuration( "eepp-ui-application-hello-world", true ) 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" project "eepp-ui-font-picker"
set_kind() set_kind()
language "C++" language "C++"
+17
View File
@@ -1,10 +1,12 @@
#include <SOIL2/src/SOIL2/SOIL2.h> #include <SOIL2/src/SOIL2/SOIL2.h>
#include <eepp/graphics/blendmode.hpp>
#include <eepp/graphics/renderer/openglext.hpp> #include <eepp/graphics/renderer/openglext.hpp>
#include <eepp/graphics/renderer/renderer.hpp> #include <eepp/graphics/renderer/renderer.hpp>
#include <eepp/graphics/renderer/renderergl.hpp> #include <eepp/graphics/renderer/renderergl.hpp>
#include <eepp/graphics/renderer/renderergl3.hpp> #include <eepp/graphics/renderer/renderergl3.hpp>
#include <eepp/graphics/renderer/renderergl3cp.hpp> #include <eepp/graphics/renderer/renderergl3cp.hpp>
#include <eepp/graphics/renderer/renderergles2.hpp> #include <eepp/graphics/renderer/renderergles2.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/system/sys.hpp> #include <eepp/system/sys.hpp>
#ifdef EE_GLES1_LATE_INCLUDE #ifdef EE_GLES1_LATE_INCLUDE
@@ -483,6 +485,21 @@ void Renderer::enable( unsigned int cap ) {
glEnable( 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 ) { const char* Renderer::getString( unsigned int name ) {
return (const char*)glGetString( name ); return (const char*)glGetString( name );
} }
@@ -115,6 +115,11 @@ void RendererGL3::reloadCurrentShader() {
reloadShader( mCurShader ); reloadShader( mCurShader );
} }
void RendererGL3::onContextChanged() {
Renderer::onContextChanged();
reloadCurrentShader();
}
ShaderProgramPtr RendererGL3::createSubpixelDualSourceShader() { ShaderProgramPtr RendererGL3::createSubpixelDualSourceShader() {
std::string vertexShader = mBaseVertexShader; std::string vertexShader = mBaseVertexShader;
String::replaceAll( vertexShader, "#version 120", "#version 130" ); String::replaceAll( vertexShader, "#version 120", "#version 130" );
@@ -147,6 +147,11 @@ void RendererGL3CP::reloadCurrentShader() {
reloadShader( mCurShader ); reloadShader( mCurShader );
} }
void RendererGL3CP::onContextChanged() {
Renderer::onContextChanged();
reloadCurrentShader();
}
ShaderProgramPtr RendererGL3CP::createSubpixelDualSourceShader() { ShaderProgramPtr RendererGL3CP::createSubpixelDualSourceShader() {
static const char fragmentShader[] = R"(#version 330 static const char fragmentShader[] = R"(#version 330
uniform sampler2D textureUnit0; uniform sampler2D textureUnit0;
@@ -148,6 +148,11 @@ void RendererGLES2::reloadCurrentShader() {
reloadShader( mCurShader ); reloadShader( mCurShader );
} }
void RendererGLES2::onContextChanged() {
Renderer::onContextChanged();
reloadCurrentShader();
}
ShaderProgramPtr RendererGLES2::createSubpixelDualSourceShader() { ShaderProgramPtr RendererGLES2::createSubpixelDualSourceShader() {
#ifdef EE_GLES2 #ifdef EE_GLES2
static const char fragmentShader[] = R"(#extension GL_EXT_blend_func_extended : require static const char fragmentShader[] = R"(#extension GL_EXT_blend_func_extended : require
+6
View File
@@ -1,5 +1,6 @@
#include <SOIL2/src/SOIL2/SOIL2.h> #include <SOIL2/src/SOIL2/SOIL2.h>
#include <SOIL2/src/SOIL2/stb_image.h> #include <SOIL2/src/SOIL2/stb_image.h>
#include <algorithm>
#include <atomic> #include <atomic>
#include <eepp/graphics/renderer/openglext.hpp> #include <eepp/graphics/renderer/openglext.hpp>
#include <eepp/graphics/renderer/renderer.hpp> #include <eepp/graphics/renderer/renderer.hpp>
@@ -221,6 +222,11 @@ void TextureFactory::setCurrentTexture( const int& textureHandle, const Uint32&
mCurrentTexture[TextureUnit] = textureHandle; mCurrentTexture[TextureUnit] = textureHandle;
} }
void TextureFactory::invalidateTextureBindings() {
std::fill( mCurrentTexture.begin(), mCurrentTexture.end(), -1 );
mLastCoordinateType = static_cast<Texture::CoordinateType>( -1 );
}
TextureRegistrySnapshot TextureFactory::snapshotTextures() { TextureRegistrySnapshot TextureFactory::snapshotTextures() {
struct LockedTextureRecord { struct LockedTextureRecord {
LiveTextureRecord record; LiveTextureRecord record;
+15 -2
View File
@@ -1,5 +1,6 @@
#include <eepp/scene/eventdispatcher.hpp> #include <eepp/scene/eventdispatcher.hpp>
#include <eepp/scene/scenenode.hpp> #include <eepp/scene/scenenode.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/window/engine.hpp> #include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp> #include <eepp/window/input.hpp>
#include <eepp/window/inputevent.hpp> #include <eepp/window/inputevent.hpp>
@@ -24,10 +25,22 @@ EventDispatcher::EventDispatcher( SceneNode* sceneNode ) :
mFirstPress( false ), mFirstPress( false ),
mNodeWasDragging( NULL ), mNodeWasDragging( NULL ),
mNodeDragging( NULL ) { mNodeDragging( NULL ) {
mCbId = mInput->pushCallback( [this]( InputEvent* event ) { inputCallback( event ); } ); mCbId = mInput->pushCallback( [this]( InputEvent* event ) {
if ( mSceneNode->isUISceneNode() ) {
auto context = mSceneNode->asType<UI::UISceneNode>()->makeCurrent();
inputCallback( event );
} else {
inputCallback( event );
}
} );
mIMECbId = mWindow->getIME().addTextEditingCb( mIMECbId = mWindow->getIME().addTextEditingCb(
[this]( const String& text, Int32 start, Int32 length ) { [this]( const String& text, Int32 start, Int32 length ) {
sendTextEditing( text, start, length ); if ( mSceneNode->isUISceneNode() ) {
auto context = mSceneNode->asType<UI::UISceneNode>()->makeCurrent();
sendTextEditing( text, start, length );
} else {
sendTextEditing( text, start, length );
}
} ); } );
} }
+12
View File
@@ -7,6 +7,8 @@
#include <eepp/scene/node.hpp> #include <eepp/scene/node.hpp>
#include <eepp/scene/scenemanager.hpp> #include <eepp/scene/scenemanager.hpp>
#include <eepp/scene/scenenode.hpp> #include <eepp/scene/scenenode.hpp>
#include <eepp/ui/uinode.hpp>
#include <eepp/ui/uiscenenode.hpp>
#include <eepp/window/engine.hpp> #include <eepp/window/engine.hpp>
namespace EE { namespace Scene { namespace EE { namespace Scene {
@@ -227,6 +229,16 @@ void Node::unsubscribeScheduledUpdate() {
Node* Node::setParent( Node* parent ) { Node* Node::setParent( Node* parent ) {
eeASSERT( NULL != parent ); eeASSERT( NULL != parent );
#ifdef EE_DEBUG
if ( isUINode() && parent->isUINode() ) {
auto* childScene = asType<UI::UINode>()->getUISceneNode();
auto* parentScene = parent->asType<UI::UINode>()->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 ) if ( parent == mParentNode )
return this; return this;
+73 -3
View File
@@ -13,7 +13,7 @@ bool SceneManager::isActive() {
!SceneManager::isShuttingDown(); !SceneManager::isShuttingDown();
} }
SceneManager::SceneManager() : mUISceneNode( NULL ) {} SceneManager::SceneManager() : mUISceneNode( NULL ), mScopedUISceneNode( NULL ) {}
SceneManager::~SceneManager() { SceneManager::~SceneManager() {
for ( auto& it : mSceneNodes ) { for ( auto& it : mSceneNodes ) {
@@ -32,6 +32,10 @@ SceneNode* SceneManager::add( SceneNode* sceneNode ) {
bool SceneManager::remove( SceneNode* sceneNode ) { bool SceneManager::remove( SceneNode* sceneNode ) {
auto it = std::find( mSceneNodes.begin(), mSceneNodes.end(), sceneNode ); auto it = std::find( mSceneNodes.begin(), mSceneNodes.end(), sceneNode );
if ( it != mSceneNodes.end() ) { if ( it != mSceneNodes.end() ) {
if ( mUISceneNode == sceneNode )
mUISceneNode = nullptr;
if ( mScopedUISceneNode == sceneNode )
mScopedUISceneNode = nullptr;
mSceneNodes.erase( it ); mSceneNodes.erase( it );
return true; return true;
} }
@@ -44,13 +48,39 @@ size_t SceneManager::count() const {
void SceneManager::draw() { void SceneManager::draw() {
for ( auto& sceneNode : mSceneNodes ) { for ( auto& sceneNode : mSceneNodes ) {
sceneNode->draw(); if ( sceneNode->isUISceneNode() ) {
auto context = sceneNode->asType<UISceneNode>()->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<UISceneNode>()->makeCurrent();
sceneNode->draw();
} else {
auto context = Engine::instance()->makeWindowCurrent( window );
sceneNode->draw();
}
} }
} }
void SceneManager::update( const Time& elapsed ) { void SceneManager::update( const Time& elapsed ) {
for ( auto& sceneNode : mSceneNodes ) { for ( auto& sceneNode : mSceneNodes ) {
sceneNode->update( elapsed ); if ( sceneNode->isUISceneNode() ) {
auto context = sceneNode->asType<UISceneNode>()->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() { 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 ) { if ( NULL == mUISceneNode ) {
for ( auto& sceneNode : mSceneNodes ) { for ( auto& sceneNode : mSceneNodes ) {
if ( sceneNode->isUISceneNode() ) { if ( sceneNode->isUISceneNode() ) {
@@ -71,10 +111,40 @@ UISceneNode* SceneManager::getUISceneNode() {
return mUISceneNode; return mUISceneNode;
} }
UISceneNode* SceneManager::getUISceneNode( EE::Window::Window* window ) {
for ( auto& sceneNode : mSceneNodes ) {
if ( sceneNode->isUISceneNode() && sceneNode->getWindow() == window )
return sceneNode->asType<UISceneNode>();
}
return nullptr;
}
void SceneManager::setCurrentUISceneNode( UISceneNode* uiSceneNode ) { void SceneManager::setCurrentUISceneNode( UISceneNode* uiSceneNode ) {
mUISceneNode = 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 { Time SceneManager::getElapsed() const {
return mClock.getElapsedTime(); return mClock.getElapsedTime();
} }
+230 -18
View File
@@ -5,7 +5,9 @@
#include <eepp/scene/scenemanager.hpp> #include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp> #include <eepp/system/filesystem.hpp>
#include <eepp/system/thread.hpp> #include <eepp/system/thread.hpp>
#include <eepp/ui/iconmanager.hpp>
#include <eepp/ui/uiapplication.hpp> #include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uiiconthememanager.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitheme.hpp> #include <eepp/ui/uitheme.hpp>
#include <eepp/ui/uithememanager.hpp> #include <eepp/ui/uithememanager.hpp>
@@ -17,6 +19,10 @@
#include <atomic> #include <atomic>
#include <iostream> #include <iostream>
#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
#include <emscripten/emscripten.h>
#endif
using namespace EE::Graphics; using namespace EE::Graphics;
using namespace EE::System; using namespace EE::System;
using namespace EE::Scene; using namespace EE::Scene;
@@ -40,7 +46,8 @@ class UIApplicationSystemFontState {
static std::atomic<bool> sSystemFontsEnabledByDefault{ true }; static std::atomic<bool> sSystemFontsEnabledByDefault{ true };
UIApplication::UIApplication( const WindowSettings& windowSettings, const Settings& appSettings, UIApplication::UIApplication( const WindowSettings& windowSettings, const Settings& appSettings,
const ContextSettings& contextSettings ) { const ContextSettings& contextSettings ) :
mSettings( appSettings ) {
const bool enableSystemFonts = appSettings.enableSystemFonts.value_or( const bool enableSystemFonts = appSettings.enableSystemFonts.value_or(
sSystemFontsEnabledByDefault.load( std::memory_order_acquire ) ); sSystemFontsEnabledByDefault.load( std::memory_order_acquire ) );
if ( enableSystemFonts ) { if ( enableSystemFonts ) {
@@ -67,6 +74,7 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin
std::cerr << "Could not create window, exiting" << std::endl; std::cerr << "Could not create window, exiting" << std::endl;
return; return;
} }
mWindow->setDeferNativeResourceDestructionOnClose( true );
mDidRun = true; mDidRun = true;
@@ -93,11 +101,12 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin
defaultFontService.setHinting( appSettings.fontHinting ); defaultFontService.setHinting( appSettings.fontHinting );
defaultFontService.setAntialiasing( appSettings.fontAntialiasing ); defaultFontService.setAntialiasing( appSettings.fontAntialiasing );
mUISceneNode = UISceneNode::New(); mUISceneNode = UISceneNode::New( mWindow );
FontService& uiFontService = mUISceneNode->getResourceScope()->getFontService(); FontService& uiFontService = mUISceneNode->getResourceScope()->getFontService();
uiFontService.setHinting( appSettings.fontHinting ); uiFontService.setHinting( appSettings.fontHinting );
uiFontService.setAntialiasing( appSettings.fontAntialiasing ); uiFontService.setAntialiasing( appSettings.fontAntialiasing );
SceneManager::instance()->add( mUISceneNode ); SceneManager::instance()->add( mUISceneNode );
mWindows.push_back( { mWindow, mUISceneNode, true, false } );
if ( !appSettings.loadBaseResources ) if ( !appSettings.loadBaseResources )
return; return;
@@ -144,6 +153,24 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin
mUISceneNode->setStyleSheet( theme->getStyleSheet() ); mUISceneNode->setStyleSheet( theme->getStyleSheet() );
mUISceneNode->getStyleSheet().setMarker( mStyleSheetMarker ); mUISceneNode->getStyleSheet().setMarker( mStyleSheetMarker );
mUISceneNode->getUIThemeManager()->setDefaultTheme( std::move( theme ) ); 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<FontTrueType*>( 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() { UIApplication::~UIApplication() {
@@ -161,31 +188,216 @@ UISceneNode* UIApplication::getUI() const {
return mUISceneNode; 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() { int UIApplication::run() {
if ( !mDidRun )
return EXIT_FAILURE;
// Offscreen SDL windows do not receive an initial expose event. Ensure the first logical // 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. // framebuffer is rendered even when the scene was fully laid out before entering the loop.
if ( Runtime::isOffscreen() ) if ( Runtime::isOffscreen() ) {
mUISceneNode->invalidate( nullptr ); for ( auto& entry : mWindows )
entry.ui->invalidate( nullptr );
}
mWindow->runMainLoop( [this]() { mRunning = true;
mWindow->getInput()->update(); #if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
SceneManager::instance()->update(); emscripten_set_main_loop_arg(
if ( mUISceneNode->invalidated() ) { []( void* application ) { static_cast<UIApplication*>( application )->tick(); }, this,
mWindow->clear(); mWindow->getFrameRateLimit(), 1 );
#else
SceneManager::instance()->draw(); while ( mRunning )
tick();
mWindow->display();
} else {
#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN
mWindow->getInput()->waitEvent( Milliseconds( mWindow->hasFocus() ? 16 : 100 ) );
#endif #endif
}
} );
return mDidRun ? EXIT_SUCCESS : EXIT_FAILURE; 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<Int64>( 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<std::string> basePath, UIApplication::Settings::Settings( std::optional<std::string> basePath,
std::optional<Float> pixelDensity, bool loadBaseResources, std::optional<Float> pixelDensity, bool loadBaseResources,
Font* baseFont, std::optional<std::string> baseStyleSheetPath, Font* baseFont, std::optional<std::string> baseStyleSheetPath,
+29
View File
@@ -10,7 +10,9 @@
#include <eepp/system/log.hpp> #include <eepp/system/log.hpp>
#include <eepp/ui/models/filesystemmodel.hpp> #include <eepp/ui/models/filesystemmodel.hpp>
#include <eepp/ui/models/sortingproxymodel.hpp> #include <eepp/ui/models/sortingproxymodel.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uifiledialog.hpp> #include <eepp/ui/uifiledialog.hpp>
#include <eepp/window/runtime.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -46,6 +48,33 @@ UIFileDialog* UIFileDialog::New( Uint32 dialogFlags, const std::string& defaultF
return eeNew( UIFileDialog, ( dialogFlags, defaultFilePattern, defaultDirectory ) ); 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<Uint32>( PixelDensity::dpToPxI( FDLG_MIN_WIDTH ) ) );
dialogWindowSettings.Height =
eemax( dialogWindowSettings.Height,
static_cast<Uint32>( PixelDensity::dpToPxI( FDLG_MIN_HEIGHT ) ) );
}
#endif
return static_cast<UIFileDialog*>( createInApplicationWindow(
application, dialogWindowSettings,
[dialogFlags, defaultFilePattern, defaultDirectory] {
return New( dialogFlags, defaultFilePattern, defaultDirectory );
},
contextSettings, modal, position ) );
}
UIFileDialog::UIFileDialog( Uint32 dialogFlags, const std::string& defaultFilePattern, UIFileDialog::UIFileDialog( Uint32 dialogFlags, const std::string& defaultFilePattern,
const std::string& defaultDirectory ) : const std::string& defaultDirectory ) :
UIWindow(), UIWindow(),
+7
View File
@@ -35,6 +35,13 @@ UIIconTheme* UIIconThemeManager::getCurrentTheme() const {
return mCurrentTheme; 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 ) { UIIconThemeManager* UIIconThemeManager::setCurrentTheme( UIIconThemePtr currentTheme ) {
if ( currentTheme.get() != mCurrentTheme && currentTheme.get() != mFallbackTheme ) { if ( currentTheme.get() != mCurrentTheme && currentTheme.get() != mFallbackTheme ) {
mCurrentTheme = currentTheme.get(); mCurrentTheme = currentTheme.get();
+12
View File
@@ -1,3 +1,4 @@
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uicombobox.hpp> #include <eepp/ui/uicombobox.hpp>
#include <eepp/ui/uidropdownlist.hpp> #include <eepp/ui/uidropdownlist.hpp>
#include <eepp/ui/uilayout.hpp> #include <eepp/ui/uilayout.hpp>
@@ -18,6 +19,17 @@ UIMessageBox* UIMessageBox::New( const Type& type, const String& message,
return eeNew( UIMessageBox, ( type, message, windowFlags ) ); 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<UIMessageBox*>( 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 ) : UIMessageBox::UIMessageBox( const Type& type, const String& message, const Uint32& windowFlags ) :
UIWindow(), mMsgBoxType( type ), mCloseShortcut( KEY_UNKNOWN ) { UIWindow(), mMsgBoxType( type ), mCloseShortcut( KEY_UNKNOWN ) {
mVisible = false; mVisible = false;
+1 -1
View File
@@ -44,7 +44,7 @@ UINode::UINode() :
mUISceneNode( SceneManager::instance()->getUISceneNode() ) { mUISceneNode( SceneManager::instance()->getUISceneNode() ) {
mNodeFlags |= NODE_FLAG_UINODE | NODE_FLAG_OVER_FIND_ALLOWED; mNodeFlags |= NODE_FLAG_UINODE | NODE_FLAG_OVER_FIND_ALLOWED;
if ( NULL != mUISceneNode ) if ( NULL != mUISceneNode && NULL != mUISceneNode->getRoot() )
setParent( (Node*)mUISceneNode->getRoot() ); setParent( (Node*)mUISceneNode->getRoot() );
} }
+26 -10
View File
@@ -133,6 +133,28 @@ UISceneNode* UISceneNode::New( EE::Window::Window* window, bool importDefaultRes
return eeNew( UISceneNode, ( window, importDefaultResources ) ); 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 ) : UISceneNode::UISceneNode( EE::Window::Window* window, bool importDefaultResources ) :
SceneNode( window ), SceneNode( window ),
mRoot( NULL ), mRoot( NULL ),
@@ -146,6 +168,7 @@ UISceneNode::UISceneNode( EE::Window::Window* window, bool importDefaultResource
mDrawableResolver( *this ), mDrawableResolver( *this ),
mWebResourceCache( WebResourceCache::New() ), mWebResourceCache( WebResourceCache::New() ),
mKeyBindings( mWindow->getInput() ) { mKeyBindings( mWindow->getInput() ) {
auto context = makeCurrent();
if ( mImportDefaultResources ) if ( mImportDefaultResources )
mResourceScope->importCatalog( defaultResourceScope().getLocalCatalog() ); mResourceScope->importCatalog( defaultResourceScope().getLocalCatalog() );
@@ -547,7 +570,7 @@ bool UISceneNode::windowExists( UIWindow* win ) {
} }
SmallVector<UIWidget*, 8> UISceneNode::loadNode( pugi::xml_node node, Node* parent, SmallVector<UIWidget*, 8> UISceneNode::loadNode( pugi::xml_node node, Node* parent,
const Uint32& marker ) { const Uint32& marker ) {
Uint32 oldMarker = mCurrentMarker; Uint32 oldMarker = mCurrentMarker;
mCurrentMarker = marker; mCurrentMarker = marker;
@@ -641,8 +664,7 @@ SmallVector<UIWidget*, 8> UISceneNode::loadNode( pugi::xml_node node, Node* pare
UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const Uint32& marker ) { UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const Uint32& marker ) {
Clock clock; Clock clock;
UISceneNode* prevUISceneNode = SceneManager::instance()->getUISceneNode(); auto context = makeCurrent();
SceneManager::instance()->setCurrentUISceneNode( this );
std::string id( node.attribute( "id" ).as_string() ); std::string id( node.attribute( "id" ).as_string() );
mIsLoading = true; mIsLoading = true;
Clock innerClock; Clock innerClock;
@@ -684,8 +706,6 @@ UIWidget* UISceneNode::loadLayoutNodes( pugi::xml_node node, Node* parent, const
mIsLoading = false; mIsLoading = false;
SceneManager::instance()->setCurrentUISceneNode( prevUISceneNode );
if ( mVerbose ) { if ( mVerbose ) {
Log::debug( "UISceneNode::loadLayoutNodes loaded in: %.2f ms", Log::debug( "UISceneNode::loadLayoutNodes loaded in: %.2f ms",
clock.getElapsedTime().asMilliseconds() ); clock.getElapsedTime().asMilliseconds() );
@@ -1174,7 +1194,7 @@ void UISceneNode::flushDirtyStyleAndLayout() {
} }
void UISceneNode::update( const Time& elapsed ) { void UISceneNode::update( const Time& elapsed ) {
UISceneNode* uiSceneNode = SceneManager::instance()->getUISceneNode(); auto context = makeCurrent();
drainAsyncResourceMainThreadQueue(); drainAsyncResourceMainThreadQueue();
@@ -1182,8 +1202,6 @@ void UISceneNode::update( const Time& elapsed ) {
mClock.restart(); mClock.restart();
} }
SceneManager::instance()->setCurrentUISceneNode( this );
updateDirtyStyles(); updateDirtyStyles();
updateDirtyStyleStates(); updateDirtyStyleStates();
updateDirtyLayouts(); updateDirtyLayouts();
@@ -1221,8 +1239,6 @@ void UISceneNode::update( const Time& elapsed ) {
invalidationDepth--; invalidationDepth--;
} }
SceneManager::instance()->setCurrentUISceneNode( uiSceneNode );
if ( mFirstUpdate && mVerbose ) { if ( mFirstUpdate && mVerbose ) {
mFirstUpdate = false; mFirstUpdate = false;
Log::debug( "UISceneNode::update first update took: %.2f ms", Log::debug( "UISceneNode::update first update took: %.2f ms",
+150
View File
@@ -7,7 +7,9 @@
#include <eepp/scene/actions/actions.hpp> #include <eepp/scene/actions/actions.hpp>
#include <eepp/scene/scenemanager.hpp> #include <eepp/scene/scenemanager.hpp>
#include <eepp/scene/scenenode.hpp> #include <eepp/scene/scenenode.hpp>
#include <eepp/system/log.hpp>
#include <eepp/ui/css/propertydefinition.hpp> #include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uieventdispatcher.hpp> #include <eepp/ui/uieventdispatcher.hpp>
#include <eepp/ui/uilinearlayout.hpp> #include <eepp/ui/uilinearlayout.hpp>
#include <eepp/ui/uipushbutton.hpp> #include <eepp/ui/uipushbutton.hpp>
@@ -17,6 +19,9 @@
#include <eepp/ui/uitextview.hpp> #include <eepp/ui/uitextview.hpp>
#include <eepp/ui/uithememanager.hpp> #include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uiwindow.hpp> #include <eepp/ui/uiwindow.hpp>
#include <eepp/window/displaymanager.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/runtime.hpp>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
@@ -46,6 +51,134 @@ UIWindow* UIWindow::NewRelLay() {
return eeNew( UIWindow, ( RELATIVE_LAYOUT ) ); 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<UIWindow*()>& 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 ) : UIWindow( type, StyleConfig() ) {}
UIWindow::UIWindow( UIWindow::WindowBaseContainerType type, const StyleConfig& windowStyleConfig ) : UIWindow::UIWindow( UIWindow::WindowBaseContainerType type, const StyleConfig& windowStyleConfig ) :
@@ -322,6 +455,13 @@ void UIWindow::updateWinFlags() {
if ( isModal() && NULL == mModalNode ) { if ( isModal() && NULL == mModalNode ) {
createModalNode(); 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 ) { if ( needsUpdate ) {
@@ -452,6 +592,7 @@ bool UIWindow::isType( const Uint32& type ) const {
void UIWindow::closeWindow() { void UIWindow::closeWindow() {
if ( mClosing ) if ( mClosing )
return; return;
mClosing = true;
if ( NULL != mButtonClose ) if ( NULL != mButtonClose )
mButtonClose->setEnabled( false ); mButtonClose->setEnabled( false );
@@ -473,8 +614,17 @@ void UIWindow::closeWindow() {
} }
void UIWindow::close() { void UIWindow::close() {
mClosing = true;
UIWidget::close(); 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 ) { if ( NULL != mModalNode ) {
mModalNode->setEnabled( false ); mModalNode->setEnabled( false );
mModalNode->setVisible( false ); mModalNode->setVisible( false );
@@ -35,7 +35,7 @@ std::string DisplaySDL2::getName() const {
Rect DisplaySDL2::getBounds() const { Rect DisplaySDL2::getBounds() const {
SDL_Rect r; SDL_Rect r;
if ( SDL_GetDisplayBounds( index, &r ) == 0 ) 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(); return Rect();
} }
@@ -114,7 +114,7 @@ DisplayMode DisplaySDL2::getClosestDisplayMode( DisplayMode wantedMode ) const {
Rect DisplaySDL2::getUsableBounds() const { Rect DisplaySDL2::getUsableBounds() const {
SDL_Rect r; SDL_Rect r;
if ( SDL_GetDisplayUsableBounds( index, &r ) == 0 ) 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(); return Rect();
} }
+13 -28
View File
@@ -21,14 +21,8 @@ InputSDL::InputSDL( EE::Window::Window* window ) :
InputSDL::~InputSDL() {} InputSDL::~InputSDL() {}
void InputSDL::update() { void InputSDL::update() {
beginInputFrame();
SDL_Event SDLEvent; SDL_Event SDLEvent;
cleanStates();
++mEventsSentId;
if ( mEventsSentId == std::numeric_limits<Uint64>::max() )
mEventsSentId = 0;
drainQueuedEvents();
if ( !mQueuedEvents.empty() ) { if ( !mQueuedEvents.empty() ) {
for ( const auto& prevEvent : mQueuedEvents ) for ( const auto& prevEvent : mQueuedEvents )
@@ -37,9 +31,7 @@ void InputSDL::update() {
} }
while ( SDL_PollEvent( &SDLEvent ) ) while ( SDL_PollEvent( &SDLEvent ) )
sendEvent( SDLEvent ); sendEvent( SDLEvent );
InputEvent endProcessingEvent; endInputFrame();
endProcessingEvent.Type = InputEvent::EventsSent;
processEvent( &endProcessingEvent );
} }
void InputSDL::waitEvent( const Time& timeout ) { void InputSDL::waitEvent( const Time& timeout ) {
@@ -244,14 +236,16 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
} }
case SDL_TEXTINPUT: { case SDL_TEXTINPUT: {
String txt = String::fromUtf8( std::string_view{ SDLEvent.text.text } ); String txt = String::fromUtf8( std::string_view{ SDLEvent.text.text } );
if ( txt.empty() )
break;
event.Type = InputEvent::TextInput; event.Type = InputEvent::TextInput;
event.text.timestamp = SDLEvent.text.timestamp; event.text.timestamp = SDLEvent.text.timestamp;
event.WinID = SDLEvent.text.windowID; event.WinID = SDLEvent.text.windowID;
for ( size_t i = 0; i < txt.size() - 1; i++ ) { for ( const auto& character : txt ) {
event.text.text = txt[i]; event.text.text = character;
processEvent( &event ); processEventForWindow( &event );
} }
event.text.text = txt[txt.size() - 1]; event.Type = InputEvent::NoEvent;
break; break;
} }
case SDL_TEXTEDITING: { case SDL_TEXTEDITING: {
@@ -357,11 +351,11 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
event.Type = InputEvent::MouseButtonDown; event.Type = InputEvent::MouseButtonDown;
event.button.state = 1; event.button.state = 1;
processEvent( &event ); processEventForWindow( &event );
event.Type = InputEvent::MouseButtonUp; event.Type = InputEvent::MouseButtonUp;
event.button.state = 0; event.button.state = 0;
processEvent( &event ); processEventForWindow( &event );
event.Type = InputEvent::MouseWheel; event.Type = InputEvent::MouseWheel;
event.wheel.which = SDLEvent.wheel.which; 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.x = SDLEvent.wheel.x;
event.wheel.y = SDLEvent.wheel.y; event.wheel.y = SDLEvent.wheel.y;
#endif #endif
processEvent( &event ); processEventForWindow( &event );
break; break;
} }
case SDL_FINGERMOTION: { case SDL_FINGERMOTION: {
@@ -496,17 +490,8 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
} }
} }
EE::Window::Window* win; if ( InputEvent::NoEvent != event.Type )
processEventForWindow( &event );
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::FileDropped == event.Type || InputEvent::TextDropped == event.Type ) if ( InputEvent::FileDropped == event.Type || InputEvent::TextDropped == event.Type )
SDL_free( SDLEvent.drop.file ); SDL_free( SDLEvent.drop.file );
+13 -1
View File
@@ -316,7 +316,8 @@ void WindowSDL::makeCurrent() {
} }
void WindowSDL::close() { void WindowSDL::close() {
destroySDLResources(); if ( !getDeferNativeResourceDestructionOnClose() )
destroySDLResources();
Window::close(); Window::close();
} }
@@ -540,6 +541,17 @@ void WindowSDL::setSize( Uint32 width, Uint32 height, bool windowed ) {
sendVideoResizeCb(); sendVideoResizeCb();
} }
void WindowSDL::setMinimumSize( Uint32 width, Uint32 height ) {
SDL_SetWindowMinimumSize( mSDLWindow, static_cast<int>( width ), static_cast<int>( height ) );
}
bool WindowSDL::setModalFor( Window* parent ) {
auto* parentWindow = dynamic_cast<WindowSDL*>( parent );
if ( nullptr == parentWindow )
return false;
return 0 == SDL_SetWindowModalFor( mSDLWindow, parentWindow->mSDLWindow );
}
void WindowSDL::swapBuffers() { void WindowSDL::swapBuffers() {
SDL_GL_SwapWindow( mSDLWindow ); SDL_GL_SwapWindow( mSDLWindow );
} }
@@ -51,6 +51,12 @@ class EE_API WindowSDL : public Window {
void setSize( Uint32 width, Uint32 height, bool windowed ); 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<DisplayMode> getDisplayModes() const; std::vector<DisplayMode> getDisplayModes() const;
void setGamma( Float Red, Float Green, Float Blue ); void setGamma( Float Red, Float Green, Float Blue );
@@ -17,14 +17,14 @@ std::string DisplaySDL3::getName() const {
Rect DisplaySDL3::getBounds() const { Rect DisplaySDL3::getBounds() const {
SDL_Rect r{}; SDL_Rect r{};
if ( mDisplayId && SDL_GetDisplayBounds( mDisplayId, &r ) == 0 ) 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(); return Rect();
} }
Rect DisplaySDL3::getUsableBounds() const { Rect DisplaySDL3::getUsableBounds() const {
SDL_Rect r{}; SDL_Rect r{};
if ( mDisplayId && SDL_GetDisplayUsableBounds( mDisplayId, &r ) == 0 ) 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(); return Rect();
} }
+8 -25
View File
@@ -17,14 +17,8 @@ InputSDL::InputSDL( Window* window ) :
InputSDL::~InputSDL() {} InputSDL::~InputSDL() {}
void InputSDL::update() { void InputSDL::update() {
beginInputFrame();
SDL_Event SDLEvent; SDL_Event SDLEvent;
cleanStates();
++mEventsSentId;
if ( mEventsSentId == std::numeric_limits<Uint64>::max() )
mEventsSentId = 0;
drainQueuedEvents();
if ( !mQueuedEvents.empty() ) { if ( !mQueuedEvents.empty() ) {
for ( const auto& prevEvent : mQueuedEvents ) for ( const auto& prevEvent : mQueuedEvents )
@@ -33,9 +27,7 @@ void InputSDL::update() {
} }
while ( SDL_PollEvent( &SDLEvent ) ) while ( SDL_PollEvent( &SDLEvent ) )
sendEvent( SDLEvent ); sendEvent( SDLEvent );
InputEvent endProcessingEvent; endInputFrame();
endProcessingEvent.Type = InputEvent::EventsSent;
processEvent( &endProcessingEvent );
} }
void InputSDL::waitEvent( const Time& timeout ) { void InputSDL::waitEvent( const Time& timeout ) {
@@ -237,7 +229,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
event.WinID = SDLEvent.text.windowID; event.WinID = SDLEvent.text.windowID;
for ( const auto& character : txt ) { for ( const auto& character : txt ) {
event.text.text = character; event.text.text = character;
processEvent( &event ); processEventForWindow( &event );
} }
event.Type = InputEvent::NoEvent; // Already processed all characters event.Type = InputEvent::NoEvent; // Already processed all characters
break; break;
@@ -334,11 +326,11 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
event.Type = InputEvent::MouseButtonDown; event.Type = InputEvent::MouseButtonDown;
event.button.state = 1; event.button.state = 1;
processEvent( &event ); processEventForWindow( &event );
event.Type = InputEvent::MouseButtonUp; event.Type = InputEvent::MouseButtonUp;
event.button.state = 0; event.button.state = 0;
processEvent( &event ); processEventForWindow( &event );
event.Type = InputEvent::MouseWheel; event.Type = InputEvent::MouseWheel;
event.wheel.which = SDLEvent.wheel.windowID; event.wheel.which = SDLEvent.wheel.windowID;
@@ -347,7 +339,7 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
: InputEvent::WheelEvent::Flipped; : InputEvent::WheelEvent::Flipped;
event.wheel.x = SDLEvent.wheel.x; event.wheel.x = SDLEvent.wheel.x;
event.wheel.y = SDLEvent.wheel.y; event.wheel.y = SDLEvent.wheel.y;
processEvent( &event ); processEventForWindow( &event );
break; break;
} }
case SDL_EVENT_JOYSTICK_AXIS_MOTION: { 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 )
processEventForWindow( &event );
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 );
}
}
// In SDL3, drop event data is managed by SDL, do not free // In SDL3, drop event data is managed by SDL, do not free
} }
+18 -1
View File
@@ -291,7 +291,8 @@ void WindowSDL::makeCurrent() {
} }
void WindowSDL::close() { void WindowSDL::close() {
destroySDLResources(); if ( !getDeferNativeResourceDestructionOnClose() )
destroySDLResources();
Window::close(); Window::close();
} }
@@ -513,6 +514,22 @@ void WindowSDL::setSize( Uint32 width, Uint32 height, bool windowed ) {
sendVideoResizeCb(); sendVideoResizeCb();
} }
void WindowSDL::setMinimumSize( Uint32 width, Uint32 height ) {
SDL_SetWindowMinimumSize( mSDLWindow, static_cast<int>( width ), static_cast<int>( height ) );
}
bool WindowSDL::setModalFor( Window* parent ) {
auto* parentWindow = dynamic_cast<WindowSDL*>( 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() { void WindowSDL::swapBuffers() {
SDL_GL_SwapWindow( mSDLWindow ); SDL_GL_SwapWindow( mSDLWindow );
} }
@@ -45,6 +45,12 @@ class EE_API WindowSDL : public Window {
void setSize( Uint32 width, Uint32 height, bool windowed ); 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<DisplayMode> getDisplayModes() const; std::vector<DisplayMode> getDisplayModes() const;
void setGamma( Float Red, Float Green, Float Blue ); void setGamma( Float Red, Float Green, Float Blue );
+48 -10
View File
@@ -30,6 +30,7 @@
#include <eepp/window/backend/SDL3/platformhelpersdl3.hpp> #include <eepp/window/backend/SDL3/platformhelpersdl3.hpp>
#endif #endif
#include <eepp/window/engine.hpp> #include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp>
#include <eepp/window/runtime.hpp> #include <eepp/window/runtime.hpp>
#include <eepp/window/terminal/terminalruntime.hpp> #include <eepp/window/terminal/terminalruntime.hpp>
@@ -54,6 +55,21 @@ using namespace EE::Graphics;
namespace EE { namespace Window { 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 { namespace {
void configureRuntimeVideoDriver() { void configureRuntimeVideoDriver() {
@@ -245,6 +261,7 @@ EE::Window::Window* Engine::createDefaultWindow( const WindowSettings& Settings,
} }
EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettings Context ) { EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettings Context ) {
const bool firstWindow = mWindows.empty();
if ( Runtime::mode() == RuntimeMode::Terminal && !mWindows.empty() ) { if ( Runtime::mode() == RuntimeMode::Terminal && !mWindows.empty() ) {
Log::error( "Terminal runtime currently supports one top-level Window" ); Log::error( "Terminal runtime currently supports one top-level Window" );
return nullptr; return nullptr;
@@ -287,7 +304,7 @@ EE::Window::Window* Engine::createWindow( WindowSettings Settings, ContextSettin
mWindows.insert( { mWindow->getWindowID(), mWindow } ); mWindows.insert( { mWindow->getWindowID(), mWindow } );
if ( Settings.PixelDensity > 0 ) if ( firstWindow && Settings.PixelDensity > 0 )
PixelDensity::setPixelDensity( Settings.PixelDensity ); PixelDensity::setPixelDensity( Settings.PixelDensity );
return window; return window;
@@ -297,11 +314,9 @@ void Engine::destroyWindow( EE::Window::Window* window ) {
mWindows.erase( window->getWindowID() ); mWindows.erase( window->getWindowID() );
if ( window == mWindow ) { if ( window == mWindow ) {
if ( mWindows.size() > 0 ) { mWindow = NULL;
mWindow = mWindows.begin()->second; if ( !mWindows.empty() )
} else { setCurrentWindow( mWindows.begin()->second );
mWindow = NULL;
}
} }
eeSAFE_DELETE( window ); eeSAFE_DELETE( window );
@@ -329,18 +344,41 @@ EE::Window::Window* Engine::getWindowID( const Uint32& winID ) {
return nullptr; 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 { EE::Window::Window* Engine::getCurrentWindow() const {
return mWindow; return mWindow;
} }
void Engine::setCurrentWindow( EE::Window::Window* window ) { void Engine::setCurrentWindow( EE::Window::Window* window ) {
if ( NULL != window && window != mWindow ) { if ( window != mWindow ) {
mWindow = window; mWindow = window;
if ( mWindow ) {
mWindow->setCurrent(); 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 { Uint32 Engine::getWindowCount() const {
return mWindows.size(); return mWindows.size();
} }
@@ -468,7 +506,7 @@ void Engine::disableSharedGLContext() {
} }
bool Engine::isSharedGLContextEnabled() { bool Engine::isSharedGLContextEnabled() {
return mSharedGLContext && mWindow->isThreadedGLContext(); return mSharedGLContext && mWindow && mWindow->isThreadedGLContext();
} }
bool Engine::isThreaded() { bool Engine::isThreaded() {
+22
View File
@@ -31,6 +31,18 @@ Input::~Input() {
eeSAFE_DELETE( mJoystickManager ); eeSAFE_DELETE( mJoystickManager );
} }
void Input::beginInputFrame() {
cleanStates();
++mEventsSentId;
drainQueuedEvents();
}
void Input::endInputFrame() {
InputEvent endProcessingEvent;
endProcessingEvent.Type = InputEvent::EventsSent;
processEvent( &endProcessingEvent );
}
void Input::cleanStates() { void Input::cleanStates() {
memset( mScancodeUp, 0, EE_KEYS_SPACE ); memset( mScancodeUp, 0, EE_KEYS_SPACE );
@@ -229,6 +241,16 @@ void Input::processEvent( InputEvent* Event ) {
sendEvent( 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 ) { bool Input::enqueueEvent( InputEvent event ) {
static constexpr size_t MaxInjectedEvents = 4096; static constexpr size_t MaxInjectedEvents = 4096;
std::lock_guard<std::mutex> lock( mInjectedEventsMutex ); std::lock_guard<std::mutex> lock( mInjectedEventsMutex );
+20 -1
View File
@@ -75,6 +75,12 @@ void Window::setSize( Uint32 Width, Uint32 Height ) {
setSize( Width, Height, isWindowed() ); setSize( Width, Height, isWindowed() );
} }
void Window::setMinimumSize( Uint32, Uint32 ) {}
bool Window::setModalFor( Window* ) {
return false;
}
bool Window::isWindowed() const { bool Window::isWindowed() const {
return 0 != !( mWindow.WindowConfig.Style & WindowStyle::Fullscreen ); return 0 != !( mWindow.WindowConfig.Style & WindowStyle::Fullscreen );
} }
@@ -390,6 +396,14 @@ void Window::close() {
mWindow.Created = false; mWindow.Created = false;
} }
void Window::setDeferNativeResourceDestructionOnClose( bool defer ) {
mDeferNativeResourceDestructionOnClose = defer;
}
bool Window::getDeferNativeResourceDestructionOnClose() const {
return mDeferNativeResourceDestructionOnClose;
}
void Window::setFrameRateLimit( Uint32 FrameRateLimit ) { void Window::setFrameRateLimit( Uint32 FrameRateLimit ) {
if ( FrameRateLimit == ContextSettings::FrameRateLimitScreenRefreshRate ) { if ( FrameRateLimit == ContextSettings::FrameRateLimitScreenRefreshRate ) {
Display* currentDisplay = nullptr; Display* currentDisplay = nullptr;
@@ -504,6 +518,10 @@ void Window::clear() {
} }
void Window::display( bool clear ) { void Window::display( bool clear ) {
display( clear, true );
}
void Window::display( bool clear, bool limitFrameRate ) {
GlobalBatchRenderer::instance()->draw(); GlobalBatchRenderer::instance()->draw();
if ( mFramePresenter ) if ( mFramePresenter )
mFramePresenter->present( *this ); mFramePresenter->present( *this );
@@ -526,7 +544,8 @@ void Window::display( bool clear ) {
updateElapsedTime(); updateElapsedTime();
limitFps(); if ( limitFrameRate )
limitFps();
calculateFps(); calculateFps();
@@ -0,0 +1,34 @@
#include <eepp/ee.hpp>
#include <eepp/ui/uifiledialog.hpp>
#include <eepp/ui/uimessagebox.hpp>
EE_MAIN_FUNC int main( int, char** ) {
UIApplication app( { 640, 480, "eepp - UIApplication Multi-window" } );
auto* layout = app.getUI()->loadLayoutFromString( R"xml(
<RelativeLayout layout_width="match_parent" layout_height="match_parent">
<PushButton id="open_file" layout_width="wrap_content" layout_height="wrap_content"
layout_gravity="center" text="Open File..." />
</RelativeLayout>
)xml" );
auto* openFile = layout->querySelector<UIPushButton>( "#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<UIFileDialog>()->getFullPath();
UIMessageBox::NewInApplicationWindow( app, { 520, 100, "File Opened" },
UIMessageBox::OK,
String::format( "File %s opened", path ) );
} );
} );
return app.run();
}
+232 -3
View File
@@ -2,19 +2,25 @@
#include <eepp/graphics/fontfamily.hpp> #include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp> #include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/texturefactory.hpp>
#include <eepp/scene/eventdispatcher.hpp> #include <eepp/scene/eventdispatcher.hpp>
#include <eepp/scene/scenemanager.hpp> #include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp> #include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp> #include <eepp/system/sys.hpp>
#include <eepp/ui/uiapplication.hpp>
#include <eepp/ui/uifiledialog.hpp> #include <eepp/ui/uifiledialog.hpp>
#include <eepp/ui/uiiconthememanager.hpp>
#include <eepp/ui/uimessagebox.hpp>
#include <eepp/ui/uiroot.hpp> #include <eepp/ui/uiroot.hpp>
#include <eepp/ui/uiscenenode.hpp> #include <eepp/ui/uiscenenode.hpp>
#include <eepp/ui/uitextinput.hpp>
#include <eepp/ui/uithememanager.hpp> #include <eepp/ui/uithememanager.hpp>
#include <eepp/ui/uiwidget.hpp> #include <eepp/ui/uiwidget.hpp>
#include <eepp/ui/uiwindow.hpp> #include <eepp/ui/uiwindow.hpp>
#include <eepp/window/cursor.hpp> #include <eepp/window/cursor.hpp>
#include <eepp/window/engine.hpp> #include <eepp/window/engine.hpp>
#include <eepp/window/input.hpp> #include <eepp/window/input.hpp>
#include <eepp/window/runtime.hpp>
using namespace EE; using namespace EE;
using namespace EE::Graphics; using namespace EE::Graphics;
@@ -29,6 +35,231 @@ UTEST( UISceneNode, CssPointerCursorUsesHandCursor ) {
EXPECT_STREQ( Cursor::toName( Cursor::Arrow ), "arrow" ); 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 ) { UTEST( Node, DescendantWorldBoundsRefreshAfterDirtyAncestorMoves ) {
Node* parent = Node::New(); Node* parent = Node::New();
Node* child = Node::New(); Node* child = Node::New();
@@ -79,9 +310,7 @@ class InvalidationTestSceneNode : public UISceneNode {
size_t processedStyleRootCount() const { return mDirtyStylesSnapshot.size(); } size_t processedStyleRootCount() const { return mDirtyStylesSnapshot.size(); }
UIWidget* processedStyleRoot( size_t index ) const { UIWidget* processedStyleRoot( size_t index ) const { return mDirtyStylesSnapshot[index].first; }
return mDirtyStylesSnapshot[index].first;
}
bool processedStyleRootDisablesAnimations( size_t index ) const { bool processedStyleRootDisablesAnimations( size_t index ) const {
return mDirtyStylesSnapshot[index].second; return mDirtyStylesSnapshot[index].second;