diff --git a/Headers/gsc/GSGState.h b/Headers/gsc/GSGState.h index 0ca0b0ba..5f6f0d92 100644 --- a/Headers/gsc/GSGState.h +++ b/Headers/gsc/GSGState.h @@ -38,6 +38,7 @@ @class NSBezierPath; @class NSFont; @class NSColorSpace; +@class NSShadow; @class GSContext; typedef enum { @@ -72,6 +73,7 @@ typedef enum { BOOL _antialias; NSPoint _patternPhase; NSCompositingOperation _compositingOperation; + NSShadow *_shadow; /* Current shadow, or nil */ } - initWithDrawContext: (GSContext *)context; @@ -89,6 +91,8 @@ typedef enum { - (void) setPatternPhase: (NSPoint)phase; - (NSCompositingOperation) compositingOperation; - (void) setCompositingOperation:(NSCompositingOperation) operation; +- (void) setShadow: (NSShadow *)shadow; +- (NSShadow *) shadow; - (void) compositeGState: (GSGState *)source fromRect: (NSRect)aRect diff --git a/Source/art/path.m b/Source/art/path.m index 4d3de874..ff80b641 100644 --- a/Source/art/path.m +++ b/Source/art/path.m @@ -30,6 +30,9 @@ #include #include +#include +#include +#include #include "ARTGState.h" @@ -42,6 +45,9 @@ #include #include +@interface ARTGState (ShadowRendering) +- (void) _stroke: (ArtVpath *)vp; +@end #if 0 /* useful when debugging */ @@ -950,6 +956,59 @@ - (void) _fill: (int)rule UPDATE_UNBUFFERED } +/* Renders the current path offset by the active shadow, in the shadow colour, + as a plain (unblurred) shadow, before the path itself is drawn. The shadow + parameters live on the shared GSGState. */ +- (void) _drawShadowForOperation: (ctxt_object_t)drawType +{ + NSColor *shadowColor; + NSBezierPath *savedPath, *shadowPath; + NSAffineTransform *xform; + device_color_t savedColor, dc; + NSSize soffset; + CGFloat r, g, b, a; + BOOL isStroke = (drawType == path_stroke); + color_state_t cs = isStroke ? COLOR_STROKE : COLOR_FILL; + + if (_shadow == nil || path == nil || [path isEmpty]) + return; + + shadowColor = [[_shadow shadowColor] + colorUsingColorSpaceName: NSDeviceRGBColorSpace]; + if (shadowColor == nil) + return; + [shadowColor getRed: &r green: &g blue: &b alpha: &a]; + + soffset = [_shadow shadowOffset]; + savedPath = path; + shadowPath = [path copy]; + xform = [NSAffineTransform transform]; + [xform translateXBy: soffset.width yBy: soffset.height]; + [shadowPath transformUsingAffineTransform: xform]; + + savedColor = isStroke ? strokeColor : fillColor; + gsMakeColor(&dc, rgb_colorspace, r, g, b, 0); + dc.field[AINDEX] = a; + + path = shadowPath; + [self setColor: &dc state: cs]; + if (isStroke) + { + ArtVpath *vp = [self _vpath_from_current_path: NO]; + + if (vp) + [self _stroke: vp]; + } + else + { + [self _fill: (drawType == path_eofill) + ? ART_WIND_RULE_ODDEVEN : ART_WIND_RULE_NONZERO]; + } + [self setColor: &savedColor state: cs]; + path = savedPath; + RELEASE(shadowPath); +} + - (void) DPSeofill { if (pattern != nil) @@ -958,6 +1017,7 @@ - (void) DPSeofill return; } + [self _drawShadowForOperation: path_eofill]; [self _fill: ART_WIND_RULE_ODDEVEN]; } @@ -969,6 +1029,7 @@ - (void) DPSfill return; } + [self _drawShadowForOperation: path_fill]; [self _fill: ART_WIND_RULE_NONZERO]; } @@ -1381,6 +1442,8 @@ - (void) DPSstroke if (all_clipped) return; if (!stroke_color[3]) return; + [self _drawShadowForOperation: path_stroke]; + /* TODO: this is wrong. we should transform _after_ we dash and stroke */ vp = [self _vpath_from_current_path: NO]; diff --git a/Source/cairo/CairoGState.m b/Source/cairo/CairoGState.m index b08467e5..58284431 100644 --- a/Source/cairo/CairoGState.m +++ b/Source/cairo/CairoGState.m @@ -33,6 +33,7 @@ #include #include #include +#include #include "cairo/CairoGState.h" #include "cairo/CairoFontInfo.h" #include "cairo/CairoSurface.h" @@ -760,6 +761,8 @@ - (void) DPSeofill return; } + [self _drawShadowForOperation: path_eofill]; + c = fillColor; gsColorToRGB(&c); // The underlying concept does not allow to determine if alpha is set or not. @@ -772,6 +775,50 @@ - (void) DPSeofill [self DPSnewpath]; } +/* Renders the current path offset by the active shadow, in the shadow colour, + as a plain (unblurred) shadow. The shadow parameters live on the shared + GSGState so that any backend can render a shadow the same way; blur is not + yet applied. */ +- (void) _drawShadowForOperation: (ctxt_object_t)drawType +{ + NSColor *shadowColor; + NSSize soffset; + CGFloat r, g, b, a; + + if (_shadow == nil || _ct == NULL) + return; + + shadowColor = [[_shadow shadowColor] + colorUsingColorSpaceName: NSDeviceRGBColorSpace]; + if (shadowColor == nil) + return; + [shadowColor getRed: &r green: &g blue: &b alpha: &a]; + + soffset = [_shadow shadowOffset]; + + cairo_save(_ct); + /* The offset is expressed in the current user space, which is what the path + is built in, so translate by it directly. */ + cairo_translate(_ct, soffset.width, soffset.height); + cairo_set_source_rgba(_ct, r, g, b, a); + [self _setPath]; + switch (drawType) + { + case path_eofill: + cairo_set_fill_rule(_ct, CAIRO_FILL_RULE_EVEN_ODD); + /* fall through */ + case path_fill: + cairo_fill(_ct); + break; + case path_stroke: + cairo_stroke(_ct); + break; + default: + break; + } + cairo_restore(_ct); +} + - (void) DPSfill { if (_ct) @@ -784,6 +831,8 @@ - (void) DPSfill return; } + [self _drawShadowForOperation: path_fill]; + c = fillColor; gsColorToRGB(&c); // The underlying concept does not allow to determine if alpha is set or not. @@ -809,6 +858,8 @@ - (void) DPSstroke BOOL zeroLineWidth = NO; device_color_t c; + [self _drawShadowForOperation: path_stroke]; + c = strokeColor; gsColorToRGB(&c); // The underlying concept does not allow to determine if alpha is set or not. diff --git a/Source/gsc/GSContext.m b/Source/gsc/GSContext.m index 5c06d79b..44913eff 100644 --- a/Source/gsc/GSContext.m +++ b/Source/gsc/GSContext.m @@ -290,6 +290,16 @@ - (void) setCompositingOperation: (NSCompositingOperation)operation [gstate setCompositingOperation: operation]; } +- (void) setShadow: (NSShadow *)shadow +{ + [gstate setShadow: shadow]; +} + +- (NSShadow *) shadow +{ + return [gstate shadow]; +} + @end @implementation GSContext (Ops) diff --git a/Source/gsc/GSGState.m b/Source/gsc/GSGState.m index a4547e7b..2332b7cd 100644 --- a/Source/gsc/GSGState.m +++ b/Source/gsc/GSGState.m @@ -75,6 +75,7 @@ - (void) dealloc RELEASE(fillColorS); RELEASE(strokeColorS); TEST_RELEASE(pattern); + TEST_RELEASE(_shadow); [super dealloc]; } @@ -97,6 +98,8 @@ - (id) deepen RETAIN(strokeColorS); if (pattern != nil) RETAIN(pattern); + if (_shadow != nil) + RETAIN(_shadow); return self; } @@ -177,6 +180,16 @@ - (void) setCompositingOperation: (NSCompositingOperation)operation _compositingOperation = operation; } +- (void) setShadow: (NSShadow *)shadow +{ + ASSIGN(_shadow, shadow); +} + +- (NSShadow *) shadow +{ + return _shadow; +} + // This is only a fall back, the method should not be called any more. - (void) compositeGState: (GSGState *)source fromRect: (NSRect)aRect diff --git a/Source/winlib/WIN32GState.m b/Source/winlib/WIN32GState.m index 3d20061f..856e55c9 100644 --- a/Source/winlib/WIN32GState.m +++ b/Source/winlib/WIN32GState.m @@ -46,6 +46,7 @@ #import #import #import +#import #import #import @@ -1165,17 +1166,60 @@ - (void)DPSeoclip [self _paintPath: path_eoclip]; } -- (void)DPSeofill +/* Renders the current path offset by the active shadow, in the shadow colour, + as a plain (unblurred) shadow, before the path itself is drawn. The shadow + parameters live on the shared GSGState. */ +- (void) _drawShadowForOperation: (ctxt_object_t)drawType +{ + NSColor *shadowColor; + NSBezierPath *savedPath, *shadowPath; + NSAffineTransform *xform; + device_color_t savedColor, dc; + NSSize soffset; + CGFloat r, g, b, a; + color_state_t cs = (drawType == path_stroke) ? COLOR_STROKE : COLOR_FILL; + + if (_shadow == nil || path == nil || [path isEmpty]) + return; + + shadowColor = [[_shadow shadowColor] + colorUsingColorSpaceName: NSDeviceRGBColorSpace]; + if (shadowColor == nil) + return; + [shadowColor getRed: &r green: &g blue: &b alpha: &a]; + + soffset = [_shadow shadowOffset]; + savedPath = path; + shadowPath = [path copy]; + xform = [NSAffineTransform transform]; + [xform translateXBy: soffset.width yBy: soffset.height]; + [shadowPath transformUsingAffineTransform: xform]; + + savedColor = (cs == COLOR_STROKE) ? strokeColor : fillColor; + gsMakeColor(&dc, rgb_colorspace, r, g, b, 0); + dc.field[AINDEX] = a; + + path = shadowPath; + [self setColor: &dc state: cs]; + [self _paintPath: drawType]; + [self setColor: &savedColor state: cs]; + path = savedPath; + RELEASE(shadowPath); +} + +- (void)DPSeofill { if (pattern != nil) { [self eofillPath: path withPattern: pattern]; return; } + + [self _drawShadowForOperation: path_eofill]; [self _paintPath: path_eofill]; } -- (void)DPSfill +- (void)DPSfill { if (pattern != nil) { @@ -1183,11 +1227,13 @@ - (void)DPSfill return; } + [self _drawShadowForOperation: path_fill]; [self _paintPath: path_fill]; } -- (void)DPSstroke +- (void)DPSstroke { + [self _drawShadowForOperation: path_stroke]; [self _paintPath: path_stroke]; } diff --git a/Source/xlib/XGGState.m b/Source/xlib/XGGState.m index b646b7aa..5f11b83b 100644 --- a/Source/xlib/XGGState.m +++ b/Source/xlib/XGGState.m @@ -31,9 +31,12 @@ #import #import #import +#import #import +#import #import #import +#import #import "xlib/XGGeometry.h" #import "xlib/XGContext.h" @@ -1469,7 +1472,56 @@ - (void)DPSeoclip [self _paintPath: path_eoclip]; } -- (void)DPSeofill +/* Renders the current path offset by the active shadow, in the shadow colour, + as a plain (unblurred) shadow, before the path itself is drawn. The shadow + parameters live on the shared GSGState. The colour state is invalidated so + that the following real paint reloads its own colour. */ +- (void) _drawShadowForOperation: (ctxt_object_t)drawType +{ + NSColor *shadowColor; + NSBezierPath *savedPath, *shadowPath; + NSAffineTransform *xform; + device_color_t savedColor, dc; + NSSize soffset; + CGFloat r, g, b, a; + color_state_t cs = (drawType == path_stroke) ? COLOR_STROKE : COLOR_FILL; + + if (_shadow == nil || path == nil || [path isEmpty]) + return; + + shadowColor = [[_shadow shadowColor] + colorUsingColorSpaceName: NSDeviceRGBColorSpace]; + if (shadowColor == nil) + return; + [shadowColor getRed: &r green: &g blue: &b alpha: &a]; + + soffset = [_shadow shadowOffset]; + savedPath = path; + shadowPath = [path copy]; + xform = [NSAffineTransform transform]; + [xform translateXBy: soffset.width yBy: soffset.height]; + [shadowPath transformUsingAffineTransform: xform]; + + savedColor = (cs == COLOR_STROKE) ? strokeColor : fillColor; + gsMakeColor(&dc, rgb_colorspace, r, g, b, 0); + dc.field[AINDEX] = a; + + path = shadowPath; + [self setColor: &dc state: cs]; + [self _paintPath: drawType]; + path = savedPath; + RELEASE(shadowPath); + + /* Setting the shadow colour overwrote the fill or stroke colour ivar; put it + back and invalidate the loaded colour so the real paint reloads it. */ + if (cs == COLOR_STROKE) + strokeColor = savedColor; + else + fillColor = savedColor; + cstate &= ~cs; +} + +- (void)DPSeofill { if (pattern != nil) { @@ -1477,13 +1529,15 @@ - (void)DPSeofill return; } + [self _drawShadowForOperation: path_eofill]; + if ((cstate & COLOR_FILL) == 0) [self setColor: &fillColor state: COLOR_FILL]; [self _paintPath: path_eofill]; } -- (void)DPSfill +- (void)DPSfill { if (pattern != nil) { @@ -1491,6 +1545,8 @@ - (void)DPSfill return; } + [self _drawShadowForOperation: path_fill]; + if ((cstate & COLOR_FILL) == 0) [self setColor: &fillColor state: COLOR_FILL]; @@ -1609,8 +1665,10 @@ - (void)DPSrectstroke: (CGFloat)x : (CGFloat)y : (CGFloat)w : (CGFloat)h } } -- (void)DPSstroke +- (void)DPSstroke { + [self _drawShadowForOperation: path_stroke]; + if ((cstate & COLOR_STROKE) == 0) [self setColor: &fillColor state: COLOR_STROKE]; diff --git a/Tests/cairo/GNUmakefile.preamble b/Tests/cairo/GNUmakefile.preamble index 90425f80..f0f166d9 100644 --- a/Tests/cairo/GNUmakefile.preamble +++ b/Tests/cairo/GNUmakefile.preamble @@ -7,9 +7,9 @@ ADDITIONAL_INCLUDE_DIRS += -I$(GNUSTEP_BUILD_DIR)/../.. -I../.. -include $(GNUSTEP_BUILD_DIR)/../../config.make -include ../../config.make -# The rendering test drives the installed backend through AppKit and builds -# only for the cairo graphics backend, so link the gui there. -ifeq ($(BUILD_GRAPHICS),cairo) +# The rendering tests drive the installed backend through AppKit, so link the +# gui for the raster graphics backends they build for. +ifneq ($(filter $(BUILD_GRAPHICS),cairo art),) ADDITIONAL_TOOL_LIBS += -lgnustep-gui endif diff --git a/Tests/cairo/dropshadow.m b/Tests/cairo/dropshadow.m new file mode 100644 index 00000000..f340a78a --- /dev/null +++ b/Tests/cairo/dropshadow.m @@ -0,0 +1,175 @@ +/* Tests that a shadow set on the graphics context is painted by the backend + * through the AppKit offscreen path. A shape is drawn with a coloured shadow + * at a known offset and the offset region is checked for the shadow colour, + * the shape is checked to still sit on top of its own shadow, and the shadow + * state is checked to follow the graphics state stack. Fill, even-odd fill and + * stroke are each exercised. + * + * It needs a running window server, so it skips cleanly when there is none, and + * it guards on the cairo or art graphics backend, whose offscreen drawing can + * be read back reliably in a headless test run. The xlib and winlib backends + * render shadows the same way but are not exercised here. Colours are checked + * with a small tolerance to allow for the backend's fixed-point arithmetic. + */ +#import +#import "Testing.h" +#include "config.h" + +#if defined(BUILD_GRAPHICS) \ + && (BUILD_GRAPHICS == GRAPHICS_cairo \ + || BUILD_GRAPHICS == GRAPHICS_art) + +#import +#import +#import +#include + +static NSImage * +beginImage(int w, int h) +{ + NSImage *img = [[NSImage alloc] initWithSize: NSMakeSize(w, h)]; + [img lockFocus]; + return img; +} + +static NSBitmapImageRep * +endImage(NSImage *img, int w, int h) +{ + NSBitmapImageRep *rep; + + [[NSGraphicsContext currentContext] flushGraphics]; + rep = [[NSBitmapImageRep alloc] + initWithFocusedViewRect: NSMakeRect(0, 0, w, h)]; + [img unlockFocus]; + [img release]; + return [rep autorelease]; +} + +/* Check the RGB sample at (x, y) with a small tolerance. The rep row 0 is the + * top of the image, so callers flip y themselves. */ +static BOOL +pixelIs(NSBitmapImageRep *rep, int x, int y, int r, int g, int b) +{ + NSUInteger px[5]; + + [rep getPixel: px atX: x y: y]; + return (abs((int)px[0] - r) <= 2 + && abs((int)px[1] - g) <= 2 + && abs((int)px[2] - b) <= 2); +} + +static NSShadow * +redShadow(void) +{ + NSShadow *s = [[[NSShadow alloc] init] autorelease]; + + [s setShadowColor: [NSColor colorWithDeviceRed: 1.0 green: 0.0 blue: 0.0 + alpha: 1.0]]; + [s setShadowOffset: NSMakeSize(8, -8)]; + [s setShadowBlurRadius: 0.0]; + return s; +} + +int +main(int argc, const char **argv) +{ + CREATE_AUTORELEASE_POOL(pool); + int w = 40, h = 40; + NSImage *img; + NSBitmapImageRep *rep; + NSBezierPath *p; + + { + const char *x11 = getenv("DISPLAY"); + const char *wayland = getenv("WAYLAND_DISPLAY"); + + if ((x11 == NULL || *x11 == '\0') + && (wayland == NULL || *wayland == '\0')) + { + NSLog(@"no window server available; skipping shadow tests"); + DESTROY(pool); + return 0; + } + } + + [NSApplication sharedApplication]; + + /* A blue rectangle with a red shadow offset to the lower right. The shape + * spans device x,y in 10..22 and the shadow 18..30 vertically 12..24. */ + img = beginImage(w, h); + [[NSColor whiteColor] set]; + NSRectFill(NSMakeRect(0, 0, w, h)); + [NSGraphicsContext saveGraphicsState]; + [redShadow() set]; + [[NSColor colorWithDeviceRed: 0.0 green: 0.0 blue: 1.0 alpha: 1.0] set]; + [[NSBezierPath bezierPathWithRect: NSMakeRect(10, 20, 12, 12)] fill]; + [NSGraphicsContext restoreGraphicsState]; + rep = endImage(img, w, h); + PASS(rep != nil && pixelIs(rep, 28, h - 1 - 16, 255, 0, 0), + "a filled shape casts a shadow at the offset location"); + PASS(rep != nil && pixelIs(rep, 12, h - 1 - 30, 0, 0, 255), + "a filled shape is drawn in its own colour"); + PASS(rep != nil && pixelIs(rep, 20, h - 1 - 22, 0, 0, 255), + "a filled shape sits on top of its own shadow"); + PASS(rep != nil && pixelIs(rep, 3, h - 1 - 3, 255, 255, 255), + "the shadow does not extend beyond its offset region"); + + /* Even-odd fill casts a shadow through the same path. */ + img = beginImage(w, h); + [[NSColor whiteColor] set]; + NSRectFill(NSMakeRect(0, 0, w, h)); + [NSGraphicsContext saveGraphicsState]; + [redShadow() set]; + [[NSColor colorWithDeviceRed: 0.0 green: 0.0 blue: 1.0 alpha: 1.0] set]; + p = [NSBezierPath bezierPathWithRect: NSMakeRect(10, 20, 12, 12)]; + [p setWindingRule: NSEvenOddWindingRule]; + [p fill]; + [NSGraphicsContext restoreGraphicsState]; + rep = endImage(img, w, h); + PASS(rep != nil && pixelIs(rep, 28, h - 1 - 16, 255, 0, 0), + "an even-odd fill casts a shadow at the offset location"); + + /* A stroked shape casts a shadow of its outline. */ + img = beginImage(w, h); + [[NSColor whiteColor] set]; + NSRectFill(NSMakeRect(0, 0, w, h)); + [NSGraphicsContext saveGraphicsState]; + [redShadow() set]; + [[NSColor colorWithDeviceRed: 0.0 green: 0.0 blue: 1.0 alpha: 1.0] set]; + p = [NSBezierPath bezierPathWithRect: NSMakeRect(10, 20, 12, 12)]; + [p setLineWidth: 6]; + [p stroke]; + [NSGraphicsContext restoreGraphicsState]; + rep = endImage(img, w, h); + PASS(rep != nil && pixelIs(rep, 30, h - 1 - 18, 255, 0, 0), + "a stroked shape casts a shadow of its outline"); + PASS(rep != nil && pixelIs(rep, 22, h - 1 - 26, 0, 0, 255), + "a stroked shape is drawn in its own colour"); + + /* The shadow follows the graphics state stack: after the state that set it is + * restored, a further fill casts no shadow. */ + img = beginImage(w, h); + [[NSColor whiteColor] set]; + NSRectFill(NSMakeRect(0, 0, w, h)); + [NSGraphicsContext saveGraphicsState]; + [redShadow() set]; + [NSGraphicsContext restoreGraphicsState]; + [[NSColor colorWithDeviceRed: 0.0 green: 0.0 blue: 1.0 alpha: 1.0] set]; + [[NSBezierPath bezierPathWithRect: NSMakeRect(10, 20, 12, 12)] fill]; + rep = endImage(img, w, h); + PASS(rep != nil && pixelIs(rep, 28, h - 1 - 16, 255, 255, 255), + "a restored graphics state clears the shadow"); + + DESTROY(pool); + return 0; +} + +#else + +int +main(int argc, const char **argv) +{ + return 0; +} + +#endif