diff --git a/docs/changelog.rst b/docs/changelog.rst index 32530ba3..10b949df 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,7 +11,7 @@ New features Visual improvements ~~~~~~~~~~~~~~~~~~~ -- The background of ``napari-matplotlib`` figures and axes is now transparent. +- The background of ``napari-matplotlib`` figures and axes is now transparent, and the text and axis colour respects the ``napari`` theme. - The icons in the Matplotlib toolbar are now the same size as icons in the napari window. Changes diff --git a/src/napari_matplotlib/base.py b/src/napari_matplotlib/base.py index 25b10ee7..07873896 100644 --- a/src/napari_matplotlib/base.py +++ b/src/napari_matplotlib/base.py @@ -38,9 +38,11 @@ class BaseNapariMPLWidget(QWidget): def __init__( self, + napari_viewer: napari.Viewer, parent: Optional[QWidget] = None, ): super().__init__(parent=parent) + self.viewer = napari_viewer self.canvas = FigureCanvas() @@ -50,6 +52,10 @@ def __init__( self.canvas, parent=self ) # type: ignore[no-untyped-call] self._replace_toolbar_icons() + # callback to update when napari theme changed + # TODO: this isn't working completely (see issue #140) + # most of our styling respects the theme change but not all + self.viewer.events.theme.connect(self._on_theme_change) self.setLayout(QVBoxLayout()) self.layout().addWidget(self.toolbar) @@ -69,25 +75,64 @@ def add_single_axes(self) -> None: self.axes = self.figure.subplots() self.apply_napari_colorscheme(self.axes) - @staticmethod - def apply_napari_colorscheme(ax: Axes) -> None: + def apply_napari_colorscheme(self, ax: Axes) -> None: """Apply napari-compatible colorscheme to an Axes.""" + # get the foreground colours from current theme + theme = napari.utils.theme.get_theme(self.viewer.theme, as_dict=False) + fg_colour = theme.foreground.as_hex() # fg is a muted contrast to bg + text_colour = theme.text.as_hex() # text is high contrast to bg + # changing color of axes background to transparent ax.set_facecolor("none") # changing colors of all axes for spine in ax.spines: - ax.spines[spine].set_color("white") + ax.spines[spine].set_color(fg_colour) - ax.xaxis.label.set_color("white") - ax.yaxis.label.set_color("white") + ax.xaxis.label.set_color(text_colour) + ax.yaxis.label.set_color(text_colour) # changing colors of axes labels - ax.tick_params(axis="x", colors="white") - ax.tick_params(axis="y", colors="white") + ax.tick_params(axis="x", colors=text_colour) + ax.tick_params(axis="y", colors=text_colour) + + def _on_theme_change(self) -> None: + """Update MPL toolbar and axis styling when `napari.Viewer.theme` is changed. + + Note: + At the moment we only handle the default 'light' and 'dark' napari themes. + """ + self._replace_toolbar_icons() + if self.figure.gca(): + self.apply_napari_colorscheme(self.figure.gca()) + + def _theme_has_light_bg(self) -> bool: + """ + Does this theme have a light background? + + Returns + ------- + bool + True if theme's background colour has hsl lighter than 50%, False if darker. + """ + theme = napari.utils.theme.get_theme(self.viewer.theme, as_dict=False) + _, _, bg_lightness = theme.background.as_hsl_tuple() + return bg_lightness > 0.5 + + def _get_path_to_icon(self) -> Path: + """ + Get the icons directory (which is theme-dependent). + """ + if self._theme_has_light_bg(): + return ICON_ROOT / "black" + else: + return ICON_ROOT / "white" def _replace_toolbar_icons(self) -> None: - # Modify toolbar icons and some tooltips + """ + Modifies toolbar icons to match the napari theme, and add some tooltips. + """ + icon_dir = self._get_path_to_icon() for action in self.toolbar.actions(): text = action.text() if text == "Pan": @@ -101,7 +146,7 @@ def _replace_toolbar_icons(self) -> None: "Click again to deactivate" ) if len(text) > 0: # i.e. not a separator item - icon_path = os.path.join(ICON_ROOT, text + ".png") + icon_path = os.path.join(icon_dir, text + ".png") action.setIcon(QIcon(icon_path)) @@ -138,9 +183,7 @@ def __init__( napari_viewer: napari.viewer.Viewer, parent: Optional[QWidget] = None, ): - super().__init__(parent=parent) - - self.viewer = napari_viewer + super().__init__(napari_viewer=napari_viewer, parent=parent) self._setup_callbacks() self.layers: List[napari.layers.Layer] = [] @@ -235,22 +278,24 @@ def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def] def _update_buttons_checked(self) -> None: """Update toggle tool icons when selected/unselected.""" super()._update_buttons_checked() + icon_dir = self.parentWidget()._get_path_to_icon() + # changes pan/zoom icons depending on state (checked or not) if "pan" in self._actions: if self._actions["pan"].isChecked(): self._actions["pan"].setIcon( - QIcon(os.path.join(ICON_ROOT, "Pan_checked.png")) + QIcon(os.path.join(icon_dir, "Pan_checked.png")) ) else: self._actions["pan"].setIcon( - QIcon(os.path.join(ICON_ROOT, "Pan.png")) + QIcon(os.path.join(icon_dir, "Pan.png")) ) if "zoom" in self._actions: if self._actions["zoom"].isChecked(): self._actions["zoom"].setIcon( - QIcon(os.path.join(ICON_ROOT, "Zoom_checked.png")) + QIcon(os.path.join(icon_dir, "Zoom_checked.png")) ) else: self._actions["zoom"].setIcon( - QIcon(os.path.join(ICON_ROOT, "Zoom.png")) + QIcon(os.path.join(icon_dir, "Zoom.png")) ) diff --git a/src/napari_matplotlib/icons/black/Back.png b/src/napari_matplotlib/icons/black/Back.png new file mode 100644 index 00000000..d7c65b43 Binary files /dev/null and b/src/napari_matplotlib/icons/black/Back.png differ diff --git a/src/napari_matplotlib/icons/black/Customize.png b/src/napari_matplotlib/icons/black/Customize.png new file mode 100644 index 00000000..9f56bb6d Binary files /dev/null and b/src/napari_matplotlib/icons/black/Customize.png differ diff --git a/src/napari_matplotlib/icons/black/Forward.png b/src/napari_matplotlib/icons/black/Forward.png new file mode 100644 index 00000000..52770f6f Binary files /dev/null and b/src/napari_matplotlib/icons/black/Forward.png differ diff --git a/src/napari_matplotlib/icons/black/Home.png b/src/napari_matplotlib/icons/black/Home.png new file mode 100644 index 00000000..9e527bfd Binary files /dev/null and b/src/napari_matplotlib/icons/black/Home.png differ diff --git a/src/napari_matplotlib/icons/black/Pan.png b/src/napari_matplotlib/icons/black/Pan.png new file mode 100644 index 00000000..36332c34 Binary files /dev/null and b/src/napari_matplotlib/icons/black/Pan.png differ diff --git a/src/napari_matplotlib/icons/black/Pan_checked.png b/src/napari_matplotlib/icons/black/Pan_checked.png new file mode 100644 index 00000000..eb0b908f Binary files /dev/null and b/src/napari_matplotlib/icons/black/Pan_checked.png differ diff --git a/src/napari_matplotlib/icons/black/Save.png b/src/napari_matplotlib/icons/black/Save.png new file mode 100644 index 00000000..79b0d030 Binary files /dev/null and b/src/napari_matplotlib/icons/black/Save.png differ diff --git a/src/napari_matplotlib/icons/black/Subplots.png b/src/napari_matplotlib/icons/black/Subplots.png new file mode 100644 index 00000000..aa15d760 Binary files /dev/null and b/src/napari_matplotlib/icons/black/Subplots.png differ diff --git a/src/napari_matplotlib/icons/black/Zoom.png b/src/napari_matplotlib/icons/black/Zoom.png new file mode 100644 index 00000000..4d2898b7 Binary files /dev/null and b/src/napari_matplotlib/icons/black/Zoom.png differ diff --git a/src/napari_matplotlib/icons/black/Zoom_checked.png b/src/napari_matplotlib/icons/black/Zoom_checked.png new file mode 100644 index 00000000..ad769e66 Binary files /dev/null and b/src/napari_matplotlib/icons/black/Zoom_checked.png differ diff --git a/src/napari_matplotlib/icons/Back.png b/src/napari_matplotlib/icons/white/Back.png similarity index 100% rename from src/napari_matplotlib/icons/Back.png rename to src/napari_matplotlib/icons/white/Back.png diff --git a/src/napari_matplotlib/icons/Customize.png b/src/napari_matplotlib/icons/white/Customize.png similarity index 100% rename from src/napari_matplotlib/icons/Customize.png rename to src/napari_matplotlib/icons/white/Customize.png diff --git a/src/napari_matplotlib/icons/Forward.png b/src/napari_matplotlib/icons/white/Forward.png similarity index 100% rename from src/napari_matplotlib/icons/Forward.png rename to src/napari_matplotlib/icons/white/Forward.png diff --git a/src/napari_matplotlib/icons/Home.png b/src/napari_matplotlib/icons/white/Home.png similarity index 100% rename from src/napari_matplotlib/icons/Home.png rename to src/napari_matplotlib/icons/white/Home.png diff --git a/src/napari_matplotlib/icons/Pan.png b/src/napari_matplotlib/icons/white/Pan.png similarity index 100% rename from src/napari_matplotlib/icons/Pan.png rename to src/napari_matplotlib/icons/white/Pan.png diff --git a/src/napari_matplotlib/icons/Pan_checked.png b/src/napari_matplotlib/icons/white/Pan_checked.png similarity index 100% rename from src/napari_matplotlib/icons/Pan_checked.png rename to src/napari_matplotlib/icons/white/Pan_checked.png diff --git a/src/napari_matplotlib/icons/Save.png b/src/napari_matplotlib/icons/white/Save.png similarity index 100% rename from src/napari_matplotlib/icons/Save.png rename to src/napari_matplotlib/icons/white/Save.png diff --git a/src/napari_matplotlib/icons/Subplots.png b/src/napari_matplotlib/icons/white/Subplots.png similarity index 100% rename from src/napari_matplotlib/icons/Subplots.png rename to src/napari_matplotlib/icons/white/Subplots.png diff --git a/src/napari_matplotlib/icons/Zoom.png b/src/napari_matplotlib/icons/white/Zoom.png similarity index 100% rename from src/napari_matplotlib/icons/Zoom.png rename to src/napari_matplotlib/icons/white/Zoom.png diff --git a/src/napari_matplotlib/icons/Zoom_checked.png b/src/napari_matplotlib/icons/white/Zoom_checked.png similarity index 100% rename from src/napari_matplotlib/icons/Zoom_checked.png rename to src/napari_matplotlib/icons/white/Zoom_checked.png diff --git a/src/napari_matplotlib/tests/baseline/test_example_q_widget.png b/src/napari_matplotlib/tests/baseline/test_example_q_widget.png deleted file mode 100644 index 5b3dcd94..00000000 Binary files a/src/napari_matplotlib/tests/baseline/test_example_q_widget.png and /dev/null differ diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_2D.png b/src/napari_matplotlib/tests/baseline/test_histogram_2D.png index 5b3dcd94..f3f53aea 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_histogram_2D.png and b/src/napari_matplotlib/tests/baseline/test_histogram_2D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_3D.png b/src/napari_matplotlib/tests/baseline/test_histogram_3D.png index 6a09711e..f9320022 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_histogram_3D.png and b/src/napari_matplotlib/tests/baseline/test_histogram_3D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_scatter.png b/src/napari_matplotlib/tests/baseline/test_scatter.png index 1fd7d9e8..1977d45f 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_scatter.png and b/src/napari_matplotlib/tests/baseline/test_scatter.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_slice_2D.png b/src/napari_matplotlib/tests/baseline/test_slice_2D.png index 9636b891..de2cbd42 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_slice_2D.png and b/src/napari_matplotlib/tests/baseline/test_slice_2D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_slice_3D.png b/src/napari_matplotlib/tests/baseline/test_slice_3D.png index 4b974fa4..f5077ea4 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_slice_3D.png and b/src/napari_matplotlib/tests/baseline/test_slice_3D.png differ diff --git a/src/napari_matplotlib/tests/test_theme.py b/src/napari_matplotlib/tests/test_theme.py new file mode 100644 index 00000000..207171cf --- /dev/null +++ b/src/napari_matplotlib/tests/test_theme.py @@ -0,0 +1,20 @@ +import pytest + +from napari_matplotlib.base import NapariMPLWidget + + +@pytest.mark.parametrize( + "theme_name, expected_icons", + [("dark", "white"), ("light", "black")], +) +def test_theme_mpl_toolbar_icons( + make_napari_viewer, theme_name, expected_icons +): + """Check that the icons are taken from the correct folder for each napari theme.""" + viewer = make_napari_viewer() + viewer.theme = theme_name + path_to_icons = NapariMPLWidget(viewer)._get_path_to_icon() + assert path_to_icons.exists(), "The theme points to non-existant icons." + assert ( + path_to_icons.stem == expected_icons + ), "The theme is selecting unexpected icons."