diff --git a/src/addon.mm b/src/addon.mm index 7c08432..3f36380 100644 --- a/src/addon.mm +++ b/src/addon.mm @@ -1581,6 +1581,35 @@ static bool ReadFromTo(Napi::Env env, CABasicAnimation* a, Napi::Object o) { // native controls: NSCell rendered offscreen (the WebKit/Firefox technique) // --------------------------------------------------------------------------- +// A view control drawn into the current NSGraphicsContext, filling (0, 0, w, +// h) points over pw x ph pixels. Through a bitmap rep rather than +// displayRectIgnoringOpacity:inContext:, which under the macOS 26 design (a +// process linked against the 26 SDK — Bun is) draws NSSlider and NSSwitch +// through their layers and leaves a foreign context empty. +// cacheDisplayInRect: renders the layer tree, and on the older design gives +// the pixels displayRect did. +static void DrawViewControl(NSControl* v, double w, double h, size_t pw, size_t ph) { + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:(NSInteger)pw + pixelsHigh:(NSInteger)ph + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:0 + bitsPerPixel:0]; + rep.size = NSMakeSize(w, h); + [v cacheDisplayInRect:v.bounds toBitmapImageRep:rep]; + [rep drawInRect:NSMakeRect(0, 0, w, h) + fromRect:NSZeroRect + operation:NSCompositingOperationSourceOver + fraction:1 + respectFlipped:YES + hints:nil]; +} + static NSView* DummyDrawView() { // Cells only use the view for flippedness/appearance queries; it never needs // to be in a window. @@ -1631,7 +1660,7 @@ static bool ReadFromTo(Napi::Env env, CABasicAnimation* a, Napi::Object o) { // Controls whose cells no longer draw offscreen (NSSliderCell renders via // the view's layer machinery) or that have no cell at all (NSSwitch): use a - // real offscreen NSControl and displayRectIgnoringOpacity:inContext:. + // real offscreen NSControl, captured with DrawViewControl. NSControl* viewControl = nil; if ([kind isEqualToString:@"slider"]) { NSSlider* s = [[NSSlider alloc] init]; @@ -1697,7 +1726,7 @@ static bool ReadFromTo(Napi::Env env, CABasicAnimation* a, Napi::Object o) { viewControl.frame = NSMakeRect(0, 0, w, h); viewControl.appearance = ap; [viewControl layoutSubtreeIfNeeded]; - [viewControl displayRectIgnoringOpacity:viewControl.bounds inContext:g]; + DrawViewControl(viewControl, w, h, pw, ph); } else { [ap performAsCurrentDrawingAppearance:^{ [cell drawWithFrame:NSMakeRect(0, 0, w, h) inView:DummyDrawView()]; diff --git a/src/backend.mm b/src/backend.mm index 10dcec2..f0b95e5 100644 --- a/src/backend.mm +++ b/src/backend.mm @@ -3911,10 +3911,39 @@ static void CancelPanelSheetsOn(NSWindow* win) { return v; } +// A view control drawn into the current NSGraphicsContext, filling (0, 0, w, +// h) points over pw x ph pixels. Through a bitmap rep rather than +// displayRectIgnoringOpacity:inContext:, which under the macOS 26 design (a +// process linked against the 26 SDK — Bun is) draws NSSlider and NSSwitch +// through their layers and leaves a foreign context empty. +// cacheDisplayInRect: renders the layer tree, and on the older design gives +// the pixels displayRect did. +static void BDrawViewControl(NSControl* v, double w, double h, size_t pw, size_t ph) { + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:(NSInteger)pw + pixelsHigh:(NSInteger)ph + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSDeviceRGBColorSpace + bytesPerRow:0 + bitsPerPixel:0]; + rep.size = NSMakeSize(w, h); + [v cacheDisplayInRect:v.bounds toBitmapImageRep:rep]; + [rep drawInRect:NSMakeRect(0, 0, w, h) + fromRect:NSZeroRect + operation:NSCompositingOperationSourceOver + fraction:1 + respectFlipped:YES + hints:nil]; +} + // The two render paths AppKit leaves us: classic cells draw offscreen via // drawWithFrame:, while NSSlider's cell now renders through the view's layer // machinery and NSSwitch has no cell at all — those go through a real -// offscreen NSControl and displayRectIgnoringOpacity:inContext:. +// offscreen NSControl, captured with BDrawViewControl. struct BezelControl { NSCell* cell = nil; NSControl* view = nil; @@ -4100,7 +4129,7 @@ static BezelControl BuildBezel(const BezelSpec& s) { c.view.frame = NSMakeRect(0, 0, w, h); c.view.appearance = ap; [c.view layoutSubtreeIfNeeded]; - [c.view displayRectIgnoringOpacity:c.view.bounds inContext:g]; + BDrawViewControl(c.view, w, h, pw, ph); } else if (c.cell) { NSCell* cell = c.cell; [ap performAsCurrentDrawingAppearance:^{ diff --git a/test/control-bezels.js b/test/control-bezels.js index a27ba2c..b8edb8a 100644 --- a/test/control-bezels.js +++ b/test/control-bezels.js @@ -4,7 +4,7 @@ // button stretched to its frame, where `push` keeps one height and draws it // centred in a taller frame. // -// Checked: both kinds measure and an unknown one still throws; the push's +// Checked: every kind draws ink; both kinds measure and an unknown one still throws; the push's // ink is the same 22pt (18 small) in every frame, while the flexible push // inks its frame to the same margins however tall it is; at the push's own // height the two draw the same pixels over the same ink box, plain, pressed @@ -79,6 +79,18 @@ for (const controlSize of ['regular', 'small']) { ok(threw && /unknown control kind/.test(threw.message), 'an unknown kind throws', threw); } +// --- every kind draws ------------------------------------------------------- +// NSSlider and NSSwitch are views, not cells, and under the macOS 26 design +// (a process linked against the 26 SDK — Bun, say) a view drawn with +// displayRectIgnoringOpacity:inContext: came out empty: no ink at all. +for (const kind of ['push', 'flexiblePush', 'checkbox', 'radio', 'popup', 'slider', 'switch']) { + for (const appearance of ['light', 'dark']) { + const m = n.measureControl({ kind }); + const d = draw({ kind, state: 1, value: 0.5, appearance }, m.width, m.height); + ok(d.ink.x1 >= 0, `${kind} draws (${appearance})`); + } +} + // --- one height, or stretched to the frame ------------------------------------ for (const controlSize of ['regular', 'small']) { const push = n.measureControl({ kind: 'push', controlSize });