Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/addon.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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()];
Expand Down
33 changes: 31 additions & 2 deletions src/backend.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:^{
Expand Down
14 changes: 13 additions & 1 deletion test/control-bezels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 });
Expand Down
Loading