From 763c523bdb92a2316f7a995bda7752e47db33cab Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Sat, 29 Aug 2026 14:26:16 +0200 Subject: [PATCH] [CTabFolder] Ask the renderer for the background of tab controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updateBkImages() handed a control the flat getBackground() when it was wrapped below the tab row, or when no gradient was set. That only holds for the built-in renderer. One painting its own PART_BACKGROUND, like the IDE's CTabRendering, uses different colors for tab row and body, so a wrapped view tool bar showed a block of the tab row color on the body strip. A custom renderer is now asked for the background. The image is prefilled with getBackground() first, since a renderer may leave pixels untouched that on screen show the widget background. Adds Snippet396. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../org/eclipse/swt/custom/CTabFolder.java | 15 +- examples/org.eclipse.swt.snippets/Snippets.md | 2 + .../org/eclipse/swt/snippets/Snippet396.java | 91 ++++++++++++ ...est_org_eclipse_swt_custom_CTabFolder.java | 139 ++++++++++++++++++ 4 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet396.java diff --git a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java index 418da72a628..62b89da420f 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CTabFolder.java @@ -4096,7 +4096,8 @@ void updateBkImages(boolean colorChanged) { int tabHeight = getTabHeight(); int height = this.getSize().y; boolean wrapped = onBottom ? bounds.y + bounds.height < height - tabHeight : bounds.y > tabHeight; - if (wrapped || gradientColors == null) { + // only a custom renderer can paint something else here + if (useDefaultRenderer && (gradientColors == null || wrapped)) { bkImageBounds[i]=null; control.setBackgroundImage(null); control.setBackground(getBackground()); @@ -4110,11 +4111,21 @@ void updateBkImages(boolean colorChanged) { bounds.y = -1; } bounds.x = 0; + if (bounds.height <= 0) { + // drop the cache, so a skipped color change is not lost + bkImageBounds[i] = null; + continue; + } // do not redraw when only translated: if (colorChanged || !bounds.equals(bkImageBounds[i])) { bkImageBounds[i] = bounds; if (controlBkImages[i] != null) controlBkImages[i].dispose(); - controlBkImages[i] = new Image(control.getDisplay(), (gc, imageWidth, imageHeight) -> renderer.draw(CTabFolderRenderer.PART_BACKGROUND, 0, bounds, gc), bounds.width, bounds.height); + controlBkImages[i] = new Image(control.getDisplay(), (gc, imageWidth, imageHeight) -> { + // pixels a renderer leaves untouched would stay blank in an image + gc.setBackground(getBackground()); + gc.fillRectangle(0, 0, imageWidth, imageHeight); + renderer.draw(CTabFolderRenderer.PART_BACKGROUND, 0, bounds, gc); + }, bounds.width, bounds.height); control.setBackground(null); control.setBackgroundImage(controlBkImages[i]); } diff --git a/examples/org.eclipse.swt.snippets/Snippets.md b/examples/org.eclipse.swt.snippets/Snippets.md index 08cd32f387c..84ee644a736 100644 --- a/examples/org.eclipse.swt.snippets/Snippets.md +++ b/examples/org.eclipse.swt.snippets/Snippets.md @@ -61,6 +61,7 @@ To contribute a new snippet, [create a snippet contribution as a pull request](h - [create a non-rectangular button](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet294.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet294.png "Preview for Snippet 294") - [create buttons with wrapped titles](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet345.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet345.png "Preview for Snippet 345") + ### **Canvas** - [paint a circle in a canvas](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet245.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet245.png "Preview for Snippet 245") - [scroll an image (flicker free, no double buffering)](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet48.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet48.png "Preview for Snippet 48") @@ -119,6 +120,7 @@ To contribute a new snippet, [create a snippet contribution as a pull request](h - [min and max buttons, close button and image only on selected tab](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet165.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet165.png "Preview for Snippet 165") - [demonstration of a multi line CTabFolder](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet371.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet371.png "Preview for Snippet 371") - [dirty indicator using bullet dot on close button with theme switching](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet394.java) +- [background of a top right control that wraps below the tab row](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet396.java) ### **Cursor** - [set the hand cursor into a control](https://github.com/eclipse-platform/eclipse.platform.swt/tree/master/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet44.java) – [(preview)](https://github.com/eclipse-platform/eclipse.platform.swt/blob/master/examples/org.eclipse.swt.snippets/previews/Snippet44.png "Preview for Snippet 44") diff --git a/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet396.java b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet396.java new file mode 100644 index 00000000000..818a49b7b5d --- /dev/null +++ b/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet396.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.snippets; + +/* + * CTabFolder example: background of a top right control that wrapped below the + * tab row. Widen the shell until the tool bar fits next to the tabs. + * + * For a list of all SWT example snippets see + * http://www.eclipse.org/swt/snippets/ + */ +import org.eclipse.swt.*; +import org.eclipse.swt.custom.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.layout.*; +import org.eclipse.swt.widgets.*; + +public class Snippet396 { + static final Color TAB_ROW = new Color(0x21, 0x22, 0x2C); + static final Color BODY = new Color(0x28, 0x2A, 0x36); + + /** Paints tab row and body in two different colors. */ + static class TwoToneRenderer extends CTabFolderRenderer { + TwoToneRenderer(CTabFolder parent) { + super(parent); + } + + @Override + protected void draw(int part, int state, Rectangle bounds, GC gc) { + if (part == PART_BACKGROUND) { + int split = bounds.y + parent.getTabHeight(); + gc.setBackground(TAB_ROW); + gc.fillRectangle(bounds.x, bounds.y, bounds.width, split - bounds.y); + gc.setBackground(BODY); + gc.fillRectangle(bounds.x, split, bounds.width, bounds.y + bounds.height - split); + return; + } + super.draw(part, state, bounds, gc); + if (part == PART_HEADER) { + int split = bounds.y + parent.getTabHeight() + 3; + gc.setBackground(BODY); + gc.fillRectangle(bounds.x, split, bounds.width, bounds.y + bounds.height - split); + } + } + } + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("Wrapped top right control"); + shell.setLayout(new FillLayout()); + + CTabFolder folder = new CTabFolder(shell, SWT.NONE); + folder.setRenderer(new TwoToneRenderer(folder)); + folder.setBackground(TAB_ROW); + folder.setForeground(display.getSystemColor(SWT.COLOR_WHITE)); + + for (String name : new String[] { "Variables", "Breakpoints", "Expressions" }) { + CTabItem item = new CTabItem(folder, SWT.NONE); + item.setText(name); + Composite content = new Composite(folder, SWT.NONE); + content.setBackground(BODY); + item.setControl(content); + } + folder.setSelection(0); + + Composite topRight = new Composite(folder, SWT.NONE); + topRight.setLayout(new FillLayout()); + ToolBar toolBar = new ToolBar(topRight, SWT.FLAT); + for (String text : new String[] { "Collapse", "Expand", "Filter", "Menu" }) { + new ToolItem(toolBar, SWT.PUSH).setText(text); + } + folder.setTopRight(topRight, SWT.RIGHT | SWT.WRAP); + + shell.setSize(360, 200); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + display.dispose(); + } +} diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java index 66ca7167ca3..a7515c5cd07 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_custom_CTabFolder.java @@ -39,13 +39,17 @@ import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabFolder2Listener; import org.eclipse.swt.custom.CTabFolderEvent; +import org.eclipse.swt.custom.CTabFolderRenderer; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.internal.DPIUtil; import org.eclipse.swt.layout.FillLayout; @@ -1166,6 +1170,141 @@ public void test_moveItem_errorCases() { "out-of-range to index must be rejected"); } +/* Paints tab row and body in two colors, neither of them the folder background. */ +private static final RGB TAB_ROW_COLOR = new RGB(255, 0, 0); +private static final RGB BODY_COLOR = new RGB(0, 255, 0); +private static final RGB FOLDER_BACKGROUND = new RGB(0, 0, 255); + +private static final class TwoToneRenderer extends CTabFolderRenderer { + TwoToneRenderer(CTabFolder parent) { + super(parent); + } + + @Override + protected void draw(int part, int state, Rectangle bounds, GC gc) { + if (part == PART_BACKGROUND) { + int split = bounds.y + parent.getTabHeight(); + gc.setBackground(new Color(TAB_ROW_COLOR)); + gc.fillRectangle(bounds.x, bounds.y, bounds.width, split - bounds.y); + gc.setBackground(new Color(BODY_COLOR)); + gc.fillRectangle(bounds.x, split, bounds.width, bounds.y + bounds.height - split); + return; + } + super.draw(part, state, bounds, gc); + } +} + +/** A custom renderer that leaves PART_BACKGROUND to the built-in implementation. */ +private static final class PlainSubclassRenderer extends CTabFolderRenderer { + PlainSubclassRenderer(CTabFolder parent) { + super(parent); + } +} + +/** The color a control shows: its background image if it has one, else its background. */ +private static RGB effectiveBackground(Control control) { + Image image = control.getBackgroundImage(); + if (image != null) { + ImageData data = image.getImageData(); + return data.palette.getRGB(data.getPixel(0, 0)); + } + return control.getBackground().getRGB(); +} + +private Composite createFolderWithTopRightToolBar(int shellWidth, boolean withGradient) { + return createFolderWithTopRightToolBar(shellWidth, withGradient, true); +} + +private Composite createFolderWithTopRightToolBar(int shellWidth, boolean withGradient, boolean twoTone) { + makeCleanEnvironment(); + shell.setLayout(new FillLayout()); + ctabFolder.setRenderer(twoTone ? new TwoToneRenderer(ctabFolder) : new PlainSubclassRenderer(ctabFolder)); + ctabFolder.setBackground(new Color(FOLDER_BACKGROUND)); + if (withGradient) { + ctabFolder.setBackground(new Color[] { new Color(FOLDER_BACKGROUND), new Color(FOLDER_BACKGROUND) }, + new int[] { 100 }); + } + for (int i = 0; i < 3; i++) { + CTabItem item = new CTabItem(ctabFolder, SWT.NONE); + item.setText("A rather long tab title " + i); + item.setControl(new Composite(ctabFolder, SWT.NONE)); + } + ctabFolder.setSelection(0); + + // the IDE wraps the tool bar in a Composite, which is what carries the background + Composite topRight = new Composite(ctabFolder, SWT.NONE); + topRight.setLayout(new FillLayout()); + ToolBar toolBar = new ToolBar(topRight, SWT.FLAT); + for (int i = 0; i < 6; i++) { + new ToolItem(toolBar, SWT.PUSH).setText("Item " + i); + } + ctabFolder.setTopRight(topRight, SWT.RIGHT | SWT.WRAP); + + shell.setSize(shellWidth, 300); + shell.open(); + SwtTestUtil.processEvents(); + ctabFolder.layout(true, true); + SwtTestUtil.processEvents(); + return topRight; +} + +@Test +public void test_topRightControl_wrappedBelowTabRow_matchesBody() { + Composite toolBar = createFolderWithTopRightToolBar(240, false); + assertTrue(toolBar.getBounds().y > ctabFolder.getTabHeight(), + "tool bar did not wrap below the tab row, bounds " + toolBar.getBounds()); + assertEquals(BODY_COLOR, effectiveBackground(toolBar), + "a wrapped control sits on the body and has to match what the renderer paints there"); +} + +@Test +public void test_topRightControl_inTabRow_matchesTabRow() { + Composite toolBar = createFolderWithTopRightToolBar(900, false); + assertFalse(toolBar.getBounds().y > ctabFolder.getTabHeight(), + "tool bar unexpectedly wrapped, bounds " + toolBar.getBounds()); + assertEquals(TAB_ROW_COLOR, effectiveBackground(toolBar), + "a control in the tab row has to match what the renderer paints there"); +} + +@Test +public void test_topRightControl_rendererKeepingDefaultBackground_staysOnTheFolderBackground() { + Composite toolBar = createFolderWithTopRightToolBar(240, false, false); + assertEquals(FOLDER_BACKGROUND, effectiveBackground(toolBar), + "a renderer that does not paint PART_BACKGROUND has to leave the folder background"); +} + +@Test +public void test_topRightControl_defaultRendererWithGradient_keepsFlatBackground() { + makeCleanEnvironment(); + shell.setLayout(new FillLayout()); + ctabFolder.setBackground(new Color(FOLDER_BACKGROUND)); + ctabFolder.setBackground(new Color[] { new Color(TAB_ROW_COLOR), new Color(BODY_COLOR) }, + new int[] { 100 }); + for (int i = 0; i < 3; i++) { + CTabItem item = new CTabItem(ctabFolder, SWT.NONE); + item.setText("A rather long tab title " + i); + item.setControl(new Composite(ctabFolder, SWT.NONE)); + } + ctabFolder.setSelection(0); + Composite topRight = new Composite(ctabFolder, SWT.NONE); + topRight.setLayout(new FillLayout()); + ToolBar toolBar = new ToolBar(topRight, SWT.FLAT); + for (int i = 0; i < 6; i++) { + new ToolItem(toolBar, SWT.PUSH).setText("Item " + i); + } + ctabFolder.setTopRight(topRight, SWT.RIGHT | SWT.WRAP); + shell.setSize(240, 300); + shell.open(); + SwtTestUtil.processEvents(); + ctabFolder.layout(true, true); + SwtTestUtil.processEvents(); + + assertTrue(topRight.getBounds().y > ctabFolder.getTabHeight(), + "tool bar did not wrap below the tab row, bounds " + topRight.getBounds()); + assertNull(topRight.getBackgroundImage(), + "the built-in renderer paints the body flat, a wrapped control needs no image"); +} + /** Layout with a preferred size the test can change at will. */ private static final class FixedSizeLayout extends Layout { int width;