diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 8727abd0cc09..22129d4ab346 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -637,12 +637,63 @@ def _set_image_for_button(self, button): path_large = path_regular.with_name( path_regular.name.replace('.png', '_large.png')) size = button.winfo_pixels('18p') + + # Nested functions because ToolbarTk calls _Button. + def _get_color(color_name): + # `winfo_rgb` returns an (r, g, b) tuple in the range 0-65535 + return button.winfo_rgb(button.cget(color_name)) + + def _is_dark(color): + if isinstance(color, str): + color = _get_color(color) + return max(color) < 65535 / 2 + + def _recolor_icon(image, color): + image_data = np.asarray(image).copy() + black_mask = (image_data[..., :3] == 0).all(axis=-1) + image_data[black_mask, :3] = color + return Image.fromarray(image_data, mode="RGBA") + # Use the high-resolution (48x48 px) icon if it exists and is needed with Image.open(path_large if (size > 24 and path_large.exists()) else path_regular) as im: image = ImageTk.PhotoImage(im.resize((size, size)), master=self) - button.configure(image=image, height='18p', width='18p') - button._ntimage = image # Prevent garbage collection. + button._ntimage = image + + # create a version of the icon with the button's text color + foreground = (255 / 65535) * np.array( + button.winfo_rgb(button.cget("foreground"))) + im_alt = _recolor_icon(im, foreground) + image_alt = ImageTk.PhotoImage( + im_alt.resize((size, size)), master=self) + button._ntimage_alt = image_alt + + if _is_dark("background"): + button.configure(image=image_alt) + else: + button.configure(image=image) + # Checkbuttons may switch the background to `selectcolor` in the + # checked state, so check separately which image it needs to use in + # that state to still ensure enough contrast with the background. + if ( + isinstance(button, tk.Checkbutton) + and button.cget("selectcolor") != "" + ): + if self._windowingsystem != "x11": + selectcolor = "selectcolor" + else: + # On X11, selectcolor isn't used directly for indicator-less + # buttons. See `::tk::CheckEnter` in the Tk button.tcl source + # code for details. + r1, g1, b1 = _get_color("selectcolor") + r2, g2, b2 = _get_color("activebackground") + selectcolor = ((r1+r2)/2, (g1+g2)/2, (b1+b2)/2) + if _is_dark(selectcolor): + button.configure(selectimage=image_alt) + else: + button.configure(selectimage=image) + + button.configure(height='18p', width='18p') def _Button(self, text, image_file, toggle, command): if not toggle: diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 986a89d08aa1..d03a7bbb47bb 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -216,3 +216,41 @@ def check_focus(): if success: print("success") + + +@_isolated_tk_test(success_count=2) +def test_embedding(): + import tkinter as tk + from matplotlib.backends.backend_tkagg import ( + FigureCanvasTkAgg, NavigationToolbar2Tk) + from matplotlib.backend_bases import key_press_handler + from matplotlib.figure import Figure + + root = tk.Tk() + + def test_figure(master): + fig = Figure() + ax = fig.add_subplot() + ax.plot([1, 2, 3]) + + canvas = FigureCanvasTkAgg(fig, master=master) + canvas.draw() + canvas.mpl_connect("key_press_event", key_press_handler) + canvas.get_tk_widget().pack(expand=True, fill="both") + + toolbar = NavigationToolbar2Tk(canvas, master, pack_toolbar=False) + toolbar.pack(expand=True, fill="x") + + canvas.get_tk_widget().forget() + toolbar.forget() + + test_figure(root) + print("success") + + # Test with a dark button color. Doesn't actually check whether the icon + # color becomes lighter, just that the code doesn't break. + + root.tk_setPalette(background="sky blue", selectColor="midnight blue", + foreground="white") + test_figure(root) + print("success")