diff --git a/examples/DemoRunner/Source/UI/MainComponent.cpp b/examples/DemoRunner/Source/UI/MainComponent.cpp index f121892292..474c3e69ee 100644 --- a/examples/DemoRunner/Source/UI/MainComponent.cpp +++ b/examples/DemoRunner/Source/UI/MainComponent.cpp @@ -308,6 +308,7 @@ MainComponent::MainComponent() demosPanel.setTitle ("Demos"); demosPanel.setFocusContainerType (FocusContainerType::focusContainer); + demosPanel.setContentRestrictedToSafeArea (true); showDemosButton.onClick = [this] { demosPanel.showOrHide (true); }; diff --git a/modules/juce_gui_basics/desktop/juce_Displays.h b/modules/juce_gui_basics/desktop/juce_Displays.h index dfb46d3c40..e047b96e80 100644 --- a/modules/juce_gui_basics/desktop/juce_Displays.h +++ b/modules/juce_gui_basics/desktop/juce_Displays.h @@ -56,19 +56,41 @@ public: /** The total area of this display in logical pixels including any OS-dependent objects like the taskbar, menu bar, etc. + + On mobile (Android, iOS) this is the full area of the display. */ Rectangle totalArea; /** The total area of this display in logical pixels which isn't covered by OS-dependent objects like the taskbar, menu bar, etc. + + On mobile (iOS, Android), the system UI will be made transparent whenever possible, and + the JUCE app may draw behind these bars. Therefore, on these platforms, the userArea + is *not* restricted by the system UI. Instead, potentially-obscured areas of the + display can be found by querying the safeAreaInsets and keyboardInsets. + + Mobile platforms that support multiple windows (e.g. Android in split screen) will + return the screen area currently available to the application here. The resulting + area may be significantly smaller than the total screen area, but may overlap the + system decorations. */ Rectangle userArea; /** Represents the area of this display in logical pixels that is not functional for displaying content. + These insets are applied relative to the userArea. + On mobile devices this may be the area covered by display cutouts and notches, where you still want to draw a background but should not position important content. + + Note that these insets may change depending on the current state of the system. + As a simple example, entering/leaving kiosk mode may cause the system UI visibility + to change, which may affect the safe areas. + A more complex example would be split-screen state on Android, where an activity + occupying the top portion of the screen is likely to have insets for the status bar but + not the navigation bar, whereas an activity on the bottom may have navigation insets + but not status insets. */ BorderSize safeAreaInsets; diff --git a/modules/juce_gui_basics/layout/juce_SidePanel.cpp b/modules/juce_gui_basics/layout/juce_SidePanel.cpp index 6d3ccb9a21..2b0ffdd2ff 100644 --- a/modules/juce_gui_basics/layout/juce_SidePanel.cpp +++ b/modules/juce_gui_basics/layout/juce_SidePanel.cpp @@ -131,6 +131,24 @@ void SidePanel::resized() calculateAndRemoveShadowBounds (bounds); + const auto fullScreen = std::invoke ([&] + { + if (auto* peer = getPeer()) + return peer->isFullScreen(); + + return false; + }); + + if (fullScreen && isContentRestrictedToSafeArea() && parent != nullptr) + { + if (auto* display = Desktop::getInstance().getDisplays().getDisplayForRect (parent->getScreenBounds())) + { + const auto safeArea = display->safeAreaInsets.subtractedFrom (display->keyboardInsets.subtractedFrom (display->userArea)); + const auto safeAreaInLocalSpace = getLocalArea (nullptr, safeArea) + getCurrentOffset(); + bounds = bounds.getIntersection (safeAreaInLocalSpace); + } + } + auto titleBounds = bounds.removeFromTop (titleBarHeight); if (titleBarComponent != nullptr) @@ -268,18 +286,25 @@ void SidePanel::changeListenerCallback (ChangeBroadcaster*) } } -Rectangle SidePanel::calculateBoundsInParent (Component& parentComp) const +Rectangle SidePanel::calculateShowingBoundsInParent (Component& parentComp) const { auto parentBounds = parentComp.getLocalBounds(); - if (isOnLeft) - { - return isShowing ? parentBounds.removeFromLeft (panelWidth) - : parentBounds.withX (parentBounds.getX() - panelWidth).withWidth (panelWidth); - } + return isOnLeft ? parentBounds.removeFromLeft (panelWidth) + : parentBounds.removeFromRight (panelWidth); +} - return isShowing ? parentBounds.removeFromRight (panelWidth) - : parentBounds.withX (parentBounds.getRight()).withWidth (panelWidth); +Point SidePanel::getCurrentOffset() const +{ + if (isShowing) + return {}; + + return { isOnLeft ? -panelWidth : panelWidth, 0 }; +} + +Rectangle SidePanel::calculateBoundsInParent (Component& parentComp) const +{ + return calculateShowingBoundsInParent (parentComp) + getCurrentOffset(); } void SidePanel::calculateAndRemoveShadowBounds (Rectangle& bounds) diff --git a/modules/juce_gui_basics/layout/juce_SidePanel.h b/modules/juce_gui_basics/layout/juce_SidePanel.h index 49a4b18eaa..a084096c09 100644 --- a/modules/juce_gui_basics/layout/juce_SidePanel.h +++ b/modules/juce_gui_basics/layout/juce_SidePanel.h @@ -154,6 +154,17 @@ public: /** Returns the text that is displayed in the title bar at the top of the SidePanel. */ String getTitleText() const noexcept { return titleLabel.getText(); } + /** @see isContentRestrictedToSafeArea() */ + void setContentRestrictedToSafeArea (bool x) noexcept { restrictToSafeArea = x; } + + /** When true, will avoid displaying menu content within areas of the screen that may be + obscured by display cutouts or operating system decorations. When false, the menu's + content will entirely fill the menu bounds. True by default. + + @see setContentRestrictedToSafeArea() + */ + bool isContentRestrictedToSafeArea() const noexcept { return restrictToSafeArea; } + //============================================================================== /** This abstract base class is implemented by LookAndFeel classes to provide SidePanel drawing functionality. @@ -230,12 +241,15 @@ private: int amountMoved = 0; bool shouldShowDismissButton = true; + bool restrictToSafeArea = true; //============================================================================== void lookAndFeelChanged() override; void componentMovedOrResized (Component&, bool wasMoved, bool wasResized) override; void changeListenerCallback (ChangeBroadcaster*) override; + Rectangle calculateShowingBoundsInParent (Component&) const; + Point getCurrentOffset() const; Rectangle calculateBoundsInParent (Component&) const; void calculateAndRemoveShadowBounds (Rectangle& bounds);