From b0e34fabeacd903140c22835864fdc8c98e3d39d Mon Sep 17 00:00:00 2001 From: Andreas Koch Date: Tue, 25 Feb 2025 09:40:11 +0100 Subject: [PATCH] [win32] Use operations for Path manipulation This commit refactors Path in the win32 implementation to better support multiple handles for different zoom settings of a Path when monitor-specific scaling is enabled. The previous implementation only applied adaptions to the initial handle of the path and relied on the path not to be changed afterwards. This doesn't cover all scenarios and can lead to unexpected behavior when re-using Path objects over different zoom settings. --- .../eclipse/swt/graphics/PathWin32Tests.java | 25 +- .../win32/org/eclipse/swt/graphics/Path.java | 680 ++++++++++++------ .../src/examples_graphics.properties | 11 + .../examples/graphics/GraphicsExample.java | 1 + .../swt/examples/graphics/PathReuseTab.java | 158 ++++ 5 files changed, 653 insertions(+), 222 deletions(-) create mode 100644 examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/PathReuseTab.java diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/PathWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/PathWin32Tests.java index 523102cd712..a20d363413c 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/PathWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/PathWin32Tests.java @@ -13,6 +13,7 @@ *******************************************************************************/ package org.eclipse.swt.graphics; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.util.*; @@ -49,9 +50,11 @@ public void testHandlesExistForEachZoomLevelInHashMap() { DPIUtil.setDeviceZoom(zoom); Path path = new Path(display); path.addArc(0, 0, 10, 10, 0, 90); + path.getHandle(zoom); + assertTrue("zoomLevelToHandle should contain initial zoom's handle", path.toString().contains(zoom + "=")); + assertFalse("zoomLevelToHandle should not contains scaled handle", path.toString().contains(scaledZoom + "=")); path.getHandle(scaledZoom); - assertTrue("zoomLevelToHandle contains scaled handle", path.toString().contains(scaledZoom + "=")); - assertTrue("zoomLevelToHandle contains initial zoom's handle", path.toString().contains(zoom + "=")); + assertTrue("zoomLevelToHandle should contain scaled handle", path.toString().contains(scaledZoom + "=")); } @Test @@ -70,4 +73,22 @@ public void testBoundsAreScaledWRTZoomLevel() { assertTrue("Height is scaled up wrt the scalingFactor", bounds.Height * scalingFactor == scaledBounds.Height); assertTrue("Height is scaled up wrt the scalingFactor", bounds.Width * scalingFactor == scaledBounds.Width); } + + @Test + public void testCreatePathHandleWithDisposedPathInvolved() { + Display display = Display.getDefault(); + + Path path = new Path(display); + path.addArc(0, 0, 10, 10, 0, 90); + + Path path2 = new Path(display); + path.addArc(0, 0, 30, 30, 0, 270); + + path.addPath(path2); + + path2.dispose(); + path.getHandle(100); + path.getHandle(200); + path.dispose(); + } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Path.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Path.java index bcd482b549c..54a699603f1 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Path.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Path.java @@ -42,12 +42,9 @@ * @since 3.1 */ public class Path extends Resource { - private int initialZoom; - private HashMap zoomLevelToHandle = new HashMap<>(); - - PointF currentPoint = new PointF(), startPoint = new PointF(); + private Map zoomToHandle = new HashMap<>(); /** * Constructs a new empty Path. @@ -84,7 +81,7 @@ private Path(Device device, int zoom) { initialZoom = zoom; long handle = Gdip.GraphicsPath_new(Gdip.FillModeAlternate); if (handle == 0) SWT.error(SWT.ERROR_NO_HANDLES); - zoomLevelToHandle.put(initialZoom, handle); + zoomToHandle.put(initialZoom, new PathHandle(handle, initialZoom)); init(); this.device.registerResourceWithZoomSupport(this); } @@ -132,7 +129,7 @@ public Path (Device device, Path path, float flatness) { if (flatness != 0) Gdip.GraphicsPath_Flatten(handle, 0, flatness); if (handle == 0) SWT.error(SWT.ERROR_NO_HANDLES); initialZoom = path.initialZoom; - zoomLevelToHandle.put(initialZoom, handle); + zoomToHandle.put(initialZoom, new PathHandle(handle, initialZoom)); init(); this.device.registerResourceWithZoomSupport(this); } @@ -208,38 +205,7 @@ private Path(Device device, PathData data, int zoom) { */ public void addArc (float x, float y, float width, float height, float startAngle, float arcAngle) { if (width == 0 || height == 0 || arcAngle == 0) return; - Drawable drawable = getDevice(); - x = DPIUtil.scaleUp(drawable, x, initialZoom); - y = DPIUtil.scaleUp(drawable, y, initialZoom); - width = DPIUtil.scaleUp(drawable, width, initialZoom); - height = DPIUtil.scaleUp(drawable, height, initialZoom); - addArcInPixels(x, y, width, height, startAngle, arcAngle); -} - -void addArcInPixels(float x, float y, float width, float height, float startAngle, float arcAngle) { - if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - if (width < 0) { - x = x + width; - width = -width; - } - if (height < 0) { - y = y + height; - height = -height; - } - if (width == height) { - Gdip.GraphicsPath_AddArc(getHandle(initialZoom), x, y, width, height, -startAngle, -arcAngle); - } else { - long path = Gdip.GraphicsPath_new(Gdip.FillModeAlternate); - if (path == 0) SWT.error(SWT.ERROR_NO_HANDLES); - long matrix = Gdip.Matrix_new(width, 0, 0, height, x, y); - if (matrix == 0) SWT.error(SWT.ERROR_NO_HANDLES); - Gdip.GraphicsPath_AddArc(path, 0, 0, 1, 1, -startAngle, -arcAngle); - Gdip.GraphicsPath_Transform(path, matrix); - Gdip.GraphicsPath_AddPath(getHandle(initialZoom), path, true); - Gdip.Matrix_delete(matrix); - Gdip.GraphicsPath_delete(path); - } - Gdip.GraphicsPath_GetLastPoint(getHandle(initialZoom), currentPoint); + applyOperationForAllHandles(new AddArcOperation(x, y, width, height, startAngle, arcAngle)); } /** @@ -259,10 +225,7 @@ public void addPath(Path path) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (path == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (path.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); - //TODO - expose connect? - Gdip.GraphicsPath_AddPath(getHandle(initialZoom), path.getHandle(initialZoom), false); - currentPoint.X = path.currentPoint.X; - currentPoint.Y = path.currentPoint.Y; + applyOperationForAllHandles(new AddPathOperation(path)); } /** @@ -278,19 +241,8 @@ public void addPath(Path path) { * */ public void addRectangle (float x, float y, float width, float height) { - addRectangleInPixels(x, y, width, height); -} - -void addRectangleInPixels(float x, float y, float width, float height) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - RectF rect = new RectF(); - rect.X = x; - rect.Y = y; - rect.Width = width; - rect.Height = height; - Gdip.GraphicsPath_AddRectangle(getHandle(initialZoom), rect); - currentPoint.X = x; - currentPoint.Y = y; + applyOperationForAllHandles(new AddRectangleOperation(x, y, width, height)); } /** @@ -311,29 +263,10 @@ void addRectangleInPixels(float x, float y, float width, float height) { * */ public void addString (String string, float x, float y, Font font) { - Drawable drawable = getDevice(); - x = DPIUtil.scaleUp(drawable, x, initialZoom); - y = DPIUtil.scaleUp(drawable, y, initialZoom); - addStringInPixels(string, x, y, font); -} -void addStringInPixels(String string, float x, float y, Font font) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (font == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (font.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); - char[] buffer = string.toCharArray(); - long hDC = device.internal_new_GC(null); - long [] family = new long [1]; - long gdipFont = GC.createGdipFont(hDC, SWTFontProvider.getFont(device, font.getFontData()[0], initialZoom).handle, 0, device.fontCollection, family, null); - PointF point = new PointF(); - point.X = x - (Gdip.Font_GetSize(gdipFont) / 6); - point.Y = y; - int style = Gdip.Font_GetStyle(gdipFont); - float size = Gdip.Font_GetSize(gdipFont); - Gdip.GraphicsPath_AddString(getHandle(initialZoom), buffer, buffer.length, family[0], style, size, point, 0); - Gdip.GraphicsPath_GetLastPoint(getHandle(initialZoom), currentPoint); - Gdip.FontFamily_delete(family[0]); - Gdip.Font_delete(gdipFont); - device.internal_dispose_GC(hDC, null); + applyOperationForAllHandles(new AddStringOperation(string, x, y, font)); } /** @@ -347,16 +280,7 @@ void addStringInPixels(String string, float x, float y, Font font) { */ public void close() { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - Gdip.GraphicsPath_CloseFigure(getHandle(initialZoom)); - /* - * Feature in GDI+. CloseFigure() does affect the last - * point, so GetLastPoint() does not return the starting - * point of the subpath after calling CloseFigure(). The - * fix is to remember the subpath starting point and use - * it instead. - */ - currentPoint.X = startPoint.X; - currentPoint.Y = startPoint.Y; + applyOperationForAllHandles(new CloseOperation()); } /** @@ -383,25 +307,11 @@ public void close() { * */ public boolean contains (float x, float y, GC gc, boolean outline) { - Drawable drawable = getDevice(); - x = DPIUtil.scaleUp(drawable, x, initialZoom); - y = DPIUtil.scaleUp(drawable, y, initialZoom); - return containsInPixels(x, y, gc, outline); -} -boolean containsInPixels(float x, float y, GC gc, boolean outline) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (gc == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (gc.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); - //TODO - should use GC transformation - gc.initGdip(); - gc.checkGC(GC.LINE_CAP | GC.LINE_JOIN | GC.LINE_STYLE | GC.LINE_WIDTH); - int mode = OS.GetPolyFillMode(gc.handle) == OS.WINDING ? Gdip.FillModeWinding : Gdip.FillModeAlternate; - Gdip.GraphicsPath_SetFillMode(getHandle(initialZoom), mode); - if (outline) { - return Gdip.GraphicsPath_IsOutlineVisible(getHandle(initialZoom), x, y, gc.data.gdipPen, gc.data.gdipGraphics); - } else { - return Gdip.GraphicsPath_IsVisible(getHandle(initialZoom), x, y, gc.data.gdipGraphics); - } + PathHandle handle = getPathHandle(initialZoom); + return handle.contains(x, y, gc, outline); } /** @@ -419,35 +329,23 @@ boolean containsInPixels(float x, float y, GC gc, boolean outline) { * */ public void cubicTo (float cx1, float cy1, float cx2, float cy2, float x, float y) { - Drawable drawable = getDevice(); - cx1 = DPIUtil.scaleUp(drawable, cx1, initialZoom); - cy1 = DPIUtil.scaleUp(drawable, cy1, initialZoom); - cx2 = DPIUtil.scaleUp(drawable, cx2, initialZoom); - cy2 = DPIUtil.scaleUp(drawable, cy2, initialZoom); - x = DPIUtil.scaleUp(drawable, x, initialZoom); - y = DPIUtil.scaleUp(drawable, y, initialZoom); - cubicToInPixels(cx1, cy1, cx2, cy2, x, y); -} - -void cubicToInPixels(float cx1, float cy1, float cx2, float cy2, float x, float y) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - Gdip.GraphicsPath_AddBezier(getHandle(initialZoom), currentPoint.X, currentPoint.Y, cx1, cy1, cx2, cy2, x, y); - Gdip.GraphicsPath_GetLastPoint(getHandle(initialZoom), currentPoint); + applyOperationForAllHandles(new CubicToOperation(cx1, cy1, cx2, cy2, x, y)); } @Override void destroy() { device.deregisterResourceWithZoomSupport(this); - zoomLevelToHandle.values().forEach(Gdip::GraphicsPath_delete); - zoomLevelToHandle.clear(); + zoomToHandle.values().forEach(PathHandle::destroy); + zoomToHandle.clear(); } @Override void destroyHandlesExcept(Set zoomLevels) { - zoomLevelToHandle.entrySet().removeIf(entry -> { + zoomToHandle.entrySet().removeIf(entry -> { final Integer zoom = entry.getKey(); if (!zoomLevels.contains(zoom) && zoom != initialZoom) { - Gdip.GraphicsPath_delete(entry.getValue()); + entry.getValue().destroy(); return true; } return false; @@ -471,20 +369,10 @@ void destroyHandlesExcept(Set zoomLevels) { */ public void getBounds (float[] bounds) { if (bounds == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); - getBoundsInPixels(bounds); - float[] scaledbounds= DPIUtil.scaleDown(getDevice(), bounds, initialZoom); - System.arraycopy(scaledbounds, 0, bounds, 0, 4); -} - -void getBoundsInPixels(float[] bounds) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (bounds.length < 4) SWT.error(SWT.ERROR_INVALID_ARGUMENT); - RectF rect = new RectF(); - Gdip.GraphicsPath_GetBounds(getHandle(initialZoom), rect, 0, 0); - bounds[0] = rect.X; - bounds[1] = rect.Y; - bounds[2] = rect.Width; - bounds[3] = rect.Height; + PathHandle handle = getPathHandle(initialZoom); + handle.fillBounds(bounds); } /** @@ -503,16 +391,10 @@ void getBoundsInPixels(float[] bounds) { */ public void getCurrentPoint (float[] point) { if (point == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); - getCurrentPointInPixels(point); - float[] scaledpoint= DPIUtil.scaleDown(getDevice(), point, initialZoom); - System.arraycopy(scaledpoint, 0, point, 0, 2); -} - -void getCurrentPointInPixels(float[] point) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (point.length < 2) SWT.error(SWT.ERROR_INVALID_ARGUMENT); - point[0] = currentPoint.X; - point[1] = currentPoint.Y; + PathHandle handle = getPathHandle(initialZoom); + handle.fillCurrentPoint(point); } /** @@ -528,54 +410,8 @@ void getCurrentPointInPixels(float[] point) { */ public PathData getPathData() { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - PathData result = getPathDataInPixels(); - result.points = DPIUtil.scaleDown(getDevice(), result.points, initialZoom); - return result; -} - -PathData getPathDataInPixels() { - int count = Gdip.GraphicsPath_GetPointCount(getHandle(initialZoom)); - byte[] gdipTypes = new byte[count]; - float[] points = new float[count * 2]; - Gdip.GraphicsPath_GetPathTypes(getHandle(initialZoom), gdipTypes, count); - Gdip.GraphicsPath_GetPathPoints(getHandle(initialZoom), points, count); - byte[] types = new byte[count * 2]; - int index = 0, typesIndex = 0; - while (index < count) { - byte type = gdipTypes[index]; - boolean close = false; - switch (type & Gdip.PathPointTypePathTypeMask) { - case Gdip.PathPointTypeStart: - types[typesIndex++] = SWT.PATH_MOVE_TO; - close = (type & Gdip.PathPointTypeCloseSubpath) != 0; - index += 1; - break; - case Gdip.PathPointTypeLine: - types[typesIndex++] = SWT.PATH_LINE_TO; - close = (type & Gdip.PathPointTypeCloseSubpath) != 0; - index += 1; - break; - case Gdip.PathPointTypeBezier: - types[typesIndex++] = SWT.PATH_CUBIC_TO; - close = (gdipTypes[index + 2] & Gdip.PathPointTypeCloseSubpath) != 0; - index += 3; - break; - default: - index++; - } - if (close) { - types[typesIndex++] = SWT.PATH_CLOSE; - } - } - if (typesIndex != types.length) { - byte[] newTypes = new byte[typesIndex]; - System.arraycopy(types, 0, newTypes, 0, typesIndex); - types = newTypes; - } - PathData result = new PathData(); - result.types = types; - result.points = points; - return result; + PathHandle handle = getPathHandle(initialZoom); + return handle.getPathData(); } /** @@ -590,16 +426,11 @@ PathData getPathDataInPixels() { * */ public void lineTo (float x, float y) { - Drawable drawable = getDevice(); - lineToInPixels(DPIUtil.scaleUp(drawable, x, initialZoom), DPIUtil.scaleUp(drawable, y, initialZoom)); -} - -void lineToInPixels(float x, float y) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - Gdip.GraphicsPath_AddLine(getHandle(initialZoom), currentPoint.X, currentPoint.Y, x, y); - Gdip.GraphicsPath_GetLastPoint(getHandle(initialZoom), currentPoint); + applyOperationForAllHandles(new LineToOperation(x, y)); } + void init(PathData data) { byte[] types = data.types; float[] points = data.points; @@ -639,7 +470,7 @@ void init(PathData data) { */ @Override public boolean isDisposed() { - return zoomLevelToHandle.isEmpty(); + return zoomToHandle.isEmpty(); } /** @@ -655,15 +486,8 @@ public boolean isDisposed() { * */ public void moveTo (float x, float y) { - Drawable drawable = getDevice(); - moveToInPixels(DPIUtil.scaleUp(drawable, x, initialZoom), DPIUtil.scaleUp(drawable, y, initialZoom)); -} - -void moveToInPixels(float x, float y) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - Gdip.GraphicsPath_StartFigure(getHandle(initialZoom)); - currentPoint.X = startPoint.X = x; - currentPoint.Y = startPoint.Y = y; + applyOperationForAllHandles(new MoveToOperation(x, y)); } /** @@ -679,22 +503,431 @@ void moveToInPixels(float x, float y) { * */ public void quadTo (float cx, float cy, float x, float y) { - Drawable drawable = getDevice(); - cx = DPIUtil.scaleUp(drawable, cx, initialZoom); - cy = DPIUtil.scaleUp(drawable, cy, initialZoom); - x = DPIUtil.scaleUp(drawable, x, initialZoom); - y = DPIUtil.scaleUp(drawable, y, initialZoom); - quadToInPixels(cx, cy, x, y); + if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + applyOperationForAllHandles(new QuadToOperation(cx, cy, x, y)); } -void quadToInPixels(float cx, float cy, float x, float y) { - if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); - float cx1 = currentPoint.X + 2 * (cx - currentPoint.X) / 3; - float cy1 = currentPoint.Y + 2 * (cy - currentPoint.Y) / 3; - float cx2 = cx1 + (x - currentPoint.X) / 3; - float cy2 = cy1 + (y - currentPoint.Y) / 3; - Gdip.GraphicsPath_AddBezier(getHandle(initialZoom), currentPoint.X, currentPoint.Y, cx1, cy1, cx2, cy2, x, y); - Gdip.GraphicsPath_GetLastPoint(getHandle(initialZoom), currentPoint); +private class PathHandle { + private long handle; + private int zoom; + private PointF currentPoint = new PointF(); + private PointF startPoint = new PointF(); + + public PathHandle(final long handle, final int zoom) { + this.handle = handle; + this.zoom = zoom; + } + + boolean contains (float x, float y, GC gc, boolean outline) { + Drawable drawable = getDevice(); + float xInPixels = DPIUtil.scaleUp(drawable, x, zoom); + float yInPixels = DPIUtil.scaleUp(drawable, y, zoom); + return containsInPixels(xInPixels, yInPixels, gc, outline); + } + + private boolean containsInPixels(float x, float y, GC gc, boolean outline) { + //TODO - should use GC transformation + gc.initGdip(); + gc.checkGC(GC.LINE_CAP | GC.LINE_JOIN | GC.LINE_STYLE | GC.LINE_WIDTH); + int mode = OS.GetPolyFillMode(gc.handle) == OS.WINDING ? Gdip.FillModeWinding : Gdip.FillModeAlternate; + Gdip.GraphicsPath_SetFillMode(handle, mode); + if (outline) { + return Gdip.GraphicsPath_IsOutlineVisible(handle, x, y, gc.data.gdipPen, gc.data.gdipGraphics); + } else { + return Gdip.GraphicsPath_IsVisible(handle, x, y, gc.data.gdipGraphics); + } + } + + void destroy() { + Gdip.GraphicsPath_delete(handle); + } + + void fillBounds (float[] bounds) { + getBoundsInPixels(bounds); + float[] scaledbounds= DPIUtil.scaleDown(getDevice(), bounds, zoom); + System.arraycopy(scaledbounds, 0, bounds, 0, 4); + } + + private void getBoundsInPixels(float[] bounds) { + RectF rect = new RectF(); + Gdip.GraphicsPath_GetBounds(handle, rect, 0, 0); + bounds[0] = rect.X; + bounds[1] = rect.Y; + bounds[2] = rect.Width; + bounds[3] = rect.Height; + } + + void fillCurrentPoint (float[] point) { + getCurrentPointInPixels(point); + float[] scaledpoint= DPIUtil.scaleDown(getDevice(), point, zoom); + System.arraycopy(scaledpoint, 0, point, 0, 2); + } + + private void getCurrentPointInPixels(float[] point) { + point[0] = currentPoint.X; + point[1] = currentPoint.Y; + } + + PathData getPathData() { + PathData result = getPathDataInPixels(); + result.points = DPIUtil.scaleDown(getDevice(), result.points, zoom); + return result; + } + + private PathData getPathDataInPixels() { + int count = Gdip.GraphicsPath_GetPointCount(handle); + byte[] gdipTypes = new byte[count]; + float[] points = new float[count * 2]; + Gdip.GraphicsPath_GetPathTypes(handle, gdipTypes, count); + Gdip.GraphicsPath_GetPathPoints(handle, points, count); + byte[] types = new byte[count * 2]; + int index = 0, typesIndex = 0; + while (index < count) { + byte type = gdipTypes[index]; + boolean close = false; + switch (type & Gdip.PathPointTypePathTypeMask) { + case Gdip.PathPointTypeStart: + types[typesIndex++] = SWT.PATH_MOVE_TO; + close = (type & Gdip.PathPointTypeCloseSubpath) != 0; + index += 1; + break; + case Gdip.PathPointTypeLine: + types[typesIndex++] = SWT.PATH_LINE_TO; + close = (type & Gdip.PathPointTypeCloseSubpath) != 0; + index += 1; + break; + case Gdip.PathPointTypeBezier: + types[typesIndex++] = SWT.PATH_CUBIC_TO; + close = (gdipTypes[index + 2] & Gdip.PathPointTypeCloseSubpath) != 0; + index += 3; + break; + default: + index++; + } + if (close) { + types[typesIndex++] = SWT.PATH_CLOSE; + } + } + if (typesIndex != types.length) { + byte[] newTypes = new byte[typesIndex]; + System.arraycopy(types, 0, newTypes, 0, typesIndex); + types = newTypes; + } + PathData result = new PathData(); + result.types = types; + result.points = points; + return result; + } + + @Override + public String toString() { + return "PathHandle [handle=" + handle + ", zoom=" + zoom + "]"; + } +} + +private class AddArcOperation implements Operation { + private final float x; + private final float y; + private final float width; + private final float height; + private final float startAngle; + private final float arcAngle; + + public AddArcOperation(float x, float y, float width, float height, float startAngle, float arcAngle) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.startAngle = startAngle; + this.arcAngle = arcAngle; + } + + @Override + public void apply(PathHandle pathHandle) { + if (width == 0 || height == 0 || arcAngle == 0) return; + int zoom = pathHandle.zoom; + Drawable drawable = getDevice(); + float xInPixels = DPIUtil.scaleUp(drawable, x, zoom); + float yInPixels = DPIUtil.scaleUp(drawable, y, zoom); + float widthInPixels = DPIUtil.scaleUp(drawable, width, zoom); + float heightInPixels = DPIUtil.scaleUp(drawable, height, zoom); + addArcInPixels(pathHandle, xInPixels, yInPixels, widthInPixels, heightInPixels, startAngle, arcAngle); + } + + private void addArcInPixels(PathHandle pathHandle, float x, float y, float width, float height, float startAngle, float arcAngle) { + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + if (width < 0) { + x = x + width; + width = -width; + } + if (height < 0) { + y = y + height; + height = -height; + } + if (width == height) { + Gdip.GraphicsPath_AddArc(handle, x, y, width, height, -startAngle, -arcAngle); + } else { + long path = Gdip.GraphicsPath_new(Gdip.FillModeAlternate); + if (path == 0) SWT.error(SWT.ERROR_NO_HANDLES); + long matrix = Gdip.Matrix_new(width, 0, 0, height, x, y); + if (matrix == 0) SWT.error(SWT.ERROR_NO_HANDLES); + Gdip.GraphicsPath_AddArc(path, 0, 0, 1, 1, -startAngle, -arcAngle); + Gdip.GraphicsPath_Transform(path, matrix); + Gdip.GraphicsPath_AddPath(handle, path, true); + Gdip.Matrix_delete(matrix); + Gdip.GraphicsPath_delete(path); + } + Gdip.GraphicsPath_GetLastPoint(handle, currentPoint); + } +} + +private class AddRectangleOperation implements Operation { + private final float x; + private final float y; + private final float width; + private final float height; + + public AddRectangleOperation(float x, float y, float width, float height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public void apply(PathHandle pathHandle) { + int zoom = pathHandle.zoom; + Drawable drawable = getDevice(); + float xInPixels = DPIUtil.scaleUp(drawable, x, zoom); + float yInPixels = DPIUtil.scaleUp(drawable, y, zoom); + float widthInPixels = DPIUtil.scaleUp(drawable, width, zoom); + float heightInPixels = DPIUtil.scaleUp(drawable, height, zoom); + addRectangleInPixels(pathHandle, xInPixels, yInPixels, widthInPixels, heightInPixels); + } + + private void addRectangleInPixels(PathHandle pathHandle, float x, float y, float width, float height) { + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + RectF rect = new RectF(); + rect.X = x; + rect.Y = y; + rect.Width = width; + rect.Height = height; + Gdip.GraphicsPath_AddRectangle(handle, rect); + currentPoint.X = x; + currentPoint.Y = y; + } +} + +private class AddPathOperation implements Operation { + private final Path path; + + public AddPathOperation(Path path) { + this.path = path; + } + + @Override + public void apply(PathHandle pathHandle) { + PathHandle secondPathHandle = path.getPathHandle(pathHandle.zoom); + Gdip.GraphicsPath_AddPath(pathHandle.handle, secondPathHandle.handle, false); + pathHandle.currentPoint.X = secondPathHandle.currentPoint.X; + pathHandle.currentPoint.Y = secondPathHandle.currentPoint.Y; + } +} + +private class AddStringOperation implements Operation { + private final String string; + private final float x; + private final float y; + private final FontData fontData; + + public AddStringOperation(String string, float x, float y, Font font) { + this.string = string; + this.x = x; + this.y = y; + this.fontData = font.getFontData()[0]; + } + + @Override + public void apply(PathHandle pathHandle) { + int zoom = pathHandle.zoom; + Drawable drawable = getDevice(); + float xInPixels = DPIUtil.scaleUp(drawable, x, zoom); + float yInPixels = DPIUtil.scaleUp(drawable, y, zoom); + addStringInPixels(pathHandle, xInPixels, yInPixels); + } + + private void addStringInPixels(PathHandle pathHandle, float x, float y) { + int zoom = pathHandle.zoom; + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + char[] buffer = string.toCharArray(); + long hDC = device.internal_new_GC(null); + long [] family = new long [1]; + long gdipFont = GC.createGdipFont(hDC, SWTFontProvider.getFont(device, this.fontData, zoom).handle, 0, device.fontCollection, family, null); + PointF point = new PointF(); + point.X = x - (Gdip.Font_GetSize(gdipFont) / 6); + point.Y = y; + int style = Gdip.Font_GetStyle(gdipFont); + float size = Gdip.Font_GetSize(gdipFont); + Gdip.GraphicsPath_AddString(handle, buffer, buffer.length, family[0], style, size, point, 0); + Gdip.GraphicsPath_GetLastPoint(handle, currentPoint); + Gdip.FontFamily_delete(family[0]); + Gdip.Font_delete(gdipFont); + device.internal_dispose_GC(hDC, null); + } +} + +private record CloseOperation() implements Operation { + @Override + public void apply(PathHandle pathHandle) { + PointF startPoint = pathHandle.startPoint; + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + Gdip.GraphicsPath_CloseFigure(handle); + /* + * Feature in GDI+. CloseFigure() does affect the last + * point, so GetLastPoint() does not return the starting + * point of the subpath after calling CloseFigure(). The + * fix is to remember the subpath starting point and use + * it instead. + */ + currentPoint.X = startPoint.X; + currentPoint.Y = startPoint.Y; + } +} + +private class CubicToOperation implements Operation { + private final float cx1; + private final float cy1; + private final float cx2; + private final float cy2; + private final float x; + private final float y; + + public CubicToOperation(float cx1, float cy1, float cx2, float cy2, float x, float y) { + this.cx1 = cx1; + this.cy1 = cy1; + this.cx2 = cx2; + this.cy2 = cy2; + this.x = x; + this.y = y; + } + + @Override + public void apply(PathHandle pathHandle) { + int zoom = pathHandle.zoom; + Drawable drawable = getDevice(); + float cx1InPixels = DPIUtil.scaleUp(drawable, cx1, zoom); + float cy1InPixels = DPIUtil.scaleUp(drawable, cy1, zoom); + float cx2InPixels = DPIUtil.scaleUp(drawable, cx2, zoom); + float cy2InPixels = DPIUtil.scaleUp(drawable, cy2, zoom); + float xInPixels = DPIUtil.scaleUp(drawable, x, zoom); + float yInPixels = DPIUtil.scaleUp(drawable, y, zoom); + cubicToInPixels(pathHandle, cx1InPixels, cy1InPixels, cx2InPixels, cy2InPixels, xInPixels, yInPixels); + } + + private void cubicToInPixels(PathHandle pathHandle, float cx1, float cy1, float cx2, float cy2, float x, float y) { + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + Gdip.GraphicsPath_AddBezier(handle, currentPoint.X, currentPoint.Y, cx1, cy1, cx2, cy2, x, y); + Gdip.GraphicsPath_GetLastPoint(handle, currentPoint); + } +} + +private class LineToOperation implements Operation { + private final float x; + private final float y; + + public LineToOperation(float x, float y) { + this.x = x; + this.y = y; + } + + @Override + public void apply(PathHandle pathHandle) { + int zoom = pathHandle.zoom; + Drawable drawable = getDevice(); + lineToInPixels(pathHandle, DPIUtil.scaleUp(drawable, x, zoom), DPIUtil.scaleUp(drawable, y, zoom)); + } + + private void lineToInPixels(PathHandle pathHandle, float x, float y) { + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + Gdip.GraphicsPath_AddLine(handle, currentPoint.X, currentPoint.Y, x, y); + Gdip.GraphicsPath_GetLastPoint(handle, currentPoint); + } +} + +private class MoveToOperation implements Operation { + private final float x; + private final float y; + + public MoveToOperation(float x, float y) { + this.x = x; + this.y = y; + } + + @Override + public void apply(PathHandle pathHandle) { + int zoom = pathHandle.zoom; + Drawable drawable = getDevice(); + moveToInPixels(pathHandle, DPIUtil.scaleUp(drawable, x, zoom), DPIUtil.scaleUp(drawable, y, zoom)); + } + + void moveToInPixels(PathHandle pathHandle, float x, float y) { + PointF startPoint = pathHandle.startPoint; + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + Gdip.GraphicsPath_StartFigure(handle); + currentPoint.X = startPoint.X = x; + currentPoint.Y = startPoint.Y = y; + } +} + +private class QuadToOperation implements Operation { + private final float cx; + private final float cy; + private final float x; + private final float y; + + public QuadToOperation(float cx, float cy, float x, float y) { + this.cx = cx; + this.cy = cy; + this.x = x; + this.y = y; + } + + @Override + public void apply(PathHandle pathHandle) { + Drawable drawable = getDevice(); + int zoom = pathHandle.zoom; + float cxInPixels = DPIUtil.scaleUp(drawable, cx, zoom); + float cyInPixels = DPIUtil.scaleUp(drawable, cy, zoom); + float xInPixels = DPIUtil.scaleUp(drawable, x, zoom); + float yInPixels = DPIUtil.scaleUp(drawable, y, zoom); + quadToInPixels(pathHandle, cxInPixels, cyInPixels, xInPixels, yInPixels); + } + + private void quadToInPixels(PathHandle pathHandle, float cx, float cy, float x, float y) { + PointF currentPoint = pathHandle.currentPoint; + long handle = pathHandle.handle; + float cx1 = currentPoint.X + 2 * (cx - currentPoint.X) / 3; + float cy1 = currentPoint.Y + 2 * (cy - currentPoint.Y) / 3; + float cx2 = cx1 + (x - currentPoint.X) / 3; + float cy2 = cy1 + (y - currentPoint.Y) / 3; + Gdip.GraphicsPath_AddBezier(handle, currentPoint.X, currentPoint.Y, cx1, cy1, cx2, cy2, x, y); + Gdip.GraphicsPath_GetLastPoint(handle, currentPoint); + } +} + +private interface Operation { + void apply(PathHandle pathHandle); +} + +private void applyOperationForAllHandles(Operation operation) { + zoomToHandle.values().forEach(operation::apply); } /** @@ -706,16 +939,23 @@ void quadToInPixels(float cx, float cy, float x, float y) { @Override public String toString() { if (isDisposed()) return "Path {*DISPOSED*}"; - return "Path " + zoomLevelToHandle; + return "Path " + zoomToHandle; } -long getHandle(int zoom) { - if (!zoomLevelToHandle.containsKey(zoom)) { +private PathHandle getPathHandle(int zoom) { + if (!zoomToHandle.containsKey(zoom)) { PathData pathData = getPathData(); Path scaledPath = new Path(getDevice(), pathData, zoom); - zoomLevelToHandle.put(zoom, scaledPath.getHandle(scaledPath.initialZoom)); + long handle = scaledPath.getHandle(scaledPath.initialZoom); + PathHandle pathHandle = new PathHandle(handle, zoom); + zoomToHandle.put(zoom, pathHandle); + return pathHandle; } - return zoomLevelToHandle.get(zoom); + return zoomToHandle.get(zoom); +} + +long getHandle(int zoom) { + return getPathHandle(zoom).handle; } } diff --git a/examples/org.eclipse.swt.examples/src/examples_graphics.properties b/examples/org.eclipse.swt.examples/src/examples_graphics.properties index 75ea8e75585..5f7260d4938 100644 --- a/examples/org.eclipse.swt.examples/src/examples_graphics.properties +++ b/examples/org.eclipse.swt.examples/src/examples_graphics.properties @@ -130,10 +130,20 @@ yscale=Y-Scale Invert=Invert Matrix Path=Path PathOper=Path Operations +PathReuseOper=Path Re-usage +Reset=Reset DrawPath=Draw Path FillPath=Fill Path +AddPath=Add Path ClosePath=Close Path +FlattenPath=Flatten Path FillColor=Fill Color +DrawArc=Draw Arc +DrawLine=Draw Line +DrawRectangle=Draw Rectangle +DrawText=Draw Text +DrawCubic=Draw Cubic +DrawQuad=Draw Quad Text=Text Shapes=Shapes Next=Next @@ -169,6 +179,7 @@ AnimPathClippingDesc=This tab demonstrates the use of path clipping. A clipping PathClippingDesc=This tab demonstrates the use of path clipping. A clipping is the area onto which a drawing is made visible. The default clipping is the entire canvas (all the space in the right panel). However, one can set the clipping to be something different. PathOperDescription=This tab demonstrates the use of paths. It allows the user to see the differences between filling, drawing and closing paths. TransformReuseOperDescription=This tab demonstrates the re-usage of transform. It allows the user to test with long living Transform instances. +PathReuseOperDescription=This tab demonstrates the re-usage of paths. It allows the user to see and extend long living paths. rgbDescription=Miscellaneous tab that demonstrates emerging colors from layering other colors. RegionClippingDescription=This tab demonstrates how to apply a region clipping and the effects of applying one. It also demonstrates the operations that can be applied between two regions. ShapesDescription=This tab draws 3D shapes (in 2D) using various line styles. diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java index 69b244b514f..348fa377ed7 100644 --- a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java @@ -463,6 +463,7 @@ GraphicsTab[] createTabs() { new ImageScaleTab(this), new PathTab(this), new TransformReuseTab(this), + new PathReuseTab(this), }; } diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/PathReuseTab.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/PathReuseTab.java new file mode 100644 index 00000000000..8e01638f673 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/PathReuseTab.java @@ -0,0 +1,158 @@ +/******************************************************************************* + * Copyright (c) 2025 Yatta and others. + * + * 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 + * + * Contributors: + * Yatta - initial API and implementation + *******************************************************************************/ + +package org.eclipse.swt.examples.graphics; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; + +/** + * This tab demonstrates the re-usage of paths. It allows the user to test with + * long living Path instances. + */ +public class PathReuseTab extends GraphicsTab { + Path path; + +public PathReuseTab(GraphicsExample example) { + super(example); +} + +@Override +public String getCategory() { + return GraphicsExample.getResourceString("Path"); //$NON-NLS-1$ +} + +@Override +public String getText() { + return GraphicsExample.getResourceString("PathReuseOper"); //$NON-NLS-1$ +} + +@Override +public String getDescription() { + return GraphicsExample.getResourceString("PathReuseOperDescription"); //$NON-NLS-1$ +} + +private void resetPath(Device device) { + if (this.path != null) { + this.path.dispose(); + } + this.path = new Path(device); + example.redraw(); +} + +@Override +public void createControlPanel(final Composite parent) { + resetPath(parent.getDisplay()); + // create draw button + Composite comp = new Composite(parent, SWT.NONE); + comp.setLayout(new GridLayout(9, false)); + + final Button drawArcButton = new Button(comp, SWT.PUSH); + drawArcButton.setText(GraphicsExample.getResourceString("DrawArc")); //$NON-NLS-1$ + drawArcButton.addListener(SWT.Selection, event -> { + path.moveTo(100, 100); + this.path.addArc(100, 100, 250, 250, 90, 180); + example.redraw(); + }); + + final Button drawLineButton = new Button(comp, SWT.PUSH); + drawLineButton.setText(GraphicsExample.getResourceString("DrawLine")); //$NON-NLS-1$ + drawLineButton.addListener(SWT.Selection, event -> { + path.moveTo(400, 100); + path.lineTo(500, 100); + path.lineTo(400, 200); + path.lineTo(500, 300); + path.lineTo(400, 300); + example.redraw(); + }); + + final Button drawRectangleButton = new Button(comp, SWT.PUSH); + drawRectangleButton.setText(GraphicsExample.getResourceString("DrawRectangle")); //$NON-NLS-1$ + drawRectangleButton.addListener(SWT.Selection, event -> { + path.addRectangle(600, 100, 100, 200); + example.redraw(); + }); + + final Button drawStringButton = new Button(comp, SWT.PUSH); + drawStringButton.setText(GraphicsExample.getResourceString("DrawText")); //$NON-NLS-1$ + FontData fd = comp.getFont().getFontData()[0]; + final Font font = new Font(comp.getDisplay(), fd.getName(), 60, SWT.BOLD); + drawStringButton.addListener(SWT.Selection, event -> { + path.addString("Text", 800, 100, font); + example.redraw(); + }); + + final Button drawCubicButton = new Button(comp, SWT.PUSH); + drawCubicButton.setText(GraphicsExample.getResourceString("DrawCubic")); //$NON-NLS-1$ + drawCubicButton.addListener(SWT.Selection, event -> { + path.moveTo(100, 400); + path.cubicTo(100, 400, 150, 600, 400, 500); + example.redraw(); + }); + + final Button drawQuadButton = new Button(comp, SWT.PUSH); + drawQuadButton.setText(GraphicsExample.getResourceString("DrawQuad")); //$NON-NLS-1$ + drawQuadButton.addListener(SWT.Selection, event -> { + path.moveTo(500, 400); + path.quadTo(500, 400, 500, 600); + path.quadTo(600, 500, 700, 600); + example.redraw(); + }); + + final Button addPathButton = new Button(comp, SWT.PUSH); + addPathButton.setText(GraphicsExample.getResourceString("AddPath")); //$NON-NLS-1$ + addPathButton.addListener(SWT.Selection, event -> { + Path newPath = new Path(comp.getDisplay()); + newPath.addArc(800, 400, 250, 250, 90, -180); + path.addPath(newPath); + example.redraw(); + }); + + final Button flattenButton = new Button(comp, SWT.PUSH); + flattenButton.setText(GraphicsExample.getResourceString("FlattenPath")); //$NON-NLS-1$ + flattenButton.addListener(SWT.Selection, event -> { + path = new Path(comp.getDisplay(), path, 5); + example.redraw(); + }); + + final Button closeButton = new Button(comp, SWT.PUSH); + closeButton.setText(GraphicsExample.getResourceString("ClosePath")); //$NON-NLS-1$ + closeButton.addListener(SWT.Selection, event -> { + path.close(); + example.redraw(); + }); + + final Button resetButton = new Button(comp, SWT.PUSH); + resetButton.setText(GraphicsExample.getResourceString("Reset")); //$NON-NLS-1$ + resetButton.addListener(SWT.Selection, event -> resetPath(parent.getDisplay())); +} + +@Override +public void paint(GC gc, int width, int height) { + if (!example.checkAdvancedGraphics()) return; + Device device = gc.getDevice(); + gc.setLineWidth(5); + gc.setForeground(device.getSystemColor(SWT.COLOR_BLACK)); + gc.drawPath(path); +} +} + +