eee528f12274eb06a6f06ca24d4f05277fa7e846
[WebKit-https.git] / Source / WebCore / platform / mac / ThemeMac.mm
1 /*
2  * Copyright (C) 2008-2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #import "config.h"
27 #import "ThemeMac.h"
28
29 #import "AXObjectCache.h"
30 #import "ControlStates.h"
31 #import "GraphicsContext.h"
32 #import "ImageBuffer.h"
33 #import "LengthSize.h"
34 #import "LocalCurrentGraphicsContext.h"
35 #import "ScrollView.h"
36 #import "WebCoreSystemInterface.h"
37 #import <Carbon/Carbon.h>
38 #import <pal/spi/cocoa/NSButtonCellSPI.h>
39 #import <pal/spi/mac/NSGraphicsSPI.h>
40 #import <wtf/BlockObjCExceptions.h>
41 #import <wtf/NeverDestroyed.h>
42 #import <wtf/StdLibExtras.h>
43
44 static NSRect focusRingClipRect;
45 static BOOL themeWindowHasKeyAppearance;
46
47 @interface WebCoreThemeWindow : NSWindow
48 @end
49
50 @interface WebCoreThemeView : NSControl
51 @end
52
53 @implementation WebCoreThemeWindow
54
55 - (BOOL)hasKeyAppearance
56 {
57     return themeWindowHasKeyAppearance;
58 }
59
60 - (BOOL)isKeyWindow
61 {
62     return themeWindowHasKeyAppearance;
63 }
64
65 @end
66
67 @implementation WebCoreThemeView
68
69 - (NSWindow *)window
70 {
71     // Using defer:YES prevents us from wasting any window server resources for this window, since we're not actually
72     // going to draw into it. The other arguments match what you get when calling -[NSWindow init].
73     static WebCoreThemeWindow *window = [[WebCoreThemeWindow alloc] initWithContentRect:NSMakeRect(100, 100, 100, 100)
74         styleMask:NSWindowStyleMaskTitled backing:NSBackingStoreBuffered defer:YES];
75     return window;
76 }
77
78 - (BOOL)isFlipped
79 {
80     return YES;
81 }
82
83 - (NSText *)currentEditor
84 {
85     return nil;
86 }
87
88 - (BOOL)_automaticFocusRingDisabled
89 {
90     return YES;
91 }
92
93 - (NSRect)_focusRingVisibleRect
94 {
95     if (NSIsEmptyRect(focusRingClipRect))
96         return [self visibleRect];
97     return focusRingClipRect;
98 }
99
100 - (NSView *)_focusRingClipAncestor
101 {
102     return self;
103 }
104
105 - (void)addSubview:(NSView *)subview
106 {
107     // By doing nothing in this method we forbid controls from adding subviews.
108     // This tells AppKit to not use layer-backed animation for control rendering.
109     UNUSED_PARAM(subview);
110 }
111
112 @end
113
114 // FIXME: Default buttons really should be more like push buttons and not like buttons.
115
116 namespace WebCore {
117
118 enum {
119     topMargin,
120     rightMargin,
121     bottomMargin,
122     leftMargin
123 };
124
125 Theme& Theme::singleton()
126 {
127     static NeverDestroyed<ThemeMac> themeMac;
128     return themeMac;
129 }
130
131 // Helper functions used by a bunch of different control parts.
132
133 static NSControlSize controlSizeForFont(const FontCascade& font)
134 {
135     int fontSize = font.pixelSize();
136     if (fontSize >= 16)
137         return NSControlSizeRegular;
138     if (fontSize >= 11)
139         return NSControlSizeSmall;
140     return NSControlSizeMini;
141 }
142
143 static LengthSize sizeFromNSControlSize(NSControlSize nsControlSize, const LengthSize& zoomedSize, float zoomFactor, const std::array<IntSize, 3>& sizes)
144 {
145     IntSize controlSize = sizes[nsControlSize];
146     if (zoomFactor != 1.0f)
147         controlSize = IntSize(controlSize.width() * zoomFactor, controlSize.height() * zoomFactor);
148     LengthSize result = zoomedSize;
149     if (zoomedSize.width.isIntrinsicOrAuto() && controlSize.width() > 0)
150         result.width = { controlSize.width(), Fixed };
151     if (zoomedSize.height.isIntrinsicOrAuto() && controlSize.height() > 0)
152         result.height = { controlSize.height(), Fixed };
153     return result;
154 }
155
156 static LengthSize sizeFromFont(const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor, const std::array<IntSize, 3>& sizes)
157 {
158     return sizeFromNSControlSize(controlSizeForFont(font), zoomedSize, zoomFactor, sizes);
159 }
160
161 static ControlSize controlSizeFromPixelSize(const std::array<IntSize, 3>& sizes, const IntSize& minZoomedSize, float zoomFactor)
162 {
163     if (minZoomedSize.width() >= static_cast<int>(sizes[NSControlSizeRegular].width() * zoomFactor)
164         && minZoomedSize.height() >= static_cast<int>(sizes[NSControlSizeRegular].height() * zoomFactor))
165         return NSControlSizeRegular;
166     if (minZoomedSize.width() >= static_cast<int>(sizes[NSControlSizeSmall].width() * zoomFactor)
167         && minZoomedSize.height() >= static_cast<int>(sizes[NSControlSizeSmall].height() * zoomFactor))
168         return NSControlSizeSmall;
169     return NSControlSizeMini;
170 }
171
172 static void setControlSize(NSCell* cell, const std::array<IntSize, 3>& sizes, const IntSize& minZoomedSize, float zoomFactor)
173 {
174     ControlSize size = controlSizeFromPixelSize(sizes, minZoomedSize, zoomFactor);
175     if (size != [cell controlSize]) // Only update if we have to, since AppKit does work even if the size is the same.
176         [cell setControlSize:(NSControlSize)size];
177 }
178
179 static void updateStates(NSCell* cell, const ControlStates& controlStates, bool useAnimation = false)
180 {
181     // The animated state cause this thread to start and stop repeatedly on CoreAnimation synchronize calls.
182     // This short burts of activity in between are not long enough for VoiceOver to retrieve accessibility attributes and makes the process appear unresponsive.
183     if (AXObjectCache::accessibilityEnhancedUserInterfaceEnabled())
184         useAnimation = false;
185     
186     ControlStates::States states = controlStates.states();
187
188     // Hover state is not supported by Aqua.
189     
190     // Pressed state
191     bool oldPressed = [cell isHighlighted];
192     bool pressed = states & ControlStates::PressedState;
193     if (pressed != oldPressed) {
194         [(NSButtonCell*)cell _setHighlighted:pressed animated:useAnimation];
195     }
196     
197     // Enabled state
198     bool oldEnabled = [cell isEnabled];
199     bool enabled = states & ControlStates::EnabledState;
200     if (enabled != oldEnabled)
201         [cell setEnabled:enabled];
202
203     // Checked and Indeterminate
204     bool oldIndeterminate = [cell state] == NSMixedState;
205     bool indeterminate = (states & ControlStates::IndeterminateState);
206     bool checked = states & ControlStates::CheckedState;
207     bool oldChecked = [cell state] == NSOnState;
208     if (oldIndeterminate != indeterminate || checked != oldChecked) {
209         NSCellStateValue newState = indeterminate ? NSMixedState : (checked ? NSOnState : NSOffState);
210         [(NSButtonCell*)cell _setState:newState animated:useAnimation];
211     }
212
213     // Window inactive state does not need to be checked explicitly, since we paint parented to 
214     // a view in a window whose key state can be detected.
215 }
216
217 static ThemeDrawState convertControlStatesToThemeDrawState(ThemeButtonKind kind, const ControlStates& controlStates)
218 {
219     ControlStates::States states = controlStates.states();
220
221     if (!(states & ControlStates::EnabledState))
222         return kThemeStateUnavailableInactive;
223
224     // Do not process PressedState if !EnabledState.
225     if (states & ControlStates::PressedState) {
226         if (kind == kThemeIncDecButton || kind == kThemeIncDecButtonSmall || kind == kThemeIncDecButtonMini)
227             return states & ControlStates::SpinUpState ? kThemeStatePressedUp : kThemeStatePressedDown;
228         return kThemeStatePressed;
229     }
230     return kThemeStateActive;
231 }
232
233 static FloatRect inflateRect(const FloatRect& zoomedRect, const IntSize& zoomedSize, const int* margins, float zoomFactor)
234 {
235     // Only do the inflation if the available width/height are too small.
236     // Otherwise try to fit the glow/check space into the available box's width/height.
237     int widthDelta = zoomedRect.width() - (zoomedSize.width() + margins[leftMargin] * zoomFactor + margins[rightMargin] * zoomFactor);
238     int heightDelta = zoomedRect.height() - (zoomedSize.height() + margins[topMargin] * zoomFactor + margins[bottomMargin] * zoomFactor);
239     FloatRect result(zoomedRect);
240     if (widthDelta < 0) {
241         result.setX(result.x() - margins[leftMargin] * zoomFactor);
242         result.setWidth(result.width() - widthDelta);
243     }
244     if (heightDelta < 0) {
245         result.setY(result.y() - margins[topMargin] * zoomFactor);
246         result.setHeight(result.height() - heightDelta);
247     }
248     return result;
249 }
250
251 // Checkboxes and radio buttons
252
253 static const std::array<IntSize, 3>& checkboxSizes()
254 {
255     static const std::array<IntSize, 3> sizes = { { IntSize(14, 14), IntSize(12, 12), IntSize(10, 10) } };
256     return sizes;
257 }
258
259 static const int* checkboxMargins(NSControlSize controlSize)
260 {
261     static const int margins[3][4] =
262     {
263         // top right bottom left
264         { 2, 2, 2, 2 },
265         { 2, 1, 2, 1 },
266         { 0, 0, 1, 0 },
267     };
268     return margins[controlSize];
269 }
270
271 static LengthSize checkboxSize(const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor)
272 {
273     // If the width and height are both specified, then we have nothing to do.
274     if (!zoomedSize.width.isIntrinsicOrAuto() && !zoomedSize.height.isIntrinsicOrAuto())
275         return zoomedSize;
276
277     // Use the font size to determine the intrinsic width of the control.
278     return sizeFromFont(font, zoomedSize, zoomFactor, checkboxSizes());
279 }
280
281 // Radio Buttons
282
283 static const std::array<IntSize, 3>& radioSizes()
284 {
285     static const std::array<IntSize, 3> sizes = { { IntSize(16, 16), IntSize(12, 12), IntSize(10, 10) } };
286     return sizes;
287 }
288
289 static const int* radioMargins(NSControlSize controlSize)
290 {
291     static const int margins[3][4] =
292     {
293         // top right bottom left
294         { 1, 0, 1, 2 },
295         { 1, 1, 2, 1 },
296         { 0, 0, 1, 1 },
297     };
298     return margins[controlSize];
299 }
300
301 static LengthSize radioSize(const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor)
302 {
303     // If the width and height are both specified, then we have nothing to do.
304     if (!zoomedSize.width.isIntrinsicOrAuto() && !zoomedSize.height.isIntrinsicOrAuto())
305         return zoomedSize;
306
307     // Use the font size to determine the intrinsic width of the control.
308     return sizeFromFont(font, zoomedSize, zoomFactor, radioSizes());
309 }
310     
311 static void configureToggleButton(NSCell* cell, ControlPart buttonType, const ControlStates& states, const IntSize& zoomedSize, float zoomFactor, bool isStateChange)
312 {
313     // Set the control size based off the rectangle we're painting into.
314     setControlSize(cell, buttonType == CheckboxPart ? checkboxSizes() : radioSizes(), zoomedSize, zoomFactor);
315
316     // Update the various states we respond to.
317     updateStates(cell, states, isStateChange);
318 }
319     
320 static RetainPtr<NSButtonCell> createToggleButtonCell(ControlPart buttonType)
321 {
322     RetainPtr<NSButtonCell> toggleButtonCell = adoptNS([[NSButtonCell alloc] init]);
323     
324     if (buttonType == CheckboxPart) {
325         [toggleButtonCell setButtonType:NSSwitchButton];
326         [toggleButtonCell setAllowsMixedState:YES];
327     } else {
328         ASSERT(buttonType == RadioPart);
329         [toggleButtonCell setButtonType:NSRadioButton];
330     }
331     
332     [toggleButtonCell setTitle:nil];
333     [toggleButtonCell setFocusRingType:NSFocusRingTypeExterior];
334     return toggleButtonCell;
335 }
336     
337 static NSButtonCell *sharedRadioCell(const ControlStates& states, const IntSize& zoomedSize, float zoomFactor)
338 {
339     static NSButtonCell *radioCell = createToggleButtonCell(RadioPart).leakRef();
340
341     configureToggleButton(radioCell, RadioPart, states, zoomedSize, zoomFactor, false);
342     return radioCell;
343 }
344     
345 static NSButtonCell *sharedCheckboxCell(const ControlStates& states, const IntSize& zoomedSize, float zoomFactor)
346 {
347     static NSButtonCell *checkboxCell = createToggleButtonCell(CheckboxPart).leakRef();
348
349     configureToggleButton(checkboxCell, CheckboxPart, states, zoomedSize, zoomFactor, false);
350     return checkboxCell;
351 }
352
353 static bool drawCellFocusRingWithFrameAtTime(NSCell *cell, NSRect cellFrame, NSView *controlView, NSTimeInterval timeOffset)
354 {
355     CGContextRef cgContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
356     CGContextSaveGState(cgContext);
357
358     CGFocusRingStyle focusRingStyle;
359     bool needsRepaint = NSInitializeCGFocusRingStyleForTime(NSFocusRingOnly, &focusRingStyle, timeOffset);
360     // We want to respect the CGContext clipping and also not overpaint any
361     // existing focus ring. The way to do this is set accumulate to
362     // -1. According to CoreGraphics, the reasoning for this behavior has been
363     // lost in time.
364     focusRingStyle.accumulate = -1;
365     auto style = adoptCF(CGStyleCreateFocusRingWithColor(&focusRingStyle, GraphicsContext::focusRingColor()));
366     CGContextSetStyle(cgContext, style.get());
367
368     CGContextBeginTransparencyLayerWithRect(cgContext, NSRectToCGRect(cellFrame), nullptr);
369     [cell drawFocusRingMaskWithFrame:cellFrame inView:controlView];
370     CGContextEndTransparencyLayer(cgContext);
371     CGContextRestoreGState(cgContext);
372
373     return needsRepaint;
374 }
375
376 static bool drawCellFocusRing(NSCell *cell, NSRect cellFrame, NSView *controlView)
377 {
378     drawCellFocusRingWithFrameAtTime(cell, cellFrame, controlView, std::numeric_limits<double>::max());
379     return false;
380 }
381
382 static void paintToggleButton(ControlPart buttonType, ControlStates& controlStates, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView* scrollView, float deviceScaleFactor, float pageScaleFactor)
383 {
384     BEGIN_BLOCK_OBJC_EXCEPTIONS
385
386     RetainPtr<NSButtonCell> toggleButtonCell = static_cast<NSButtonCell *>(controlStates.platformControl());
387     IntSize zoomedRectSize = IntSize(zoomedRect.size());
388
389     if (controlStates.isDirty()) {
390         if (!toggleButtonCell)
391             toggleButtonCell = createToggleButtonCell(buttonType);
392         configureToggleButton(toggleButtonCell.get(), buttonType, controlStates, zoomedRectSize, zoomFactor, true);
393     } else {
394         if (!toggleButtonCell) {
395             if (buttonType == CheckboxPart)
396                 toggleButtonCell = sharedCheckboxCell(controlStates, zoomedRectSize, zoomFactor);
397             else {
398                 ASSERT(buttonType == RadioPart);
399                 toggleButtonCell = sharedRadioCell(controlStates, zoomedRectSize, zoomFactor);
400             }
401         }
402         configureToggleButton(toggleButtonCell.get(), buttonType, controlStates, zoomedRectSize, zoomFactor, false);
403     }
404     controlStates.setDirty(false);
405
406     GraphicsContextStateSaver stateSaver(context);
407
408     NSControlSize controlSize = [toggleButtonCell controlSize];
409     IntSize zoomedSize = buttonType == CheckboxPart ? checkboxSizes()[controlSize] : radioSizes()[controlSize];
410     zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
411     zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
412     const int* controlMargins = buttonType == CheckboxPart ? checkboxMargins(controlSize) : radioMargins(controlSize);
413     FloatRect inflatedRect = inflateRect(zoomedRect, zoomedSize, controlMargins, zoomFactor);
414
415     if (zoomFactor != 1.0f) {
416         inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
417         inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
418         context.translate(inflatedRect.location());
419         context.scale(zoomFactor);
420         context.translate(-inflatedRect.location());
421     }
422
423     LocalCurrentGraphicsContext localContext(context);
424
425     NSView *view = ThemeMac::ensuredView(scrollView, controlStates, true /* useUnparentedView */);
426
427     bool needsRepaint = false;
428     bool useImageBuffer = pageScaleFactor != 1.0f || zoomFactor != 1.0f;
429     bool isCellFocused = controlStates.states() & ControlStates::FocusState;
430
431     if ([toggleButtonCell _stateAnimationRunning]) {
432         context.translate(inflatedRect.location());
433         context.scale(FloatSize(1, -1));
434         context.translate(0, -inflatedRect.height());
435
436         [toggleButtonCell _renderCurrentAnimationFrameInContext:context.platformContext() atLocation:NSMakePoint(0, 0)];
437         if (![toggleButtonCell _stateAnimationRunning] && isCellFocused)
438             needsRepaint = ThemeMac::drawCellOrFocusRingWithViewIntoContext(toggleButtonCell.get(), context, inflatedRect, view, false, true, useImageBuffer, deviceScaleFactor);
439     } else
440         needsRepaint = ThemeMac::drawCellOrFocusRingWithViewIntoContext(toggleButtonCell.get(), context, inflatedRect, view, true, isCellFocused, useImageBuffer, deviceScaleFactor);
441
442     [toggleButtonCell setControlView:nil];
443
444     needsRepaint |= [toggleButtonCell _stateAnimationRunning];
445     controlStates.setNeedsRepaint(needsRepaint);
446     if (needsRepaint)
447         controlStates.setPlatformControl(toggleButtonCell.get());
448
449     END_BLOCK_OBJC_EXCEPTIONS
450 }
451
452 // Buttons
453
454 // Buttons really only constrain height. They respect width.
455 static const std::array<IntSize, 3>& buttonSizes()
456 {
457     static const std::array<IntSize, 3> sizes = { { IntSize(0, 21), IntSize(0, 18), IntSize(0, 15) } };
458     return sizes;
459 }
460
461 static const int* buttonMargins(NSControlSize controlSize)
462 {
463     static const int margins[3][4] =
464     {
465         { 4, 6, 7, 6 },
466         { 4, 5, 6, 5 },
467         { 0, 1, 1, 1 },
468     };
469     return margins[controlSize];
470 }
471
472 enum ButtonCellType { NormalButtonCell, DefaultButtonCell };
473
474 static NSButtonCell *leakButtonCell(ButtonCellType type)
475 {
476     NSButtonCell *cell = [[NSButtonCell alloc] init];
477     [cell setTitle:nil];
478     [cell setButtonType:NSMomentaryPushInButton];
479     if (type == DefaultButtonCell)
480         [cell setKeyEquivalent:@"\r"];
481     return cell;
482 }
483
484 static void setUpButtonCell(NSButtonCell *cell, ControlPart part, const ControlStates& states, const IntSize& zoomedSize, float zoomFactor)
485 {
486     // Set the control size based off the rectangle we're painting into.
487     const std::array<IntSize, 3>& sizes = buttonSizes();
488     if (part == SquareButtonPart || zoomedSize.height() > buttonSizes()[NSControlSizeRegular].height() * zoomFactor) {
489         // Use the square button
490         if ([cell bezelStyle] != NSShadowlessSquareBezelStyle)
491             [cell setBezelStyle:NSShadowlessSquareBezelStyle];
492     } else if ([cell bezelStyle] != NSRoundedBezelStyle)
493         [cell setBezelStyle:NSRoundedBezelStyle];
494
495     setControlSize(cell, sizes, zoomedSize, zoomFactor);
496
497     // Update the various states we respond to.
498     updateStates(cell, states);
499 }
500
501 static NSButtonCell *button(ControlPart part, const ControlStates& controlStates, const IntSize& zoomedSize, float zoomFactor)
502 {
503     ControlStates::States states = controlStates.states();
504     NSButtonCell *cell;
505     if (states & ControlStates::DefaultState) {
506         static NSButtonCell *defaultCell = leakButtonCell(DefaultButtonCell);
507         cell = defaultCell;
508     } else {
509         static NSButtonCell *normalCell = leakButtonCell(NormalButtonCell);
510         cell = normalCell;
511     }
512     setUpButtonCell(cell, part, controlStates, zoomedSize, zoomFactor);
513     return cell;
514 }
515     
516 static void paintButton(ControlPart part, ControlStates& controlStates, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView* scrollView, float deviceScaleFactor, float pageScaleFactor)
517 {
518     BEGIN_BLOCK_OBJC_EXCEPTIONS
519     
520     // Determine the width and height needed for the control and prepare the cell for painting.
521     ControlStates::States states = controlStates.states();
522     NSButtonCell *buttonCell = button(part, controlStates, IntSize(zoomedRect.size()), zoomFactor);
523     GraphicsContextStateSaver stateSaver(context);
524
525     NSControlSize controlSize = [buttonCell controlSize];
526     IntSize zoomedSize = buttonSizes()[controlSize];
527     zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
528     zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
529     FloatRect inflatedRect = zoomedRect;
530     if ([buttonCell bezelStyle] == NSRoundedBezelStyle) {
531         // Center the button within the available space.
532         if (inflatedRect.height() > zoomedSize.height()) {
533             inflatedRect.setY(inflatedRect.y() + (inflatedRect.height() - zoomedSize.height()) / 2);
534             inflatedRect.setHeight(zoomedSize.height());
535         }
536
537         // Now inflate it to account for the shadow.
538         inflatedRect = inflateRect(inflatedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
539
540         if (zoomFactor != 1.0f) {
541             inflatedRect.setWidth(inflatedRect.width() / zoomFactor);
542             inflatedRect.setHeight(inflatedRect.height() / zoomFactor);
543             context.translate(inflatedRect.location());
544             context.scale(zoomFactor);
545             context.translate(-inflatedRect.location());
546         }
547     }
548     
549     LocalCurrentGraphicsContext localContext(context);
550     
551     NSView *view = ThemeMac::ensuredView(scrollView, controlStates);
552     NSWindow *window = [view window];
553     NSButtonCell *previousDefaultButtonCell = [window defaultButtonCell];
554
555     bool useImageBuffer = pageScaleFactor != 1.0f || zoomFactor != 1.0f;
556     bool needsRepaint = ThemeMac::drawCellOrFocusRingWithViewIntoContext(buttonCell, context, inflatedRect, view, true, states & ControlStates::FocusState, useImageBuffer, deviceScaleFactor);
557     if (states & ControlStates::DefaultState)
558         [window setDefaultButtonCell:buttonCell];
559     else if ([previousDefaultButtonCell isEqual:buttonCell])
560         [window setDefaultButtonCell:nil];
561     
562     controlStates.setNeedsRepaint(needsRepaint);
563
564     [buttonCell setControlView:nil];
565
566     if (![previousDefaultButtonCell isEqual:buttonCell])
567         [window setDefaultButtonCell:previousDefaultButtonCell];
568
569     END_BLOCK_OBJC_EXCEPTIONS
570 }
571
572 // Stepper
573
574 static const std::array<IntSize, 3>& stepperSizes()
575 {
576     static const std::array<IntSize, 3> sizes = { { IntSize(19, 27), IntSize(15, 22), IntSize(13, 15) } };
577     return sizes;
578 }
579
580 // We don't use controlSizeForFont() for steppers because the stepper height
581 // should be equal to or less than the corresponding text field height,
582 static NSControlSize stepperControlSizeForFont(const FontCascade& font)
583 {
584     int fontSize = font.pixelSize();
585     if (fontSize >= 18)
586         return NSControlSizeRegular;
587     if (fontSize >= 13)
588         return NSControlSizeSmall;
589     return NSControlSizeMini;
590 }
591
592 static void paintStepper(ControlStates& states, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView*)
593 {
594     // We don't use NSStepperCell because there are no ways to draw an
595     // NSStepperCell with the up button highlighted.
596
597     HIThemeButtonDrawInfo drawInfo;
598     drawInfo.version = 0;
599     drawInfo.state = convertControlStatesToThemeDrawState(kThemeIncDecButton, states);
600     drawInfo.adornment = kThemeAdornmentDefault;
601     ControlSize controlSize = controlSizeFromPixelSize(stepperSizes(), IntSize(zoomedRect.size()), zoomFactor);
602     if (controlSize == NSControlSizeSmall)
603         drawInfo.kind = kThemeIncDecButtonSmall;
604     else if (controlSize == NSControlSizeMini)
605         drawInfo.kind = kThemeIncDecButtonMini;
606     else
607         drawInfo.kind = kThemeIncDecButton;
608
609     IntRect rect(zoomedRect);
610     GraphicsContextStateSaver stateSaver(context);
611     if (zoomFactor != 1.0f) {
612         rect.setWidth(rect.width() / zoomFactor);
613         rect.setHeight(rect.height() / zoomFactor);
614         context.translate(rect.location());
615         context.scale(zoomFactor);
616         context.translate(-rect.location());
617     }
618     CGRect bounds(rect);
619     CGRect backgroundBounds;
620     HIThemeGetButtonBackgroundBounds(&bounds, &drawInfo, &backgroundBounds);
621     // Center the stepper rectangle in the specified area.
622     backgroundBounds.origin.x = bounds.origin.x + (bounds.size.width - backgroundBounds.size.width) / 2;
623     if (backgroundBounds.size.height < bounds.size.height) {
624         int heightDiff = clampToInteger(bounds.size.height - backgroundBounds.size.height);
625         backgroundBounds.origin.y = bounds.origin.y + (heightDiff / 2) + 1;
626     }
627
628     LocalCurrentGraphicsContext localContext(context);
629     HIThemeDrawButton(&backgroundBounds, &drawInfo, localContext.cgContext(), kHIThemeOrientationNormal, 0);
630 }
631
632 // This will ensure that we always return a valid NSView, even if ScrollView doesn't have an associated document NSView.
633 // If the ScrollView doesn't have an NSView, we will return a fake NSView set up in the way AppKit expects.
634 NSView *ThemeMac::ensuredView(ScrollView* scrollView, const ControlStates& controlStates, bool useUnparentedView)
635 {
636     if (!useUnparentedView) {
637         if (NSView *documentView = scrollView->documentView())
638             return documentView;
639     }
640
641     // Use a fake view.
642     static WebCoreThemeView *themeView = [[WebCoreThemeView alloc] init];
643     [themeView setFrameSize:NSSizeFromCGSize(scrollView->totalContentsSize())];
644
645     themeWindowHasKeyAppearance = !(controlStates.states() & ControlStates::WindowInactiveState);
646
647     return themeView;
648 }
649
650 void ThemeMac::setFocusRingClipRect(const FloatRect& rect)
651 {
652     focusRingClipRect = rect;
653 }
654
655 const float buttonFocusRectOutlineWidth = 3.0f;
656
657 static inline bool drawCellOrFocusRingIntoRectWithView(NSCell *cell, NSRect rect, NSView *view, bool drawButtonCell, bool drawFocusRing)
658 {
659     if (drawButtonCell) {
660         if ([cell isKindOfClass:[NSSliderCell class]]) {
661             // For slider cells, draw only the knob.
662             [(NSSliderCell *)cell drawKnob:rect];
663         } else
664             [cell drawWithFrame:rect inView:view];
665     }
666     if (drawFocusRing)
667         return drawCellFocusRing(cell, rect, view);
668
669     return false;
670 }
671
672 bool ThemeMac::drawCellOrFocusRingWithViewIntoContext(NSCell *cell, GraphicsContext& context, const FloatRect& rect, NSView *view, bool drawButtonCell, bool drawFocusRing, bool useImageBuffer, float deviceScaleFactor)
673 {
674     ASSERT(drawButtonCell || drawFocusRing);
675     bool needsRepaint = false;
676     if (useImageBuffer) {
677         NSRect imageBufferDrawRect = NSRect(FloatRect(buttonFocusRectOutlineWidth, buttonFocusRectOutlineWidth, rect.width(), rect.height()));
678         auto imageBuffer = ImageBuffer::createCompatibleBuffer(rect.size() + 2 * FloatSize(buttonFocusRectOutlineWidth, buttonFocusRectOutlineWidth), deviceScaleFactor, ColorSpaceSRGB, context);
679         if (!imageBuffer)
680             return needsRepaint;
681         {
682             LocalCurrentGraphicsContext localContext(imageBuffer->context());
683             needsRepaint = drawCellOrFocusRingIntoRectWithView(cell, imageBufferDrawRect, view, drawButtonCell, drawFocusRing);
684         }
685         context.drawConsumingImageBuffer(WTFMove(imageBuffer), rect.location() - FloatSize(buttonFocusRectOutlineWidth, buttonFocusRectOutlineWidth));
686         return needsRepaint;
687     }
688     if (drawButtonCell)
689         needsRepaint = drawCellOrFocusRingIntoRectWithView(cell, NSRect(rect), view, drawButtonCell, drawFocusRing);
690     
691     return needsRepaint;
692 }
693
694 // Theme overrides
695
696 int ThemeMac::baselinePositionAdjustment(ControlPart part) const
697 {
698     if (part == CheckboxPart || part == RadioPart)
699         return -2;
700     return Theme::baselinePositionAdjustment(part);
701 }
702
703 std::optional<FontCascadeDescription> ThemeMac::controlFont(ControlPart part, const FontCascade& font, float zoomFactor) const
704 {
705     switch (part) {
706     case PushButtonPart: {
707         FontCascadeDescription fontDescription;
708         fontDescription.setIsAbsoluteSize(true);
709
710         NSFont* nsFont = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:controlSizeForFont(font)]];
711         fontDescription.setOneFamily(AtomicString("-apple-system", AtomicString::ConstructFromLiteral));
712         fontDescription.setComputedSize([nsFont pointSize] * zoomFactor);
713         fontDescription.setSpecifiedSize([nsFont pointSize] * zoomFactor);
714         return fontDescription;
715     }
716     default:
717         return std::nullopt;
718     }
719 }
720
721 LengthSize ThemeMac::controlSize(ControlPart part, const FontCascade& font, const LengthSize& zoomedSize, float zoomFactor) const
722 {
723     switch (part) {
724     case CheckboxPart:
725         return checkboxSize(font, zoomedSize, zoomFactor);
726     case RadioPart:
727         return radioSize(font, zoomedSize, zoomFactor);
728     case PushButtonPart:
729         // Height is reset to auto so that specified heights can be ignored.
730         return sizeFromFont(font, { zoomedSize.width, { } }, zoomFactor, buttonSizes());
731     case InnerSpinButtonPart:
732         if (!zoomedSize.width.isIntrinsicOrAuto() && !zoomedSize.height.isIntrinsicOrAuto())
733             return zoomedSize;
734         return sizeFromNSControlSize(stepperControlSizeForFont(font), zoomedSize, zoomFactor, stepperSizes());
735     default:
736         return zoomedSize;
737     }
738 }
739
740 LengthSize ThemeMac::minimumControlSize(ControlPart part, const FontCascade& font, float zoomFactor) const
741 {
742     switch (part) {
743     case SquareButtonPart:
744     case DefaultButtonPart:
745     case ButtonPart:
746         return { { 0, Fixed }, { static_cast<int>(15 * zoomFactor), Fixed } };
747     case InnerSpinButtonPart: {
748         auto& base = stepperSizes()[NSControlSizeMini];
749         return { { static_cast<int>(base.width() * zoomFactor), Fixed },
750             { static_cast<int>(base.height() * zoomFactor), Fixed } };
751     }
752     default:
753         return Theme::minimumControlSize(part, font, zoomFactor);
754     }
755 }
756
757 LengthBox ThemeMac::controlBorder(ControlPart part, const FontCascade& font, const LengthBox& zoomedBox, float zoomFactor) const
758 {
759     switch (part) {
760     case SquareButtonPart:
761     case DefaultButtonPart:
762     case ButtonPart:
763         return LengthBox(0, zoomedBox.right().value(), 0, zoomedBox.left().value());
764     default:
765         return Theme::controlBorder(part, font, zoomedBox, zoomFactor);
766     }
767 }
768
769 LengthBox ThemeMac::controlPadding(ControlPart part, const FontCascade& font, const LengthBox& zoomedBox, float zoomFactor) const
770 {
771     switch (part) {
772     case PushButtonPart: {
773         // Just use 8px. AppKit wants to use 11px for mini buttons, but that padding is just too large
774         // for real-world Web sites (creating a huge necessary minimum width for buttons whose space is
775         // by definition constrained, since we select mini only for small cramped environments).
776         // This also guarantees the HTML <button> will match our rendering by default, since we're using
777         // a consistent padding.
778         int padding = 8 * zoomFactor;
779         return LengthBox(2, padding, 3, padding);
780     }
781     default:
782         return Theme::controlPadding(part, font, zoomedBox, zoomFactor);
783     }
784 }
785
786 void ThemeMac::inflateControlPaintRect(ControlPart part, const ControlStates& states, FloatRect& zoomedRect, float zoomFactor) const
787 {
788     BEGIN_BLOCK_OBJC_EXCEPTIONS
789     IntSize zoomRectSize = IntSize(zoomedRect.size());
790     switch (part) {
791         case CheckboxPart: {
792             // We inflate the rect as needed to account for padding included in the cell to accommodate the checkbox
793             // shadow" and the check. We don't consider this part of the bounds of the control in WebKit.
794             NSCell *cell = sharedCheckboxCell(states, zoomRectSize, zoomFactor);
795             NSControlSize controlSize = [cell controlSize];
796             IntSize zoomedSize = checkboxSizes()[controlSize];
797             zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
798             zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
799             zoomedRect = inflateRect(zoomedRect, zoomedSize, checkboxMargins(controlSize), zoomFactor);
800             break;
801         }
802         case RadioPart: {
803             // We inflate the rect as needed to account for padding included in the cell to accommodate the radio button
804             // shadow". We don't consider this part of the bounds of the control in WebKit.
805             NSCell *cell = sharedRadioCell(states, zoomRectSize, zoomFactor);
806             NSControlSize controlSize = [cell controlSize];
807             IntSize zoomedSize = radioSizes()[controlSize];
808             zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
809             zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
810             zoomedRect = inflateRect(zoomedRect, zoomedSize, radioMargins(controlSize), zoomFactor);
811             break;
812         }
813         case PushButtonPart:
814         case DefaultButtonPart:
815         case ButtonPart: {
816             NSButtonCell *cell = button(part, states, zoomRectSize, zoomFactor);
817             NSControlSize controlSize = [cell controlSize];
818
819             // We inflate the rect as needed to account for the Aqua button's shadow.
820             if ([cell bezelStyle] == NSRoundedBezelStyle) {
821                 IntSize zoomedSize = buttonSizes()[controlSize];
822                 zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
823                 zoomedSize.setWidth(zoomedRect.width()); // Buttons don't ever constrain width, so the zoomed width can just be honored.
824                 zoomedRect = inflateRect(zoomedRect, zoomedSize, buttonMargins(controlSize), zoomFactor);
825             }
826             break;
827         }
828         case InnerSpinButtonPart: {
829             static const int stepperMargin[4] = { 0, 0, 0, 0 };
830             ControlSize controlSize = controlSizeFromPixelSize(stepperSizes(), zoomRectSize, zoomFactor);
831             IntSize zoomedSize = stepperSizes()[controlSize];
832             zoomedSize.setHeight(zoomedSize.height() * zoomFactor);
833             zoomedSize.setWidth(zoomedSize.width() * zoomFactor);
834             zoomedRect = inflateRect(zoomedRect, zoomedSize, stepperMargin, zoomFactor);
835             break;
836         }
837         default:
838             break;
839     }
840     END_BLOCK_OBJC_EXCEPTIONS
841 }
842
843 void ThemeMac::paint(ControlPart part, ControlStates& states, GraphicsContext& context, const FloatRect& zoomedRect, float zoomFactor, ScrollView* scrollView, float deviceScaleFactor, float pageScaleFactor)
844 {
845     switch (part) {
846         case CheckboxPart:
847             paintToggleButton(part, states, context, zoomedRect, zoomFactor, scrollView, deviceScaleFactor, pageScaleFactor);
848             break;
849         case RadioPart:
850             paintToggleButton(part, states, context, zoomedRect, zoomFactor, scrollView, deviceScaleFactor, pageScaleFactor);
851             break;
852         case PushButtonPart:
853         case DefaultButtonPart:
854         case ButtonPart:
855         case SquareButtonPart:
856             paintButton(part, states, context, zoomedRect, zoomFactor, scrollView, deviceScaleFactor, pageScaleFactor);
857             break;
858         case InnerSpinButtonPart:
859             paintStepper(states, context, zoomedRect, zoomFactor, scrollView);
860             break;
861         default:
862             break;
863     }
864 }
865
866 bool ThemeMac::userPrefersReducedMotion() const
867 {
868 #if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200
869     return [[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion];
870 #else
871     return false;
872 #endif
873 }
874
875 }