From 73a20551dfc20e38927337704f475117ee9b0363 Mon Sep 17 00:00:00 2001 From: EmptyChaos Date: Sun, 2 Oct 2016 03:05:55 +0000 Subject: [PATCH] WX: HiDPI Support Framework Portable flexible HiDPI image loading and other support for Windows/GTK/OSX. --- Source/Core/DolphinWX/CMakeLists.txt | 1 + Source/Core/DolphinWX/DolphinSlider.cpp | 126 +++++ Source/Core/DolphinWX/DolphinSlider.h | 51 ++ Source/Core/DolphinWX/DolphinWX.vcxproj | 2 + .../Core/DolphinWX/DolphinWX.vcxproj.filters | 11 +- Source/Core/DolphinWX/WxUtils.cpp | 439 ++++++++++++++++-- Source/Core/DolphinWX/WxUtils.h | 111 ++++- 7 files changed, 706 insertions(+), 35 deletions(-) create mode 100644 Source/Core/DolphinWX/DolphinSlider.cpp create mode 100644 Source/Core/DolphinWX/DolphinSlider.h diff --git a/Source/Core/DolphinWX/CMakeLists.txt b/Source/Core/DolphinWX/CMakeLists.txt index 9a01978bbc..27d7c73385 100644 --- a/Source/Core/DolphinWX/CMakeLists.txt +++ b/Source/Core/DolphinWX/CMakeLists.txt @@ -40,6 +40,7 @@ set(GUI_SRCS NetPlay/NetPlaySetupFrame.cpp NetPlay/NetWindow.cpp NetPlay/PadMapDialog.cpp + DolphinSlider.cpp FifoPlayerDlg.cpp Frame.cpp FrameAui.cpp diff --git a/Source/Core/DolphinWX/DolphinSlider.cpp b/Source/Core/DolphinWX/DolphinSlider.cpp new file mode 100644 index 0000000000..673c5fd4cc --- /dev/null +++ b/Source/Core/DolphinWX/DolphinSlider.cpp @@ -0,0 +1,126 @@ +// Copyright 2016 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include + +#include "DolphinWX/DolphinSlider.h" + +#ifdef __WXMSW__ +#define WIN32_LEAN_AND_MEAN 1 +// clang-format off +#include +#include +// clang-format on +#endif + +static constexpr int SLIDER_MIN_LENGTH = 100; + +DolphinSlider::DolphinSlider() = default; +DolphinSlider::~DolphinSlider() = default; + +bool DolphinSlider::Create(wxWindow* parent, wxWindowID id, int value, int min_val, int max_val, + const wxPoint& pos, const wxSize& size, long style, + const wxValidator& validator, const wxString& name) +{ + // Sanitize the style flags. + // We don't want any label flags because those break DPI scaling on wxMSW, + // wxWidgets will internally lock the height of the slider to 32 pixels. + style &= ~wxSL_LABELS; + + return wxSlider::Create(parent, id, value, min_val, max_val, pos, size, style, validator, name); +} + +wxSize DolphinSlider::DoGetBestClientSize() const +{ +#ifdef __WXMSW__ + int ticks = 0; + int default_length = FromDIP(SLIDER_MIN_LENGTH); + if (HasFlag(wxSL_TICKS)) + { + // NOTE: Ticks do not scale at all (on Win7) + default_length += 4; + ticks = 6; + } + + int metric = 0; + { + // We need to determine the maximum thumb size because unfortunately the thumb size + // is controlled by the theme so may have a varying maximum size limit. + // NOTE: We can't use ourself because we're const so we can't change our size. + // NOTE: This is less inefficient then it seems, DoGetBestSize() is only called once + // per instance and cached until InvalidateBestSize() is called. + wxSlider* helper = new wxSlider(GetParent(), wxID_ANY, GetValue(), GetMin(), GetMax(), + wxDefaultPosition, FromDIP(wxSize(100, 100)), GetWindowStyle()); + ::RECT r{}; + ::SendMessageW(reinterpret_cast(helper->GetHWND()), TBM_GETTHUMBRECT, 0, + reinterpret_cast(&r)); + helper->Destroy(); + + // Breakdown metrics + int computed_size; + int scroll_size; + if (HasFlag(wxSL_VERTICAL)) + { + // Trackbar thumb does not directly touch the edge, we add the padding + // a second time to pad the other edge to make it symmetric. + computed_size = static_cast(r.right + r.left); + scroll_size = ::GetSystemMetrics(SM_CXVSCROLL); + } + else + { + computed_size = static_cast(r.bottom + r.top); + scroll_size = ::GetSystemMetrics(SM_CYHSCROLL); + } + + // This is based on how Microsoft calculates trackbar sizes in the .Net Framework + // when using automatic sizing in WinForms. + int max = scroll_size * 2; + + metric = wxClip(computed_size, scroll_size, max); + } + + if (HasFlag(wxSL_VERTICAL)) + return wxSize(metric + ticks, default_length); + return wxSize(default_length, metric + ticks); +#else + wxSize base_size = wxSlider::DoGetBestClientSize(); + // If the base class is not using DoGetBestClientSize(), fallback to DoGetBestSize() + if (base_size == wxDefaultSize) + return wxDefaultSize; + return CorrectMinSize(base_size); +#endif +} + +wxSize DolphinSlider::DoGetBestSize() const +{ + return CorrectMinSize(wxSlider::DoGetBestSize()); +} + +wxSize DolphinSlider::CorrectMinSize(wxSize size) const +{ + wxSize default_length = FromDIP(wxSize(SLIDER_MIN_LENGTH, SLIDER_MIN_LENGTH)); + // GTK Styles sometimes don't return a default length. + // NOTE: Vertical is the dominant flag if both are set. + if (HasFlag(wxSL_VERTICAL)) + { + if (size.GetHeight() < default_length.GetHeight()) + { + size.SetHeight(default_length.GetHeight()); + } + } + else if (size.GetWidth() < default_length.GetWidth()) + { + size.SetWidth(default_length.GetWidth()); + } + return size; +} + +#ifdef __WXMSW__ +WXLRESULT DolphinSlider::MSWWindowProc(WXUINT msg, WXWPARAM wp, WXLPARAM lp) +{ + if (msg == WM_THEMECHANGED) + InvalidateBestSize(); + return wxSlider::MSWWindowProc(msg, wp, lp); +} +#endif diff --git a/Source/Core/DolphinWX/DolphinSlider.h b/Source/Core/DolphinWX/DolphinSlider.h new file mode 100644 index 0000000000..df3883e478 --- /dev/null +++ b/Source/Core/DolphinWX/DolphinSlider.h @@ -0,0 +1,51 @@ +// Copyright 2016 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include + +// wxSlider has several bugs, including not scaling with DPI. +// This extended slider class tries to paper over the flaws. +// NOTE: wxSL_LABELS is not supported because it doesn't work correctly. +class DolphinSlider : public wxSlider +{ +public: + DolphinSlider(); + ~DolphinSlider() override; + DolphinSlider(const DolphinSlider&) = delete; + + DolphinSlider(wxWindow* parent, wxWindowID id, int value, int min_value, int max_value, + const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, + long style = wxSL_HORIZONTAL, const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxSliderNameStr) + { + Create(parent, id, value, min_value, max_value, pos, size, style, validator, name); + } + + DolphinSlider& operator=(const DolphinSlider&) = delete; + + bool Create(wxWindow* parent, wxWindowID id, int value, int min_value, int max_value, + const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, + long style = wxSL_HORIZONTAL, const wxValidator& validator = wxDefaultValidator, + const wxString& name = wxSliderNameStr); + +#ifdef __WXMSW__ + // For WM_THEMECHANGED to regenerate metrics + WXLRESULT MSWWindowProc(WXUINT msg, WXWPARAM wp, WXLPARAM lp) override; +#endif + +protected: + // DoGetBestSize() in wxMSW::wxSlider is borked. + // This is called by GetEffectiveMinSize() which is used by + // wxSizers to decide the size of the widget for generating layout. + wxSize DoGetBestClientSize() const override; + + // GTK Themes sometimes don't provide a default min size. + // Make other platforms consistent with Windows (i.e. min length = 100px) + wxSize DoGetBestSize() const override; + +private: + wxSize CorrectMinSize(wxSize size) const; +}; diff --git a/Source/Core/DolphinWX/DolphinWX.vcxproj b/Source/Core/DolphinWX/DolphinWX.vcxproj index 43bcc08c8a..d2e6fa30ba 100644 --- a/Source/Core/DolphinWX/DolphinWX.vcxproj +++ b/Source/Core/DolphinWX/DolphinWX.vcxproj @@ -89,6 +89,7 @@ + @@ -131,6 +132,7 @@ + diff --git a/Source/Core/DolphinWX/DolphinWX.vcxproj.filters b/Source/Core/DolphinWX/DolphinWX.vcxproj.filters index 2386e891db..4d597345d1 100644 --- a/Source/Core/DolphinWX/DolphinWX.vcxproj.filters +++ b/Source/Core/DolphinWX/DolphinWX.vcxproj.filters @@ -28,6 +28,9 @@ {9d8b4144-f335-4fa4-b995-852533298474} + + {a894e2e3-e577-4b65-8572-055699f23a49} + @@ -208,6 +211,9 @@ GUI\NetPlay + + GUI\Widgets + @@ -381,6 +387,9 @@ GUI\NetPlay + + GUI\Widgets + @@ -393,4 +402,4 @@ - \ No newline at end of file + diff --git a/Source/Core/DolphinWX/WxUtils.cpp b/Source/Core/DolphinWX/WxUtils.cpp index c9449df3f8..3d86954d76 100644 --- a/Source/Core/DolphinWX/WxUtils.cpp +++ b/Source/Core/DolphinWX/WxUtils.cpp @@ -2,18 +2,29 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. +#include +#include +#include #include #include #include +#include +#include +#include +#include #include #include #include -#include +#include +#include #include +#include #include #include "Common/CommonPaths.h" #include "Common/FileUtil.h" +#include "Common/StringUtil.h" +#include "Core/ConfigManager.h" #include "DolphinWX/WxUtils.h" @@ -57,43 +68,15 @@ void ShowErrorDialog(const wxString& error_msg) wxBitmap LoadResourceBitmap(const std::string& name, const wxSize& padded_size) { - const std::string path_base = File::GetSysDirectory() + RESOURCES_DIR + DIR_SEP + name; - std::string path = path_base + ".png"; - double scale_factor = 1.0; -#ifdef __APPLE__ - if (wxTheApp->GetTopWindow()->GetContentScaleFactor() >= 2) - { - const std::string path_2x = path_base + "@2x.png"; - if (File::Exists(path_2x)) - { - path = path_2x; - scale_factor = 2.0; - } - } -#endif - wxImage image(StrToWxStr(path), wxBITMAP_TYPE_PNG); - - if (padded_size != wxSize()) - { - // Add padding if necessary (or crop, but images aren't supposed to be large enough to require - // that). - // The image will be left-aligned and vertically centered. - const wxSize scaled_padded_size = padded_size * scale_factor; - image.Resize(scaled_padded_size, - wxPoint(0, (scaled_padded_size.GetHeight() - image.GetHeight()) / 2)); - } - -#ifdef __APPLE__ - return wxBitmap(image, -1, scale_factor); -#else - return wxBitmap(image); -#endif + wxWindow* context = wxTheApp->GetTopWindow(); + return LoadScaledResourceBitmap(name, context, padded_size, wxDefaultSize, + LSI_SCALE_DOWN | LSI_ALIGN_VCENTER, *wxWHITE); } wxBitmap CreateDisabledButtonBitmap(const wxBitmap& original) { wxImage image = original.ConvertToImage(); - return wxBitmap(image.ConvertToDisabled(240)); + return wxBitmap(image.ConvertToDisabled(240), wxBITMAP_SCREEN_DEPTH, original.GetScaleFactor()); } void AddToolbarButton(wxToolBar* toolbar, int toolID, const wxString& label, const wxBitmap& bitmap, @@ -105,6 +88,396 @@ void AddToolbarButton(wxToolBar* toolbar, int toolID, const wxString& label, con wxITEM_NORMAL, shortHelp); } +wxRect GetVirtualScreenGeometry() +{ + wxRect geometry; + for (unsigned int i = 0, end = wxDisplay::GetCount(); i < end; ++i) + geometry.Union(wxDisplay(i).GetGeometry()); + return geometry; +} + +void SetWindowSizeAndFitToScreen(wxTopLevelWindow* tlw, wxPoint pos, wxSize size, + wxSize default_size) +{ + if (tlw->IsMaximized()) + return; + + // NOTE: Positions can be negative and still be valid. Coordinates are relative to the + // primary monitor so if the primary monitor is in the middle then (-1000, 10) is a + // valid position on the monitor to the left of the primary. (This does not apply to + // sizes obviously) + wxRect screen_geometry; + wxRect window_geometry{pos, size}; + + if (wxDisplay::GetCount() > 1) + screen_geometry = GetVirtualScreenGeometry(); + else + screen_geometry = wxDisplay(0).GetClientArea(); + + // Initialize the default size if it is wxDefaultSize or otherwise negative. + default_size.DecTo(screen_geometry.GetSize()); + default_size.IncTo(tlw->GetMinSize()); + if (!default_size.IsFullySpecified()) + default_size.SetDefaults(wxDisplay(0).GetClientArea().GetSize() / 2); + + // If the position we're given doesn't make sense then go with the current position. + // (Assuming the window was created with wxDefaultPosition then this should be reasonable) + if (pos.x - screen_geometry.GetLeft() < -1000 || pos.y - screen_geometry.GetTop() < -1000 || + pos.x - screen_geometry.GetRight() > 1000 || pos.y - screen_geometry.GetBottom() > 1000) + { + window_geometry.SetPosition(tlw->GetPosition()); + } + + // If the window is bigger than all monitors combined, or negative (uninitialized) then reset it. + if (window_geometry.IsEmpty() || window_geometry.GetWidth() > screen_geometry.GetWidth() || + window_geometry.GetHeight() > screen_geometry.GetHeight()) + { + window_geometry.SetSize(default_size); + } + + // Check if the window entirely lives on a single monitor without spanning. + // If the window does not span multiple screens then we should constrain it within that + // single monitor instead of the entire virtual desktop space. + // The benefit to doing this is that we can account for the OS X menu bar and Windows task + // bar which are treated as invisible when only looking at the virtual desktop instead of + // an individual screen. + if (wxDisplay::GetCount() > 1) + { + // SPECIAL CASE: If the window is entirely outside the visible area of the desktop then we + // put it back on the primary (zero) monitor. + wxRect monitor_intersection{window_geometry}; + int the_monitor = 0; + if (!monitor_intersection.Intersect(screen_geometry).IsEmpty()) + { + std::array monitors{{wxDisplay::GetFromPoint(monitor_intersection.GetTopLeft()), + wxDisplay::GetFromPoint(monitor_intersection.GetTopRight()), + wxDisplay::GetFromPoint(monitor_intersection.GetBottomLeft()), + wxDisplay::GetFromPoint(monitor_intersection.GetBottomRight())}}; + the_monitor = wxNOT_FOUND; + bool intersected = false; + for (int one_monitor : monitors) + { + if (one_monitor == the_monitor || one_monitor == wxNOT_FOUND) + continue; + if (the_monitor != wxNOT_FOUND) + { + // The window is spanning multiple screens. + the_monitor = wxNOT_FOUND; + break; + } + the_monitor = one_monitor; + intersected = true; + } + // If we get wxNOT_FOUND for all corners then there are holes in the virtual desktop and the + // entire window is lost in one. (e.g. 3 monitors in an 'L', window in top-right) + if (!intersected) + the_monitor = 0; + } + if (the_monitor != wxNOT_FOUND) + { + // We'll only use the client area of this monitor if the window will actually fit. + // (It may not fit if the window is spilling off the edge so it isn't entirely visible) + wxRect client_area{wxDisplay(the_monitor).GetClientArea()}; + if (client_area.GetWidth() >= window_geometry.GetWidth() && + client_area.GetHeight() >= window_geometry.GetHeight()) + { + screen_geometry = client_area; + } + } + } + + // The window SHOULD be small enough to fit on the screen, but it might be spilling off an edge + // so we'll snap it to the nearest edge as necessary. + if (!screen_geometry.Contains(window_geometry)) + { + // NOTE: The order is important here, if the window *is* too big to fit then it will snap to + // the top-left corner. + int spill_x = std::max(0, window_geometry.GetRight() - screen_geometry.GetRight()); + int spill_y = std::max(0, window_geometry.GetBottom() - screen_geometry.GetBottom()); + window_geometry.Offset(-spill_x, -spill_y); + if (window_geometry.GetTop() < screen_geometry.GetTop()) + window_geometry.SetTop(screen_geometry.GetTop()); + if (window_geometry.GetLeft() < screen_geometry.GetLeft()) + window_geometry.SetLeft(screen_geometry.GetLeft()); + } + + tlw->SetSize(window_geometry, wxSIZE_ALLOW_MINUS_ONE); +} + +wxSizer* GiveMinSize(wxWindow* window, const wxSize& min_size) +{ + wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); + int flags = wxEXPAND; + // On Windows comboboxes will misrender when stretched vertically. + if (wxDynamicCast(window, wxChoice) || wxDynamicCast(window, wxComboBox) || + wxDynamicCast(window, wxComboCtrl)) + flags = wxALIGN_CENTER_VERTICAL; + sizer->Add(window, 1, flags); + sizer->SetMinSize(min_size); + return sizer; +} + +wxSizer* GiveMinSizeDIP(wxWindow* window, const wxSize& min_size) +{ + return GiveMinSize(window, window->FromDIP(min_size)); +} + +wxSize GetTextWidgetMinSize(const wxControl* control, const wxString& value) +{ + return control->GetSizeFromTextSize(control->GetTextExtent(value)); +} + +wxSize GetTextWidgetMinSize(const wxControl* control, unsigned int value) +{ + return GetTextWidgetMinSize(control, wxString::Format("%u", value)); +} + +wxSize GetTextWidgetMinSize(const wxControl* control, int value) +{ + return GetTextWidgetMinSize(control, wxString::Format("%d", value)); +} + +wxSize GetTextWidgetMinSize(const wxSpinCtrl* spinner) +{ + wxSize size = GetTextWidgetMinSize(spinner, spinner->GetMin()); + size.IncTo(GetTextWidgetMinSize(spinner, spinner->GetMax())); + return size; +} + +static wxImage LoadScaledImage(const std::string& file_path, const wxWindow* context, + const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags, + const wxColour& fill_color) +{ + std::string fpath, fname, fext; + SplitPath(file_path, &fpath, &fname, &fext); + + const double window_scale_factor = context->GetContentScaleFactor(); + // Compute the total scale factor from the ratio of DIPs to window pixels (FromDIP) and + // window pixels to framebuffer pixels (GetContentScaleFactor). + // NOTE: Usually only one of these is meaningful: + // - On Windows/GTK2: content_scale = 1.0, FromDIP = 96DPI -> Screen DPI + // - On Mac OS X: content_scale = screen_dpi / 96, FromDIP = 96DPI -> 96DPI (no-op) + // [The 1024 is arbitrarily large to minimise rounding error, it has no significance] + const double scale_factor = (context->FromDIP(1024) / 1024.0) * window_scale_factor; + + // We search for files on quarter ratios of DIPs to framebuffer pixels. + // By default, the algorithm prefers to find an exact or bigger size then downscale if + // needed but will resort to upscaling if a bigger image cannot be found. + // E.g. A basic retina screen on Mac OS X has a scale_factor of 2.0, so we would look for + // @2x, @2.25x, @2.5x, @2.75x, @3x, @1.75x, @1.5x, @1.25x, @1x, then give up. + // (At 125% on Windows the search is @1.25, @1.5, @1.75, @2, @2.25, @1) + // If flags does not include LSI_SCALE_DOWN (i.e. we would be forced to crop big + // images instead of scaling them) then we will only accept smaller sizes, i.e. + // @2x, @1.75, @1.5, @1.25, @1, then give up. + // NOTE: We do a lot of exact comparisons against floating point here but it's fine + // because the numbers involved are all powers of 2 so can be represented exactly. + wxImage image; + double selected_image_scale = 1; + { + auto image_check = [&](double scale) -> bool { + std::string path = fpath + fname + StringFromFormat("@%gx", scale) + fext; + if (!File::Exists(path)) + { + // Special Case: @1x may not have a suffix at all. + if (scale != 1.0 || !File::Exists(file_path)) + return false; + path = file_path; + } + if (!image.LoadFile(StrToWxStr(path), wxBITMAP_TYPE_ANY)) + return false; + selected_image_scale = scale; + return true; + }; + const bool prefer_smaller = !(flags & LSI_SCALE_DOWN); + const double scale_factor_quarter = + prefer_smaller ? std::floor(scale_factor * 4) / 4 : std::ceil(scale_factor * 4) / 4; + // Search for bigger sizes first (preferred) + if (!prefer_smaller) + { + // We search within a 'circle' of the exact match limited by scale=1.0. + // i.e. scale_factor = 1.5, radius = 0.5; scale = 2.5, radius = 1.5. + // The minimum radius is 1.0. + double limit = std::max(scale_factor_quarter * 2 - 1, scale_factor_quarter + 1); + for (double quarter = scale_factor_quarter; quarter <= limit; quarter += 0.25) + { + if (image_check(quarter)) + break; + } + } + // If we didn't hit a bigger size then we'll fallback to looking for smaller ones + if (!image.IsOk()) + { + double quarter = scale_factor_quarter; + if (!prefer_smaller) // So we don't recheck the exact match + quarter -= 0.25; + for (; quarter >= 1.0; quarter -= 0.25) + { + if (image_check(quarter)) + break; + } + } + } + + // The file apparently does not exist so we give up. Create a white square placeholder instead. + if (!image.IsOk()) + { + wxLogError("Could not find resource: %s", StrToWxStr(file_path)); + image.Create(1, 1, false); + image.Clear(0xFF); + } + + return ScaleImage(image, selected_image_scale, window_scale_factor, output_size, usable_rect, + flags, fill_color); +} + +wxBitmap LoadScaledBitmap(const std::string& file_path, const wxWindow* context, + const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags, + const wxColour& fill_color) +{ + return wxBitmap(LoadScaledImage(file_path, context, output_size, usable_rect, flags, fill_color), + wxBITMAP_SCREEN_DEPTH, context->GetContentScaleFactor()); +} + +wxBitmap LoadScaledResourceBitmap(const std::string& name, const wxWindow* context, + const wxSize& output_size, const wxRect& usable_rect, + LSIFlags flags, const wxColour& fill_color) +{ + std::string path = File::GetSysDirectory() + RESOURCES_DIR DIR_SEP + name + ".png"; + return LoadScaledBitmap(path, context, output_size, usable_rect, flags, fill_color); +} + +wxBitmap LoadScaledThemeBitmap(const std::string& name, const wxWindow* context, + const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags, + const wxColour& fill_color) +{ + std::string path = File::GetThemeDir(SConfig::GetInstance().theme_name) + name + ".png"; + return LoadScaledBitmap(path, context, output_size, usable_rect, flags, fill_color); +} + +wxBitmap ScaleImageToBitmap(const wxImage& image, const wxWindow* context, + const wxSize& output_size, const wxRect& usable_rect, LSIFlags flags, + const wxColour& fill_color) +{ + double scale_factor = context->GetContentScaleFactor(); + return wxBitmap(ScaleImage(image, 1.0, scale_factor, output_size, usable_rect, flags, fill_color), + wxBITMAP_SCREEN_DEPTH, scale_factor); +} + +wxBitmap ScaleImageToBitmap(const wxImage& image, const wxWindow* context, double source_scale, + LSIFlags flags, const wxColour& fill_color) +{ + double scale_factor = context->GetContentScaleFactor(); + return wxBitmap(ScaleImage(image, source_scale, scale_factor, wxDefaultSize, wxDefaultSize, flags, + fill_color), + wxBITMAP_SCREEN_DEPTH, scale_factor); +} + +wxImage ScaleImage(wxImage image, double source_scale_factor, double content_scale_factor, + wxSize output_size, wxRect usable_rect, LSIFlags flags, + const wxColour& fill_color) +{ + if (!image.IsOk()) + { + wxFAIL_MSG("WxUtils::ScaleImage expects a valid image."); + return image; + } + + if (content_scale_factor != 1.0) + { + output_size *= content_scale_factor; + usable_rect.SetPosition(usable_rect.GetPosition() * content_scale_factor); + usable_rect.SetSize(usable_rect.GetSize() * content_scale_factor); + } + + // Fix the output size if it's unset. + wxSize img_size = image.GetSize(); + if (output_size.GetWidth() < 1) + output_size.SetWidth( + static_cast(img_size.GetWidth() * (content_scale_factor / source_scale_factor))); + if (output_size.GetHeight() < 1) + output_size.SetHeight( + static_cast(img_size.GetHeight() * (content_scale_factor / source_scale_factor))); + + // Fix the usable rect. If it's empty then the whole canvas is usable. + if (usable_rect.IsEmpty()) + { + // Constructs a temp wxRect 0,0->output_size then move assigns it. + usable_rect = output_size; + } + else if (!usable_rect.Intersects(output_size)) + { + wxFAIL_MSG("Usable Zone Rectangle is not inside the canvas. Check the output size is correct."); + image.Create(1, 1, false); + image.SetRGB(0, 0, fill_color.Red(), fill_color.Green(), fill_color.Blue()); + if (fill_color.Alpha() == wxALPHA_TRANSPARENT) + image.SetMaskColour(fill_color.Red(), fill_color.Green(), fill_color.Blue()); + usable_rect = output_size; + } + + // Step 1: Scale the image + if ((flags & LSI_SCALE) != LSI_SCALE_NONE) + { + if (flags & LSI_SCALE_NO_ASPECT) + { + // Stretch scale without preserving the aspect ratio. + bool scale_width = (img_size.GetWidth() > usable_rect.GetWidth() && flags & LSI_SCALE_DOWN) || + (img_size.GetWidth() < usable_rect.GetWidth() && flags & LSI_SCALE_UP); + bool scale_height = + (img_size.GetHeight() > usable_rect.GetHeight() && flags & LSI_SCALE_DOWN) || + (img_size.GetHeight() < usable_rect.GetHeight() && flags & LSI_SCALE_UP); + if (scale_width || scale_height) + { + // NOTE: Using BICUBIC instead of HIGH because it's the same internally + // except that downscaling uses a box filter with awful obvious aliasing + // for non-integral scale factors. + image.Rescale(scale_width ? usable_rect.GetWidth() : img_size.GetWidth(), + scale_height ? usable_rect.GetHeight() : img_size.GetHeight(), + wxIMAGE_QUALITY_BICUBIC); + } + } + else + { + // Scale while preserving the aspect ratio. + double scale = std::min(static_cast(usable_rect.GetWidth()) / img_size.GetWidth(), + static_cast(usable_rect.GetHeight()) / img_size.GetHeight()); + int target_width = static_cast(img_size.GetWidth() * scale); + int target_height = static_cast(img_size.GetHeight() * scale); + // Bilinear produces sharper images when upscaling, bicubic tends to smear/blur sharp edges. + if (scale > 1.0 && flags & LSI_SCALE_UP) + image.Rescale(target_width, target_height, wxIMAGE_QUALITY_BILINEAR); + else if (scale < 1.0 && flags & LSI_SCALE_DOWN) + image.Rescale(target_width, target_height, wxIMAGE_QUALITY_BICUBIC); + } + img_size = image.GetSize(); + } + + // Step 2: Resize the canvas to match the output size. + // NOTE: If NOT using LSI_SCALE_DOWN then this will implicitly crop the image + if (img_size != output_size || usable_rect.GetPosition() != wxPoint()) + { + wxPoint base = usable_rect.GetPosition(); + if (flags & LSI_ALIGN_HCENTER) + base.x += (usable_rect.GetWidth() - img_size.GetWidth()) / 2; + else if (flags & LSI_ALIGN_RIGHT) + base.x += usable_rect.GetWidth() - img_size.GetWidth(); + if (flags & LSI_ALIGN_VCENTER) + base.y += (usable_rect.GetHeight() - img_size.GetHeight()) / 2; + else if (flags & LSI_ALIGN_BOTTOM) + base.y += usable_rect.GetHeight() - img_size.GetHeight(); + + int r = -1, g = -1, b = -1; + if (fill_color.Alpha() != wxALPHA_TRANSPARENT) + { + r = fill_color.Red(); + g = fill_color.Green(); + b = fill_color.Blue(); + } + image.Resize(output_size, base, r, g, b); + } + + return image; +} + } // namespace std::string WxStrToStr(const wxString& str) diff --git a/Source/Core/DolphinWX/WxUtils.h b/Source/Core/DolphinWX/WxUtils.h index 1f7937e819..9b379532ff 100644 --- a/Source/Core/DolphinWX/WxUtils.h +++ b/Source/Core/DolphinWX/WxUtils.h @@ -5,11 +5,18 @@ #pragma once #include +#include #include #include +class wxControl; class wxBitmap; +class wxImage; +class wxSizer; +class wxSpinCtrl; class wxToolBar; +class wxTopLevelWindow; +class wxWindow; namespace WxUtils { @@ -23,7 +30,7 @@ void Explore(const std::string& path); void ShowErrorDialog(const wxString& error_msg); // Reads a PNG from the Resources folder -wxBitmap LoadResourceBitmap(const std::string& name, const wxSize& padded_size = wxSize()); +wxBitmap LoadResourceBitmap(const std::string& name, const wxSize& padded_size = wxDefaultSize); // From a wxBitmap, creates the corresponding disabled version for toolbar buttons wxBitmap CreateDisabledButtonBitmap(const wxBitmap& original); @@ -32,6 +39,108 @@ wxBitmap CreateDisabledButtonBitmap(const wxBitmap& original); void AddToolbarButton(wxToolBar* toolbar, int toolID, const wxString& label, const wxBitmap& bitmap, const wxString& shortHelp); +// Get the dimensions of the virtual desktop that spans all monitors. +// Matches GetSystemMetrics(SM_XVIRTUALSCREEN), etc on Windows. +wxRect GetVirtualScreenGeometry(); + +// Takes a top-level window and resizes / repositions it so it fits on the screen. +// Supports spanning multiple monitors if there are multiple monitors. +// Will snap to edge if the window is small enough to fit but spills over the boundary. +void SetWindowSizeAndFitToScreen(wxTopLevelWindow* tlw, wxPoint pos, wxSize size, + wxSize default_size = wxDefaultSize); + +// wxSizers use the minimum size of a widget when computing layout instead of the best size. +// The best size is only taken when the minsize is -1,-1 (i.e. undefined). +// This means that elements with a MinSize specified will always have that exact size unless +// wxEXPAND-ed. +// This problem can be resolved by wrapping the widget in a sizer and setting the minimum size on +// the sizer instead. Sizers will always use the best size of the widget, treating their own MinSize +// as a floor which is usually what you want. +wxSizer* GiveMinSize(wxWindow* window, const wxSize& min_size); +wxSizer* GiveMinSizeDIP(wxWindow* window, const wxSize& min_size); + +// Compute the proper size for a text widget (wxTextCtrl, wxChoice, wxSpinCtrl, etc) +// Based on the text it will be required to hold. This gives the element the minimum +// width to hold the largest text value instead of being arbitrarily wide. +wxSize GetTextWidgetMinSize(const wxControl* control, const wxString& value); +wxSize GetTextWidgetMinSize(const wxControl* control, unsigned int value); +wxSize GetTextWidgetMinSize(const wxControl* control, int value); +wxSize GetTextWidgetMinSize(const wxSpinCtrl* spinner); + +enum LSIFlags : unsigned int +{ + LSI_SCALE_NONE = 0, // Disable scaling, only resize canvas + LSI_SCALE_UP = 1, // Scale up if needed, but crop instead of scaling down + LSI_SCALE_DOWN = 2, // Scale down if needed, only expand canvas instead of scaling up + LSI_SCALE = LSI_SCALE_UP | LSI_SCALE_DOWN, // Scale either way as needed. + LSI_SCALE_NO_ASPECT = 8, // Disable preserving the aspect ratio of the image. + + LSI_ALIGN_LEFT = 0, // Place image at the left edge of canvas + LSI_ALIGN_RIGHT = 0x10, // Place image at the right edge of canvas + LSI_ALIGN_HCENTER = 0x20, // Place image in the horizontal center of canvas + LSI_ALIGN_TOP = 0, // Place image at the top of the canvas + LSI_ALIGN_BOTTOM = 0x40, // Place image at the bottom of the canvas + LSI_ALIGN_VCENTER = 0x80, // Place image in the vertical center of canvas + + LSI_ALIGN_CENTER = LSI_ALIGN_HCENTER | LSI_ALIGN_VCENTER, + + LSI_DEFAULT = LSI_SCALE | LSI_ALIGN_CENTER +}; +constexpr LSIFlags operator|(LSIFlags left, LSIFlags right) +{ + return static_cast(static_cast(left) | right); +} +constexpr LSIFlags operator&(LSIFlags left, LSIFlags right) +{ + return static_cast(static_cast(left) & right); +} + +// Swiss army knife loader function for preparing a scaled resource image file. +// Only the path and context are mandatory, other parameters can be ignored. +// NOTE: All size parameters are in window pixels, not DIPs or framebuffer pixels. +// output_size = size of image canvas if different from native image size. E.g. 96x32 +// usable_rect = part of image canvas that is considered usable. E.g. 0,0 -> 32,32 +// Usable zone is helpful if the canvas is bigger than the area which will be drawn on screen. +// flags = See LSIFlags +// fill_color = Color to fill the unused canvas area (due to aspect ratio or usable_rect). +wxBitmap LoadScaledBitmap(const std::string& file_path, const wxWindow* context, + const wxSize& output_size = wxDefaultSize, + const wxRect& usable_rect = wxDefaultSize, LSIFlags flags = LSI_DEFAULT, + const wxColour& fill_color = wxTransparentColour); +wxBitmap LoadScaledResourceBitmap(const std::string& name, const wxWindow* context, + const wxSize& output_size = wxDefaultSize, + const wxRect& usable_rect = wxDefaultSize, + LSIFlags flags = LSI_DEFAULT, + const wxColour& fill_color = wxTransparentColour); +wxBitmap LoadScaledThemeBitmap(const std::string& name, const wxWindow* context, + const wxSize& output_size = wxDefaultSize, + const wxRect& usable_rect = wxDefaultSize, + LSIFlags flags = LSI_DEFAULT, + const wxColour& fill_color = wxTransparentColour); + +// Variant of LoadScaledBitmap to scale an image that didn't come from a file. +wxBitmap ScaleImageToBitmap(const wxImage& image, const wxWindow* context, + const wxSize& output_size = wxDefaultSize, + const wxRect& usable_rect = wxDefaultSize, LSIFlags flags = LSI_DEFAULT, + const wxColour& fill_color = wxTransparentColour); + +// Rescales image to screen DPI. +// "Source scale" is essentially the image's DPI as a ratio to 96DPI, e.g. 144DPI image has a +// scale of 1.5. +wxBitmap ScaleImageToBitmap(const wxImage& image, const wxWindow* context, double source_scale, + LSIFlags flags = LSI_DEFAULT, + const wxColour& fill_color = wxTransparentColour); + +// Internal scaling engine behind all the Scaling functions. +// Exposes all control parameters instead of infering them from other sources. +// "Content scale" is a factor applied to output_size and usable_rect internally to convert them +// to framebuffer pixel sizes. +// NOTE: Source scale factor only matters if you don't explicitly specify the output size. +wxImage ScaleImage(wxImage image, double source_scale_factor = 1.0, + double content_scale_factor = 1.0, wxSize output_size = wxDefaultSize, + wxRect usable_rect = wxDefaultSize, LSIFlags flags = LSI_DEFAULT, + const wxColour& fill_color = wxTransparentColour); + } // namespace std::string WxStrToStr(const wxString& str);