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