From 82510405557b2b5b10c77d6054673397bc7ecae5 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Fri, 6 Jun 2025 15:42:40 +0200 Subject: [PATCH 01/51] Improve lifetime tracking over messages --- .../memory/juce_ReferenceCountedObject.h | 42 ++++++++ .../messages/juce_ApplicationBase.cpp | 5 +- .../messages/juce_MessageManager.cpp | 11 ++ .../messages/juce_MessageManager.h | 16 ++- .../native/juce_MessageManager_ios.mm | 3 + .../native/juce_MessageManager_mac.mm | 4 + .../native/juce_Messaging_emscripten.cpp | 3 + modules/yup_graphics/fonts/yup_StyledText.cpp | 9 ++ modules/yup_graphics/fonts/yup_StyledText.h | 6 ++ .../yup_graphics/primitives/yup_Rectangle.h | 16 +++ modules/yup_gui/component/yup_Component.cpp | 79 ++++++++++++-- modules/yup_gui/component/yup_Component.h | 27 ++++- .../yup_gui/component/yup_ComponentNative.h | 14 ++- modules/yup_gui/desktop/yup_Desktop.cpp | 100 ++++++++++++++++++ modules/yup_gui/desktop/yup_Desktop.h | 24 +++++ modules/yup_gui/mouse/yup_MouseEvent.cpp | 9 ++ modules/yup_gui/mouse/yup_MouseEvent.h | 9 ++ modules/yup_gui/native/yup_Windowing_sdl2.cpp | 49 ++++++--- modules/yup_gui/native/yup_Windowing_sdl2.h | 3 + 19 files changed, 393 insertions(+), 36 deletions(-) diff --git a/modules/juce_core/memory/juce_ReferenceCountedObject.h b/modules/juce_core/memory/juce_ReferenceCountedObject.h index 03b94fcd1..6b39a372c 100644 --- a/modules/juce_core/memory/juce_ReferenceCountedObject.h +++ b/modules/juce_core/memory/juce_ReferenceCountedObject.h @@ -238,6 +238,31 @@ class JUCE_API SingleThreadedReferenceCountedObject friend struct ContainerDeletePolicy; }; +//============================================================================== +/** + This is a type that can be used to indicate that a ReferenceCountedObjectPtr is adopting an object. + + This is used to prevent the object from being deleted when the ReferenceCountedObjectPtr is destroyed. + Normally you won't need to use this type, but it can be useful in some cases, especially when a reference + counted object is being grabbed by multiple pointers before it finished its initialization. + + @tags{Core} + */ +struct ReferenceCountedObjectAdoptType {}; + +/** + This is a constant that can be used to indicate that a ReferenceCountedObjectPtr is adopting an object. + + This is used to prevent the object from being deleted when the ReferenceCountedObjectPtr is destroyed. + Normally you won't need to use this constant, but it can be useful in some cases, especially when a reference + counted object is being grabbed by multiple pointers before it finished its initialization. + + @see ReferenceCountedObjectAdoptType + + @tags{Core} + */ +constexpr inline ReferenceCountedObjectAdoptType ReferenceCountedObjectAdopt; + //============================================================================== /** A smart-pointer class which points to a reference-counted object. @@ -267,6 +292,7 @@ template class ReferenceCountedObjectPtr { public: + //============================================================================== /** The class being referenced by this pointer. */ using ReferencedType = ObjectType; @@ -286,6 +312,14 @@ class ReferenceCountedObjectPtr incIfNotNull (refCountedObject); } + /** Creates a pointer to an object. + This will NOT increment the object's reference-count. + */ + ReferenceCountedObjectPtr (ReferenceCountedObjectAdoptType, ReferencedType* refCountedObject) noexcept + : referencedObject (refCountedObject) + { + } + /** Creates a pointer to an object. This will increment the object's reference-count. */ @@ -295,6 +329,14 @@ class ReferenceCountedObjectPtr refCountedObject.incReferenceCount(); } + /** Creates a pointer to an object. + This will NOT increment the object's reference-count. + */ + ReferenceCountedObjectPtr (ReferenceCountedObjectAdoptType, ReferencedType& refCountedObject) noexcept + : referencedObject (&refCountedObject) + { + } + /** Copies another pointer. This will increment the object's reference-count. */ diff --git a/modules/juce_events/messages/juce_ApplicationBase.cpp b/modules/juce_events/messages/juce_ApplicationBase.cpp index c403e5109..519f240ac 100644 --- a/modules/juce_events/messages/juce_ApplicationBase.cpp +++ b/modules/juce_events/messages/juce_ApplicationBase.cpp @@ -83,7 +83,10 @@ void JUCEApplicationBase::appWillTerminateByForce() void JUCEApplicationBase::quit() { - MessageManager::getInstance()->stopDispatchLoop(); + MessageManager::callAsync([] + { + MessageManager::getInstance()->stopDispatchLoop(); + }); } void JUCEApplicationBase::sendUnhandledException (const std::exception* const e, diff --git a/modules/juce_events/messages/juce_MessageManager.cpp b/modules/juce_events/messages/juce_MessageManager.cpp index cd5b7f78b..0ae644b69 100644 --- a/modules/juce_events/messages/juce_MessageManager.cpp +++ b/modules/juce_events/messages/juce_MessageManager.cpp @@ -104,6 +104,14 @@ void MessageManager::registerEventLoopCallback (std::function loopCallba loopCallback = std::move (loopCallbackToSet); } +//============================================================================== +void MessageManager::registerShutdownCallback (std::function shutdownCallbackToAdd) +{ + jassert (isThisTheMessageThread()); // must only be called by the message thread + + shutdownCallbacks.push_back (std::move (shutdownCallbackToAdd)); +} + //============================================================================== #if ! (JUCE_MAC || JUCE_IOS || JUCE_WASM) // implemented in platform-specific code (juce_Messaging_linux.cpp, juce_Messaging_android.cpp and juce_Messaging_windows.cpp) @@ -138,6 +146,9 @@ void MessageManager::runDispatchLoop() } JUCE_CATCH_EXCEPTION } + + for (const auto& func : shutdownCallbacks) + func(); } void MessageManager::stopDispatchLoop() diff --git a/modules/juce_events/messages/juce_MessageManager.h b/modules/juce_events/messages/juce_MessageManager.h index d9d795e70..a28ce0d0f 100644 --- a/modules/juce_events/messages/juce_MessageManager.h +++ b/modules/juce_events/messages/juce_MessageManager.h @@ -103,10 +103,6 @@ class JUCE_API MessageManager final bool runDispatchLoopUntil (int millisecondsToRunFor); #endif - //============================================================================== - /** Register an event loop callback that will be processed as the event dispatch loop. */ - void registerEventLoopCallback (std::function loopCallbackToSet); - //============================================================================== /** Asynchronously invokes a function or C++11 lambda on the message thread. @@ -335,6 +331,17 @@ class JUCE_API MessageManager final mutable bool abortWait = false, acquired = false; }; + //============================================================================== + /** @internal Register an event loop callback that will be processed as the event dispatch loop. + + Only one event loop callback can be registered at any time. + */ + void registerEventLoopCallback (std::function loopCallbackToSet); + + //============================================================================== + /** @internal Register a shutdown callback. */ + void registerShutdownCallback (std::function shutdownCallbackToAdd); + //============================================================================== #ifndef DOXYGEN // Internal methods - do not use! @@ -358,6 +365,7 @@ class JUCE_API MessageManager final Atomic messageThreadId; Atomic threadWithLock; std::function loopCallback = [] {}; + std::vector> shutdownCallbacks; static bool postMessageToSystemQueue (MessageBase*); static void* exitModalLoopCallback (void*); diff --git a/modules/juce_events/native/juce_MessageManager_ios.mm b/modules/juce_events/native/juce_MessageManager_ios.mm index 8aa32a436..6681ff6de 100644 --- a/modules/juce_events/native/juce_MessageManager_ios.mm +++ b/modules/juce_events/native/juce_MessageManager_ios.mm @@ -48,6 +48,9 @@ void runNSApplication() [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.001]]; } + + for (const auto& func : shutdownCallbacks) + func(); } //============================================================================== diff --git a/modules/juce_events/native/juce_MessageManager_mac.mm b/modules/juce_events/native/juce_MessageManager_mac.mm index cd2f6ebbe..95ba53b4d 100644 --- a/modules/juce_events/native/juce_MessageManager_mac.mm +++ b/modules/juce_events/native/juce_MessageManager_mac.mm @@ -451,6 +451,10 @@ static void shutdownNSApp() if (isThisTheMessageThread()) { quitMessagePosted = true; + + for (const auto& func : shutdownCallbacks) + func(); + shutdownNSApp(); } else diff --git a/modules/juce_events/native/juce_Messaging_emscripten.cpp b/modules/juce_events/native/juce_Messaging_emscripten.cpp index 3bf58fe19..c198a05b5 100644 --- a/modules/juce_events/native/juce_Messaging_emscripten.cpp +++ b/modules/juce_events/native/juce_Messaging_emscripten.cpp @@ -140,6 +140,9 @@ void MessageManager::runDispatchLoop() constexpr int framesPerSeconds = 0; constexpr int simulateInfiniteLoop = 1; emscripten_set_main_loop_arg (mainLoop, this, framesPerSeconds, simulateInfiniteLoop); + + for (const auto& func : shutdownCallbacks) + func(); } void MessageManager::stopDispatchLoop() diff --git a/modules/yup_graphics/fonts/yup_StyledText.cpp b/modules/yup_graphics/fonts/yup_StyledText.cpp index 2cde5a6e9..2521d35a0 100644 --- a/modules/yup_graphics/fonts/yup_StyledText.cpp +++ b/modules/yup_graphics/fonts/yup_StyledText.cpp @@ -170,6 +170,15 @@ void StyledText::setWrap (TextWrap value) //============================================================================== +void StyledText::appendText (StringRef text, + const Font& font, + float fontSize, + float lineHeight, + float letterSpacing) +{ + appendText (text, nullptr, font, fontSize, lineHeight, letterSpacing); +} + void StyledText::appendText (StringRef text, rive::rcp paint, const Font& font, diff --git a/modules/yup_graphics/fonts/yup_StyledText.h b/modules/yup_graphics/fonts/yup_StyledText.h index 2d06c6baa..73e0bb147 100644 --- a/modules/yup_graphics/fonts/yup_StyledText.h +++ b/modules/yup_graphics/fonts/yup_StyledText.h @@ -76,6 +76,12 @@ class JUCE_API StyledText //============================================================================== + void appendText (StringRef text, + const Font& font, + float fontSize = 16.0f, + float lineHeight = -1.0f, + float letterSpacing = 0.0f); + void appendText (StringRef text, rive::rcp paint, const Font& font, diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 535f901af..968f4c1e8 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -531,6 +531,22 @@ class JUCE_API Rectangle return *this; } + /** Sets the size of the rectangle. + + @param newSize The new size for the rectangle as a Size object. + + @return A reference to this rectangle to allow method chaining. + */ + template + constexpr Rectangle& setSize (T width, T height) noexcept + { + static_assert (std::numeric_limits::max() >= std::numeric_limits::max(), "Invalid narrow cast"); + + size = { static_cast (width), static_cast (height) }; + + return *this; + } + /** Returns a new rectangle with the specified size. This method creates a new rectangle with the same position but a different size. diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index 90eee167f..69c77576f 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -1070,9 +1070,14 @@ void Component::internalMouseDown (const MouseEvent& event) updateMouseCursor(); + auto bailOutChecker = BailOutChecker (this); + mouseDown (event); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseDown, event); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseDown, event); } //============================================================================== @@ -1084,9 +1089,14 @@ void Component::internalMouseMove (const MouseEvent& event) updateMouseCursor(); + auto bailOutChecker = BailOutChecker (this); + mouseMove (event); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseMove, event); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseMove, event); } //============================================================================== @@ -1098,9 +1108,14 @@ void Component::internalMouseDrag (const MouseEvent& event) updateMouseCursor(); + auto bailOutChecker = BailOutChecker (this); + mouseDrag (event); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseDrag, event); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseDrag, event); } //============================================================================== @@ -1112,9 +1127,14 @@ void Component::internalMouseUp (const MouseEvent& event) updateMouseCursor(); + auto bailOutChecker = BailOutChecker (this); + mouseUp (event); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseUp, event); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseUp, event); } //============================================================================== @@ -1124,9 +1144,14 @@ void Component::internalMouseDoubleClick (const MouseEvent& event) if (! isVisible()) return; + auto bailOutChecker = BailOutChecker (this); + mouseDoubleClick (event); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseDoubleClick, event); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseDoubleClick, event); } //============================================================================== @@ -1136,9 +1161,14 @@ void Component::internalMouseWheel (const MouseEvent& event, const MouseWheelDat if (! isVisible()) return; + auto bailOutChecker = BailOutChecker (this); + mouseWheel (event, wheelData); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseWheel, event, wheelData); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseWheel, event, wheelData); } //============================================================================== @@ -1214,4 +1244,41 @@ void Component::updateMouseCursor() Desktop::getInstance()->setMouseCursor (mouseCursor); } +Point Component::getScreenPosition() const +{ + // If this component is on the desktop, its position is already in screen coordinates + if (options.onDesktop && native != nullptr) + return native->getPosition().to(); + + // For child components, accumulate transformations up the hierarchy + auto screenPos = getPosition(); + auto parent = getParentComponent(); + + while (parent != nullptr) + { + if (parent->options.onDesktop) + { + // Found the top-level component, add its screen position + if (parent->native != nullptr) + screenPos = screenPos + parent->native->getPosition().to(); + + break; + } + else + { + // Add parent's position relative to its parent + screenPos = screenPos + parent->getPosition(); + } + + parent = parent->getParentComponent(); + } + + return screenPos; +} + +Rectangle Component::getScreenBounds() const +{ + return Rectangle (getScreenPosition(), getSize()); +} + } // namespace yup diff --git a/modules/yup_gui/component/yup_Component.h b/modules/yup_gui/component/yup_Component.h index 0b67dd9f9..d97f817be 100644 --- a/modules/yup_gui/component/yup_Component.h +++ b/modules/yup_gui/component/yup_Component.h @@ -272,19 +272,38 @@ class JUCE_API Component Rectangle getBounds() const; /** - Get the local bounds of the component, with zero position. + Get the bounds of the component in screen coordinates. + + @return The bounds of the component in screen coordinates. + */ + Rectangle getScreenBounds() const; + + /** + Get the local bounds of the component. + + The local bounds is the same as getBounds() but with the position set to (0, 0). @return The local bounds of the component. */ Rectangle getLocalBounds() const; /** - Get the bounds of the component relative to its top level component. + Get the bounds of the component relative to the top level component. - @return The bounds of the component relative to its top most component (on desktop). + @return The bounds of the component relative to the top level component. */ Rectangle getBoundsRelativeToTopLevelComponent() const; + /** + Get the position of the component in absolute screen coordinates. + + This method traverses up the parent hierarchy to calculate the component's + absolute position on the screen. + + @return The absolute screen position of the component. + */ + Point getScreenPosition() const; + //============================================================================== void setTransform (const AffineTransform& transform); @@ -907,7 +926,7 @@ class JUCE_API Component Array children; Rectangle boundsInParent; AffineTransform transform; - std::unique_ptr native; + ComponentNative::Ptr native; WeakReference::Master masterReference; MouseListenerList mouseListeners; ComponentStyle::Ptr style; diff --git a/modules/yup_gui/component/yup_ComponentNative.h b/modules/yup_gui/component/yup_ComponentNative.h index 4773a6e34..c08da85ed 100644 --- a/modules/yup_gui/component/yup_ComponentNative.h +++ b/modules/yup_gui/component/yup_ComponentNative.h @@ -37,7 +37,7 @@ class Component; @see Component */ -class JUCE_API ComponentNative +class JUCE_API ComponentNative : public ReferenceCountedObject { struct decoratedWindowTag; struct resizableWindowTag; @@ -45,6 +45,10 @@ class JUCE_API ComponentNative struct allowHighDensityDisplayTag; public: + //============================================================================== + /** The pointer defintion for this native component. */ + using Ptr = ReferenceCountedObjectPtr; + //============================================================================== /** Type definition for window configuration flags. */ using Flags = FlagSet; @@ -393,11 +397,11 @@ class JUCE_API ComponentNative @param options The options to configure the native component. @param parent Optional pointer to a parent native window, or nullptr for a top-level window. - @return A unique_ptr to the created ComponentNative instance. + @return A reference counted pointer to the created ComponentNative instance. */ - static std::unique_ptr createFor (Component& component, - const Options& options, - void* parent); + static ComponentNative::Ptr createFor (Component& component, + const Options& options, + void* parent); protected: /** The Component associated with this native component. */ diff --git a/modules/yup_gui/desktop/yup_Desktop.cpp b/modules/yup_gui/desktop/yup_Desktop.cpp index 73da563a9..b08cb82f7 100644 --- a/modules/yup_gui/desktop/yup_Desktop.cpp +++ b/modules/yup_gui/desktop/yup_Desktop.cpp @@ -97,6 +97,106 @@ MouseCursor Desktop::getMouseCursor() const //============================================================================== +void Desktop::addGlobalMouseListener (MouseListener* listener) +{ + if (listener == nullptr) + return; + + // Remove any existing weak reference to this listener first + removeGlobalMouseListener (listener); + + // Add the new weak reference + globalMouseListeners.push_back (WeakReference (listener)); +} + +void Desktop::removeGlobalMouseListener (MouseListener* listener) +{ + if (listener == nullptr) + return; + + globalMouseListeners.erase ( + std::remove_if (globalMouseListeners.begin(), globalMouseListeners.end(), + [listener] (const WeakReference& ref) + { + return ref.get() == listener || ref.get() == nullptr; + }), + globalMouseListeners.end()); +} + +void Desktop::handleGlobalMouseDown (const MouseEvent& event) +{ + auto it = globalMouseListeners.begin(); + while (it != globalMouseListeners.end()) + { + auto* listener = it->get(); + if (listener == nullptr) + { + it = globalMouseListeners.erase (it); + } + else + { + listener->mouseDown (event); + ++it; + } + } +} + +void Desktop::handleGlobalMouseUp (const MouseEvent& event) +{ + auto it = globalMouseListeners.begin(); + while (it != globalMouseListeners.end()) + { + auto* listener = it->get(); + if (listener == nullptr) + { + it = globalMouseListeners.erase (it); + } + else + { + listener->mouseUp (event); + ++it; + } + } +} + +void Desktop::handleGlobalMouseMove (const MouseEvent& event) +{ + auto it = globalMouseListeners.begin(); + while (it != globalMouseListeners.end()) + { + auto* listener = it->get(); + if (listener == nullptr) + { + it = globalMouseListeners.erase (it); + } + else + { + listener->mouseMove (event); + ++it; + } + } +} + +void Desktop::handleGlobalMouseDrag (const MouseEvent& event) +{ + auto it = globalMouseListeners.begin(); + while (it != globalMouseListeners.end()) + { + auto* listener = it->get(); + if (listener == nullptr) + { + it = globalMouseListeners.erase (it); + } + else + { + listener->mouseDrag (event); + ++it; + } + } +} + +//============================================================================== + void Desktop::handleScreenConnected (int screenIndex) { updateScreens(); diff --git a/modules/yup_gui/desktop/yup_Desktop.h b/modules/yup_gui/desktop/yup_Desktop.h index 461cd70d7..be268feb2 100644 --- a/modules/yup_gui/desktop/yup_Desktop.h +++ b/modules/yup_gui/desktop/yup_Desktop.h @@ -103,6 +103,19 @@ class JUCE_API Desktop */ void setCurrentMouseLocation (const Point& location); + //============================================================================== + /** Adds a global mouse listener that will receive mouse events from anywhere on the desktop. + + @param listener The mouse listener to add + */ + void addGlobalMouseListener (MouseListener* listener); + + /** Removes a global mouse listener. + + @param listener The mouse listener to remove + */ + void removeGlobalMouseListener (MouseListener* listener); + //============================================================================== /** Updates the list of screens. */ void updateScreens(); @@ -117,6 +130,15 @@ class JUCE_API Desktop /** @internal */ void handleScreenOrientationChanged (int screenIndex); + /** @internal */ + void handleGlobalMouseDown (const MouseEvent& event); + /** @internal */ + void handleGlobalMouseUp (const MouseEvent& event); + /** @internal */ + void handleGlobalMouseMove (const MouseEvent& event); + /** @internal */ + void handleGlobalMouseDrag (const MouseEvent& event); + //============================================================================== JUCE_DECLARE_SINGLETON (Desktop, false) @@ -128,6 +150,8 @@ class JUCE_API Desktop Screen::Array screens; std::optional currentMouseCursor; + std::vector> globalMouseListeners; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Desktop) }; diff --git a/modules/yup_gui/mouse/yup_MouseEvent.cpp b/modules/yup_gui/mouse/yup_MouseEvent.cpp index 1faed878c..6a0ef294f 100644 --- a/modules/yup_gui/mouse/yup_MouseEvent.cpp +++ b/modules/yup_gui/mouse/yup_MouseEvent.cpp @@ -107,6 +107,15 @@ Point MouseEvent::getPosition() const noexcept return position; } +Point MouseEvent::getScreenPosition() const noexcept +{ + if (sourceComponent == nullptr) + return position; + + // Get the source component's screen position and add our relative position + return sourceComponent->getScreenPosition() + position; +} + MouseEvent MouseEvent::withPosition (const Point& newPosition) const noexcept { return { buttons, modifiers, newPosition, lastMouseDownPosition, lastMouseDownTime, sourceComponent }; diff --git a/modules/yup_gui/mouse/yup_MouseEvent.h b/modules/yup_gui/mouse/yup_MouseEvent.h index 07ebf3e47..4959df544 100644 --- a/modules/yup_gui/mouse/yup_MouseEvent.h +++ b/modules/yup_gui/mouse/yup_MouseEvent.h @@ -164,6 +164,15 @@ class JUCE_API MouseEvent */ Point getPosition() const noexcept; + /** Returns the mouse position in absolute screen coordinates. + + This method converts the mouse position from component-relative coordinates + to absolute screen coordinates by taking into account the component hierarchy. + + @returns the mouse position in absolute screen coordinates + */ + Point getScreenPosition() const noexcept; + /** Creates a copy of this event with a different position. @param newPosition the new position to use diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 04aebbff1..159ed681f 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -56,6 +56,8 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, , shouldRenderContinuous (options.flags.test (renderContinuous)) , updateOnlyWhenFocused (options.updateOnlyWhenFocused) { + incReferenceCount(); + SDL_AddEventWatch (eventDispatcher, this); // Setup window hints and get flags @@ -489,6 +491,9 @@ void SDL2ComponentNative::run() triggerAsyncUpdate(); renderEvent.wait (maxFrameTimeMs - 4.0); + if (threadShouldExit()) + break; + // Measure spent time and cap the framerate double currentTimeSeconds = juce::Time::getMillisecondCounterHiRes() / 1000.0; double timeSpentSeconds = currentTimeSeconds - frameStartTimeSeconds; @@ -1035,15 +1040,6 @@ void SDL2ComponentNative::updateComponentUnderMouse (const MouseEvent& event) //============================================================================== -std::unique_ptr ComponentNative::createFor (Component& component, - const Options& options, - void* parent) -{ - return std::make_unique (component, options, parent); -} - -//============================================================================== - void SDL2ComponentNative::handleWindowEvent (const SDL_WindowEvent& windowEvent) { YUP_PROFILE_INTERNAL_TRACE(); @@ -1145,12 +1141,6 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) switch (event->type) { - case SDL_QUIT: - { - YUP_WINDOWING_LOG ("SDL_QUIT"); - break; - } - case SDL_WINDOWEVENT: { if (event->window.windowID == SDL_GetWindowID (window)) @@ -1266,12 +1256,37 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) int SDL2ComponentNative::eventDispatcher (void* userdata, SDL_Event* event) { - static_cast (userdata)->handleEvent (event); + switch (event->type) + { + case SDL_QUIT: + { + YUP_WINDOWING_LOG ("SDL_QUIT"); + break; + } + + default: + { + auto nativeComponent = SDL2ComponentNative::Ptr (static_cast (userdata)); + nativeComponent->handleEvent (event); + break; + } + } + + return 0; } //============================================================================== +ComponentNative::Ptr ComponentNative::createFor (Component& component, + const Options& options, + void* parent) +{ + return ComponentNative::Ptr (ReferenceCountedObjectAdopt, new SDL2ComponentNative (component, options, parent)); +} + +//============================================================================== + namespace { @@ -1308,6 +1323,8 @@ int displayEventDispatcher (void* userdata, SDL_Event* event) } // namespace +//============================================================================== + void Desktop::updateScreens() { const int numScreens = SDL_GetNumVideoDisplays(); diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.h b/modules/yup_gui/native/yup_Windowing_sdl2.h index e32b080c4..fe0031b2e 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.h +++ b/modules/yup_gui/native/yup_Windowing_sdl2.h @@ -36,6 +36,9 @@ class SDL2ComponentNative final #endif public: + //============================================================================== + using Ptr = ReferenceCountedObjectPtr; + //============================================================================== SDL2ComponentNative (Component& component, const Options& options, From c37e62238de9279b0c12475aad4d781e2acc026d Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Fri, 6 Jun 2025 13:43:23 +0000 Subject: [PATCH 02/51] Code formatting --- modules/juce_core/memory/juce_ReferenceCountedObject.h | 4 +++- modules/juce_events/messages/juce_ApplicationBase.cpp | 2 +- modules/yup_gui/desktop/yup_Desktop.cpp | 9 ++++----- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 1 - 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/juce_core/memory/juce_ReferenceCountedObject.h b/modules/juce_core/memory/juce_ReferenceCountedObject.h index 6b39a372c..bc7e8c2d0 100644 --- a/modules/juce_core/memory/juce_ReferenceCountedObject.h +++ b/modules/juce_core/memory/juce_ReferenceCountedObject.h @@ -248,7 +248,9 @@ class JUCE_API SingleThreadedReferenceCountedObject @tags{Core} */ -struct ReferenceCountedObjectAdoptType {}; +struct ReferenceCountedObjectAdoptType +{ +}; /** This is a constant that can be used to indicate that a ReferenceCountedObjectPtr is adopting an object. diff --git a/modules/juce_events/messages/juce_ApplicationBase.cpp b/modules/juce_events/messages/juce_ApplicationBase.cpp index 519f240ac..307c95f42 100644 --- a/modules/juce_events/messages/juce_ApplicationBase.cpp +++ b/modules/juce_events/messages/juce_ApplicationBase.cpp @@ -83,7 +83,7 @@ void JUCEApplicationBase::appWillTerminateByForce() void JUCEApplicationBase::quit() { - MessageManager::callAsync([] + MessageManager::callAsync ([] { MessageManager::getInstance()->stopDispatchLoop(); }); diff --git a/modules/yup_gui/desktop/yup_Desktop.cpp b/modules/yup_gui/desktop/yup_Desktop.cpp index b08cb82f7..5b3f4e038 100644 --- a/modules/yup_gui/desktop/yup_Desktop.cpp +++ b/modules/yup_gui/desktop/yup_Desktop.cpp @@ -115,11 +115,10 @@ void Desktop::removeGlobalMouseListener (MouseListener* listener) return; globalMouseListeners.erase ( - std::remove_if (globalMouseListeners.begin(), globalMouseListeners.end(), - [listener] (const WeakReference& ref) - { - return ref.get() == listener || ref.get() == nullptr; - }), + std::remove_if (globalMouseListeners.begin(), globalMouseListeners.end(), [listener] (const WeakReference& ref) + { + return ref.get() == listener || ref.get() == nullptr; + }), globalMouseListeners.end()); } diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 159ed681f..6f8653eef 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -1272,7 +1272,6 @@ int SDL2ComponentNative::eventDispatcher (void* userdata, SDL_Event* event) } } - return 0; } From 7df72f230b701fa929324ca69ef0166c9c14e6cb Mon Sep 17 00:00:00 2001 From: kunitoki Date: Fri, 6 Jun 2025 16:37:16 +0200 Subject: [PATCH 03/51] Fix component bail outs --- modules/yup_gui/component/yup_Component.cpp | 40 ++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index 69c77576f..e7894637b 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -76,7 +76,6 @@ void Component::setEnabled (bool shouldBeEnabled) // native->setEnabled (shouldBeEnabled); if (options.isDisabled && hasKeyboardFocus()) - enablementChanged(); } @@ -99,8 +98,13 @@ void Component::setVisible (bool shouldBeVisible) if (options.onDesktop && native != nullptr) native->setVisible (shouldBeVisible); + auto bailOutChecker = BailOutChecker (this); + visibilityChanged(); + if (bailOutChecker.shouldBailOut()) + return; + repaint(); } @@ -337,7 +341,13 @@ void Component::setBounds (const Rectangle& newBounds) if (options.onDesktop && native != nullptr) native->setBounds (newBounds.to()); + auto bailOutChecker = BailOutChecker (this); + resized(); + + if (bailOutChecker.shouldBailOut()) + return; + moved(); } @@ -717,8 +727,13 @@ void Component::removeChildComponent (int index) auto component = children.removeAndReturn (index); component->parentComponent = nullptr; + auto bailOutChecker = BailOutChecker (this); + component->internalHierarchyChanged(); + if (bailOutChecker.shouldBailOut()) + return; + childrenChanged(); } @@ -732,13 +747,13 @@ void Component::internalHierarchyChanged() { parentHierarchyChanged(); - auto checker = BailOutChecker (this); + auto bailOutChecker = BailOutChecker (this); for (int index = children.size(); --index >= 0;) { auto child = children.getUnchecked (index); - if (checker.shouldBailOut()) + if (bailOutChecker.shouldBailOut()) { jassertfalse; // Deleting a parent component when notifying its children! return; @@ -927,8 +942,13 @@ void Component::setStyle (ComponentStyle::Ptr newStyle) style = std::move (newStyle); + auto bailOutChecker = BailOutChecker (this); + styleChanged(); + if (bailOutChecker.shouldBailOut()) + return; + repaint(); } @@ -1042,9 +1062,14 @@ void Component::internalMouseEnter (const MouseEvent& event) updateMouseCursor(); + auto bailOutChecker = BailOutChecker (this); + mouseEnter (event); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseEnter, event); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseEnter, event); } //============================================================================== @@ -1056,9 +1081,14 @@ void Component::internalMouseExit (const MouseEvent& event) updateMouseCursor(); + auto bailOutChecker = BailOutChecker (this); + mouseExit (event); - mouseListeners.callChecked (BailOutChecker (this), &MouseListener::mouseExit, event); + if (bailOutChecker.shouldBailOut()) + return; + + mouseListeners.callChecked (bailOutChecker, &MouseListener::mouseExit, event); } //============================================================================== From f4979c5e7544f38368dff33846c887ab7d456004 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Fri, 6 Jun 2025 17:04:39 +0200 Subject: [PATCH 04/51] More work on popup menus --- .../graphics/source/examples/PopupMenuDemo.h | 309 +++++++++ examples/graphics/source/main.cpp | 9 +- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 78 ++- modules/yup_gui/widgets/yup_PopupMenu.cpp | 611 ++++++++++++++++++ modules/yup_gui/widgets/yup_PopupMenu.h | 180 ++++++ modules/yup_gui/yup_gui.cpp | 1 + modules/yup_gui/yup_gui.h | 1 + 7 files changed, 1173 insertions(+), 16 deletions(-) create mode 100644 examples/graphics/source/examples/PopupMenuDemo.h create mode 100644 modules/yup_gui/widgets/yup_PopupMenu.cpp create mode 100644 modules/yup_gui/widgets/yup_PopupMenu.h diff --git a/examples/graphics/source/examples/PopupMenuDemo.h b/examples/graphics/source/examples/PopupMenuDemo.h new file mode 100644 index 000000000..017a4d81c --- /dev/null +++ b/examples/graphics/source/examples/PopupMenuDemo.h @@ -0,0 +1,309 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +//============================================================================== + +class PopupMenuDemo : public yup::Component +{ +public: + PopupMenuDemo() + : Component ("PopupMenuDemo") + , basicMenuButton ("basicMenuButton") + , subMenuButton ("subMenuButton") + , customMenuButton ("customMenuButton") + , nativeMenuButton ("nativeMenuButton") + , statusLabel ("statusLabel") + { + addAndMakeVisible (statusLabel); + statusLabel.setTitle ("Right-click anywhere to show context menu"); + + addAndMakeVisible (basicMenuButton); + basicMenuButton.setTitle ("Show Basic Menu"); + basicMenuButton.onClick = [this] { showBasicMenu(); }; + + addAndMakeVisible (subMenuButton); + subMenuButton.setTitle ("Show Sub-Menu"); + subMenuButton.onClick = [this] { showSubMenu(); }; + + addAndMakeVisible (customMenuButton); + customMenuButton.setTitle ("Show Custom Menu"); + customMenuButton.onClick = [this] { showCustomMenu(); }; + + addAndMakeVisible (nativeMenuButton); + nativeMenuButton.setTitle ("Show Native Menu"); + nativeMenuButton.onClick = [this] { showNativeMenu(); }; + + setSize ({ 400, 300 }); + } + + void resized() override + { + auto area = getLocalBounds().reduced (20); + + area.removeFromTop (20); + statusLabel.setBounds (area.removeFromTop (30)); + + basicMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); + subMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); + customMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); + nativeMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); + } + + void paint (yup::Graphics& g) override + { + auto area = getLocalBounds().reduced (20); + + g.setFillColor (yup::Color (0xff1e1e1e)); + g.fillAll(); + + g.setStrokeColor (yup::Color (0xff555555)); + g.setStrokeWidth (1.0f); + g.strokeRect (getLocalBounds().to().reduced (2.0f)); + + g.setFillColor (yup::Color (0xffffffff)); + auto styledText = yup::StyledText(); + styledText.appendText("PopupMenu Demo", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + g.fillFittedText (styledText, area.removeFromTop (20).to()); + } + + void mouseDown (const yup::MouseEvent& event) override + { + if (event.isRightButtonDown()) + { + showContextMenu (event.getPosition()); + } + } + +private: + yup::TextButton basicMenuButton; + yup::TextButton subMenuButton; + yup::TextButton customMenuButton; + yup::TextButton nativeMenuButton; + yup::Label statusLabel; + + enum MenuItemIDs + { + newFile = 1, + openFile, + saveFile, + saveAsFile, + recentFile1, + recentFile2, + exitApp, + + editUndo = 10, + editRedo, + editCut, + editCopy, + editPaste, + + colorRed = 20, + colorGreen, + colorBlue, + + customSlider = 30, + customButton = 31 + }; + + void showBasicMenu() + { + auto menu = yup::PopupMenu::create(); + + menu->addItem ("New File", newFile, true, false, "Cmd+N"); + menu->addItem ("Open File", openFile, true, false, "Cmd+O"); + menu->addSeparator(); + menu->addItem ("Save File", saveFile, true, false, "Cmd+S"); + menu->addItem ("Save As...", saveAsFile, true, false, "Shift+Cmd+S"); + menu->addSeparator(); + menu->addItem ("Disabled Item", 999, false); + menu->addItem ("Checked Item", 998, true, true); + menu->addSeparator(); + menu->addItem ("Exit", exitApp, true, false, "Cmd+Q"); + + menu->onItemSelected = [this] (int selectedID) + { + handleMenuSelection (selectedID); + }; + + menu->showAt (&basicMenuButton); + } + + void showSubMenu() + { + auto recentFilesMenu = yup::PopupMenu::create(); + recentFilesMenu->addItem ("Recent File 1.txt", recentFile1); + recentFilesMenu->addItem ("Recent File 2.txt", recentFile2); + + auto colorMenu = yup::PopupMenu::create(); + colorMenu->addItem ("Red", colorRed); + colorMenu->addItem ("Green", colorGreen); + colorMenu->addItem ("Blue", colorBlue); + + auto menu = yup::PopupMenu::create(); + menu->addItem ("New", newFile); + menu->addItem ("Open", openFile); + menu->addSubMenu ("Recent Files", recentFilesMenu); + menu->addSeparator(); + menu->addSubMenu ("Colors", colorMenu); + menu->addSeparator(); + menu->addItem ("Exit", exitApp); + + menu->onItemSelected = [this] (int selectedID) + { + handleMenuSelection (selectedID); + }; + + menu->showAt (&subMenuButton); + } + + void showCustomMenu() + { + auto menu = yup::PopupMenu::create(); + + menu->addItem ("Regular Item", 1); + menu->addSeparator(); + + // Add custom slider component + auto slider = std::make_unique ("CustomSlider"); + slider->setSize ({ 250, 250 }); + slider->setValue (0.5); + menu->addCustomItem (std::move (slider), customSlider); + + menu->addSeparator(); + + // Add custom button component + auto button = std::make_unique ("CustomButton"); + button->setSize ({ 120, 30 }); + button->setTitle ("Custom Button"); + menu->addCustomItem (std::move (button), customButton); + + menu->addSeparator(); + menu->addItem ("Another Item", 2); + + menu->onItemSelected = [this] (int selectedID) + { + handleMenuSelection (selectedID); + }; + + menu->showAt (&customMenuButton); + } + + void showNativeMenu() + { + auto menu = yup::PopupMenu::create(); + + menu->addItem ("Native Item 1", 1); + menu->addItem ("Native Item 2", 2); + menu->addSeparator(); + menu->addItem ("Native Item 3", 3); + + menu->onItemSelected = [this] (int selectedID) + { + handleMenuSelection (selectedID); + }; + + yup::PopupMenu::Options options; + options.useNativeMenus = true; + options.parentComponent = this; + + menu->show (options, [this] (int selectedID) + { + handleMenuSelection (selectedID); + }); + } + + void showContextMenu (yup::Point position) + { + auto contextMenu = yup::PopupMenu::create(); + + contextMenu->addItem ("Copy", editCopy); + contextMenu->addItem ("Paste", editPaste); + contextMenu->addSeparator(); + contextMenu->addItem ("Select All", 100); + contextMenu->addSeparator(); + contextMenu->addItem ("Properties", 101); + + contextMenu->onItemSelected = [this] (int selectedID) + { + handleMenuSelection (selectedID); + }; + + yup::PopupMenu::Options options; + options.parentComponent = this; + options.targetScreenPosition = position.to(); + + contextMenu->show (options, [this] (int selectedID) + { + handleMenuSelection (selectedID); + }); + } + + void handleMenuSelection (int selectedID) + { + yup::String message = "Selected item ID: " + yup::String (selectedID); + + switch (selectedID) + { + case newFile: + message = "New File selected"; + break; + case openFile: + message = "Open File selected"; + break; + case saveFile: + message = "Save File selected"; + break; + case saveAsFile: + message = "Save As selected"; + break; + case exitApp: + message = "Exit selected"; + break; + case editCopy: + message = "Copy selected"; + break; + case editPaste: + message = "Paste selected"; + break; + case colorRed: + message = "Red color selected"; + break; + case colorGreen: + message = "Green color selected"; + break; + case colorBlue: + message = "Blue color selected"; + break; + case customSlider: + message = "Custom slider interacted"; + break; + case customButton: + message = "Custom button clicked"; + break; + default: + break; + } + + statusLabel.setTitle (message); + } +}; diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 3d6c1abf4..78da26456 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -32,6 +32,7 @@ #include "examples/LayoutFonts.h" #include "examples/VariableFonts.h" #include "examples/Paths.h" +#include "examples/PopupMenuDemo.h" //============================================================================== @@ -74,7 +75,7 @@ class CustomWindow */ // Add the demos - int demo = 1; + int demo = 4; if (demo == 0) { @@ -100,6 +101,12 @@ class CustomWindow addAndMakeVisible (components.getLast()); } + if (demo == 4) + { + components.add (std::make_unique()); + addAndMakeVisible (components.getLast()); + } + // Timer startTimerHz (10); } diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 6f8653eef..dee695ee5 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -1163,8 +1163,10 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_MOUSEMOTION: { + auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + if (event->window.windowID == SDL_GetWindowID (window)) - handleMouseMoveOrDrag ({ static_cast (event->motion.x), static_cast (event->motion.y) }); + handleMouseMoveOrDrag (cursorPosition); break; } @@ -1291,30 +1293,76 @@ namespace int displayEventDispatcher (void* userdata, SDL_Event* event) { - if (event->type != SDL_DISPLAYEVENT) - return 0; - auto desktop = static_cast (userdata); - switch (event->display.event) + if (event->type == SDL_DISPLAYEVENT) + { + switch (event->display.event) + { + case SDL_DISPLAYEVENT_CONNECTED: + desktop->handleScreenConnected (event->display.display); + break; + + case SDL_DISPLAYEVENT_DISCONNECTED: + desktop->handleScreenDisconnected (event->display.display); + break; + + case SDL_DISPLAYEVENT_ORIENTATION: + desktop->handleScreenOrientationChanged (event->display.display); + break; + +#if ! JUCE_EMSCRIPTEN + case SDL_DISPLAYEVENT_MOVED: + desktop->handleScreenMoved (event->display.display); + break; +#endif + + default: + break; + } + + return; + } + + switch (event->type) { - case SDL_DISPLAYEVENT_CONNECTED: - desktop->handleScreenConnected (event->display.display); + case SDL_MOUSEMOTION: + { + auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + + break; + } + + case SDL_MOUSEBUTTONDOWN: + { + auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + auto button = toMouseButton (event->button.button); + auto keyModifiers = KeyModifiers(); + break; + } + + case SDL_MOUSEBUTTONUP: + { + auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + auto button = toMouseButton (event->button.button); + auto keyModifiers = KeyModifiers(); - case SDL_DISPLAYEVENT_DISCONNECTED: - desktop->handleScreenDisconnected (event->display.display); break; + } + + case SDL_MOUSEWHEEL: + { + int x = 0, y = 0; + SDL_GetMouseState (&x, &y); + auto cursorPosition = Point { static_cast (x), static_cast (y) }; + auto mouseWheelData = MouseWheelData { static_cast (event->wheel.x), static_cast (event->wheel.y) }; - case SDL_DISPLAYEVENT_ORIENTATION: - desktop->handleScreenOrientationChanged (event->display.display); break; + } -#if ! JUCE_EMSCRIPTEN - case SDL_DISPLAYEVENT_MOVED: - desktop->handleScreenMoved (event->display.display); + default: break; -#endif } return 0; diff --git a/modules/yup_gui/widgets/yup_PopupMenu.cpp b/modules/yup_gui/widgets/yup_PopupMenu.cpp new file mode 100644 index 000000000..d2a859d3c --- /dev/null +++ b/modules/yup_gui/widgets/yup_PopupMenu.cpp @@ -0,0 +1,611 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== + +namespace +{ + +static std::vector activePopups; + +} // namespace + +//============================================================================== + +class PopupMenu::PopupMenuItem +{ +public: + //============================================================================== + + PopupMenuItem() + { + } + + PopupMenuItem (const String& itemText, int itemID, bool isEnabled = true, bool isTicked = false) + : text (itemText), itemID (itemID), isEnabled (isEnabled), isTicked (isTicked) + { + } + + PopupMenuItem (const String& itemText, PopupMenu::Ptr subMenu, bool isEnabled = true) + : text (itemText), isEnabled (isEnabled), subMenu (std::move (subMenu)) + { + } + + PopupMenuItem (std::unique_ptr component, int itemID) + : itemID (itemID), customComponent (std::move (component)) + { + } + + //============================================================================== + + ~PopupMenuItem() = default; + + //============================================================================== + + bool isSeparator() const { return text.isEmpty() && itemID == 0 && subMenu == nullptr && customComponent == nullptr; } + + bool isSubMenu() const { return subMenu != nullptr; } + + bool isCustomComponent() const { return customComponent != nullptr; } + + //============================================================================== + + String text; + + int itemID = 0; + + bool isEnabled = true; + + bool isTicked = false; + + PopupMenu::Ptr subMenu; + + std::unique_ptr customComponent; + + String shortcutKeyText; + + std::optional textColor; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenuItem) +}; + +//============================================================================== + +class PopupMenu::MenuWindow : public Component +{ +public: + MenuWindow (PopupMenu::Ptr menu, const PopupMenu::Options& opts) + : Component ("PopupMenuWindow") + , owner (menu) + , options (opts) + { + setWantsKeyboardFocus (true); + + // Calculate required size and create menu items + setupMenuItems(); + + // Add to desktop as a popup + ComponentNative::Options nativeOptions; + nativeOptions + .withDecoration (false) + .withResizableWindow (false); + + addToDesktop (nativeOptions); + + // Position the menu + positionMenu(); + + // Add to active popups list for modal behavior + activePopups.push_back (this); + + setVisible (true); + takeKeyboardFocus(); + } + + ~MenuWindow() override + { + activePopups.erase (std::remove (activePopups.begin(), activePopups.end(), this), activePopups.end()); + } + + void paint (Graphics& g) override + { + // Draw menu background + g.setFillColor (findColor (Colours::menuBackground).value_or (Color (0xff2a2a2a))); + g.fillRoundedRect (getLocalBounds().to(), 4.0f); + + // Draw border + g.setStrokeColor (findColor (Colours::menuBorder).value_or (Color (0xff555555))); + g.setStrokeWidth (1.0f); + g.strokeRoundedRect (getLocalBounds().to().reduced (0.5f), 4.0f); + + // Draw menu items + drawMenuItems (g); + } + + void focusLost() override + { + dismiss (0); + } + + bool isWithinBounds (Point globalPoint) const + { + auto localPoint = globalPoint - getScreenPosition().to(); + return getLocalBounds().to().contains (localPoint); + } + + void mouseDown (const MouseEvent& event) override + { + auto itemIndex = getItemIndexAt (event.getPosition()); + if (itemIndex >= 0 && itemIndex < owner->items.size()) + { + auto& item = *owner->items[itemIndex]; + if (! item.isSeparator() && item.isEnabled) + { + if (item.isSubMenu()) + { + // TODO: Show sub-menu + } + else + { + dismiss (item.itemID); + } + } + } + } + + void mouseMove (const MouseEvent& event) override + { + auto newHoveredIndex = getItemIndexAt (event.getPosition()); + if (newHoveredIndex != hoveredItemIndex) + { + hoveredItemIndex = newHoveredIndex; + repaint(); + } + } + + void mouseExit (const MouseEvent& event) override + { + if (hoveredItemIndex >= 0) + { + hoveredItemIndex = -1; + repaint(); + } + } + + void keyDown (const KeyPress& key, const Point& position) override + { + if (key.getKey() == KeyPress::escapeKey) + { + dismiss (0); + } + } + + void dismiss (int itemID) + { + selectedItemID = itemID; + setVisible (false); + + // Call the owner's callback + if (owner->onItemSelected) + owner->onItemSelected (itemID); + + delete this; + } + +private: + // Color identifiers for theming + struct Colours + { + static inline const Identifier menuBackground { "menuBackground" }; + static inline const Identifier menuBorder { "menuBorder" }; + static inline const Identifier menuItemText { "menuItemText" }; + static inline const Identifier menuItemTextDisabled { "menuItemTextDisabled" }; + static inline const Identifier menuItemBackground { "menuItemBackground" }; + static inline const Identifier menuItemBackgroundHighlighted { "menuItemBackgroundHighlighted" }; + }; + + void setupMenuItems() + { + constexpr float separatorHeight = 8.0f; + constexpr float verticalPadding = 4.0f; + + float y = verticalPadding; // Top padding + float itemHeight = static_cast (options.standardItemHeight); + float width = static_cast (options.minWidth > 0 ? options.minWidth : 200); + + itemRects.clear(); + + for (const auto& item : owner->items) + { + if (item->isCustomComponent()) + width = jmax (width, item->customComponent->getWidth()); + } + + for (const auto& item : owner->items) + { + if (item->isSeparator()) + { + itemRects.push_back ({ 0, y, width, separatorHeight }); + y += separatorHeight; + } + else if (item->isCustomComponent()) + { + addChildComponent (*item->customComponent); + + float horizontalOffset = 0.0f; + + auto compHeight = item->customComponent->getHeight(); + auto compWidth = item->customComponent->getWidth(); + if (compWidth < width) + horizontalOffset = (width - compWidth) / 2.0f; + + auto& rect = itemRects.emplace_back (horizontalOffset, y, compWidth, compHeight); + item->customComponent->setBounds (rect); + item->customComponent->setVisible (true); + + y += compHeight; + } + else + { + itemRects.push_back ({ 0, y, width, itemHeight }); + y += itemHeight; + } + } + + setSize ({ width, y + verticalPadding }); // Bottom padding + } + + void positionMenu() + { + Rectangle bounds; + + if (options.parentComponent) + { + auto parentBounds = options.parentComponent->getBoundsRelativeToTopLevelComponent(); + + if (auto native = options.parentComponent->getNativeComponent()) + parentBounds = native->getBounds(); + + bounds.setTopLeft ({ parentBounds.getX() + options.targetScreenPosition.getX(), + parentBounds.getY() + options.targetScreenPosition.getY() }); + } + else + { + bounds.setTopLeft (options.targetScreenPosition); + } + + bounds.setSize (getWidth(), getHeight()); + setBounds (bounds.roundToInt()); + } + + int getItemIndexAt (Point position) const + { + for (int i = 0; i < static_cast (itemRects.size()); ++i) + { + if (itemRects[i].contains (position)) + return i; + } + return -1; + } + + void drawMenuItems (Graphics& g) + { + auto itemFont = ApplicationTheme::getGlobalTheme()->getDefaultFont(); + + for (int i = 0; i < static_cast (owner->items.size()); ++i) + { + const auto& item = *owner->items[i]; + const auto& rect = itemRects[i]; + + // Skip custom components as they render themselves + if (item.isCustomComponent()) + continue; + + // Draw hover background + if (i == hoveredItemIndex && ! item.isSeparator() && item.isEnabled) + { + g.setFillColor (findColor (Colours::menuItemBackgroundHighlighted).value_or (Color (0xff404040))); + g.fillRoundedRect (rect.reduced (2.0f, 1.0f), 2.0f); + } + + if (item.isSeparator()) + { + // Draw separator line + auto lineY = rect.getCenterY(); + g.setStrokeColor (findColor (Colours::menuBorder).value_or (Color (0xff555555))); + g.setStrokeWidth (1.0f); + g.strokeLine (rect.getX() + 8.0f, lineY, rect.getRight() - 8.0f, lineY); + } + else + { + // Draw menu item text + auto textColor = item.textColor.value_or (findColor (Colours::menuItemText).value_or (Color (0xffffffff))); + if (! item.isEnabled) + textColor = findColor (Colours::menuItemTextDisabled).value_or (Color (0xff808080)); + + g.setFillColor (textColor); + + auto textRect = rect.reduced (12.0f, 2.0f); + + { + auto styledText = yup::StyledText(); + styledText.appendText (item.text, itemFont); + g.fillFittedText (styledText, textRect); + } + + // Draw checkmark if ticked + if (item.isTicked) + { + auto checkRect = Rectangle (rect.getX() + 4.0f, rect.getY() + 4.0f, 12.0f, 12.0f); + g.setStrokeColor (textColor); + g.setStrokeWidth (2.0f); + g.strokeLine (checkRect.getX() + 2.0f, checkRect.getCenterY(), + checkRect.getCenterX(), checkRect.getBottom() - 2.0f); + g.strokeLine (checkRect.getCenterX(), checkRect.getBottom() - 2.0f, + checkRect.getRight() - 2.0f, checkRect.getY() + 2.0f); + } + + // Draw shortcut text + if (item.shortcutKeyText.isNotEmpty()) + { + auto shortcutRect = Rectangle (rect.getRight() - 80.0f, rect.getY(), 75.0f, rect.getHeight()); + g.setOpacity (0.7f); + + auto styledText = yup::StyledText(); + styledText.setHorizontalAlign (yup::StyledText::right); + styledText.appendText (item.shortcutKeyText, itemFont); + g.fillFittedText (styledText, shortcutRect); + + g.setOpacity (1.0f); + } + + // Draw submenu arrow + if (item.isSubMenu()) + { + auto arrowRect = Rectangle (rect.getRight() - 16.0f, rect.getY() + 4.0f, 8.0f, rect.getHeight() - 8.0f); + g.setStrokeColor (textColor); + g.setStrokeWidth (1.5f); + g.strokeLine (arrowRect.getX() + 2.0f, arrowRect.getY() + 2.0f, + arrowRect.getRight() - 2.0f, arrowRect.getCenterY()); + g.strokeLine (arrowRect.getRight() - 2.0f, arrowRect.getCenterY(), + arrowRect.getX() + 2.0f, arrowRect.getBottom() - 2.0f); + } + } + } + } + + PopupMenu::Ptr owner; + PopupMenu::Options options; + int selectedItemID = 0; + int hoveredItemIndex = -1; + std::vector> itemRects; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow) +}; + +//============================================================================== + +namespace +{ + +struct GlobalMouseListener : public MouseListener +{ + void mouseDown (const MouseEvent& event) override + { + Point globalPos = event.getScreenPosition().to(); + + bool clickedInsidePopup = false; + for (auto* popup : activePopups) + { + if (auto* menuWindow = dynamic_cast (popup)) + { + if (menuWindow->isWithinBounds (globalPos)) + { + clickedInsidePopup = true; + break; + } + } + } + + if (! clickedInsidePopup && ! activePopups.empty()) + PopupMenu::dismissAllPopups(); + } +}; + +void installGlobalMouseListener() +{ + static bool mouseListenerAdded = [] + { + static GlobalMouseListener globalMouseListener{}; + Desktop::getInstance()->addGlobalMouseListener (&globalMouseListener); + + MessageManager::getInstance()->registerShutdownCallback([] { PopupMenu::dismissAllPopups(); }); + + return true; + }(); +} + +} // namespace + +//============================================================================== + +PopupMenu::PopupMenu() = default; + +PopupMenu::~PopupMenu() = default; + +//============================================================================== + +PopupMenu::Ptr PopupMenu::create() +{ + return new PopupMenu(); +} + +//============================================================================== + +void PopupMenu::dismissAllPopups() +{ + // Make a copy to avoid issues with the vector being modified during iteration + auto popupsToClose = std::move (activePopups); + + for (auto* popup : popupsToClose) + { + if (auto* menuWindow = dynamic_cast (popup)) + menuWindow->dismiss (0); + } + + activePopups.clear(); +} + +//============================================================================== + +void PopupMenu::addItem (const String& text, int itemID, bool isEnabled, bool isTicked, const String& shortcutText) +{ + auto item = std::make_unique (text, itemID, isEnabled, isTicked); + item->shortcutKeyText = shortcutText; + items.push_back (std::move (item)); +} + +void PopupMenu::addSeparator() +{ + items.push_back (std::make_unique()); +} + +void PopupMenu::addSubMenu (const String& text, PopupMenu::Ptr subMenu, bool isEnabled) +{ + auto item = std::make_unique (text, std::move (subMenu), isEnabled); + items.push_back (std::move (item)); +} + +void PopupMenu::addCustomItem (std::unique_ptr component, int itemID) +{ + auto item = std::make_unique (std::move (component), itemID); + items.push_back (std::move (item)); +} + +void PopupMenu::addItemsFromMenu (const PopupMenu& otherMenu) +{ + for (const auto& otherItem : otherMenu.items) + { + if (otherItem->isSeparator()) + { + addSeparator(); + } + else if (otherItem->isSubMenu()) + { + addSubMenu (otherItem->text, otherItem->subMenu, otherItem->isEnabled); + } + else if (otherItem->isCustomComponent()) + { + // Note: Custom components can't be easily copied, so we skip them + // In a real implementation, you might want to clone them or handle differently + } + else + { + auto item = std::make_unique (otherItem->text, otherItem->itemID, + otherItem->isEnabled, otherItem->isTicked); + item->shortcutKeyText = otherItem->shortcutKeyText; + item->textColor = otherItem->textColor; + items.push_back (std::move (item)); + } + } +} + +//============================================================================== + +int PopupMenu::getNumItems() const +{ + return static_cast (items.size()); +} + +void PopupMenu::clear() +{ + items.clear(); +} + +//============================================================================== + +void PopupMenu::show (const Options& options, std::function callback) +{ + if (isEmpty()) + { + if (callback) + callback (0); + + return; + } + + // Dismiss any existing popups first + dismissAllPopups(); + + // Show the custom menu with callback + showCustom (options, callback); +} + +//============================================================================== + +void PopupMenu::showAt (Point screenPos, std::function callback) +{ + Options options; + options.targetScreenPosition = screenPos; + + show (options, callback); +} + +//============================================================================== + +void PopupMenu::showAt (Component* targetComp, std::function callback) +{ + if (targetComp == nullptr) + { + if (callback) + callback (0); + + return; + } + + Options options; + options.parentComponent = targetComp; + + show (options, callback); +} + +//============================================================================== + +void PopupMenu::showCustom (const Options& options, std::function callback) +{ + installGlobalMouseListener(); + + if (isEmpty()) + { + if (callback) + callback (0); + return; + } + + // Create the menu window with a reference to this menu - it will manage its own lifetime + new MenuWindow (this, options); +} + +} // namespace yup diff --git a/modules/yup_gui/widgets/yup_PopupMenu.h b/modules/yup_gui/widgets/yup_PopupMenu.h new file mode 100644 index 000000000..7aafd6e25 --- /dev/null +++ b/modules/yup_gui/widgets/yup_PopupMenu.h @@ -0,0 +1,180 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2024 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +class PopupMenu; + +/** + A popup menu that can display a list of items. + + This class supports both native system menus and custom rendered menus. +*/ +class JUCE_API PopupMenu : public ReferenceCountedObject +{ +public: + //============================================================================== + /** Convenience typedef for a reference-counted pointer to a PopupMenu. */ + using Ptr = ReferenceCountedObjectPtr; + + //============================================================================== + /** Options for showing the popup menu. */ + struct Options + { + Options() + : useNativeMenus (false) + , parentComponent (nullptr) + , minWidth (0) + , maxWidth (0) + , standardItemHeight (22) + , dismissOnSelection (true) + { + } + + /** Whether to use native system menus (when available). */ + bool useNativeMenus; + + /** The parent component to attach the menu to. */ + Component* parentComponent; + + /** The position to show the menu at (relative to parent). */ + Point targetScreenPosition; + + /** The area to position the menu relative to. */ + Rectangle targetArea; + + /** Minimum width for the menu. */ + int minWidth; + + /** Maximum width for the menu. */ + int maxWidth; + + /** Standard menu item height. */ + int standardItemHeight; + + /** Whether to dismiss the menu when an item is selected. */ + bool dismissOnSelection; + }; + + //============================================================================== + /** Creates an empty popup menu. */ + PopupMenu(); + + /** Destructor. */ + ~PopupMenu(); + + //============================================================================== + + static Ptr create(); + + //============================================================================== + /** Adds a menu item. + + @param text The text to display + @param itemID Unique ID for this item + @param isEnabled Whether the item is enabled + @param isTicked Whether to show a checkmark + @param shortcutText Optional shortcut key text + */ + void addItem (const String& text, int itemID, bool isEnabled = true, bool isTicked = false, const String& shortcutText = {}); + + + + /** Adds a separator line. */ + void addSeparator(); + + /** Adds a sub-menu. + + @param text The text to display for the sub-menu + @param subMenu The sub-menu to show + @param isEnabled Whether the sub-menu is enabled + */ + void addSubMenu (const String& text, PopupMenu::Ptr subMenu, bool isEnabled = true); + + /** Adds a custom component as a menu item (non-native mode only). + + @param component The component to add + @param itemID Unique ID for this item + */ + void addCustomItem (std::unique_ptr component, int itemID); + + /** Adds all items from another menu. */ + void addItemsFromMenu (const PopupMenu& otherMenu); + + //============================================================================== + /** Returns the number of items in the menu. */ + int getNumItems() const; + + /** Returns true if the menu is empty. */ + bool isEmpty() const { return getNumItems() == 0; } + + /** Clears all items from the menu. */ + void clear(); + + //============================================================================== + /** Shows the menu asynchronously and calls the callback when an item is selected. + + @param options Options for showing the menu + @param callback Function to call when an item is selected (optional) + */ + void show (const Options& options = Options{}, std::function callback = nullptr); + + /** Shows the menu at a specific screen position. + + @param screenPos Screen position to show the menu + @param callback Function to call when an item is selected (optional) + */ + void showAt (Point screenPos, std::function callback = nullptr); + + /** Shows the menu relative to a component. + + @param targetComp Component to show the menu relative to + @param callback Function to call when an item is selected (optional) + */ + void showAt (Component* targetComp, std::function callback = nullptr); + + //============================================================================== + /** Callback type for menu item selection. */ + std::function onItemSelected; + + //============================================================================== + /** Dismisses all currently open popup menus. */ + static void dismissAllPopups(); + + //============================================================================== + class MenuWindow; + +private: + //============================================================================== + friend class MenuWindow; + + // PopupMenuItem is now an implementation detail + class PopupMenuItem; + std::vector> items; + + void showCustom (const Options& options, std::function callback); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) +}; + +} // namespace yup diff --git a/modules/yup_gui/yup_gui.cpp b/modules/yup_gui/yup_gui.cpp index d100b5bdf..ea246b567 100644 --- a/modules/yup_gui/yup_gui.cpp +++ b/modules/yup_gui/yup_gui.cpp @@ -97,6 +97,7 @@ #include "widgets/yup_TextButton.cpp" #include "widgets/yup_Label.cpp" #include "widgets/yup_Slider.cpp" +#include "widgets/yup_PopupMenu.cpp" #include "artboard/yup_Artboard.cpp" #include "windowing/yup_DocumentWindow.cpp" #include "themes/yup_ApplicationTheme.cpp" diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index d5e82f898..1954369a5 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -84,6 +84,7 @@ #include "widgets/yup_TextButton.h" #include "widgets/yup_Label.h" #include "widgets/yup_Slider.h" +#include "widgets/yup_PopupMenu.h" #include "artboard/yup_Artboard.h" #include "windowing/yup_DocumentWindow.h" From 9dae7dad14c5c998f0201929cc3b4d4424b827d8 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Fri, 6 Jun 2025 15:05:19 +0000 Subject: [PATCH 05/51] Code formatting --- .../graphics/source/examples/PopupMenuDemo.h | 22 +++++++--- modules/yup_gui/widgets/yup_PopupMenu.cpp | 42 ++++++++++--------- modules/yup_gui/widgets/yup_PopupMenu.h | 12 +++--- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenuDemo.h b/examples/graphics/source/examples/PopupMenuDemo.h index 017a4d81c..d0fc36d0e 100644 --- a/examples/graphics/source/examples/PopupMenuDemo.h +++ b/examples/graphics/source/examples/PopupMenuDemo.h @@ -39,19 +39,31 @@ class PopupMenuDemo : public yup::Component addAndMakeVisible (basicMenuButton); basicMenuButton.setTitle ("Show Basic Menu"); - basicMenuButton.onClick = [this] { showBasicMenu(); }; + basicMenuButton.onClick = [this] + { + showBasicMenu(); + }; addAndMakeVisible (subMenuButton); subMenuButton.setTitle ("Show Sub-Menu"); - subMenuButton.onClick = [this] { showSubMenu(); }; + subMenuButton.onClick = [this] + { + showSubMenu(); + }; addAndMakeVisible (customMenuButton); customMenuButton.setTitle ("Show Custom Menu"); - customMenuButton.onClick = [this] { showCustomMenu(); }; + customMenuButton.onClick = [this] + { + showCustomMenu(); + }; addAndMakeVisible (nativeMenuButton); nativeMenuButton.setTitle ("Show Native Menu"); - nativeMenuButton.onClick = [this] { showNativeMenu(); }; + nativeMenuButton.onClick = [this] + { + showNativeMenu(); + }; setSize ({ 400, 300 }); } @@ -82,7 +94,7 @@ class PopupMenuDemo : public yup::Component g.setFillColor (yup::Color (0xffffffff)); auto styledText = yup::StyledText(); - styledText.appendText("PopupMenu Demo", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + styledText.appendText ("PopupMenu Demo", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); g.fillFittedText (styledText, area.removeFromTop (20).to()); } diff --git a/modules/yup_gui/widgets/yup_PopupMenu.cpp b/modules/yup_gui/widgets/yup_PopupMenu.cpp index d2a859d3c..bb5fbf51e 100644 --- a/modules/yup_gui/widgets/yup_PopupMenu.cpp +++ b/modules/yup_gui/widgets/yup_PopupMenu.cpp @@ -43,17 +43,23 @@ class PopupMenu::PopupMenuItem } PopupMenuItem (const String& itemText, int itemID, bool isEnabled = true, bool isTicked = false) - : text (itemText), itemID (itemID), isEnabled (isEnabled), isTicked (isTicked) + : text (itemText) + , itemID (itemID) + , isEnabled (isEnabled) + , isTicked (isTicked) { } PopupMenuItem (const String& itemText, PopupMenu::Ptr subMenu, bool isEnabled = true) - : text (itemText), isEnabled (isEnabled), subMenu (std::move (subMenu)) + : text (itemText) + , isEnabled (isEnabled) + , subMenu (std::move (subMenu)) { } PopupMenuItem (std::unique_ptr component, int itemID) - : itemID (itemID), customComponent (std::move (component)) + : itemID (itemID) + , customComponent (std::move (component)) { } @@ -361,10 +367,8 @@ class PopupMenu::MenuWindow : public Component auto checkRect = Rectangle (rect.getX() + 4.0f, rect.getY() + 4.0f, 12.0f, 12.0f); g.setStrokeColor (textColor); g.setStrokeWidth (2.0f); - g.strokeLine (checkRect.getX() + 2.0f, checkRect.getCenterY(), - checkRect.getCenterX(), checkRect.getBottom() - 2.0f); - g.strokeLine (checkRect.getCenterX(), checkRect.getBottom() - 2.0f, - checkRect.getRight() - 2.0f, checkRect.getY() + 2.0f); + g.strokeLine (checkRect.getX() + 2.0f, checkRect.getCenterY(), checkRect.getCenterX(), checkRect.getBottom() - 2.0f); + g.strokeLine (checkRect.getCenterX(), checkRect.getBottom() - 2.0f, checkRect.getRight() - 2.0f, checkRect.getY() + 2.0f); } // Draw shortcut text @@ -387,10 +391,8 @@ class PopupMenu::MenuWindow : public Component auto arrowRect = Rectangle (rect.getRight() - 16.0f, rect.getY() + 4.0f, 8.0f, rect.getHeight() - 8.0f); g.setStrokeColor (textColor); g.setStrokeWidth (1.5f); - g.strokeLine (arrowRect.getX() + 2.0f, arrowRect.getY() + 2.0f, - arrowRect.getRight() - 2.0f, arrowRect.getCenterY()); - g.strokeLine (arrowRect.getRight() - 2.0f, arrowRect.getCenterY(), - arrowRect.getX() + 2.0f, arrowRect.getBottom() - 2.0f); + g.strokeLine (arrowRect.getX() + 2.0f, arrowRect.getY() + 2.0f, arrowRect.getRight() - 2.0f, arrowRect.getCenterY()); + g.strokeLine (arrowRect.getRight() - 2.0f, arrowRect.getCenterY(), arrowRect.getX() + 2.0f, arrowRect.getBottom() - 2.0f); } } } @@ -438,10 +440,13 @@ void installGlobalMouseListener() { static bool mouseListenerAdded = [] { - static GlobalMouseListener globalMouseListener{}; + static GlobalMouseListener globalMouseListener {}; Desktop::getInstance()->addGlobalMouseListener (&globalMouseListener); - MessageManager::getInstance()->registerShutdownCallback([] { PopupMenu::dismissAllPopups(); }); + MessageManager::getInstance()->registerShutdownCallback ([] + { + PopupMenu::dismissAllPopups(); + }); return true; }(); @@ -523,8 +528,7 @@ void PopupMenu::addItemsFromMenu (const PopupMenu& otherMenu) } else { - auto item = std::make_unique (otherItem->text, otherItem->itemID, - otherItem->isEnabled, otherItem->isTicked); + auto item = std::make_unique (otherItem->text, otherItem->itemID, otherItem->isEnabled, otherItem->isTicked); item->shortcutKeyText = otherItem->shortcutKeyText; item->textColor = otherItem->textColor; items.push_back (std::move (item)); @@ -546,7 +550,7 @@ void PopupMenu::clear() //============================================================================== -void PopupMenu::show (const Options& options, std::function callback) +void PopupMenu::show (const Options& options, std::function callback) { if (isEmpty()) { @@ -565,7 +569,7 @@ void PopupMenu::show (const Options& options, std::function callback) //============================================================================== -void PopupMenu::showAt (Point screenPos, std::function callback) +void PopupMenu::showAt (Point screenPos, std::function callback) { Options options; options.targetScreenPosition = screenPos; @@ -575,7 +579,7 @@ void PopupMenu::showAt (Point screenPos, std::function callback) //============================================================================== -void PopupMenu::showAt (Component* targetComp, std::function callback) +void PopupMenu::showAt (Component* targetComp, std::function callback) { if (targetComp == nullptr) { @@ -593,7 +597,7 @@ void PopupMenu::showAt (Component* targetComp, std::function callback //============================================================================== -void PopupMenu::showCustom (const Options& options, std::function callback) +void PopupMenu::showCustom (const Options& options, std::function callback) { installGlobalMouseListener(); diff --git a/modules/yup_gui/widgets/yup_PopupMenu.h b/modules/yup_gui/widgets/yup_PopupMenu.h index 7aafd6e25..77a426801 100644 --- a/modules/yup_gui/widgets/yup_PopupMenu.h +++ b/modules/yup_gui/widgets/yup_PopupMenu.h @@ -98,8 +98,6 @@ class JUCE_API PopupMenu : public ReferenceCountedObject */ void addItem (const String& text, int itemID, bool isEnabled = true, bool isTicked = false, const String& shortcutText = {}); - - /** Adds a separator line. */ void addSeparator(); @@ -137,25 +135,25 @@ class JUCE_API PopupMenu : public ReferenceCountedObject @param options Options for showing the menu @param callback Function to call when an item is selected (optional) */ - void show (const Options& options = Options{}, std::function callback = nullptr); + void show (const Options& options = Options {}, std::function callback = nullptr); /** Shows the menu at a specific screen position. @param screenPos Screen position to show the menu @param callback Function to call when an item is selected (optional) */ - void showAt (Point screenPos, std::function callback = nullptr); + void showAt (Point screenPos, std::function callback = nullptr); /** Shows the menu relative to a component. @param targetComp Component to show the menu relative to @param callback Function to call when an item is selected (optional) */ - void showAt (Component* targetComp, std::function callback = nullptr); + void showAt (Component* targetComp, std::function callback = nullptr); //============================================================================== /** Callback type for menu item selection. */ - std::function onItemSelected; + std::function onItemSelected; //============================================================================== /** Dismisses all currently open popup menus. */ @@ -172,7 +170,7 @@ class JUCE_API PopupMenu : public ReferenceCountedObject class PopupMenuItem; std::vector> items; - void showCustom (const Options& options, std::function callback); + void showCustom (const Options& options, std::function callback); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) }; From 818cd3a374950f5dcfac72424699059969164692 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 8 Jun 2025 14:12:58 +0200 Subject: [PATCH 06/51] Improvements to native component tracking --- modules/yup_gui/desktop/yup_Desktop.cpp | 44 +++++++++++ modules/yup_gui/desktop/yup_Desktop.h | 18 ++++- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 77 +++++++++++++++---- modules/yup_gui/yup_gui.h | 1 + 4 files changed, 124 insertions(+), 16 deletions(-) diff --git a/modules/yup_gui/desktop/yup_Desktop.cpp b/modules/yup_gui/desktop/yup_Desktop.cpp index 5b3f4e038..abed91c55 100644 --- a/modules/yup_gui/desktop/yup_Desktop.cpp +++ b/modules/yup_gui/desktop/yup_Desktop.cpp @@ -194,6 +194,24 @@ void Desktop::handleGlobalMouseDrag (const MouseEvent& event) } } +void Desktop::handleGlobalMouseWheel (const MouseEvent& event, const MouseWheelData& wheelData) +{ + auto it = globalMouseListeners.begin(); + while (it != globalMouseListeners.end()) + { + auto* listener = it->get(); + if (listener == nullptr) + { + it = globalMouseListeners.erase (it); + } + else + { + listener->mouseWheel (event, wheelData); + ++it; + } + } +} + //============================================================================== void Desktop::handleScreenConnected (int screenIndex) @@ -216,4 +234,30 @@ void Desktop::handleScreenOrientationChanged (int screenIndex) updateScreens(); } +//============================================================================== + +void Desktop::registerNativeComponent (ComponentNative* nativeComponent) +{ + if (nativeComponent != nullptr) + nativeComponents[nativeComponent] = nativeComponent; +} + +void Desktop::unregisterNativeComponent (ComponentNative* nativeComponent) +{ + if (nativeComponent != nullptr) + nativeComponents.erase (nativeComponent); +} + +ComponentNative::Ptr Desktop::getNativeComponent (void* userdata) const +{ + if (userdata == nullptr) + return nullptr; + + auto it = nativeComponents.find (userdata); + if (it != nativeComponents.end()) + return ComponentNative::Ptr{ it->second }; + + return nullptr; +} + } // namespace yup diff --git a/modules/yup_gui/desktop/yup_Desktop.h b/modules/yup_gui/desktop/yup_Desktop.h index be268feb2..f899c8d1e 100644 --- a/modules/yup_gui/desktop/yup_Desktop.h +++ b/modules/yup_gui/desktop/yup_Desktop.h @@ -22,6 +22,8 @@ namespace yup { +class ComponentNative; + //============================================================================== /** Represents the desktop environment, providing access to screen information and management. @@ -116,6 +118,14 @@ class JUCE_API Desktop */ void removeGlobalMouseListener (MouseListener* listener); + //============================================================================== + /** Gets a native component by its userdata pointer. + + @param userdata The userdata pointer used to identify the component + @return The native component, or nullptr if not found + */ + ReferenceCountedObjectPtr getNativeComponent (void* userdata) const; + //============================================================================== /** Updates the list of screens. */ void updateScreens(); @@ -129,7 +139,6 @@ class JUCE_API Desktop void handleScreenMoved (int screenIndex); /** @internal */ void handleScreenOrientationChanged (int screenIndex); - /** @internal */ void handleGlobalMouseDown (const MouseEvent& event); /** @internal */ @@ -138,19 +147,26 @@ class JUCE_API Desktop void handleGlobalMouseMove (const MouseEvent& event); /** @internal */ void handleGlobalMouseDrag (const MouseEvent& event); + /** @internal */ + void handleGlobalMouseWheel (const MouseEvent& event, const MouseWheelData& wheelData); //============================================================================== JUCE_DECLARE_SINGLETON (Desktop, false) private: friend class YUPApplication; + friend class SDL2ComponentNative; Desktop(); + void registerNativeComponent (ComponentNative* nativeComponent); + void unregisterNativeComponent (ComponentNative* nativeComponent); + Screen::Array screens; std::optional currentMouseCursor; std::vector> globalMouseListeners; + std::unordered_map nativeComponents; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Desktop) }; diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index dee695ee5..b33960915 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -58,6 +58,8 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, { incReferenceCount(); + Desktop::getInstance()->registerNativeComponent (this); + SDL_AddEventWatch (eventDispatcher, this); // Setup window hints and get flags @@ -126,12 +128,15 @@ SDL2ComponentNative::SDL2ComponentNative (Component& component, SDL2ComponentNative::~SDL2ComponentNative() { - // Stop the rendering - stopRendering(); - // Remove event watch SDL_DelEventWatch (eventDispatcher, this); + // Unregister this component from the desktop + Desktop::getInstance()->unregisterNativeComponent (this); + + // Stop the rendering + stopRendering(); + // Destroy the window if (window != nullptr) { @@ -1163,7 +1168,7 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_MOUSEMOTION: { - auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + auto cursorPosition = Point { static_cast (event->motion.x), static_cast (event->motion.y) }; if (event->window.windowID == SDL_GetWindowID (window)) handleMouseMoveOrDrag (cursorPosition); @@ -1176,7 +1181,7 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; if (event->button.windowID == SDL_GetWindowID (window)) - handleMouseDown (cursorPosition, toMouseButton (event->button.button), KeyModifiers()); + handleMouseDown (cursorPosition, toMouseButton (event->button.button), KeyModifiers (SDL_GetModState())); break; } @@ -1186,7 +1191,7 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; if (event->button.windowID == SDL_GetWindowID (window)) - handleMouseUp (cursorPosition, toMouseButton (event->button.button), KeyModifiers()); + handleMouseUp (cursorPosition, toMouseButton (event->button.button), KeyModifiers (SDL_GetModState())); break; } @@ -1268,8 +1273,9 @@ int SDL2ComponentNative::eventDispatcher (void* userdata, SDL_Event* event) default: { - auto nativeComponent = SDL2ComponentNative::Ptr (static_cast (userdata)); - nativeComponent->handleEvent (event); + if (auto nativeComponent = Desktop::getInstance()->getNativeComponent (userdata)) + dynamic_cast (nativeComponent.get())->handleEvent (event); + break; } } @@ -1321,43 +1327,84 @@ int displayEventDispatcher (void* userdata, SDL_Event* event) break; } - return; + return 0; } switch (event->type) { case SDL_MOUSEMOTION: { - auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + int x = 0, y = 0; + SDL_GetGlobalMouseState (&x, &y); + auto cursorPosition = Point { static_cast (x), static_cast (y) }; + auto keyModifiers = toKeyModifiers (SDL_GetModState()); + + MouseEvent mouseEvent ( + static_cast (event->motion.state), + keyModifiers, + cursorPosition + ); + + // Call drag handler if any mouse buttons are pressed, otherwise call move handler + if (event->motion.state != 0) + desktop->handleGlobalMouseDrag (mouseEvent); + else + desktop->handleGlobalMouseMove (mouseEvent); break; } case SDL_MOUSEBUTTONDOWN: { - auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + int x = 0, y = 0; + SDL_GetGlobalMouseState (&x, &y); + auto cursorPosition = Point { static_cast (x), static_cast (y) }; auto button = toMouseButton (event->button.button); - auto keyModifiers = KeyModifiers(); + auto keyModifiers = toKeyModifiers (SDL_GetModState()); + + MouseEvent mouseEvent ( + button, + keyModifiers, + cursorPosition + ); + desktop->handleGlobalMouseDown (mouseEvent); break; } case SDL_MOUSEBUTTONUP: { - auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; + int x = 0, y = 0; + SDL_GetGlobalMouseState (&x, &y); + auto cursorPosition = Point { static_cast (x), static_cast (y) }; auto button = toMouseButton (event->button.button); - auto keyModifiers = KeyModifiers(); + auto keyModifiers = toKeyModifiers (SDL_GetModState()); + MouseEvent mouseEvent ( + button, + keyModifiers, + cursorPosition + ); + + desktop->handleGlobalMouseUp (mouseEvent); break; } case SDL_MOUSEWHEEL: { int x = 0, y = 0; - SDL_GetMouseState (&x, &y); + SDL_GetGlobalMouseState (&x, &y); auto cursorPosition = Point { static_cast (x), static_cast (y) }; + auto keyModifiers = toKeyModifiers (SDL_GetModState()); auto mouseWheelData = MouseWheelData { static_cast (event->wheel.x), static_cast (event->wheel.y) }; + MouseEvent mouseEvent ( + MouseEvent::noButtons, + keyModifiers, + cursorPosition + ); + + desktop->handleGlobalMouseWheel (mouseEvent, mouseWheelData); break; } diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index 1954369a5..282119df7 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -65,6 +65,7 @@ #include #include +#include //============================================================================== From 0ff4e7e1fd1b81f2da72b480205964e1d88e0b5d Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Sun, 8 Jun 2025 12:13:32 +0000 Subject: [PATCH 07/51] Code formatting --- modules/yup_gui/desktop/yup_Desktop.cpp | 2 +- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/modules/yup_gui/desktop/yup_Desktop.cpp b/modules/yup_gui/desktop/yup_Desktop.cpp index abed91c55..8976ded3e 100644 --- a/modules/yup_gui/desktop/yup_Desktop.cpp +++ b/modules/yup_gui/desktop/yup_Desktop.cpp @@ -255,7 +255,7 @@ ComponentNative::Ptr Desktop::getNativeComponent (void* userdata) const auto it = nativeComponents.find (userdata); if (it != nativeComponents.end()) - return ComponentNative::Ptr{ it->second }; + return ComponentNative::Ptr { it->second }; return nullptr; } diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index b33960915..f09d805ba 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -1342,8 +1342,7 @@ int displayEventDispatcher (void* userdata, SDL_Event* event) MouseEvent mouseEvent ( static_cast (event->motion.state), keyModifiers, - cursorPosition - ); + cursorPosition); // Call drag handler if any mouse buttons are pressed, otherwise call move handler if (event->motion.state != 0) @@ -1365,8 +1364,7 @@ int displayEventDispatcher (void* userdata, SDL_Event* event) MouseEvent mouseEvent ( button, keyModifiers, - cursorPosition - ); + cursorPosition); desktop->handleGlobalMouseDown (mouseEvent); break; @@ -1383,8 +1381,7 @@ int displayEventDispatcher (void* userdata, SDL_Event* event) MouseEvent mouseEvent ( button, keyModifiers, - cursorPosition - ); + cursorPosition); desktop->handleGlobalMouseUp (mouseEvent); break; @@ -1401,8 +1398,7 @@ int displayEventDispatcher (void* userdata, SDL_Event* event) MouseEvent mouseEvent ( MouseEvent::noButtons, keyModifiers, - cursorPosition - ); + cursorPosition); desktop->handleGlobalMouseWheel (mouseEvent, mouseWheelData); break; From bdfbe38f05c16a97f9377dfea4ef92f75f952dfe Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 8 Jun 2025 17:37:31 +0200 Subject: [PATCH 08/51] More tweaks --- modules/yup_gui/component/yup_Component.cpp | 21 +++++++++++++++---- modules/yup_gui/component/yup_Component.h | 3 ++- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 20 ++++++++++++++++++ modules/yup_gui/widgets/yup_PopupMenu.cpp | 12 +++++------ 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index e7894637b..2f6d73bed 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -95,10 +95,13 @@ void Component::setVisible (bool shouldBeVisible) options.isVisible = shouldBeVisible; + auto bailOutChecker = BailOutChecker (this); + if (options.onDesktop && native != nullptr) native->setVisible (shouldBeVisible); - auto bailOutChecker = BailOutChecker (this); + if (bailOutChecker.shouldBailOut()) + return; visibilityChanged(); @@ -138,7 +141,7 @@ void Component::setTitle (const String& title) { componentTitle = title; - if (options.onDesktop) + if (options.onDesktop && native != nullptr) native->setTitle (title); } @@ -441,7 +444,7 @@ void Component::displayChanged() {} float Component::getScaleDpi() const { - if (native != nullptr) + if (options.onDesktop && native != nullptr) return native->getScaleDpi(); if (parentComponent == nullptr) @@ -460,7 +463,7 @@ void Component::setOpacity (float newOpacity) opacity = static_cast (newOpacity * 255); - if (native != nullptr) + if (options.onDesktop && native != nullptr) native->setOpacity (newOpacity); } @@ -1251,6 +1254,16 @@ void Component::internalMoved (int xpos, int ypos) //============================================================================== +void Component::internalFocusChanged (bool gotFocus) +{ + if (gotFocus) + focusGained(); + else + focusLost(); +} + +//============================================================================== + void Component::internalDisplayChanged() {} //============================================================================== diff --git a/modules/yup_gui/component/yup_Component.h b/modules/yup_gui/component/yup_Component.h index d97f817be..c28b7a7f4 100644 --- a/modules/yup_gui/component/yup_Component.h +++ b/modules/yup_gui/component/yup_Component.h @@ -906,8 +906,9 @@ class JUCE_API Component void internalKeyDown (const KeyPress& keys, const Point& position); void internalKeyUp (const KeyPress& keys, const Point& position); void internalTextInput (const String& text); - void internalMoved (int xpos, int ypos); void internalResized (int width, int height); + void internalMoved (int xpos, int ypos); + void internalFocusChanged (bool gotFocus); void internalDisplayChanged(); void internalContentScaleChanged (float dpiScale); void internalUserTriedToCloseWindow(); diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index f09d805ba..5ec66df94 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -961,9 +961,17 @@ void SDL2ComponentNative::handleFocusChanged (bool gotFocus) if (! isRendering()) startRendering(); + + component.internalFocusChanged (true); } else { + component.internalFocusChanged (false); + + lastComponentClicked = nullptr; + lastMouseDownPosition.reset(); + lastMouseDownTime.reset(); + SDL_StopTextInput(); if (updateOnlyWhenFocused) @@ -1168,6 +1176,8 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_MOUSEMOTION: { + //YUP_WINDOWING_LOG ("SDL_MOUSEMOTION " << event->motion.x << " " << event->motion.y); + auto cursorPosition = Point { static_cast (event->motion.x), static_cast (event->motion.y) }; if (event->window.windowID == SDL_GetWindowID (window)) @@ -1178,26 +1188,36 @@ void SDL2ComponentNative::handleEvent (SDL_Event* event) case SDL_MOUSEBUTTONDOWN: { + YUP_WINDOWING_LOG ("SDL_MOUSEBUTTONDOWN " << event->button.x << " " << event->button.y); + auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; if (event->button.windowID == SDL_GetWindowID (window)) handleMouseDown (cursorPosition, toMouseButton (event->button.button), KeyModifiers (SDL_GetModState())); + else + ; // TODO - when opening a window in mouse down, mouse up is sent to the other window break; } case SDL_MOUSEBUTTONUP: { + YUP_WINDOWING_LOG ("SDL_MOUSEBUTTONUP " << event->button.x << " " << event->button.y); + auto cursorPosition = Point { static_cast (event->button.x), static_cast (event->button.y) }; if (event->button.windowID == SDL_GetWindowID (window)) handleMouseUp (cursorPosition, toMouseButton (event->button.button), KeyModifiers (SDL_GetModState())); + else + ; // TODO - when opening a window in mouse down, mouse up is sent to the other window break; } case SDL_MOUSEWHEEL: { + YUP_WINDOWING_LOG ("SDL_MOUSEWHEEL " << event->wheel.x << " " << event->wheel.y); + auto cursorPosition = getCursorPosition(); if (event->wheel.windowID == SDL_GetWindowID (window)) diff --git a/modules/yup_gui/widgets/yup_PopupMenu.cpp b/modules/yup_gui/widgets/yup_PopupMenu.cpp index bb5fbf51e..b7560c6c8 100644 --- a/modules/yup_gui/widgets/yup_PopupMenu.cpp +++ b/modules/yup_gui/widgets/yup_PopupMenu.cpp @@ -27,7 +27,7 @@ namespace yup namespace { -static std::vector activePopups; +static std::vector> activePopups; } // namespace @@ -211,7 +211,7 @@ class PopupMenu::MenuWindow : public Component void dismiss (int itemID) { selectedItemID = itemID; - setVisible (false); + //setVisible (false); // Call the owner's callback if (owner->onItemSelected) @@ -419,9 +419,9 @@ struct GlobalMouseListener : public MouseListener Point globalPos = event.getScreenPosition().to(); bool clickedInsidePopup = false; - for (auto* popup : activePopups) + for (const auto& popup : activePopups) { - if (auto* menuWindow = dynamic_cast (popup)) + if (auto* menuWindow = dynamic_cast (popup.get())) { if (menuWindow->isWithinBounds (globalPos)) { @@ -474,9 +474,9 @@ void PopupMenu::dismissAllPopups() // Make a copy to avoid issues with the vector being modified during iteration auto popupsToClose = std::move (activePopups); - for (auto* popup : popupsToClose) + for (const auto& popup : popupsToClose) { - if (auto* menuWindow = dynamic_cast (popup)) + if (auto* menuWindow = dynamic_cast (popup.get())) menuWindow->dismiss (0); } From 5117eff6754877ea7e82231d76efc5cfcb9aaaae Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Fri, 13 Jun 2025 06:15:14 +0000 Subject: [PATCH 09/51] Code formatting --- examples/graphics/source/examples/Audio.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index fd776e656..c16073376 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -123,7 +123,7 @@ class Oscilloscope : public yup::Component for (std::size_t i = 1; i < renderData.size(); ++i) path.lineTo (i * xSize, (renderData[i] + 1.0f) * 0.5f * getHeight()); - filledPath = path.createStrokePolygon(4.0f); + filledPath = path.createStrokePolygon (4.0f); g.setFillColor (lineColor); g.setFeather (8.0f); From 7e0900c63b5f0a8e937571c0669d6dc15e9a757a Mon Sep 17 00:00:00 2001 From: kunitoki Date: Fri, 13 Jun 2025 09:02:09 +0200 Subject: [PATCH 10/51] More popup work --- .../examples/{PopupMenuDemo.h => PopupMenu.h} | 16 +++---- examples/graphics/source/main.cpp | 2 +- modules/yup_graphics/fonts/yup_StyledText.cpp | 9 ---- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 45 ++++++++++++------- modules/yup_gui/native/yup_Windowing_sdl2.h | 1 + modules/yup_gui/widgets/yup_PopupMenu.cpp | 31 +++++++------ modules/yup_gui/widgets/yup_PopupMenu.h | 6 +-- 7 files changed, 56 insertions(+), 54 deletions(-) rename examples/graphics/source/examples/{PopupMenuDemo.h => PopupMenu.h} (95%) diff --git a/examples/graphics/source/examples/PopupMenuDemo.h b/examples/graphics/source/examples/PopupMenu.h similarity index 95% rename from examples/graphics/source/examples/PopupMenuDemo.h rename to examples/graphics/source/examples/PopupMenu.h index d0fc36d0e..1609f530a 100644 --- a/examples/graphics/source/examples/PopupMenuDemo.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -83,18 +83,15 @@ class PopupMenuDemo : public yup::Component void paint (yup::Graphics& g) override { - auto area = getLocalBounds().reduced (20); - - g.setFillColor (yup::Color (0xff1e1e1e)); - g.fillAll(); + auto area = getLocalBounds().reduced (5); - g.setStrokeColor (yup::Color (0xff555555)); - g.setStrokeWidth (1.0f); - g.strokeRect (getLocalBounds().to().reduced (2.0f)); + auto styledText = yup::StyledText(); + { + auto modifier = styledText.startUpdate(); + modifier.appendText ("PopupMenu Demo", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + } g.setFillColor (yup::Color (0xffffffff)); - auto styledText = yup::StyledText(); - styledText.appendText ("PopupMenu Demo", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); g.fillFittedText (styledText, area.removeFromTop (20).to()); } @@ -207,6 +204,7 @@ class PopupMenuDemo : public yup::Component auto button = std::make_unique ("CustomButton"); button->setSize ({ 120, 30 }); button->setTitle ("Custom Button"); + button->onClick = [] { YUP_DBG("Clicked!"); }; menu->addCustomItem (std::move (button), customButton); menu->addSeparator(); diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index e4d6fb609..23bace20d 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -33,7 +33,7 @@ #include "examples/VariableFonts.h" #include "examples/TextEditor.h" #include "examples/Paths.h" -#include "examples/PopupMenuDemo.h" +#include "examples/PopupMenu.h" //============================================================================== diff --git a/modules/yup_graphics/fonts/yup_StyledText.cpp b/modules/yup_graphics/fonts/yup_StyledText.cpp index e2b2d37e2..b12f00908 100644 --- a/modules/yup_graphics/fonts/yup_StyledText.cpp +++ b/modules/yup_graphics/fonts/yup_StyledText.cpp @@ -253,15 +253,6 @@ void StyledText::setWrap (TextWrap value) //============================================================================== -void StyledText::appendText (StringRef text, - const Font& font, - float fontSize, - float lineHeight, - float letterSpacing) -{ - appendText (text, nullptr, font, fontSize, lineHeight, letterSpacing); -} - void StyledText::appendText (StringRef text, rive::rcp paint, const Font& font, diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 22ff6871c..6c85c8c75 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -359,18 +359,31 @@ float SDL2ComponentNative::getOpacity() const void SDL2ComponentNative::setFocusedComponent (Component* comp) { + auto compBailOut = Component::BailOutChecker (comp); + if (lastComponentFocused != nullptr) { + auto focusBailOut = Component::BailOutChecker (lastComponentFocused.get()); + lastComponentFocused->focusLost(); - lastComponentFocused->repaint(); + + if (! focusBailOut.shouldBailOut()) + lastComponentFocused->repaint(); } + if (compBailOut.shouldBailOut()) + return; + lastComponentFocused = comp; - if (lastComponentFocused) + if (lastComponentFocused != nullptr) { + auto focusBailOut = Component::BailOutChecker (lastComponentFocused.get()); + lastComponentFocused->focusGained(); - lastComponentFocused->repaint(); + + if (! focusBailOut.shouldBailOut()) + lastComponentFocused->repaint(); } if (window != nullptr) @@ -763,20 +776,7 @@ void SDL2ComponentNative::handleMouseDown (const Point& position, MouseEv event = event.withSourceComponent (lastComponentClicked); - if (lastMouseDownTime - && lastMouseDownPosition - && *lastMouseDownTime > yup::Time() - && currentMouseDownTime - *lastMouseDownTime < doubleClickTime) - { - event = event.withLastMouseDownPosition (*lastMouseDownPosition); - event = event.withLastMouseDownTime (*lastMouseDownTime); - - lastComponentClicked->internalMouseDoubleClick (event.withRelativePositionTo (lastComponentClicked)); - } - else - { - lastComponentClicked->internalMouseDown (event.withRelativePositionTo (lastComponentClicked)); - } + lastComponentClicked->internalMouseDown (event.withRelativePositionTo (lastComponentClicked)); lastMouseDownPosition = position; lastMouseDownTime = currentMouseDownTime; @@ -803,9 +803,20 @@ void SDL2ComponentNative::handleMouseUp (const Point& position, MouseEven if (lastComponentClicked != nullptr) { + const auto currentMouseDownTime = yup::Time::getCurrentTime(); + event = event.withSourceComponent (lastComponentClicked); + if (lastMouseUpTime + && *lastMouseUpTime > yup::Time() + && currentMouseDownTime - *lastMouseUpTime < doubleClickTime) + { + lastComponentClicked->internalMouseDoubleClick (event.withRelativePositionTo (lastComponentClicked)); + } + lastComponentClicked->internalMouseUp (event.withRelativePositionTo (lastComponentClicked)); + + lastMouseUpTime = currentMouseDownTime; } if (currentMouseButtons == MouseEvent::noButtons) diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.h b/modules/yup_gui/native/yup_Windowing_sdl2.h index 21eca3512..ba5fc1f32 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.h +++ b/modules/yup_gui/native/yup_Windowing_sdl2.h @@ -169,6 +169,7 @@ class SDL2ComponentNative final Point lastMouseMovePosition = { -1.0f, -1.0f }; std::optional> lastMouseDownPosition; std::optional lastMouseDownTime; + std::optional lastMouseUpTime; WeakReference lastComponentClicked; WeakReference lastComponentFocused; diff --git a/modules/yup_gui/widgets/yup_PopupMenu.cpp b/modules/yup_gui/widgets/yup_PopupMenu.cpp index b7560c6c8..08e5045e9 100644 --- a/modules/yup_gui/widgets/yup_PopupMenu.cpp +++ b/modules/yup_gui/widgets/yup_PopupMenu.cpp @@ -94,7 +94,7 @@ class PopupMenu::PopupMenuItem std::optional textColor; private: - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenuItem) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenuItem) }; //============================================================================== @@ -285,25 +285,21 @@ class PopupMenu::MenuWindow : public Component void positionMenu() { - Rectangle bounds; + Point position; if (options.parentComponent) { - auto parentBounds = options.parentComponent->getBoundsRelativeToTopLevelComponent(); + position = options.parentComponent->getBoundsRelativeToTopLevelComponent().getBottomLeft(); if (auto native = options.parentComponent->getNativeComponent()) - parentBounds = native->getBounds(); - - bounds.setTopLeft ({ parentBounds.getX() + options.targetScreenPosition.getX(), - parentBounds.getY() + options.targetScreenPosition.getY() }); + position.translate (native->getPosition()); } else { - bounds.setTopLeft (options.targetScreenPosition); + position = options.targetScreenPosition; } - bounds.setSize (getWidth(), getHeight()); - setBounds (bounds.roundToInt()); + setTopLeft (position.roundToInt()); } int getItemIndexAt (Point position) const @@ -313,6 +309,7 @@ class PopupMenu::MenuWindow : public Component if (itemRects[i].contains (position)) return i; } + return -1; } @@ -357,7 +354,10 @@ class PopupMenu::MenuWindow : public Component { auto styledText = yup::StyledText(); - styledText.appendText (item.text, itemFont); + { + auto modifier = styledText.startUpdate(); + modifier.appendText (item.text, itemFont); + } g.fillFittedText (styledText, textRect); } @@ -378,8 +378,11 @@ class PopupMenu::MenuWindow : public Component g.setOpacity (0.7f); auto styledText = yup::StyledText(); - styledText.setHorizontalAlign (yup::StyledText::right); - styledText.appendText (item.shortcutKeyText, itemFont); + { + auto modifier = styledText.startUpdate(); + modifier.setHorizontalAlign (yup::StyledText::right); + modifier.appendText (item.shortcutKeyText, itemFont); + } g.fillFittedText (styledText, shortcutRect); g.setOpacity (1.0f); @@ -404,7 +407,7 @@ class PopupMenu::MenuWindow : public Component int hoveredItemIndex = -1; std::vector> itemRects; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow) }; //============================================================================== diff --git a/modules/yup_gui/widgets/yup_PopupMenu.h b/modules/yup_gui/widgets/yup_PopupMenu.h index 77a426801..c0af17d96 100644 --- a/modules/yup_gui/widgets/yup_PopupMenu.h +++ b/modules/yup_gui/widgets/yup_PopupMenu.h @@ -23,14 +23,12 @@ namespace yup { //============================================================================== -class PopupMenu; - /** A popup menu that can display a list of items. This class supports both native system menus and custom rendered menus. */ -class JUCE_API PopupMenu : public ReferenceCountedObject +class YUP_API PopupMenu : public ReferenceCountedObject { public: //============================================================================== @@ -172,7 +170,7 @@ class JUCE_API PopupMenu : public ReferenceCountedObject void showCustom (const Options& options, std::function callback); - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) }; } // namespace yup From 7a16bdee41266dea9c6a2124535f4ea922fe05bb Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Fri, 13 Jun 2025 07:02:44 +0000 Subject: [PATCH 11/51] Code formatting --- examples/graphics/source/examples/PopupMenu.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 1609f530a..8fc358a12 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -204,7 +204,10 @@ class PopupMenuDemo : public yup::Component auto button = std::make_unique ("CustomButton"); button->setSize ({ 120, 30 }); button->setTitle ("Custom Button"); - button->onClick = [] { YUP_DBG("Clicked!"); }; + button->onClick = [] + { + YUP_DBG ("Clicked!"); + }; menu->addCustomItem (std::move (button), customButton); menu->addSeparator(); From 6c3959534df44d231187f7f676bf78185474a169 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Fri, 13 Jun 2025 13:08:56 +0200 Subject: [PATCH 12/51] Coverage updates --- codecov.yml | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index 292e08e32..9aaa2865c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -25,6 +25,57 @@ coverage: target: 70% threshold: 5% +component_management: + default_rules: + statuses: + - type: project + target: auto + branches: + - "!main" + + individual_components: + - component_id: yup_core + name: "YUP Core" + description: "Core functionality of the YUP framework." + paths: + - modules/yup_core/** + + - component_id: yup_events + name: "YUP Events" + description: "Event handling system for the YUP framework." + paths: + - modules/yup_events/** + + - component_id: yup_audio_basics + name: "YUP Audio Basics" + description: "Basic audio functionalities in the YUP framework." + paths: + - modules/yup_audio_basics/** + + - component_id: yup_audio_devices + name: "YUP Audio Devices" + description: "Audio device management in the YUP framework." + paths: + - modules/yup_audio_devices/** + + - component_id: yup_audio_processors + name: "YUP Audio Processors" + description: "Audio processing capabilities in the YUP framework." + paths: + - modules/yup_audio_processors/** + + - component_id: yup_graphics + name: "YUP Graphics" + description: "Graphics rendering and management in the YUP framework." + paths: + - modules/yup_graphics/** + + - component_id: yup_gui + name: "YUP GUI" + description: "Graphical User Interface components of the YUP framework." + paths: + - modules/yup_gui/** + flags: yup_core: paths: @@ -73,6 +124,6 @@ ignore: - "**/*_wasm.*" comment: - layout: "reach,diff,flags,files,footer" + layout: "header, reach, components, diff, flags, files, footer" behavior: default - require_changes: false \ No newline at end of file + require_changes: false From 6c9602c075e013143a7e64c9eb7221fa54e51ef7 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Fri, 13 Jun 2025 17:18:07 +0200 Subject: [PATCH 13/51] Moved menu --- examples/graphics/source/examples/PopupMenu.h | 57 ++- modules/yup_graphics/fonts/yup_Font.h | 129 +++++- .../yup_graphics/layout/yup_Justification.h | 51 +++ .../yup_graphics/primitives/yup_Rectangle.h | 9 + modules/yup_graphics/yup_graphics.h | 1 + .../yup_gui/clipboard/yup_SystemClipboard.h | 16 +- .../{widgets => menus}/yup_PopupMenu.cpp | 394 ++++++++++++++---- .../{widgets => menus}/yup_PopupMenu.h | 78 ++-- modules/yup_gui/yup_gui.cpp | 2 +- modules/yup_gui/yup_gui.h | 2 +- 10 files changed, 599 insertions(+), 140 deletions(-) create mode 100644 modules/yup_graphics/layout/yup_Justification.h rename modules/yup_gui/{widgets => menus}/yup_PopupMenu.cpp (58%) rename modules/yup_gui/{widgets => menus}/yup_PopupMenu.h (80%) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 8fc358a12..3401747b8 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -136,7 +136,10 @@ class PopupMenuDemo : public yup::Component void showBasicMenu() { - auto menu = yup::PopupMenu::create(); + auto options = yup::PopupMenu::Options{} + .withParentComponent(&basicMenuButton); + + auto menu = yup::PopupMenu::create (options); menu->addItem ("New File", newFile, true, false, "Cmd+N"); menu->addItem ("Open File", openFile, true, false, "Cmd+O"); @@ -154,7 +157,7 @@ class PopupMenuDemo : public yup::Component handleMenuSelection (selectedID); }; - menu->showAt (&basicMenuButton); + menu->show(); } void showSubMenu() @@ -168,7 +171,9 @@ class PopupMenuDemo : public yup::Component colorMenu->addItem ("Green", colorGreen); colorMenu->addItem ("Blue", colorBlue); - auto menu = yup::PopupMenu::create(); + auto options = yup::PopupMenu::Options{} + .withParentComponent(&subMenuButton); + auto menu = yup::PopupMenu::create (options); menu->addItem ("New", newFile); menu->addItem ("Open", openFile); menu->addSubMenu ("Recent Files", recentFilesMenu); @@ -182,12 +187,14 @@ class PopupMenuDemo : public yup::Component handleMenuSelection (selectedID); }; - menu->showAt (&subMenuButton); + menu->show(); } void showCustomMenu() { - auto menu = yup::PopupMenu::create(); + auto options = yup::PopupMenu::Options{} + .withParentComponent(&customMenuButton); + auto menu = yup::PopupMenu::create (options); menu->addItem ("Regular Item", 1); menu->addSeparator(); @@ -218,12 +225,17 @@ class PopupMenuDemo : public yup::Component handleMenuSelection (selectedID); }; - menu->showAt (&customMenuButton); + menu->show(); } void showNativeMenu() { - auto menu = yup::PopupMenu::create(); + auto options = yup::PopupMenu::Options{} + .withNativeMenus(true) + .withJustification(yup::Justification::topLeft) + .withParentComponent(this); + + auto menu = yup::PopupMenu::create (options); menu->addItem ("Native Item 1", 1); menu->addItem ("Native Item 2", 2); @@ -235,11 +247,7 @@ class PopupMenuDemo : public yup::Component handleMenuSelection (selectedID); }; - yup::PopupMenu::Options options; - options.useNativeMenus = true; - options.parentComponent = this; - - menu->show (options, [this] (int selectedID) + menu->show ([this] (int selectedID) { handleMenuSelection (selectedID); }); @@ -247,7 +255,12 @@ class PopupMenuDemo : public yup::Component void showContextMenu (yup::Point position) { - auto contextMenu = yup::PopupMenu::create(); + auto options = yup::PopupMenu::Options{} + // .withTargetScreenPosition(position.to()) // TODO: doesn't seem to work + .withParentComponent(this) + .withAsChildToTopmost(true); + + auto contextMenu = yup::PopupMenu::create (options); contextMenu->addItem ("Copy", editCopy); contextMenu->addItem ("Paste", editPaste); @@ -261,11 +274,7 @@ class PopupMenuDemo : public yup::Component handleMenuSelection (selectedID); }; - yup::PopupMenu::Options options; - options.parentComponent = this; - options.targetScreenPosition = position.to(); - - contextMenu->show (options, [this] (int selectedID) + contextMenu->show ([this] (int selectedID) { handleMenuSelection (selectedID); }); @@ -280,39 +289,51 @@ class PopupMenuDemo : public yup::Component case newFile: message = "New File selected"; break; + case openFile: message = "Open File selected"; break; + case saveFile: message = "Save File selected"; break; + case saveAsFile: message = "Save As selected"; break; + case exitApp: message = "Exit selected"; break; + case editCopy: message = "Copy selected"; break; + case editPaste: message = "Paste selected"; break; + case colorRed: message = "Red color selected"; break; + case colorGreen: message = "Green color selected"; break; + case colorBlue: message = "Blue color selected"; break; + case customSlider: message = "Custom slider interacted"; break; + case customButton: message = "Custom button clicked"; break; + default: break; } diff --git a/modules/yup_graphics/fonts/yup_Font.h b/modules/yup_graphics/fonts/yup_Font.h index 892634f54..3bc786af9 100644 --- a/modules/yup_graphics/fonts/yup_Font.h +++ b/modules/yup_graphics/fonts/yup_Font.h @@ -23,11 +23,15 @@ namespace yup { //============================================================================== +/** Font. + This class represents a font. +*/ class YUP_API Font { public: //============================================================================== + /** Creates an empty font. */ Font() = default; //============================================================================== @@ -38,18 +42,39 @@ class YUP_API Font Font& operator= (Font&& other) noexcept = default; //============================================================================== + /** Loads a font from a memory block. + + @param fontBytes The memory block containing the font data. + @return The result of the operation. + */ Result loadFromData (const MemoryBlock& fontBytes); //============================================================================== + /** Loads a font from a file. + + @param fontFile The file containing the font data. + @return The result of the operation. + */ Result loadFromFile (const File& fontFile); //============================================================================== + /** Returns the ascent of the font. */ float getAscent() const; + + /** Returns the descent of the font. */ float getDescent() const; + + /** Returns the weight of the font. */ int getWeight() const; + + /** Returns true if the font is italic. */ bool isItalic() const; //============================================================================== + /** Axis. + + This struct represents an axis of the font. + */ struct Axis { Axis() = default; @@ -60,19 +85,102 @@ class YUP_API Font float defaultValue = 0.0f; }; + /** Returns the number of axes in the font. + + @return The number of axes in the font. + */ int getNumAxis() const; + + /** Returns the description of the axis at the given index. + + @param index The index of the axis. + + @return The description of the axis. + */ std::optional getAxisDescription (int index) const; + + /** Returns the description of the axis with the given tag name. + + @param tagName The tag name of the axis. + + @return The description of the axis. + */ std::optional getAxisDescription (StringRef tagName) const; + //============================================================================== + /** Returns the value of the axis at the given index. + + @param index The index of the axis. + + @return The value of the axis. + */ float getAxisValue (int index) const; - float getAxisValue (StringRef tagName) const; + /** Sets the value of the axis at the given index. + + @param index The index of the axis. + @param value The value of the axis. + */ void setAxisValue (int index, float value); - void setAxisValue (StringRef tagName, float value); + /** Returns a new font with the value of the axis at the given index. + + @param index The index of the axis. + @param value The value of the axis. + + @return A new font with the value of the axis at the given index. + */ Font withAxisValue (int index, float value) const; + + /** Resets the value of the axis at the given index. + + @param index The index of the axis. + */ + void resetAxisValue (int index); + + //============================================================================== + /** Returns the value of the axis with the given tag name. + + @param tagName The tag name of the axis. + + @return The value of the axis. + */ + float getAxisValue (StringRef tagName) const; + + /** Sets the value of the axis with the given tag name. + + @param tagName The tag name of the axis. + @param value The value of the axis. + */ + void setAxisValue (StringRef tagName, float value); + + /** Returns a new font with the value of the axis with the given tag name. + + @param tagName The tag name of the axis. + @param value The value of the axis. + + @return A new font with the value of the axis with the given tag name. + */ Font withAxisValue (StringRef tagName, float value) const; + /** Resets the value of the axis with the given tag name. + + @param tagName The tag name of the axis. + */ + void resetAxisValue (StringRef tagName); + + //============================================================================== + /** Resets the values of all axes. + + @return A new font with the values of all axes reset. + */ + void resetAllAxisValues(); + + //============================================================================== + /** Axis option. + + This struct represents an option of the axis. + */ struct AxisOption { AxisOption (StringRef tagName, float value) @@ -85,16 +193,25 @@ class YUP_API Font float value; }; + /** Sets the values of the axes. + + @param axisOptions The options of the axes. + */ void setAxisValues (std::initializer_list axisOptions); - Font withAxisValues (std::initializer_list axisOptions) const; - void resetAxisValue (int index); - void resetAxisValue (StringRef tagName); + /** Returns a new font with the values of the axes. - void resetAllAxisValues(); + @param axisOptions The options of the axes. + + @return A new font with the values of the axes. + */ + Font withAxisValues (std::initializer_list axisOptions) const; //============================================================================== + /** Returns true if the fonts are equal. */ bool operator== (const Font& other) const; + + /** Returns true if the fonts are not equal. */ bool operator!= (const Font& other) const; //============================================================================== diff --git a/modules/yup_graphics/layout/yup_Justification.h b/modules/yup_graphics/layout/yup_Justification.h new file mode 100644 index 000000000..64d659330 --- /dev/null +++ b/modules/yup_graphics/layout/yup_Justification.h @@ -0,0 +1,51 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Specifies the positioning of an item relative to its target area. +*/ +enum class Justification +{ + left = 1 << 0, /**< Aligns the content to the left. */ + right = 1 << 1, /**< Aligns the content to the right. */ + horizontalCenter = 1 << 2, /**< Centers the content horizontally. */ + + top = 1 << 3, /**< Aligns the content to the top. */ + bottom = 1 << 4, /**< Aligns the content to the bottom. */ + verticalCenter = 1 << 5, /**< Centers the content vertically. */ + + topLeft = left | top, /**< Aligns the content to the top left corner. */ + topRight = right | top, /**< Aligns the content to the top right corner. */ + bottomLeft = left | bottom, /**< Aligns the content to the bottom left corner. */ + bottomRight = right | bottom, /**< Aligns the content to the bottom right corner. */ + + centerLeft = left | verticalCenter, /**< Aligns the content to the left and centers it vertically. */ + centerTop = horizontalCenter | top, /**< Centers the content horizontally and aligns it to the top. */ + center = horizontalCenter | verticalCenter, /**< Centers the content both horizontally and vertically. */ + centerRight = right | verticalCenter, /**< Aligns the content to the right and centers it vertically. */ + centerBottom = horizontalCenter | bottom /**< Centers the content horizontally and aligns it to the bottom. */ +}; + +} // namespace yup diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 79ad8a653..ec6b1077e 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -1535,6 +1535,15 @@ class YUP_API Rectangle return contains (p.getX(), p.getY()); } + /** TODO: doxygen */ + [[nodiscard]] constexpr bool contains (const Rectangle& p) const noexcept + { + return p.getX() >= xy.getX() + && p.getY() >= xy.getY() + && p.getRight() <= (xy.getX() + size.getWidth()) + && p.getBottom() <= (xy.getY() + size.getHeight()); + } + //============================================================================== /** Calculates the area of the rectangle. diff --git a/modules/yup_graphics/yup_graphics.h b/modules/yup_graphics/yup_graphics.h index 62d21cc38..21b8b97f3 100644 --- a/modules/yup_graphics/yup_graphics.h +++ b/modules/yup_graphics/yup_graphics.h @@ -65,6 +65,7 @@ YUP_END_IGNORE_WARNINGS_GCC_LIKE //============================================================================== +#include "layout/yup_Justification.h" #include "primitives/yup_AffineTransform.h" #include "primitives/yup_Size.h" #include "primitives/yup_Point.h" diff --git a/modules/yup_gui/clipboard/yup_SystemClipboard.h b/modules/yup_gui/clipboard/yup_SystemClipboard.h index ccf9b21b8..638a07a8d 100644 --- a/modules/yup_gui/clipboard/yup_SystemClipboard.h +++ b/modules/yup_gui/clipboard/yup_SystemClipboard.h @@ -22,13 +22,25 @@ namespace yup { +/** System clipboard. + + This class provides methods to copy and retrieve text from the system clipboard. + + @note This class is not thread-safe. +*/ class YUP_API SystemClipboard { public: - /** Copies the given text to the system clipboard. */ + /** Copies the given text to the system clipboard. + + @param text The text to copy to the clipboard. + */ static void copyTextToClipboard (const String& text); - /** Retrieves the text from the system clipboard. */ + /** Retrieves the text from the system clipboard. + + @return The text from the clipboard. + */ static String getTextFromClipboard(); }; diff --git a/modules/yup_gui/widgets/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp similarity index 58% rename from modules/yup_gui/widgets/yup_PopupMenu.cpp rename to modules/yup_gui/menus/yup_PopupMenu.cpp index 08e5045e9..d12c131b8 100644 --- a/modules/yup_gui/widgets/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -112,22 +112,30 @@ class PopupMenu::MenuWindow : public Component // Calculate required size and create menu items setupMenuItems(); - // Add to desktop as a popup - ComponentNative::Options nativeOptions; - nativeOptions - .withDecoration (false) - .withResizableWindow (false); - - addToDesktop (nativeOptions); + // Add as child to topmost component or to desktop + if (options.addAsChildToTopmost && options.parentComponent) + { + // Add this menu as a child + options.parentComponent->addChildComponent (this); + } + else + { + // Add to desktop as a popup + auto nativeOptions = ComponentNative::Options{} + .withDecoration (false) + .withResizableWindow (false); - // Position the menu - positionMenu(); + addToDesktop (nativeOptions); + } // Add to active popups list for modal behavior activePopups.push_back (this); + // Position the menu + positionMenu(); + setVisible (true); - takeKeyboardFocus(); + toFront (true); } ~MenuWindow() override @@ -137,6 +145,18 @@ class PopupMenu::MenuWindow : public Component void paint (Graphics& g) override { + // Draw drop shadow if enabled + if (options.addAsChildToTopmost) + { + auto shadowBounds = getLocalBounds().to(); + auto shadowRadius = static_cast (8.0f); + + g.setFillColor (Color (0, 0, 0)); + g.setFeather (shadowRadius); + g.fillRoundedRect (shadowBounds.translated (0.0f, 2.0f).enlarged (2.0f), 4.0f); + g.setFeather (0.0f); + } + // Draw menu background g.setFillColor (findColor (Colours::menuBackground).value_or (Color (0xff2a2a2a))); g.fillRoundedRect (getLocalBounds().to(), 4.0f); @@ -157,29 +177,61 @@ class PopupMenu::MenuWindow : public Component bool isWithinBounds (Point globalPoint) const { - auto localPoint = globalPoint - getScreenPosition().to(); - return getLocalBounds().to().contains (localPoint); + if (getParentComponent() != nullptr) + { + // When added as a child, convert to local coordinates + return getLocalBounds().to().contains (globalPoint); + } + else + { + // When added to desktop, use screen coordinates + auto localPoint = globalPoint - getScreenPosition().to(); + return getLocalBounds().to().contains (localPoint); + } } void mouseDown (const MouseEvent& event) override { - auto itemIndex = getItemIndexAt (event.getPosition()); - if (itemIndex >= 0 && itemIndex < owner->items.size()) + // Check if click is inside the menu + if (getLocalBounds().contains (event.getPosition())) { - auto& item = *owner->items[itemIndex]; - if (! item.isSeparator() && item.isEnabled) + auto itemIndex = getItemIndexAt (event.getPosition()); + if (itemIndex >= 0 && itemIndex < owner->items.size()) { - if (item.isSubMenu()) + auto& item = *owner->items[itemIndex]; + if (! item.isSeparator() && item.isEnabled) { - // TODO: Show sub-menu - } - else - { - dismiss (item.itemID); + if (item.isSubMenu()) + { + // TODO: Show sub-menu + } + else + { + dismiss (item.itemID); + } } } } + else + { + // Click outside menu - dismiss + dismiss (0); + } + } + + /* + void inputAttemptWhenModal() override + { + // Handle clicks outside when modal + auto mousePos = Desktop::getInstance()->getMousePosition(); + auto localPos = getLocalPoint (nullptr, mousePos); + + if (! getLocalBounds().contains (localPos)) + { + dismiss (0); + } } + */ void mouseMove (const MouseEvent& event) override { @@ -238,8 +290,8 @@ class PopupMenu::MenuWindow : public Component constexpr float verticalPadding = 4.0f; float y = verticalPadding; // Top padding - float itemHeight = static_cast (options.standardItemHeight); - float width = static_cast (options.minWidth > 0 ? options.minWidth : 200); + float itemHeight = static_cast (22); + float width = options.minWidth.value_or (200); itemRects.clear(); @@ -285,21 +337,195 @@ class PopupMenu::MenuWindow : public Component void positionMenu() { - Point position; + auto menuSize = getSize(); + Rectangle targetArea; + Rectangle availableArea; + // Determine target area and available area if (options.parentComponent) { - position = options.parentComponent->getBoundsRelativeToTopLevelComponent().getBottomLeft(); + // Get the bounds relative to the screen or topmost component + if (options.addAsChildToTopmost) + { + // Target area is relative to topmost component + targetArea = options.parentComponent->getBounds().to(); + availableArea = targetArea; + } + else + { + // Target area is in screen coordinates + targetArea = options.parentComponent->getScreenBounds().to(); + + // Available area is the screen bounds + if (auto* desktop = Desktop::getInstance()) + { + if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) + availableArea = screen->workArea; + else if (auto screen = desktop->getPrimaryScreen()) + availableArea = screen->workArea; + else + availableArea = targetArea; + } + } - if (auto native = options.parentComponent->getNativeComponent()) - position.translate (native->getPosition()); + // Override with explicit target area if provided + if (! options.targetArea.isEmpty()) + { + if (options.addAsChildToTopmost) + targetArea = options.targetArea; + else + targetArea = options.targetArea.translated (targetArea.getPosition()); + } } else { - position = options.targetScreenPosition; + if (! options.targetArea.isEmpty()) + targetArea = options.targetArea; + else + targetArea = Rectangle (options.targetPosition, options.targetArea.getSize()); + + // Get screen bounds for available area + if (auto* desktop = Desktop::getInstance()) + { + if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) + availableArea = screen->workArea; + else if (auto screen = desktop->getPrimaryScreen()) + availableArea = screen->workArea; + else + availableArea = targetArea; + } + } + + // Calculate position based on justification + Point position = calculatePositionWithJustification (targetArea, menuSize.to(), options.justification).to(); + + // Adjust position to fit within available area + position = constrainPositionToAvailableArea (position, menuSize.to(), availableArea, targetArea).to(); + + setTopLeft (position); + } + + Point calculatePositionWithJustification (const Rectangle& targetArea, + const Size& menuSize, + Justification justification) + { + Point position; + + switch (justification) + { + case Justification::topLeft: + position = targetArea.getTopLeft(); + break; + + case Justification::centerTop: + position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getTop()); + break; + + case Justification::topRight: + position = targetArea.getTopRight().translated (-menuSize.getWidth(), 0); + break; + + case Justification::bottomLeft: + position = targetArea.getBottomLeft(); + break; + + case Justification::centerBottom: + position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getBottom()); + break; + + case Justification::bottomRight: + position = targetArea.getBottomRight().translated (-menuSize.getWidth(), 0); + break; + + case Justification::centerRight: + position = Point (targetArea.getRight(), targetArea.getCenterY() - menuSize.getHeight() / 2); + break; + + case Justification::centerLeft: + position = Point (targetArea.getX() - menuSize.getWidth(), targetArea.getCenterY() - menuSize.getHeight() / 2); + break; + } + + return position; + } + + Point constrainPositionToAvailableArea (Point desiredPosition, + const Size& menuSize, + const Rectangle& availableArea, + const Rectangle& targetArea) + { + // Add padding to keep menu slightly away from screen edges + const int padding = 5; + auto constrainedArea = availableArea.reduced (padding); + + Point position = desiredPosition; + + // Check if menu fits in desired position + Rectangle menuBounds (position, menuSize); + + // If menu doesn't fit, try alternative positions + if (! constrainedArea.contains (menuBounds)) + { + // Try to keep menu fully visible by adjusting position + + // Horizontal adjustment + if (menuBounds.getRight() > constrainedArea.getRight()) + { + // Try moving left + position.setX (constrainedArea.getRight() - menuSize.getWidth()); + + // If that puts us over the target, try positioning on the left side + if (Rectangle (position, menuSize).intersects (targetArea)) + { + position.setX (targetArea.getX() - menuSize.getWidth()); + } + } + else if (menuBounds.getX() < constrainedArea.getX()) + { + // Try moving right + position.setX (constrainedArea.getX()); + + // If that puts us over the target, try positioning on the right side + if (Rectangle (position, menuSize).intersects (targetArea)) + { + position.setX (targetArea.getRight()); + } + } + + // Vertical adjustment + if (menuBounds.getBottom() > constrainedArea.getBottom()) + { + // Try moving up + position.setY (constrainedArea.getBottom() - menuSize.getHeight()); + + // If that puts us over the target, try positioning above + if (Rectangle (position, menuSize).intersects (targetArea)) + { + position.setY (targetArea.getY() - menuSize.getHeight()); + } + } + else if (menuBounds.getY() < constrainedArea.getY()) + { + // Try moving down + position.setY (constrainedArea.getY()); + + // If that puts us over the target, try positioning below + if (Rectangle (position, menuSize).intersects (targetArea)) + { + position.setY (targetArea.getBottom()); + } + } + + // Final bounds check - ensure we're at least partially visible + position.setX (jlimit (constrainedArea.getX(), + jmax (constrainedArea.getX(), constrainedArea.getRight() - menuSize.getWidth()), + position.getX())); + position.setY (jlimit (constrainedArea.getY(), + jmax (constrainedArea.getY(), constrainedArea.getBottom() - menuSize.getHeight()), + position.getY())); } - setTopLeft (position.roundToInt()); + return position; } int getItemIndexAt (Point position) const @@ -459,15 +685,77 @@ void installGlobalMouseListener() //============================================================================== -PopupMenu::PopupMenu() = default; +PopupMenu::Options::Options() + : parentComponent (nullptr) + , dismissOnSelection (true) + , justification (Justification::bottomLeft) + , addAsChildToTopmost (false) + , useNativeMenus (false) +{ +} + +PopupMenu::Options& PopupMenu::Options::withParentComponent (Component* parentComponent) +{ + this->parentComponent = parentComponent; + return *this; +} + +PopupMenu::Options& PopupMenu::Options::withTargetArea (const Rectangle& targetArea) +{ + this->targetArea = targetArea; + return *this; +} + +PopupMenu::Options& PopupMenu::Options::withJustification (Justification justification) +{ + this->justification = justification; + return *this; +} + +PopupMenu::Options& PopupMenu::Options::withTargetPosition (const Point& targetPosition) +{ + this->targetPosition = targetPosition; + return *this; +} + +PopupMenu::Options& PopupMenu::Options::withMinimumWidth (int minWidth) +{ + this->minWidth = minWidth; + return *this; +} + +PopupMenu::Options& PopupMenu::Options::withMaximumWidth (int maxWidth) +{ + this->maxWidth = maxWidth; + return *this; +} + +PopupMenu::Options& PopupMenu::Options::withAsChildToTopmost (bool addAsChildToTopmost) +{ + this->addAsChildToTopmost = addAsChildToTopmost; + return *this; +} + +PopupMenu::Options& PopupMenu::Options::withNativeMenus (bool useNativeMenus) +{ + this->useNativeMenus = useNativeMenus; + return *this; +} + +//============================================================================== + +PopupMenu::PopupMenu (const Options& options) + : options (options) +{ +} PopupMenu::~PopupMenu() = default; //============================================================================== -PopupMenu::Ptr PopupMenu::create() +PopupMenu::Ptr PopupMenu::create (const Options& options) { - return new PopupMenu(); + return new PopupMenu (options); } //============================================================================== @@ -553,7 +841,7 @@ void PopupMenu::clear() //============================================================================== -void PopupMenu::show (const Options& options, std::function callback) +void PopupMenu::show (std::function callback) { if (isEmpty()) { @@ -572,46 +860,12 @@ void PopupMenu::show (const Options& options, std::function callback //============================================================================== -void PopupMenu::showAt (Point screenPos, std::function callback) -{ - Options options; - options.targetScreenPosition = screenPos; - - show (options, callback); -} - -//============================================================================== - -void PopupMenu::showAt (Component* targetComp, std::function callback) -{ - if (targetComp == nullptr) - { - if (callback) - callback (0); - - return; - } - - Options options; - options.parentComponent = targetComp; - - show (options, callback); -} - -//============================================================================== - void PopupMenu::showCustom (const Options& options, std::function callback) { - installGlobalMouseListener(); + jassert (! isEmpty()); - if (isEmpty()) - { - if (callback) - callback (0); - return; - } + installGlobalMouseListener(); - // Create the menu window with a reference to this menu - it will manage its own lifetime new MenuWindow (this, options); } diff --git a/modules/yup_gui/widgets/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h similarity index 80% rename from modules/yup_gui/widgets/yup_PopupMenu.h rename to modules/yup_gui/menus/yup_PopupMenu.h index c0af17d96..e05249015 100644 --- a/modules/yup_gui/widgets/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -39,51 +39,55 @@ class YUP_API PopupMenu : public ReferenceCountedObject /** Options for showing the popup menu. */ struct Options { - Options() - : useNativeMenus (false) - , parentComponent (nullptr) - , minWidth (0) - , maxWidth (0) - , standardItemHeight (22) - , dismissOnSelection (true) - { - } - - /** Whether to use native system menus (when available). */ - bool useNativeMenus; + Options(); /** The parent component to attach the menu to. */ - Component* parentComponent; - - /** The position to show the menu at (relative to parent). */ - Point targetScreenPosition; + Options& withParentComponent (Component* parentComponent); /** The area to position the menu relative to. */ - Rectangle targetArea; + Options& withTargetArea (const Rectangle& targetArea); + + /** How to position the menu relative to the target area. */ + Options& withJustification (Justification justification); + + /** The position to show the menu at (relative to parent). */ + Options& withTargetPosition (const Point& targetPosition); /** Minimum width for the menu. */ - int minWidth; + Options& withMinimumWidth (int minWidth); /** Maximum width for the menu. */ - int maxWidth; + Options& withMaximumWidth (int maxWidth); - /** Standard menu item height. */ - int standardItemHeight; + /** Whether to add the menu as a child to the topmost component. */ + Options& withAsChildToTopmost (bool addAsChildToTopmost); + + /** Whether to use native system menus (when available). */ + Options& withNativeMenus (bool useNativeMenus); - /** Whether to dismiss the menu when an item is selected. */ + Component* parentComponent; + Point targetPosition; + Rectangle targetArea; + Justification justification; + std::optional minWidth; + std::optional maxWidth; bool dismissOnSelection; + bool addAsChildToTopmost; + bool useNativeMenus; }; //============================================================================== - /** Creates an empty popup menu. */ - PopupMenu(); - /** Destructor. */ ~PopupMenu(); //============================================================================== + /** Creates a popup menu with the given options. + + @param options The options for the popup menu. - static Ptr create(); + @return A pointer to the popup menu. + */ + static Ptr create (const Options& options = {}); //============================================================================== /** Adds a menu item. @@ -133,21 +137,7 @@ class YUP_API PopupMenu : public ReferenceCountedObject @param options Options for showing the menu @param callback Function to call when an item is selected (optional) */ - void show (const Options& options = Options {}, std::function callback = nullptr); - - /** Shows the menu at a specific screen position. - - @param screenPos Screen position to show the menu - @param callback Function to call when an item is selected (optional) - */ - void showAt (Point screenPos, std::function callback = nullptr); - - /** Shows the menu relative to a component. - - @param targetComp Component to show the menu relative to - @param callback Function to call when an item is selected (optional) - */ - void showAt (Component* targetComp, std::function callback = nullptr); + void show (std::function callback = nullptr); //============================================================================== /** Callback type for menu item selection. */ @@ -164,11 +154,15 @@ class YUP_API PopupMenu : public ReferenceCountedObject //============================================================================== friend class MenuWindow; + PopupMenu (const Options& options = {}); + void showCustom (const Options& options, std::function callback); + // PopupMenuItem is now an implementation detail class PopupMenuItem; std::vector> items; - void showCustom (const Options& options, std::function callback); + Options options; + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) }; diff --git a/modules/yup_gui/yup_gui.cpp b/modules/yup_gui/yup_gui.cpp index f05cabff8..ead2e8a01 100644 --- a/modules/yup_gui/yup_gui.cpp +++ b/modules/yup_gui/yup_gui.cpp @@ -102,12 +102,12 @@ #include "clipboard/yup_SystemClipboard.cpp" #include "component/yup_ComponentNative.cpp" #include "component/yup_Component.cpp" +#include "menus/yup_PopupMenu.cpp" #include "widgets/yup_Button.cpp" #include "widgets/yup_TextButton.cpp" #include "widgets/yup_TextEditor.cpp" #include "widgets/yup_Label.cpp" #include "widgets/yup_Slider.cpp" -#include "widgets/yup_PopupMenu.cpp" #include "artboard/yup_Artboard.cpp" #include "windowing/yup_DocumentWindow.cpp" #include "themes/yup_ApplicationTheme.cpp" diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index 21c4ded90..17ef87611 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -80,12 +80,12 @@ #include "component/yup_ComponentNative.h" #include "component/yup_ComponentStyle.h" #include "component/yup_Component.h" +#include "menus/yup_PopupMenu.h" #include "widgets/yup_Button.h" #include "widgets/yup_TextButton.h" #include "widgets/yup_TextEditor.h" #include "widgets/yup_Label.h" #include "widgets/yup_Slider.h" -#include "widgets/yup_PopupMenu.h" #include "artboard/yup_Artboard.h" #include "windowing/yup_DocumentWindow.h" From 2c6f49c27c33e01b23dc488f17c76c111b643b62 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 14 Jun 2025 00:44:44 +0200 Subject: [PATCH 14/51] Improved Path handling --- examples/graphics/source/examples/Paths.h | 4 +- modules/yup_graphics/primitives/yup_Path.cpp | 75 +++++++++++++++++++- modules/yup_graphics/primitives/yup_Path.h | 15 +++- tests/yup_graphics/yup_Path.cpp | 10 +-- 4 files changed, 94 insertions(+), 10 deletions(-) diff --git a/examples/graphics/source/examples/Paths.h b/examples/graphics/source/examples/Paths.h index 1f91ae106..8eabb934c 100644 --- a/examples/graphics/source/examples/Paths.h +++ b/examples/graphics/source/examples/Paths.h @@ -400,7 +400,7 @@ class PathsExample : public yup::Component // Parse SVG path data examples - smaller scale yup::Path svgHeart; - svgHeart.parsePathData ("M12,21.35l-1.45-1.32C5.4,15.36,2,12.28,2,8.5 C2,5.42,4.42,3,7.5,3c1.74,0,3.41,0.81,4.5,2.09C13.09,3.81,14.76,3,16.5,3 C19.58,3,22,5.42,22,8.5c0,3.78-3.4,6.86-8.55,11.54L12,21.35z"); + svgHeart.fromString ("M12,21.35l-1.45-1.32C5.4,15.36,2,12.28,2,8.5 C2,5.42,4.42,3,7.5,3c1.74,0,3.41,0.81,4.5,2.09C13.09,3.81,14.76,3,16.5,3 C19.58,3,22,5.42,22,8.5c0,3.78-3.4,6.86-8.55,11.54L12,21.35z"); // Scale and position the heart - smaller yup::Rectangle heartBounds = svgHeart.getBounds(); @@ -416,7 +416,7 @@ class PathsExample : public yup::Component // Simple SVG path - smaller yup::Path svgTriangle; - svgTriangle.parsePathData ("M100,20 L180,160 L20,160 Z"); + svgTriangle.fromString ("M100,20 L180,160 L20,160 Z"); svgTriangle.scaleToFit (x + 60, y, 50, 50, true); g.setFillColor (yup::Color (150, 255, 150)); diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index c92b04cd6..ed7fe585d 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -733,6 +733,79 @@ rive::RiveRenderPath* Path::getRenderPath() const return path.get(); } +//============================================================================== +String Path::toString() const +{ + const auto& rawPath = path->getRawPath(); + const auto& points = rawPath.points(); + const auto& verbs = rawPath.verbs(); + + if (points.empty() || verbs.empty()) + return String(); + + String result; + result.preallocateBytes (points.size() * 20); // Rough estimate for performance + + size_t pointIndex = 0; + + for (size_t i = 0; i < verbs.size(); ++i) + { + auto verb = verbs[i]; + + switch (verb) + { + case rive::PathVerb::move: + if (pointIndex < points.size()) + { + result << "M " << points[pointIndex].x << " " << points[pointIndex].y << " "; + pointIndex++; + } + break; + + case rive::PathVerb::line: + if (pointIndex < points.size()) + { + result << "L " << points[pointIndex].x << " " << points[pointIndex].y << " "; + pointIndex++; + } + break; + + case rive::PathVerb::quad: + // Rive doesn't seem to use quad verbs based on the existing code + // But if it does, we'll handle it + if (pointIndex + 1 < points.size()) + { + result << "Q " + << points[pointIndex].x << " " << points[pointIndex].y << " " + << points[pointIndex + 1].x << " " << points[pointIndex + 1].y << " "; + pointIndex += 2; + } + break; + + case rive::PathVerb::cubic: + if (pointIndex + 2 < points.size()) + { + result << "C " + << points[pointIndex].x << " " << points[pointIndex].y << " " + << points[pointIndex + 1].x << " " << points[pointIndex + 1].y << " " + << points[pointIndex + 2].x << " " << points[pointIndex + 2].y << " "; + pointIndex += 3; + } + break; + + case rive::PathVerb::close: + result << "Z "; + break; + } + } + + // Remove trailing space if present + if (result.endsWithChar (' ')) + result = result.trimEnd(); + + return result; +} + //============================================================================== namespace { @@ -1181,7 +1254,7 @@ void handleEllipticalArc (String::CharPointerType& data, Path& path, float& curr } // namespace -bool Path::parsePathData (const String& pathData) +bool Path::fromString (const String& pathData) { // https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/ diff --git a/modules/yup_graphics/primitives/yup_Path.h b/modules/yup_graphics/primitives/yup_Path.h index fc70ca61e..38175f576 100644 --- a/modules/yup_graphics/primitives/yup_Path.h +++ b/modules/yup_graphics/primitives/yup_Path.h @@ -583,15 +583,24 @@ class YUP_API Path Point getPointAlongPath (float distance) const; //============================================================================== + /** Converts the path to an SVG path data string. + + This method converts the path to a string representation using SVG path data format. + The resulting string can be used with fromString() to recreate the path. + + @return A string containing the SVG path data representation of this path. + */ + String toString() const; + /** Parses the path data from a string. - This method parses the path data from a string and updates the path accordingly. + This method parses the path data from a string in SVG path data format and updates the path accordingly. - @param pathData The string containing the path data. + @param pathData The string containing the SVG path data. @return True if the path data was parsed successfully, false otherwise. */ - bool parsePathData (const String& pathData); + bool fromString (const String& pathData); //============================================================================== /** Provides an iterator to the beginning of the path data. diff --git a/tests/yup_graphics/yup_Path.cpp b/tests/yup_graphics/yup_Path.cpp index 2af032dcd..62ddb2c6a 100644 --- a/tests/yup_graphics/yup_Path.cpp +++ b/tests/yup_graphics/yup_Path.cpp @@ -276,16 +276,18 @@ TEST (PathTests, WithRoundedCorners) EXPECT_FALSE (same.getBounds().isEmpty()); } -TEST (PathTests, ParsePathData) +TEST (PathTests, FromString) { Path p; // Simple SVG path: M10 10 H 90 V 90 H 10 Z - bool ok = p.parsePathData ("M10 10 H 90 V 90 H 10 Z"); + bool ok = p.fromString ("M 10 10 H 90 V 90 H 10 Z"); EXPECT_TRUE (ok); EXPECT_FALSE (p.getBounds().isEmpty()); + EXPECT_EQ (p.toString(), "M 10 10 L 90 10 L 90 90 L 10 90 Z"); + // Edge: malformed path Path p2; - ok = p2.parsePathData ("M10 10 Q"); + ok = p2.fromString ("M 10 10 Q"); EXPECT_TRUE (ok); // Should not throw, but result is empty } @@ -455,7 +457,7 @@ TEST (PathTests, AllPublicApiErrorCases) p.getPointAlongPath (0.0f); p.createStrokePolygon (0.0f); p.withRoundedCorners (0.0f); - p.parsePathData (""); + p.fromString (""); SUCCEED(); } From 6f6ea706e2541510f326194d405ec411384781ab Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Fri, 13 Jun 2025 22:45:23 +0000 Subject: [PATCH 15/51] Code formatting --- examples/graphics/source/examples/PopupMenu.h | 28 +++++++++---------- .../yup_graphics/layout/yup_Justification.h | 14 +++++----- modules/yup_graphics/primitives/yup_Path.cpp | 10 +++---- modules/yup_gui/menus/yup_PopupMenu.cpp | 8 +++--- modules/yup_gui/menus/yup_PopupMenu.h | 1 - 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 3401747b8..0fd9c6115 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -136,8 +136,8 @@ class PopupMenuDemo : public yup::Component void showBasicMenu() { - auto options = yup::PopupMenu::Options{} - .withParentComponent(&basicMenuButton); + auto options = yup::PopupMenu::Options {} + .withParentComponent (&basicMenuButton); auto menu = yup::PopupMenu::create (options); @@ -171,8 +171,8 @@ class PopupMenuDemo : public yup::Component colorMenu->addItem ("Green", colorGreen); colorMenu->addItem ("Blue", colorBlue); - auto options = yup::PopupMenu::Options{} - .withParentComponent(&subMenuButton); + auto options = yup::PopupMenu::Options {} + .withParentComponent (&subMenuButton); auto menu = yup::PopupMenu::create (options); menu->addItem ("New", newFile); menu->addItem ("Open", openFile); @@ -192,8 +192,8 @@ class PopupMenuDemo : public yup::Component void showCustomMenu() { - auto options = yup::PopupMenu::Options{} - .withParentComponent(&customMenuButton); + auto options = yup::PopupMenu::Options {} + .withParentComponent (&customMenuButton); auto menu = yup::PopupMenu::create (options); menu->addItem ("Regular Item", 1); @@ -230,10 +230,10 @@ class PopupMenuDemo : public yup::Component void showNativeMenu() { - auto options = yup::PopupMenu::Options{} - .withNativeMenus(true) - .withJustification(yup::Justification::topLeft) - .withParentComponent(this); + auto options = yup::PopupMenu::Options {} + .withNativeMenus (true) + .withJustification (yup::Justification::topLeft) + .withParentComponent (this); auto menu = yup::PopupMenu::create (options); @@ -255,10 +255,10 @@ class PopupMenuDemo : public yup::Component void showContextMenu (yup::Point position) { - auto options = yup::PopupMenu::Options{} - // .withTargetScreenPosition(position.to()) // TODO: doesn't seem to work - .withParentComponent(this) - .withAsChildToTopmost(true); + auto options = yup::PopupMenu::Options {} + // .withTargetScreenPosition(position.to()) // TODO: doesn't seem to work + .withParentComponent (this) + .withAsChildToTopmost (true); auto contextMenu = yup::PopupMenu::create (options); diff --git a/modules/yup_graphics/layout/yup_Justification.h b/modules/yup_graphics/layout/yup_Justification.h index 64d659330..a88821993 100644 --- a/modules/yup_graphics/layout/yup_Justification.h +++ b/modules/yup_graphics/layout/yup_Justification.h @@ -32,20 +32,20 @@ enum class Justification right = 1 << 1, /**< Aligns the content to the right. */ horizontalCenter = 1 << 2, /**< Centers the content horizontally. */ - top = 1 << 3, /**< Aligns the content to the top. */ - bottom = 1 << 4, /**< Aligns the content to the bottom. */ - verticalCenter = 1 << 5, /**< Centers the content vertically. */ + top = 1 << 3, /**< Aligns the content to the top. */ + bottom = 1 << 4, /**< Aligns the content to the bottom. */ + verticalCenter = 1 << 5, /**< Centers the content vertically. */ topLeft = left | top, /**< Aligns the content to the top left corner. */ topRight = right | top, /**< Aligns the content to the top right corner. */ bottomLeft = left | bottom, /**< Aligns the content to the bottom left corner. */ bottomRight = right | bottom, /**< Aligns the content to the bottom right corner. */ - centerLeft = left | verticalCenter, /**< Aligns the content to the left and centers it vertically. */ - centerTop = horizontalCenter | top, /**< Centers the content horizontally and aligns it to the top. */ + centerLeft = left | verticalCenter, /**< Aligns the content to the left and centers it vertically. */ + centerTop = horizontalCenter | top, /**< Centers the content horizontally and aligns it to the top. */ center = horizontalCenter | verticalCenter, /**< Centers the content both horizontally and vertically. */ - centerRight = right | verticalCenter, /**< Aligns the content to the right and centers it vertically. */ - centerBottom = horizontalCenter | bottom /**< Centers the content horizontally and aligns it to the bottom. */ + centerRight = right | verticalCenter, /**< Aligns the content to the right and centers it vertically. */ + centerBottom = horizontalCenter | bottom /**< Centers the content horizontally and aligns it to the bottom. */ }; } // namespace yup diff --git a/modules/yup_graphics/primitives/yup_Path.cpp b/modules/yup_graphics/primitives/yup_Path.cpp index ed7fe585d..f482a31f5 100644 --- a/modules/yup_graphics/primitives/yup_Path.cpp +++ b/modules/yup_graphics/primitives/yup_Path.cpp @@ -776,8 +776,8 @@ String Path::toString() const if (pointIndex + 1 < points.size()) { result << "Q " - << points[pointIndex].x << " " << points[pointIndex].y << " " - << points[pointIndex + 1].x << " " << points[pointIndex + 1].y << " "; + << points[pointIndex].x << " " << points[pointIndex].y << " " + << points[pointIndex + 1].x << " " << points[pointIndex + 1].y << " "; pointIndex += 2; } break; @@ -786,9 +786,9 @@ String Path::toString() const if (pointIndex + 2 < points.size()) { result << "C " - << points[pointIndex].x << " " << points[pointIndex].y << " " - << points[pointIndex + 1].x << " " << points[pointIndex + 1].y << " " - << points[pointIndex + 2].x << " " << points[pointIndex + 2].y << " "; + << points[pointIndex].x << " " << points[pointIndex].y << " " + << points[pointIndex + 1].x << " " << points[pointIndex + 1].y << " " + << points[pointIndex + 2].x << " " << points[pointIndex + 2].y << " "; pointIndex += 3; } break; diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index d12c131b8..1eca20b51 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -121,9 +121,9 @@ class PopupMenu::MenuWindow : public Component else { // Add to desktop as a popup - auto nativeOptions = ComponentNative::Options{} - .withDecoration (false) - .withResizableWindow (false); + auto nativeOptions = ComponentNative::Options {} + .withDecoration (false) + .withResizableWindow (false); addToDesktop (nativeOptions); } @@ -381,7 +381,7 @@ class PopupMenu::MenuWindow : public Component { if (! options.targetArea.isEmpty()) targetArea = options.targetArea; - else + else targetArea = Rectangle (options.targetPosition, options.targetArea.getSize()); // Get screen bounds for available area diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index e05249015..4bb97fc92 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -163,7 +163,6 @@ class YUP_API PopupMenu : public ReferenceCountedObject Options options; - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) }; From 1add1f3003a01847bfed652c3634ce16fb7108b8 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Fri, 20 Jun 2025 22:58:22 +0200 Subject: [PATCH 16/51] Fix issues --- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 1 - modules/yup_gui/native/yup_Windowing_sdl2.h | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index cab65eb29..1c7e70a2b 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -23,7 +23,6 @@ namespace yup { //============================================================================== - #ifndef YUP_WINDOWING_LOGGING #define YUP_WINDOWING_LOGGING 1 #endif diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.h b/modules/yup_gui/native/yup_Windowing_sdl2.h index e905c2e2a..ba5fc1f32 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.h +++ b/modules/yup_gui/native/yup_Windowing_sdl2.h @@ -22,19 +22,6 @@ namespace yup { -//============================================================================== -#ifndef YUP_WINDOWING_LOGGING -#define YUP_WINDOWING_LOGGING 1 -#endif - -#if YUP_WINDOWING_LOGGING -#define YUP_WINDOWING_LOG(textToWrite) JUCE_DBG (textToWrite) -#else -#define YUP_WINDOWING_LOG(textToWrite) \ - { \ - } -#endif - //============================================================================== class SDL2ComponentNative final : public ComponentNative From c339ef8a1b2babb6f176e265be288d4d6f2e7122 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 21 Jun 2025 15:22:42 +0200 Subject: [PATCH 17/51] More tweaks to opengl --- examples/graphics/source/main.cpp | 30 +- justfile | 2 +- .../native/yup_GraphicsContext_gl.cpp | 22 +- .../native/yup_GraphicsContext_opengl.cpp | 545 ++++++++++++++++++ modules/yup_graphics/yup_graphics.cpp | 9 +- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 2 + modules/yup_gui/native/yup_Windowing_sdl2.h | 2 +- .../yup_gui/windowing/yup_DocumentWindow.cpp | 9 +- 8 files changed, 583 insertions(+), 38 deletions(-) create mode 100644 modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 121b6ca50..091a7abe4 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -76,11 +76,13 @@ class CustomWindow } */ + int counter = 0; + { auto button = std::make_unique ("Audio"); - button->onClick = [this] + button->onClick = [this, &counter] { - selectComponent (0); + selectComponent (counter++); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -91,9 +93,9 @@ class CustomWindow { auto button = std::make_unique ("Layout Fonts"); - button->onClick = [this] + button->onClick = [this, &counter] { - selectComponent (1); + selectComponent (counter++); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -104,9 +106,9 @@ class CustomWindow { auto button = std::make_unique ("Variable Fonts"); - button->onClick = [this] + button->onClick = [this, &counter] { - selectComponent (2); + selectComponent (counter++); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -117,9 +119,9 @@ class CustomWindow { auto button = std::make_unique ("Paths"); - button->onClick = [this] + button->onClick = [this, &counter] { - selectComponent (3); + selectComponent (counter++); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -130,9 +132,9 @@ class CustomWindow { auto button = std::make_unique ("Text Editor"); - button->onClick = [this] + button->onClick = [this, &counter] { - selectComponent (4); + selectComponent (counter++); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -143,9 +145,9 @@ class CustomWindow { auto button = std::make_unique ("Popup Menu"); - button->onClick = [this] + button->onClick = [this, &counter] { - selectComponent (5); + selectComponent (counter++); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -156,9 +158,9 @@ class CustomWindow { auto button = std::make_unique ("File Chooser"); - button->onClick = [this] + button->onClick = [this, &counter] { - selectComponent (6); + selectComponent (counter++); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); diff --git a/justfile b/justfile index ab76fdcf1..7bd34b18c 100644 --- a/justfile +++ b/justfile @@ -76,6 +76,6 @@ emscripten_test CONFIG="Debug": node build/tests/{{CONFIG}}/yup_tests.js --gtest_filter={{gtest_filter}} [doc("serve project for WASM")] -emscripten_serve CONFIG="Debug": +emscripten_serve: python3 -m http.server -d . #python3 tools/serve.py -p 8000 -d . diff --git a/modules/yup_graphics/native/yup_GraphicsContext_gl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_gl.cpp index 177db4135..be514d63e 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_gl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_gl.cpp @@ -19,7 +19,7 @@ ============================================================================== */ -#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_WASM || YUP_ANDROID +#if 0 && (YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_WASM || YUP_ANDROID) #include "rive/renderer/rive_renderer.hpp" #include "rive/renderer/gl/gles3.hpp" #include "rive/renderer/gl/render_buffer_gl_impl.hpp" @@ -80,8 +80,8 @@ class LowLevelRenderContextGL : public GraphicsContext } #endif - m_plsContext = rive::gpu::RenderContextGLImpl::MakeContext (rive::gpu::RenderContextGLImpl::ContextOptions()); - if (! m_plsContext) + m_renderContext = rive::gpu::RenderContextGLImpl::MakeContext (rive::gpu::RenderContextGLImpl::ContextOptions()); + if (! m_renderContext) { fprintf (stderr, "Failed to create a renderer.\n"); exit (-1); @@ -121,9 +121,9 @@ class LowLevelRenderContextGL : public GraphicsContext #endif } - rive::Factory* factory() override { return m_plsContext.get(); } + rive::Factory* factory() override { return m_renderContext.get(); } - rive::gpu::RenderContext* renderContext() override { return m_plsContext.get(); } + rive::gpu::RenderContext* renderContext() override { return m_renderContext.get(); } rive::gpu::RenderTarget* renderTarget() override { return m_renderTarget.get(); } @@ -134,24 +134,24 @@ class LowLevelRenderContextGL : public GraphicsContext std::unique_ptr makeRenderer (int width, int height) override { - return std::make_unique (m_plsContext.get()); + return std::make_unique (m_renderContext.get()); } void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override { - m_plsContext->static_impl_cast()->invalidateGLState(); - m_plsContext->beginFrame (frameDescriptor); + m_renderContext->static_impl_cast()->invalidateGLState(); + m_renderContext->beginFrame (frameDescriptor); } void end (void*) override { - m_plsContext->flush ({ m_renderTarget.get() }); + m_renderContext->flush ({ m_renderTarget.get() }); - m_plsContext->static_impl_cast()->unbindGLInternalResources(); + m_renderContext->static_impl_cast()->unbindGLInternalResources(); } private: - std::unique_ptr m_plsContext; + std::unique_ptr m_renderContext; rive::rcp m_renderTarget; }; diff --git a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp new file mode 100644 index 000000000..83f7c7292 --- /dev/null +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -0,0 +1,545 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if YUP_RIVE_USE_OPENGL || YUP_LINUX || YUP_WASM || YUP_ANDROID +#include "rive/renderer/rive_renderer.hpp" +#include "rive/renderer/gl/gles3.hpp" +#include "rive/renderer/gl/render_buffer_gl_impl.hpp" +#include "rive/renderer/gl/render_context_gl_impl.hpp" +#include "rive/renderer/gl/render_target_gl.hpp" +#include +#include + +namespace yup +{ + +#if RIVE_DESKTOP_GL && DEBUG +static void GLAPIENTRY err_msg_callback (GLenum source, + GLenum type, + GLuint id, + GLenum severity, + GLsizei length, + const GLchar* message, + const void* userParam) +{ + if (type == GL_DEBUG_TYPE_ERROR) + { + printf ("GL ERROR: %s\n", message); + fflush (stdout); + + assert (false); + } + else if (type == GL_DEBUG_TYPE_PERFORMANCE) + { + if (strcmp (message, + "API_ID_REDUNDANT_FBO performance warning has been generated. Redundant state " + "change in glBindFramebuffer API call, FBO 0, \"\", already bound.") + == 0) + { + return; + } + + if (strstr (message, "is being recompiled based on GL state.")) + { + return; + } + + printf ("GL PERF: %s\n", message); + fflush (stdout); + } +} +#endif + +namespace +{ + +//============================================================================== + +struct Vertex +{ + float position[2]; + float texCoord[2]; +}; + +// Full-screen quad covering clip space, with texture coordinates mapping the texture. +const Vertex quadVertices[] = { + { { -1.0f, 1.0f }, { 0.0f, 0.0f } }, // Top-left + { { -1.0f, -1.0f }, { 0.0f, 1.0f } }, // Bottom-left + { { 1.0f, 1.0f }, { 1.0f, 0.0f } }, // Top-right + { { 1.0f, -1.0f }, { 1.0f, 1.0f } } // Bottom-right +}; + +const char* getVertexShaderSource (bool isGLES) +{ + if (isGLES) + return R"(#version 300 es +precision highp float; + +layout(location = 0) in vec2 position; +layout(location = 1) in vec2 texCoord; + +out vec2 vTexCoord; + +void main() +{ + gl_Position = vec4(position, 0.0, 1.0); + vTexCoord = texCoord; +} +)"; + else + return R"(#version 330 core + +layout(location = 0) in vec2 position; +layout(location = 1) in vec2 texCoord; + +out vec2 vTexCoord; + +void main() +{ + gl_Position = vec4(position, 0.0, 1.0); + vTexCoord = texCoord; +} +)"; +} + +const char* getFragmentShaderSource (bool isGLES) +{ + if (isGLES) + return R"(#version 300 es +precision highp float; +precision highp sampler2D; + +in vec2 vTexCoord; +uniform sampler2D uTexture; + +out vec4 fragColor; + +void main() +{ + // Fix Y-flip by inverting the Y coordinate + vec2 flippedCoord = vec2(vTexCoord.x, 1.0 - vTexCoord.y); + fragColor = texture(uTexture, flippedCoord); +} +)"; + else + return R"(#version 330 core + +in vec2 vTexCoord; +uniform sampler2D uTexture; + +out vec4 fragColor; + +void main() +{ + // Fix Y-flip by inverting the Y coordinate + vec2 flippedCoord = vec2(vTexCoord.x, 1.0 - vTexCoord.y); + fragColor = texture(uTexture, flippedCoord); +} +)"; +} + +GLuint compileShader (GLenum type, const char* source) +{ + GLuint shader = glCreateShader (type); + glShaderSource (shader, 1, &source, nullptr); + glCompileShader (shader); + + GLint compiled = 0; + glGetShaderiv (shader, GL_COMPILE_STATUS, &compiled); + if (compiled == GL_FALSE) + { + GLint maxLength = 0; + glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector infoLog (maxLength); + glGetShaderInfoLog (shader, maxLength, &maxLength, &infoLog[0]); + + printf ("Shader compilation failed: %s\n", &infoLog[0]); + glDeleteShader (shader); + return 0; + } + + return shader; +} + +GLuint createBlitProgram (bool isGLES) +{ + GLuint vertexShader = compileShader (GL_VERTEX_SHADER, getVertexShaderSource (isGLES)); + if (vertexShader == 0) + return 0; + + GLuint fragmentShader = compileShader (GL_FRAGMENT_SHADER, getFragmentShaderSource (isGLES)); + if (fragmentShader == 0) + { + glDeleteShader (vertexShader); + return 0; + } + + GLuint program = glCreateProgram(); + glAttachShader (program, vertexShader); + glAttachShader (program, fragmentShader); + glLinkProgram (program); + + glDeleteShader (vertexShader); + glDeleteShader (fragmentShader); + + GLint linked = 0; + glGetProgramiv (program, GL_LINK_STATUS, &linked); + if (linked == GL_FALSE) + { + GLint maxLength = 0; + glGetProgramiv (program, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector infoLog (maxLength); + glGetProgramInfoLog (program, maxLength, &maxLength, &infoLog[0]); + + printf ("Program linking failed: %s\n", &infoLog[0]); + glDeleteProgram (program); + return 0; + } + + return program; +} + +} // namespace + +//============================================================================== + +/** + * OpenGL Graphics Context implementation that renders Rive content into an + * offscreen framebuffer with attached texture, then blits the result to the + * main framebuffer. This approach enables optimizations like dirty rect + * rendering and matches the approach used in other backends. + */ +class LowLevelRenderContextGL : public GraphicsContext +{ +public: + LowLevelRenderContextGL (Options options) + : m_options (options) + { +#if RIVE_DESKTOP_GL + // Load the OpenGL API using glad. + if (! gladLoadCustomLoader ((GLADloadproc) options.loaderFunction)) + { + fprintf (stderr, "Failed to initialize glad.\n"); + exit (-1); + } +#endif + + m_renderContext = rive::gpu::RenderContextGLImpl::MakeContext (rive::gpu::RenderContextGLImpl::ContextOptions()); + if (! m_renderContext) + { + fprintf (stderr, "Failed to create a renderer.\n"); + exit (-1); + } + + printf ("GL_VENDOR: %s\n", glGetString (GL_VENDOR)); + printf ("GL_RENDERER: %s\n", glGetString (GL_RENDERER)); + printf ("GL_VERSION: %s\n", glGetString (GL_VERSION)); + +#if RIVE_DESKTOP_GL + m_isGLES = strstr ((const char*) glGetString (GL_VERSION), "OpenGL ES") != nullptr; + + printf ("GL_ANGLE_shader_pixel_local_storage_coherent: %i\n", GLAD_GL_ANGLE_shader_pixel_local_storage_coherent); + +#if DEBUG + if (GLAD_GL_KHR_debug) + { + glEnable (GL_DEBUG_OUTPUT); + glDebugMessageControlKHR (GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); + glDebugMessageCallbackKHR (&err_msg_callback, nullptr); + } +#endif +#else + m_isGLES = true; +#endif + +#if DEBUG && ! RIVE_ANDROID + int n; + glGetIntegerv (GL_NUM_EXTENSIONS, &n); + for (size_t i = 0; i < n; ++i) + printf (" %s\n", glGetStringi (GL_EXTENSIONS, i)); +#endif + + initializeBlitResources(); + } + + ~LowLevelRenderContextGL() + { + cleanupBlitResources(); + } + + float dpiScale (void*) const override + { +#if RIVE_DESKTOP_GL && __APPLE__ + return 2; +#else + return 1; +#endif + } + + rive::Factory* factory() override + { + return m_renderContext.get(); + } + + rive::gpu::RenderContext* renderContext() override + { + return m_renderContext.get(); + } + + rive::gpu::RenderTarget* renderTarget() override + { + return m_offscreenRenderTarget.get(); + } + + void onSizeChanged (void* window, int width, int height, uint32_t sampleCount) override + { + m_width = width; + m_height = height; + m_sampleCount = sampleCount; + + createOffscreenResources(); + } + + std::unique_ptr makeRenderer (int width, int height) override + { + return std::make_unique (m_renderContext.get()); + } + + void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override + { + m_renderContext->static_impl_cast()->invalidateGLState(); + m_renderContext->beginFrame (frameDescriptor); + } + + void end (void*) override + { + // Render Rive content to offscreen framebuffer + m_renderContext->flush ({ m_offscreenRenderTarget.get() }); + + // Blit offscreen texture to main framebuffer + blitToMainFramebuffer(); + + m_renderContext->static_impl_cast()->unbindGLInternalResources(); + } + +private: + void initializeBlitResources() + { + printf ("initializeBlitResources: Creating blit resources\n"); + + // Create blit shader program + m_blitProgram = createBlitProgram (m_isGLES); + if (m_blitProgram == 0) + { + printf ("Failed to create blit shader program.\n"); + return; + } + + printf ("Blit shader program created: %u\n", m_blitProgram); + + // Get uniform location + m_textureUniformLocation = glGetUniformLocation (m_blitProgram, "uTexture"); + printf ("Uniform location for uTexture: %d\n", m_textureUniformLocation); + + // Create vertex buffer for fullscreen quad + glGenBuffers (1, &m_quadVertexBuffer); + glBindBuffer (GL_ARRAY_BUFFER, m_quadVertexBuffer); + glBufferData (GL_ARRAY_BUFFER, sizeof (quadVertices), quadVertices, GL_STATIC_DRAW); + + // Create vertex array object + glGenVertexArrays (1, &m_quadVAO); + glBindVertexArray (m_quadVAO); + + // Setup vertex attributes + glBindBuffer (GL_ARRAY_BUFFER, m_quadVertexBuffer); + + // Position attribute + glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, sizeof (Vertex), (void*) offsetof (Vertex, position)); + glEnableVertexAttribArray (0); + + // Texture coordinate attribute + glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, sizeof (Vertex), (void*) offsetof (Vertex, texCoord)); + glEnableVertexAttribArray (1); + + // Unbind + glBindVertexArray (0); + glBindBuffer (GL_ARRAY_BUFFER, 0); + } + + void cleanupBlitResources() + { + if (m_quadVAO != 0) + { + glDeleteVertexArrays (1, &m_quadVAO); + m_quadVAO = 0; + } + + if (m_quadVertexBuffer != 0) + { + glDeleteBuffers (1, &m_quadVertexBuffer); + m_quadVertexBuffer = 0; + } + + if (m_blitProgram != 0) + { + glDeleteProgram (m_blitProgram); + m_blitProgram = 0; + } + + cleanupOffscreenResources(); + } + + void createOffscreenResources() + { + if (m_width <= 0 || m_height <= 0) + { + printf ("createOffscreenResources: Invalid size %dx%d\n", m_width, m_height); + return; + } + + printf ("createOffscreenResources: Creating %dx%d framebuffer\n", m_width, m_height); + + cleanupOffscreenResources(); + + // Create offscreen texture + glGenTextures (1, &m_offscreenTexture); + glBindTexture (GL_TEXTURE_2D, m_offscreenTexture); + glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, m_width, m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Check for GL errors after texture creation + GLenum error = glGetError(); + if (error != GL_NO_ERROR) + { + printf ("GL error after texture creation: 0x%x\n", error); + } + + glBindTexture (GL_TEXTURE_2D, 0); + + // Create framebuffer and attach texture + glGenFramebuffers (1, &m_offscreenFramebuffer); + glBindFramebuffer (GL_FRAMEBUFFER, m_offscreenFramebuffer); + glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_offscreenTexture, 0); + + // Check framebuffer completeness + GLenum status = glCheckFramebufferStatus (GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) + { + printf ("Offscreen framebuffer is not complete: 0x%x\n", status); + } + else + { + printf ("Offscreen framebuffer created successfully: FBO=%u, TEX=%u\n", m_offscreenFramebuffer, m_offscreenTexture); + } + + glBindFramebuffer (GL_FRAMEBUFFER, 0); + + // Create Rive render target that uses our offscreen framebuffer + m_offscreenRenderTarget = rive::make_rcp (m_width, m_height, m_offscreenFramebuffer, m_sampleCount); + + printf ("Rive render target created\n"); + } + + void cleanupOffscreenResources() + { + if (m_offscreenFramebuffer != 0) + { + glDeleteFramebuffers (1, &m_offscreenFramebuffer); + m_offscreenFramebuffer = 0; + } + + if (m_offscreenTexture != 0) + { + glDeleteTextures (1, &m_offscreenTexture); + m_offscreenTexture = 0; + } + + m_offscreenRenderTarget.reset(); + } + + void blitToMainFramebuffer() + { + if (m_blitProgram == 0 || m_offscreenTexture == 0) + return; + + // Bind main framebuffer + glBindFramebuffer (GL_FRAMEBUFFER, 0); + glViewport (0, 0, m_width, m_height); + + // Disable depth test, blending, and face culling for the blit + glDisable (GL_DEPTH_TEST); + glDisable (GL_BLEND); + glDisable (GL_CULL_FACE); + glDisable (GL_SCISSOR_TEST); + + // Use blit shader + glUseProgram (m_blitProgram); + + // Bind offscreen texture + glActiveTexture (GL_TEXTURE0); + glBindTexture (GL_TEXTURE_2D, m_offscreenTexture); + glUniform1i (m_textureUniformLocation, 0); + + // Draw fullscreen quad + glBindVertexArray (m_quadVAO); + glDrawArrays (GL_TRIANGLE_STRIP, 0, 4); + + // Cleanup + glBindVertexArray (0); + glUseProgram (0); + glBindTexture (GL_TEXTURE_2D, 0); + } + +private: + Options m_options; + std::unique_ptr m_renderContext; + rive::rcp m_offscreenRenderTarget; + + // Offscreen rendering resources + GLuint m_offscreenFramebuffer = 0; + GLuint m_offscreenTexture = 0; + int m_width = 0; + int m_height = 0; + uint32_t m_sampleCount = 0; + + // Blit resources + GLuint m_blitProgram = 0; + GLuint m_quadVertexBuffer = 0; + GLuint m_quadVAO = 0; + GLint m_textureUniformLocation = -1; + + bool m_isGLES = false; +}; + +//============================================================================== + +std::unique_ptr yup_constructOpenGLGraphicsContext (GraphicsContext::Options options) +{ + return std::make_unique (options); +} + +} // namespace yup +#endif \ No newline at end of file diff --git a/modules/yup_graphics/yup_graphics.cpp b/modules/yup_graphics/yup_graphics.cpp index 486ef9a9c..b5632bb19 100644 --- a/modules/yup_graphics/yup_graphics.cpp +++ b/modules/yup_graphics/yup_graphics.cpp @@ -46,7 +46,8 @@ #endif #if YUP_RIVE_USE_OPENGL -#include "native/yup_GraphicsContext_gl.cpp" +//#include "native/yup_GraphicsContext_gl.cpp" +#include "native/yup_GraphicsContext_opengl.cpp" #endif //============================================================================== @@ -67,7 +68,8 @@ #endif #if YUP_RIVE_USE_OPENGL -#include "native/yup_GraphicsContext_gl.cpp" +//#include "native/yup_GraphicsContext_gl.cpp" +#include "native/yup_GraphicsContext_opengl.cpp" #endif //============================================================================== @@ -79,7 +81,8 @@ #include #endif -#include "native/yup_GraphicsContext_gl.cpp" +//#include "native/yup_GraphicsContext_gl.cpp" +#include "native/yup_GraphicsContext_opengl.cpp" #endif diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 1c7e70a2b..2590ddce6 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -1583,8 +1583,10 @@ void initialiseYup_Windowing() break; } +#if ! YUP_WASM if (! timeoutDetector.hasTimedOut()) Thread::sleep (1); +#endif }); // Set the default theme on ios diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.h b/modules/yup_gui/native/yup_Windowing_sdl2.h index ba5fc1f32..9a13502ce 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.h +++ b/modules/yup_gui/native/yup_Windowing_sdl2.h @@ -32,7 +32,7 @@ class SDL2ComponentNative final #if (YUP_EMSCRIPTEN && RIVE_WEBGL) && ! defined(__EMSCRIPTEN_PTHREADS__) static constexpr bool renderDrivenByTimer = true; #else - static constexpr bool renderDrivenByTimer = false; + static constexpr bool renderDrivenByTimer = true; #endif public: diff --git a/modules/yup_gui/windowing/yup_DocumentWindow.cpp b/modules/yup_gui/windowing/yup_DocumentWindow.cpp index e25bb7b31..516506cc0 100644 --- a/modules/yup_gui/windowing/yup_DocumentWindow.cpp +++ b/modules/yup_gui/windowing/yup_DocumentWindow.cpp @@ -27,14 +27,7 @@ namespace yup DocumentWindow::DocumentWindow (const ComponentNative::Options& options, const Color& backgroundColor) : backgroundColor (backgroundColor) { - auto finalOptions = options; - -#if YUP_EMSCRIPTEN - // This is enforced for now until we have a better way to handle dirty regions on emscripten - finalOptions = finalOptions.withRenderContinuous (true); -#endif - - addToDesktop (finalOptions, nullptr); + addToDesktop (options, nullptr); } DocumentWindow::~DocumentWindow() From 302010da00aadd7831a917748928718972df4344 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 21 Jun 2025 15:23:58 +0200 Subject: [PATCH 18/51] Fixes --- modules/yup_events/native/yup_MessageManager_ios.mm | 11 +++++++---- modules/yup_events/yup_events.cpp | 2 +- modules/yup_gui/menus/yup_PopupMenu.cpp | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/yup_events/native/yup_MessageManager_ios.mm b/modules/yup_events/native/yup_MessageManager_ios.mm index ccfb89ece..9df3c0637 100644 --- a/modules/yup_events/native/yup_MessageManager_ios.mm +++ b/modules/yup_events/native/yup_MessageManager_ios.mm @@ -48,9 +48,6 @@ void runNSApplication() [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.001]]; } - - for (const auto& func : shutdownCallbacks) - func(); } //============================================================================== @@ -108,7 +105,13 @@ void runNSApplication() if (messageQueue == nullptr) messageQueue.reset(new InternalMessageQueue()); - MessageManager::getInstance()->registerEventLoopCallback(runNSApplication); + MessageManager::getInstance()->registerEventLoopCallback ([this] + { + runNSApplication(); + + for (const auto& func : shutdownCallbacks) + func(); + }); } void MessageManager::doPlatformSpecificShutdown() diff --git a/modules/yup_events/yup_events.cpp b/modules/yup_events/yup_events.cpp index 6bd337ab8..a7168f496 100644 --- a/modules/yup_events/yup_events.cpp +++ b/modules/yup_events/yup_events.cpp @@ -120,7 +120,7 @@ #include "native/yup_EventLoopInternal_linux.h" #include "native/yup_Messaging_linux.cpp" -#elif YUP_WASM && YUP_EMSCRIPTEN +#elif YUP_EMSCRIPTEN #include "native/yup_Messaging_emscripten.cpp" #elif YUP_ANDROID diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 1eca20b51..7fd2205d9 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -413,6 +413,7 @@ class PopupMenu::MenuWindow : public Component switch (justification) { + default: case Justification::topLeft: position = targetArea.getTopLeft(); break; From 51f3495e26922db467f907ed9a6927f429a927f4 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Sat, 21 Jun 2025 15:29:23 +0000 Subject: [PATCH 19/51] Code formatting --- modules/yup_events/native/yup_MessageManager_ios.mm | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/yup_events/native/yup_MessageManager_ios.mm b/modules/yup_events/native/yup_MessageManager_ios.mm index 9df3c0637..92981a984 100644 --- a/modules/yup_events/native/yup_MessageManager_ios.mm +++ b/modules/yup_events/native/yup_MessageManager_ios.mm @@ -105,13 +105,12 @@ void runNSApplication() if (messageQueue == nullptr) messageQueue.reset(new InternalMessageQueue()); - MessageManager::getInstance()->registerEventLoopCallback ([this] - { + MessageManager::getInstance()->registerEventLoopCallback([this] + { runNSApplication(); for (const auto& func : shutdownCallbacks) - func(); - }); + func(); }); } void MessageManager::doPlatformSpecificShutdown() From cfe7ed008526f36fce9301bd505f9e32e6dbdc3b Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 22 Jun 2025 23:26:23 +0200 Subject: [PATCH 20/51] More tweaks --- examples/graphics/source/examples/Audio.h | 4 +--- modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp | 3 +++ modules/yup_gui/native/yup_Windowing_sdl2.cpp | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index c24692253..4f44be68d 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -281,14 +281,12 @@ class AudioExample readPos = readPos % inputData.size(); } - const yup::CriticalSection::ScopedLockType sl (renderMutex); + //const yup::CriticalSection::ScopedLockType sl (renderMutex); std::swap (inputData, renderData); } void audioDeviceAboutToStart (yup::AudioIODevice* device) override { - const yup::CriticalSection::ScopedLockType sl (renderMutex); - inputData.resize (device->getDefaultBufferSize()); renderData.resize (device->getDefaultBufferSize()); readPos = 0; diff --git a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp index 83f7c7292..ce35179d9 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -291,6 +291,8 @@ class LowLevelRenderContextGL : public GraphicsContext { #if RIVE_DESKTOP_GL && __APPLE__ return 2; +#elif RIVE_WEBGL && YUP_EMSCRIPTEN + return (float) emscripten_get_device_pixel_ratio(); #else return 1; #endif @@ -328,6 +330,7 @@ class LowLevelRenderContextGL : public GraphicsContext void begin (const rive::gpu::RenderContext::FrameDescriptor& frameDescriptor) override { m_renderContext->static_impl_cast()->invalidateGLState(); + m_renderContext->beginFrame (frameDescriptor); } diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 1cfb8ded8..8d6365242 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -258,6 +258,11 @@ void SDL2ComponentNative::setBounds (const Rectangle& newBounds) int leftMargin = 0, topMargin = 0, rightMargin = 0, bottomMargin = 0; #if YUP_EMSCRIPTEN && RIVE_WEBGL + //const double devicePixelRatio = emscripten_get_device_pixel_ratio(); + //SDL_SetWindowSize (window, + // jmax (0, (int) (newBounds.getWidth() * devicePixelRatio)), + // jmax (0, (int) (newBounds.getHeight() * devicePixelRatio))); + SDL_SetWindowSize (window, jmax (0, newBounds.getWidth()), jmax (0, newBounds.getHeight())); From ae4cc65d90270d8e2371859ff2273eaab780d7e5 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 23 Jun 2025 00:44:38 +0200 Subject: [PATCH 21/51] More tweaks --- examples/graphics/source/examples/Audio.h | 5 +- examples/graphics/source/main.cpp | 28 ++++---- .../native/yup_GraphicsContext_opengl.cpp | 71 +++++-------------- 3 files changed, 34 insertions(+), 70 deletions(-) diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index 4f44be68d..f0e86c64a 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -245,11 +245,12 @@ class AudioExample void refreshDisplay (double lastFrameTimeSeconds) override { { - const yup::CriticalSection::ScopedLockType sl (renderMutex); + // const yup::CriticalSection::ScopedLockType sl (renderMutex); oscilloscope.setRenderData (renderData, readPos); } - oscilloscope.repaint(); + if (oscilloscope.isVisible()) + oscilloscope.repaint(); } void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index 091a7abe4..91f0a1c51 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -80,9 +80,9 @@ class CustomWindow { auto button = std::make_unique ("Audio"); - button->onClick = [this, &counter] + button->onClick = [this, number = counter++] { - selectComponent (counter++); + selectComponent (number); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -93,9 +93,9 @@ class CustomWindow { auto button = std::make_unique ("Layout Fonts"); - button->onClick = [this, &counter] + button->onClick = [this, number = counter++] { - selectComponent (counter++); + selectComponent (number); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -106,9 +106,9 @@ class CustomWindow { auto button = std::make_unique ("Variable Fonts"); - button->onClick = [this, &counter] + button->onClick = [this, number = counter++] { - selectComponent (counter++); + selectComponent (number); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -119,9 +119,9 @@ class CustomWindow { auto button = std::make_unique ("Paths"); - button->onClick = [this, &counter] + button->onClick = [this, number = counter++] { - selectComponent (counter++); + selectComponent (number); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -132,9 +132,9 @@ class CustomWindow { auto button = std::make_unique ("Text Editor"); - button->onClick = [this, &counter] + button->onClick = [this, number = counter++] { - selectComponent (counter++); + selectComponent (number); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -145,9 +145,9 @@ class CustomWindow { auto button = std::make_unique ("Popup Menu"); - button->onClick = [this, &counter] + button->onClick = [this, number = counter++] { - selectComponent (counter++); + selectComponent (number); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); @@ -158,9 +158,9 @@ class CustomWindow { auto button = std::make_unique ("File Chooser"); - button->onClick = [this, &counter] + button->onClick = [this, number = counter++] { - selectComponent (counter++); + selectComponent (number); }; addAndMakeVisible (button.get()); buttons.add (std::move (button)); diff --git a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp index ce35179d9..642ad28e3 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -240,7 +240,7 @@ class LowLevelRenderContextGL : public GraphicsContext if (! gladLoadCustomLoader ((GLADloadproc) options.loaderFunction)) { fprintf (stderr, "Failed to initialize glad.\n"); - exit (-1); + return; } #endif @@ -248,7 +248,7 @@ class LowLevelRenderContextGL : public GraphicsContext if (! m_renderContext) { fprintf (stderr, "Failed to create a renderer.\n"); - exit (-1); + return; } printf ("GL_VENDOR: %s\n", glGetString (GL_VENDOR)); @@ -336,33 +336,28 @@ class LowLevelRenderContextGL : public GraphicsContext void end (void*) override { - // Render Rive content to offscreen framebuffer - m_renderContext->flush ({ m_offscreenRenderTarget.get() }); - - // Blit offscreen texture to main framebuffer - blitToMainFramebuffer(); + rive::gpu::RenderContext::FlushResources flushResources; + flushResources.renderTarget = m_offscreenRenderTarget.get(); + m_renderContext->flush (flushResources); m_renderContext->static_impl_cast()->unbindGLInternalResources(); + + blitToMainFramebuffer(); } private: void initializeBlitResources() { - printf ("initializeBlitResources: Creating blit resources\n"); - // Create blit shader program m_blitProgram = createBlitProgram (m_isGLES); if (m_blitProgram == 0) { - printf ("Failed to create blit shader program.\n"); + fprintf (stderr, "Failed to create blit shader program.\n"); return; } - printf ("Blit shader program created: %u\n", m_blitProgram); - // Get uniform location m_textureUniformLocation = glGetUniformLocation (m_blitProgram, "uTexture"); - printf ("Uniform location for uTexture: %d\n", m_textureUniformLocation); // Create vertex buffer for fullscreen quad glGenBuffers (1, &m_quadVertexBuffer); @@ -416,12 +411,10 @@ class LowLevelRenderContextGL : public GraphicsContext { if (m_width <= 0 || m_height <= 0) { - printf ("createOffscreenResources: Invalid size %dx%d\n", m_width, m_height); + fprintf (stderr, "createOffscreenResources: Invalid size %dx%d\n", m_width, m_height); return; } - printf ("createOffscreenResources: Creating %dx%d framebuffer\n", m_width, m_height); - cleanupOffscreenResources(); // Create offscreen texture @@ -436,9 +429,7 @@ class LowLevelRenderContextGL : public GraphicsContext // Check for GL errors after texture creation GLenum error = glGetError(); if (error != GL_NO_ERROR) - { - printf ("GL error after texture creation: 0x%x\n", error); - } + fprintf (stderr, "GL error after texture creation: 0x%x\n", error); glBindTexture (GL_TEXTURE_2D, 0); @@ -450,20 +441,12 @@ class LowLevelRenderContextGL : public GraphicsContext // Check framebuffer completeness GLenum status = glCheckFramebufferStatus (GL_FRAMEBUFFER); if (status != GL_FRAMEBUFFER_COMPLETE) - { - printf ("Offscreen framebuffer is not complete: 0x%x\n", status); - } - else - { - printf ("Offscreen framebuffer created successfully: FBO=%u, TEX=%u\n", m_offscreenFramebuffer, m_offscreenTexture); - } + fprintf (stderr, "Offscreen framebuffer is not complete: 0x%x\n", status); glBindFramebuffer (GL_FRAMEBUFFER, 0); // Create Rive render target that uses our offscreen framebuffer m_offscreenRenderTarget = rive::make_rcp (m_width, m_height, m_offscreenFramebuffer, m_sampleCount); - - printf ("Rive render target created\n"); } void cleanupOffscreenResources() @@ -486,34 +469,14 @@ class LowLevelRenderContextGL : public GraphicsContext void blitToMainFramebuffer() { if (m_blitProgram == 0 || m_offscreenTexture == 0) + { + fprintf (stderr, "blitToMainFramebuffer: Invalid program or texture\n"); return; + } - // Bind main framebuffer - glBindFramebuffer (GL_FRAMEBUFFER, 0); - glViewport (0, 0, m_width, m_height); - - // Disable depth test, blending, and face culling for the blit - glDisable (GL_DEPTH_TEST); - glDisable (GL_BLEND); - glDisable (GL_CULL_FACE); - glDisable (GL_SCISSOR_TEST); - - // Use blit shader - glUseProgram (m_blitProgram); - - // Bind offscreen texture - glActiveTexture (GL_TEXTURE0); - glBindTexture (GL_TEXTURE_2D, m_offscreenTexture); - glUniform1i (m_textureUniformLocation, 0); - - // Draw fullscreen quad - glBindVertexArray (m_quadVAO); - glDrawArrays (GL_TRIANGLE_STRIP, 0, 4); - - // Cleanup - glBindVertexArray (0); - glUseProgram (0); - glBindTexture (GL_TEXTURE_2D, 0); + glBindFramebuffer (GL_READ_FRAMEBUFFER, m_offscreenFramebuffer); + glBindFramebuffer (GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer (0, 0, m_width, m_height, 0, 0, m_width, m_height, GL_COLOR_BUFFER_BIT, GL_NEAREST); } private: From 9ba588b1b11cf6ee08deea8b03bf5a84ea318417 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Sun, 22 Jun 2025 22:45:15 +0000 Subject: [PATCH 22/51] Code formatting --- .../native/yup_GraphicsContext_opengl.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp index 642ad28e3..1d987cee0 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -81,10 +81,10 @@ struct Vertex // Full-screen quad covering clip space, with texture coordinates mapping the texture. const Vertex quadVertices[] = { - { { -1.0f, 1.0f }, { 0.0f, 0.0f } }, // Top-left - { { -1.0f, -1.0f }, { 0.0f, 1.0f } }, // Bottom-left - { { 1.0f, 1.0f }, { 1.0f, 0.0f } }, // Top-right - { { 1.0f, -1.0f }, { 1.0f, 1.0f } } // Bottom-right + { { -1.0f, 1.0f }, { 0.0f, 0.0f } }, // Top-left + { { -1.0f, -1.0f }, { 0.0f, 1.0f } }, // Bottom-left + { { 1.0f, 1.0f }, { 1.0f, 0.0f } }, // Top-right + { { 1.0f, -1.0f }, { 1.0f, 1.0f } } // Bottom-right }; const char* getVertexShaderSource (bool isGLES) @@ -122,8 +122,8 @@ void main() const char* getFragmentShaderSource (bool isGLES) { - if (isGLES) - return R"(#version 300 es + if (isGLES) + return R"(#version 300 es precision highp float; precision highp sampler2D; @@ -139,8 +139,8 @@ void main() fragColor = texture(uTexture, flippedCoord); } )"; - else - return R"(#version 330 core + else + return R"(#version 330 core in vec2 vTexCoord; uniform sampler2D uTexture; From 0824f9ddeb287a809ada168a925febfc563fbb76 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 23 Jun 2025 08:56:26 +0200 Subject: [PATCH 23/51] More fixes --- modules/yup_events/native/yup_MessageManager_ios.mm | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/modules/yup_events/native/yup_MessageManager_ios.mm b/modules/yup_events/native/yup_MessageManager_ios.mm index 92981a984..341cad85c 100644 --- a/modules/yup_events/native/yup_MessageManager_ios.mm +++ b/modules/yup_events/native/yup_MessageManager_ios.mm @@ -105,16 +105,14 @@ void runNSApplication() if (messageQueue == nullptr) messageQueue.reset(new InternalMessageQueue()); - MessageManager::getInstance()->registerEventLoopCallback([this] - { - runNSApplication(); - - for (const auto& func : shutdownCallbacks) - func(); }); + MessageManager::getInstance()->registerEventLoopCallback (runNSApplication); } void MessageManager::doPlatformSpecificShutdown() { + //for (const auto& func : shutdownCallbacks) + // func(); + messageQueue = nullptr; } From 8996144a9baada978d3cbd76c0cacbc3d2a7deed Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Mon, 23 Jun 2025 06:56:57 +0000 Subject: [PATCH 24/51] Code formatting --- modules/yup_events/native/yup_MessageManager_ios.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/yup_events/native/yup_MessageManager_ios.mm b/modules/yup_events/native/yup_MessageManager_ios.mm index 341cad85c..0f783d466 100644 --- a/modules/yup_events/native/yup_MessageManager_ios.mm +++ b/modules/yup_events/native/yup_MessageManager_ios.mm @@ -105,13 +105,13 @@ void runNSApplication() if (messageQueue == nullptr) messageQueue.reset(new InternalMessageQueue()); - MessageManager::getInstance()->registerEventLoopCallback (runNSApplication); + MessageManager::getInstance()->registerEventLoopCallback(runNSApplication); } void MessageManager::doPlatformSpecificShutdown() { - //for (const auto& func : shutdownCallbacks) - // func(); + // for (const auto& func : shutdownCallbacks) + // func(); messageQueue = nullptr; } From a3463f665e6cc3c71daa77bf168f242bbb31d14a Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 23 Jun 2025 09:02:57 +0200 Subject: [PATCH 25/51] Fixes --- modules/yup_events/native/yup_MessageManager_ios.mm | 6 +++--- modules/yup_events/native/yup_MessageManager_mac.mm | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/yup_events/native/yup_MessageManager_ios.mm b/modules/yup_events/native/yup_MessageManager_ios.mm index 0f783d466..a93e2a396 100644 --- a/modules/yup_events/native/yup_MessageManager_ios.mm +++ b/modules/yup_events/native/yup_MessageManager_ios.mm @@ -64,6 +64,9 @@ void runNSApplication() void MessageManager::stopDispatchLoop() { + for (const auto& func : shutdownCallbacks) + func(); + if (!SystemStats::isRunningInAppExtensionSandbox()) [[[UIApplication sharedApplication] delegate] applicationWillTerminate:[UIApplication sharedApplication]]; @@ -110,9 +113,6 @@ void runNSApplication() void MessageManager::doPlatformSpecificShutdown() { - // for (const auto& func : shutdownCallbacks) - // func(); - messageQueue = nullptr; } diff --git a/modules/yup_events/native/yup_MessageManager_mac.mm b/modules/yup_events/native/yup_MessageManager_mac.mm index 6eb59fcc5..150d5a18e 100644 --- a/modules/yup_events/native/yup_MessageManager_mac.mm +++ b/modules/yup_events/native/yup_MessageManager_mac.mm @@ -568,6 +568,7 @@ static void changed(id self, SEL, NSNotification*) { pimpl.reset(new Pimpl(*this)); } + MountedVolumeListChangeDetector::~MountedVolumeListChangeDetector() {} #endif From 691ceaa4ac6dfc06a124318886644317303f5180 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 23 Jun 2025 09:29:06 +0200 Subject: [PATCH 26/51] More fixes --- .../native/yup_GraphicsContext_opengl.cpp | 236 +----------------- modules/yup_gui/native/yup_Windowing_sdl2.h | 2 +- 2 files changed, 4 insertions(+), 234 deletions(-) diff --git a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp index 1d987cee0..83758abe8 100644 --- a/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp +++ b/modules/yup_graphics/native/yup_GraphicsContext_opengl.cpp @@ -68,159 +68,6 @@ static void GLAPIENTRY err_msg_callback (GLenum source, } #endif -namespace -{ - -//============================================================================== - -struct Vertex -{ - float position[2]; - float texCoord[2]; -}; - -// Full-screen quad covering clip space, with texture coordinates mapping the texture. -const Vertex quadVertices[] = { - { { -1.0f, 1.0f }, { 0.0f, 0.0f } }, // Top-left - { { -1.0f, -1.0f }, { 0.0f, 1.0f } }, // Bottom-left - { { 1.0f, 1.0f }, { 1.0f, 0.0f } }, // Top-right - { { 1.0f, -1.0f }, { 1.0f, 1.0f } } // Bottom-right -}; - -const char* getVertexShaderSource (bool isGLES) -{ - if (isGLES) - return R"(#version 300 es -precision highp float; - -layout(location = 0) in vec2 position; -layout(location = 1) in vec2 texCoord; - -out vec2 vTexCoord; - -void main() -{ - gl_Position = vec4(position, 0.0, 1.0); - vTexCoord = texCoord; -} -)"; - else - return R"(#version 330 core - -layout(location = 0) in vec2 position; -layout(location = 1) in vec2 texCoord; - -out vec2 vTexCoord; - -void main() -{ - gl_Position = vec4(position, 0.0, 1.0); - vTexCoord = texCoord; -} -)"; -} - -const char* getFragmentShaderSource (bool isGLES) -{ - if (isGLES) - return R"(#version 300 es -precision highp float; -precision highp sampler2D; - -in vec2 vTexCoord; -uniform sampler2D uTexture; - -out vec4 fragColor; - -void main() -{ - // Fix Y-flip by inverting the Y coordinate - vec2 flippedCoord = vec2(vTexCoord.x, 1.0 - vTexCoord.y); - fragColor = texture(uTexture, flippedCoord); -} -)"; - else - return R"(#version 330 core - -in vec2 vTexCoord; -uniform sampler2D uTexture; - -out vec4 fragColor; - -void main() -{ - // Fix Y-flip by inverting the Y coordinate - vec2 flippedCoord = vec2(vTexCoord.x, 1.0 - vTexCoord.y); - fragColor = texture(uTexture, flippedCoord); -} -)"; -} - -GLuint compileShader (GLenum type, const char* source) -{ - GLuint shader = glCreateShader (type); - glShaderSource (shader, 1, &source, nullptr); - glCompileShader (shader); - - GLint compiled = 0; - glGetShaderiv (shader, GL_COMPILE_STATUS, &compiled); - if (compiled == GL_FALSE) - { - GLint maxLength = 0; - glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &maxLength); - - std::vector infoLog (maxLength); - glGetShaderInfoLog (shader, maxLength, &maxLength, &infoLog[0]); - - printf ("Shader compilation failed: %s\n", &infoLog[0]); - glDeleteShader (shader); - return 0; - } - - return shader; -} - -GLuint createBlitProgram (bool isGLES) -{ - GLuint vertexShader = compileShader (GL_VERTEX_SHADER, getVertexShaderSource (isGLES)); - if (vertexShader == 0) - return 0; - - GLuint fragmentShader = compileShader (GL_FRAGMENT_SHADER, getFragmentShaderSource (isGLES)); - if (fragmentShader == 0) - { - glDeleteShader (vertexShader); - return 0; - } - - GLuint program = glCreateProgram(); - glAttachShader (program, vertexShader); - glAttachShader (program, fragmentShader); - glLinkProgram (program); - - glDeleteShader (vertexShader); - glDeleteShader (fragmentShader); - - GLint linked = 0; - glGetProgramiv (program, GL_LINK_STATUS, &linked); - if (linked == GL_FALSE) - { - GLint maxLength = 0; - glGetProgramiv (program, GL_INFO_LOG_LENGTH, &maxLength); - - std::vector infoLog (maxLength); - glGetProgramInfoLog (program, maxLength, &maxLength, &infoLog[0]); - - printf ("Program linking failed: %s\n", &infoLog[0]); - glDeleteProgram (program); - return 0; - } - - return program; -} - -} // namespace - //============================================================================== /** @@ -256,8 +103,6 @@ class LowLevelRenderContextGL : public GraphicsContext printf ("GL_VERSION: %s\n", glGetString (GL_VERSION)); #if RIVE_DESKTOP_GL - m_isGLES = strstr ((const char*) glGetString (GL_VERSION), "OpenGL ES") != nullptr; - printf ("GL_ANGLE_shader_pixel_local_storage_coherent: %i\n", GLAD_GL_ANGLE_shader_pixel_local_storage_coherent); #if DEBUG @@ -268,8 +113,6 @@ class LowLevelRenderContextGL : public GraphicsContext glDebugMessageCallbackKHR (&err_msg_callback, nullptr); } #endif -#else - m_isGLES = true; #endif #if DEBUG && ! RIVE_ANDROID @@ -278,13 +121,11 @@ class LowLevelRenderContextGL : public GraphicsContext for (size_t i = 0; i < n; ++i) printf (" %s\n", glGetStringi (GL_EXTENSIONS, i)); #endif - - initializeBlitResources(); } ~LowLevelRenderContextGL() { - cleanupBlitResources(); + cleanupOffscreenResources(); } float dpiScale (void*) const override @@ -336,9 +177,7 @@ class LowLevelRenderContextGL : public GraphicsContext void end (void*) override { - rive::gpu::RenderContext::FlushResources flushResources; - flushResources.renderTarget = m_offscreenRenderTarget.get(); - m_renderContext->flush (flushResources); + m_renderContext->flush ({ m_offscreenRenderTarget.get() }); m_renderContext->static_impl_cast()->unbindGLInternalResources(); @@ -346,67 +185,6 @@ class LowLevelRenderContextGL : public GraphicsContext } private: - void initializeBlitResources() - { - // Create blit shader program - m_blitProgram = createBlitProgram (m_isGLES); - if (m_blitProgram == 0) - { - fprintf (stderr, "Failed to create blit shader program.\n"); - return; - } - - // Get uniform location - m_textureUniformLocation = glGetUniformLocation (m_blitProgram, "uTexture"); - - // Create vertex buffer for fullscreen quad - glGenBuffers (1, &m_quadVertexBuffer); - glBindBuffer (GL_ARRAY_BUFFER, m_quadVertexBuffer); - glBufferData (GL_ARRAY_BUFFER, sizeof (quadVertices), quadVertices, GL_STATIC_DRAW); - - // Create vertex array object - glGenVertexArrays (1, &m_quadVAO); - glBindVertexArray (m_quadVAO); - - // Setup vertex attributes - glBindBuffer (GL_ARRAY_BUFFER, m_quadVertexBuffer); - - // Position attribute - glVertexAttribPointer (0, 2, GL_FLOAT, GL_FALSE, sizeof (Vertex), (void*) offsetof (Vertex, position)); - glEnableVertexAttribArray (0); - - // Texture coordinate attribute - glVertexAttribPointer (1, 2, GL_FLOAT, GL_FALSE, sizeof (Vertex), (void*) offsetof (Vertex, texCoord)); - glEnableVertexAttribArray (1); - - // Unbind - glBindVertexArray (0); - glBindBuffer (GL_ARRAY_BUFFER, 0); - } - - void cleanupBlitResources() - { - if (m_quadVAO != 0) - { - glDeleteVertexArrays (1, &m_quadVAO); - m_quadVAO = 0; - } - - if (m_quadVertexBuffer != 0) - { - glDeleteBuffers (1, &m_quadVertexBuffer); - m_quadVertexBuffer = 0; - } - - if (m_blitProgram != 0) - { - glDeleteProgram (m_blitProgram); - m_blitProgram = 0; - } - - cleanupOffscreenResources(); - } - void createOffscreenResources() { if (m_width <= 0 || m_height <= 0) @@ -468,7 +246,7 @@ class LowLevelRenderContextGL : public GraphicsContext void blitToMainFramebuffer() { - if (m_blitProgram == 0 || m_offscreenTexture == 0) + if (m_offscreenTexture == 0) { fprintf (stderr, "blitToMainFramebuffer: Invalid program or texture\n"); return; @@ -490,14 +268,6 @@ class LowLevelRenderContextGL : public GraphicsContext int m_width = 0; int m_height = 0; uint32_t m_sampleCount = 0; - - // Blit resources - GLuint m_blitProgram = 0; - GLuint m_quadVertexBuffer = 0; - GLuint m_quadVAO = 0; - GLint m_textureUniformLocation = -1; - - bool m_isGLES = false; }; //============================================================================== diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.h b/modules/yup_gui/native/yup_Windowing_sdl2.h index 9a13502ce..c817794c9 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.h +++ b/modules/yup_gui/native/yup_Windowing_sdl2.h @@ -30,7 +30,7 @@ class SDL2ComponentNative final , public AsyncUpdater { #if (YUP_EMSCRIPTEN && RIVE_WEBGL) && ! defined(__EMSCRIPTEN_PTHREADS__) - static constexpr bool renderDrivenByTimer = true; + static constexpr bool renderDrivenByTimer = false; #else static constexpr bool renderDrivenByTimer = true; #endif From 30f9c76ddf7b0918525af7f4df6d403b815de4b1 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 23 Jun 2025 17:54:02 +0200 Subject: [PATCH 27/51] Tweaks --- examples/graphics/source/examples/PopupMenu.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 0fd9c6115..8f85634e1 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -38,28 +38,28 @@ class PopupMenuDemo : public yup::Component statusLabel.setTitle ("Right-click anywhere to show context menu"); addAndMakeVisible (basicMenuButton); - basicMenuButton.setTitle ("Show Basic Menu"); + basicMenuButton.setButtonText ("Show Basic Menu"); basicMenuButton.onClick = [this] { showBasicMenu(); }; addAndMakeVisible (subMenuButton); - subMenuButton.setTitle ("Show Sub-Menu"); + subMenuButton.setButtonText ("Show Sub-Menu"); subMenuButton.onClick = [this] { showSubMenu(); }; addAndMakeVisible (customMenuButton); - customMenuButton.setTitle ("Show Custom Menu"); + customMenuButton.setButtonText ("Show Custom Menu"); customMenuButton.onClick = [this] { showCustomMenu(); }; addAndMakeVisible (nativeMenuButton); - nativeMenuButton.setTitle ("Show Native Menu"); + nativeMenuButton.setButtonText ("Show Native Menu"); nativeMenuButton.onClick = [this] { showNativeMenu(); From 3045af785ba8c2f81f22a7dd472290a734c94da5 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 23 Jun 2025 17:59:36 +0200 Subject: [PATCH 28/51] Improve code coverage --- codecov.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/codecov.yml b/codecov.yml index 9aaa2865c..36fe860c6 100644 --- a/codecov.yml +++ b/codecov.yml @@ -114,14 +114,7 @@ ignore: - "cmake/**/*" - "docs/**/*" - "standalone/**/*" - - "**/*.mm" - - "**/*_android.*" - - "**/*_windows.*" - - "**/*_ios.*" - - "**/*_mac.*" - - "**/*_apple.*" - - "**/*_emscripten.*" - - "**/*_wasm.*" + - "**/modules/*/native/*" comment: layout: "header, reach, components, diff, flags, files, footer" From 7defb7bc69685fcdaf0ec2a6a66d1c3c048a526e Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 25 Jun 2025 09:37:18 +0200 Subject: [PATCH 29/51] Shutdown --- modules/yup_events/native/yup_Messaging_emscripten.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/yup_events/native/yup_Messaging_emscripten.cpp b/modules/yup_events/native/yup_Messaging_emscripten.cpp index 5ad39c7a1..c39136b23 100644 --- a/modules/yup_events/native/yup_Messaging_emscripten.cpp +++ b/modules/yup_events/native/yup_Messaging_emscripten.cpp @@ -140,15 +140,16 @@ void MessageManager::runDispatchLoop() constexpr int framesPerSeconds = 0; constexpr int simulateInfiniteLoop = 1; emscripten_set_main_loop_arg (mainLoop, this, framesPerSeconds, simulateInfiniteLoop); - - for (const auto& func : shutdownCallbacks) - func(); } void MessageManager::stopDispatchLoop() { quitMessagePosted = true; + emscripten_cancel_main_loop(); + + for (const auto& func : shutdownCallbacks) + func(); } #if YUP_MODAL_LOOPS_PERMITTED From 886f1cf33b5832a10cb08c949b4e80063ff1e9a4 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 25 Jun 2025 09:40:38 +0200 Subject: [PATCH 30/51] More tweaks --- modules/yup_events/native/yup_Messaging_emscripten.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/yup_events/native/yup_Messaging_emscripten.cpp b/modules/yup_events/native/yup_Messaging_emscripten.cpp index c39136b23..ad7aed2ce 100644 --- a/modules/yup_events/native/yup_Messaging_emscripten.cpp +++ b/modules/yup_events/native/yup_Messaging_emscripten.cpp @@ -131,8 +131,8 @@ void MessageManager::runDispatchLoop() auto* messageManager = static_cast (arg); jassert (messageManager != nullptr); - jassert (messageManager->loopCallback != nullptr); - messageManager->loopCallback(); + if (messageManager != nullptr && messageManager->loopCallback != nullptr) + messageManager->loopCallback(); InternalMessageQueue::getInstance()->deliverNextMessages(); }; From 1b804b206fd73af284f012578032f3e36241deff Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 25 Jun 2025 16:04:07 +0200 Subject: [PATCH 31/51] More popup menu work --- examples/graphics/source/examples/Audio.h | 4 +- examples/graphics/source/examples/PopupMenu.h | 5 +- modules/yup_gui/component/yup_Component.cpp | 3 + .../yup_gui/component/yup_ComponentNative.h | 3 + modules/yup_gui/menus/yup_PopupMenu.cpp | 1017 +++++++---------- modules/yup_gui/menus/yup_PopupMenu.h | 84 +- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 8 + modules/yup_gui/native/yup_Windowing_sdl2.h | 3 + .../themes/theme_v1/yup_ThemeVersion1.cpp | 131 +++ 9 files changed, 665 insertions(+), 593 deletions(-) diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index f0e86c64a..4fd18726e 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -245,7 +245,7 @@ class AudioExample void refreshDisplay (double lastFrameTimeSeconds) override { { - // const yup::CriticalSection::ScopedLockType sl (renderMutex); + const yup::CriticalSection::ScopedLockType sl (renderMutex); oscilloscope.setRenderData (renderData, readPos); } @@ -282,7 +282,7 @@ class AudioExample readPos = readPos % inputData.size(); } - //const yup::CriticalSection::ScopedLockType sl (renderMutex); + const yup::CriticalSection::ScopedLockType sl (renderMutex); std::swap (inputData, renderData); } diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 8f85634e1..131c8934e 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -233,6 +233,7 @@ class PopupMenuDemo : public yup::Component auto options = yup::PopupMenu::Options {} .withNativeMenus (true) .withJustification (yup::Justification::topLeft) + .withMinimumWidth (500) .withParentComponent (this); auto menu = yup::PopupMenu::create (options); @@ -256,9 +257,9 @@ class PopupMenuDemo : public yup::Component void showContextMenu (yup::Point position) { auto options = yup::PopupMenu::Options {} - // .withTargetScreenPosition(position.to()) // TODO: doesn't seem to work + .withTargetPosition (position.to()) .withParentComponent (this) - .withAsChildToTopmost (true); + .withAsChildToTopmost (false); auto contextMenu = yup::PopupMenu::create (options); diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index 9551422ca..ddc57d3f5 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -591,6 +591,9 @@ void Component::removeFromDesktop() void Component::toFront (bool shouldGainKeyboardFocus) { + if (options.onDesktop && native != nullptr) + native->toFront(); + if (parentComponent == nullptr) return; diff --git a/modules/yup_gui/component/yup_ComponentNative.h b/modules/yup_gui/component/yup_ComponentNative.h index d7cca2f21..f9357f3be 100644 --- a/modules/yup_gui/component/yup_ComponentNative.h +++ b/modules/yup_gui/component/yup_ComponentNative.h @@ -211,6 +211,9 @@ class YUP_API ComponentNative : public ReferenceCountedObject */ virtual bool isVisible() const = 0; + //============================================================================== + virtual void toFront() = 0; + //============================================================================== /** Sets the size of the window. diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 7fd2205d9..279e405e7 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -27,615 +27,173 @@ namespace yup namespace { -static std::vector> activePopups; +static std::vector activePopups; -} // namespace - -//============================================================================== - -class PopupMenu::PopupMenuItem +Point calculatePositionWithJustification (const Rectangle& targetArea, + const Size& menuSize, + Justification justification) { -public: - //============================================================================== - - PopupMenuItem() - { - } - - PopupMenuItem (const String& itemText, int itemID, bool isEnabled = true, bool isTicked = false) - : text (itemText) - , itemID (itemID) - , isEnabled (isEnabled) - , isTicked (isTicked) - { - } - - PopupMenuItem (const String& itemText, PopupMenu::Ptr subMenu, bool isEnabled = true) - : text (itemText) - , isEnabled (isEnabled) - , subMenu (std::move (subMenu)) - { - } + Point position; - PopupMenuItem (std::unique_ptr component, int itemID) - : itemID (itemID) - , customComponent (std::move (component)) + switch (justification) { + default: + case Justification::topLeft: + position = targetArea.getTopLeft(); + break; + + case Justification::centerTop: + position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getTop()); + break; + + case Justification::topRight: + position = targetArea.getTopRight().translated (-menuSize.getWidth(), 0); + break; + + case Justification::bottomLeft: + position = targetArea.getBottomLeft(); + break; + + case Justification::centerBottom: + position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getBottom()); + break; + + case Justification::bottomRight: + position = targetArea.getBottomRight().translated (-menuSize.getWidth(), 0); + break; + + case Justification::centerRight: + position = Point (targetArea.getRight(), targetArea.getCenterY() - menuSize.getHeight() / 2); + break; + + case Justification::centerLeft: + position = Point (targetArea.getX() - menuSize.getWidth(), targetArea.getCenterY() - menuSize.getHeight() / 2); + break; } - //============================================================================== - - ~PopupMenuItem() = default; - - //============================================================================== - - bool isSeparator() const { return text.isEmpty() && itemID == 0 && subMenu == nullptr && customComponent == nullptr; } - - bool isSubMenu() const { return subMenu != nullptr; } - - bool isCustomComponent() const { return customComponent != nullptr; } - - //============================================================================== - - String text; - - int itemID = 0; - - bool isEnabled = true; - - bool isTicked = false; - - PopupMenu::Ptr subMenu; - - std::unique_ptr customComponent; - - String shortcutKeyText; - - std::optional textColor; - -private: - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenuItem) -}; - -//============================================================================== + return position; +} -class PopupMenu::MenuWindow : public Component +Point constrainPositionToAvailableArea (Point desiredPosition, + const Size& menuSize, + const Rectangle& availableArea, + const Rectangle& targetArea) { -public: - MenuWindow (PopupMenu::Ptr menu, const PopupMenu::Options& opts) - : Component ("PopupMenuWindow") - , owner (menu) - , options (opts) - { - setWantsKeyboardFocus (true); + // Add padding to keep menu slightly away from screen edges + const int padding = 5; + auto constrainedArea = availableArea.reduced (padding); - // Calculate required size and create menu items - setupMenuItems(); + Point position = desiredPosition; - // Add as child to topmost component or to desktop - if (options.addAsChildToTopmost && options.parentComponent) - { - // Add this menu as a child - options.parentComponent->addChildComponent (this); - } - else - { - // Add to desktop as a popup - auto nativeOptions = ComponentNative::Options {} - .withDecoration (false) - .withResizableWindow (false); + // Check if menu fits in desired position + Rectangle menuBounds (position, menuSize); - addToDesktop (nativeOptions); - } - - // Add to active popups list for modal behavior - activePopups.push_back (this); - - // Position the menu - positionMenu(); - - setVisible (true); - toFront (true); - } - - ~MenuWindow() override + // If menu doesn't fit, try alternative positions + if (! constrainedArea.contains (menuBounds)) { - activePopups.erase (std::remove (activePopups.begin(), activePopups.end(), this), activePopups.end()); - } + // Try to keep menu fully visible by adjusting position - void paint (Graphics& g) override - { - // Draw drop shadow if enabled - if (options.addAsChildToTopmost) + // Horizontal adjustment + if (menuBounds.getRight() > constrainedArea.getRight()) { - auto shadowBounds = getLocalBounds().to(); - auto shadowRadius = static_cast (8.0f); - - g.setFillColor (Color (0, 0, 0)); - g.setFeather (shadowRadius); - g.fillRoundedRect (shadowBounds.translated (0.0f, 2.0f).enlarged (2.0f), 4.0f); - g.setFeather (0.0f); - } - - // Draw menu background - g.setFillColor (findColor (Colours::menuBackground).value_or (Color (0xff2a2a2a))); - g.fillRoundedRect (getLocalBounds().to(), 4.0f); - - // Draw border - g.setStrokeColor (findColor (Colours::menuBorder).value_or (Color (0xff555555))); - g.setStrokeWidth (1.0f); - g.strokeRoundedRect (getLocalBounds().to().reduced (0.5f), 4.0f); + // Try moving left + position.setX (constrainedArea.getRight() - menuSize.getWidth()); - // Draw menu items - drawMenuItems (g); - } - - void focusLost() override - { - dismiss (0); - } - - bool isWithinBounds (Point globalPoint) const - { - if (getParentComponent() != nullptr) - { - // When added as a child, convert to local coordinates - return getLocalBounds().to().contains (globalPoint); - } - else - { - // When added to desktop, use screen coordinates - auto localPoint = globalPoint - getScreenPosition().to(); - return getLocalBounds().to().contains (localPoint); - } - } - - void mouseDown (const MouseEvent& event) override - { - // Check if click is inside the menu - if (getLocalBounds().contains (event.getPosition())) - { - auto itemIndex = getItemIndexAt (event.getPosition()); - if (itemIndex >= 0 && itemIndex < owner->items.size()) + // If that puts us over the target, try positioning on the left side + if (Rectangle (position, menuSize).intersects (targetArea)) { - auto& item = *owner->items[itemIndex]; - if (! item.isSeparator() && item.isEnabled) - { - if (item.isSubMenu()) - { - // TODO: Show sub-menu - } - else - { - dismiss (item.itemID); - } - } + position.setX (targetArea.getX() - menuSize.getWidth()); } } - else + else if (menuBounds.getX() < constrainedArea.getX()) { - // Click outside menu - dismiss - dismiss (0); - } - } - - /* - void inputAttemptWhenModal() override - { - // Handle clicks outside when modal - auto mousePos = Desktop::getInstance()->getMousePosition(); - auto localPos = getLocalPoint (nullptr, mousePos); + // Try moving right + position.setX (constrainedArea.getX()); - if (! getLocalBounds().contains (localPos)) - { - dismiss (0); - } - } - */ - - void mouseMove (const MouseEvent& event) override - { - auto newHoveredIndex = getItemIndexAt (event.getPosition()); - if (newHoveredIndex != hoveredItemIndex) - { - hoveredItemIndex = newHoveredIndex; - repaint(); - } - } - - void mouseExit (const MouseEvent& event) override - { - if (hoveredItemIndex >= 0) - { - hoveredItemIndex = -1; - repaint(); - } - } - - void keyDown (const KeyPress& key, const Point& position) override - { - if (key.getKey() == KeyPress::escapeKey) - { - dismiss (0); - } - } - - void dismiss (int itemID) - { - selectedItemID = itemID; - //setVisible (false); - - // Call the owner's callback - if (owner->onItemSelected) - owner->onItemSelected (itemID); - - delete this; - } - -private: - // Color identifiers for theming - struct Colours - { - static inline const Identifier menuBackground { "menuBackground" }; - static inline const Identifier menuBorder { "menuBorder" }; - static inline const Identifier menuItemText { "menuItemText" }; - static inline const Identifier menuItemTextDisabled { "menuItemTextDisabled" }; - static inline const Identifier menuItemBackground { "menuItemBackground" }; - static inline const Identifier menuItemBackgroundHighlighted { "menuItemBackgroundHighlighted" }; - }; - - void setupMenuItems() - { - constexpr float separatorHeight = 8.0f; - constexpr float verticalPadding = 4.0f; - - float y = verticalPadding; // Top padding - float itemHeight = static_cast (22); - float width = options.minWidth.value_or (200); - - itemRects.clear(); - - for (const auto& item : owner->items) - { - if (item->isCustomComponent()) - width = jmax (width, item->customComponent->getWidth()); - } - - for (const auto& item : owner->items) - { - if (item->isSeparator()) + // If that puts us over the target, try positioning on the right side + if (Rectangle (position, menuSize).intersects (targetArea)) { - itemRects.push_back ({ 0, y, width, separatorHeight }); - y += separatorHeight; - } - else if (item->isCustomComponent()) - { - addChildComponent (*item->customComponent); - - float horizontalOffset = 0.0f; - - auto compHeight = item->customComponent->getHeight(); - auto compWidth = item->customComponent->getWidth(); - if (compWidth < width) - horizontalOffset = (width - compWidth) / 2.0f; - - auto& rect = itemRects.emplace_back (horizontalOffset, y, compWidth, compHeight); - item->customComponent->setBounds (rect); - item->customComponent->setVisible (true); - - y += compHeight; - } - else - { - itemRects.push_back ({ 0, y, width, itemHeight }); - y += itemHeight; + position.setX (targetArea.getRight()); } } - setSize ({ width, y + verticalPadding }); // Bottom padding - } - - void positionMenu() - { - auto menuSize = getSize(); - Rectangle targetArea; - Rectangle availableArea; - - // Determine target area and available area - if (options.parentComponent) + // Vertical adjustment + if (menuBounds.getBottom() > constrainedArea.getBottom()) { - // Get the bounds relative to the screen or topmost component - if (options.addAsChildToTopmost) - { - // Target area is relative to topmost component - targetArea = options.parentComponent->getBounds().to(); - availableArea = targetArea; - } - else - { - // Target area is in screen coordinates - targetArea = options.parentComponent->getScreenBounds().to(); - - // Available area is the screen bounds - if (auto* desktop = Desktop::getInstance()) - { - if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) - availableArea = screen->workArea; - else if (auto screen = desktop->getPrimaryScreen()) - availableArea = screen->workArea; - else - availableArea = targetArea; - } - } - - // Override with explicit target area if provided - if (! options.targetArea.isEmpty()) - { - if (options.addAsChildToTopmost) - targetArea = options.targetArea; - else - targetArea = options.targetArea.translated (targetArea.getPosition()); - } - } - else - { - if (! options.targetArea.isEmpty()) - targetArea = options.targetArea; - else - targetArea = Rectangle (options.targetPosition, options.targetArea.getSize()); + // Try moving up + position.setY (constrainedArea.getBottom() - menuSize.getHeight()); - // Get screen bounds for available area - if (auto* desktop = Desktop::getInstance()) + // If that puts us over the target, try positioning above + if (Rectangle (position, menuSize).intersects (targetArea)) { - if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) - availableArea = screen->workArea; - else if (auto screen = desktop->getPrimaryScreen()) - availableArea = screen->workArea; - else - availableArea = targetArea; + position.setY (targetArea.getY() - menuSize.getHeight()); } } - - // Calculate position based on justification - Point position = calculatePositionWithJustification (targetArea, menuSize.to(), options.justification).to(); - - // Adjust position to fit within available area - position = constrainPositionToAvailableArea (position, menuSize.to(), availableArea, targetArea).to(); - - setTopLeft (position); - } - - Point calculatePositionWithJustification (const Rectangle& targetArea, - const Size& menuSize, - Justification justification) - { - Point position; - - switch (justification) - { - default: - case Justification::topLeft: - position = targetArea.getTopLeft(); - break; - - case Justification::centerTop: - position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getTop()); - break; - - case Justification::topRight: - position = targetArea.getTopRight().translated (-menuSize.getWidth(), 0); - break; - - case Justification::bottomLeft: - position = targetArea.getBottomLeft(); - break; - - case Justification::centerBottom: - position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getBottom()); - break; - - case Justification::bottomRight: - position = targetArea.getBottomRight().translated (-menuSize.getWidth(), 0); - break; - - case Justification::centerRight: - position = Point (targetArea.getRight(), targetArea.getCenterY() - menuSize.getHeight() / 2); - break; - - case Justification::centerLeft: - position = Point (targetArea.getX() - menuSize.getWidth(), targetArea.getCenterY() - menuSize.getHeight() / 2); - break; - } - - return position; - } - - Point constrainPositionToAvailableArea (Point desiredPosition, - const Size& menuSize, - const Rectangle& availableArea, - const Rectangle& targetArea) - { - // Add padding to keep menu slightly away from screen edges - const int padding = 5; - auto constrainedArea = availableArea.reduced (padding); - - Point position = desiredPosition; - - // Check if menu fits in desired position - Rectangle menuBounds (position, menuSize); - - // If menu doesn't fit, try alternative positions - if (! constrainedArea.contains (menuBounds)) + else if (menuBounds.getY() < constrainedArea.getY()) { - // Try to keep menu fully visible by adjusting position - - // Horizontal adjustment - if (menuBounds.getRight() > constrainedArea.getRight()) - { - // Try moving left - position.setX (constrainedArea.getRight() - menuSize.getWidth()); + // Try moving down + position.setY (constrainedArea.getY()); - // If that puts us over the target, try positioning on the left side - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setX (targetArea.getX() - menuSize.getWidth()); - } - } - else if (menuBounds.getX() < constrainedArea.getX()) + // If that puts us over the target, try positioning below + if (Rectangle (position, menuSize).intersects (targetArea)) { - // Try moving right - position.setX (constrainedArea.getX()); - - // If that puts us over the target, try positioning on the right side - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setX (targetArea.getRight()); - } + position.setY (targetArea.getBottom()); } - - // Vertical adjustment - if (menuBounds.getBottom() > constrainedArea.getBottom()) - { - // Try moving up - position.setY (constrainedArea.getBottom() - menuSize.getHeight()); - - // If that puts us over the target, try positioning above - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setY (targetArea.getY() - menuSize.getHeight()); - } - } - else if (menuBounds.getY() < constrainedArea.getY()) - { - // Try moving down - position.setY (constrainedArea.getY()); - - // If that puts us over the target, try positioning below - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setY (targetArea.getBottom()); - } - } - - // Final bounds check - ensure we're at least partially visible - position.setX (jlimit (constrainedArea.getX(), - jmax (constrainedArea.getX(), constrainedArea.getRight() - menuSize.getWidth()), - position.getX())); - position.setY (jlimit (constrainedArea.getY(), - jmax (constrainedArea.getY(), constrainedArea.getBottom() - menuSize.getHeight()), - position.getY())); - } - - return position; - } - - int getItemIndexAt (Point position) const - { - for (int i = 0; i < static_cast (itemRects.size()); ++i) - { - if (itemRects[i].contains (position)) - return i; } - return -1; + // Final bounds check - ensure we're at least partially visible + position.setX (jlimit (constrainedArea.getX(), + jmax (constrainedArea.getX(), constrainedArea.getRight() - menuSize.getWidth()), + position.getX())); + position.setY (jlimit (constrainedArea.getY(), + jmax (constrainedArea.getY(), constrainedArea.getBottom() - menuSize.getHeight()), + position.getY())); } - void drawMenuItems (Graphics& g) - { - auto itemFont = ApplicationTheme::getGlobalTheme()->getDefaultFont(); - - for (int i = 0; i < static_cast (owner->items.size()); ++i) - { - const auto& item = *owner->items[i]; - const auto& rect = itemRects[i]; - - // Skip custom components as they render themselves - if (item.isCustomComponent()) - continue; - - // Draw hover background - if (i == hoveredItemIndex && ! item.isSeparator() && item.isEnabled) - { - g.setFillColor (findColor (Colours::menuItemBackgroundHighlighted).value_or (Color (0xff404040))); - g.fillRoundedRect (rect.reduced (2.0f, 1.0f), 2.0f); - } + return position; +} - if (item.isSeparator()) - { - // Draw separator line - auto lineY = rect.getCenterY(); - g.setStrokeColor (findColor (Colours::menuBorder).value_or (Color (0xff555555))); - g.setStrokeWidth (1.0f); - g.strokeLine (rect.getX() + 8.0f, lineY, rect.getRight() - 8.0f, lineY); - } - else - { - // Draw menu item text - auto textColor = item.textColor.value_or (findColor (Colours::menuItemText).value_or (Color (0xffffffff))); - if (! item.isEnabled) - textColor = findColor (Colours::menuItemTextDisabled).value_or (Color (0xff808080)); +} // namespace - g.setFillColor (textColor); +//============================================================================== - auto textRect = rect.reduced (12.0f, 2.0f); +PopupMenu::Item::Item (const String& itemText, int itemID, bool isEnabled, bool isTicked) + : text (itemText) + , itemID (itemID) + , isEnabled (isEnabled) + , isTicked (isTicked) +{ +} - { - auto styledText = yup::StyledText(); - { - auto modifier = styledText.startUpdate(); - modifier.appendText (item.text, itemFont); - } - g.fillFittedText (styledText, textRect); - } +PopupMenu::Item::Item (const String& itemText, PopupMenu::Ptr subMenu, bool isEnabled) + : text (itemText) + , isEnabled (isEnabled) + , subMenu (std::move (subMenu)) +{ +} - // Draw checkmark if ticked - if (item.isTicked) - { - auto checkRect = Rectangle (rect.getX() + 4.0f, rect.getY() + 4.0f, 12.0f, 12.0f); - g.setStrokeColor (textColor); - g.setStrokeWidth (2.0f); - g.strokeLine (checkRect.getX() + 2.0f, checkRect.getCenterY(), checkRect.getCenterX(), checkRect.getBottom() - 2.0f); - g.strokeLine (checkRect.getCenterX(), checkRect.getBottom() - 2.0f, checkRect.getRight() - 2.0f, checkRect.getY() + 2.0f); - } +PopupMenu::Item::Item (std::unique_ptr component, int itemID) + : itemID (itemID) + , customComponent (std::move (component)) +{ +} - // Draw shortcut text - if (item.shortcutKeyText.isNotEmpty()) - { - auto shortcutRect = Rectangle (rect.getRight() - 80.0f, rect.getY(), 75.0f, rect.getHeight()); - g.setOpacity (0.7f); - - auto styledText = yup::StyledText(); - { - auto modifier = styledText.startUpdate(); - modifier.setHorizontalAlign (yup::StyledText::right); - modifier.appendText (item.shortcutKeyText, itemFont); - } - g.fillFittedText (styledText, shortcutRect); - - g.setOpacity (1.0f); - } +PopupMenu::Item::~Item() = default; - // Draw submenu arrow - if (item.isSubMenu()) - { - auto arrowRect = Rectangle (rect.getRight() - 16.0f, rect.getY() + 4.0f, 8.0f, rect.getHeight() - 8.0f); - g.setStrokeColor (textColor); - g.setStrokeWidth (1.5f); - g.strokeLine (arrowRect.getX() + 2.0f, arrowRect.getY() + 2.0f, arrowRect.getRight() - 2.0f, arrowRect.getCenterY()); - g.strokeLine (arrowRect.getRight() - 2.0f, arrowRect.getCenterY(), arrowRect.getX() + 2.0f, arrowRect.getBottom() - 2.0f); - } - } - } - } +bool PopupMenu::Item::isSeparator() const +{ + return text.isEmpty() && itemID == 0 && subMenu == nullptr && customComponent == nullptr; +} - PopupMenu::Ptr owner; - PopupMenu::Options options; - int selectedItemID = 0; - int hoveredItemIndex = -1; - std::vector> itemRects; +bool PopupMenu::Item::isSubMenu() const +{ + return subMenu != nullptr; +} - YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MenuWindow) -}; +bool PopupMenu::Item::isCustomComponent() const +{ + return customComponent != nullptr; +} //============================================================================== @@ -651,9 +209,9 @@ struct GlobalMouseListener : public MouseListener bool clickedInsidePopup = false; for (const auto& popup : activePopups) { - if (auto* menuWindow = dynamic_cast (popup.get())) + if (auto* popupMenu = dynamic_cast (popup.get())) { - if (menuWindow->isWithinBounds (globalPos)) + if (popupMenu->getScreenBounds().contains (globalPos)) { clickedInsidePopup = true; break; @@ -750,7 +308,11 @@ PopupMenu::PopupMenu (const Options& options) { } -PopupMenu::~PopupMenu() = default; +PopupMenu::~PopupMenu() +{ + if (isVisible()) + dimiss(); +} //============================================================================== @@ -768,8 +330,8 @@ void PopupMenu::dismissAllPopups() for (const auto& popup : popupsToClose) { - if (auto* menuWindow = dynamic_cast (popup.get())) - menuWindow->dismiss (0); + if (auto* popupMenu = dynamic_cast (popup.get())) + popupMenu->dismiss(); } activePopups.clear(); @@ -779,25 +341,25 @@ void PopupMenu::dismissAllPopups() void PopupMenu::addItem (const String& text, int itemID, bool isEnabled, bool isTicked, const String& shortcutText) { - auto item = std::make_unique (text, itemID, isEnabled, isTicked); + auto item = std::make_unique (text, itemID, isEnabled, isTicked); item->shortcutKeyText = shortcutText; items.push_back (std::move (item)); } void PopupMenu::addSeparator() { - items.push_back (std::make_unique()); + items.push_back (std::make_unique()); } void PopupMenu::addSubMenu (const String& text, PopupMenu::Ptr subMenu, bool isEnabled) { - auto item = std::make_unique (text, std::move (subMenu), isEnabled); + auto item = std::make_unique (text, std::move (subMenu), isEnabled); items.push_back (std::move (item)); } void PopupMenu::addCustomItem (std::unique_ptr component, int itemID) { - auto item = std::make_unique (std::move (component), itemID); + auto item = std::make_unique (std::move (component), itemID); items.push_back (std::move (item)); } @@ -820,7 +382,7 @@ void PopupMenu::addItemsFromMenu (const PopupMenu& otherMenu) } else { - auto item = std::make_unique (otherItem->text, otherItem->itemID, otherItem->isEnabled, otherItem->isTicked); + auto item = std::make_unique (otherItem->text, otherItem->itemID, otherItem->isEnabled, otherItem->isTicked); item->shortcutKeyText = otherItem->shortcutKeyText; item->textColor = otherItem->textColor; items.push_back (std::move (item)); @@ -842,32 +404,319 @@ void PopupMenu::clear() //============================================================================== -void PopupMenu::show (std::function callback) +int PopupMenu::getHoveredItem() const { - if (isEmpty()) + int currentIndex = 0; + + for (auto& item : items) { - if (callback) - callback (0); + if (item->isHovered) + return currentIndex; - return; + ++currentIndex; } - // Dismiss any existing popups first - dismissAllPopups(); + return -1; +} - // Show the custom menu with callback - showCustom (options, callback); +void PopupMenu::setHoveredItem (int itemIndex) +{ + bool hasChanged = false; + + int currentIndex = 0; + for (auto& item : items) + { + bool newHoveredState = (currentIndex == itemIndex); + + if (newHoveredState != item->isHovered) + hasChanged = true; + + item->isHovered = newHoveredState; + + ++currentIndex; + } + + if (hasChanged) + repaint(); +} + +//============================================================================== + +void PopupMenu::setSelectedItemID (int itemID) +{ + if (selectedItemID != itemID) + { + selectedItemID = itemID; + + if (auto itemCallback = std::exchange (menuCallback, {})) + itemCallback (itemID); + + if (onItemSelected != nullptr) + onItemSelected (itemID); + } +} + +//============================================================================== + +void PopupMenu::setupMenuItems() +{ + constexpr float separatorHeight = 8.0f; + constexpr float verticalPadding = 4.0f; + + float y = verticalPadding; // Top padding + float itemHeight = static_cast (22); + float width = options.minWidth.value_or (200); + + for (const auto& item : items) + { + if (item->isCustomComponent()) + width = jmax (width, item->customComponent->getWidth()); + } + + for (auto& item : items) + { + if (item->isCustomComponent()) + { + addChildComponent (*item->customComponent); + + float horizontalOffset = 0.0f; + + auto compWidth = item->customComponent->getWidth(); + auto compHeight = item->customComponent->getHeight(); + jassert (compWidth != 0 && compHeight != 0); + if (compWidth < width) + horizontalOffset = (width - compWidth) / 2.0f; + + item->area = { horizontalOffset, y, compWidth, compHeight }; + item->customComponent->setBounds (item->area); + item->customComponent->setVisible (true); + + y += compHeight; + } + else + { + const auto height = item->isSeparator() ? separatorHeight : itemHeight; + item->area = { 0, y, width, height }; + y += height; + } + } + + setSize ({ width, y + verticalPadding }); // Bottom padding +} + +void PopupMenu::positionMenu() +{ + auto menuSize = getSize(); + Rectangle targetArea; + Rectangle availableArea; + + // Determine target area and available area + if (options.parentComponent) + { + // Get the bounds relative to the screen or topmost component + if (options.addAsChildToTopmost) + { + // Target area is relative to topmost component + targetArea = options.parentComponent->getBounds().to(); + availableArea = targetArea; + } + else + { + // Target area is in screen coordinates + targetArea = options.parentComponent->getScreenBounds().to(); + + // Available area is the screen bounds + if (auto* desktop = Desktop::getInstance()) + { + if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) + availableArea = screen->workArea; + else if (auto screen = desktop->getPrimaryScreen()) + availableArea = screen->workArea; + else + availableArea = targetArea; + } + } + + // Override with explicit target area if provided + if (! options.targetArea.isEmpty()) + { + if (options.addAsChildToTopmost) + targetArea = options.targetArea; + else + targetArea = options.targetArea.translated (targetArea.getPosition()); + } + } + else + { + if (! options.targetArea.isEmpty()) + targetArea = options.targetArea; + else + targetArea = Rectangle (options.targetPosition, options.targetArea.getSize()); + + // Get screen bounds for available area + if (auto* desktop = Desktop::getInstance()) + { + if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) + availableArea = screen->workArea; + else if (auto screen = desktop->getPrimaryScreen()) + availableArea = screen->workArea; + else + availableArea = targetArea; + } + } + + // Calculate position based on justification + Point position = calculatePositionWithJustification (targetArea, menuSize.to(), options.justification).to(); + + // Adjust position to fit within available area + position = constrainPositionToAvailableArea (position, menuSize.to(), availableArea, targetArea).to(); + + setTopLeft (position); +} + +//============================================================================== + +int PopupMenu::getItemIndexAt (Point position) const +{ + int itemIndex = 0; + + for (const auto& item : items) + { + if (item->area.contains (position)) + return itemIndex; + + ++itemIndex; + } + + return -1; +} + +//============================================================================== + +void PopupMenu::show (std::function callback) +{ + showCustom (options, std::move (callback)); } //============================================================================== void PopupMenu::showCustom (const Options& options, std::function callback) { - jassert (! isEmpty()); + dismissAllPopups(); + + menuCallback = std::move (callback); + + if (isEmpty()) + { + dismiss(); + return; + } installGlobalMouseListener(); - new MenuWindow (this, options); + setWantsKeyboardFocus (true); + + if (options.addAsChildToTopmost && options.parentComponent) + { + options.parentComponent->addChildComponent (this); + } + else + { + auto nativeOptions = ComponentNative::Options {} + .withDecoration (false) + .withResizableWindow (false); + + addToDesktop (nativeOptions); + } + + activePopups.push_back (this); + + setupMenuItems(); + positionMenu(); + + setVisible (true); + toFront (true); +} + +//============================================================================== + +void PopupMenu::dismiss() +{ + dismiss (0); +} + +void PopupMenu::dismiss (int itemID) +{ + setVisible (false); + + setSelectedItemID (itemID); + + for (auto it = activePopups.begin(); it != activePopups.end();) + { + if (it->get() == this) + it = activePopups.erase (it); + else + ++it; + } +} + +//============================================================================== + +void PopupMenu::paint (Graphics& g) +{ + if (auto style = ApplicationTheme::findComponentStyle (*this)) + style->paint (g, *ApplicationTheme::getGlobalTheme(), *this); +} + +//============================================================================== + +void PopupMenu::mouseDown (const MouseEvent& event) +{ + if (! getLocalBounds().contains (event.getPosition())) + { + dismiss(); + return; + } + + auto itemIndex = getItemIndexAt (event.getPosition()); + if (! isPositiveAndBelow (itemIndex, getNumItems())) + return; + + auto& item = *items[itemIndex]; + if (item.isSeparator() || ! item.isEnabled) + return; + + if (item.isSubMenu()) + { + // TODO: Show sub-menu + } + else + { + dismiss (item.itemID); + } +} + +void PopupMenu::mouseMove (const MouseEvent& event) +{ + setHoveredItem (getItemIndexAt (event.getPosition())); +} + +void PopupMenu::mouseExit (const MouseEvent& event) +{ + setHoveredItem (-1); +} + +void PopupMenu::keyDown (const KeyPress& key, const Point& position) +{ + if (key.getKey() == KeyPress::escapeKey) + dismiss(); +} + +//============================================================================== + +void PopupMenu::focusLost() +{ + dismiss(); } } // namespace yup diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 4bb97fc92..045c804ba 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -28,7 +28,7 @@ namespace yup This class supports both native system menus and custom rendered menus. */ -class YUP_API PopupMenu : public ReferenceCountedObject +class YUP_API PopupMenu : public Component, public ReferenceCountedObject { public: //============================================================================== @@ -131,6 +131,41 @@ class YUP_API PopupMenu : public ReferenceCountedObject /** Clears all items from the menu. */ void clear(); + //============================================================================== + class Item + { + public: + Item() = default; + Item (const String& itemText, int itemID, bool isEnabled = true, bool isTicked = false); + Item (const String& itemText, PopupMenu::Ptr subMenu, bool isEnabled = true); + Item (std::unique_ptr component, int itemID); + ~Item(); + + bool isSeparator() const; + bool isSubMenu() const; + bool isCustomComponent() const; + + String text; + int itemID = 0; + bool isEnabled = true; + bool isTicked = false; + bool isHovered = false; + PopupMenu::Ptr subMenu; + std::unique_ptr customComponent; + String shortcutKeyText; + std::optional textColor; + Rectangle area; + + private: + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Item) + }; + + /** Returns an iterator to the first item in the menu. */ + auto begin() const { return items.begin(); } + + /** Returns an iterator to the end of the menu. */ + auto end() const { return items.end(); } + //============================================================================== /** Shows the menu asynchronously and calls the callback when an item is selected. @@ -139,29 +174,68 @@ class YUP_API PopupMenu : public ReferenceCountedObject */ void show (std::function callback = nullptr); + //============================================================================== + /** Dismiss popup if visible. */ + void dismiss(); + //============================================================================== /** Callback type for menu item selection. */ std::function onItemSelected; + //============================================================================== + // Color identifiers for theming + struct Colors + { + static inline const Identifier menuBackground { "menuBackground" }; + static inline const Identifier menuBorder { "menuBorder" }; + static inline const Identifier menuItemText { "menuItemText" }; + static inline const Identifier menuItemTextDisabled { "menuItemTextDisabled" }; + static inline const Identifier menuItemBackground { "menuItemBackground" }; + static inline const Identifier menuItemBackgroundHighlighted { "menuItemBackgroundHighlighted" }; + }; + //============================================================================== /** Dismisses all currently open popup menus. */ static void dismissAllPopups(); //============================================================================== - class MenuWindow; + /** @internal */ + void paint (Graphics& g) override; + /** @internal */ + void mouseDown (const MouseEvent& event) override; + /** @internal */ + void mouseMove (const MouseEvent& event) override; + /** @internal */ + void mouseExit (const MouseEvent& event) override; + /** @internal */ + void keyDown (const KeyPress& key, const Point& position) override; + /** @internal */ + void focusLost() override; private: - //============================================================================== - friend class MenuWindow; PopupMenu (const Options& options = {}); void showCustom (const Options& options, std::function callback); + int getHoveredItem() const; + void setHoveredItem (int itemIndex); + + int getItemIndexAt (Point position) const; + + void dismiss (int itemID); + void setSelectedItemID (int itemID); + + void setupMenuItems(); + void positionMenu(); + // PopupMenuItem is now an implementation detail class PopupMenuItem; - std::vector> items; + std::vector> items; Options options; + int selectedItemID = -1; + + std::function menuCallback; YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) }; diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.cpp b/modules/yup_gui/native/yup_Windowing_sdl2.cpp index 8d6365242..ad11d1e4c 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.cpp +++ b/modules/yup_gui/native/yup_Windowing_sdl2.cpp @@ -191,6 +191,14 @@ bool SDL2ComponentNative::isVisible() const //============================================================================== +void SDL2ComponentNative::toFront() +{ + if (window != nullptr && isVisible()) + SDL_RaiseWindow (window); +} + +//============================================================================== + Size SDL2ComponentNative::getContentSize() const { const auto dpiScale = getScaleDpi(); diff --git a/modules/yup_gui/native/yup_Windowing_sdl2.h b/modules/yup_gui/native/yup_Windowing_sdl2.h index c817794c9..6ee42863b 100644 --- a/modules/yup_gui/native/yup_Windowing_sdl2.h +++ b/modules/yup_gui/native/yup_Windowing_sdl2.h @@ -54,6 +54,9 @@ class SDL2ComponentNative final void setVisible (bool shouldBeVisible) override; bool isVisible() const override; + //============================================================================== + void toFront() override; + //============================================================================== void setSize (const Size& newSize) override; Size getSize() const override; diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index 37d6cb93d..4b87077fe 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -233,6 +233,135 @@ void paintLabel (Graphics& g, const ApplicationTheme& theme, const Label& l) //============================================================================== +void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu& p) +{ + // Draw drop shadow if enabled + if (false) // owner->options.addAsChildToTopmost) + { + auto shadowBounds = p.getLocalBounds().to(); + auto shadowRadius = static_cast (8.0f); + + g.setFillColor (Color (0, 0, 0)); + g.setFeather (shadowRadius); + g.fillRoundedRect (shadowBounds.translated (0.0f, 2.0f).enlarged (2.0f), 4.0f); + g.setFeather (0.0f); + } + + // Draw menu background + g.setFillColor (p.findColor (PopupMenu::Colors::menuBackground).value_or (Color (0xff2a2a2a))); + g.fillRoundedRect (p.getLocalBounds().to(), 4.0f); + + // Draw border + g.setStrokeColor (p.findColor (PopupMenu::Colors::menuBorder).value_or (Color (0xff555555))); + g.setStrokeWidth (1.0f); + g.strokeRoundedRect (p.getLocalBounds().to().reduced (0.5f), 4.0f); + + // Draw items + bool anyItemIsTicked = false; + for (const auto& item : p) + { + if (item->isTicked) + { + anyItemIsTicked = true; + break; + } + } + + int itemIndex = -1; + auto itemFont = theme.getDefaultFont(); + + for (const auto& item : p) + { + ++itemIndex; + const auto rect = item->area; + + // Skip custom components as they render themselves + if (item->isCustomComponent()) + continue; + + g.setOpacity (1.0f); + + // Draw hover background + if (item->isHovered && ! item->isSeparator() && item->isEnabled) + { + g.setFillColor (p.findColor (PopupMenu::Colors::menuItemBackgroundHighlighted).value_or (Color (0xff404040))); + g.fillRoundedRect (rect.reduced (2.0f, 1.0f), 2.0f); + } + + if (item->isSeparator()) + { + // Draw separator line + auto lineY = rect.getCenterY(); + g.setStrokeColor (p.findColor (PopupMenu::Colors::menuBorder).value_or (Color (0xff555555))); + g.setStrokeWidth (1.0f); + g.strokeLine (rect.getX() + 8.0f, lineY, rect.getRight() - 8.0f, lineY); + } + else + { + // Draw menu item text + auto textColor = item->textColor.value_or (p.findColor (PopupMenu::Colors::menuItemText).value_or (Color (0xffffffff))); + if (! item->isEnabled) + textColor = p.findColor (PopupMenu::Colors::menuItemTextDisabled).value_or (Color (0xff808080)); + + g.setFillColor (textColor); + + auto textRect = rect.reduced (12.0f, 2.0f); + if (anyItemIsTicked) + textRect.setX (textRect.getX() + 8.0f); + + { + auto styledText = yup::StyledText(); + { + auto modifier = styledText.startUpdate(); + modifier.appendText (item->text, itemFont, 14.0f); + } + + g.fillFittedText (styledText, textRect); + } + + // Draw checkmark if ticked + if (item->isTicked) + { + auto checkRect = Rectangle (rect.getX() + 4.0f, rect.getY() + 4.0f, 12.0f, 12.0f); + g.setStrokeColor (textColor); + g.setStrokeWidth (2.0f); + g.strokeLine (checkRect.getX() + 2.0f, checkRect.getCenterY(), checkRect.getCenterX(), checkRect.getBottom() - 2.0f); + g.strokeLine (checkRect.getCenterX(), checkRect.getBottom() - 2.0f, checkRect.getRight() - 2.0f, checkRect.getY() + 2.0f); + } + + // Draw shortcut text + if (item->shortcutKeyText.isNotEmpty()) + { + auto shortcutRect = Rectangle (rect.getRight() - 80.0f, rect.getY(), 75.0f, rect.getHeight()); + + auto styledText = yup::StyledText(); + { + auto modifier = styledText.startUpdate(); + modifier.setHorizontalAlign (yup::StyledText::right); + modifier.appendText (item->shortcutKeyText, itemFont, 13.0f); + } + + g.setOpacity (0.7f); + g.setFillColor (textColor); + g.fillFittedText (styledText, shortcutRect); + g.setOpacity (1.0f); + } + + // Draw submenu arrow + if (item->isSubMenu()) + { + auto arrowRect = Rectangle (rect.getRight() - 16.0f, rect.getY() + 4.0f, 8.0f, rect.getHeight() - 8.0f); + g.setStrokeColor (textColor); + g.setStrokeWidth (1.5f); + g.strokeLine (arrowRect.getX() + 2.0f, arrowRect.getY() + 2.0f, arrowRect.getRight() - 2.0f, arrowRect.getCenterY()); + g.strokeLine (arrowRect.getRight() - 2.0f, arrowRect.getCenterY(), arrowRect.getX() + 2.0f, arrowRect.getBottom() - 2.0f); + } + } + } +} + +//============================================================================== + ApplicationTheme::Ptr createThemeVersion1() { ApplicationTheme::Ptr theme (new ApplicationTheme); @@ -253,6 +382,8 @@ ApplicationTheme::Ptr createThemeVersion1() theme->setColor (Label::Colors::fillColorId, Colors::white); theme->setColor (Label::Colors::strokeColorId, Colors::transparentBlack); + theme->setComponentStyle (ComponentStyle::createStyle (paintPopupMenu)); + return theme; } From c7dc3d8fe8f6db9d30c9c6f7489b9935c0ce97cd Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Wed, 25 Jun 2025 14:04:40 +0000 Subject: [PATCH 32/51] Code formatting --- modules/yup_gui/menus/yup_PopupMenu.cpp | 4 ++-- modules/yup_gui/menus/yup_PopupMenu.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 279e405e7..8bc8fae2f 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -623,8 +623,8 @@ void PopupMenu::showCustom (const Options& options, std::function ca else { auto nativeOptions = ComponentNative::Options {} - .withDecoration (false) - .withResizableWindow (false); + .withDecoration (false) + .withResizableWindow (false); addToDesktop (nativeOptions); } diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 045c804ba..e6c5a5e53 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -28,7 +28,8 @@ namespace yup This class supports both native system menus and custom rendered menus. */ -class YUP_API PopupMenu : public Component, public ReferenceCountedObject +class YUP_API PopupMenu : public Component + , public ReferenceCountedObject { public: //============================================================================== @@ -213,7 +214,6 @@ class YUP_API PopupMenu : public Component, public ReferenceCountedObject void focusLost() override; private: - PopupMenu (const Options& options = {}); void showCustom (const Options& options, std::function callback); From e5c8733a307e9c2582ee3c58d4572e971beaadc2 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 25 Jun 2025 17:06:52 +0200 Subject: [PATCH 33/51] Improvements --- examples/graphics/source/examples/PopupMenu.h | 32 ++++++++++++----- modules/yup_graphics/fonts/yup_Font.cpp | 27 ++++++++++++++ modules/yup_graphics/fonts/yup_Font.h | 26 ++++++++++++++ modules/yup_gui/menus/yup_PopupMenu.cpp | 36 +++++-------------- modules/yup_gui/menus/yup_PopupMenu.h | 4 +-- .../themes/theme_v1/yup_ThemeVersion1.cpp | 2 +- 6 files changed, 86 insertions(+), 41 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 131c8934e..fe68c7a5e 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -104,12 +104,6 @@ class PopupMenuDemo : public yup::Component } private: - yup::TextButton basicMenuButton; - yup::TextButton subMenuButton; - yup::TextButton customMenuButton; - yup::TextButton nativeMenuButton; - yup::Label statusLabel; - enum MenuItemIDs { newFile = 1, @@ -120,6 +114,9 @@ class PopupMenuDemo : public yup::Component recentFile2, exitApp, + checkedItem = 998, + disabledItem = 999, + editUndo = 10, editRedo, editCut, @@ -147,8 +144,8 @@ class PopupMenuDemo : public yup::Component menu->addItem ("Save File", saveFile, true, false, "Cmd+S"); menu->addItem ("Save As...", saveAsFile, true, false, "Shift+Cmd+S"); menu->addSeparator(); - menu->addItem ("Disabled Item", 999, false); - menu->addItem ("Checked Item", 998, true, true); + menu->addItem ("Disabled Item", disabledItem, false); + menu->addItem ("Checked Item", checkedItem, true, isChecked); menu->addSeparator(); menu->addItem ("Exit", exitApp, true, false, "Cmd+Q"); @@ -335,10 +332,27 @@ class PopupMenuDemo : public yup::Component message = "Custom button clicked"; break; + case disabledItem: + message = "I'm disabled!"; + break; + + case checkedItem: + message = "I'm checked!"; + isChecked = !isChecked; + break; + default: + message = "Cancelled or unknown!"; break; } - statusLabel.setTitle (message); + statusLabel.setText (message); } + + yup::TextButton basicMenuButton; + yup::TextButton subMenuButton; + yup::TextButton customMenuButton; + yup::TextButton nativeMenuButton; + yup::Label statusLabel; + bool isChecked = true; }; diff --git a/modules/yup_graphics/fonts/yup_Font.cpp b/modules/yup_graphics/fonts/yup_Font.cpp index bd0b8d515..ba99a7b78 100644 --- a/modules/yup_graphics/fonts/yup_Font.cpp +++ b/modules/yup_graphics/fonts/yup_Font.cpp @@ -345,6 +345,33 @@ void Font::resetAllAxisValues() //============================================================================== +Font Font::withFeature (Feature feature) const +{ + if (font == nullptr) + return {}; + + std::vector realFeatures; + realFeatures.push_back (rive::Font::Feature{ feature.tag, feature.value }); + + return Font (font->withOptions ({}, realFeatures)); +} + +Font Font::withFeatures (std::initializer_list features) const +{ + if (font == nullptr) + return {}; + + std::vector realFeatures; + realFeatures.reserve (features.size()); + + for (const auto& feature : features) + realFeatures.push_back (rive::Font::Feature{ feature.tag, feature.value }); + + return Font (font->withOptions ({}, realFeatures)); +} + +//============================================================================== + bool Font::operator== (const Font& other) const { return font == other.font; diff --git a/modules/yup_graphics/fonts/yup_Font.h b/modules/yup_graphics/fonts/yup_Font.h index 3bc786af9..b726db401 100644 --- a/modules/yup_graphics/fonts/yup_Font.h +++ b/modules/yup_graphics/fonts/yup_Font.h @@ -207,6 +207,32 @@ class YUP_API Font */ Font withAxisValues (std::initializer_list axisOptions) const; + //============================================================================== + struct Feature + { + Feature (uint32_t tag, uint32_t value) + : tag (tag) + , value (value) + { + } + + Feature (StringRef stringTag, uint32_t value) + : tag (0) + , value (value) + { + jassert (stringTag.length() == 4); + if (stringTag.length() == 4) + tag = (uint32_t (stringTag.text[0]) << 24) | (uint32_t (stringTag.text[1]) << 16) | (uint32_t (stringTag.text[2]) << 8) | uint32_t (stringTag.text[3]); + } + + uint32_t tag; + uint32_t value; + }; + + Font withFeature (Feature feature) const; + + Font withFeatures (std::initializer_list features) const; + //============================================================================== /** Returns true if the fonts are equal. */ bool operator== (const Font& other) const; diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 8bc8fae2f..af689f265 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -311,7 +311,7 @@ PopupMenu::PopupMenu (const Options& options) PopupMenu::~PopupMenu() { if (isVisible()) - dimiss(); + dismiss(); } //============================================================================== @@ -363,33 +363,6 @@ void PopupMenu::addCustomItem (std::unique_ptr component, int itemID) items.push_back (std::move (item)); } -void PopupMenu::addItemsFromMenu (const PopupMenu& otherMenu) -{ - for (const auto& otherItem : otherMenu.items) - { - if (otherItem->isSeparator()) - { - addSeparator(); - } - else if (otherItem->isSubMenu()) - { - addSubMenu (otherItem->text, otherItem->subMenu, otherItem->isEnabled); - } - else if (otherItem->isCustomComponent()) - { - // Note: Custom components can't be easily copied, so we skip them - // In a real implementation, you might want to clone them or handle differently - } - else - { - auto item = std::make_unique (otherItem->text, otherItem->itemID, otherItem->isEnabled, otherItem->isTicked); - item->shortcutKeyText = otherItem->shortcutKeyText; - item->textColor = otherItem->textColor; - items.push_back (std::move (item)); - } - } -} - //============================================================================== int PopupMenu::getNumItems() const @@ -453,6 +426,8 @@ void PopupMenu::setSelectedItemID (int itemID) if (onItemSelected != nullptr) onItemSelected (itemID); + + isBeingDismissed = false; } } @@ -647,6 +622,11 @@ void PopupMenu::dismiss() void PopupMenu::dismiss (int itemID) { + if (isBeingDismissed) + return; + + isBeingDismissed = true; + setVisible (false); setSelectedItemID (itemID); diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index e6c5a5e53..6d7bd0594 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -119,9 +119,6 @@ class YUP_API PopupMenu : public Component */ void addCustomItem (std::unique_ptr component, int itemID); - /** Adds all items from another menu. */ - void addItemsFromMenu (const PopupMenu& otherMenu); - //============================================================================== /** Returns the number of items in the menu. */ int getNumItems() const; @@ -234,6 +231,7 @@ class YUP_API PopupMenu : public Component Options options; int selectedItemID = -1; + bool isBeingDismissed = false; std::function menuCallback; diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index 4b87077fe..a5be62c58 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -332,7 +332,7 @@ void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu // Draw shortcut text if (item->shortcutKeyText.isNotEmpty()) { - auto shortcutRect = Rectangle (rect.getRight() - 80.0f, rect.getY(), 75.0f, rect.getHeight()); + auto shortcutRect = Rectangle (rect.getRight() - 80.0f, rect.getY() + 2.0f, 75.0f, rect.getHeight() - 2.0f); auto styledText = yup::StyledText(); { From 03e135fab3b8e7387fffbcc2953e8149bc1cb8b9 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Wed, 25 Jun 2025 15:07:38 +0000 Subject: [PATCH 34/51] Code formatting --- examples/graphics/source/examples/PopupMenu.h | 2 +- modules/yup_graphics/fonts/yup_Font.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index fe68c7a5e..52c2a2d7d 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -338,7 +338,7 @@ class PopupMenuDemo : public yup::Component case checkedItem: message = "I'm checked!"; - isChecked = !isChecked; + isChecked = ! isChecked; break; default: diff --git a/modules/yup_graphics/fonts/yup_Font.cpp b/modules/yup_graphics/fonts/yup_Font.cpp index ba99a7b78..cea80740a 100644 --- a/modules/yup_graphics/fonts/yup_Font.cpp +++ b/modules/yup_graphics/fonts/yup_Font.cpp @@ -351,7 +351,7 @@ Font Font::withFeature (Feature feature) const return {}; std::vector realFeatures; - realFeatures.push_back (rive::Font::Feature{ feature.tag, feature.value }); + realFeatures.push_back (rive::Font::Feature { feature.tag, feature.value }); return Font (font->withOptions ({}, realFeatures)); } @@ -365,7 +365,7 @@ Font Font::withFeatures (std::initializer_list features) const realFeatures.reserve (features.size()); for (const auto& feature : features) - realFeatures.push_back (rive::Font::Feature{ feature.tag, feature.value }); + realFeatures.push_back (rive::Font::Feature { feature.tag, feature.value }); return Font (font->withOptions ({}, realFeatures)); } From ab1e47a27983b634ead23c8578645cdefe8b223a Mon Sep 17 00:00:00 2001 From: kunitoki Date: Wed, 25 Jun 2025 23:42:19 +0200 Subject: [PATCH 35/51] More tweaks --- examples/graphics/source/examples/PopupMenu.h | 42 ++-- .../primitives/yup_AffineTransform.h | 22 +- modules/yup_graphics/primitives/yup_Point.h | 41 ++++ modules/yup_gui/component/yup_Component.cpp | 128 +++++++++- modules/yup_gui/component/yup_Component.h | 220 +++++++++++++++++- modules/yup_gui/menus/yup_PopupMenu.cpp | 208 ++++++++++------- modules/yup_gui/menus/yup_PopupMenu.h | 69 ++++-- 7 files changed, 581 insertions(+), 149 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 52c2a2d7d..92bd24158 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -134,7 +134,8 @@ class PopupMenuDemo : public yup::Component void showBasicMenu() { auto options = yup::PopupMenu::Options {} - .withParentComponent (&basicMenuButton); + .withParentComponent (this) + .withRelativePosition (&basicMenuButton, yup::PopupMenu::Placement::below); auto menu = yup::PopupMenu::create (options); @@ -169,7 +170,8 @@ class PopupMenuDemo : public yup::Component colorMenu->addItem ("Blue", colorBlue); auto options = yup::PopupMenu::Options {} - .withParentComponent (&subMenuButton); + .withParentComponent (this) + .withRelativePosition (&subMenuButton, yup::PopupMenu::Placement::toRight); auto menu = yup::PopupMenu::create (options); menu->addItem ("New", newFile); menu->addItem ("Open", openFile); @@ -179,18 +181,17 @@ class PopupMenuDemo : public yup::Component menu->addSeparator(); menu->addItem ("Exit", exitApp); - menu->onItemSelected = [this] (int selectedID) + menu->show([this] (int selectedID) { handleMenuSelection (selectedID); - }; - - menu->show(); + }); } void showCustomMenu() { auto options = yup::PopupMenu::Options {} - .withParentComponent (&customMenuButton); + .withParentComponent (this) + .withRelativePosition (&customMenuButton, yup::PopupMenu::Placement::above); auto menu = yup::PopupMenu::create (options); menu->addItem ("Regular Item", 1); @@ -228,16 +229,13 @@ class PopupMenuDemo : public yup::Component void showNativeMenu() { auto options = yup::PopupMenu::Options {} - .withNativeMenus (true) - .withJustification (yup::Justification::topLeft) - .withMinimumWidth (500) - .withParentComponent (this); + .withParentComponent (this) + .withRelativePosition (&nativeMenuButton, yup::PopupMenu::Placement::centered); auto menu = yup::PopupMenu::create (options); menu->addItem ("Native Item 1", 1); menu->addItem ("Native Item 2", 2); - menu->addSeparator(); menu->addItem ("Native Item 3", 3); menu->onItemSelected = [this] (int selectedID) @@ -245,32 +243,22 @@ class PopupMenuDemo : public yup::Component handleMenuSelection (selectedID); }; - menu->show ([this] (int selectedID) - { - handleMenuSelection (selectedID); - }); + menu->show(); } void showContextMenu (yup::Point position) { auto options = yup::PopupMenu::Options {} - .withTargetPosition (position.to()) .withParentComponent (this) - .withAsChildToTopmost (false); + .withPosition (position, yup::Justification::topLeft); auto contextMenu = yup::PopupMenu::create (options); - contextMenu->addItem ("Copy", editCopy); - contextMenu->addItem ("Paste", editPaste); + contextMenu->addItem ("Context Item 1", 1); + contextMenu->addItem ("Context Item 2", 2); contextMenu->addSeparator(); - contextMenu->addItem ("Select All", 100); - contextMenu->addSeparator(); - contextMenu->addItem ("Properties", 101); + contextMenu->addItem ("Context Item 3", 3); - contextMenu->onItemSelected = [this] (int selectedID) - { - handleMenuSelection (selectedID); - }; contextMenu->show ([this] (int selectedID) { diff --git a/modules/yup_graphics/primitives/yup_AffineTransform.h b/modules/yup_graphics/primitives/yup_AffineTransform.h index 6848eff7b..6445a71e1 100644 --- a/modules/yup_graphics/primitives/yup_AffineTransform.h +++ b/modules/yup_graphics/primitives/yup_AffineTransform.h @@ -22,6 +22,8 @@ namespace yup { +template class YUP_API Point; + //============================================================================== /** Class representing a 2D affine transformation. @@ -238,6 +240,8 @@ class YUP_API AffineTransform return { scaleX, shearX, translateX + tx, shearY, scaleY, translateY + ty }; } + [[nodiscard]] constexpr AffineTransform translated (Point p) const noexcept; + /** Create a translation transformation Creates an AffineTransform object representing a translation by specified amounts in the x and y directions. @@ -252,6 +256,8 @@ class YUP_API AffineTransform return { 1.0f, 0.0f, tx, 0.0f, 1.0f, ty }; } + [[nodiscard]] static constexpr AffineTransform translation (Point p) noexcept; + /** Create a translated transformation Creates a new AffineTransform object that represents this transformation translated absolutely in the x and y directions. @@ -266,6 +272,8 @@ class YUP_API AffineTransform return { scaleX, shearX, tx, shearY, scaleY, ty }; } + [[nodiscard]] constexpr AffineTransform withAbsoluteTranslation (Point p) const noexcept; + //============================================================================== /** Create a rotated transformation @@ -308,6 +316,8 @@ class YUP_API AffineTransform return followedBy (rotation (angleInRadians, centerX, centerY)); } + [[nodiscard]] constexpr AffineTransform rotated (float angleInRadians, Point center) const noexcept; + /** Create a rotation transformation Creates an AffineTransform object representing a rotation by a specified angle around the origin. @@ -355,6 +365,8 @@ class YUP_API AffineTransform }; } + [[nodiscard]] static constexpr AffineTransform rotation (float angleInRadians, Point center) noexcept; + //============================================================================== /** Create a scaled transformation @@ -420,6 +432,8 @@ class YUP_API AffineTransform }; } + [[nodiscard]] constexpr AffineTransform scaled (float factorX, float factorY, Point center) const noexcept; + /** Create a scaling transformation Creates an AffineTransform object representing a uniform scaling by a specified factor. @@ -463,6 +477,8 @@ class YUP_API AffineTransform return { factorX, 0.0f, centerX * (1.0f - factorX), 0.0f, factorY, centerY * (1.0f - factorY) }; } + [[nodiscard]] static constexpr AffineTransform scaling (float factorX, float factorY, Point center) noexcept; + //============================================================================== /** Create a sheared transformation @@ -522,6 +538,8 @@ class YUP_API AffineTransform }; } + [[nodiscard]] static constexpr AffineTransform shearing (float factorX, float factorY, Point center) noexcept; + //============================================================================== /** Create a transformation that follows another. @@ -578,7 +596,7 @@ class YUP_API AffineTransform @param y A reference to the y-coordinate of the point. */ template - constexpr void transformPoint (T& x, T& y) const noexcept + constexpr auto transformPoint (T& x, T& y) const noexcept -> std::enable_if_t> { const T originalX = x; x = static_cast (scaleX * originalX + shearX * y + translateX); @@ -596,7 +614,7 @@ class YUP_API AffineTransform @return A tuple representing the transformed coordinates of the last point processed. */ template - constexpr void transformPoints (T& x, T& y, Args&&... args) const noexcept + constexpr auto transformPoints (T& x, T& y, Args&&... args) const noexcept -> std::enable_if_t> { transformPoint (x, y); diff --git a/modules/yup_graphics/primitives/yup_Point.h b/modules/yup_graphics/primitives/yup_Point.h index 940d584e3..37c6de402 100644 --- a/modules/yup_graphics/primitives/yup_Point.h +++ b/modules/yup_graphics/primitives/yup_Point.h @@ -1291,6 +1291,47 @@ template static_assert (dependentFalse); } +/** Forwarded methods implementation. */ +[[nodiscard]] constexpr AffineTransform AffineTransform::translated (Point p) const noexcept +{ + return { scaleX, shearX, translateX + p.getX(), shearY, scaleY, translateY + p.getY() }; +} + +[[nodiscard]] constexpr AffineTransform AffineTransform::translation (Point p) noexcept +{ + return translation (p.getX(), p.getY()); +} + +[[nodiscard]] constexpr AffineTransform AffineTransform::withAbsoluteTranslation (Point p) const noexcept +{ + return withAbsoluteTranslation (p.getX(), p.getY()); +} + +[[nodiscard]] constexpr AffineTransform AffineTransform::rotated (float angleInRadians, Point center) const noexcept +{ + return rotated (angleInRadians, center.getX(), center.getY()); +} + +[[nodiscard]] constexpr AffineTransform AffineTransform::rotation (float angleInRadians, Point center) noexcept +{ + return rotation (angleInRadians, center.getX(), center.getY()); +} + +[[nodiscard]] constexpr AffineTransform AffineTransform::scaled (float factorX, float factorY, Point center) const noexcept +{ + return scaled (factorX, factorY, center.getX(), center.getY()); +} + +[[nodiscard]] constexpr AffineTransform AffineTransform::scaling (float factorX, float factorY, Point center) noexcept +{ + return scaling (factorX, factorY, center.getX(), center.getY()); +} + +[[nodiscard]] constexpr AffineTransform AffineTransform::shearing (float factorX, float factorY, Point center) noexcept +{ + return shearing (factorX, factorY, center.getX(), center.getY()); +} + } // namespace yup namespace std diff --git a/modules/yup_gui/component/yup_Component.cpp b/modules/yup_gui/component/yup_Component.cpp index ddc57d3f5..2f1a76f52 100644 --- a/modules/yup_gui/component/yup_Component.cpp +++ b/modules/yup_gui/component/yup_Component.cpp @@ -1301,30 +1301,42 @@ void Component::updateMouseCursor() Desktop::getInstance()->setMouseCursor (mouseCursor); } +//============================================================================== + Point Component::getScreenPosition() const { - // If this component is on the desktop, its position is already in screen coordinates + return localToScreen (getPosition()); +} + +//============================================================================== + +Rectangle Component::getScreenBounds() const +{ + return localToScreen (getBounds()); +} + +//============================================================================== + +Point Component::localToScreen (const Point& localPoint) const +{ if (options.onDesktop && native != nullptr) - return native->getPosition().to(); + return native->getPosition().to() + localPoint; - // For child components, accumulate transformations up the hierarchy - auto screenPos = getPosition(); + auto screenPos = localPoint; auto parent = getParentComponent(); while (parent != nullptr) { if (parent->options.onDesktop) { - // Found the top-level component, add its screen position if (parent->native != nullptr) - screenPos = screenPos + parent->native->getPosition().to(); + screenPos += parent->native->getPosition().to(); break; } else { - // Add parent's position relative to its parent - screenPos = screenPos + parent->getPosition(); + screenPos += parent->getPosition(); } parent = parent->getParentComponent(); @@ -1333,9 +1345,105 @@ Point Component::getScreenPosition() const return screenPos; } -Rectangle Component::getScreenBounds() const +Point Component::screenToLocal (const Point& screenPoint) const +{ + return screenPoint - localToScreen (getPosition()); +} + +Rectangle Component::localToScreen (const Rectangle& localRectangle) const +{ + return Rectangle (localToScreen (localRectangle.getPosition()), localRectangle.getSize()); +} + +Rectangle Component::screenToLocal (const Rectangle& screenRectangle) const +{ + return Rectangle (screenToLocal (screenRectangle.getPosition()), screenRectangle.getSize()); +} + +//============================================================================== + +Point Component::getLocalPoint (const Component* sourceComponent, Point pointInSource) const +{ + if (sourceComponent == nullptr || sourceComponent == this) + return pointInSource; + + return screenToLocal (sourceComponent->localToScreen (pointInSource)); +} + +Rectangle Component::getLocalArea (const Component* sourceComponent, Rectangle rectangleInSource) const +{ + if (sourceComponent == nullptr || sourceComponent == this) + return rectangleInSource; + + return screenToLocal (sourceComponent->localToScreen (rectangleInSource)); +} + +Point Component::getRelativePoint (const Component* targetComponent, Point localPoint) const { - return Rectangle (getScreenPosition(), getSize()); + if (targetComponent == nullptr || targetComponent == this) + return localPoint; + + return targetComponent->screenToLocal (localToScreen (localPoint)); +} + +Rectangle Component::getRelativeArea (const Component* targetComponent, Rectangle localRectangle) const +{ + if (targetComponent == nullptr || targetComponent == this) + return localRectangle; + + return targetComponent->screenToLocal (localToScreen (localRectangle)); +} + +AffineTransform Component::getTransformToComponent (const Component* targetComponent) const +{ + if (targetComponent == nullptr || targetComponent == this) + return AffineTransform(); + + AffineTransform transform; + + auto thisToScreen = getTransformToScreen(); + auto targetToScreen = targetComponent->getTransformToScreen(); + + transform = thisToScreen.followedBy (targetToScreen.inverted()); + + return transform; +} + +AffineTransform Component::getTransformFromComponent (const Component* sourceComponent) const +{ + if (sourceComponent == nullptr) + return AffineTransform(); + + return sourceComponent->getTransformToComponent (this); +} + +AffineTransform Component::getTransformToScreen() const +{ + AffineTransform transform; + const Component* comp = this; + + while (comp != nullptr) + { + if (comp->isTransformed()) + transform = transform.followedBy (comp->getTransform()); + + transform = transform.translated (comp->getPosition()); + + if (comp->options.onDesktop) + { + if (comp->native != nullptr) + { + auto nativePos = comp->native->getPosition().to(); + transform = transform.translated (nativePos); + } + + break; + } + + comp = comp->getParentComponent(); + } + + return transform; } } // namespace yup diff --git a/modules/yup_gui/component/yup_Component.h b/modules/yup_gui/component/yup_Component.h index bb6b8ea57..eda0849bb 100644 --- a/modules/yup_gui/component/yup_Component.h +++ b/modules/yup_gui/component/yup_Component.h @@ -138,6 +138,16 @@ class YUP_API Component */ void setPosition (const Point& newPosition); + /** + Get the position of the component in absolute screen coordinates. + + This method traverses up the parent hierarchy to calculate the component's + absolute position on the screen. + + @return The absolute screen position of the component. + */ + Point getScreenPosition() const; + /** Get the x position of the component relative to its parent. @@ -180,25 +190,91 @@ class YUP_API Component */ float getBottom() const; + /** + Get the top left position of the component relative to its parent. + + @return The top left position of the component relative to its parent. + */ Point getTopLeft() const; + + /** + Set the top left position of the component relative to its parent. + + @param newTopLeft The new top left position of the component relative to its parent. + */ void setTopLeft (const Point& newTopLeft); + /** + Get the bottom left position of the component relative to its parent. + + @return The bottom left position of the component relative to its parent. + */ Point getBottomLeft() const; + + /** + Set the bottom left position of the component relative to its parent. + + @param newBottomLeft The new bottom left position of the component relative to its parent. + */ void setBottomLeft (const Point& newBottomLeft); + /** + Get the top right position of the component relative to its parent. + + @return The top right position of the component relative to its parent. + */ Point getTopRight() const; + + /** + Set the top right position of the component relative to its parent. + + @param newTopRight The new top right position of the component relative to its parent. + */ void setTopRight (const Point& newTopRight); + /** + Get the bottom right position of the component relative to its parent. + + @return The bottom right position of the component relative to its parent. + */ Point getBottomRight() const; + + /** + Set the bottom right position of the component relative to its parent. + + @param newBottomRight The new bottom right position of the component relative to its parent. + */ void setBottomRight (const Point& newBottomRight); Point getCenter() const; void setCenter (const Point& newCenter); + /** + Get the center x position of the component relative to its parent. + + @return The center x position of the component relative to its parent. + */ float getCenterX() const; + + /** + Set the center x position of the component relative to its parent. + + @param newCenterX The new center x position of the component relative to its parent. + */ void setCenterX (float newCenterX); + /** + Get the center y position of the component relative to its parent. + + @return The center y position of the component relative to its parent. + */ float getCenterY() const; + + /** + Set the center y position of the component relative to its parent. + + @param newCenterY The new center y position of the component relative to its parent. + */ void setCenterY (float newCenterY); /** @@ -271,13 +347,6 @@ class YUP_API Component */ Rectangle getBounds() const; - /** - Get the bounds of the component in screen coordinates. - - @return The bounds of the component in screen coordinates. - */ - Rectangle getScreenBounds() const; - /** Get the local bounds of the component. @@ -295,25 +364,152 @@ class YUP_API Component Rectangle getBoundsRelativeToTopLevelComponent() const; /** - Get the position of the component in absolute screen coordinates. + Get the bounds of the component in screen coordinates. - This method traverses up the parent hierarchy to calculate the component's - absolute position on the screen. + @return The bounds of the component in screen coordinates. + */ + Rectangle getScreenBounds() const; - @return The absolute screen position of the component. + //============================================================================== + + /** + Convert a point from local coordinates to screen coordinates. + + @param localPoint The point to convert. + + @return The point in screen coordinates. */ - Point getScreenPosition() const; + Point localToScreen (const Point& localPoint) const; + + /** + Convert a point from screen coordinates to local coordinates. + + @param screenPoint The point to convert. + + @return The point in local coordinates. + */ + Point screenToLocal (const Point& screenPoint) const; + + /** + Convert a rectangle from local coordinates to screen coordinates. + + @param localRectangle The rectangle to convert. + + @return The rectangle in screen coordinates. + */ + Rectangle localToScreen (const Rectangle& localRectangle) const; + + /** + Convert a rectangle from screen coordinates to local coordinates. + + @param screenRectangle The rectangle to convert. + + @return The rectangle in local coordinates. + */ + Rectangle screenToLocal (const Rectangle& screenRectangle) const; //============================================================================== + /** + Convert a point from another component's coordinate system to this component's local coordinates. + This method handles all transforms in the component hierarchy. + + @param sourceComponent The component whose coordinate system the point is in + @param pointInSource The point in the source component's coordinate system + + @return The point converted to this component's local coordinate system + */ + Point getLocalPoint (const Component* sourceComponent, Point pointInSource) const; + + /** + Convert a rectangle from another component's coordinate system to this component's local coordinates. + This method handles all transforms in the component hierarchy. + + @param sourceComponent The component whose coordinate system the rectangle is in + @param rectangleInSource The rectangle in the source component's coordinate system + + @return The rectangle converted to this component's local coordinate system + */ + Rectangle getLocalArea (const Component* sourceComponent, Rectangle rectangleInSource) const; + + /** + Convert a point from this component's local coordinates to another component's coordinate system. + This method handles all transforms in the component hierarchy. + + @param targetComponent The component whose coordinate system to convert to + @param localPoint The point in this component's local coordinate system + + @return The point converted to the target component's coordinate system + */ + Point getRelativePoint (const Component* targetComponent, Point localPoint) const; + + /** + Convert a rectangle from this component's local coordinates to another component's coordinate system. + This method handles all transforms in the component hierarchy. + + @param targetComponent The component whose coordinate system to convert to + @param localRectangle The rectangle in this component's local coordinate system + + @return The rectangle converted to the target component's coordinate system + */ + Rectangle getRelativeArea (const Component* targetComponent, Rectangle localRectangle) const; + + //============================================================================== + /** + Set the transform of the component. + + @param transform The new transform of the component. + */ void setTransform (const AffineTransform& transform); + /** + Get the transform of the component. + + @return The transform of the component. + */ AffineTransform getTransform() const; + /** + Check if the component is transformed. + + @return True if the component is transformed, false otherwise. + */ bool isTransformed() const; + /** + Called when the transform of the component changes. + */ virtual void transformChanged(); + //============================================================================== + /** + Get the transform from this component's coordinate system to another component's coordinate system. + This calculates the combined transform needed to convert coordinates from this component to the target. + + @param targetComponent The component to get the transform to + + @return The combined transform from this component to the target component + */ + AffineTransform getTransformToComponent (const Component* targetComponent) const; + + /** + Get the transform from another component's coordinate system to this component's coordinate system. + This calculates the combined transform needed to convert coordinates from the source to this component. + + @param sourceComponent The component to get the transform from + + @return The combined transform from the source component to this component + */ + AffineTransform getTransformFromComponent (const Component* sourceComponent) const; + + /** + Get the transform from this component's coordinate system to screen coordinates. + This calculates the combined transform needed to convert coordinates from this component to screen space. + + @return The combined transform from this component to screen coordinates + */ + AffineTransform getTransformToScreen() const; + //============================================================================== /** Set the full screen state of the component. diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index af689f265..9f0460578 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -29,45 +29,80 @@ namespace static std::vector activePopups; -Point calculatePositionWithJustification (const Rectangle& targetArea, - const Size& menuSize, - Justification justification) +Point calculatePositionAtPoint (Point targetPoint, Size menuSize, Justification alignment) { - Point position; + Point position = targetPoint; - switch (justification) + switch (alignment) { default: case Justification::topLeft: - position = targetArea.getTopLeft(); + // Menu's top-left at target point (default) break; case Justification::centerTop: - position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getTop()); + position.setX (targetPoint.getX() - menuSize.getWidth() / 2); break; case Justification::topRight: - position = targetArea.getTopRight().translated (-menuSize.getWidth(), 0); + position.setX (targetPoint.getX() - menuSize.getWidth()); + break; + + case Justification::centerLeft: + position.setY (targetPoint.getY() - menuSize.getHeight() / 2); + break; + + case Justification::center: + position = targetPoint - Point{ menuSize.getWidth() / 2, menuSize.getHeight() / 2 }; + break; + + case Justification::centerRight: + position.setX (targetPoint.getX() - menuSize.getWidth()); + position.setY (targetPoint.getY() - menuSize.getHeight() / 2); break; case Justification::bottomLeft: - position = targetArea.getBottomLeft(); + position.setY (targetPoint.getY() - menuSize.getHeight()); break; case Justification::centerBottom: - position = Point (targetArea.getCenterX() - menuSize.getWidth() / 2, targetArea.getBottom()); + position.setX (targetPoint.getX() - menuSize.getWidth() / 2); + position.setY (targetPoint.getY() - menuSize.getHeight()); break; case Justification::bottomRight: - position = targetArea.getBottomRight().translated (-menuSize.getWidth(), 0); + position = targetPoint - Point{ menuSize.getWidth(), menuSize.getHeight() }; break; + } - case Justification::centerRight: - position = Point (targetArea.getRight(), targetArea.getCenterY() - menuSize.getHeight() / 2); + return position; +} + +Point calculatePositionRelativeToArea (Rectangle targetArea, Size menuSize, PopupMenu::Placement placement) +{ + Point position; + + switch (placement) + { + default: + case PopupMenu::Placement::below: + position = Point (targetArea.getX(), targetArea.getBottom()); break; - case Justification::centerLeft: - position = Point (targetArea.getX() - menuSize.getWidth(), targetArea.getCenterY() - menuSize.getHeight() / 2); + case PopupMenu::Placement::above: + position = Point (targetArea.getX(), targetArea.getY() - menuSize.getHeight()); + break; + + case PopupMenu::Placement::toRight: + position = Point (targetArea.getRight(), targetArea.getY()); + break; + + case PopupMenu::Placement::toLeft: + position = Point (targetArea.getX() - menuSize.getWidth(), targetArea.getY()); + break; + + case PopupMenu::Placement::centered: + position = targetArea.getCenter() - Point{ menuSize.getWidth() / 2, menuSize.getHeight() / 2 };; break; } @@ -247,9 +282,10 @@ void installGlobalMouseListener() PopupMenu::Options::Options() : parentComponent (nullptr) , dismissOnSelection (true) - , justification (Justification::bottomLeft) - , addAsChildToTopmost (false) - , useNativeMenus (false) + , alignment (Justification::topLeft) + , placement (Placement::below) + , positioningMode (PositioningMode::atPoint) + , targetComponent (nullptr) { } @@ -259,45 +295,49 @@ PopupMenu::Options& PopupMenu::Options::withParentComponent (Component* parentCo return *this; } -PopupMenu::Options& PopupMenu::Options::withTargetArea (const Rectangle& targetArea) +PopupMenu::Options& PopupMenu::Options::withPosition (Point position, Justification alignment) { - this->targetArea = targetArea; + this->positioningMode = PositioningMode::atPoint; + this->targetPosition = position; + this->alignment = alignment; return *this; } -PopupMenu::Options& PopupMenu::Options::withJustification (Justification justification) +PopupMenu::Options& PopupMenu::Options::withPosition (Point position, Justification alignment) { - this->justification = justification; - return *this; + return withPosition (position.to(), alignment); } -PopupMenu::Options& PopupMenu::Options::withTargetPosition (const Point& targetPosition) +PopupMenu::Options& PopupMenu::Options::withTargetArea (Rectangle area, Placement placement) { - this->targetPosition = targetPosition; + this->positioningMode = PositioningMode::relativeToArea; + this->targetArea = area; + this->placement = placement; return *this; } -PopupMenu::Options& PopupMenu::Options::withMinimumWidth (int minWidth) +PopupMenu::Options& PopupMenu::Options::withTargetArea (Rectangle area, Placement placement) { - this->minWidth = minWidth; - return *this; + return withTargetArea (area.to(), placement); } -PopupMenu::Options& PopupMenu::Options::withMaximumWidth (int maxWidth) +PopupMenu::Options& PopupMenu::Options::withRelativePosition (Component* component, Placement placement) { - this->maxWidth = maxWidth; + this->positioningMode = PositioningMode::relativeToComponent; + this->targetComponent = component; + this->placement = placement; return *this; } -PopupMenu::Options& PopupMenu::Options::withAsChildToTopmost (bool addAsChildToTopmost) +PopupMenu::Options& PopupMenu::Options::withMinimumWidth (int minWidth) { - this->addAsChildToTopmost = addAsChildToTopmost; + this->minWidth = minWidth; return *this; } -PopupMenu::Options& PopupMenu::Options::withNativeMenus (bool useNativeMenus) +PopupMenu::Options& PopupMenu::Options::withMaximumWidth (int maxWidth) { - this->useNativeMenus = useNativeMenus; + this->maxWidth = maxWidth; return *this; } @@ -479,72 +519,76 @@ void PopupMenu::setupMenuItems() setSize ({ width, y + verticalPadding }); // Bottom padding } +//============================================================================== + void PopupMenu::positionMenu() { - auto menuSize = getSize(); + auto menuSize = getSize().to(); Rectangle targetArea; Rectangle availableArea; - // Determine target area and available area + // Determine coordinate system and available area if (options.parentComponent) { - // Get the bounds relative to the screen or topmost component - if (options.addAsChildToTopmost) + // Working in parent component's local coordinates + availableArea = options.parentComponent->getLocalBounds().to(); + } + else + { + // Working in screen coordinates + if (auto* desktop = Desktop::getInstance()) { - // Target area is relative to topmost component - targetArea = options.parentComponent->getBounds().to(); - availableArea = targetArea; + if (auto screen = desktop->getPrimaryScreen()) + availableArea = screen->workArea; + else + availableArea = Rectangle (0, 0, 1920, 1080); // Fallback } else { - // Target area is in screen coordinates - targetArea = options.parentComponent->getScreenBounds().to(); - - // Available area is the screen bounds - if (auto* desktop = Desktop::getInstance()) - { - if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) - availableArea = screen->workArea; - else if (auto screen = desktop->getPrimaryScreen()) - availableArea = screen->workArea; - else - availableArea = targetArea; - } - } - - // Override with explicit target area if provided - if (! options.targetArea.isEmpty()) - { - if (options.addAsChildToTopmost) - targetArea = options.targetArea; - else - targetArea = options.targetArea.translated (targetArea.getPosition()); + availableArea = Rectangle (0, 0, 1920, 1080); // Fallback } } - else + + // Calculate position based on positioning mode + Point position; + + switch (options.positioningMode) { - if (! options.targetArea.isEmpty()) + case PositioningMode::atPoint: + position = calculatePositionAtPoint (options.targetPosition, menuSize, options.alignment); + break; + + case PositioningMode::relativeToArea: targetArea = options.targetArea; - else - targetArea = Rectangle (options.targetPosition, options.targetArea.getSize()); + position = calculatePositionRelativeToArea (targetArea, menuSize, options.placement); + break; - // Get screen bounds for available area - if (auto* desktop = Desktop::getInstance()) - { - if (auto screen = desktop->getScreenContaining (targetArea.getCenter())) - availableArea = screen->workArea; - else if (auto screen = desktop->getPrimaryScreen()) - availableArea = screen->workArea; + case PositioningMode::relativeToComponent: + if (options.targetComponent) + { + // Get target component bounds in appropriate coordinate system + if (options.parentComponent) + { + // Convert to parent component's local coordinates + targetArea = options.parentComponent->getLocalArea (options.targetComponent, options.targetComponent->getLocalBounds()).to(); + } + else + { + // Use screen coordinates + targetArea = options.targetComponent->getScreenBounds().to(); + } + position = calculatePositionRelativeToArea (targetArea, menuSize, options.placement); + } else - availableArea = targetArea; - } + { + // Fallback to center of available area + position = availableArea.getCenter() - Point{ menuSize.getWidth() / 2, menuSize.getHeight() / 2 }; + } + break; } - // Calculate position based on justification - Point position = calculatePositionWithJustification (targetArea, menuSize.to(), options.justification).to(); - // Adjust position to fit within available area - position = constrainPositionToAvailableArea (position, menuSize.to(), availableArea, targetArea).to(); + position = constrainPositionToAvailableArea (position, menuSize, availableArea, targetArea); setTopLeft (position); } @@ -591,12 +635,14 @@ void PopupMenu::showCustom (const Options& options, std::function ca setWantsKeyboardFocus (true); - if (options.addAsChildToTopmost && options.parentComponent) + if (options.parentComponent) { + // When we have a parent component, add as child to work in local coordinates options.parentComponent->addChildComponent (this); } else { + // When we have no parent component, add to desktop to work in screen coordinates auto nativeOptions = ComponentNative::Options {} .withDecoration (false) .withResizableWindow (false); diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 6d7bd0594..1f1996a6d 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -28,7 +28,8 @@ namespace yup This class supports both native system menus and custom rendered menus. */ -class YUP_API PopupMenu : public Component +class YUP_API PopupMenu + : public Component , public ReferenceCountedObject { public: @@ -36,23 +37,62 @@ class YUP_API PopupMenu : public Component /** Convenience typedef for a reference-counted pointer to a PopupMenu. */ using Ptr = ReferenceCountedObjectPtr; + //============================================================================== + /** Menu positioning relative to rectangles/components */ + enum class Placement + { + above, //< Menu appears above the target + below, //< Menu appears below the target (default) + toLeft, //< Menu appears to the left of the target + toRight, //< Menu appears to the right of the target + centered //< Menu is centered on the target + }; + + enum class PositioningMode + { + atPoint, + relativeToArea, + relativeToComponent + }; + //============================================================================== /** Options for showing the popup menu. */ struct Options { Options(); - /** The parent component to attach the menu to. */ + /** Sets the parent component. When set, menu appears as child using local coordinates. + When not set, menu appears as desktop window using screen coordinates. */ Options& withParentComponent (Component* parentComponent); - /** The area to position the menu relative to. */ - Options& withTargetArea (const Rectangle& targetArea); - /** How to position the menu relative to the target area. */ - Options& withJustification (Justification justification); + /** Position menu at a specific point. + - With parent: point is relative to parent component + - Without parent: point is in screen coordinates - /** The position to show the menu at (relative to parent). */ - Options& withTargetPosition (const Point& targetPosition); + @param position The point to show the menu at + @param alignment How to align the menu relative to the point (default: top-left of menu at point) + */ + Options& withPosition (Point position, Justification alignment = Justification::topLeft); + Options& withPosition (Point position, Justification alignment = Justification::topLeft); + + /** Position menu relative to a rectangle (like a button). + - With parent: rectangle is relative to parent component + - Without parent: rectangle is in screen coordinates + + @param area The rectangle to position relative to + @param placement Where to place menu relative to rectangle (default: below the rectangle) + */ + Options& withTargetArea (Rectangle area, Placement placement = Placement::below); + Options& withTargetArea (Rectangle area, Placement placement = Placement::below); + + /** Position menu relative to a component (uses the component's bounds). + The component must be a child of the parent component (if parent is set). + + @param component The component to position relative to + @param placement Where to place menu relative to component (default: below) + */ + Options& withRelativePosition (Component* component, Placement placement = Placement::below); /** Minimum width for the menu. */ Options& withMinimumWidth (int minWidth); @@ -60,21 +100,16 @@ class YUP_API PopupMenu : public Component /** Maximum width for the menu. */ Options& withMaximumWidth (int maxWidth); - /** Whether to add the menu as a child to the topmost component. */ - Options& withAsChildToTopmost (bool addAsChildToTopmost); - - /** Whether to use native system menus (when available). */ - Options& withNativeMenus (bool useNativeMenus); - Component* parentComponent; + Component* targetComponent; Point targetPosition; Rectangle targetArea; - Justification justification; + Justification alignment; + Placement placement; + PositioningMode positioningMode; std::optional minWidth; std::optional maxWidth; bool dismissOnSelection; - bool addAsChildToTopmost; - bool useNativeMenus; }; //============================================================================== From 2182c78ef6b6400795e8b02f2a85dbca4e38b3be Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 26 Jun 2025 00:03:56 +0200 Subject: [PATCH 36/51] More tweaks --- .../primitives/yup_AffineTransform.h | 118 +++++++++++++++++- modules/yup_graphics/primitives/yup_Line.h | 37 +++++- modules/yup_graphics/primitives/yup_Point.h | 56 ++++++++- .../yup_graphics/primitives/yup_Rectangle.h | 16 ++- .../primitives/yup_RectangleList.h | 4 + modules/yup_graphics/primitives/yup_Size.h | 17 +++ modules/yup_gui/keyboard/yup_KeyModifiers.h | 26 ++-- modules/yup_gui/keyboard/yup_KeyPress.h | 10 +- modules/yup_gui/menus/yup_PopupMenu.h | 14 ++- modules/yup_gui/widgets/yup_TextButton.cpp | 2 +- 10 files changed, 265 insertions(+), 35 deletions(-) diff --git a/modules/yup_graphics/primitives/yup_AffineTransform.h b/modules/yup_graphics/primitives/yup_AffineTransform.h index 6445a71e1..8ad5240d9 100644 --- a/modules/yup_graphics/primitives/yup_AffineTransform.h +++ b/modules/yup_graphics/primitives/yup_AffineTransform.h @@ -22,7 +22,8 @@ namespace yup { -template class YUP_API Point; +template +class YUP_API Point; //============================================================================== /** Class representing a 2D affine transformation. @@ -149,6 +150,16 @@ class YUP_API AffineTransform return translateY; } + //============================================================================== + + /** Get translation + + Returns the translation components of the AffineTransform object. + + @return The translation components of this AffineTransform. + */ + [[nodiscard]] constexpr Point getTranslation() const noexcept; + //============================================================================== /** Get span of matrix array @@ -240,6 +251,14 @@ class YUP_API AffineTransform return { scaleX, shearX, translateX + tx, shearY, scaleY, translateY + ty }; } + /** Create a translated transformation + + Creates a new AffineTransform object that represents this transformation translated by specified amounts in the x and y directions. + + @param p The point to translate. + + @return A new AffineTransform object representing the translated transformation. + */ [[nodiscard]] constexpr AffineTransform translated (Point p) const noexcept; /** Create a translation transformation @@ -256,6 +275,14 @@ class YUP_API AffineTransform return { 1.0f, 0.0f, tx, 0.0f, 1.0f, ty }; } + /** Create a translation transformation + + Creates an AffineTransform object representing a translation by specified amounts in the x and y directions. + + @param p The point to translate. + + @return An AffineTransform object representing the specified translation. + */ [[nodiscard]] static constexpr AffineTransform translation (Point p) noexcept; /** Create a translated transformation @@ -272,6 +299,14 @@ class YUP_API AffineTransform return { scaleX, shearX, tx, shearY, scaleY, ty }; } + /** Create a translated transformation + + Creates a new AffineTransform object that represents this transformation translated absolutely in the x and y directions. + + @param p The point to translate. + + @return A new AffineTransform object representing the translated transformation. + */ [[nodiscard]] constexpr AffineTransform withAbsoluteTranslation (Point p) const noexcept; //============================================================================== @@ -316,6 +351,15 @@ class YUP_API AffineTransform return followedBy (rotation (angleInRadians, centerX, centerY)); } + /** Create a rotated transformation around a point + + Creates a new AffineTransform object that represents this transformation rotated by a specified angle around a specified point. + + @param angleInRadians The angle in radians by which to rotate. + @param center The point around which to rotate. + + @return A new AffineTransform object representing the rotated transformation. + */ [[nodiscard]] constexpr AffineTransform rotated (float angleInRadians, Point center) const noexcept; /** Create a rotation transformation @@ -365,6 +409,15 @@ class YUP_API AffineTransform }; } + /** Create a rotation transformation around a point + + Creates an AffineTransform object representing a rotation by a specified angle around a specified point. + + @param angleInRadians The angle in radians by which to rotate. + @param center The point around which to rotate. + + @return An AffineTransform object representing the specified rotation around the point. + */ [[nodiscard]] static constexpr AffineTransform rotation (float angleInRadians, Point center) noexcept; //============================================================================== @@ -432,6 +485,16 @@ class YUP_API AffineTransform }; } + /** Create a scaled transformation non-uniformly around a point + + Creates a new AffineTransform object that represents this transformation scaled by specified factors along the x and y axes around a specified point. + + @param factorX The scale factor to apply to the x-axis. + @param factorY The scale factor to apply to the y-axis. + @param center The point around which to scale. + + @return A new AffineTransform object representing the non-uniformly scaled transformation around the point. + */ [[nodiscard]] constexpr AffineTransform scaled (float factorX, float factorY, Point center) const noexcept; /** Create a scaling transformation @@ -477,6 +540,16 @@ class YUP_API AffineTransform return { factorX, 0.0f, centerX * (1.0f - factorX), 0.0f, factorY, centerY * (1.0f - factorY) }; } + /** Create a scaling transformation non-uniformly around a point + + Creates an AffineTransform object representing a non-uniform scaling by specified factors along the x and y axes around a specified point. + + @param factorX The scale factor to apply to the x-axis. + @param factorY The scale factor to apply to the y-axis. + @param center The point around which to scale. + + @return An AffineTransform object representing the specified non-uniform scaling around the point. + */ [[nodiscard]] static constexpr AffineTransform scaling (float factorX, float factorY, Point center) noexcept; //============================================================================== @@ -538,6 +611,16 @@ class YUP_API AffineTransform }; } + /** Create a shearing transformation around a point + + Creates an AffineTransform object representing a shearing by specified factors along the x and y axes around a specified point. + + @param factorX The shear factor to apply to the x-axis. + @param factorY The shear factor to apply to the y-axis. + @param center The point around which to shear. + + @return An AffineTransform object representing the specified shearing around the point. + */ [[nodiscard]] static constexpr AffineTransform shearing (float factorX, float factorY, Point center) noexcept; //============================================================================== @@ -561,7 +644,14 @@ class YUP_API AffineTransform }; } - // TODO - doxygen + /** Create a transformation that precedes another. + + Creates a new AffineTransform object that represents this transformation preceded by another specified AffineTransform. + + @param other The AffineTransform to precede this one. + + @return A new AffineTransform object representing the combined transformation. + */ [[nodiscard]] constexpr AffineTransform prependedBy (const AffineTransform& other) const noexcept { return { @@ -575,13 +665,23 @@ class YUP_API AffineTransform } //============================================================================== - // TODO - doxygen + /** Get the determinant of the transformation + + Calculates the determinant of the transformation matrix, which is a measure of the scaling factor. + + @return The determinant of the transformation. + */ [[nodiscard]] constexpr float getDeterminant() const noexcept { return (scaleX * scaleY) - (shearX * shearY); } - // TODO - doxygen + /** Get the scale factor of the transformation + + Calculates the average of the absolute values of the scale factors along the x and y axes. + + @return The scale factor of the transformation. + */ [[nodiscard]] constexpr float getScaleFactor() const noexcept { return (yup_abs (scaleX) + yup_abs (scaleY)) / 2.0f; @@ -685,6 +785,16 @@ class YUP_API AffineTransform }; }; +//============================================================================== +/** Get the matrix component at the specified index + + Returns the matrix component at the specified index. + + @param transform The AffineTransform to get the component from. + @param I The index of the component to get. + + @return The matrix component at the specified index. +*/ template constexpr float get (const AffineTransform& transform) noexcept { diff --git a/modules/yup_graphics/primitives/yup_Line.h b/modules/yup_graphics/primitives/yup_Line.h index eedf0e17b..2f09f6cbc 100644 --- a/modules/yup_graphics/primitives/yup_Line.h +++ b/modules/yup_graphics/primitives/yup_Line.h @@ -523,7 +523,15 @@ class YUP_API Line } //============================================================================== - // TODO - doxygen + /** Transforms the line by the specified affine transform. + + This function applies the given affine transform to both the start and end points of the line, + effectively transforming the line's geometry. + + @param t The affine transform to apply. + + @return A reference to this line after transformation. + */ constexpr Line& transform (const AffineTransform& t) noexcept { auto x1 = static_cast (p1.x); @@ -541,7 +549,15 @@ class YUP_API Line return *this; } - // TODO - doxygen + /** Transforms the line by the specified affine transform. + + This function applies the given affine transform to both the start and end points of the line, + effectively transforming the line's geometry. + + @param t The affine transform to apply. + + @return A new `Line` object representing the transformed line. + */ constexpr Line transformed (const AffineTransform& t) const noexcept { Line result (*this); @@ -565,6 +581,14 @@ class YUP_API Line return { p1.template to(), p2.template to() }; } + /** Round the coordinates of this line to integers + + Rounds the coordinates of this line to integers and returns a new Line object with the rounded coordinates. + + @tparam T The type of the coordinates, constrained to floating-point types. + + @return A new Line object with the rounded coordinates. + */ template constexpr auto roundToInt() const noexcept -> std::enable_if_t, Line> @@ -639,6 +663,15 @@ YUP_API String& YUP_CALLTYPE operator<< (String& string1, const Line& return string1; } +/** Get the coordinate at the specified index + + Returns the coordinate at the specified index. + + @param point The Point to get the coordinate from. + @param I The index of the coordinate to get. + + @return The coordinate at the specified index. +*/ template constexpr ValueType get (const Line& line) noexcept { diff --git a/modules/yup_graphics/primitives/yup_Point.h b/modules/yup_graphics/primitives/yup_Point.h index 37c6de402..ac01c0453 100644 --- a/modules/yup_graphics/primitives/yup_Point.h +++ b/modules/yup_graphics/primitives/yup_Point.h @@ -109,7 +109,12 @@ class YUP_API Point return x; } - // TODO - doxygen + /** Sets the x coordinate of this point. + + @param newX The new x coordinate. + + @return A reference to this point after setting the x coordinate. + */ constexpr Point& setX (ValueType newX) noexcept { x = newX; @@ -137,7 +142,12 @@ class YUP_API Point return y; } - // TODO - doxygen + /** Sets the y coordinate of this point. + + @param newY The new y coordinate. + + @return A reference to this point after setting the y coordinate. + */ constexpr Point& setY (ValueType newY) noexcept { y = newY; @@ -894,14 +904,29 @@ class YUP_API Point } //============================================================================== - // TODO - doxygen + /** Transforms this point by an AffineTransform. + + This method modifies the coordinates of this point by applying an AffineTransform to them. + It is useful for applying geometric transformations to points. + + @param t The AffineTransform to apply to this point. + + @return A reference to this point after the transformation. + */ constexpr Point& transform (const AffineTransform& t) noexcept { t.transformPoints (x, y); return *this; } - // TODO - doxygen + /** Transforms this point by an AffineTransform. + + This method creates a new Point object with coordinates that are the result of applying an AffineTransform to this point's coordinates. + It is useful for applying geometric transformations to points without modifying the original point. + + @param t The AffineTransform to apply to this point. + @return A new Point object representing the transformed coordinates. + */ [[nodiscard]] constexpr Point transformed (const AffineTransform& t) const noexcept { Point result (*this); @@ -925,6 +950,15 @@ class YUP_API Point return { static_cast (x), static_cast (y) }; } + /** Rounds the coordinates of this point to integers. + + This method creates a new Point object with coordinates that are the result of rounding this point's coordinates to integers. + It is useful for converting floating-point coordinates to integer coordinates. + + @tparam T The numeric type of the coordinates, constrained to floating-point types. + + @return A new Point object with the rounded coordinates. + */ template [[nodiscard]] constexpr auto roundToInt() const noexcept -> std::enable_if_t, Point> @@ -1280,6 +1314,15 @@ YUP_API String& YUP_CALLTYPE operator<< (String& string1, const Point return string1; } +/** Get the coordinate at the specified index + + Returns the coordinate at the specified index. + + @param point The Point to get the coordinate from. + @param I The index of the coordinate to get. + + @return The coordinate at the specified index. +*/ template [[nodiscard]] constexpr ValueType get (const Point& point) noexcept { @@ -1292,6 +1335,11 @@ template } /** Forwarded methods implementation. */ +[[nodiscard]] constexpr Point AffineTransform::getTranslation() const noexcept +{ + return { translateX, translateY }; +} + [[nodiscard]] constexpr AffineTransform AffineTransform::translated (Point p) const noexcept { return { scaleX, shearX, translateX + p.getX(), shearY, scaleY, translateY + p.getY() }; diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index ec6b1077e..243823642 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -1535,7 +1535,12 @@ class YUP_API Rectangle return contains (p.getX(), p.getY()); } - /** TODO: doxygen */ + /** Checks if the specified rectangle lies within the bounds of this rectangle. + + @param p The rectangle to check. + + @return True if the rectangle is within the bounds of this rectangle, otherwise false. + */ [[nodiscard]] constexpr bool contains (const Rectangle& p) const noexcept { return p.getX() >= xy.getX() @@ -1904,6 +1909,15 @@ YUP_API String& YUP_CALLTYPE operator<< (String& string1, const Rectangle constexpr ValueType get (const Rectangle& point) noexcept { diff --git a/modules/yup_graphics/primitives/yup_RectangleList.h b/modules/yup_graphics/primitives/yup_RectangleList.h index 328d8ad7f..074cd8da9 100644 --- a/modules/yup_graphics/primitives/yup_RectangleList.h +++ b/modules/yup_graphics/primitives/yup_RectangleList.h @@ -328,21 +328,25 @@ class YUP_API RectangleList } //============================================================================== + /** Returns a pointer to the first rectangle in the list. */ const Rectangle* begin() const { return rectangles.begin(); } + /** Returns a pointer to the end of the list. */ const Rectangle* end() const { return rectangles.end(); } + /** Returns a pointer to the first rectangle in the list. */ Rectangle* begin() { return rectangles.begin(); } + /** Returns a pointer to the end of the list. */ Rectangle* end() { return rectangles.end(); diff --git a/modules/yup_graphics/primitives/yup_Size.h b/modules/yup_graphics/primitives/yup_Size.h index b4782d081..0680fbe44 100644 --- a/modules/yup_graphics/primitives/yup_Size.h +++ b/modules/yup_graphics/primitives/yup_Size.h @@ -446,6 +446,14 @@ class YUP_API Size return { static_cast (width), static_cast (height) }; } + /** Round the dimensions of this Size object to integers + + Rounds the dimensions of this Size object to integers and returns a new Size object with the rounded dimensions. + + @tparam T The type of the dimensions, constrained to floating-point types. + + @return A new Size object with the rounded dimensions. + */ template constexpr auto roundToInt() const noexcept -> std::enable_if_t, Size> @@ -596,6 +604,15 @@ YUP_API String& YUP_CALLTYPE operator<< (String& string1, const Size& return string1; } +/** Get the coordinate at the specified index + + Returns the coordinate at the specified index. + + @param point The Point to get the coordinate from. + @param I The index of the coordinate to get. + + @return The coordinate at the specified index. +*/ template constexpr ValueType get (const Size& point) noexcept { diff --git a/modules/yup_gui/keyboard/yup_KeyModifiers.h b/modules/yup_gui/keyboard/yup_KeyModifiers.h index bf91fd563..a8441aebf 100644 --- a/modules/yup_gui/keyboard/yup_KeyModifiers.h +++ b/modules/yup_gui/keyboard/yup_KeyModifiers.h @@ -55,7 +55,7 @@ class YUP_API KeyModifiers @return True if Shift is active, false otherwise. */ - constexpr bool isShiftDown() const noexcept + [[nodiscard]] constexpr bool isShiftDown() const noexcept { return modifiers & shiftMask; } @@ -64,7 +64,7 @@ class YUP_API KeyModifiers @return True if Control is active, false otherwise. */ - constexpr bool isControlDown() const noexcept + [[nodiscard]] constexpr bool isControlDown() const noexcept { return modifiers & controlMask; } @@ -73,7 +73,7 @@ class YUP_API KeyModifiers @return True if Command is active, false otherwise. */ - constexpr bool isCommandDown() const noexcept + [[nodiscard]] constexpr bool isCommandDown() const noexcept { return modifiers & commandMask; } @@ -82,7 +82,7 @@ class YUP_API KeyModifiers @return True if Alt is active, false otherwise. */ - constexpr bool isAltDown() const noexcept + [[nodiscard]] constexpr bool isAltDown() const noexcept { return modifiers & altMask; } @@ -91,7 +91,7 @@ class YUP_API KeyModifiers @return True if Super is active, false otherwise. */ - constexpr bool isSuperDown() const noexcept + [[nodiscard]] constexpr bool isSuperDown() const noexcept { return modifiers & superMask; } @@ -100,7 +100,7 @@ class YUP_API KeyModifiers @return True if Caps Lock is active, false otherwise. */ - constexpr bool isCapsLockDown() const noexcept + [[nodiscard]] constexpr bool isCapsLockDown() const noexcept { return modifiers & capsLockMask; } @@ -109,7 +109,7 @@ class YUP_API KeyModifiers @return True if Num Lock is active, false otherwise. */ - constexpr bool isNumLockDown() const noexcept + [[nodiscard]] constexpr bool isNumLockDown() const noexcept { return modifiers & numLockMask; } @@ -119,7 +119,7 @@ class YUP_API KeyModifiers @return The bitmask of active modifiers. */ - constexpr int getFlags() const noexcept + [[nodiscard]] constexpr int getFlags() const noexcept { return modifiers; } @@ -131,7 +131,7 @@ class YUP_API KeyModifiers @return A new KeyModifiers object with the added flags. */ - constexpr KeyModifiers withFlags (int modifiersToAdd) const noexcept + [[nodiscard]] constexpr KeyModifiers withFlags (int modifiersToAdd) const noexcept { return { modifiers | modifiersToAdd }; } @@ -142,7 +142,7 @@ class YUP_API KeyModifiers @return A new KeyModifiers object with the removed flags. */ - constexpr KeyModifiers withoutFlags (int modifiersToRemove) const noexcept + [[nodiscard]] constexpr KeyModifiers withoutFlags (int modifiersToRemove) const noexcept { return { modifiers & ~modifiersToRemove }; } @@ -154,7 +154,7 @@ class YUP_API KeyModifiers @return True if all specified flags are active, false otherwise. */ - constexpr bool testFlags (int modifiersToTest) const noexcept + [[nodiscard]] constexpr bool testFlags (int modifiersToTest) const noexcept { return modifiers & modifiersToTest; } @@ -166,7 +166,7 @@ class YUP_API KeyModifiers @return True if the modifiers are the same, false otherwise. */ - constexpr bool operator== (const KeyModifiers& other) const noexcept + [[nodiscard]] constexpr bool operator== (const KeyModifiers& other) const noexcept { return modifiers == other.modifiers; } @@ -177,7 +177,7 @@ class YUP_API KeyModifiers @return True if the modifiers are not the same, false otherwise. */ - constexpr bool operator!= (const KeyModifiers& other) const noexcept + [[nodiscard]] constexpr bool operator!= (const KeyModifiers& other) const noexcept { return ! (*this == other); } diff --git a/modules/yup_gui/keyboard/yup_KeyPress.h b/modules/yup_gui/keyboard/yup_KeyPress.h index 59e053e3b..71319f7c5 100644 --- a/modules/yup_gui/keyboard/yup_KeyPress.h +++ b/modules/yup_gui/keyboard/yup_KeyPress.h @@ -79,7 +79,7 @@ class YUP_API KeyPress @return The key code. */ - constexpr int getKey() const noexcept + [[nodiscard]] constexpr int getKey() const noexcept { return key; } @@ -89,7 +89,7 @@ class YUP_API KeyPress @return The modifiers. */ - constexpr KeyModifiers getModifiers() const noexcept + [[nodiscard]] constexpr KeyModifiers getModifiers() const noexcept { return modifiers; } @@ -101,7 +101,7 @@ class YUP_API KeyPress @return The Unicode character code. */ - constexpr char32_t getTextCharacter() const noexcept + [[nodiscard]] constexpr char32_t getTextCharacter() const noexcept { return scancode; } @@ -115,7 +115,7 @@ class YUP_API KeyPress @return True if the KeyPress objects are equal, false otherwise. */ - constexpr bool operator== (const KeyPress& other) const noexcept + [[nodiscard]] constexpr bool operator== (const KeyPress& other) const noexcept { return key == other.key && modifiers == other.modifiers && scancode == other.scancode; } @@ -126,7 +126,7 @@ class YUP_API KeyPress @return True if the KeyPress objects are not equal, false otherwise. */ - constexpr bool operator!= (const KeyPress& other) const noexcept + [[nodiscard]] constexpr bool operator!= (const KeyPress& other) const noexcept { return ! (*this == other); } diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 1f1996a6d..a91223a3d 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -123,7 +123,11 @@ class YUP_API PopupMenu @return A pointer to the popup menu. */ - static Ptr create (const Options& options = {}); + [[nodiscard]] static Ptr create (const Options& options = {}); + + //============================================================================== + + [[nodiscard]] const Options& getOptions() const { return options; } //============================================================================== /** Adds a menu item. @@ -156,10 +160,10 @@ class YUP_API PopupMenu //============================================================================== /** Returns the number of items in the menu. */ - int getNumItems() const; + [[nodiscard]] int getNumItems() const; /** Returns true if the menu is empty. */ - bool isEmpty() const { return getNumItems() == 0; } + [[nodiscard]] bool isEmpty() const { return getNumItems() == 0; } /** Clears all items from the menu. */ void clear(); @@ -194,10 +198,10 @@ class YUP_API PopupMenu }; /** Returns an iterator to the first item in the menu. */ - auto begin() const { return items.begin(); } + [[nodiscard]] auto begin() const { return items.begin(); } /** Returns an iterator to the end of the menu. */ - auto end() const { return items.end(); } + [[nodiscard]] auto end() const { return items.end(); } //============================================================================== /** Shows the menu asynchronously and calls the callback when an item is selected. diff --git a/modules/yup_gui/widgets/yup_TextButton.cpp b/modules/yup_gui/widgets/yup_TextButton.cpp index 425875b33..63f961925 100644 --- a/modules/yup_gui/widgets/yup_TextButton.cpp +++ b/modules/yup_gui/widgets/yup_TextButton.cpp @@ -82,7 +82,7 @@ void TextButton::resized() Rectangle TextButton::getTextBounds() const { - return getLocalBounds().reduced (proportionOfWidth (0.04f)); + return getLocalBounds().reduced (proportionOfWidth (0.04f), proportionOfHeight (0.04f)); } } // namespace yup From 8c5e09cfd05d3169d8647b45077719048e2a745c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 26 Jun 2025 00:09:55 +0200 Subject: [PATCH 37/51] More tweaks --- .../yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp index a5be62c58..497b1294d 100644 --- a/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp +++ b/modules/yup_gui/themes/theme_v1/yup_ThemeVersion1.cpp @@ -235,26 +235,28 @@ void paintLabel (Graphics& g, const ApplicationTheme& theme, const Label& l) void paintPopupMenu (Graphics& g, const ApplicationTheme& theme, const PopupMenu& p) { - // Draw drop shadow if enabled - if (false) // owner->options.addAsChildToTopmost) + auto localBounds = p.getLocalBounds(); + + // TODO: Draw drop shadow if enabled + if (false) // (p.getOptions().parentComponent != nullptr) { - auto shadowBounds = p.getLocalBounds().to(); auto shadowRadius = static_cast (8.0f); + localBounds = localBounds.reduced (shadowRadius); g.setFillColor (Color (0, 0, 0)); g.setFeather (shadowRadius); - g.fillRoundedRect (shadowBounds.translated (0.0f, 2.0f).enlarged (2.0f), 4.0f); + g.fillRoundedRect (localBounds.translated (0.0f, 2.0f), 4.0f); g.setFeather (0.0f); } // Draw menu background g.setFillColor (p.findColor (PopupMenu::Colors::menuBackground).value_or (Color (0xff2a2a2a))); - g.fillRoundedRect (p.getLocalBounds().to(), 4.0f); + g.fillRoundedRect (localBounds, 4.0f); // Draw border g.setStrokeColor (p.findColor (PopupMenu::Colors::menuBorder).value_or (Color (0xff555555))); g.setStrokeWidth (1.0f); - g.strokeRoundedRect (p.getLocalBounds().to().reduced (0.5f), 4.0f); + g.strokeRoundedRect (localBounds.reduced (0.5f), 4.0f); // Draw items bool anyItemIsTicked = false; From 2fd15a2b2c5491d943ba24c16e196b55ab262654 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 26 Jun 2025 08:17:51 +0200 Subject: [PATCH 38/51] More work --- examples/graphics/source/examples/PopupMenu.h | 397 ++++++++---------- .../yup_graphics/primitives/yup_Rectangle.h | 192 ++++++++- modules/yup_graphics/primitives/yup_Size.h | 138 ++++-- modules/yup_gui/menus/yup_PopupMenu.cpp | 270 ++++++------ modules/yup_gui/menus/yup_PopupMenu.h | 24 +- 5 files changed, 628 insertions(+), 393 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 92bd24158..ea13347e0 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -28,44 +28,24 @@ class PopupMenuDemo : public yup::Component public: PopupMenuDemo() : Component ("PopupMenuDemo") - , basicMenuButton ("basicMenuButton") - , subMenuButton ("subMenuButton") - , customMenuButton ("customMenuButton") - , nativeMenuButton ("nativeMenuButton") + , targetButton ("targetButton") , statusLabel ("statusLabel") + , currentPlacementIndex (0) { addAndMakeVisible (statusLabel); - statusLabel.setTitle ("Right-click anywhere to show context menu"); + statusLabel.setTitle ("Click the button to test different placements"); - addAndMakeVisible (basicMenuButton); - basicMenuButton.setButtonText ("Show Basic Menu"); - basicMenuButton.onClick = [this] + addAndMakeVisible (targetButton); + targetButton.setButtonText ("Test Placement (Click Me!)"); + targetButton.onClick = [this] { - showBasicMenu(); + showPlacementTest(); }; - addAndMakeVisible (subMenuButton); - subMenuButton.setButtonText ("Show Sub-Menu"); - subMenuButton.onClick = [this] - { - showSubMenu(); - }; + // Initialize all placement combinations + initializePlacements(); - addAndMakeVisible (customMenuButton); - customMenuButton.setButtonText ("Show Custom Menu"); - customMenuButton.onClick = [this] - { - showCustomMenu(); - }; - - addAndMakeVisible (nativeMenuButton); - nativeMenuButton.setButtonText ("Show Native Menu"); - nativeMenuButton.onClick = [this] - { - showNativeMenu(); - }; - - setSize ({ 400, 300 }); + setSize ({ 600, 500 }); } void resized() override @@ -75,10 +55,11 @@ class PopupMenuDemo : public yup::Component area.removeFromTop (20); statusLabel.setBounds (area.removeFromTop (30)); - basicMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); - subMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); - customMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); - nativeMenuButton.setBounds (area.removeFromTop (40).reduced (0, 5)); + // Center the target button in the middle of the remaining area + auto buttonArea = area.reduced (100); + auto buttonCenter = buttonArea.getCenter(); + auto buttonBounds = yup::Rectangle (buttonCenter.getX() - 100, buttonCenter.getY() - 20, 200, 40); + targetButton.setBounds (buttonBounds); } void paint (yup::Graphics& g) override @@ -88,11 +69,46 @@ class PopupMenuDemo : public yup::Component auto styledText = yup::StyledText(); { auto modifier = styledText.startUpdate(); - modifier.appendText ("PopupMenu Demo", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + modifier.appendText ("PopupMenu Placement Test", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); } g.setFillColor (yup::Color (0xffffffff)); g.fillFittedText (styledText, area.removeFromTop (20).to()); + + // Draw grid lines to help visualize positioning + g.setStrokeColor (yup::Color (0x33ffffff)); + g.setStrokeWidth (1.0f); + + auto buttonBounds = targetButton.getBounds().to(); + auto bounds = getLocalBounds().to(); + + // Horizontal lines through button center and edges + g.strokeLine ({ 0, buttonBounds.getCenterY() }, { bounds.getWidth(), buttonBounds.getCenterY() }); + g.strokeLine ({ 0, buttonBounds.getY() }, { bounds.getWidth(), buttonBounds.getY() }); + g.strokeLine ({ 0, buttonBounds.getBottom() }, { bounds.getWidth(), buttonBounds.getBottom() }); + + // Vertical lines through button center and edges + g.strokeLine ({ buttonBounds.getCenterX(), 0 }, { buttonBounds.getCenterX(), bounds.getHeight() }); + g.strokeLine ({ buttonBounds.getX(), 0 }, { buttonBounds.getX(), bounds.getHeight() }); + g.strokeLine ({ buttonBounds.getRight(), 0 }, { buttonBounds.getRight(), bounds.getHeight() }); + } + + void keyDown (const yup::KeyPress& key, const yup::Point& position) override + { + if (key.getKey() == yup::KeyPress::spaceKey || key.getKey() == yup::KeyPress::enterKey) + { + showPlacementTest(); + } + else if (key.getKey() == yup::KeyPress::rightKey) + { + currentPlacementIndex = (currentPlacementIndex + 1) % static_cast(placements.size()); + showPlacementTest(); + } + else if (key.getKey() == yup::KeyPress::leftKey) + { + currentPlacementIndex = (currentPlacementIndex - 1 + static_cast(placements.size())) % static_cast(placements.size()); + showPlacementTest(); + } } void mouseDown (const yup::MouseEvent& event) override @@ -104,243 +120,172 @@ class PopupMenuDemo : public yup::Component } private: - enum MenuItemIDs + struct PlacementTest { - newFile = 1, - openFile, - saveFile, - saveAsFile, - recentFile1, - recentFile2, - exitApp, - - checkedItem = 998, - disabledItem = 999, - - editUndo = 10, - editRedo, - editCut, - editCopy, - editPaste, - - colorRed = 20, - colorGreen, - colorBlue, - - customSlider = 30, - customButton = 31 + yup::PopupMenu::Placement placement; + yup::String description; + + PlacementTest (yup::PopupMenu::Placement p, const yup::String& desc) + : placement (p), description (desc) {} }; - void showBasicMenu() + void initializePlacements() { - auto options = yup::PopupMenu::Options {} - .withParentComponent (this) - .withRelativePosition (&basicMenuButton, yup::PopupMenu::Placement::below); - - auto menu = yup::PopupMenu::create (options); - - menu->addItem ("New File", newFile, true, false, "Cmd+N"); - menu->addItem ("Open File", openFile, true, false, "Cmd+O"); - menu->addSeparator(); - menu->addItem ("Save File", saveFile, true, false, "Cmd+S"); - menu->addItem ("Save As...", saveAsFile, true, false, "Shift+Cmd+S"); - menu->addSeparator(); - menu->addItem ("Disabled Item", disabledItem, false); - menu->addItem ("Checked Item", checkedItem, true, isChecked); - menu->addSeparator(); - menu->addItem ("Exit", exitApp, true, false, "Cmd+Q"); - - menu->onItemSelected = [this] (int selectedID) - { - handleMenuSelection (selectedID); - }; - - menu->show(); + using Side = yup::PopupMenu::Side; + using J = yup::Justification; + using Placement = yup::PopupMenu::Placement; + + placements.clear(); + + // Below placements + placements.emplace_back (Placement::below (J::topLeft), "Below - Left Aligned"); + placements.emplace_back (Placement::below (J::centerTop), "Below - Center Aligned"); + placements.emplace_back (Placement::below (J::topRight), "Below - Right Aligned"); + + // Above placements + placements.emplace_back (Placement::above (J::topLeft), "Above - Left Aligned"); + placements.emplace_back (Placement::above (J::centerTop), "Above - Center Aligned"); + placements.emplace_back (Placement::above (J::topRight), "Above - Right Aligned"); + + // Right placements + placements.emplace_back (Placement::toRight (J::topLeft), "Right - Top Aligned"); + placements.emplace_back (Placement::toRight (J::centerLeft), "Right - Center Aligned"); + placements.emplace_back (Placement::toRight (J::bottomLeft), "Right - Bottom Aligned"); + + // Left placements + placements.emplace_back (Placement::toLeft (J::topRight), "Left - Top Aligned"); + placements.emplace_back (Placement::toLeft (J::centerRight), "Left - Center Aligned"); + placements.emplace_back (Placement::toLeft (J::bottomRight), "Left - Bottom Aligned"); + + // Centered + placements.emplace_back (Placement::centered(), "Centered"); + + // Additional interesting combinations + placements.emplace_back (Placement::below (J::center), "Below - Center (any)"); + placements.emplace_back (Placement::above (J::center), "Above - Center (any)"); + placements.emplace_back (Placement::toRight (J::center), "Right - Center (any)"); + placements.emplace_back (Placement::toLeft (J::center), "Left - Center (any)"); } - void showSubMenu() + void showPlacementTest() { - auto recentFilesMenu = yup::PopupMenu::create(); - recentFilesMenu->addItem ("Recent File 1.txt", recentFile1); - recentFilesMenu->addItem ("Recent File 2.txt", recentFile2); + if (placements.empty()) return; - auto colorMenu = yup::PopupMenu::create(); - colorMenu->addItem ("Red", colorRed); - colorMenu->addItem ("Green", colorGreen); - colorMenu->addItem ("Blue", colorBlue); + auto& test = placements[currentPlacementIndex]; auto options = yup::PopupMenu::Options {} .withParentComponent (this) - .withRelativePosition (&subMenuButton, yup::PopupMenu::Placement::toRight); - auto menu = yup::PopupMenu::create (options); - menu->addItem ("New", newFile); - menu->addItem ("Open", openFile); - menu->addSubMenu ("Recent Files", recentFilesMenu); - menu->addSeparator(); - menu->addSubMenu ("Colors", colorMenu); - menu->addSeparator(); - menu->addItem ("Exit", exitApp); + .withRelativePosition (&targetButton, test.placement); - menu->show([this] (int selectedID) - { - handleMenuSelection (selectedID); - }); - } - - void showCustomMenu() - { - auto options = yup::PopupMenu::Options {} - .withParentComponent (this) - .withRelativePosition (&customMenuButton, yup::PopupMenu::Placement::above); auto menu = yup::PopupMenu::create (options); - menu->addItem ("Regular Item", 1); + // Add items to show menu content clearly + menu->addItem ("Item 1", 1); + menu->addItem ("Item 2", 2); + menu->addItem ("Item 3", 3); menu->addSeparator(); + menu->addItem ("Previous (<)", 998); + menu->addItem ("Next (>)", 999); - // Add custom slider component - auto slider = std::make_unique ("CustomSlider"); - slider->setSize ({ 250, 250 }); - slider->setValue (0.5); - menu->addCustomItem (std::move (slider), customSlider); - - menu->addSeparator(); - - // Add custom button component - auto button = std::make_unique ("CustomButton"); - button->setSize ({ 120, 30 }); - button->setTitle ("Custom Button"); - button->onClick = [] + menu->show ([this, test] (int selectedID) { - YUP_DBG ("Clicked!"); - }; - menu->addCustomItem (std::move (button), customButton); - - menu->addSeparator(); - menu->addItem ("Another Item", 2); - - menu->onItemSelected = [this] (int selectedID) - { - handleMenuSelection (selectedID); - }; + handlePlacementMenuSelection (selectedID, test); + }); - menu->show(); + // Update status + auto statusText = yup::String::formatted ("Test %d/%d: %s", + currentPlacementIndex + 1, + (int)placements.size(), + test.description.toRawUTF8()); + statusLabel.setText (statusText); } - void showNativeMenu() + void showContextMenu (yup::Point position) { auto options = yup::PopupMenu::Options {} - .withParentComponent (this) - .withRelativePosition (&nativeMenuButton, yup::PopupMenu::Placement::centered); + .withPosition (localToScreen (position), yup::Justification::topLeft); - auto menu = yup::PopupMenu::create (options); + auto contextMenu = yup::PopupMenu::create (options); - menu->addItem ("Native Item 1", 1); - menu->addItem ("Native Item 2", 2); - menu->addItem ("Native Item 3", 3); + contextMenu->addItem ("Reset to first test", 1); + contextMenu->addItem ("Show all placements info", 2); + contextMenu->addSeparator(); + contextMenu->addItem ("Toggle grid lines", 3); - menu->onItemSelected = [this] (int selectedID) + contextMenu->show ([this] (int selectedID) { - handleMenuSelection (selectedID); - }; - - menu->show(); + switch (selectedID) + { + case 1: + currentPlacementIndex = 0; + statusLabel.setText ("Reset to first placement test"); + break; + case 2: + showPlacementInfo(); + break; + case 3: + repaint(); // Grid lines are always shown in this demo + break; + } + }); } - void showContextMenu (yup::Point position) + void showPlacementInfo() { auto options = yup::PopupMenu::Options {} .withParentComponent (this) - .withPosition (position, yup::Justification::topLeft); - - auto contextMenu = yup::PopupMenu::create (options); - - contextMenu->addItem ("Context Item 1", 1); - contextMenu->addItem ("Context Item 2", 2); - contextMenu->addSeparator(); - contextMenu->addItem ("Context Item 3", 3); - - - contextMenu->show ([this] (int selectedID) - { - handleMenuSelection (selectedID); + .withRelativePosition (&targetButton, yup::PopupMenu::Placement::centered()); + + auto infoMenu = yup::PopupMenu::create (options); + + infoMenu->addItem (L"Placement System Info:", 0, false); + infoMenu->addSeparator(); + infoMenu->addItem (L"• Side: Primary positioning", 0, false); + infoMenu->addItem (L"• Justification: Alignment", 0, false); + infoMenu->addSeparator(); + infoMenu->addItem (L"Controls:", 0, false); + infoMenu->addItem (L"• Click button: Next test", 0, false); + infoMenu->addItem (L"• ← →: Navigate tests", 0, false); + infoMenu->addItem (L"• Right-click: Context menu", 0, false); + + infoMenu->show ([this] (int selectedID) { + // Info only, no actions }); } - void handleMenuSelection (int selectedID) + void handlePlacementMenuSelection (int selectedID, const PlacementTest& test) { - yup::String message = "Selected item ID: " + yup::String (selectedID); + yup::String message; switch (selectedID) { - case newFile: - message = "New File selected"; - break; - - case openFile: - message = "Open File selected"; - break; - - case saveFile: - message = "Save File selected"; - break; - - case saveAsFile: - message = "Save As selected"; - break; - - case exitApp: - message = "Exit selected"; - break; - - case editCopy: - message = "Copy selected"; - break; - - case editPaste: - message = "Paste selected"; - break; - - case colorRed: - message = "Red color selected"; - break; - - case colorGreen: - message = "Green color selected"; - break; - - case colorBlue: - message = "Blue color selected"; - break; - - case customSlider: - message = "Custom slider interacted"; - break; - - case customButton: - message = "Custom button clicked"; - break; - - case disabledItem: - message = "I'm disabled!"; - break; - - case checkedItem: - message = "I'm checked!"; - isChecked = ! isChecked; + case 998: // Previous + currentPlacementIndex = (currentPlacementIndex - 1 + static_cast(placements.size())) % static_cast(placements.size()); + showPlacementTest(); + return; + + case 999: // Next + currentPlacementIndex = (currentPlacementIndex + 1) % static_cast(placements.size()); + showPlacementTest(); + return; + + case 1: + case 2: + case 3: + message = yup::String::formatted ("Selected Item %d from: %s", selectedID, test.description.toRawUTF8()); break; default: - message = "Cancelled or unknown!"; + message = "No selection"; break; } statusLabel.setText (message); } - yup::TextButton basicMenuButton; - yup::TextButton subMenuButton; - yup::TextButton customMenuButton; - yup::TextButton nativeMenuButton; + yup::TextButton targetButton; yup::Label statusLabel; - bool isChecked = true; + + std::vector placements; + int currentPlacementIndex; }; diff --git a/modules/yup_graphics/primitives/yup_Rectangle.h b/modules/yup_graphics/primitives/yup_Rectangle.h index 243823642..61100c43a 100644 --- a/modules/yup_graphics/primitives/yup_Rectangle.h +++ b/modules/yup_graphics/primitives/yup_Rectangle.h @@ -207,6 +207,18 @@ class YUP_API Rectangle return xy.getX(); } + /** Sets the left-coordinate of the rectangle's top-left corner. + + @param newLeft The new left-coordinate for the top-left corner. + + @return A reference to this rectangle to allow method chaining. + */ + constexpr Rectangle& setLeft (ValueType newLeft) noexcept + { + xy.setX (newLeft); + return *this; + } + /** Returns a new rectangle with the left-coordinate of the top-left corner set to a new value. This method creates a new rectangle with the same size and y-coordinate, but with the left-coordinate of the top-left corner changed to the specified value. @@ -215,9 +227,9 @@ class YUP_API Rectangle @return A new rectangle with the updated left-coordinate. */ - [[nodiscard]] constexpr Rectangle withLeft (ValueType amount) const noexcept + [[nodiscard]] constexpr Rectangle withLeft (ValueType newLeft) const noexcept { - return { xy.withX (amount), size }; + return { xy.withX (newLeft), size }; } /** Returns a new rectangle with the left-coordinate of the top-left corner trimmed by a specified amount. @@ -243,6 +255,18 @@ class YUP_API Rectangle return xy.getY(); } + /** Sets the top-coordinate of the rectangle's top-left corner. + + @param newTop The new top-coordinate for the top-left corner. + + @return A reference to this rectangle to allow method chaining. + */ + constexpr Rectangle& setTop (ValueType newTop) noexcept + { + xy.setY (newTop); + return *this; + } + /** Returns a new rectangle with the top-coordinate of the top-left corner set to a new value. This method creates a new rectangle with the same size and x-coordinate, but with the top-coordinate of the top-left corner changed to the specified value. @@ -251,9 +275,9 @@ class YUP_API Rectangle @return A new rectangle with the updated top-coordinate. */ - [[nodiscard]] constexpr Rectangle withTop (ValueType amount) const noexcept + [[nodiscard]] constexpr Rectangle withTop (ValueType newTop) const noexcept { - return { xy.withY (amount), size }; + return { xy.withY (newTop), size }; } /** Returns a new rectangle with the top-coordinate of the top-left corner trimmed by a specified amount. @@ -279,17 +303,42 @@ class YUP_API Rectangle return xy.getX() + size.getWidth(); } + /** Sets the right-coordinate of the rectangle's bottom-right corner. + + @param newRight The new right-coordinate for the bottom-right corner. + + @return A reference to this rectangle to allow method chaining. + */ + constexpr Rectangle& setRight (ValueType newRight) noexcept + { + xy.setX (newRight - size.getWidth()); + return *this; + } + + /** Returns a new rectangle with the right-coordinate of the bottom-right corner set to a new value. + + This method creates a new rectangle with the same height and y-coordinate, but with the right-coordinate of the bottom-right corner changed to the specified value. + + @param newRight The new right-coordinate for the bottom-right corner. + + @return A new rectangle with the updated right-coordinate. + */ + [[nodiscard]] constexpr Rectangle withRight (ValueType newRight) const noexcept + { + return { xy.withX (newRight - size.getWidth()), size }; + } + /** Returns a new rectangle with the right-coordinate of the bottom-right corner trimmed by a specified amount. - This method creates a new rectangle with the same size and y-coordinate, but with the right-coordinate of the bottom-right corner reduced by the specified amount. + This method creates a new rectangle with the same height and y-coordinate, but with the right-coordinate of the bottom-right corner reduced by the specified amount. @param amountToTrim The amount to trim from the right-coordinate. - @return A new rectangle with the updated right-coordinate. + @return A new rectangle with the updated right-coordinate (width). */ [[nodiscard]] constexpr Rectangle withTrimmedRight (ValueType amountToTrim) const noexcept { - return withWidth (size.getWidth() - amountToTrim); + return withWidth (jmax (static_cast (0), size.getWidth() - amountToTrim)); } //============================================================================== @@ -302,17 +351,42 @@ class YUP_API Rectangle return xy.getY() + size.getHeight(); } + /** Sets the bottom-coordinate of the rectangle's bottom-right corner. + + @param newBottom The new bottom-coordinate for the bottom-right corner. + + @return A reference to this rectangle to allow method chaining. + */ + constexpr Rectangle& setBottom (ValueType newBottom) noexcept + { + xy.setY (newBottom - size.getHeight()); + return *this; + } + + /** Returns a new rectangle with the bottom-coordinate of the bottom-right corner set to a new value. + + This method creates a new rectangle with the same width and x-coordinate, but with the bottom-coordinate of the bottom-right corner changed to the specified value. + + @param newBottom The new bottom-coordinate for the bottom-right corner. + + @return A new rectangle with the updated bottom-coordinate. + */ + [[nodiscard]] constexpr Rectangle withBottom (ValueType newBottom) const noexcept + { + return { xy.withY (newBottom - size.getHeight()), size }; + } + /** Returns a new rectangle with the bottom-coordinate of the bottom-right corner trimmed by a specified amount. - This method creates a new rectangle with the same size and x-coordinate, but with the bottom-coordinate of the bottom-right corner reduced by the specified amount. + This method creates a new rectangle with the same width and x-coordinate, but with the bottom-coordinate of the bottom-right corner reduced by the specified amount. @param amountToTrim The amount to trim from the bottom-coordinate. - @return A new rectangle with the updated bottom-coordinate. + @return A new rectangle with the updated bottom-coordinate (height). */ [[nodiscard]] constexpr Rectangle withTrimmedBottom (ValueType amountToTrim) const noexcept { - return withHeight (size.getHeight() - amountToTrim); + return withHeight (jmax (static_cast (0), size.getHeight() - amountToTrim)); } //============================================================================== @@ -351,6 +425,21 @@ class YUP_API Rectangle return { xy, size.withWidth (newWidth) }; } + /** Returns a new rectangle with the width set to a new value while maintaining the aspect ratio. + + This method creates a new rectangle with the same position but changes the width to the specified value while keeping the aspect ratio. + The height is adjusted to maintain the same aspect ratio. + + @param newWidth The new width for the rectangle. + + @return A new rectangle with the updated width while maintaining the aspect ratio. + */ + [[nodiscard]] constexpr Rectangle withWidthKeepingAspectRatio (ValueType newWidth) const noexcept + { + auto deltaRatio = heightOverWidthRatio(); + return { xy, size.withWidth (newWidth).withHeight (newWidth * deltaRatio) }; + } + /** Returns a new rectangle with the width set to a specified proportion of the original width. This method creates a new rectangle with the same position but changes the width to a specified proportion of the original width. @@ -401,6 +490,21 @@ class YUP_API Rectangle return { xy, size.withHeight (newHeight) }; } + /** Returns a new rectangle with the height set to a new value while maintaining the aspect ratio. + + This method creates a new rectangle with the same position but changes the height to the specified value while keeping the aspect ratio. + The width is adjusted to maintain the same aspect ratio. + + @param newHeight The new height for the rectangle. + + @return A new rectangle with the updated height while maintaining the aspect ratio. + */ + [[nodiscard]] constexpr Rectangle withHeightKeepingAspectRatio (ValueType newHeight) const noexcept + { + auto deltaRatio = widthOverHeightRatio(); + return { xy, size.withWidth (newHeight * deltaRatio).withHeight (newHeight) }; + } + /** Returns a new rectangle with the height set to a specified proportion of the original height. This method creates a new rectangle with the same position but changes the height to a specified proportion of the original height. @@ -1535,6 +1639,17 @@ class YUP_API Rectangle return contains (p.getX(), p.getY()); } + /** Checks if the specified line lies within the bounds of the rectangle. + + @param l The line to check. + + @return True if the line is within the rectangle, otherwise false. + */ + [[nodiscard]] constexpr bool contains (const Line& l) const noexcept + { + return contains (l.getStart()) && contains (l.getEnd()); + } + /** Checks if the specified rectangle lies within the bounds of this rectangle. @param p The rectangle to check. @@ -1543,8 +1658,7 @@ class YUP_API Rectangle */ [[nodiscard]] constexpr bool contains (const Rectangle& p) const noexcept { - return p.getX() >= xy.getX() - && p.getY() >= xy.getY() + return contains (p.getTopLeft()) && p.getRight() <= (xy.getX() + size.getWidth()) && p.getBottom() <= (xy.getY() + size.getHeight()); } @@ -1559,6 +1673,38 @@ class YUP_API Rectangle return size.area(); } + //============================================================================== + + /** Returns the aspect ratio of the rectangle. + + @return A tuple containing the width and height of the rectangle, simplified to their greatest common factor. + */ + template + [[nodiscard]] constexpr auto aspectRatio() const noexcept + -> std::enable_if_t, std::tuple> + { + auto factor = std::gcd (size.getWidth(), size.getHeight()); + return std::make_tuple (size.getWidth() / factor, size.getHeight() / factor); + } + + /** Returns the ratio of the width to the height of the rectangle. + + @return The ratio of the width to the height of the rectangle. + */ + [[nodiscard]] constexpr float widthOverHeightRatio() const noexcept + { + return static_cast (size.getWidth()) / static_cast (size.getHeight()); + } + + /** Returns the ratio of the height to the width of the rectangle. + + @return The ratio of the height to the width of the rectangle. + */ + [[nodiscard]] constexpr float heightOverWidthRatio() const noexcept + { + return static_cast (size.getHeight()) / static_cast (size.getWidth()); + } + //============================================================================== /** Checks if this rectangle intersects with another rectangle. @@ -1933,6 +2079,28 @@ constexpr ValueType get (const Rectangle& point) noexcept static_assert (dependentFalse); } +/** Forwarded methods implementation. */ +template +template +[[nodiscard]] constexpr Rectangle Size::toRectangle() const noexcept +{ + return { static_cast (0), static_cast (0), *this }; +} + +template +template +[[nodiscard]] constexpr Rectangle Size::toRectangle (T x, T y) const noexcept +{ + return { x, y, *this }; +} + +template +template +[[nodiscard]] constexpr Rectangle Size::toRectangle (Point xy) const noexcept +{ + return { xy, *this }; +} + } // namespace yup namespace std diff --git a/modules/yup_graphics/primitives/yup_Size.h b/modules/yup_graphics/primitives/yup_Size.h index 0680fbe44..4991143a4 100644 --- a/modules/yup_graphics/primitives/yup_Size.h +++ b/modules/yup_graphics/primitives/yup_Size.h @@ -22,6 +22,9 @@ namespace yup { +template +class YUP_API Rectangle; + //============================================================================== /** Represents a 2D size with width and height. @@ -68,7 +71,7 @@ class YUP_API Size @return The current width. */ - constexpr ValueType getWidth() const noexcept + [[nodiscard]] constexpr ValueType getWidth() const noexcept { return width; } @@ -95,7 +98,7 @@ class YUP_API Size @return A new size object with the specified width and current height. */ - constexpr Size withWidth (ValueType newWidth) const noexcept + [[nodiscard]] constexpr Size withWidth (ValueType newWidth) const noexcept { return { newWidth, height }; } @@ -107,7 +110,7 @@ class YUP_API Size @return The current height. */ - constexpr ValueType getHeight() const noexcept + [[nodiscard]] constexpr ValueType getHeight() const noexcept { return height; } @@ -134,7 +137,7 @@ class YUP_API Size @return A new size object with the current width and specified height. */ - constexpr Size withHeight (ValueType newHeight) const noexcept + [[nodiscard]] constexpr Size withHeight (ValueType newHeight) const noexcept { return { width, newHeight }; } @@ -146,7 +149,7 @@ class YUP_API Size @return True if both width and height are zero, false otherwise. */ - constexpr bool isZero() const noexcept + [[nodiscard]] constexpr bool isZero() const noexcept { return width == ValueType (0) && height == ValueType (0); } @@ -157,7 +160,7 @@ class YUP_API Size @return True if either width or height is zero, false otherwise. */ - constexpr bool isEmpty() const noexcept + [[nodiscard]] constexpr bool isEmpty() const noexcept { return width == ValueType (0) || height == ValueType (0); } @@ -168,7 +171,7 @@ class YUP_API Size @return True if height is zero and width is not, false otherwise. */ - constexpr bool isVerticallyEmpty() const noexcept + [[nodiscard]] constexpr bool isVerticallyEmpty() const noexcept { return width != ValueType (0) && height == ValueType (0); } @@ -179,7 +182,7 @@ class YUP_API Size @return True if width is zero and height is not, false otherwise. */ - constexpr bool isHorizontallyEmpty() const noexcept + [[nodiscard]] constexpr bool isHorizontallyEmpty() const noexcept { return width == ValueType (0) && height != ValueType (0); } @@ -191,7 +194,7 @@ class YUP_API Size @return True if the size is a square, false otherwise. */ - constexpr bool isSquare() const noexcept + [[nodiscard]] constexpr bool isSquare() const noexcept { return width == height; } @@ -203,7 +206,7 @@ class YUP_API Size @return The area calculated as width multiplied by height. */ - constexpr ValueType area() const noexcept + [[nodiscard]] constexpr ValueType area() const noexcept { return width * height; } @@ -230,7 +233,7 @@ class YUP_API Size @return A new Size object with reversed dimensions. */ - constexpr Size reversed() const noexcept + [[nodiscard]] constexpr Size reversed() const noexcept { Size result (*this); result.reverse(); @@ -275,7 +278,7 @@ class YUP_API Size @return A new Size object with enlarged dimensions. */ - constexpr Size enlarged (ValueType amount) const noexcept + [[nodiscard]] constexpr Size enlarged (ValueType amount) const noexcept { Size result (*this); result.enlarge (amount); @@ -291,7 +294,7 @@ class YUP_API Size @return A new Size object with enlarged dimensions. */ - constexpr Size enlarged (ValueType widthAmount, ValueType heightAmount) const noexcept + [[nodiscard]] constexpr Size enlarged (ValueType widthAmount, ValueType heightAmount) const noexcept { Size result (*this); result.enlarge (widthAmount, heightAmount); @@ -338,7 +341,7 @@ class YUP_API Size @return A new Size object with reduced dimensions. */ - constexpr Size reduced (ValueType amount) const noexcept + [[nodiscard]] constexpr Size reduced (ValueType amount) const noexcept { Size result (*this); result.reduce (amount); @@ -354,7 +357,7 @@ class YUP_API Size @return A new Size object with reduced dimensions. */ - constexpr Size reduced (ValueType widthAmount, ValueType heightAmount) const noexcept + [[nodiscard]] constexpr Size reduced (ValueType widthAmount, ValueType heightAmount) const noexcept { Size result (*this); result.reduce (widthAmount, heightAmount); @@ -371,7 +374,7 @@ class YUP_API Size @return A reference to this updated Size object. */ template - constexpr auto scale (T scaleFactor) noexcept + [[nodiscard]] constexpr auto scale (T scaleFactor) noexcept -> std::enable_if_t, Size&> { scale (scaleFactor, scaleFactor); @@ -405,7 +408,7 @@ class YUP_API Size @return A new Size object with scaled dimensions. */ template - constexpr auto scaled (T scaleFactor) const noexcept + [[nodiscard]] constexpr auto scaled (T scaleFactor) const noexcept -> std::enable_if_t, Size> { Size result (*this); @@ -423,7 +426,7 @@ class YUP_API Size @return A new Size object with scaled dimensions. */ template - constexpr auto scaled (T scaleFactorX, T scaleFactorY) const noexcept + [[nodiscard]] constexpr auto scaled (T scaleFactorX, T scaleFactorY) const noexcept -> std::enable_if_t, Size> { Size result (*this); @@ -441,7 +444,7 @@ class YUP_API Size @return A new Size object with dimensions converted to type T. */ template - constexpr Size to() const noexcept + [[nodiscard]] constexpr Size to() const noexcept { return { static_cast (width), static_cast (height) }; } @@ -455,12 +458,65 @@ class YUP_API Size @return A new Size object with the rounded dimensions. */ template - constexpr auto roundToInt() const noexcept + [[nodiscard]] auto roundToInt() const noexcept -> std::enable_if_t, Size> { return { yup::roundToInt (width), yup::roundToInt (height) }; } + //============================================================================== + /** Convert Size to Point + + Converts the dimensions of this Size object to a Point object and returns a new Point object with the converted dimensions. + + @tparam T The type of the dimensions, constrained to numeric types. + + @return A new Point object with the converted dimensions. + */ + template + [[nodiscard]] constexpr Point toPoint() const noexcept + { + return { static_cast (width), static_cast (height) }; + } + + /** Convert Size to Rectangle + + Converts the dimensions of this Size object to a Rectangle object and returns a new Rectangle object with the converted dimensions. + + @tparam T The type of the dimensions, constrained to numeric types. + + @return A new Rectangle object with the converted dimensions. + */ + template + [[nodiscard]] constexpr Rectangle toRectangle() const noexcept; + + /** Convert Size to Rectangle with specified coordinates + + Converts the dimensions of this Size object to a Rectangle object with specified coordinates and returns a new Rectangle object with the converted dimensions. + + @tparam T The type of the dimensions, constrained to numeric types. + + @param x The x-coordinate of the top-left corner of the Rectangle. + @param y The y-coordinate of the top-left corner of the Rectangle. + + @return A new Rectangle object with the converted dimensions. + */ + template + [[nodiscard]] constexpr Rectangle toRectangle (T x, T y) const noexcept; + + /** Convert Size to Rectangle with specified coordinates + + Converts the dimensions of this Size object to a Rectangle object with specified coordinates and returns a new Rectangle object with the converted dimensions. + + @tparam T The type of the dimensions, constrained to numeric types. + + @param xy The Point object containing the x and y coordinates of the top-left corner of the Rectangle. + + @return A new Rectangle object with the converted dimensions. + */ + template + [[nodiscard]] constexpr Rectangle toRectangle (Point xy) const noexcept; + //============================================================================== /** Multiplication operator @@ -473,8 +529,7 @@ class YUP_API Size @return A new Size object with dimensions scaled by the given factor. */ template - constexpr auto operator* (T scaleFactor) const noexcept - -> std::enable_if_t, Size> + [[nodiscard]] constexpr Size operator* (T scaleFactor) const noexcept { Size result (*this); result *= scaleFactor; @@ -500,6 +555,25 @@ class YUP_API Size return *this; } + /** Multiplication assignment operator + + Multiplies the dimensions of this Size object by a scale factor and updates this Size object. + + @tparam T The type of the scale factor. + + @param scaleFactor The scale factor to multiply the dimensions by. + + @return A reference to this updated Size object. + */ + template + constexpr auto operator*= (T scaleFactor) noexcept + -> std::enable_if_t, Size&> + { + width = static_cast (width * static_cast (scaleFactor)); + height = static_cast (height * static_cast (scaleFactor)); + return *this; + } + /** Division operator Divides the dimensions of this Size object by a scale factor and returns a new Size object with the scaled dimensions. @@ -511,8 +585,7 @@ class YUP_API Size @return A new Size object with dimensions scaled down by the given factor. */ template - constexpr auto operator/ (T scaleFactor) const noexcept - -> std::enable_if_t, Size> + [[nodiscard]] constexpr Size operator/ (T scaleFactor) const noexcept { Size result (*this); result /= scaleFactor; @@ -538,6 +611,23 @@ class YUP_API Size return *this; } + /** Division assignment operator + + Divides the dimensions of this Size object by a scale factor and updates this Size object. + + @tparam T The type of the scale factor. + + @param scaleFactor The scale factor to divide the dimensions by. + */ + template + constexpr auto operator/= (T scaleFactor) noexcept + -> std::enable_if_t, Size&> + { + width = static_cast (width / static_cast (scaleFactor)); + height = static_cast (height / static_cast (scaleFactor)); + return *this; + } + //============================================================================== /** Returns true if the two sizes are approximately equal. */ constexpr bool approximatelyEqualTo (const Size& other) const noexcept diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 9f0460578..d9acf4cf7 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -22,13 +22,54 @@ namespace yup { -//============================================================================== - namespace { +//============================================================================== + static std::vector activePopups; +void installGlobalMouseListener() +{ + static bool mouseListenerAdded = [] + { + static struct GlobalMouseListener : MouseListener + { + void mouseDown (const MouseEvent& event) override + { + Point globalPos = event.getScreenPosition().to(); + + bool clickedInsidePopup = false; + for (const auto& popup : activePopups) + { + if (auto* popupMenu = dynamic_cast (popup.get())) + { + if (popupMenu->getScreenBounds().contains (globalPos)) + { + clickedInsidePopup = true; + break; + } + } + } + + if (! clickedInsidePopup && ! activePopups.empty()) + PopupMenu::dismissAllPopups(); + } + } globalMouseListener {}; + + Desktop::getInstance()->addGlobalMouseListener (&globalMouseListener); + + MessageManager::getInstance()->registerShutdownCallback ([] + { + PopupMenu::dismissAllPopups(); + }); + + return true; + }(); +} + +//============================================================================== + Point calculatePositionAtPoint (Point targetPoint, Size menuSize, Justification alignment) { Point position = targetPoint; @@ -53,7 +94,7 @@ Point calculatePositionAtPoint (Point targetPoint, Size menuSize, break; case Justification::center: - position = targetPoint - Point{ menuSize.getWidth() / 2, menuSize.getHeight() / 2 }; + position = targetPoint - (menuSize / 2).toPoint(); break; case Justification::centerRight: @@ -71,44 +112,95 @@ Point calculatePositionAtPoint (Point targetPoint, Size menuSize, break; case Justification::bottomRight: - position = targetPoint - Point{ menuSize.getWidth(), menuSize.getHeight() }; + position = targetPoint - menuSize.toPoint(); break; } return position; } +//============================================================================== + Point calculatePositionRelativeToArea (Rectangle targetArea, Size menuSize, PopupMenu::Placement placement) { Point position; - switch (placement) + // Handle special case first + if (placement.side == PopupMenu::Side::centered) { - default: - case PopupMenu::Placement::below: - position = Point (targetArea.getX(), targetArea.getBottom()); + return targetArea.getCenter() - (menuSize / 2).toPoint(); + } + + // Set position based on side (primary axis) + switch (placement.side) + { + case PopupMenu::Side::below: + position.setY (targetArea.getBottom()); break; - case PopupMenu::Placement::above: - position = Point (targetArea.getX(), targetArea.getY() - menuSize.getHeight()); + case PopupMenu::Side::above: + position.setY (targetArea.getY() - menuSize.getHeight()); break; - case PopupMenu::Placement::toRight: - position = Point (targetArea.getRight(), targetArea.getY()); + case PopupMenu::Side::toRight: + position.setX (targetArea.getRight()); break; - case PopupMenu::Placement::toLeft: - position = Point (targetArea.getX() - menuSize.getWidth(), targetArea.getY()); + case PopupMenu::Side::toLeft: + position.setX (targetArea.getX() - menuSize.getWidth()); break; - case PopupMenu::Placement::centered: - position = targetArea.getCenter() - Point{ menuSize.getWidth() / 2, menuSize.getHeight() / 2 };; + default: break; } + // Set alignment on perpendicular axis (secondary axis) + if (placement.side == PopupMenu::Side::below || placement.side == PopupMenu::Side::above) + { + // For above/below: align horizontally + if (placement.alignment == Justification::centerTop || + placement.alignment == Justification::center || + placement.alignment == Justification::centerBottom) + { + position.setX (targetArea.getCenterX() - menuSize.getWidth() / 2); + } + else if (placement.alignment == Justification::topRight || + placement.alignment == Justification::centerRight || + placement.alignment == Justification::bottomRight) + { + position.setX (targetArea.getRight() - menuSize.getWidth()); + } + else // Default: left-aligned + { + position.setX (targetArea.getX()); + } + } + else if (placement.side == PopupMenu::Side::toLeft || placement.side == PopupMenu::Side::toRight) + { + // For left/right: align vertically + if (placement.alignment == Justification::centerLeft || + placement.alignment == Justification::center || + placement.alignment == Justification::centerRight) + { + position.setY (targetArea.getCenterY() - menuSize.getHeight() / 2); + } + else if (placement.alignment == Justification::bottomLeft || + placement.alignment == Justification::centerBottom || + placement.alignment == Justification::bottomRight) + { + position.setY (targetArea.getBottom() - menuSize.getHeight()); + } + else // Default: top-aligned + { + position.setY (targetArea.getY()); + } + } + return position; } +//============================================================================== + Point constrainPositionToAvailableArea (Point desiredPosition, const Size& menuSize, const Rectangle& availableArea, @@ -120,69 +212,32 @@ Point constrainPositionToAvailableArea (Point desiredPosition, Point position = desiredPosition; - // Check if menu fits in desired position + // Only make minimal adjustments to keep menu visible + // Don't override the placement strategy, just nudge the menu if needed Rectangle menuBounds (position, menuSize); - // If menu doesn't fit, try alternative positions - if (! constrainedArea.contains (menuBounds)) + // Horizontal constraint - only adjust if menu goes off screen + if (menuBounds.getRight() > constrainedArea.getRight()) { - // Try to keep menu fully visible by adjusting position - - // Horizontal adjustment - if (menuBounds.getRight() > constrainedArea.getRight()) - { - // Try moving left - position.setX (constrainedArea.getRight() - menuSize.getWidth()); - - // If that puts us over the target, try positioning on the left side - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setX (targetArea.getX() - menuSize.getWidth()); - } - } - else if (menuBounds.getX() < constrainedArea.getX()) - { - // Try moving right - position.setX (constrainedArea.getX()); - - // If that puts us over the target, try positioning on the right side - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setX (targetArea.getRight()); - } - } - - // Vertical adjustment - if (menuBounds.getBottom() > constrainedArea.getBottom()) - { - // Try moving up - position.setY (constrainedArea.getBottom() - menuSize.getHeight()); - - // If that puts us over the target, try positioning above - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setY (targetArea.getY() - menuSize.getHeight()); - } - } - else if (menuBounds.getY() < constrainedArea.getY()) - { - // Try moving down - position.setY (constrainedArea.getY()); - - // If that puts us over the target, try positioning below - if (Rectangle (position, menuSize).intersects (targetArea)) - { - position.setY (targetArea.getBottom()); - } - } + // Move left just enough to fit + position.setX (constrainedArea.getRight() - menuSize.getWidth()); + } + else if (menuBounds.getX() < constrainedArea.getX()) + { + // Move right just enough to fit + position.setX (constrainedArea.getX()); + } - // Final bounds check - ensure we're at least partially visible - position.setX (jlimit (constrainedArea.getX(), - jmax (constrainedArea.getX(), constrainedArea.getRight() - menuSize.getWidth()), - position.getX())); - position.setY (jlimit (constrainedArea.getY(), - jmax (constrainedArea.getY(), constrainedArea.getBottom() - menuSize.getHeight()), - position.getY())); + // Vertical constraint - only adjust if menu goes off screen + if (menuBounds.getBottom() > constrainedArea.getBottom()) + { + // Move up just enough to fit + position.setY (constrainedArea.getBottom() - menuSize.getHeight()); + } + else if (menuBounds.getY() < constrainedArea.getY()) + { + // Move down just enough to fit + position.setY (constrainedArea.getY()); } return position; @@ -232,58 +287,11 @@ bool PopupMenu::Item::isCustomComponent() const //============================================================================== -namespace -{ - -struct GlobalMouseListener : public MouseListener -{ - void mouseDown (const MouseEvent& event) override - { - Point globalPos = event.getScreenPosition().to(); - - bool clickedInsidePopup = false; - for (const auto& popup : activePopups) - { - if (auto* popupMenu = dynamic_cast (popup.get())) - { - if (popupMenu->getScreenBounds().contains (globalPos)) - { - clickedInsidePopup = true; - break; - } - } - } - - if (! clickedInsidePopup && ! activePopups.empty()) - PopupMenu::dismissAllPopups(); - } -}; - -void installGlobalMouseListener() -{ - static bool mouseListenerAdded = [] - { - static GlobalMouseListener globalMouseListener {}; - Desktop::getInstance()->addGlobalMouseListener (&globalMouseListener); - - MessageManager::getInstance()->registerShutdownCallback ([] - { - PopupMenu::dismissAllPopups(); - }); - - return true; - }(); -} - -} // namespace - -//============================================================================== - PopupMenu::Options::Options() : parentComponent (nullptr) , dismissOnSelection (true) , alignment (Justification::topLeft) - , placement (Placement::below) + , placement (Placement::below()) , positioningMode (PositioningMode::atPoint) , targetComponent (nullptr) { @@ -569,12 +577,21 @@ void PopupMenu::positionMenu() // Get target component bounds in appropriate coordinate system if (options.parentComponent) { - // Convert to parent component's local coordinates - targetArea = options.parentComponent->getLocalArea (options.targetComponent, options.targetComponent->getLocalBounds()).to(); + // Check if target is a direct child of parent + if (options.targetComponent->getParentComponent() == options.parentComponent) + { + // Target is direct child - use its bounds directly + targetArea = options.targetComponent->getBounds().to(); + } + else + { + // Target is not a direct child - need coordinate conversion + targetArea = options.parentComponent->getLocalArea (options.targetComponent, options.targetComponent->getLocalBounds()).to(); + } } else { - // Use screen coordinates + // No parent component - use screen coordinates targetArea = options.targetComponent->getScreenBounds().to(); } position = calculatePositionRelativeToArea (targetArea, menuSize, options.placement); @@ -645,6 +662,7 @@ void PopupMenu::showCustom (const Options& options, std::function ca // When we have no parent component, add to desktop to work in screen coordinates auto nativeOptions = ComponentNative::Options {} .withDecoration (false) + .withClearColor (::yup::Colors::transparentBlack) .withResizableWindow (false); addToDesktop (nativeOptions); diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index a91223a3d..38ab27e6c 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -39,7 +39,7 @@ class YUP_API PopupMenu //============================================================================== /** Menu positioning relative to rectangles/components */ - enum class Placement + enum class Side { above, //< Menu appears above the target below, //< Menu appears below the target (default) @@ -48,6 +48,21 @@ class YUP_API PopupMenu centered //< Menu is centered on the target }; + struct Placement + { + Side side = Side::below; + Justification alignment = Justification::topLeft; + + Placement() = default; + Placement (Side s, Justification align = Justification::topLeft) : side (s), alignment (align) {} + + static Placement below (Justification align = Justification::topLeft) { return { Side::below, align }; } + static Placement above (Justification align = Justification::topLeft) { return { Side::above, align }; } + static Placement toRight (Justification align = Justification::topLeft) { return { Side::toRight, align }; } + static Placement toLeft (Justification align = Justification::topLeft) { return { Side::toLeft, align }; } + static Placement centered() { return { Side::centered, Justification::center }; } + }; + enum class PositioningMode { atPoint, @@ -65,7 +80,6 @@ class YUP_API PopupMenu When not set, menu appears as desktop window using screen coordinates. */ Options& withParentComponent (Component* parentComponent); - /** Position menu at a specific point. - With parent: point is relative to parent component - Without parent: point is in screen coordinates @@ -83,8 +97,8 @@ class YUP_API PopupMenu @param area The rectangle to position relative to @param placement Where to place menu relative to rectangle (default: below the rectangle) */ - Options& withTargetArea (Rectangle area, Placement placement = Placement::below); - Options& withTargetArea (Rectangle area, Placement placement = Placement::below); + Options& withTargetArea (Rectangle area, Placement placement = Placement::below()); + Options& withTargetArea (Rectangle area, Placement placement = Placement::below()); /** Position menu relative to a component (uses the component's bounds). The component must be a child of the parent component (if parent is set). @@ -92,7 +106,7 @@ class YUP_API PopupMenu @param component The component to position relative to @param placement Where to place menu relative to component (default: below) */ - Options& withRelativePosition (Component* component, Placement placement = Placement::below); + Options& withRelativePosition (Component* component, Placement placement = Placement::below()); /** Minimum width for the menu. */ Options& withMinimumWidth (int minWidth); From 1616bf6a7788356620c40b563ae34d661812d903 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 26 Jun 2025 10:05:39 +0200 Subject: [PATCH 39/51] More work --- examples/graphics/source/examples/PopupMenu.h | 71 +++- modules/yup_events/timers/yup_TimedCallback.h | 43 +- .../yup_graphics/graphics/yup_Graphics.cpp | 9 +- modules/yup_graphics/graphics/yup_Graphics.h | 3 + modules/yup_gui/menus/yup_PopupMenu.cpp | 400 +++++++++++++++++- modules/yup_gui/menus/yup_PopupMenu.h | 40 ++ 6 files changed, 537 insertions(+), 29 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index ea13347e0..f9ee17c45 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -33,7 +33,7 @@ class PopupMenuDemo : public yup::Component , currentPlacementIndex (0) { addAndMakeVisible (statusLabel); - statusLabel.setTitle ("Click the button to test different placements"); + statusLabel.setTitle ("Click the button to test placements. Right-click for submenus and scrollable menus."); addAndMakeVisible (targetButton); targetButton.setButtonText ("Test Placement (Click Me!)"); @@ -69,7 +69,7 @@ class PopupMenuDemo : public yup::Component auto styledText = yup::StyledText(); { auto modifier = styledText.startUpdate(); - modifier.appendText ("PopupMenu Placement Test", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); + modifier.appendText ("PopupMenu Features: Placement, Submenus, Scrolling", yup::ApplicationTheme::getGlobalTheme()->getDefaultFont()); } g.setFillColor (yup::Color (0xffffffff)); @@ -184,6 +184,24 @@ class PopupMenuDemo : public yup::Component menu->addItem ("Item 2", 2); menu->addItem ("Item 3", 3); menu->addSeparator(); + + // Add a small submenu as well + auto quickSubmenu = yup::PopupMenu::create(); + quickSubmenu->addItem ("Quick Action 1", 501); + quickSubmenu->addItem ("Quick Action 2", 502); + menu->addSubMenu ("More Actions", std::move (quickSubmenu)); + + auto scrollableMenu = yup::PopupMenu::create(); + for (int i = 1; i <= 25; ++i) + { + scrollableMenu->addItem (yup::String::formatted ("Scroll Item %d", i), 400 + i); + if (i % 5 == 0) + scrollableMenu->addSeparator(); + } + menu->addSubMenu ("Scrollable Menu", std::move (scrollableMenu)); + + menu->addSeparator(); + menu->addItem ("Previous (<)", 998); menu->addItem ("Next (>)", 999); @@ -210,6 +228,34 @@ class PopupMenuDemo : public yup::Component contextMenu->addItem ("Reset to first test", 1); contextMenu->addItem ("Show all placements info", 2); contextMenu->addSeparator(); + + // Add submenu example + auto submenu = yup::PopupMenu::create(); + submenu->addItem ("Submenu Item 1", 201); + submenu->addItem ("Submenu Item 2", 202); + submenu->addSeparator(); + + // Create nested submenu to demonstrate recursive submenus + auto nestedSubmenu = yup::PopupMenu::create(); + nestedSubmenu->addItem ("Nested Item 1", 301); + nestedSubmenu->addItem ("Nested Item 2", 302); + nestedSubmenu->addItem ("Nested Item 3", 303); + + submenu->addSubMenu ("Nested Menu", std::move (nestedSubmenu)); + submenu->addItem ("Submenu Item 3", 203); + + contextMenu->addSubMenu ("Submenu Example", std::move (submenu)); + + // Add scrollable menu example + auto scrollableMenu = yup::PopupMenu::create(); + for (int i = 1; i <= 25; ++i) + { + scrollableMenu->addItem (yup::String::formatted ("Scroll Item %d", i), 400 + i); + if (i % 5 == 0) + scrollableMenu->addSeparator(); + } + + contextMenu->addSubMenu ("Scrollable Menu (25 items)", std::move (scrollableMenu)); contextMenu->addItem ("Toggle grid lines", 3); contextMenu->show ([this] (int selectedID) @@ -226,6 +272,13 @@ class PopupMenuDemo : public yup::Component case 3: repaint(); // Grid lines are always shown in this demo break; + default: + if (selectedID >= 200) + { + auto text = yup::String::formatted ("Selected submenu item ID: %d", selectedID); + statusLabel.setText (text); + } + break; } }); } @@ -238,15 +291,16 @@ class PopupMenuDemo : public yup::Component auto infoMenu = yup::PopupMenu::create (options); - infoMenu->addItem (L"Placement System Info:", 0, false); + infoMenu->addItem (L"PopupMenu Features:", 0, false); infoMenu->addSeparator(); - infoMenu->addItem (L"• Side: Primary positioning", 0, false); - infoMenu->addItem (L"• Justification: Alignment", 0, false); + infoMenu->addItem (L"• Placement: Side + Justification", 0, false); + infoMenu->addItem (L"• Submenus: Hover to show", 0, false); + infoMenu->addItem (L"• Scrolling: Mouse wheel support", 0, false); infoMenu->addSeparator(); infoMenu->addItem (L"Controls:", 0, false); infoMenu->addItem (L"• Click button: Next test", 0, false); infoMenu->addItem (L"• ← →: Navigate tests", 0, false); - infoMenu->addItem (L"• Right-click: Context menu", 0, false); + infoMenu->addItem (L"• Right-click: Feature demo", 0, false); infoMenu->show ([this] (int selectedID) { // Info only, no actions @@ -275,6 +329,11 @@ class PopupMenuDemo : public yup::Component message = yup::String::formatted ("Selected Item %d from: %s", selectedID, test.description.toRawUTF8()); break; + case 501: + case 502: + message = yup::String::formatted ("Selected submenu action %d from: %s", selectedID, test.description.toRawUTF8()); + break; + default: message = "No selection"; break; diff --git a/modules/yup_events/timers/yup_TimedCallback.h b/modules/yup_events/timers/yup_TimedCallback.h index 367dcdff4..4069ff59a 100644 --- a/modules/yup_events/timers/yup_TimedCallback.h +++ b/modules/yup_events/timers/yup_TimedCallback.h @@ -52,15 +52,40 @@ namespace yup class TimedCallback final : private Timer { public: - /** Constructor. The passed in callback must be non-null. */ - explicit TimedCallback (std::function callbackIn) - : callback (std::move (callbackIn)) + /** Constructor. + + The passed in callback won't be set but must be then set before starting the timer. + + @see onTimer + */ + TimedCallback() = default; + + /** Constructor. + + The passed in callback must be non-null. + */ + explicit TimedCallback (std::function timerCallback) + : onTimer (std::move (timerCallback)) { - jassert (callback); + jassert (onTimer != nullptr); } + /** Constructor. + + The passed in callback can be null but must be then set before starting the timer. + + @see onTimer + */ + explicit TimedCallback (std::nullptr_t) = delete; + /** Destructor. */ - ~TimedCallback() noexcept override { stopTimer(); } + ~TimedCallback() noexcept override + { + stopTimer(); + } + + /** Timer callback. */ + std::function onTimer; using Timer::getTimerInterval; using Timer::isTimerRunning; @@ -69,9 +94,13 @@ class TimedCallback final : private Timer using Timer::stopTimer; private: - void timerCallback() override { callback(); } + void timerCallback() override + { + jassert (onTimer != nullptr); // Did you forgot to set a timer callback before starting it ? - std::function callback; + if (onTimer) + onTimer(); + } }; } // namespace yup diff --git a/modules/yup_graphics/graphics/yup_Graphics.cpp b/modules/yup_graphics/graphics/yup_Graphics.cpp index 00982dcdd..27ec796cd 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.cpp +++ b/modules/yup_graphics/graphics/yup_Graphics.cpp @@ -170,8 +170,13 @@ Graphics::SavedState& Graphics::SavedState::operator= (SavedState&& other) Graphics::SavedState::~SavedState() { - if (g != nullptr) - g->restoreState(); + restore(); +} + +void Graphics::SavedState::restore() +{ + if (auto graphics = std::exchange (g, nullptr)) + graphics->restoreState(); } //============================================================================== diff --git a/modules/yup_graphics/graphics/yup_Graphics.h b/modules/yup_graphics/graphics/yup_Graphics.h index 1981a1b60..c0075ec4d 100644 --- a/modules/yup_graphics/graphics/yup_Graphics.h +++ b/modules/yup_graphics/graphics/yup_Graphics.h @@ -83,6 +83,9 @@ class YUP_API Graphics /** Destroys the SavedState, potentially restoring the Graphics state. */ ~SavedState(); + /** Restore state. */ + void restore(); + private: Graphics* g = nullptr; }; diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index d9acf4cf7..5691655e9 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -29,6 +29,17 @@ namespace static std::vector activePopups; +void removeActivePopup (PopupMenu* popupMenu) +{ + for (auto it = activePopups.begin(); it != activePopups.end();) + { + if (it->get() == popupMenu) + it = activePopups.erase (it); + else + ++it; + } +} + void installGlobalMouseListener() { static bool mouseListenerAdded = [] @@ -49,6 +60,13 @@ void installGlobalMouseListener() clickedInsidePopup = true; break; } + + // Also check if clicked inside any submenu + if (popupMenu->submenuContains (globalPos)) + { + clickedInsidePopup = true; + break; + } } } @@ -289,11 +307,12 @@ bool PopupMenu::Item::isCustomComponent() const PopupMenu::Options::Options() : parentComponent (nullptr) - , dismissOnSelection (true) + , targetComponent (nullptr) , alignment (Justification::topLeft) , placement (Placement::below()) , positioningMode (PositioningMode::atPoint) - , targetComponent (nullptr) + , dismissOnSelection (true) + , dismissAllPopups (true) { } @@ -458,7 +477,11 @@ void PopupMenu::setHoveredItem (int itemIndex) } if (hasChanged) + { + updateSubmenuVisibility (itemIndex); + repaint(); + } } //============================================================================== @@ -524,7 +547,21 @@ void PopupMenu::setupMenuItems() } } - setSize ({ width, y + verticalPadding }); // Bottom padding + totalContentHeight = y + verticalPadding; // Store total content height + + // Update scrolling calculations + updateScrolling(); + + // Calculate final menu size considering scrolling + float finalHeight = totalContentHeight; + if (needsScrolling()) + { + finalHeight = availableContentHeight; + if (showScrollIndicators) + finalHeight += 2 * scrollIndicatorHeight; + } + + setSize ({ width, finalHeight }); } //============================================================================== @@ -614,6 +651,16 @@ void PopupMenu::positionMenu() int PopupMenu::getItemIndexAt (Point position) const { + // Adjust position for scrolling + if (needsScrolling()) + { + auto contentBounds = getMenuContentBounds(); + if (! contentBounds.contains (position)) + return -1; // Click was outside content area (maybe on scroll indicators) + + position.setY (position.getY() + scrollOffset); + } + int itemIndex = 0; for (const auto& item : items) @@ -638,8 +685,10 @@ void PopupMenu::show (std::function callback) void PopupMenu::showCustom (const Options& options, std::function callback) { - dismissAllPopups(); + if (options.dismissAllPopups) + dismissAllPopups(); + this->options = options; menuCallback = std::move (callback); if (isEmpty()) @@ -662,7 +711,6 @@ void PopupMenu::showCustom (const Options& options, std::function ca // When we have no parent component, add to desktop to work in screen coordinates auto nativeOptions = ComponentNative::Options {} .withDecoration (false) - .withClearColor (::yup::Colors::transparentBlack) .withResizableWindow (false); addToDesktop (nativeOptions); @@ -691,25 +739,42 @@ void PopupMenu::dismiss (int itemID) isBeingDismissed = true; + // Hide any submenus first + hideSubmenus(); + setVisible (false); setSelectedItemID (itemID); - for (auto it = activePopups.begin(); it != activePopups.end();) - { - if (it->get() == this) - it = activePopups.erase (it); - else - ++it; - } + removeActivePopup (this); } //============================================================================== void PopupMenu::paint (Graphics& g) { + auto state = g.saveState(); + + if (needsScrolling()) + { + // Create a clipping region for scrollable content + auto contentBounds = getMenuContentBounds(); + g.setClipPath (contentBounds); + + // Translate graphics context by scroll offset + g.setTransform (AffineTransform::translation (0.0f, -scrollOffset)); + } + if (auto style = ApplicationTheme::findComponentStyle (*this)) style->paint (g, *ApplicationTheme::getGlobalTheme(), *this); + + if (needsScrolling()) + { + state.restore(); + + // Paint scroll indicators + paintScrollIndicators (g); + } } //============================================================================== @@ -732,7 +797,8 @@ void PopupMenu::mouseDown (const MouseEvent& event) if (item.isSubMenu()) { - // TODO: Show sub-menu + // For submenus, we show them on hover, not on click + showSubmenu (itemIndex); } else { @@ -748,6 +814,21 @@ void PopupMenu::mouseMove (const MouseEvent& event) void PopupMenu::mouseExit (const MouseEvent& event) { setHoveredItem (-1); + + // Don't start hide timer on mouse exit - let the hover logic handle submenu visibility + // This prevents the main menu from disappearing when moving to submenus +} + +void PopupMenu::mouseWheel (const MouseEvent& event, const MouseWheelData& wheel) +{ + if (! needsScrolling()) + return; + + auto deltaY = wheel.getDeltaY() * scrollSpeed; + scrollOffset = jlimit (0.0f, getMaxScrollOffset(), scrollOffset - deltaY); + + constrainScrollOffset(); + repaint(); } void PopupMenu::keyDown (const KeyPress& key, const Point& position) @@ -760,7 +841,298 @@ void PopupMenu::keyDown (const KeyPress& key, const Point& position) void PopupMenu::focusLost() { - dismiss(); + // Don't dismiss if we have a visible submenu or are in the process of showing one + if (hasVisibleSubmenu() || isShowingSubmenu) + return; + + //dismiss(); +} + +//============================================================================== +// Submenu functionality + +void PopupMenu::showSubmenu (int itemIndex) +{ + if (! isPositiveAndBelow (itemIndex, getNumItems())) + return; + + auto& item = *items[itemIndex]; + if (! item.isSubMenu() || ! item.subMenu) + return; + + // If we're already showing this submenu, no need to do anything + if (submenuItemIndex == itemIndex && currentSubmenu && currentSubmenu == item.subMenu && currentSubmenu->isVisible()) + return; + + // Set flag to prevent dismissal during submenu operations + isShowingSubmenu = true; + + // Hide current submenu if different item + if (submenuItemIndex != itemIndex) + hideSubmenus(); + + submenuItemIndex = itemIndex; + currentSubmenu = item.subMenu; + + // Stop any pending timers that might interfere + submenuShowTimer.stopTimer(); + submenuHideTimer.stopTimer(); + + // Position submenu to the right of the current menu item + auto itemBounds = item.area; + + // Account for scroll offset if menu is scrollable + if (needsScrolling()) + { + itemBounds.setY (itemBounds.getY() - static_cast (scrollOffset)); + } + + Options submenuOptions; + submenuOptions.parentComponent = options.parentComponent; // Respect parent component + submenuOptions.dismissAllPopups = false; + + if (options.parentComponent) + { + // Position relative to parent component - need to transform coordinates properly + auto menuPosInParent = getTopLeft(); // This menu's position within parent + auto itemBoundsInParent = itemBounds.translated (menuPosInParent); + + submenuOptions.withTargetArea (itemBoundsInParent, Placement::toRight (Justification::topRight)); + } + else + { + // Use screen coordinates when no parent + auto itemScreenPos = getScreenBounds().getTopLeft() + itemBounds.getTopRight(); + submenuOptions.withTargetArea (Rectangle (itemScreenPos.getX(), itemScreenPos.getY(), 1, itemBounds.getHeight()), + Placement::toRight (Justification::topLeft)); + } + + // Add mouse listeners to handle submenu interaction + currentSubmenu->showCustom (submenuOptions, [this] (int selectedID) + { + if (selectedID != 0) + { + dismiss (selectedID); + } + }); + + // Clear the flag after showing submenu + isShowingSubmenu = false; +} + +void PopupMenu::hideSubmenus() +{ + if (currentSubmenu) + { + // Use the cleanup method to hide without triggering callbacks + cleanupSubmenu (currentSubmenu); + currentSubmenu = nullptr; + } + + submenuItemIndex = -1; + isShowingSubmenu = false; +} + +void PopupMenu::cleanupSubmenu (PopupMenu::Ptr submenu) +{ + if (! submenu) + return; + + // Just hide without triggering callbacks + submenu->setVisible (false); + + // Remove from activePopups list + removeActivePopup (submenu.get()); +} + +bool PopupMenu::hasVisibleSubmenu() const +{ + return currentSubmenu != nullptr && currentSubmenu->isVisible(); +} + +bool PopupMenu::submenuContains (const Point& position) const +{ + if (! hasVisibleSubmenu()) + return false; + + return currentSubmenu->getScreenBounds().contains (position); +} + +void PopupMenu::updateSubmenuVisibility (int hoveredItemIndex) +{ + // Always stop existing timers first + submenuShowTimer.stopTimer(); + submenuHideTimer.stopTimer(); + + if (isPositiveAndBelow (hoveredItemIndex, getNumItems())) + { + auto& item = *items[hoveredItemIndex]; + if (item.isSubMenu() && item.isEnabled) + { + // If this is the same submenu item that's already showing, do nothing + if (submenuItemIndex == hoveredItemIndex && hasVisibleSubmenu()) + return; + + // Show submenu immediately if we're hovering over a submenu item + // No timer delay to prevent the main menu from disappearing + showSubmenu (hoveredItemIndex); + return; + } + } + + // If we're not hovering over a submenu item and we have a visible submenu, + // use a longer delay before hiding to allow mouse movement to submenu + if (hasVisibleSubmenu() && submenuItemIndex != hoveredItemIndex) + { + submenuHideTimer.onTimer = [this] + { + hideSubmenus(); + submenuHideTimer.stopTimer(); + }; + + submenuHideTimer.startTimer (200); + } +} + +//============================================================================== +// Scrolling functionality + +void PopupMenu::updateScrolling() +{ + auto bounds = getLocalBounds().to(); + + if (options.parentComponent) + { + // Calculate available height within parent component bounds + auto parentBounds = options.parentComponent->getLocalBounds().to(); + auto menuScreenPos = getScreenPosition().to(); + auto parentScreenPos = options.parentComponent->getScreenPosition().to(); + + // Calculate available space from current position to parent bottom + availableContentHeight = parentBounds.getBottom() - (menuScreenPos.getY() - parentScreenPos.getY()); + availableContentHeight = jmax (100.0f, availableContentHeight); // Minimum height + } + else + { + // Use screen bounds + if (auto* desktop = Desktop::getInstance()) + { + if (auto screen = desktop->getPrimaryScreen()) + { + auto screenBounds = screen->workArea.to(); + auto menuScreenPos = getScreenPosition().to(); + availableContentHeight = screenBounds.getBottom() - menuScreenPos.getY(); + availableContentHeight = jmax (100.0f, availableContentHeight); + } + } + } + + totalContentHeight = 0.0f; + for (const auto& item : items) + { + totalContentHeight += item->area.getHeight(); + } + + // Add padding + totalContentHeight += 8.0f; // Top + bottom padding + + showScrollIndicators = needsScrolling(); + + if (showScrollIndicators) + availableContentHeight -= 2 * scrollIndicatorHeight; + + constrainScrollOffset(); +} + +void PopupMenu::constrainScrollOffset() +{ + auto maxOffset = getMaxScrollOffset(); + scrollOffset = jlimit (0.0f, maxOffset, scrollOffset); +} + +float PopupMenu::getMaxScrollOffset() const +{ + if (! needsScrolling()) + return 0.0f; + + return jmax (0.0f, totalContentHeight - availableContentHeight); +} + +bool PopupMenu::needsScrolling() const +{ + return totalContentHeight > availableContentHeight; +} + +Rectangle PopupMenu::getMenuContentBounds() const +{ + auto bounds = getLocalBounds().to(); + + if (showScrollIndicators) + { + bounds.removeFromTop (scrollIndicatorHeight); + bounds.removeFromBottom (scrollIndicatorHeight); + } + + return bounds; +} + +Rectangle PopupMenu::getScrollUpIndicatorBounds() const +{ + if (! showScrollIndicators) + return {}; + + auto bounds = getLocalBounds().to(); + return bounds.removeFromTop (scrollIndicatorHeight); +} + +Rectangle PopupMenu::getScrollDownIndicatorBounds() const +{ + if (! showScrollIndicators) + return {}; + + auto bounds = getLocalBounds().to(); + return bounds.removeFromBottom (scrollIndicatorHeight); +} + +void PopupMenu::paintScrollIndicators (Graphics& g) +{ + if (! showScrollIndicators) + return; + + auto theme = ApplicationTheme::getGlobalTheme(); + g.setFillColor (findColor (Colors::menuItemText).value_or (Color (0xff000000))); + + // Up arrow + if (scrollOffset > 0.0f) + { + /* + auto upBounds = getScrollUpIndicatorBounds(); + auto center = upBounds.getCenter(); + auto arrowSize = 4.0f; + + Path upArrow; + upArrow.addTriangle (center.getX(), center.getY() - arrowSize * 0.5f, + center.getX() - arrowSize, center.getY() + arrowSize * 0.5f, + center.getX() + arrowSize, center.getY() + arrowSize * 0.5f); + g.fillPath (upArrow); + */ + } + + // Down arrow + if (scrollOffset < getMaxScrollOffset()) + { + /* + auto downBounds = getScrollDownIndicatorBounds(); + auto center = downBounds.getCenter(); + auto arrowSize = 4.0f; + + Path downArrow; + downArrow.addTriangle (center.getX(), center.getY() + arrowSize * 0.5f, + center.getX() - arrowSize, center.getY() - arrowSize * 0.5f, + center.getX() + arrowSize, center.getY() - arrowSize * 0.5f); + g.fillPath (downArrow); + */ + } } } // namespace yup diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 38ab27e6c..d50994569 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -124,6 +124,7 @@ class YUP_API PopupMenu std::optional minWidth; std::optional maxWidth; bool dismissOnSelection; + bool dismissAllPopups; }; //============================================================================== @@ -249,6 +250,12 @@ class YUP_API PopupMenu /** Dismisses all currently open popup menus. */ static void dismissAllPopups(); + //============================================================================== + /** @internal */ + bool hasVisibleSubmenu() const; + /** @internal */ + bool submenuContains (const Point& position) const; + //============================================================================== /** @internal */ void paint (Graphics& g) override; @@ -259,6 +266,8 @@ class YUP_API PopupMenu /** @internal */ void mouseExit (const MouseEvent& event) override; /** @internal */ + void mouseWheel (const MouseEvent& event, const MouseWheelData& wheel) override; + /** @internal */ void keyDown (const KeyPress& key, const Point& position) override; /** @internal */ void focusLost() override; @@ -278,6 +287,22 @@ class YUP_API PopupMenu void setupMenuItems(); void positionMenu(); + // Submenu functionality + void showSubmenu (int itemIndex); + void hideSubmenus(); + void updateSubmenuVisibility (int hoveredItemIndex); + void cleanupSubmenu (PopupMenu::Ptr submenu); + + // Scrolling functionality + void updateScrolling(); + void constrainScrollOffset(); + float getMaxScrollOffset() const; + void paintScrollIndicators (Graphics& g); + Rectangle getScrollUpIndicatorBounds() const; + Rectangle getScrollDownIndicatorBounds() const; + bool needsScrolling() const; + Rectangle getMenuContentBounds() const; + // PopupMenuItem is now an implementation detail class PopupMenuItem; std::vector> items; @@ -286,6 +311,21 @@ class YUP_API PopupMenu int selectedItemID = -1; bool isBeingDismissed = false; + // Submenu support + PopupMenu::Ptr currentSubmenu; + int submenuItemIndex = -1; + TimedCallback submenuShowTimer; + TimedCallback submenuHideTimer; + bool isShowingSubmenu = false; + + // Scrolling support + float scrollOffset = 0.0f; + float availableContentHeight = 0.0f; + float totalContentHeight = 0.0f; + bool showScrollIndicators = false; + static constexpr float scrollIndicatorHeight = 12.0f; + static constexpr float scrollSpeed = 20.0f; + std::function menuCallback; YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupMenu) From 492d3fbc67a6a900db5b8f12a99c1f16d377562c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Thu, 26 Jun 2025 10:29:08 +0200 Subject: [PATCH 40/51] More work --- examples/graphics/source/examples/PopupMenu.h | 5 +- modules/yup_gui/menus/yup_PopupMenu.cpp | 48 +++++++++++++++++++ modules/yup_gui/menus/yup_PopupMenu.h | 7 +++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index f9ee17c45..751972c8a 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -335,7 +335,10 @@ class PopupMenuDemo : public yup::Component break; default: - message = "No selection"; + if (selectedID >= 400) + message = yup::String::formatted ("Selected submenu item ID: %d", selectedID); + else + message = "No selection"; break; } diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 5691655e9..8f8026add 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -808,13 +808,30 @@ void PopupMenu::mouseDown (const MouseEvent& event) void PopupMenu::mouseMove (const MouseEvent& event) { + // Cancel hide timers when mouse is actively moving in menu + submenuHideTimer.stopTimer(); + setHoveredItem (getItemIndexAt (event.getPosition())); } +void PopupMenu::mouseEnter (const MouseEvent& event) +{ + // Call custom mouse enter callback for submenu coordination + if (onMouseEnter) + onMouseEnter(); + + // Cancel any pending hide timers when mouse enters + submenuHideTimer.stopTimer(); +} + void PopupMenu::mouseExit (const MouseEvent& event) { setHoveredItem (-1); + // Call custom mouse exit callback for submenu coordination + if (onMouseExit) + onMouseExit(); + // Don't start hide timer on mouse exit - let the hover logic handle submenu visibility // This prevents the main menu from disappearing when moving to submenus } @@ -916,6 +933,9 @@ void PopupMenu::showSubmenu (int itemIndex) } }); + // Set up submenu mouse tracking to prevent premature hiding + setupSubmenuMouseTracking (currentSubmenu); + // Clear the flag after showing submenu isShowingSubmenu = false; } @@ -933,6 +953,34 @@ void PopupMenu::hideSubmenus() isShowingSubmenu = false; } +void PopupMenu::setupSubmenuMouseTracking (PopupMenu::Ptr submenu) +{ + if (! submenu) + return; + + // Set up callbacks for coordinating mouse events between parent and submenu + auto parentMenu = this; // Capture parent menu reference + + // When mouse enters submenu, cancel any hide timers on parent + submenu->onMouseEnter = [parentMenu]() + { + parentMenu->submenuHideTimer.stopTimer(); + }; + + // When mouse exits submenu, start hide timer with generous delay + submenu->onMouseExit = [parentMenu]() + { + // Only start hide timer if we're not returning to parent menu + parentMenu->submenuHideTimer.onTimer = [parentMenu] + { + parentMenu->hideSubmenus(); + parentMenu->submenuHideTimer.stopTimer(); + }; + + parentMenu->submenuHideTimer.startTimer (400); // Longer delay for better UX + }; +} + void PopupMenu::cleanupSubmenu (PopupMenu::Ptr submenu) { if (! submenu) diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index d50994569..78e809d76 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -234,6 +234,10 @@ class YUP_API PopupMenu /** Callback type for menu item selection. */ std::function onItemSelected; + /** Mouse tracking callbacks for submenu coordination. */ + std::function onMouseEnter; + std::function onMouseExit; + //============================================================================== // Color identifiers for theming struct Colors @@ -264,6 +268,8 @@ class YUP_API PopupMenu /** @internal */ void mouseMove (const MouseEvent& event) override; /** @internal */ + void mouseEnter (const MouseEvent& event) override; + /** @internal */ void mouseExit (const MouseEvent& event) override; /** @internal */ void mouseWheel (const MouseEvent& event, const MouseWheelData& wheel) override; @@ -292,6 +298,7 @@ class YUP_API PopupMenu void hideSubmenus(); void updateSubmenuVisibility (int hoveredItemIndex); void cleanupSubmenu (PopupMenu::Ptr submenu); + void setupSubmenuMouseTracking (PopupMenu::Ptr submenu); // Scrolling functionality void updateScrolling(); From 065da80cab793e1bc4be5d4198bcd98570e6a623 Mon Sep 17 00:00:00 2001 From: Yup Bot Date: Thu, 26 Jun 2025 08:32:14 +0000 Subject: [PATCH 41/51] Code formatting --- examples/graphics/source/examples/PopupMenu.h | 21 ++++++++++++------- modules/yup_gui/menus/yup_PopupMenu.cpp | 20 ++++++------------ modules/yup_gui/menus/yup_PopupMenu.h | 21 +++++++++++++------ 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/examples/graphics/source/examples/PopupMenu.h b/examples/graphics/source/examples/PopupMenu.h index 751972c8a..cac895cda 100644 --- a/examples/graphics/source/examples/PopupMenu.h +++ b/examples/graphics/source/examples/PopupMenu.h @@ -101,12 +101,12 @@ class PopupMenuDemo : public yup::Component } else if (key.getKey() == yup::KeyPress::rightKey) { - currentPlacementIndex = (currentPlacementIndex + 1) % static_cast(placements.size()); + currentPlacementIndex = (currentPlacementIndex + 1) % static_cast (placements.size()); showPlacementTest(); } else if (key.getKey() == yup::KeyPress::leftKey) { - currentPlacementIndex = (currentPlacementIndex - 1 + static_cast(placements.size())) % static_cast(placements.size()); + currentPlacementIndex = (currentPlacementIndex - 1 + static_cast (placements.size())) % static_cast (placements.size()); showPlacementTest(); } } @@ -126,7 +126,10 @@ class PopupMenuDemo : public yup::Component yup::String description; PlacementTest (yup::PopupMenu::Placement p, const yup::String& desc) - : placement (p), description (desc) {} + : placement (p) + , description (desc) + { + } }; void initializePlacements() @@ -169,7 +172,8 @@ class PopupMenuDemo : public yup::Component void showPlacementTest() { - if (placements.empty()) return; + if (placements.empty()) + return; auto& test = placements[currentPlacementIndex]; @@ -213,7 +217,7 @@ class PopupMenuDemo : public yup::Component // Update status auto statusText = yup::String::formatted ("Test %d/%d: %s", currentPlacementIndex + 1, - (int)placements.size(), + (int) placements.size(), test.description.toRawUTF8()); statusLabel.setText (statusText); } @@ -302,7 +306,8 @@ class PopupMenuDemo : public yup::Component infoMenu->addItem (L"• ← →: Navigate tests", 0, false); infoMenu->addItem (L"• Right-click: Feature demo", 0, false); - infoMenu->show ([this] (int selectedID) { + infoMenu->show ([this] (int selectedID) + { // Info only, no actions }); } @@ -314,12 +319,12 @@ class PopupMenuDemo : public yup::Component switch (selectedID) { case 998: // Previous - currentPlacementIndex = (currentPlacementIndex - 1 + static_cast(placements.size())) % static_cast(placements.size()); + currentPlacementIndex = (currentPlacementIndex - 1 + static_cast (placements.size())) % static_cast (placements.size()); showPlacementTest(); return; case 999: // Next - currentPlacementIndex = (currentPlacementIndex + 1) % static_cast(placements.size()); + currentPlacementIndex = (currentPlacementIndex + 1) % static_cast (placements.size()); showPlacementTest(); return; diff --git a/modules/yup_gui/menus/yup_PopupMenu.cpp b/modules/yup_gui/menus/yup_PopupMenu.cpp index 8f8026add..30a70537e 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.cpp +++ b/modules/yup_gui/menus/yup_PopupMenu.cpp @@ -176,15 +176,11 @@ Point calculatePositionRelativeToArea (Rectangle targetArea, Size if (placement.side == PopupMenu::Side::below || placement.side == PopupMenu::Side::above) { // For above/below: align horizontally - if (placement.alignment == Justification::centerTop || - placement.alignment == Justification::center || - placement.alignment == Justification::centerBottom) + if (placement.alignment == Justification::centerTop || placement.alignment == Justification::center || placement.alignment == Justification::centerBottom) { position.setX (targetArea.getCenterX() - menuSize.getWidth() / 2); } - else if (placement.alignment == Justification::topRight || - placement.alignment == Justification::centerRight || - placement.alignment == Justification::bottomRight) + else if (placement.alignment == Justification::topRight || placement.alignment == Justification::centerRight || placement.alignment == Justification::bottomRight) { position.setX (targetArea.getRight() - menuSize.getWidth()); } @@ -196,15 +192,11 @@ Point calculatePositionRelativeToArea (Rectangle targetArea, Size else if (placement.side == PopupMenu::Side::toLeft || placement.side == PopupMenu::Side::toRight) { // For left/right: align vertically - if (placement.alignment == Justification::centerLeft || - placement.alignment == Justification::center || - placement.alignment == Justification::centerRight) + if (placement.alignment == Justification::centerLeft || placement.alignment == Justification::center || placement.alignment == Justification::centerRight) { position.setY (targetArea.getCenterY() - menuSize.getHeight() / 2); } - else if (placement.alignment == Justification::bottomLeft || - placement.alignment == Justification::centerBottom || - placement.alignment == Justification::bottomRight) + else if (placement.alignment == Justification::bottomLeft || placement.alignment == Justification::centerBottom || placement.alignment == Justification::bottomRight) { position.setY (targetArea.getBottom() - menuSize.getHeight()); } @@ -636,7 +628,7 @@ void PopupMenu::positionMenu() else { // Fallback to center of available area - position = availableArea.getCenter() - Point{ menuSize.getWidth() / 2, menuSize.getHeight() / 2 }; + position = availableArea.getCenter() - Point { menuSize.getWidth() / 2, menuSize.getHeight() / 2 }; } break; } @@ -895,7 +887,7 @@ void PopupMenu::showSubmenu (int itemIndex) submenuShowTimer.stopTimer(); submenuHideTimer.stopTimer(); - // Position submenu to the right of the current menu item + // Position submenu to the right of the current menu item auto itemBounds = item.area; // Account for scroll offset if menu is scrollable diff --git a/modules/yup_gui/menus/yup_PopupMenu.h b/modules/yup_gui/menus/yup_PopupMenu.h index 78e809d76..e575e7d07 100644 --- a/modules/yup_gui/menus/yup_PopupMenu.h +++ b/modules/yup_gui/menus/yup_PopupMenu.h @@ -41,11 +41,11 @@ class YUP_API PopupMenu /** Menu positioning relative to rectangles/components */ enum class Side { - above, //< Menu appears above the target - below, //< Menu appears below the target (default) - toLeft, //< Menu appears to the left of the target - toRight, //< Menu appears to the right of the target - centered //< Menu is centered on the target + above, //< Menu appears above the target + below, //< Menu appears below the target (default) + toLeft, //< Menu appears to the left of the target + toRight, //< Menu appears to the right of the target + centered //< Menu is centered on the target }; struct Placement @@ -54,12 +54,21 @@ class YUP_API PopupMenu Justification alignment = Justification::topLeft; Placement() = default; - Placement (Side s, Justification align = Justification::topLeft) : side (s), alignment (align) {} + + Placement (Side s, Justification align = Justification::topLeft) + : side (s) + , alignment (align) + { + } static Placement below (Justification align = Justification::topLeft) { return { Side::below, align }; } + static Placement above (Justification align = Justification::topLeft) { return { Side::above, align }; } + static Placement toRight (Justification align = Justification::topLeft) { return { Side::toRight, align }; } + static Placement toLeft (Justification align = Justification::topLeft) { return { Side::toLeft, align }; } + static Placement centered() { return { Side::centered, Justification::center }; } }; From d087d55678acf5b265f3e4ff9283fd1ce350ba73 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 30 Jun 2025 13:15:21 +0200 Subject: [PATCH 42/51] Combo Box and Other Widgets (#62) --- .github/workflows/build_android.yml | 30 - .github/workflows/build_ios.yml | 19 - .github/workflows/build_linux.yml | 18 - .github/workflows/build_macos.yml | 21 +- .github/workflows/build_wasm.yml | 16 - .github/workflows/build_windows.yml | 8 - .github/workflows/coverage.yml | 1 + CMakeLists.txt | 1 - codecov.yml | 4 +- examples/app/source/main.cpp | 4 +- examples/graphics/CMakeLists.txt | 32 +- examples/{render => graphics}/data/alien.rev | Bin examples/{render => graphics}/data/alien.riv | Bin .../data/arcade_controls.rev | Bin .../data/arcade_controls.riv | Bin .../data/audio_sampler.rev | Bin .../data/audio_sampler.riv | Bin .../data/car_interface.rev | Bin .../data/car_interface.riv | Bin examples/{render => graphics}/data/charge.riv | Bin .../{render => graphics}/data/seasynth.rev | Bin .../{render => graphics}/data/seasynth.riv | Bin .../data/toymachine_3.rev | Bin .../data/toymachine_3.riv | Bin .../{render => graphics}/data/ui_elements.rev | Bin .../{render => graphics}/data/ui_elements.riv | Bin examples/graphics/source/examples/Artboard.h | 115 ++ examples/graphics/source/examples/Audio.h | 6 + .../graphics/source/examples/FileChooser.h | 8 + .../graphics/source/examples/LayoutFonts.h | 3 + .../graphics/source/examples/OpaqueDemo.h | 224 +++ examples/graphics/source/examples/Paths.h | 3 + examples/graphics/source/examples/PopupMenu.h | 3 + .../graphics/source/examples/TextEditor.h | 3 + .../graphics/source/examples/VariableFonts.h | 3 + examples/graphics/source/examples/Widgets.h | 218 ++ examples/graphics/source/main.cpp | 82 +- examples/render/CMakeLists.txt | 77 - examples/render/source/main.cpp | 269 --- .../yup_audio_plugin_client_Standalone.cpp | 4 +- modules/yup_core/containers/yup_Variant.cpp | 8 - modules/yup_core/containers/yup_Variant.h | 4 - .../yup_events/messages/yup_ApplicationBase.h | 8 +- .../yup_graphics/graphics/yup_Graphics.cpp | 9 + modules/yup_graphics/graphics/yup_Graphics.h | 4 + .../primitives/yup_AffineTransform.h | 19 + modules/yup_graphics/primitives/yup_Point.h | 7 + .../yup_graphics/primitives/yup_Rectangle.h | 13 +- modules/yup_graphics/primitives/yup_Size.h | 17 +- modules/yup_gui/artboard/yup_Artboard.cpp | 20 +- modules/yup_gui/artboard/yup_Artboard.h | 11 + .../{widgets => buttons}/yup_Button.cpp | 2 + .../yup_gui/{widgets => buttons}/yup_Button.h | 0 modules/yup_gui/buttons/yup_ImageButton.cpp | 0 modules/yup_gui/buttons/yup_ImageButton.h | 0 modules/yup_gui/buttons/yup_SwitchButton.cpp | 184 ++ modules/yup_gui/buttons/yup_SwitchButton.h | 128 ++ .../{widgets => buttons}/yup_TextButton.cpp | 19 +- .../{widgets => buttons}/yup_TextButton.h | 4 +- modules/yup_gui/buttons/yup_ToggleButton.cpp | 123 ++ modules/yup_gui/buttons/yup_ToggleButton.h | 112 ++ modules/yup_gui/component/yup_Component.cpp | 229 ++- modules/yup_gui/component/yup_Component.h | 110 +- modules/yup_gui/menus/yup_PopupMenu.cpp | 37 +- modules/yup_gui/menus/yup_PopupMenu.h | 2 +- modules/yup_gui/native/yup_Windowing_sdl2.cpp | 37 +- modules/yup_gui/native/yup_Windowing_sdl2.h | 1 + .../themes/theme_v1/yup_ThemeVersion1.cpp | 139 +- modules/yup_gui/widgets/yup_ComboBox.cpp | 358 ++++ modules/yup_gui/widgets/yup_ComboBox.h | 200 ++ modules/yup_gui/widgets/yup_Label.cpp | 5 +- modules/yup_gui/widgets/yup_Label.h | 2 +- modules/yup_gui/widgets/yup_Slider.cpp | 2 + modules/yup_gui/widgets/yup_TextEditor.cpp | 14 +- modules/yup_gui/widgets/yup_TextEditor.h | 2 +- .../yup_gui/windowing/yup_DocumentWindow.cpp | 8 +- .../yup_gui/windowing/yup_DocumentWindow.h | 8 +- modules/yup_gui/yup_gui.cpp | 8 +- modules/yup_gui/yup_gui.h | 8 +- tests/CMakeLists.txt | 44 +- tests/main.cpp | 77 + .../yup_AudioDeviceManager.cpp | 10 - tests/yup_graphics/yup_Rectangle.cpp | 7 + tests/yup_graphics/yup_Size.cpp | 83 +- tests/yup_gui/yup_ComboBox.cpp | 305 +++ tests/yup_gui/yup_Component.cpp | 1745 +++++++++++++++++ tests/yup_gui/yup_Label.cpp | 344 ++++ tests/yup_gui/yup_PopupMenu.cpp | 686 +++++++ tests/yup_gui/yup_SwitchButton.cpp | 306 +++ tests/yup_gui/yup_TextButton.cpp | 254 +++ tests/yup_gui/yup_TextEditor.cpp | 136 +- tests/yup_gui/yup_ToggleButton.cpp | 220 +++ 92 files changed, 6528 insertions(+), 723 deletions(-) rename examples/{render => graphics}/data/alien.rev (100%) rename examples/{render => graphics}/data/alien.riv (100%) rename examples/{render => graphics}/data/arcade_controls.rev (100%) rename examples/{render => graphics}/data/arcade_controls.riv (100%) rename examples/{render => graphics}/data/audio_sampler.rev (100%) rename examples/{render => graphics}/data/audio_sampler.riv (100%) rename examples/{render => graphics}/data/car_interface.rev (100%) rename examples/{render => graphics}/data/car_interface.riv (100%) rename examples/{render => graphics}/data/charge.riv (100%) rename examples/{render => graphics}/data/seasynth.rev (100%) rename examples/{render => graphics}/data/seasynth.riv (100%) rename examples/{render => graphics}/data/toymachine_3.rev (100%) rename examples/{render => graphics}/data/toymachine_3.riv (100%) rename examples/{render => graphics}/data/ui_elements.rev (100%) rename examples/{render => graphics}/data/ui_elements.riv (100%) create mode 100644 examples/graphics/source/examples/Artboard.h create mode 100644 examples/graphics/source/examples/OpaqueDemo.h create mode 100644 examples/graphics/source/examples/Widgets.h delete mode 100644 examples/render/CMakeLists.txt delete mode 100644 examples/render/source/main.cpp rename modules/yup_gui/{widgets => buttons}/yup_Button.cpp (95%) rename modules/yup_gui/{widgets => buttons}/yup_Button.h (100%) create mode 100644 modules/yup_gui/buttons/yup_ImageButton.cpp create mode 100644 modules/yup_gui/buttons/yup_ImageButton.h create mode 100644 modules/yup_gui/buttons/yup_SwitchButton.cpp create mode 100644 modules/yup_gui/buttons/yup_SwitchButton.h rename modules/yup_gui/{widgets => buttons}/yup_TextButton.cpp (79%) rename modules/yup_gui/{widgets => buttons}/yup_TextButton.h (97%) create mode 100644 modules/yup_gui/buttons/yup_ToggleButton.cpp create mode 100644 modules/yup_gui/buttons/yup_ToggleButton.h create mode 100644 modules/yup_gui/widgets/yup_ComboBox.cpp create mode 100644 modules/yup_gui/widgets/yup_ComboBox.h create mode 100644 tests/main.cpp create mode 100644 tests/yup_gui/yup_ComboBox.cpp create mode 100644 tests/yup_gui/yup_Component.cpp create mode 100644 tests/yup_gui/yup_Label.cpp create mode 100644 tests/yup_gui/yup_PopupMenu.cpp create mode 100644 tests/yup_gui/yup_SwitchButton.cpp create mode 100644 tests/yup_gui/yup_TextButton.cpp create mode 100644 tests/yup_gui/yup_ToggleButton.cpp diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 2b5b351ac..ca95f690a 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -119,33 +119,3 @@ jobs: run: ./gradlew assembleDebug - working-directory: ${{ runner.workspace }}/build/examples/graphics run: ./gradlew assemble - - build_render: - runs-on: ubuntu-latest - needs: [configure] - steps: - - uses: actions/checkout@v4 - - uses: seanmiddleditch/gha-setup-ninja@master - - name: Setup Java - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 17 - - name: Setup Android SDK - uses: android-actions/setup-android@v2.0.10 - - name: Setup Android NDK - uses: nttld/setup-ndk@v1 - with: - ndk-version: r26d - - uses: actions/cache/restore@v4 - id: cache-restore - with: - path: ${{ runner.workspace }}/build - key: android-build-${{ github.sha }} - - name: Configure If Cache Missed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_TARGET_ANDROID=ON -DYUP_ENABLE_EXAMPLES=ON - - working-directory: ${{ runner.workspace }}/build/examples/render - run: ./gradlew assembleDebug - - working-directory: ${{ runner.workspace }}/build/examples/render - run: ./gradlew assemble diff --git a/.github/workflows/build_ios.yml b/.github/workflows/build_ios.yml index fffc744a9..2c9e38c88 100644 --- a/.github/workflows/build_ios.yml +++ b/.github/workflows/build_ios.yml @@ -107,22 +107,3 @@ jobs: -DPLATFORM=${{ env.IOS_PLATFORM }} -B ${{ runner.workspace }}/build -DYUP_ENABLE_EXAMPLES=ON - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_graphics - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_graphics - - build_render: - runs-on: macos-latest - needs: [configure] - steps: - - uses: actions/checkout@v4 - - uses: seanmiddleditch/gha-setup-ninja@master - - uses: actions/cache/restore@v4 - id: cache-restore - with: - path: ${{ runner.workspace }}/build - key: ios-build-${{ github.sha }} - - name: Configure If Cache Missed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: | - cmake ${{ github.workspace }} -G "Ninja Multi-Config" -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/ios.cmake \ - -DPLATFORM=${{ env.IOS_PLATFORM }} -B ${{ runner.workspace }}/build -DYUP_ENABLE_EXAMPLES=ON - - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_render - - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_render diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index f8d87f429..69a5c7422 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -131,24 +131,6 @@ jobs: - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_graphics - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_graphics - build_render: - runs-on: ubuntu-latest - needs: [configure] - steps: - - uses: actions/checkout@v4 - - uses: seanmiddleditch/gha-setup-ninja@master - - run: sudo apt-get update && sudo apt-get install -y ${INSTALL_DEPS} - - uses: actions/cache/restore@v4 - id: cache-restore - with: - path: ${{ runner.workspace }}/build - key: linux-build-${{ github.sha }} - - name: Configure If Cache Missed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_EXAMPLES=ON - - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_render - - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_render - build_plugin: runs-on: ubuntu-latest needs: [configure] diff --git a/.github/workflows/build_macos.yml b/.github/workflows/build_macos.yml index dcb453ace..e323c4a39 100644 --- a/.github/workflows/build_macos.yml +++ b/.github/workflows/build_macos.yml @@ -65,10 +65,10 @@ jobs: run: cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_TESTS=ON - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target yup_tests - working-directory: ${{ runner.workspace }}/build/tests/Debug - run: ./yup_tests + run: ./yup_tests.app/Contents/MacOS/yup_tests - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target yup_tests - working-directory: ${{ runner.workspace }}/build/tests/Release - run: ./yup_tests + run: ./yup_tests.app/Contents/MacOS/yup_tests build_console: runs-on: macos-latest @@ -121,23 +121,6 @@ jobs: - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_graphics - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_graphics - build_render: - runs-on: macos-latest - needs: [configure] - steps: - - uses: actions/checkout@v4 - - uses: seanmiddleditch/gha-setup-ninja@master - - uses: actions/cache/restore@v4 - id: cache-restore - with: - path: ${{ runner.workspace }}/build - key: macos-build-${{ github.sha }} - - name: Configure If Cache Missed - if: steps.cache-restore.outputs.cache-hit != 'true' - run: cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_EXAMPLES=ON - - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_render - - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_render - build_plugins: runs-on: macos-latest needs: [configure] diff --git a/.github/workflows/build_wasm.yml b/.github/workflows/build_wasm.yml index b5f3f5e6d..1804d0fdb 100644 --- a/.github/workflows/build_wasm.yml +++ b/.github/workflows/build_wasm.yml @@ -99,19 +99,3 @@ jobs: run: emcmake cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_TESTS=ON -DYUP_ENABLE_EXAMPLES=ON - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_graphics - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_graphics - - build_render: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: seanmiddleditch/gha-setup-ninja@master - - run: sudo apt-get update && sudo apt-get install -y ${INSTALL_DEPS} - - name: Setup emsdk - uses: mymindstorm/setup-emsdk@v14 - with: - version: ${{ env.EM_VERSION }} - actions-cache-folder: ${{ env.EM_CACHE_FOLDER }} - - name: Configure - run: emcmake cmake ${{ github.workspace }} -G "Ninja Multi-Config" -B ${{ runner.workspace }}/build -DYUP_ENABLE_TESTS=ON -DYUP_ENABLE_EXAMPLES=ON - - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_render - - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_render diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index 3925e075a..35e257003 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -62,14 +62,6 @@ jobs: - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_graphics - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_graphics - build_render: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - run: cmake ${{ github.workspace }} -B ${{ runner.workspace }}/build -DYUP_ENABLE_EXAMPLES=ON - - run: cmake --build ${{ runner.workspace }}/build --config Debug --parallel 4 --target example_render - - run: cmake --build ${{ runner.workspace }}/build --config Release --parallel 4 --target example_render - build_plugin: runs-on: windows-latest steps: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4a339e4d6..36d6bd10f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -46,6 +46,7 @@ env: libasound2-dev libjack-jackd2-dev ladspa-sdk libcurl4-openssl-dev libfreetype6-dev libx11-dev libxcomposite-dev libxcursor-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxrandr-dev libxrender-dev libglu1-mesa-dev libegl1-mesa-dev mesa-common-dev lcov + IGNORE_ERRORS: "mismatch,gcov,source,negative,unused,empty" jobs: coverage: diff --git a/CMakeLists.txt b/CMakeLists.txt index 103d13348..9d2ba6003 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,6 @@ if (YUP_BUILD_EXAMPLES) add_subdirectory (examples/console) add_subdirectory (examples/app) add_subdirectory (examples/graphics) - add_subdirectory (examples/render) if (YUP_PLATFORM_DESKTOP) add_subdirectory (examples/plugin) endif() diff --git a/codecov.yml b/codecov.yml index 36fe860c6..0d47b4d57 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,6 +9,7 @@ coverage: status: project: default: + informational: true target: 70% threshold: 5% base: auto @@ -22,6 +23,7 @@ coverage: - yup_gui patch: default: + informational: true target: 70% threshold: 5% @@ -114,7 +116,7 @@ ignore: - "cmake/**/*" - "docs/**/*" - "standalone/**/*" - - "**/modules/*/native/*" + - "modules/*/native/**/*" comment: layout: "header, reach, components, diff, flags, files, footer" diff --git a/examples/app/source/main.cpp b/examples/app/source/main.cpp index 2549e56cf..7b6d52ed2 100644 --- a/examples/app/source/main.cpp +++ b/examples/app/source/main.cpp @@ -27,12 +27,12 @@ class Application : public yup::YUPApplication public: Application() = default; - const yup::String getApplicationName() override + yup::String getApplicationName() override { return "yup app!"; } - const yup::String getApplicationVersion() override + yup::String getApplicationVersion() override { return "1.0"; } diff --git a/examples/graphics/CMakeLists.txt b/examples/graphics/CMakeLists.txt index d2abf8a77..02c32094a 100644 --- a/examples/graphics/CMakeLists.txt +++ b/examples/graphics/CMakeLists.txt @@ -25,6 +25,8 @@ set (target_version "1.0.0") project (${target_name} VERSION ${target_version}) +set (rive_file "data/alien.riv") + # ==== Prepare Android build set (link_libraries "") if (ANDROID) @@ -32,16 +34,17 @@ if (ANDROID) _yup_setup_platform() _yup_add_default_modules ("${CMAKE_CURRENT_LIST_DIR}/../..") - #yup_add_embedded_binary_resources ( - # "${target_name}_binary_data" - # OUT_DIR BinaryData - # HEADER BinaryData.h - # NAMESPACE yup - # RESOURCE_NAMES - # RobotoFont - # RESOURCES - # "${CMAKE_CURRENT_LIST_DIR}/data/RobotoFlex-VariableFont.ttf") - #set (link_libraries "${target_name}_binary_data") + yup_add_embedded_binary_resources ( + "${target_name}_binary_data" + OUT_DIR BinaryData + HEADER BinaryData.h + NAMESPACE yup + RESOURCE_NAMES + RiveFile + RESOURCES + "${CMAKE_CURRENT_LIST_DIR}/${rive_file}") + + set (link_libraries "${target_name}_binary_data") endif() # ==== Prepare target @@ -51,13 +54,20 @@ yup_standalone_app ( TARGET_IDE_GROUP "Examples" TARGET_APP_ID "org.yup.${target_name}" TARGET_APP_NAMESPACE "org.yup" + DEFINITIONS + YUP_EXAMPLE_GRAPHICS_RIVE_FILE="${rive_file}" PRELOAD_FILES - "${CMAKE_CURRENT_LIST_DIR}/data/RobotoFlex-VariableFont.ttf@data/RobotoFlex-VariableFont.ttf" + "${CMAKE_CURRENT_LIST_DIR}/${rive_file}@${rive_file}" MODULES + yup::yup_core + yup::yup_audio_basics yup::yup_audio_devices + yup::yup_events + yup::yup_graphics yup::yup_gui yup::yup_audio_processors libpng + libwebp ${link_libraries}) # ==== Prepare sources diff --git a/examples/render/data/alien.rev b/examples/graphics/data/alien.rev similarity index 100% rename from examples/render/data/alien.rev rename to examples/graphics/data/alien.rev diff --git a/examples/render/data/alien.riv b/examples/graphics/data/alien.riv similarity index 100% rename from examples/render/data/alien.riv rename to examples/graphics/data/alien.riv diff --git a/examples/render/data/arcade_controls.rev b/examples/graphics/data/arcade_controls.rev similarity index 100% rename from examples/render/data/arcade_controls.rev rename to examples/graphics/data/arcade_controls.rev diff --git a/examples/render/data/arcade_controls.riv b/examples/graphics/data/arcade_controls.riv similarity index 100% rename from examples/render/data/arcade_controls.riv rename to examples/graphics/data/arcade_controls.riv diff --git a/examples/render/data/audio_sampler.rev b/examples/graphics/data/audio_sampler.rev similarity index 100% rename from examples/render/data/audio_sampler.rev rename to examples/graphics/data/audio_sampler.rev diff --git a/examples/render/data/audio_sampler.riv b/examples/graphics/data/audio_sampler.riv similarity index 100% rename from examples/render/data/audio_sampler.riv rename to examples/graphics/data/audio_sampler.riv diff --git a/examples/render/data/car_interface.rev b/examples/graphics/data/car_interface.rev similarity index 100% rename from examples/render/data/car_interface.rev rename to examples/graphics/data/car_interface.rev diff --git a/examples/render/data/car_interface.riv b/examples/graphics/data/car_interface.riv similarity index 100% rename from examples/render/data/car_interface.riv rename to examples/graphics/data/car_interface.riv diff --git a/examples/render/data/charge.riv b/examples/graphics/data/charge.riv similarity index 100% rename from examples/render/data/charge.riv rename to examples/graphics/data/charge.riv diff --git a/examples/render/data/seasynth.rev b/examples/graphics/data/seasynth.rev similarity index 100% rename from examples/render/data/seasynth.rev rename to examples/graphics/data/seasynth.rev diff --git a/examples/render/data/seasynth.riv b/examples/graphics/data/seasynth.riv similarity index 100% rename from examples/render/data/seasynth.riv rename to examples/graphics/data/seasynth.riv diff --git a/examples/render/data/toymachine_3.rev b/examples/graphics/data/toymachine_3.rev similarity index 100% rename from examples/render/data/toymachine_3.rev rename to examples/graphics/data/toymachine_3.rev diff --git a/examples/render/data/toymachine_3.riv b/examples/graphics/data/toymachine_3.riv similarity index 100% rename from examples/render/data/toymachine_3.riv rename to examples/graphics/data/toymachine_3.riv diff --git a/examples/render/data/ui_elements.rev b/examples/graphics/data/ui_elements.rev similarity index 100% rename from examples/render/data/ui_elements.rev rename to examples/graphics/data/ui_elements.rev diff --git a/examples/render/data/ui_elements.riv b/examples/graphics/data/ui_elements.riv similarity index 100% rename from examples/render/data/ui_elements.riv rename to examples/graphics/data/ui_elements.riv diff --git a/examples/graphics/source/examples/Artboard.h b/examples/graphics/source/examples/Artboard.h new file mode 100644 index 000000000..f59761c7d --- /dev/null +++ b/examples/graphics/source/examples/Artboard.h @@ -0,0 +1,115 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace yup +{ + +class ArtboardDemo : public yup::Component +{ +public: + ArtboardDemo() + { + setWantsKeyboardFocus (true); + } + + bool loadArtboard() + { + auto factory = getNativeComponent()->getFactory(); + if (factory == nullptr) + return false; + +#if JUCE_ANDROID + yup::MemoryInputStream is (yup::RiveFile_data, yup::RiveFile_size, false); + auto artboardFile = yup::ArtboardFile::load (is, *factory); + +#else + yup::File riveBasePath; +#if YUP_WASM + riveBasePath = yup::File ("/"); +#else + riveBasePath = yup::File (__FILE__) + .getParentDirectory() + .getParentDirectory() + .getParentDirectory(); +#endif + + auto artboardFile = yup::ArtboardFile::load (riveBasePath.getChildFile (YUP_EXAMPLE_GRAPHICS_RIVE_FILE), *factory); +#endif + if (! artboardFile) + return false; + + // Setup artboards + for (int i = 0; i < totalRows * totalColumns; ++i) + { + auto art = artboards.add (std::make_unique (yup::String ("art") + yup::String (i))); + addAndMakeVisible (art); + + art->setFile (artboardFile.getValue()); + + art->advanceAndApply (i * art->durationSeconds()); + } + + return true; + } + + void refreshDisplay (double lastFrameTimeSeconds) override + { + repaint(); + } + + void resized() override + { + //for (int i = 0; i < totalRows * totalColumns; ++i) + // artboards.getUnchecked (i)->setBounds (getLocalBounds().reduced (100.0f)); + + if (artboards.size() != totalRows * totalColumns) + return; + + auto bounds = getLocalBounds().reduced (10, 20); + auto width = bounds.getWidth() / totalColumns; + auto height = bounds.getHeight() / totalRows; + + for (int i = 0; i < totalRows; ++i) + { + auto row = bounds.removeFromTop (height); + for (int j = 0; j < totalColumns; ++j) + { + auto col = row.removeFromLeft (width); + artboards.getUnchecked (j * totalRows + i)->setBounds (col.largestFittingSquare()); + } + } + } + + void paint (Graphics& g) override + { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); + g.fillAll(); + } + +private: + yup::OwnedArray artboards; + int totalRows = 1; + int totalColumns = 1; +}; + +} // namespace yup diff --git a/examples/graphics/source/examples/Audio.h b/examples/graphics/source/examples/Audio.h index 4fd18726e..2e036afc2 100644 --- a/examples/graphics/source/examples/Audio.h +++ b/examples/graphics/source/examples/Audio.h @@ -237,6 +237,12 @@ class AudioExample oscilloscope.setBounds (bottomBounds); } + void paint (yup::Graphics& g) override + { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); + g.fillAll(); + } + void mouseDown (const yup::MouseEvent& event) override { takeKeyboardFocus(); diff --git a/examples/graphics/source/examples/FileChooser.h b/examples/graphics/source/examples/FileChooser.h index b3764163f..e2e457a2b 100644 --- a/examples/graphics/source/examples/FileChooser.h +++ b/examples/graphics/source/examples/FileChooser.h @@ -29,6 +29,8 @@ class FileChooserDemo : public yup::Component , openFile ("Open File") , openMultipleFiles ("Multiple Files") { + setOpaque (false); + addAndMakeVisible (openFile); openFile.onClick = [this] { @@ -80,6 +82,12 @@ class FileChooserDemo : public yup::Component openMultipleFiles.setBounds (buttons1.removeFromLeft (buttonWidth)); } + void paint (yup::Graphics& g) override + { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); + g.fillAll(); + } + private: yup::TextButton openFile; yup::TextButton openMultipleFiles; diff --git a/examples/graphics/source/examples/LayoutFonts.h b/examples/graphics/source/examples/LayoutFonts.h index dde8c2a81..4cd9b8554 100644 --- a/examples/graphics/source/examples/LayoutFonts.h +++ b/examples/graphics/source/examples/LayoutFonts.h @@ -50,6 +50,9 @@ class LayoutFontsExample : public yup::Component void paint (yup::Graphics& g) override { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); + g.fillAll(); + const int numTexts = yup::numElementsInArray (text); for (int i = 0; i < numTexts; ++i) { diff --git a/examples/graphics/source/examples/OpaqueDemo.h b/examples/graphics/source/examples/OpaqueDemo.h new file mode 100644 index 000000000..f568c6ae5 --- /dev/null +++ b/examples/graphics/source/examples/OpaqueDemo.h @@ -0,0 +1,224 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2025 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +class OpaqueDemo : public Component +{ +public: + OpaqueDemo() + { + auto theme = ApplicationTheme::getGlobalTheme(); + exampleFont = theme->getDefaultFont(); + + // Title label + titleLabel = std::make_unique