diff --git a/docs/doxygen/overviews/high_dpi.md b/docs/doxygen/overviews/high_dpi.md index 4d33ed102a..ffc6e8fed0 100644 --- a/docs/doxygen/overviews/high_dpi.md +++ b/docs/doxygen/overviews/high_dpi.md @@ -285,6 +285,24 @@ XRC format has been updated to allow specifying wxBitmapBundle with multiple bitmaps or a single SVG image can be used. +Adapting Existing Code To High DPI {#high_dpi_existing_code} +================================== + +Generally speaking, adding support for high DPI to the existing wxWidgets +programs involves doing at least the following: + +1. Not using any hard-coded pixel values outside of `FromDIP()` (note that + this does _not_ apply to XRC). +2. Using wxBitmapBundle containing at least 2 (normal and high DPI) bitmaps + instead of wxBitmap and wxImageList when setting bitmaps. +3. Updating any custom art providers to override + wxArtProvider::CreateBitmapBundle() (and, of course, return multiple bitmaps + from it) instead of wxArtProvider::CreateBitmap(). +4. Removing any calls to wxToolBar::SetToolBitmapSize() or, equivalently, + `` attributes from the XRC, as this forces unwanted scaling. + + + Platform-Specific Build Issues {#high_dpi_platform_specific} ============================== diff --git a/include/wx/bmpbndl.h b/include/wx/bmpbndl.h index 248dd63274..1b137e588a 100644 --- a/include/wx/bmpbndl.h +++ b/include/wx/bmpbndl.h @@ -224,6 +224,29 @@ wxBitmapBundle wxBitmapBundle::FromImage(const wxImage& image) class WXDLLIMPEXP_CORE wxBitmapBundleImpl : public wxRefCounter { protected: + // Standard implementation of GetPreferredBitmapSizeAtScale(): choose the + // scale closest to the given one from the available bitmap scales. + // + // If this function is used, GetNextAvailableScale() must be overridden! + wxSize DoGetPreferredSize(double scale) const; + + // Helper for implementing GetBitmap(): if we need to upscale a bitmap, + // uses GetNextAvailableScale() to find the index of the best bitmap to + // use, where "best" is defined as "using scale which is a divisor of the + // given one", as upscaling by an integer factor is strongly preferable. + size_t GetIndexToUpscale(const wxSize& size) const; + + // Override this function if DoGetPreferredSize() or GetIndexToUpscale() is + // used: it can use the provided parameter as an internal index, it's + // guaranteed to be 0 when calling this function for the first time. When + // there are no more scales, return 0. + // + // This function is not pure virtual because it doesn't need to be + // implemented if DoGetPreferredSize() is never used, but it will assert if + // it's called. + virtual double GetNextAvailableScale(size_t& i) const; + + virtual ~wxBitmapBundleImpl(); public: diff --git a/interface/wx/bmpbndl.h b/interface/wx/bmpbndl.h index 901b56d20b..959919c3c4 100644 --- a/interface/wx/bmpbndl.h +++ b/interface/wx/bmpbndl.h @@ -442,7 +442,8 @@ public: return GetDefaultSize()*scale; ... otherwise, an existing bitmap of the size closest to the - one above would need to be found and its size returned ... + one above would need to be found and its size returned, + possibly by letting DoGetPreferredSize() choose it ... } wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE @@ -488,6 +489,110 @@ public: on demand and cache it. */ virtual wxBitmap GetBitmap(const wxSize& size) = 0; + +protected: + /** + Helper for implementing GetPreferredBitmapSizeAtScale() in the derived + classes. + + This function implements the standard algorithm used inside wxWidgets + itself and tries to find the scale closest to the given one, while also + trying to choose one of the available scales, to avoid actually + rescaling the bitmaps. + + It relies on GetNextAvailableScale() to get information about the + available bitmaps, so that function must be overridden if this one is + used. + + Typically this function is used in the derived classes implementation + to forward GetPreferredBitmapSizeAtScale() to it and when this is done, + GetBitmap() may also use GetIndexToUpscale() to choose the bitmap to + upscale if necessary: + @code + class MyCustomBitmapBundleImpl : public wxBitmapBundleImpl + { + public: + wxSize GetDefaultSize() const + { + return wxSize(32, 32); + } + + wxSize GetPreferredBitmapSizeAtScale(double scale) const wxOVERRIDE + { + return DoGetPreferredSize(scale); + } + + wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE + { + // For consistency with GetNextAvailableScale(), we must have + // bitmap variants for 32, 48 and 64px sizes. + const wxSize availableSizes[] = { 32, 48, 64 }; + if ( size.y <= 64 ) + { + ... get the bitmap from somewhere ... + } + else + { + size_t n = GetIndexToUpscale(size); + bitmap = ... get bitmap for availableSizes[n] ...; + wxBitmap::Rescale(bitmap, size); + } + + return bitmap; + } + + protected: + double GetNextAvailableScale(size_t& i) const wxOVERRIDE + { + const double availableScales[] = { 1, 1.5, 2, 0 }; + + // We can rely on not being called again once we return 0. + return availableScales[i++]; + } + + ... + }; + @endcode + + @param scale The required scale, typically the same one as passed to + GetPreferredBitmapSizeAtScale(). + + @since 3.1.7 + */ + wxSize DoGetPreferredSize(double scale) const; + + /** + Return the index of the available scale most suitable to be upscaled to + the given size. + + See DoGetPreferredSize() for an example of using this function. + + @param size The required size, typically the same one as passed to + GetBitmap() + + @since 3.1.7 + */ + size_t GetIndexToUpscale(const wxSize& size) const; + + /** + Return information about the available bitmaps. + + Overriding this function is optional and only needs to be done if + either DoGetPreferredSize() or GetIndexToUpscale() are called. If you + do override it, this function must return the next available scale or + 0.0 if there are no more. + + The returned scales must be in ascending order and the first returned + scale, for the initial @a i value of 0, should be 1. The function must + change @a i, but the values of this index don't have to be consecutive + and it's only used by this function itself, the caller only initializes + it to 0 before the first call. + + See DoGetPreferredSize() for an example of implementing this function. + + @since 3.1.7 + */ + virtual double GetNextAvailableScale(size_t& i) const; }; /** diff --git a/interface/wx/toolbar.h b/interface/wx/toolbar.h index 8392a2cec7..c021c1ec56 100644 --- a/interface/wx/toolbar.h +++ b/interface/wx/toolbar.h @@ -882,6 +882,10 @@ public: It is usually unnecessary to call this function, as the tools will always be made big enough to fit the size of the bitmaps used in them. + Moreover, calling it may force wxToolBar to scale its images, even + using non-integer scaling factor, which will usually look bad, instead + of adapting the image size to the current DPI scaling in order to avoid + doing this. If you do call it, it must be done before toolbar is Realize()'d. @@ -894,6 +898,9 @@ public: toolbar->Realize(); @endcode + Note that this example would scale bitmaps to 48 pixels when using 150% + DPI scaling, which wouldn't happen without calling SetToolBitmapSize(). + @param size The size of the bitmaps in the toolbar in logical pixels. diff --git a/src/common/artprov.cpp b/src/common/artprov.cpp index 24d85dd622..1cde929a62 100644 --- a/src/common/artprov.cpp +++ b/src/common/artprov.cpp @@ -149,12 +149,19 @@ namespace class wxBitmapBundleImplArt : public wxBitmapBundleImpl { public: - wxBitmapBundleImplArt(const wxArtID& id, + wxBitmapBundleImplArt(const wxBitmap& bitmap, + const wxArtID& id, const wxArtClient& client, - const wxSize& size) + const wxSize& sizeRequested) : m_artId(id), m_artClient(client), - m_sizeDefault(GetValidSize(id, client, size)) + // The bitmap bundle must have the requested size if it was + // specified, but if it wasn't just use the (scale-independent) + // bitmap size. + m_sizeDefault(sizeRequested.IsFullySpecified() + ? sizeRequested + : bitmap.GetDIPSize()), + m_bitmapScale(bitmap.GetScaleFactor()) { } @@ -165,8 +172,8 @@ public: virtual wxSize GetPreferredBitmapSizeAtScale(double scale) const wxOVERRIDE { - // We have no preferred sizes. - return m_sizeDefault*scale; + // Use the standard logic for integer-factor upscaling. + return DoGetPreferredSize(scale); } virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE @@ -174,36 +181,24 @@ public: return wxArtProvider::GetBitmap(m_artId, m_artClient, size); } -private: - static wxSize GetValidSize(const wxArtID& id, - const wxArtClient& client, - const wxSize& size) +protected: + virtual double GetNextAvailableScale(size_t& i) const wxOVERRIDE { - // If valid size is provided, just use it. - if ( size != wxDefaultSize ) - return size; - - // Otherwise, try to get the size we'd use without creating a bitmap - // immediately. - const wxSize sizeHint = wxArtProvider::GetSizeHint(client); - if ( sizeHint != wxDefaultSize ) - return sizeHint; - - // If we really have to, do create a bitmap just to get its size. Note - // we need the size in logical pixels here, it will be scaled later if - // necessary, so use GetDIPSize() and not GetSize(). - const wxBitmap bitmap = wxArtProvider::GetBitmap(id, client); - if ( bitmap.IsOk() ) - return bitmap.GetDIPSize(); - - // We really need some default size, so just return this hardcoded - // value if all else fails -- what else can we do. - return wxSize(16, 16); + // Unfortunately we don't know what bitmap sizes are available here as + // there is simply nothing in wxArtProvider API that returns this (and + // adding something to the API doesn't make sense as all this is only + // used for compatibility with the existing custom art providers -- new + // ones should just override CreateBitmapBundle() directly), so we only + // return the original bitmap scale, but hope that perhaps the provider + // will have other (e.g. x2) scales too, when our GetBitmap() is called. + return i++ ? 0.0 : m_bitmapScale; } +private: const wxArtID m_artId; const wxArtClient m_artClient; const wxSize m_sizeDefault; + const double m_bitmapScale; wxDECLARE_NO_COPY_CLASS(wxBitmapBundleImplArt); }; @@ -322,7 +317,11 @@ wxArtProvider::RescaleOrResizeIfNeeded(wxBitmap& bmp, const wxSize& sizeNeeded) return; #if wxUSE_IMAGE - if ((bmp_h <= sizeNeeded.x) && (bmp_w <= sizeNeeded.y)) + // Allow upscaling by an integer factor: this looks not too horribly and is + // needed to use reasonably-sized bitmaps in the code not yet updated to + // use wxBitmapBundle but using custom art providers. + if ((bmp_w <= sizeNeeded.x) && (bmp_h <= sizeNeeded.y) && + (sizeNeeded.x % bmp_w || sizeNeeded.y % bmp_h)) { // the caller wants default size, which is larger than // the image we have; to avoid degrading it visually by @@ -332,7 +331,7 @@ wxArtProvider::RescaleOrResizeIfNeeded(wxBitmap& bmp, const wxSize& sizeNeeded) img.Resize(sizeNeeded, offset); bmp = wxBitmap(img); } - else // scale (down or mixed, but not up) + else // scale (down or mixed, but not up, or at least not by an int factor) #endif // wxUSE_IMAGE { wxBitmap::Rescale(bmp, sizeNeeded); @@ -438,9 +437,12 @@ wxBitmapBundle wxArtProvider::GetBitmapBundle(const wxArtID& id, // lower priority one: even if this means that the bitmap will be // scaled, at least we'll be using the expected bitmap rather than // potentially using a bitmap of a different style. - if ( provider->CreateBitmap(id, client, size).IsOk() ) + const wxBitmap& bitmap = provider->CreateBitmap(id, client, size); + if ( bitmap.IsOk() ) { - bitmapbundle = wxBitmapBundle::FromImpl(new wxBitmapBundleImplArt(id, client, size)); + bitmapbundle = wxBitmapBundle::FromImpl( + new wxBitmapBundleImplArt(bitmap, id, client, size) + ); break; } } diff --git a/src/common/bmpbndl.cpp b/src/common/bmpbndl.cpp index 51317d3d9d..3316c0b14e 100644 --- a/src/common/bmpbndl.cpp +++ b/src/common/bmpbndl.cpp @@ -26,6 +26,7 @@ #include "wx/icon.h" #include "wx/iconbndl.h" #include "wx/imaglist.h" +#include "wx/scopeguard.h" #include "wx/window.h" #include "wx/private/bmpbndl.h" @@ -132,6 +133,9 @@ public: virtual wxSize GetPreferredBitmapSizeAtScale(double scale) const wxOVERRIDE; virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE; +protected: + virtual double GetNextAvailableScale(size_t& i) const wxOVERRIDE; + private: // Struct containing bitmap itself as well as a flag indicating whether we // generated it by rescaling the existing bitmap or not. @@ -235,52 +239,24 @@ wxSize wxBitmapBundleImplSet::GetDefaultSize() const return m_sizeDefault; } -wxSize wxBitmapBundleImplSet::GetPreferredBitmapSizeAtScale(double scale) const +double wxBitmapBundleImplSet::GetNextAvailableScale(size_t& i) const { - // Target size is the ideal size we'd like the bitmap to have at this scale. - const wxSize sizeTarget = GetDefaultSize()*scale; - - const size_t n = m_entries.size(); - for ( size_t i = 0; i < n; ++i ) + while ( i < m_entries.size() ) { - const wxSize sizeThis = m_entries[i].bitmap.GetSize(); + const Entry& entry = m_entries[i++]; - // Keep looking for the exact match which we still can hope to find - // while the current bitmap is smaller. - if ( sizeThis.y < sizeTarget.y ) + if ( entry.generated ) continue; - // If we've found the exact match, just return it. - if ( sizeThis.y == sizeTarget.y ) - return sizeThis; - - // We've found the closest bigger bitmap. - - // If there is no smaller bitmap, we have no choice but to use this one. - if ( i == 0 ) - return sizeThis; - - // Decide whether we should use this one or the previous smaller one - // depending on which of them is closer to the target size, breaking - // the tie in favour of the bigger size. - const wxSize sizeLast = m_entries[i - 1].bitmap.GetSize(); - - return sizeThis.y - sizeTarget.y <= sizeTarget.y - sizeLast.y - ? sizeThis - : sizeLast; - + return static_cast(entry.bitmap.GetSize().y) / GetDefaultSize().y; } - // We only get here if the target size is bigger than all the available - // sizes, in which case we have no choice but to use the biggest bitmap. - const wxSize sizeMax = m_entries.back().bitmap.GetSize(); + return 0.0; +} - // But check how far is it from the requested scale: if it's more than 1.5 - // times smaller, we should still scale it, notably to ensure that bitmaps - // of standard size are scaled when 2x DPI scaling is used. - return static_cast(sizeTarget.y) / sizeMax.y >= 1.5 - ? sizeTarget - : sizeMax; +wxSize wxBitmapBundleImplSet::GetPreferredBitmapSizeAtScale(double scale) const +{ + return DoGetPreferredSize(scale); } wxBitmap wxBitmapBundleImplSet::GetBitmap(const wxSize& size) @@ -322,24 +298,14 @@ wxBitmap wxBitmapBundleImplSet::GetBitmap(const wxSize& size) // We only get here if the requested size is larger than the size of all // the bitmaps we have, in which case we have no choice but to upscale one - // of the bitmaps, so find the largest available non-generated bitmap. - for ( size_t i = n; n > 0; --i ) - { - const Entry& entry = m_entries[i - 1]; - if ( !entry.generated ) - { - const Entry entryNew(entry, size); + // of the bitmaps, so find the most appropriate one for doing it. + const size_t i = GetIndexToUpscale(size); - m_entries.push_back(entryNew); + const Entry entryNew(m_entries[i], size); - return entryNew.bitmap; - } - } + m_entries.push_back(entryNew); - // We should have at least one non-generated bitmap. - wxFAIL_MSG( wxS("unreachable") ); - - return wxBitmap(); + return entryNew.bitmap; } #ifdef __WXOSX__ @@ -715,6 +681,148 @@ wxBitmapBundle::CreateImageList(wxWindow* win, // wxBitmapBundleImpl implementation // ============================================================================ +double +wxBitmapBundleImpl::GetNextAvailableScale(size_t& WXUNUSED(i)) const +{ + wxFAIL_MSG( wxS("must be overridden if called") ); + + return 0.0; +} + +wxSize +wxBitmapBundleImpl::DoGetPreferredSize(double scaleTarget) const +{ + double scaleBest = 0.0; + double scaleLast = 0.0; + + for ( size_t i = 0;; ) + { + const double scaleThis = GetNextAvailableScale(i); + if ( scaleThis == 0.0 ) + { + // We only get here if the target scale is bigger than all the + // available scales, in which case we have no choice but to use the + // biggest bitmap, which corresponds to the last used scale that we + // should have by now. + wxASSERT_MSG( scaleLast != 0.0, "must have some available scales" ); + + // But check how far is it from the requested scale: if it's more than + // 1.5 times larger, we should still scale it, notably to ensure that + // bitmaps of standard size are scaled when 2x DPI scaling is used. + if ( scaleTarget > 1.5*scaleLast ) + { + // However scaling by non-integer scales doesn't work well at + // all, so try to find a bitmap that we may rescale by an + // integer factor. + // + // Note that this is similar to GetIndexToUpscale(), but we + // don't want to fall back on the largest bitmap here, so we + // can't reuse it. + // + // Also, while we reenter GetNextAvailableScale() here, it + // doesn't matter because we're not going to continue using it + // in the outer loop any more. + for ( i = 0;; ) + { + const double scale = GetNextAvailableScale(i); + if ( scale == 0.0 ) + break; + + const double factor = scaleTarget / scale; + if ( wxRound(factor) == factor ) + { + scaleBest = scaleTarget; + + // We don't need to keep going: if there is a bigger + // bitmap which can be scaled using an integer factor + // to the target scale, our GetIndexToUpscale() will + // find it, we don't need to do it here. + break; + } + } + + // If none of the bitmaps can be upscaled by an integer factor, + // round the target scale itself, as we can be sure to be able + // to scale at least the base bitmap to it using an integer + // factor then. + if ( scaleBest == 0.0 ) + scaleBest = wxRound(scaleTarget); + } + else // Target scale is not much greater than the biggest one we have. + { + scaleBest = scaleLast; + } + + break; + } + + // Ensure we remember the last used scale value. + wxON_BLOCK_EXIT_SET(scaleLast, scaleThis); + + // Keep looking for the exact match which we still can hope to find + // while the current bitmap is smaller. + if ( scaleThis < scaleTarget ) + continue; + + // If we've found the exact match, just return it. + if ( scaleThis == scaleTarget ) + { + scaleBest = scaleThis; + break; + } + + // We've found the closest bigger bitmap. + + // If there is no smaller bitmap, we have no choice but to use this one. + if ( scaleLast == 0.0 ) + { + scaleBest = scaleThis; + break; + } + + // Decide whether we should use this one or the previous smaller one + // depending on which of them is closer to the target size, breaking + // the tie in favour of the smaller size as it's arguably better to use + // slightly smaller bitmaps than too big ones. + scaleBest = scaleThis - scaleTarget < scaleTarget - scaleLast + ? scaleThis + : scaleLast; + break; + } + + return GetDefaultSize()*scaleBest; +} + +size_t wxBitmapBundleImpl::GetIndexToUpscale(const wxSize& size) const +{ + // Our best hope is to find a scale dividing the given one evenly. + size_t indexBest = (size_t)-1; + + // In the worst case, we will use the largest index, as it should hopefully + // result in the least bad results. + size_t indexLast = 0; + + const wxSize sizeDef = GetDefaultSize(); + for ( size_t i = 0;; ) + { + // Save it before it's updated by GetNextAvailableScale(). + size_t indexPrev = i; + + const double scaleThis = GetNextAvailableScale(i); + if ( scaleThis == 0.0 ) + break; + + // Only update it now, knowing that this index could have been used. + indexLast = indexPrev; + + const double scale = size.y / (sizeDef.y*scaleThis); + if (wxRound(scale) == scale) + indexBest = indexLast; + } + + return indexBest != (size_t)-1 ? indexBest : indexLast; +} + wxBitmapBundleImpl::~wxBitmapBundleImpl() { #ifdef __WXOSX__ diff --git a/src/gtk/artgtk.cpp b/src/gtk/artgtk.cpp index 7523452e01..985b83d77c 100644 --- a/src/gtk/artgtk.cpp +++ b/src/gtk/artgtk.cpp @@ -306,22 +306,7 @@ wxBitmap wxGTK2ArtProvider::CreateBitmap(const wxArtID& id, if (stocksize == GTK_ICON_SIZE_INVALID) stocksize = GTK_ICON_SIZE_BUTTON; - GdkPixbuf *pixbuf = CreateGtkIcon(stockid.utf8_str(), stocksize, size); - - if (pixbuf && size != wxDefaultSize && - (size.x != gdk_pixbuf_get_width(pixbuf) || - size.y != gdk_pixbuf_get_height(pixbuf))) - { - GdkPixbuf *p2 = gdk_pixbuf_scale_simple(pixbuf, size.x, size.y, - GDK_INTERP_BILINEAR); - if (p2) - { - g_object_unref (pixbuf); - pixbuf = p2; - } - } - - return wxBitmap(pixbuf); + return wxBitmap(CreateGtkIcon(stockid.utf8_str(), stocksize, size)); } wxIconBundle diff --git a/src/msw/artmsw.cpp b/src/msw/artmsw.cpp index 3acbca568f..6f18c09fdd 100644 --- a/src/msw/artmsw.cpp +++ b/src/msw/artmsw.cpp @@ -203,28 +203,6 @@ protected: const wxSize& size) wxOVERRIDE; }; -static wxBitmap CreateFromStdIcon(const char *iconName, - const wxArtClient& client) -{ - wxIcon icon(iconName); - wxBitmap bmp; - bmp.CopyFromIcon(icon); - - // The standard native message box icons are in message box size (32x32). - // If they are requested in any size other than the default or message - // box size, they must be rescaled first. - if ( client != wxART_MESSAGE_BOX && client != wxART_OTHER ) - { - const wxSize size = wxArtProvider::GetNativeSizeHint(client); - if ( size != wxDefaultSize ) - { - wxBitmap::Rescale(bmp, size); - } - } - - return bmp; -} - wxBitmap wxWindowsArtProvider::CreateBitmap(const wxArtID& id, const wxArtClient& client, const wxSize& size) @@ -251,14 +229,7 @@ wxBitmap wxWindowsArtProvider::CreateBitmap(const wxArtID& id, bitmap = MSWGetBitmapFromIconLocation(sii.szPath, sii.iIcon, sizeNeeded); if ( bitmap.IsOk() ) - { - if ( bitmap.GetSize() != sizeNeeded ) - { - wxBitmap::Rescale(bitmap, sizeNeeded); - } - return bitmap; - } } } #endif // wxHAS_SHGetStockIconInfo @@ -303,7 +274,7 @@ wxBitmap wxWindowsArtProvider::CreateBitmap(const wxArtID& id, name = "wxICON_QUESTION"; if ( name ) - return CreateFromStdIcon(name, client); + return wxIcon(name); } // for anything else, fall back to generic provider: diff --git a/src/msw/bmpbndl.cpp b/src/msw/bmpbndl.cpp index f5ec7b25dd..11bea81577 100644 --- a/src/msw/bmpbndl.cpp +++ b/src/msw/bmpbndl.cpp @@ -144,6 +144,9 @@ public: virtual wxSize GetPreferredBitmapSizeAtScale(double scale) const wxOVERRIDE; virtual wxBitmap GetBitmap(const wxSize& size) wxOVERRIDE; +protected: + virtual double GetNextAvailableScale(size_t& i) const wxOVERRIDE; + private: // Load the bitmap from the given resource and add it m_bitmaps, after // rescaling it to the given size if necessary, i.e. if the needed size is @@ -189,50 +192,21 @@ wxSize wxBitmapBundleImplRC::GetDefaultSize() const return m_bitmaps[0].GetSize(); } +double wxBitmapBundleImplRC::GetNextAvailableScale(size_t& i) const +{ + return i < m_resourceInfos.size() ? m_resourceInfos[i++].scale : 0.0; +} + wxSize wxBitmapBundleImplRC::GetPreferredBitmapSizeAtScale(double scale) const { - // Optimistically assume we're going to use this exact scale by default. - double scalePreferred = scale; - - for ( size_t i = 0; ; ++i ) + const size_t n = m_resourceInfos.size(); + wxVector scales(n); + for ( size_t i = 0; i < n; ++i ) { - if ( i == m_resourceInfos.size() ) - { - // The requested scale is bigger than anything we have, so use the - // biggest available one. - scalePreferred = m_resourceInfos[i - 1].scale; - break; - } - - const double scaleThis = m_resourceInfos[i].scale; - - // Keep looking for the exact match which we still can hope to find - // while the current scale is smaller. - if ( scaleThis < scale ) - continue; - - // If we've found the exact match, just use it. - if ( scaleThis == scale ) - break; - - // We've found the closest bigger scale. - - // If there is no smaller one, we have no choice but to use this one. - if ( i == 0 ) - break; - - // Decide whether we should use this one or the previous smaller one - // depending on which of them is closer to the target scale, breaking - // the tie in favour of the bigger one. - const double scaleLast = m_resourceInfos[i - 1].scale; - - scalePreferred = scaleThis - scale <= scale - scaleLast - ? scaleThis - : scaleLast; - break; + scales[i] = m_resourceInfos[i].scale; } - return GetDefaultSize()*scalePreferred; + return DoGetPreferredSize(scale); } wxBitmap @@ -303,17 +277,19 @@ wxBitmap wxBitmapBundleImplRC::GetBitmap(const wxSize& size) const wxSize sizeThis = sizeDef*info.scale; - // Use this bitmap if it's the first one bigger than the requested size - // or if it's the last item as in this case we're not going to find any - // bitmap bigger than the given one anyhow and we don't have any choice - // but to upscale the largest one we have. - if ( sizeThis.y >= size.y || i + 1 == m_resourceInfos.size() ) + // Use this bitmap if it's the first one bigger than the requested size. + if ( sizeThis.y >= size.y ) return AddBitmap(info, sizeThis, size); } - wxFAIL_MSG( wxS("unreachable") ); + // We have to upscale some bitmap because we don't have any bitmaps larger + // than the requested size. Try to find one which can be upscaled using an + // integer factor. + const size_t i = GetIndexToUpscale(size); - return wxBitmap(); + const ResourceInfo& info = m_resourceInfos[i]; + + return AddBitmap(info, sizeDef*info.scale, size); } // ============================================================================ diff --git a/tests/graphics/bmpbundle.cpp b/tests/graphics/bmpbundle.cpp index a703fe0bec..c90e022e6d 100644 --- a/tests/graphics/bmpbundle.cpp +++ b/tests/graphics/bmpbundle.cpp @@ -15,6 +15,7 @@ #include "wx/bmpbndl.h" #include "wx/artprov.h" +#include "wx/dcmemory.h" #include "asserthelper.h" @@ -49,30 +50,268 @@ TEST_CASE("BitmapBundle::FromBitmaps", "[bmpbundle]") CHECK( b.GetBitmap(wxSize(24, 24)).GetSize() == wxSize(24, 24) ); } +TEST_CASE("BitmapBundle::GetBitmap", "[bmpbundle]") +{ + const wxBitmapBundle b = wxBitmapBundle::FromBitmap(wxBitmap(16, 16)); + + CHECK( b.GetBitmap(wxSize(16, 16)).GetSize() == wxSize(16, 16) ); + CHECK( b.GetBitmap(wxSize(32, 32)).GetSize() == wxSize(32, 32) ); + CHECK( b.GetBitmap(wxSize(24, 24)).GetSize() == wxSize(24, 24) ); +} + +// Helper functions for the test below. +namespace +{ + +// Default size here doesn't really matter. +const wxSize BITMAP_SIZE(16, 16); + +// The choice of colours here is arbitrary too, but they need to be all +// different to allow identifying which bitmap got scaled. +struct ColourAtScale +{ + double scale; + wxUint32 rgb; +}; + +const ColourAtScale colours[] = +{ + { 1.0, 0x000000ff }, + { 1.5, 0x0000ff00 }, + { 2.0, 0x00ff0000 }, +}; + +// Return the colour used for the (original) bitmap at the given scale. +wxColour GetColourForScale(double scale) +{ + wxColour col; + for ( size_t n = 0; n < WXSIZEOF(colours); ++n ) + { + if ( colours[n].scale == scale ) + { + col.SetRGB(colours[n].rgb); + return col; + } + } + + wxFAIL_MSG("no colour for this scale"); + + return col; +} + +double GetScaleFromColour(const wxColour& col) +{ + const wxUint32 rgb = col.GetRGB(); + for ( size_t n = 0; n < WXSIZEOF(colours); ++n ) + { + if ( colours[n].rgb == rgb ) + return colours[n].scale; + } + + wxFAIL_MSG(wxString::Format("no scale for colour %s", col.GetAsString())); + + return 0.0; +} + +double SizeToScale(const wxSize& size) +{ + return static_cast(size.y) / BITMAP_SIZE.y; +} + +wxBitmap MakeSolidBitmap(double scale) +{ + wxBitmap bmp(BITMAP_SIZE*scale); + + wxMemoryDC dc(bmp); + dc.SetBackground(GetColourForScale(scale)); + dc.Clear(); + + return bmp; +} + +wxColour GetBitmapColour(const wxBitmap& bmp) +{ + const wxImage img = bmp.ConvertToImage(); + + // We just assume the bitmap is solid colour, we could check it, but it + // doesn't seem really useful to do it. + return wxColour(img.GetRed(0, 0), img.GetGreen(0, 0), img.GetBlue(0, 0)); +} + +// This struct exists just to allow using it conveniently in CHECK_THAT(). +struct BitmapAtScale +{ + BitmapAtScale(const wxBitmapBundle& b, double scale) + : size(b.GetPreferredBitmapSizeAtScale(scale)), + bitmap(b.GetBitmap(size)) + { + } + + const wxSize size; + const wxBitmap bitmap; +}; + +class BitmapAtScaleMatcher : public Catch::MatcherBase +{ +public: + explicit BitmapAtScaleMatcher(double scale, double scaleOrig) + : m_scale(scale), + m_scaleOrig(scaleOrig) + { + } + + bool match(const BitmapAtScale& bitmapAtScale) const wxOVERRIDE + { + const wxBitmap& bmp = bitmapAtScale.bitmap; + + if ( SizeToScale(bitmapAtScale.size) != m_scale || + SizeToScale(bmp.GetSize()) != m_scale ) + { + m_diffDesc.Printf("should have scale %.1f", m_scale); + } + + if ( GetBitmapColour(bmp) != GetColourForScale(m_scaleOrig) ) + { + if ( m_diffDesc.empty() ) + m_diffDesc = "should be "; + else + m_diffDesc += " and be "; + + m_diffDesc += wxString::Format("created from x%.1f", m_scaleOrig); + } + + return m_diffDesc.empty(); + } + + std::string describe() const wxOVERRIDE + { + return m_diffDesc.utf8_string(); + } + +private: + const double m_scale; + const double m_scaleOrig; + mutable wxString m_diffDesc; +}; + +// The first parameter here determines the size of the expected bitmap and the +// second one, which defaults to the first one if it's not specified, the size +// of the bitmap which must have been scaled to create the bitmap of the right +// size. +BitmapAtScaleMatcher SameAs(double scale, double scaleOrig = 0.0) +{ + if ( scaleOrig == 0.0 ) + scaleOrig = scale; + + return BitmapAtScaleMatcher(scale, scaleOrig); +} + +} // anonymous namespace + +namespace Catch +{ + template <> + struct StringMaker + { + static std::string convert(const BitmapAtScale& bitmapAtScale) + { + const wxBitmap& bmp = bitmapAtScale.bitmap; + + wxString scaleError; + if ( bmp.GetSize() != bitmapAtScale.size ) + { + scaleError.Printf(" (DIFFERENT from expected %.1f)", + SizeToScale(bitmapAtScale.size)); + } + + return wxString::Format + ( + "x%.1f bitmap%s created from x%.1f", + SizeToScale(bmp.GetSize()), + scaleError, + GetScaleFromColour(GetBitmapColour(bmp)) + ).utf8_string(); + } + }; +} + TEST_CASE("BitmapBundle::GetPreferredSize", "[bmpbundle]") { - CHECK( wxBitmapBundle().GetPreferredBitmapSizeAtScale(1) == wxDefaultSize ); + // Check that empty bundle doesn't have any preferred size. + wxBitmapBundle b; + CHECK( b.GetPreferredBitmapSizeAtScale(1) == wxDefaultSize ); - const wxSize normal(32, 32); - const wxSize bigger(64, 64); + const wxBitmap normal = MakeSolidBitmap(1.0); + const wxBitmap middle = MakeSolidBitmap(1.5); + const wxBitmap bigger = MakeSolidBitmap(2.0); - const wxBitmapBundle - b = wxBitmapBundle::FromBitmaps(wxBitmap(normal), wxBitmap(bigger)); + + // Then check what happens if there is only a single bitmap. + b = wxBitmapBundle::FromBitmap(normal); + + // We should avoid scaling as long as the size is close enough to the + // actual bitmap size. + CHECK_THAT( BitmapAtScale(b, 0 ), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1 ), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1.25), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1.4 ), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1.5 ), SameAs(1) ); + + // Once it becomes too big, we're going to need to scale, but we should be + // scaling by an integer factor. + CHECK_THAT( BitmapAtScale(b, 1.75), SameAs(2, 1) ); + CHECK_THAT( BitmapAtScale(b, 2 ), SameAs(2, 1) ); + CHECK_THAT( BitmapAtScale(b, 2.25), SameAs(2, 1) ); + CHECK_THAT( BitmapAtScale(b, 2.5 ), SameAs(3, 1) ); + + + // Now check what happens when there is also a double size bitmap. + b = wxBitmapBundle::FromBitmaps(normal, bigger); // Check that the existing bitmaps are used without scaling for most of the // typical scaling values. - CHECK( b.GetPreferredBitmapSizeAtScale(0 ) == normal ); - CHECK( b.GetPreferredBitmapSizeAtScale(1 ) == normal ); - CHECK( b.GetPreferredBitmapSizeAtScale(1.25) == normal ); - CHECK( b.GetPreferredBitmapSizeAtScale(1.4 ) == normal ); - CHECK( b.GetPreferredBitmapSizeAtScale(1.5 ) == bigger ); - CHECK( b.GetPreferredBitmapSizeAtScale(1.75) == bigger ); - CHECK( b.GetPreferredBitmapSizeAtScale(2 ) == bigger ); - CHECK( b.GetPreferredBitmapSizeAtScale(2.5 ) == bigger ); + CHECK_THAT( BitmapAtScale(b, 0 ), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1 ), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1.25), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1.4 ), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1.5 ), SameAs(1) ); + CHECK_THAT( BitmapAtScale(b, 1.75), SameAs(2) ); + CHECK_THAT( BitmapAtScale(b, 2 ), SameAs(2) ); + CHECK_THAT( BitmapAtScale(b, 2.5 ), SameAs(2) ); + CHECK_THAT( BitmapAtScale(b, 3 ), SameAs(2) ); // This scale is too big to use any of the existing bitmaps, so they will - // be scaled. - CHECK( b.GetPreferredBitmapSizeAtScale(3 ) == 3*normal ); + // be scaled, but use integer factors and, importantly, scale the correct + // bitmap using them: we need to scale the small bitmap by a factor of 3, + // rather than scaling the larger bitmap by a factor of 1.5 here, but we + // must also scale the larger one by a factor of 2 rather than scaling the + // small one by a factor of 4. + CHECK_THAT( BitmapAtScale(b, 3.33), SameAs(3, 1) ); + CHECK_THAT( BitmapAtScale(b, 4 ), SameAs(4, 2) ); + CHECK_THAT( BitmapAtScale(b, 5 ), SameAs(5, 1) ); + + + // Finally check that things work as expected when we have 3 versions. + wxVector bitmaps; + bitmaps.push_back(normal); + bitmaps.push_back(middle); + bitmaps.push_back(bigger); + b = wxBitmapBundle::FromBitmaps(bitmaps); + + CHECK_THAT( BitmapAtScale(b, 0 ), SameAs(1.0) ); + CHECK_THAT( BitmapAtScale(b, 1 ), SameAs(1.0) ); + CHECK_THAT( BitmapAtScale(b, 1.25), SameAs(1.0) ); + CHECK_THAT( BitmapAtScale(b, 1.4 ), SameAs(1.5) ); + CHECK_THAT( BitmapAtScale(b, 1.5 ), SameAs(1.5) ); + CHECK_THAT( BitmapAtScale(b, 1.75), SameAs(1.5) ); + CHECK_THAT( BitmapAtScale(b, 2 ), SameAs(2.0) ); + CHECK_THAT( BitmapAtScale(b, 2.5 ), SameAs(2.0) ); + CHECK_THAT( BitmapAtScale(b, 3 ), SameAs(2.0) ); + + CHECK_THAT( BitmapAtScale(b, 3.33), SameAs(3.0, 1.5) ); + CHECK_THAT( BitmapAtScale(b, 4.25), SameAs(4.0, 2.0) ); + CHECK_THAT( BitmapAtScale(b, 4.50), SameAs(4.5, 1.5) ); + CHECK_THAT( BitmapAtScale(b, 5 ), SameAs(5.0, 1.0) ); } #ifdef wxHAS_DPI_INDEPENDENT_PIXELS