From 53bb307fd305dc9427fec4ecef0dcf8e4e9bfbaf Mon Sep 17 00:00:00 2001 From: pkgdemon Date: Sun, 5 Apr 2026 20:48:32 -0500 Subject: [PATCH 1/8] Fixes to make opal functional --- Source/opal/OpalContext.m | 10 ++----- Source/opal/OpalGState.m | 33 +++++++++++++++++++++++ Source/opal/OpalSurface.m | 3 ++- Source/x11/XGServerEvent.m | 27 +++++++++++++++++++ Source/x11/XGServerWindow.m | 52 ++++++++++++++++++++++++++++++------- 5 files changed, 106 insertions(+), 19 deletions(-) diff --git a/Source/opal/OpalContext.m b/Source/opal/OpalContext.m index 30a6b31b..2542f000 100644 --- a/Source/opal/OpalContext.m +++ b/Source/opal/OpalContext.m @@ -45,6 +45,7 @@ @implementation OpalContext + (void) initializeBackend { + NSLog(@"OpalContext: initializeBackend called"); [NSGraphicsContext setDefaultContextClass: self]; [GSFontEnumerator setDefaultClass: [OpalFontEnumerator class]]; @@ -63,16 +64,8 @@ - (BOOL) supportsDrawGState - (BOOL) isDrawingToScreen { -#warning isDrawingToScreen returning NO to fix DPSimage - return NO; - - // NOTE: This was returning NO because it was not looking at the - // return value of GSCurrentSurface. Now it returns YES, which - // seems to have broken image drawing (yellow rectangles are drawn instead) OpalSurface *surface; - [OGSTATE GSCurrentSurface: &surface : NULL : NULL]; - return [surface isDrawingToScreen]; } @@ -175,6 +168,7 @@ - (void) GSCurrentDevice: (void **)device : (int *)x : (int *)y } - (void) GSSetDevice: (void *)device + : (int)x : (int)y { diff --git a/Source/opal/OpalGState.m b/Source/opal/OpalGState.m index 31333d0b..be80ce45 100644 --- a/Source/opal/OpalGState.m +++ b/Source/opal/OpalGState.m @@ -1104,4 +1104,37 @@ - (void) restoreClip: (void *)savedClip free(savedClip); } + +// Gradient rendering - stub implementations to prevent crashes +- (void) drawGradient: (NSGradient*)gradient + fromPoint: (NSPoint)startPoint + toPoint: (NSPoint)endPoint + options: (NSUInteger)options +{ + // TODO: Implement using CGGradientCreateWithColorComponents + CGContextDrawLinearGradient + NSLog(@"OpalGState: drawGradient (linear) - stub, drawing fallback fill"); + + // Fallback: fill with the first color of the gradient + CGContextRef ctx = [self CGContext]; + if (ctx && gradient) + { + NSColor *color = [gradient interpolatedColorAtLocation: 0.0]; + CGFloat r, g, b, a; + [[color colorUsingColorSpaceName: NSCalibratedRGBColorSpace] + getRed: &r green: &g blue: &b alpha: &a]; + CGContextSetRGBFillColor(ctx, r, g, b, a); + } +} + +- (void) drawGradient: (NSGradient*)gradient + fromCenter: (NSPoint)startCenter + radius: (CGFloat)startRadius + toCenter: (NSPoint)endCenter + radius: (CGFloat)endRadius + options: (NSUInteger)options +{ + // TODO: Implement using CGGradientCreateWithColorComponents + CGContextDrawRadialGradient + NSLog(@"OpalGState: drawGradient (radial) - stub"); +} + @end diff --git a/Source/opal/OpalSurface.m b/Source/opal/OpalSurface.m index 1dcd833e..f9521b57 100644 --- a/Source/opal/OpalSurface.m +++ b/Source/opal/OpalSurface.m @@ -119,7 +119,7 @@ - (void) createCGContextsWithSuppliedBackingContext: (CGContextRef)ctx _backingCGContext = createCGBitmapContext(pixelsWide, pixelsHigh); } - NSDebugLLog(@"OpalSurface", @"Created CGContexts: X11=%p, backing=%p, width=%d height=%d", + NSLog(@"OpalSurface Created CGContexts: X11=%p, backing=%p, width=%d height=%d", _x11CGContext, _backingCGContext, pixelsWide, pixelsHigh); } @@ -163,6 +163,7 @@ - (CGContextRef) x11CGContext - (void) handleExposeRect: (NSRect)rect { + NSLog(@"OpalSurface handleExposeRect: %@ backing=%p x11=%p", NSStringFromRect(rect), _backingCGContext, _x11CGContext); NSDebugLLog(@"OpalSurface", @"handleExposeRect %@", NSStringFromRect(rect)); if (!_backingCGContext) diff --git a/Source/x11/XGServerEvent.m b/Source/x11/XGServerEvent.m index fb8f9761..80bf975d 100644 --- a/Source/x11/XGServerEvent.m +++ b/Source/x11/XGServerEvent.m @@ -940,6 +940,22 @@ - (void) processEvent: (XEvent *) event if (cWin != 0) { + /* If no graphics driver attached yet, create the surface now. + * This is critical for the opal backend where the surface + * only gets created via GSSetDevice, which requires + * setWindowdevice to be called. Without this, opal windows + * never render because _processResizeEvent never fires. + */ + if (cWin->gdriver == NULL && cWin->ident != 0) + { + NSGraphicsContext *ctxt = GSCurrentContext(); + if (ctxt != nil) + { + NSLog(@"ConfigureNotify: creating surface for window %lu", cWin->number); + [self setWindowdevice: cWin->number forContext: ctxt]; + } + } + NSRect r, x, n, h; NSTimeInterval ts = (NSTimeInterval)generic.lastMotion; @@ -1454,6 +1470,17 @@ - (void) processEvent: (XEvent *) event if (cWin != 0) { cWin->map_state = IsViewable; + + /* Create surface on first map if not yet created */ + if (cWin->gdriver == NULL && cWin->ident != 0) + { + NSGraphicsContext *ctxt = GSCurrentContext(); + if (ctxt != nil) + { + NSLog(@"MapNotify: creating surface for window %lu", cWin->number); + [self setWindowdevice: cWin->number forContext: ctxt]; + } + } /* * if the window that was just mapped wants the input * focus, re-do the request. diff --git a/Source/x11/XGServerWindow.m b/Source/x11/XGServerWindow.m index 98392693..b4e049d9 100644 --- a/Source/x11/XGServerWindow.m +++ b/Source/x11/XGServerWindow.m @@ -951,6 +951,13 @@ - (BOOL) _checkStyle: (unsigned)style // _NET_REQUEST_FRAME_EXTENTS [self orderwindow: NSWindowAbove : 0 : window->number]; + /* Ensure the window has visible content by clearing it. + * This is needed for backends (like opal) that don't attach + * an X11 surface until GSSetDevice is called. Without content, + * the X server may not generate VisibilityNotify. + */ + XClearWindow(dpy, window->ident); + XFlush(dpy); XSync(dpy, False); while (XPending(dpy) > 0 || window->visibility > 1) { @@ -958,15 +965,11 @@ - (BOOL) _checkStyle: (unsigned)style { NSDate *until; - /* In theory, after executing XSync() all events resulting from - * our window creation and ordering front should be available in - * the X event queue. - * However, it's possible that a window manager - * could send some events after the XSync() has been satisfied, - * so if we have not received a visibility notification - * we can wait for up to a second for more events. + /* Wait briefly for visibility notification. + * Reduced from 1.0s to 0.1s - if the window isn't visible + * by now, it won't become visible (e.g. no window manager). */ - until = [NSDate dateWithTimeIntervalSinceNow: 1.0]; + until = [NSDate dateWithTimeIntervalSinceNow: 0.1]; while (XPending(dpy) == 0 && [until timeIntervalSinceNow] > 0.0) { CREATE_AUTORELEASE_POOL(pool); @@ -978,8 +981,8 @@ - (BOOL) _checkStyle: (unsigned)style } if (XPending(dpy) == 0) { - NSLog(@"Waited for a second, but the X system never" - @" made the window visible"); + NSDebugLLog(@"Offset", @"No visibility notification" + @" for probe window - using default offsets"); break; } } @@ -2861,6 +2864,7 @@ - (int) _createAppIconPixmaps - (void) orderwindow: (int)op : (int)otherWin : (int)winNum { + NSLog(@"orderwindow: op=%d otherWin=%d winNum=%d", op, otherWin, winNum); gswindow_device_t *window; gswindow_device_t *other; int level; @@ -3088,6 +3092,21 @@ - (void) orderwindow: (int)op : (int)otherWin : (int)winNum CWStackMode, &chg); } XMapWindow(dpy, window->ident); + /* Ensure the window has a graphics context/surface attached. + * This is critical for backends like opal that don't create + * their surface until GSSetDevice is called. Without this, + * the window maps but has no drawing surface, so expose events + * produce no visible content. + */ + NSLog(@"orderwindow: gdriver=%p ctxt=%p winNum=%d", window->gdriver, GSCurrentContext(), window->number); + if (window->gdriver == NULL) + { + NSGraphicsContext *ctxt = GSCurrentContext(); + if (ctxt != nil) + { + [self setWindowdevice: window->number forContext: ctxt]; + } + } break; case NSWindowOut: @@ -3677,6 +3696,19 @@ - (void) _addExposedRectangle: (XRectangle)rectangle : (int)win : (BOOL) ignoreB if (!window) return; + /* If no graphics driver is attached to this window yet, create one. + * This is needed for the opal backend which only creates its surface + * in GSSetDevice, which may not have been called yet. + */ + if (window->gdriver == NULL && window->ident != 0) + { + NSGraphicsContext *ctxt = GSCurrentContext(); + if (ctxt != nil) + { + [self setWindowdevice: win forContext: ctxt]; + } + } + if (!ignoreBacking && window->type != NSBackingStoreNonretained) { XGCValues values; From eccedfcacae3487b51f704ccbb60da0369bd4745 Mon Sep 17 00:00:00 2001 From: pkgdemon Date: Sun, 5 Apr 2026 21:26:23 -0500 Subject: [PATCH 2/8] Fix more usability issues --- Source/opal/OpalContext.m | 12 +- Source/opal/OpalFontInfo.m | 44 ++++--- Source/opal/OpalGState.m | 262 ++++++++++++++++++++++++++++++++----- Source/opal/OpalSurface.m | 2 +- 4 files changed, 262 insertions(+), 58 deletions(-) diff --git a/Source/opal/OpalContext.m b/Source/opal/OpalContext.m index 2542f000..dd631a8f 100644 --- a/Source/opal/OpalContext.m +++ b/Source/opal/OpalContext.m @@ -143,17 +143,15 @@ - (BOOL) isCompatibleBitmap: (NSBitmapImageRep*)bitmap return NO; } - // FIXME: Allow more image types as soon as the Opal backend handles them correctly colorSpaceName = [bitmap colorSpaceName]; - if (![colorSpaceName isEqualToString: NSDeviceRGBColorSpace] && - ![colorSpaceName isEqualToString: NSCalibratedRGBColorSpace]) - { - return NO; - } - else + if ([colorSpaceName isEqualToString: NSDeviceRGBColorSpace] || + [colorSpaceName isEqualToString: NSCalibratedRGBColorSpace] || + [colorSpaceName isEqualToString: NSDeviceWhiteColorSpace] || + [colorSpaceName isEqualToString: NSCalibratedWhiteColorSpace]) { return YES; } + return NO; } - (void) GSCurrentDevice: (void **)device : (int *)x : (int *)y diff --git a/Source/opal/OpalFontInfo.m b/Source/opal/OpalFontInfo.m index 0b904bf5..9d82e8c0 100644 --- a/Source/opal/OpalFontInfo.m +++ b/Source/opal/OpalFontInfo.m @@ -302,35 +302,39 @@ - (NSGlyph) glyphWithName: (NSString *) glyphName - (NSRect) boundingRectForGlyph: (NSGlyph)glyph { -#if 0 - cairo_text_extents_t ctext; - - if (_cairo_extents_for_NSGlyph(_scaled, glyph, &ctext)) + // Use glyph advance as approximation for bounding rect + CGGlyph cgGlyph = (CGGlyph)glyph; + int advance = 0; + if (_faceInfo && [_faceInfo fontFace]) { - return NSMakeRect(ctext.x_bearing, ctext.y_bearing, - ctext.width, ctext.height); + CGFontGetGlyphAdvances([_faceInfo fontFace], &cgGlyph, 1, &advance); + int unitsPerEm = CGFontGetUnitsPerEm([_faceInfo fontFace]); + if (unitsPerEm > 0) + { + CGFloat scale = matrix[0] / (CGFloat)unitsPerEm; + CGFloat w = advance * scale; + return NSMakeRect(0, descender, w, ascender - descender); + } } -#endif - return NSMakeRect(0,0,10,10); + return NSMakeRect(0, descender, matrix[0] * 0.6, ascender - descender); } - (CGFloat) widthOfString: (NSString *)string { -#if 0 - cairo_text_extents_t ctext; - - if (!string) - { - return 0.0; - } + if (!string || [string length] == 0) + return 0.0; - cairo_scaled_font_text_extents(_scaled, [string UTF8String], &ctext); - if (cairo_scaled_font_status(_scaled) == CAIRO_STATUS_SUCCESS) + // Sum glyph advances for the string + CGFloat totalWidth = 0; + NSUInteger len = [string length]; + for (NSUInteger i = 0; i < len; i++) { - return ctext.width; + unichar ch = [string characterAtIndex: i]; + NSGlyph g = [self glyphForCharacter: ch]; + NSSize adv = [self advancementForGlyph: g]; + totalWidth += adv.width; } -#endif - return 100.0; + return totalWidth; } - (void) appendBezierPathWithGlyphs: (NSGlyph *)glyphs diff --git a/Source/opal/OpalGState.m b/Source/opal/OpalGState.m index be80ce45..bf35c1aa 100644 --- a/Source/opal/OpalGState.m +++ b/Source/opal/OpalGState.m @@ -29,6 +29,8 @@ #import // NS*ColorSpace #import #import +#import +#import #import "opal/OpalGState.h" #import "opal/OpalSurface.h" #import "opal/OpalFontInfo.h" @@ -53,6 +55,32 @@ static inline NSPoint _NSPointFromCGPoint(CGPoint cgpoint) return NSMakePoint(cgpoint.x, cgpoint.y); } + +#import + +// Map NSCompositingOperation to CGBlendMode +static inline CGBlendMode +_opalBlendModeForOp(NSCompositingOperation op) +{ + switch (op) + { + case NSCompositeClear: return kCGBlendModeClear; + case NSCompositeCopy: return kCGBlendModeCopy; + case NSCompositeSourceOver: return kCGBlendModeNormal; + case NSCompositeSourceIn: return kCGBlendModeSourceIn; + case NSCompositeSourceOut: return kCGBlendModeSourceOut; + case NSCompositeSourceAtop: return kCGBlendModeSourceAtop; + case NSCompositeDestinationOver: return kCGBlendModeDestinationOver; + case NSCompositeDestinationIn: return kCGBlendModeDestinationIn; + case NSCompositeDestinationOut: return kCGBlendModeDestinationOut; + case NSCompositeDestinationAtop: return kCGBlendModeDestinationAtop; + case NSCompositeXOR: return kCGBlendModeXOR; + case NSCompositePlusDarker: return kCGBlendModePlusDarker; + case NSCompositePlusLighter: return kCGBlendModePlusLighter; + default: return kCGBlendModeNormal; + } +} + @implementation OpalGState - (void) dealloc @@ -145,6 +173,7 @@ - (void) setColor: (device_color_t *)color state: (color_state_t)cState @end + @implementation OpalGState (Ops) - (void) DPSshow: (const char *)s @@ -166,13 +195,12 @@ - (void) GSShowText: (const char *)s : (size_t) length NSDebugLLog(@"OpalGState", @"%p (%@): %s", self, [self class], __PRETTY_FUNCTION__); CGContextRef cgctx = CGCTX; - if (cgctx) + if (cgctx && s && length > 0) { - CGContextSaveGState(cgctx); - CGContextSetRGBFillColor(cgctx, 0, 1, 0, 1); - CGContextFillRect(cgctx, CGRectMake(0, 0, length * 12, 12)); - CGContextRestoreGState(cgctx); - // TODO: implement! + CGPoint pt = CGContextGetPathCurrentPoint(cgctx); + pt.y += [self->font defaultLineHeightForFont] * 0.3; + CGContextSetTextPosition(cgctx, pt.x, pt.y); + CGContextShowText(cgctx, s, length); } } @@ -215,8 +243,8 @@ - (void) GSShowGlyphsWithAdvances: (const NSGlyph *)glyphs : (const NSSize *)adv } CGPoint pt = CGContextGetPathCurrentPoint(cgctx); - // FIXME: why? - pt.y += [self->font defaultLineHeightForFont] * 0.5; + // Offset Y to account for flipped coordinate system + pt.y += [self->font defaultLineHeightForFont] * 0.3; CGContextSetTextPosition(cgctx, pt.x, pt.y); CGContextShowGlyphsWithAdvances(cgctx, cgglyphs, (const CGSize *)advances, length); @@ -734,8 +762,79 @@ - (void) GSSendBezierPath: (NSBezierPath *)newpath - (NSDictionary *) GSReadRect: (NSRect)r { - NSDebugLLog(@"OpalGState", @"%p (%@): %s", self, [self class], __PRETTY_FUNCTION__); - return nil; + NSDebugLLog(@"OpalGState", @"%p (%@): %s - %@", self, [self class], __PRETTY_FUNCTION__, NSStringFromRect(r)); + + CGContextRef ctx = CGCTX; + if (!ctx) return nil; + + int x = (int)r.origin.x; + int y = (int)r.origin.y; + int w = (int)r.size.width; + int h = (int)r.size.height; + if (w <= 0 || h <= 0) return nil; + + // Create a temporary bitmap context to read pixels into + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef tmpCtx = CGBitmapContextCreate(NULL, w, h, 8, w * 4, cs, + kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); + CGColorSpaceRelease(cs); + if (!tmpCtx) return nil; + + // Get image from the backing context + CGImageRef img = CGBitmapContextCreateImage(ctx); + if (!img) { CGContextRelease(tmpCtx); return nil; } + + // Flip Y coordinate for the source rect (CGImage is top-down) + CGFloat ctxHeight = [_opalSurface size].height; + CGRect srcRect = CGRectMake(x, ctxHeight - y - h, w, h); + CGImageRef subImg = CGImageCreateWithImageInRect(img, srcRect); + CGImageRelease(img); + if (!subImg) { CGContextRelease(tmpCtx); return nil; } + + // Draw into temp context + CGContextDrawImage(tmpCtx, CGRectMake(0, 0, w, h), subImg); + CGImageRelease(subImg); + + // Extract pixel data + unsigned char *srcData = (unsigned char *)CGBitmapContextGetData(tmpCtx); + if (!srcData) { CGContextRelease(tmpCtx); return nil; } + + // Convert from BGRA premultiplied to RGBA non-premultiplied + NSMutableData *data = [NSMutableData dataWithLength: w * h * 4]; + unsigned char *dst = [data mutableBytes]; + for (int row = 0; row < h; row++) + { + unsigned char *s = srcData + row * w * 4; + unsigned char *d = dst + row * w * 4; + for (int col = 0; col < w; col++) + { + // BGRA premul -> RGBA + unsigned char b = s[col*4+0]; + unsigned char g = s[col*4+1]; + unsigned char r = s[col*4+2]; + unsigned char a = s[col*4+3]; + d[col*4+0] = r; + d[col*4+1] = g; + d[col*4+2] = b; + d[col*4+3] = a; + } + } + + CGContextRelease(tmpCtx); + + NSAffineTransform *matrix = [NSAffineTransform transform]; + NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: + data, @"Data", + [NSNumber numberWithInt: 8], @"BitsPerSample", + [NSNumber numberWithInt: 4], @"SamplesPerPixel", + [NSNumber numberWithBool: YES], @"HasAlpha", + [NSValue valueWithSize: NSMakeSize(w, h)], @"Size", + NSCalibratedRGBColorSpace, @"ColorSpace", + matrix, @"Matrix", + [NSNumber numberWithInt: 0], @"BitmapFormat", + nil]; + + return dict; } - (void) DPSimage: (NSAffineTransform *)matrix @@ -836,15 +935,19 @@ - (void) compositeGState: (OpalGState *)source NSDebugLLog(@"OpalGState", @"Source cgctx: %p, self: %p - from %@ to %@ with ctm %@", [source CGContext], self, _CGRectRepr(srcCGRect), _CGRectRepr(destCGRect), [self GSCurrentCTM]); // FIXME: this presumes that the backing CGContext of 'source' is // an OpalSurface with a backing CGBitmapContext - CGImageRef backingImage = CGBitmapContextCreateImage([source CGContext]); + CGContextRef srcCtx = [source CGContext]; + if (!srcCtx) return; + CGImageRef backingImage = CGBitmapContextCreateImage(srcCtx); + if (!backingImage) return; CGImageRef subImage = CGImageCreateWithImageInRect(backingImage, srcCGRect); CGContextSaveGState(destCGContext); OPContextSetIdentityCTM(destCGContext); OPContextSetCairoDeviceOffset(destCGContext, 0, 0); - // TODO: this ignores op - // TODO: this ignores delta + // Apply compositing operation and alpha + CGContextSetBlendMode(destCGContext, _opalBlendModeForOp(op)); + CGContextSetAlpha(destCGContext, delta); CGContextDrawImage(destCGContext, destCGRect, subImage); OPContextSetCairoDeviceOffset(CGCTX, -offset.x, @@ -907,11 +1010,17 @@ - (void) drawGState: (OpalGState *)source srcRect.size.width, srcRect.size.height); CGRect destCGRect = CGRectMake(destPoint.x, destPoint.y, srcRect.size.width, srcRect.size.height); - CGImageRef backingImage = CGBitmapContextCreateImage([source CGContext]); + CGContextRef srcCtx = [source CGContext]; + if (!srcCtx) return; + CGImageRef backingImage = CGBitmapContextCreateImage(srcCtx); + if (!backingImage) return; CGImageRef subImage = CGImageCreateWithImageInRect(backingImage, srcCGRect); - // TODO: this ignores op - // TODO: this ignores delta + // Apply compositing operation and alpha + CGContextSaveGState(destCGContext); + CGContextSetBlendMode(destCGContext, _opalBlendModeForOp(op)); + CGContextSetAlpha(destCGContext, delta); CGContextDrawImage(destCGContext, destCGRect, subImage); + CGContextRestoreGState(destCGContext); CGImageRelease(subImage); CGImageRelease(backingImage); } @@ -949,7 +1058,7 @@ - (void) compositerect: (NSRect)aRect { CGContextSaveGState(cgctx); OPContextSetIdentityCTM(cgctx); - // FIXME: Set operator + CGContextSetBlendMode(cgctx, _opalBlendModeForOp(op)); CGContextFillRect(cgctx, CGRectMake(aRect.origin.x, [_opalSurface size].height - aRect.origin.y, @@ -963,6 +1072,7 @@ - (void) compositerect: (NSRect)aRect // MARK: Initialization methods // MARK: - + @implementation OpalGState (InitializationMethods) - (void) DPSinitgraphics @@ -1024,6 +1134,7 @@ - (void) GSCurrentSurface: (OpalSurface **)surface // MARK: Accessors // MARK: - + @implementation OpalGState (Accessors) - (CGContextRef) CGContext @@ -1060,6 +1171,7 @@ - (void) setOPGState: (OPGStateRef)opGState // MARK: Non-required methods // MARK: - + @implementation OpalGState (NonrequiredMethods) - (void) DPSgsave @@ -1082,6 +1194,7 @@ - (void) DPSgrestore @end + @implementation OpalGState (PatternColor) - (void *) saveClip @@ -1105,24 +1218,64 @@ - (void) restoreClip: (void *)savedClip } -// Gradient rendering - stub implementations to prevent crashes +// Gradient rendering using CoreGraphics CGGradient API - (void) drawGradient: (NSGradient*)gradient fromPoint: (NSPoint)startPoint toPoint: (NSPoint)endPoint options: (NSUInteger)options { - // TODO: Implement using CGGradientCreateWithColorComponents + CGContextDrawLinearGradient - NSLog(@"OpalGState: drawGradient (linear) - stub, drawing fallback fill"); + CGContextRef ctx = CGCTX; + if (!ctx || !gradient) return; + + NSInteger stops = [gradient numberOfColorStops]; + if (stops == 0) return; + + CGFloat *components = malloc(sizeof(CGFloat) * stops * 4); + CGFloat *locations = malloc(sizeof(CGFloat) * stops); + if (!components || !locations) { free(components); free(locations); return; } - // Fallback: fill with the first color of the gradient - CGContextRef ctx = [self CGContext]; - if (ctx && gradient) + for (int i = 0; i < stops; i++) { - NSColor *color = [gradient interpolatedColorAtLocation: 0.0]; - CGFloat r, g, b, a; - [[color colorUsingColorSpaceName: NSCalibratedRGBColorSpace] - getRed: &r green: &g blue: &b alpha: &a]; - CGContextSetRGBFillColor(ctx, r, g, b, a); + NSColor *color; + CGFloat location; + [gradient getColor: &color location: &location atIndex: i]; + NSColor *rgb = [color colorUsingColorSpaceName: NSCalibratedRGBColorSpace]; + if (rgb) + { + components[i*4+0] = [rgb redComponent]; + components[i*4+1] = [rgb greenComponent]; + components[i*4+2] = [rgb blueComponent]; + components[i*4+3] = [rgb alphaComponent]; + } + else + { + // Fallback for non-RGB colors + CGFloat w = [color whiteComponent]; + components[i*4+0] = w; + components[i*4+1] = w; + components[i*4+2] = w; + components[i*4+3] = [color alphaComponent]; + } + locations[i] = location; + } + + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGGradientRef grad = CGGradientCreateWithColorComponents(cs, components, locations, stops); + CGColorSpaceRelease(cs); + free(components); + free(locations); + + if (grad) + { + // CGContext already applies CTM to gradient coordinates, + // so pass them in user space without manual transform. + CGContextSaveGState(ctx); + CGContextDrawLinearGradient(ctx, grad, + CGPointMake(startPoint.x, startPoint.y), + CGPointMake(endPoint.x, endPoint.y), + (CGGradientDrawingOptions)options); + CGContextRestoreGState(ctx); + CGGradientRelease(grad); } } @@ -1133,8 +1286,57 @@ - (void) drawGradient: (NSGradient*)gradient radius: (CGFloat)endRadius options: (NSUInteger)options { - // TODO: Implement using CGGradientCreateWithColorComponents + CGContextDrawRadialGradient - NSLog(@"OpalGState: drawGradient (radial) - stub"); + CGContextRef ctx = CGCTX; + if (!ctx || !gradient) return; + + NSInteger stops = [gradient numberOfColorStops]; + if (stops == 0) return; + + CGFloat *components = malloc(sizeof(CGFloat) * stops * 4); + CGFloat *locations = malloc(sizeof(CGFloat) * stops); + if (!components || !locations) { free(components); free(locations); return; } + + for (int i = 0; i < stops; i++) + { + NSColor *color; + CGFloat location; + [gradient getColor: &color location: &location atIndex: i]; + NSColor *rgb = [color colorUsingColorSpaceName: NSCalibratedRGBColorSpace]; + if (rgb) + { + components[i*4+0] = [rgb redComponent]; + components[i*4+1] = [rgb greenComponent]; + components[i*4+2] = [rgb blueComponent]; + components[i*4+3] = [rgb alphaComponent]; + } + else + { + CGFloat w = [color whiteComponent]; + components[i*4+0] = w; + components[i*4+1] = w; + components[i*4+2] = w; + components[i*4+3] = [color alphaComponent]; + } + locations[i] = location; + } + + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGGradientRef grad = CGGradientCreateWithColorComponents(cs, components, locations, stops); + CGColorSpaceRelease(cs); + free(components); + free(locations); + + if (grad) + { + // CGContext already applies CTM to gradient coordinates + CGContextSaveGState(ctx); + CGContextDrawRadialGradient(ctx, grad, + CGPointMake(startCenter.x, startCenter.y), startRadius, + CGPointMake(endCenter.x, endCenter.y), endRadius, + (CGGradientDrawingOptions)options); + CGContextRestoreGState(ctx); + CGGradientRelease(grad); + } } @end diff --git a/Source/opal/OpalSurface.m b/Source/opal/OpalSurface.m index f9521b57..e5701f5d 100644 --- a/Source/opal/OpalSurface.m +++ b/Source/opal/OpalSurface.m @@ -166,7 +166,7 @@ - (void) handleExposeRect: (NSRect)rect NSLog(@"OpalSurface handleExposeRect: %@ backing=%p x11=%p", NSStringFromRect(rect), _backingCGContext, _x11CGContext); NSDebugLLog(@"OpalSurface", @"handleExposeRect %@", NSStringFromRect(rect)); - if (!_backingCGContext) + if (!_backingCGContext || !_x11CGContext) { return; } From c7ee0bb721cf83dad4a8457ba0c748926859270a Mon Sep 17 00:00:00 2001 From: pkgdemon Date: Sun, 5 Apr 2026 22:52:14 -0500 Subject: [PATCH 3/8] Opal backend: font hinting, gradients, blend modes, gray-to-RGB colors, lazy surface creation --- Source/opal/OpalFontInfo.m | 44 ++++++++++++++++++++++++++++--------- Source/opal/OpalGState.m | 9 ++++++-- Source/x11/XGServerWindow.m | 9 ++++++++ 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/Source/opal/OpalFontInfo.m b/Source/opal/OpalFontInfo.m index 9d82e8c0..4971c765 100644 --- a/Source/opal/OpalFontInfo.m +++ b/Source/opal/OpalFontInfo.m @@ -118,16 +118,40 @@ - (BOOL) setupAttributes return NO; } - // We must not leave the hinting settings as their defaults, - // because if we did, that would mean using the surface defaults - // which might or might not use hinting (xlib does by default.) - // - // Since we make measurements outside of the context of a surface - // (-advancementForGlyph:), we need to ensure that the same - // hinting settings are used there as when we draw. For now, - // just force hinting to be off. - cairo_font_options_set_hint_metrics(options, CAIRO_HINT_METRICS_ON); - cairo_font_options_set_hint_style(options, CAIRO_HINT_STYLE_NONE); + { + NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; + cairo_hint_metrics_t metrics = CAIRO_HINT_METRICS_ON; + cairo_hint_style_t style = CAIRO_HINT_STYLE_NONE; + int hinting = [ud integerForKey: @"GSFontHinting"]; + cairo_antialias_t antialias = CAIRO_ANTIALIAS_DEFAULT; + if (hinting == 0) + { + float scaleFactor = [ud floatForKey: @"GSScaleFactor"]; + if (scaleFactor != 0.0 && scaleFactor != 1.0) + hinting = 33; + else + hinting = 17; + } + switch (hinting >> 4) + { + case 0: metrics = CAIRO_HINT_METRICS_DEFAULT; break; + case 1: metrics = CAIRO_HINT_METRICS_ON; break; + case 2: metrics = CAIRO_HINT_METRICS_OFF; break; + } + switch (hinting & 0x0f) + { + case 0: style = CAIRO_HINT_STYLE_DEFAULT; break; + case 1: style = CAIRO_HINT_STYLE_NONE; break; + case 2: style = CAIRO_HINT_STYLE_SLIGHT; break; + case 3: style = CAIRO_HINT_STYLE_MEDIUM; break; + case 4: style = CAIRO_HINT_STYLE_FULL; break; + } + cairo_font_options_set_hint_metrics(options, metrics); + cairo_font_options_set_hint_style(options, style); + if ([ud objectForKey: @"back_art_subpixel_text"]) + antialias = CAIRO_ANTIALIAS_SUBPIXEL; + cairo_font_options_set_antialias(options, antialias); + } _scaled = cairo_scaled_font_create(face, &font_matrix, &ctm, options); cairo_font_options_destroy(options); diff --git a/Source/opal/OpalGState.m b/Source/opal/OpalGState.m index bf35c1aa..7868db83 100644 --- a/Source/opal/OpalGState.m +++ b/Source/opal/OpalGState.m @@ -31,6 +31,8 @@ #import #import #import +#import +#import #import "opal/OpalGState.h" #import "opal/OpalSurface.h" #import "opal/OpalFontInfo.h" @@ -132,13 +134,16 @@ - (void) setColor: (device_color_t *)color state: (color_state_t)cState if (color->space == gray_colorspace) { + // Use RGB path for gray colors to ensure consistent text rendering + CGFloat gray = color->field[0]; + CGFloat alpha = color->field[AINDEX]; if (cState & COLOR_STROKE) { - CGContextSetGrayStrokeColor(cgctx, color->field[0], color->field[AINDEX]); + CGContextSetRGBStrokeColor(cgctx, gray, gray, gray, alpha); } if (cState & COLOR_FILL) { - CGContextSetGrayFillColor(cgctx, color->field[0], color->field[AINDEX]); + CGContextSetRGBFillColor(cgctx, gray, gray, gray, alpha); } } else if (color->space == rgb_colorspace) diff --git a/Source/x11/XGServerWindow.m b/Source/x11/XGServerWindow.m index b4e049d9..22480017 100644 --- a/Source/x11/XGServerWindow.m +++ b/Source/x11/XGServerWindow.m @@ -3709,6 +3709,15 @@ - (void) _addExposedRectangle: (XRectangle)rectangle : (int)win : (BOOL) ignoreB } } + if (window->gdriver == NULL && window->ident != 0) + { + NSGraphicsContext *ctxt = GSCurrentContext(); + if (ctxt != nil) + { + [self setWindowdevice: win forContext: ctxt]; + } + } + if (!ignoreBacking && window->type != NSBackingStoreNonretained) { XGCValues values; From faa15ae0d107f099d4603b440ee9149a8772841a Mon Sep 17 00:00:00 2001 From: pkgdemon Date: Sun, 5 Apr 2026 23:35:41 -0500 Subject: [PATCH 4/8] Fix window movement trails: flush backing and X11 contexts in OpalSurface handleExposeRect --- Source/opal/OpalSurface.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/opal/OpalSurface.m b/Source/opal/OpalSurface.m index e5701f5d..b421401d 100644 --- a/Source/opal/OpalSurface.m +++ b/Source/opal/OpalSurface.m @@ -171,6 +171,8 @@ - (void) handleExposeRect: (NSRect)rect return; } + // Flush backing context to ensure all drawing is committed + CGContextFlush(_backingCGContext); CGImageRef backingImage = CGBitmapContextCreateImage(_backingCGContext); if (!backingImage) // FIXME: writing a nil image fails with Opal return; @@ -192,6 +194,7 @@ - (void) handleExposeRect: (NSRect)rect CGContextDrawImage(_x11CGContext, cgRect, subImage); + CGContextFlush(_x11CGContext); #if 0 #warning Saving debug images From e2fa0f76da2014ae4c0f8eb45b67b9b48e658140 Mon Sep 17 00:00:00 2001 From: pkgdemon Date: Wed, 22 Apr 2026 21:41:38 -0500 Subject: [PATCH 5/8] Opal: make X11 surface attachment self-contained --- Headers/opal/OpalSurface.h | 1 + Source/opal/OpalSurface.m | 72 ++++++++++++++++++++++++++++++++++--- Source/x11/XGServerEvent.m | 27 -------------- Source/x11/XGServerWindow.m | 61 ++++++------------------------- 4 files changed, 78 insertions(+), 83 deletions(-) diff --git a/Headers/opal/OpalSurface.h b/Headers/opal/OpalSurface.h index 4e574255..b6c129b9 100644 --- a/Headers/opal/OpalSurface.h +++ b/Headers/opal/OpalSurface.h @@ -42,6 +42,7 @@ - (CGContextRef) backingCGContext; - (CGContextRef) x11CGContext; +- (void) ensureX11Context; - (void) handleExposeRect: (NSRect)rect; - (BOOL) isDrawingToScreen; @end diff --git a/Source/opal/OpalSurface.m b/Source/opal/OpalSurface.m index b421401d..9d460b6c 100644 --- a/Source/opal/OpalSurface.m +++ b/Source/opal/OpalSurface.m @@ -80,20 +80,38 @@ - (void) createCGContextsWithSuppliedBackingContext: (CGContextRef)ctx if (_x11CGContext || _backingCGContext) { NSLog(@"FIXME: Replacement of OpalSurface %p's CGContexts (x11=%p,backing=%p) without transfer of gstate", self, _x11CGContext, _backingCGContext); + // Resize path: drop the old X11 CGContext so the next expose + // recreates one at the new window size. The backing bitmap + // below is reallocated at the new size, too. + if (_x11CGContext) + { + CGContextRelease(_x11CGContext); + _x11CGContext = NULL; + } + if (_backingCGContext) + { + CGContextRelease(_backingCGContext); + _backingCGContext = NULL; + } } if (ctx) { + // Client supplied a ready-made CGContext. Treat it as the x11 + // destination (pre-existing behaviour) and derive the size from it. _x11CGContext = ctx; pixelsWide = CGBitmapContextGetWidth(ctx); pixelsHigh = CGBitmapContextGetHeight(ctx); } else { - Display * display = _gsWindowDevice->display; - Window window = _gsWindowDevice->ident; - - _x11CGContext = OPX11ContextCreate(display, window); + // Lazy path: defer creation of the X11 CGContext until the X window + // is actually mapped. -[NSWindow _startBackendWindow] invokes + // GSSetDevice (which ends up here) before the window is mapped, so + // OPX11ContextCreate() either fails or returns a context that does + // not draw. We instead create the X11 context on first use, which + // is triggered by the initial Expose event (or by any GState path + // that touches _x11CGContext). pixelsWide = _gsWindowDevice->buffer_width; pixelsHigh = _gsWindowDevice->buffer_height; @@ -119,11 +137,47 @@ - (void) createCGContextsWithSuppliedBackingContext: (CGContextRef)ctx _backingCGContext = createCGBitmapContext(pixelsWide, pixelsHigh); } - NSLog(@"OpalSurface Created CGContexts: X11=%p, backing=%p, width=%d height=%d", + NSLog(@"OpalSurface Created CGContexts: X11=%p (deferred unless non-nil), backing=%p, width=%d height=%d", _x11CGContext, _backingCGContext, pixelsWide, pixelsHigh); } +/** + * Lazily create the X11-backed CGContext. Called from every code path + * that actually draws to or otherwise touches _x11CGContext. Safe to + * call repeatedly; only has side effects the first time (per surface + * instance) or after an explicit invalidation. + * + * We do nothing unless the associated gswindow_device_t has a valid + * X Window id (ident != 0), because OPX11ContextCreate on an unmapped + * or zero drawable returns a context that cannot draw. + */ +- (void) ensureX11Context +{ + if (_x11CGContext != NULL) + return; + if (_gsWindowDevice == NULL) + return; + if (_gsWindowDevice->ident == 0) + return; + + Display *display = _gsWindowDevice->display; + Window window = _gsWindowDevice->ident; + + _x11CGContext = OPX11ContextCreate(display, window); + if (_x11CGContext == NULL) + { + NSDebugLLog(@"OpalSurface", + @"OpalSurface %p: OPX11ContextCreate(display=%p, window=%lu) returned NULL; will retry on next use", + self, display, (unsigned long)window); + return; + } + + NSDebugLLog(@"OpalSurface", + @"OpalSurface %p: lazily created X11 CGContext=%p for window=%lu", + self, _x11CGContext, (unsigned long)window); +} + // FIXME: *VERY* bad things will happen if a non-bitmap // context is passed here. - (id) initWithDevice: (void *)device context: (CGContextRef)ctx @@ -158,11 +212,19 @@ - (CGContextRef) backingCGContext - (CGContextRef) x11CGContext { + // All external readers of the X11 context must see a valid context + // if one can be created right now, so route through the lazy path. + [self ensureX11Context]; return _x11CGContext; } - (void) handleExposeRect: (NSRect)rect { + // Expose events only fire on mapped windows, so this is the first + // safe moment at which we know the X Window is real. Create the + // X11 CGContext on demand if we haven't already. + [self ensureX11Context]; + NSLog(@"OpalSurface handleExposeRect: %@ backing=%p x11=%p", NSStringFromRect(rect), _backingCGContext, _x11CGContext); NSDebugLLog(@"OpalSurface", @"handleExposeRect %@", NSStringFromRect(rect)); diff --git a/Source/x11/XGServerEvent.m b/Source/x11/XGServerEvent.m index 80bf975d..fb8f9761 100644 --- a/Source/x11/XGServerEvent.m +++ b/Source/x11/XGServerEvent.m @@ -940,22 +940,6 @@ - (void) processEvent: (XEvent *) event if (cWin != 0) { - /* If no graphics driver attached yet, create the surface now. - * This is critical for the opal backend where the surface - * only gets created via GSSetDevice, which requires - * setWindowdevice to be called. Without this, opal windows - * never render because _processResizeEvent never fires. - */ - if (cWin->gdriver == NULL && cWin->ident != 0) - { - NSGraphicsContext *ctxt = GSCurrentContext(); - if (ctxt != nil) - { - NSLog(@"ConfigureNotify: creating surface for window %lu", cWin->number); - [self setWindowdevice: cWin->number forContext: ctxt]; - } - } - NSRect r, x, n, h; NSTimeInterval ts = (NSTimeInterval)generic.lastMotion; @@ -1470,17 +1454,6 @@ - (void) processEvent: (XEvent *) event if (cWin != 0) { cWin->map_state = IsViewable; - - /* Create surface on first map if not yet created */ - if (cWin->gdriver == NULL && cWin->ident != 0) - { - NSGraphicsContext *ctxt = GSCurrentContext(); - if (ctxt != nil) - { - NSLog(@"MapNotify: creating surface for window %lu", cWin->number); - [self setWindowdevice: cWin->number forContext: ctxt]; - } - } /* * if the window that was just mapped wants the input * focus, re-do the request. diff --git a/Source/x11/XGServerWindow.m b/Source/x11/XGServerWindow.m index 22480017..98392693 100644 --- a/Source/x11/XGServerWindow.m +++ b/Source/x11/XGServerWindow.m @@ -951,13 +951,6 @@ - (BOOL) _checkStyle: (unsigned)style // _NET_REQUEST_FRAME_EXTENTS [self orderwindow: NSWindowAbove : 0 : window->number]; - /* Ensure the window has visible content by clearing it. - * This is needed for backends (like opal) that don't attach - * an X11 surface until GSSetDevice is called. Without content, - * the X server may not generate VisibilityNotify. - */ - XClearWindow(dpy, window->ident); - XFlush(dpy); XSync(dpy, False); while (XPending(dpy) > 0 || window->visibility > 1) { @@ -965,11 +958,15 @@ - (BOOL) _checkStyle: (unsigned)style { NSDate *until; - /* Wait briefly for visibility notification. - * Reduced from 1.0s to 0.1s - if the window isn't visible - * by now, it won't become visible (e.g. no window manager). + /* In theory, after executing XSync() all events resulting from + * our window creation and ordering front should be available in + * the X event queue. + * However, it's possible that a window manager + * could send some events after the XSync() has been satisfied, + * so if we have not received a visibility notification + * we can wait for up to a second for more events. */ - until = [NSDate dateWithTimeIntervalSinceNow: 0.1]; + until = [NSDate dateWithTimeIntervalSinceNow: 1.0]; while (XPending(dpy) == 0 && [until timeIntervalSinceNow] > 0.0) { CREATE_AUTORELEASE_POOL(pool); @@ -981,8 +978,8 @@ - (BOOL) _checkStyle: (unsigned)style } if (XPending(dpy) == 0) { - NSDebugLLog(@"Offset", @"No visibility notification" - @" for probe window - using default offsets"); + NSLog(@"Waited for a second, but the X system never" + @" made the window visible"); break; } } @@ -2864,7 +2861,6 @@ - (int) _createAppIconPixmaps - (void) orderwindow: (int)op : (int)otherWin : (int)winNum { - NSLog(@"orderwindow: op=%d otherWin=%d winNum=%d", op, otherWin, winNum); gswindow_device_t *window; gswindow_device_t *other; int level; @@ -3092,21 +3088,6 @@ - (void) orderwindow: (int)op : (int)otherWin : (int)winNum CWStackMode, &chg); } XMapWindow(dpy, window->ident); - /* Ensure the window has a graphics context/surface attached. - * This is critical for backends like opal that don't create - * their surface until GSSetDevice is called. Without this, - * the window maps but has no drawing surface, so expose events - * produce no visible content. - */ - NSLog(@"orderwindow: gdriver=%p ctxt=%p winNum=%d", window->gdriver, GSCurrentContext(), window->number); - if (window->gdriver == NULL) - { - NSGraphicsContext *ctxt = GSCurrentContext(); - if (ctxt != nil) - { - [self setWindowdevice: window->number forContext: ctxt]; - } - } break; case NSWindowOut: @@ -3696,28 +3677,6 @@ - (void) _addExposedRectangle: (XRectangle)rectangle : (int)win : (BOOL) ignoreB if (!window) return; - /* If no graphics driver is attached to this window yet, create one. - * This is needed for the opal backend which only creates its surface - * in GSSetDevice, which may not have been called yet. - */ - if (window->gdriver == NULL && window->ident != 0) - { - NSGraphicsContext *ctxt = GSCurrentContext(); - if (ctxt != nil) - { - [self setWindowdevice: win forContext: ctxt]; - } - } - - if (window->gdriver == NULL && window->ident != 0) - { - NSGraphicsContext *ctxt = GSCurrentContext(); - if (ctxt != nil) - { - [self setWindowdevice: win forContext: ctxt]; - } - } - if (!ignoreBacking && window->type != NSBackingStoreNonretained) { XGCValues values; From b535c1da785c936b1e525ecfb9c7f4ed685743c0 Mon Sep 17 00:00:00 2001 From: pkgdemon Date: Thu, 30 Apr 2026 13:19:56 -0500 Subject: [PATCH 6/8] Remove newline --- Source/opal/OpalContext.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/opal/OpalContext.m b/Source/opal/OpalContext.m index dd631a8f..02bc52b3 100644 --- a/Source/opal/OpalContext.m +++ b/Source/opal/OpalContext.m @@ -166,7 +166,6 @@ - (void) GSCurrentDevice: (void **)device : (int *)x : (int *)y } - (void) GSSetDevice: (void *)device - : (int)x : (int)y { From 45a979472c7f44fa960eb7900166f69765ea1891 Mon Sep 17 00:00:00 2001 From: pkgdemon Date: Thu, 30 Apr 2026 14:50:23 -0500 Subject: [PATCH 7/8] convert directly to RGB --- Source/opal/OpalGState.m | 43 +++++++++++----------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/Source/opal/OpalGState.m b/Source/opal/OpalGState.m index 7868db83..9af279ea 100644 --- a/Source/opal/OpalGState.m +++ b/Source/opal/OpalGState.m @@ -1245,22 +1245,12 @@ - (void) drawGradient: (NSGradient*)gradient CGFloat location; [gradient getColor: &color location: &location atIndex: i]; NSColor *rgb = [color colorUsingColorSpaceName: NSCalibratedRGBColorSpace]; - if (rgb) - { - components[i*4+0] = [rgb redComponent]; - components[i*4+1] = [rgb greenComponent]; - components[i*4+2] = [rgb blueComponent]; - components[i*4+3] = [rgb alphaComponent]; - } - else - { - // Fallback for non-RGB colors - CGFloat w = [color whiteComponent]; - components[i*4+0] = w; - components[i*4+1] = w; - components[i*4+2] = w; - components[i*4+3] = [color alphaComponent]; - } + if (rgb == nil) + rgb = [NSColor blackColor]; + components[i*4+0] = [rgb redComponent]; + components[i*4+1] = [rgb greenComponent]; + components[i*4+2] = [rgb blueComponent]; + components[i*4+3] = [rgb alphaComponent]; locations[i] = location; } @@ -1307,21 +1297,12 @@ - (void) drawGradient: (NSGradient*)gradient CGFloat location; [gradient getColor: &color location: &location atIndex: i]; NSColor *rgb = [color colorUsingColorSpaceName: NSCalibratedRGBColorSpace]; - if (rgb) - { - components[i*4+0] = [rgb redComponent]; - components[i*4+1] = [rgb greenComponent]; - components[i*4+2] = [rgb blueComponent]; - components[i*4+3] = [rgb alphaComponent]; - } - else - { - CGFloat w = [color whiteComponent]; - components[i*4+0] = w; - components[i*4+1] = w; - components[i*4+2] = w; - components[i*4+3] = [color alphaComponent]; - } + if (rgb == nil) + rgb = [NSColor blackColor]; + components[i*4+0] = [rgb redComponent]; + components[i*4+1] = [rgb greenComponent]; + components[i*4+2] = [rgb blueComponent]; + components[i*4+3] = [rgb alphaComponent]; locations[i] = location; } From 8ea24deba851dfd9d4fd5d1d95e1676da8e1dab1 Mon Sep 17 00:00:00 2001 From: Joe Maloney Date: Thu, 30 Apr 2026 15:34:50 -0500 Subject: [PATCH 8/8] Opal gradient helper (#2) * Use shared gradient helper * Restore comments --- Source/opal/OpalGState.m | 53 ++++++++++++---------------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/Source/opal/OpalGState.m b/Source/opal/OpalGState.m index 9af279ea..7842199a 100644 --- a/Source/opal/OpalGState.m +++ b/Source/opal/OpalGState.m @@ -1224,20 +1224,14 @@ - (void) restoreClip: (void *)savedClip // Gradient rendering using CoreGraphics CGGradient API -- (void) drawGradient: (NSGradient*)gradient - fromPoint: (NSPoint)startPoint - toPoint: (NSPoint)endPoint - options: (NSUInteger)options +static CGGradientRef OpalCreateGradientFromNSGradient(NSGradient *gradient) { - CGContextRef ctx = CGCTX; - if (!ctx || !gradient) return; - NSInteger stops = [gradient numberOfColorStops]; - if (stops == 0) return; + if (stops == 0) return NULL; CGFloat *components = malloc(sizeof(CGFloat) * stops * 4); CGFloat *locations = malloc(sizeof(CGFloat) * stops); - if (!components || !locations) { free(components); free(locations); return; } + if (!components || !locations) { free(components); free(locations); return NULL; } for (int i = 0; i < stops; i++) { @@ -1260,6 +1254,18 @@ - (void) drawGradient: (NSGradient*)gradient free(components); free(locations); + return grad; +} + +- (void) drawGradient: (NSGradient*)gradient + fromPoint: (NSPoint)startPoint + toPoint: (NSPoint)endPoint + options: (NSUInteger)options +{ + CGContextRef ctx = CGCTX; + if (!ctx || !gradient) return; + + CGGradientRef grad = OpalCreateGradientFromNSGradient(gradient); if (grad) { // CGContext already applies CTM to gradient coordinates, @@ -1284,34 +1290,7 @@ - (void) drawGradient: (NSGradient*)gradient CGContextRef ctx = CGCTX; if (!ctx || !gradient) return; - NSInteger stops = [gradient numberOfColorStops]; - if (stops == 0) return; - - CGFloat *components = malloc(sizeof(CGFloat) * stops * 4); - CGFloat *locations = malloc(sizeof(CGFloat) * stops); - if (!components || !locations) { free(components); free(locations); return; } - - for (int i = 0; i < stops; i++) - { - NSColor *color; - CGFloat location; - [gradient getColor: &color location: &location atIndex: i]; - NSColor *rgb = [color colorUsingColorSpaceName: NSCalibratedRGBColorSpace]; - if (rgb == nil) - rgb = [NSColor blackColor]; - components[i*4+0] = [rgb redComponent]; - components[i*4+1] = [rgb greenComponent]; - components[i*4+2] = [rgb blueComponent]; - components[i*4+3] = [rgb alphaComponent]; - locations[i] = location; - } - - CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); - CGGradientRef grad = CGGradientCreateWithColorComponents(cs, components, locations, stops); - CGColorSpaceRelease(cs); - free(components); - free(locations); - + CGGradientRef grad = OpalCreateGradientFromNSGradient(gradient); if (grad) { // CGContext already applies CTM to gradient coordinates