diff --git a/Common/UI/Root.cpp b/Common/UI/Root.cpp index 1bd67325c7..f896770046 100644 --- a/Common/UI/Root.cpp +++ b/Common/UI/Root.cpp @@ -149,17 +149,20 @@ bool IsFocusMovementEnabled() { void LayoutViewHierarchy(const UIContext &dc, const UI::Margins &rootMargins, ViewGroup *root, ViewLayoutMode layoutMode, bool immersiveMode) { Bounds rootBounds = dc.GetLayoutBounds(layoutMode, immersiveMode); - - MeasureSpec horiz(EXACTLY, rootBounds.w - (rootMargins.left + rootMargins.right)); - MeasureSpec vert(EXACTLY, rootBounds.h - (rootMargins.top + rootMargins.bottom)); - - // Two phases - measure contents, layout. - root->Measure(dc, horiz, vert); - // Root has a specified size. Set it, then let root layout all its children. rootBounds.x += rootMargins.left; rootBounds.y += rootMargins.top; rootBounds.w -= rootMargins.left + rootMargins.right; rootBounds.h -= rootMargins.top + rootMargins.bottom; + LayoutViewHierarchy(dc, rootBounds, root); +} + +void LayoutViewHierarchy(const UIContext &dc, const Bounds &rootBounds, ViewGroup *root) { + MeasureSpec horiz(EXACTLY, rootBounds.w); + MeasureSpec vert(EXACTLY, rootBounds.h); + + // Two phases - measure contents, layout. + root->Measure(dc, horiz, vert); + // Root has a specified size. Set it, then let root layout all its children. root->SetBounds(rootBounds); root->Layout(); } diff --git a/Common/UI/Root.h b/Common/UI/Root.h index bf1b0ab76f..d783107194 100644 --- a/Common/UI/Root.h +++ b/Common/UI/Root.h @@ -35,6 +35,7 @@ class ViewGroup; View *FindTabOrderNeighbor(ViewGroup *root, View *focusedView, FocusMove direction); void LayoutViewHierarchy(const UIContext &dc, const UI::Margins &rootMargins, UI::ViewGroup *root, ViewLayoutMode layoutMode, bool immersiveMode); +void LayoutViewHierarchy(const UIContext &dc, const Bounds &rootBounds, UI::ViewGroup *root); DialogResult UpdateViewHierarchy(ViewGroup *root, bool canEnableFocusMovement = true); enum class KeyEventResult { diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp index d1547cf460..d2fdfbd185 100644 --- a/Common/UI/UIScreen.cpp +++ b/Common/UI/UIScreen.cpp @@ -58,7 +58,7 @@ void UIScreen::DoRecreateViews() { // Update layout and refocus so things scroll into view. // This is for resizing down, when focused on something now offscreen. - UI::LayoutViewHierarchy(*screenManager()->getUIContext(), RootMargins(), root_, LayoutMode(), UseImmersiveMode()); + UI::LayoutViewHierarchy(*screenManager()->getUIContext(), GetContentBounds(*screenManager()->getUIContext()), root_); UI::View *focused = UI::GetFocusedView(); if (focused) { root_->SubviewFocused(focused); @@ -128,6 +128,42 @@ Bounds UIScreen::GetLayoutBounds(UIContext &dc) const { return dc.GetLayoutBounds(LayoutMode(), UseImmersiveMode()); } +// On TVs and big monitors, controls out at the edges are hard to see and far apart. Grows with the +// screen, from nothing at 600 high (or wide, if smaller) to the full inset at 1000, so resizing a window +// doesn't make it jump. +static float LargeScreenInset() { + const float fullInset = 40.0f; + if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV) { + return fullInset; + } + const float size = (float)std::min(g_display.dp_xres, g_display.dp_yres); + return fullInset * std::clamp((size - 600.0f) / 400.0f, 0.0f, 1.0f); +} + +Bounds UIScreen::GetContentBounds(const UIContext &dc) const { + const UI::Margins margins = RootMargins(); + Bounds bounds = dc.GetLayoutBounds(LayoutMode(), UseImmersiveMode()); + bounds.x += margins.left; + bounds.y += margins.top; + bounds.w -= margins.horiz(); + bounds.h -= margins.vert(); + if (!LimitContentArea()) { + return bounds; + } + + const float inset = LargeScreenInset(); + bounds.x += inset; + bounds.y += inset; + bounds.w -= inset * 2.0f; + bounds.h -= inset * 2.0f; + const float maxWidth = MaxContentWidth(); + if (maxWidth > 0.0f && bounds.w > maxWidth) { + bounds.x += (bounds.w - maxWidth) * 0.5f; + bounds.w = maxWidth; + } + return bounds; +} + ScreenRenderFlags UIScreen::render(ScreenRenderMode mode) { // A screen rendered under a popup must keep its views: the popup's callbacks point into them (the choice // that opened it, the value it writes to). update() recreates them once the screen is on top again. @@ -137,7 +173,7 @@ ScreenRenderFlags UIScreen::render(ScreenRenderMode mode) { UIContext &uiContext = *screenManager()->getUIContext(); if (root_) { - UI::LayoutViewHierarchy(uiContext, RootMargins(), root_, LayoutMode(), UseImmersiveMode()); + UI::LayoutViewHierarchy(uiContext, GetContentBounds(uiContext), root_); } uiContext.PushTransform({translation_, scale_, alpha_}); diff --git a/Common/UI/UIScreen.h b/Common/UI/UIScreen.h index c1bf67f2ac..42de88b561 100644 --- a/Common/UI/UIScreen.h +++ b/Common/UI/UIScreen.h @@ -41,6 +41,11 @@ public: void OnBack(UI::EventParams &e); virtual UI::Margins RootMargins() const { return UI::Margins(0); } + // Whether to keep the content off the edges of large screens (like TVs), and within MaxContentWidth. + // Off for screens that cover the whole display, like the game. + virtual bool LimitContentArea() const { return false; } + // Wider content is centered at this width. 0 for no limit. + virtual float MaxContentWidth() const { return 0.0f; } virtual void focusChanged(ScreenFocusChange focusChange) override { Screen::focusChanged(focusChange); @@ -56,6 +61,8 @@ protected: virtual bool AllowFocusMovement() const { return true; } Bounds GetLayoutBounds(UIContext &dc) const; + // Where root_ goes: the layout bounds, less RootMargins and the limits above. + Bounds GetContentBounds(const UIContext &dc) const; void RecreateViews() override { recreateViews_ = true; } DeviceOrientation GetDeviceOrientation() const; diff --git a/UI/BaseScreens.h b/UI/BaseScreens.h index cb8d1e0918..c9dd203d38 100644 --- a/UI/BaseScreens.h +++ b/UI/BaseScreens.h @@ -12,6 +12,7 @@ public: return ViewLayoutMode::IgnoreBottomInset; } bool UseImmersiveMode() const override; + bool LimitContentArea() const override { return true; } protected: void sendMessage(UIMessage message, const char *value) override; }; @@ -20,6 +21,10 @@ class UIBaseDialogScreen : public UIDialogScreen { public: UIBaseDialogScreen() : UIDialogScreen(), gamePath_() {} explicit UIBaseDialogScreen(const Path &gamePath) : UIDialogScreen(), gamePath_(gamePath) {} + + bool LimitContentArea() const override { return true; } + float MaxContentWidth() const override { return 1400.0f; } + protected: ViewLayoutMode LayoutMode() const override; bool UseImmersiveMode() const override; diff --git a/UI/ControlMappingScreen.h b/UI/ControlMappingScreen.h index 5cb2491748..bbeb4d9287 100644 --- a/UI/ControlMappingScreen.h +++ b/UI/ControlMappingScreen.h @@ -165,6 +165,8 @@ class MockPSP; class VisualMappingScreen : public UIBaseDialogScreen { public: + // Places the controller picture from the screen bounds. + bool LimitContentArea() const override { return false; } VisualMappingScreen(const Path &gamePath) : UIBaseDialogScreen(gamePath) {} const char *tag() const override { return "VisualMapping"; } diff --git a/UI/CustomButtonMappingScreen.h b/UI/CustomButtonMappingScreen.h index 159866f07a..a857884e4c 100644 --- a/UI/CustomButtonMappingScreen.h +++ b/UI/CustomButtonMappingScreen.h @@ -27,6 +27,8 @@ namespace UI { class CustomButtonMappingScreen : public UISimpleBaseDialogScreen { public: + // Places the button preview from the screen bounds. + bool LimitContentArea() const override { return false; } CustomButtonMappingScreen(DeviceOrientation deviceOrientation, const Path &gamePath, int id) : UISimpleBaseDialogScreen(gamePath, SimpleDialogFlags::Default), deviceOrientation_(deviceOrientation), id_(id) {} const char *tag() const override { return "CustomButton"; } diff --git a/UI/DevScreens.h b/UI/DevScreens.h index ee39634592..5613dc5e93 100644 --- a/UI/DevScreens.h +++ b/UI/DevScreens.h @@ -157,6 +157,8 @@ private: class TouchTestScreen : public UIBaseDialogScreen { public: + // Shows touches where they are on the screen. + bool LimitContentArea() const override { return false; } TouchTestScreen(const Path &gamePath) : UIBaseDialogScreen(gamePath) { for (int i = 0; i < MAX_TOUCH_POINTS; i++) { touches_[i].id = -1; diff --git a/UI/DisplayLayoutScreen.h b/UI/DisplayLayoutScreen.h index ac9b07e8c5..dd7b77059b 100644 --- a/UI/DisplayLayoutScreen.h +++ b/UI/DisplayLayoutScreen.h @@ -31,6 +31,8 @@ class ChoiceStrip; class DisplayLayoutScreen : public UIBaseDialogScreen { public: + // The game's image goes where it will be in the game. + bool LimitContentArea() const override { return false; } DisplayLayoutScreen(const Path &filename); void CreateViews() override; void dialogFinished(const Screen *dialog, DialogResult result) override; diff --git a/UI/GPUDriverTestScreen.h b/UI/GPUDriverTestScreen.h index 66e28b9779..95ee4f1099 100644 --- a/UI/GPUDriverTestScreen.h +++ b/UI/GPUDriverTestScreen.h @@ -12,6 +12,7 @@ class GPUDriverTestScreen : public UIBaseDialogScreen { public: + bool LimitContentArea() const override { return false; } GPUDriverTestScreen(); ~GPUDriverTestScreen(); diff --git a/UI/MiscScreens.h b/UI/MiscScreens.h index ceb8b35ae8..ba7d8a53a6 100644 --- a/UI/MiscScreens.h +++ b/UI/MiscScreens.h @@ -99,6 +99,7 @@ enum class AfterLogoScreen { class LogoScreen : public UIBaseScreen { public: + bool LimitContentArea() const override { return false; } LogoScreen(AfterLogoScreen afterLogoScreen = AfterLogoScreen::DEFAULT); bool key(const KeyInput &key) override; diff --git a/UI/TouchControlLayoutScreen.h b/UI/TouchControlLayoutScreen.h index 5287387737..32fbc4ea2f 100644 --- a/UI/TouchControlLayoutScreen.h +++ b/UI/TouchControlLayoutScreen.h @@ -26,6 +26,8 @@ class ControlLayoutView; class TouchControlLayoutScreen : public UIBaseDialogScreen { public: + // The controls go where they will be in the game. + bool LimitContentArea() const override { return false; } TouchControlLayoutScreen(const Path &gamePath) : UIBaseDialogScreen(gamePath) {} void CreateViews() override;