Skip to content

Commit

Permalink
Properly support high DPI images
Browse files Browse the repository at this point in the history
Turns out the high DPI image support introduced in #557 had some
significant issues - most notably, it was rendering crisp high-res
images, but wx then decided to upscale them *again* if your DPI scaling
was set to 200%.

To fix this, we need to move to wx.BitmapBundle. This is intended to be
used by bundling multiple versions of the asset and sticking them all
into a bundle, then having wx choose the most appropriate resolution. Or
use an SVG, statically render a bunch of sizes and stick them all in a
bundle. We don't do that and instead dynamically create them, sticking
only a base resolution asset and the actual version we need in. That way
wx will use the higher-resolution version without applying its own
scaling. This does of course look ugly with checkboxes and such, but
those are temporary anyway - #511 will replace them with SVGs.

Note that we now pass in iconSize for (nearly?) everything, so maybe we
should just make it a required parameter.

Under #511, #557
  • Loading branch information
Infernio committed Dec 29, 2023
1 parent edff669 commit ff276b1
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 21 deletions.
13 changes: 8 additions & 5 deletions Mopy/bash/balt.py
Expand Up @@ -2132,9 +2132,10 @@ def __init__(self, parent):
on_drag_end_forced=self._on_drag_end_forced):
self.buttons[link.uid] = link
self._set_fields_size()
##: Why - 12? I just tried values until it looked good, why does
##: Why - 10? I just tried values until it looked good, why does
# this one work best?
self._native_widget.SetMinHeight(self.icon_size - 12)
self._native_widget.SetMinHeight(self._native_widget.FromDIP(
self.icon_size - 10))
self._draw_buttons()
#--Setup Drag-n-Drop reordering
self._reset_drag(False)
Expand Down Expand Up @@ -2218,10 +2219,11 @@ def _sort_buttons(self, uid_order):

def _draw_buttons(self):
rect = self._native_widget.GetFieldRect(0)
xPos, yPos = rect.x + 4, rect.y
xPos, yPos = rect.x + self._native_widget.FromDIP(4), rect.y
button_spacing = self._native_widget.FromDIP(self.icon_size)
for button_link in self.buttons.values():
button_link.component_position = (xPos, yPos)
xPos += self.icon_size
xPos += button_spacing

def toggle_buttons_visible(self, hide_ids=(), unhide_ids=()):
"""Toggle the visibility of the specified buttons."""
Expand Down Expand Up @@ -2264,7 +2266,8 @@ def _set_fields_size(self):
if wx.Platform != '__WXMSW__':
text_length_px += 10
self._native_widget.SetStatusWidths(
[self.icon_size * len(self.buttons), -1, text_length_px])
[self._native_widget.FromDIP(self.icon_size) * len(self.buttons),
-1, text_length_px])

@classmethod
def set_tooltips(cls):
Expand Down
2 changes: 1 addition & 1 deletion Mopy/bash/basher/links_init.py
Expand Up @@ -62,7 +62,7 @@ def InitStatusBar():
badIcons = [get_image('error_cross.16')] * 3 ##: 16, 24, 32?
__fp = GuiImage.from_path
def _png_list(template):
return [__fp(template % i) for i in (16, 24, 32)]
return [__fp(template % i, iconSize=i) for i in (16, 24, 32)]
def _svg_list(svg_fname):
return [__fp(svg_fname, iconSize=i) for i in (16, 24, 32)]
#--Bash Status/LinkBar
Expand Down
4 changes: 2 additions & 2 deletions Mopy/bash/gui/_gui_globals.py
Expand Up @@ -78,11 +78,11 @@ def _icc(fname, bm_px_size=16):
f'{inst_key}.{col}') or _icc(f'checkbox_{col}_{st}.png')
_gui_images.update(_color_checks)
# PNGs --------------------------------------------------------------------
# Checkboxes ##: some of it loaded with iconSize 16 above - why we use _png??
# Checkboxes
pixs = (16, 24, 32)
for st, col, pix in product(['off', 'on'], ('blue', 'green', 'red'), pixs):
fname = f'checkbox_{col}_{st}%s.png' % ('' if pix == 16 else f'_{pix}')
_gui_images[f'checkbox.{col}.{st}.{pix}'] = GuiImage.from_path(fname)
_gui_images[f'checkbox.{col}.{st}.{pix}'] = _icc(fname, pix)
# SVGs --------------------------------------------------------------------
# Modification time button
_gui_images['calendar.16'] = _icc('calendar.svg')
Expand Down
46 changes: 36 additions & 10 deletions Mopy/bash/gui/images.py
Expand Up @@ -92,7 +92,7 @@ def from_path(cls, img_path: str| Path, imageType=None, iconSize=-1,

class _SvgFromPath(GuiImage):
"""Wrap an svg."""
_native_widget: _svg.SVGimage.ConvertToScaledBitmap
_native_widget: _wx.BitmapBundle.FromBitmaps

@property
def _native_widget(self):
Expand All @@ -103,8 +103,11 @@ def _native_widget(self):
svg_data = svg_data.replace(b'var(--invert)',
b'#FFF' if self._should_invert_svg() else b'#000')
svg_img = _svg.SVGimage.CreateFromBytes(svg_data)
svg_size = scaled(self.iconSize)
self._cached_args = svg_img, (svg_size, svg_size)
# Use a bitmap bundle so we get an actual high-res asset at high
# DPIs, rather than wx deciding to scale up the low-res asset
wanted_svgs = [svg_img.ConvertToScaledBitmap((s, s))
for s in (self.iconSize, scaled(self.iconSize))]
self._cached_args = (wanted_svgs,)
return super()._native_widget

@staticmethod
Expand All @@ -124,7 +127,10 @@ def __init__(self, gui_image):
def _native_widget(self):
if self._is_created(): return self._cached_widget
native = super()._native_widget # create a plain wx.Icon
native.CopyFromBitmap(self._resolve(self._gui_image))
native_bmp = self._resolve(self._gui_image)
if isinstance(native_bmp, _wx.BitmapBundle):
native_bmp = native_bmp.GetBitmap(native_bmp.GetDefaultSize())
native.CopyFromBitmap(native_bmp)
return native

class _IcoFromPath(GuiImage):
Expand Down Expand Up @@ -177,7 +183,9 @@ def _native_widget(self):
self._cached_args = self._img_path, self._img_type
native = super()._native_widget
if self.iconSize != -1:
wanted_size = scaled(self.iconSize)
# Don't use the scaled icon size here - _BmpFromPath performs its
# own scaling and Screen_ConvertTo wouldn't want to scale anyways
wanted_size = self.iconSize
if self.get_img_size() != (wanted_size, wanted_size):
native.Rescale(wanted_size, wanted_size,
_wx.IMAGE_QUALITY_HIGH)
Expand All @@ -189,12 +197,23 @@ def save_bmp(self, imagePath, exten='.jpg'):
return self._native_widget.SaveFile(imagePath, self.img_types[exten])

class _BmpFromPath(GuiImage):
_native_widget: _wx.Bitmap
_native_widget: _wx.BitmapBundle.FromBitmaps

@property
def _native_widget(self):
self._cached_args = ImgFromPath(self._img_path, self.iconSize,
self._img_type)._native_widget, # pass wx.Image to wx.Bitmap
# Pass wx.Image to wx.Bitmap
base_img: _wx.Image = self._resolve(ImgFromPath(self._img_path,
imageType=self._img_type))
scaled_imgs = [base_img]
if self.iconSize != -1:
# If we can, also add a scaled-up version so wx stops trying to
# scale this by itself - using a higher-res image here if we have
# one would be better, but that would be very difficult to
# implement, something for the (far) future
wanted_size = scaled(self.iconSize)
scaled_imgs.append(base_img.Scale(wanted_size, wanted_size,
quality=_wx.IMAGE_QUALITY_HIGH))
self._cached_args = (list(map(_wx.Bitmap, scaled_imgs)),)
return super()._native_widget

class BmpFromStream(GuiImage):
Expand Down Expand Up @@ -257,9 +276,16 @@ def _native_widget(self):
def native_init(self, *args, **kwargs):
kwargs.setdefault('recreate', False)
freshly_created = super().native_init(*args, **kwargs)
##: Accessing these like this feels wrong - maybe store the scaled size
# somewhere and retrieve it here?
scaled_sb_size = self._cached_args[0:2]
if freshly_created: # ONCE! we don't support adding more images
self._indices = {k: self._native_widget.Add(self._resolve(im)) for
k, im in self._images}
self._indices = {}
for k, im in self._images:
nat_img = self._resolve(im)
if isinstance(nat_img, _wx.BitmapBundle):
nat_img = nat_img.GetBitmap(scaled_sb_size)
self._indices[k] = self._native_widget.Add(nat_img)

def img_dex(self, *args) -> int | None:
"""Return the index of the specified image in the native control."""
Expand Down
12 changes: 9 additions & 3 deletions Mopy/bash/gui/misc_components.py
Expand Up @@ -92,6 +92,13 @@ def set_bitmap(self, bmp):
caching"""
if isinstance(bmp, Path):
bmp = (bmp.is_file() and GuiImage.from_path(bmp)) or None
if bmp is not None:
bmp = self._resolve(bmp)
# If bmp comes from a BmpFromStream, this will be a bitmap; if it
# comes from a _BmpFromPath, it will be a bitmap bundle (with only
# one bitmap in it)
if isinstance(bmp, _wx.BitmapBundle):
bmp = bmp.GetBitmap(bmp.GetDefaultSize())
self._gui_bitmap = bmp
self._handle_resize()
return self._gui_bitmap
Expand All @@ -106,14 +113,13 @@ def _handle_resize(self): ##: is all these wx.Bitmap calls needed? One right way
dc.SetBackground(self.background)
dc.Clear()
if self._gui_bitmap is not None:
##: wrap _native_widget calls below - a better way?
old_x,old_y = self._gui_bitmap._native_widget.GetSize()
old_x,old_y = self._gui_bitmap.GetSize()
scale = min(float(x)/old_x, float(y)/old_y)
new_x = old_x * scale
new_y = old_y * scale
pos_x = max(0,x-new_x)/2
pos_y = max(0,y-new_y)/2
image = self._gui_bitmap._native_widget.ConvertToImage()
image = self._gui_bitmap.ConvertToImage()
image.Rescale(int(new_x), int(new_y), _wx.IMAGE_QUALITY_HIGH)
dc.DrawBitmap(_wx.Bitmap(image), int(pos_x), int(pos_y))
del dc
Expand Down

0 comments on commit ff276b1

Please sign in to comment.