diff --git a/napari/_vispy/_tests/test_vispy_scale_bar_visual.py b/napari/_vispy/_tests/test_vispy_scale_bar_visual.py index e899f2f42b2..1e9391ea6b2 100644 --- a/napari/_vispy/_tests/test_vispy_scale_bar_visual.py +++ b/napari/_vispy/_tests/test_vispy_scale_bar_visual.py @@ -1,3 +1,7 @@ +from unittest.mock import MagicMock + +import pytest + from napari._vispy.overlays.scale_bar import VispyScaleBarOverlay from napari.components.overlays import ScaleBarOverlay @@ -9,3 +13,30 @@ def test_scale_bar_instantiation(make_napari_viewer): assert vispy_scale_bar.overlay.length is None model.length = 50 assert vispy_scale_bar.overlay.length == 50 + + +def test_scale_bar_positioning(make_napari_viewer): + viewer = make_napari_viewer() + # set devicePixelRatio to 2 so testing works on CI and local + viewer.window._qt_window.devicePixelRatio = MagicMock(return_value=2) + model = ScaleBarOverlay() + scale_bar = VispyScaleBarOverlay(overlay=model, viewer=viewer) + + assert model.position == 'bottom_right' + assert model.font_size == 10 + assert scale_bar.y_offset == 20 + + model.position = 'top_right' + assert scale_bar.y_offset == pytest.approx(20.333, abs=0.1) + + # increasing size while at top should increase y_offset to 7 + font_size*1.33 + model.font_size = 30 + assert scale_bar.y_offset == pytest.approx(47, abs=0.1) + + # moving scale bar back to bottom should reset y_offset to 20 + model.position = 'bottom_right' + assert scale_bar.y_offset == 20 + + # changing font_size at bottom should have no effect + model.font_size = 50 + assert scale_bar.y_offset == 20 diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py index 94c9e3e0263..d1dc64dd993 100644 --- a/napari/_vispy/overlays/scale_bar.py +++ b/napari/_vispy/overlays/scale_bar.py @@ -135,7 +135,7 @@ def _on_zoom_change(self, *, force: bool = False): self.node.transform.scale = [scale, 1, 1, 1] self.node.text.text = f'{new_dim:g~#P}' self.x_size = scale # needed to offset properly - self._on_position_change() + super()._on_position_change() def _on_data_change(self): """Change color and data of scale bar and box.""" @@ -173,7 +173,37 @@ def _on_box_change(self): def _on_text_change(self): """Update text information""" - self.node.text.font_size = self.overlay.font_size + # update the dpi scale factor to account for screen dpi + # because vispy scales pixel height of text by screen dpi + if self.node.text.transforms.dpi: + # use 96 as the napari reference dpi for historical reasons + dpi_scale_factor = 96 / self.node.text.transforms.dpi + else: + dpi_scale_factor = 1 + + self.node.text.font_size = self.overlay.font_size * dpi_scale_factor + # ensure we recalculate the y_offset from the text size when at top of canvas + if 'top' in self.overlay.position: + self._on_position_change() + + def _on_position_change(self, event=None): + # prevent the text from being cut off by shifting down + if 'top' in self.overlay.position: + # convert font_size to logical pixels as vispy does + # in vispy/visuals/text/text.py + # 72 is the vispy reference dpi + # 96 dpi is used as the napari reference dpi + font_logical_pix = self.overlay.font_size * 96 / 72 + # 7 is base value for the default 10 font size + self.y_offset = 7 + font_logical_pix + else: + self.y_offset = 20 + super()._on_position_change() + + def _on_visible_change(self): + # ensure that dpi is updated when the scale bar is visible + self._on_text_change() + return super()._on_visible_change() def reset(self): super().reset()