Bug 1761357 [wpt PR 33355] - Fix #33204: Move Safari stable runs to Big Sur, a=testonly
[gecko.git] / widget / Theme.cpp
blob6a50d99f171d31f558d13bbff2cb97d609c538f5
1 /* -*- Mode: C++; tab-width: 40; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "Theme.h"
7 #include "ThemeCocoa.h"
9 #include "mozilla/MathAlgorithms.h"
10 #include "mozilla/ClearOnShutdown.h"
11 #include "mozilla/dom/Document.h"
12 #include "mozilla/dom/HTMLMeterElement.h"
13 #include "mozilla/dom/HTMLProgressElement.h"
14 #include "mozilla/gfx/Rect.h"
15 #include "mozilla/gfx/Types.h"
16 #include "mozilla/gfx/Filters.h"
17 #include "mozilla/RelativeLuminanceUtils.h"
18 #include "mozilla/StaticPrefs_widget.h"
19 #include "mozilla/webrender/WebRenderAPI.h"
20 #include "nsCSSColorUtils.h"
21 #include "nsCSSRendering.h"
22 #include "nsDeviceContext.h"
23 #include "nsLayoutUtils.h"
24 #include "nsRangeFrame.h"
25 #include "PathHelpers.h"
26 #include "ScrollbarDrawingAndroid.h"
27 #include "ScrollbarDrawingCocoa.h"
28 #include "ScrollbarDrawingGTK.h"
29 #include "ScrollbarDrawingWin.h"
30 #include "ScrollbarDrawingWin11.h"
32 #ifdef XP_WIN
33 # include "mozilla/WindowsVersion.h"
34 #endif
36 using namespace mozilla;
37 using namespace mozilla::dom;
38 using namespace mozilla::gfx;
39 using namespace mozilla::widget;
41 namespace {
43 static constexpr gfx::sRGBColor sColorGrey10(
44 gfx::sRGBColor::UnusualFromARGB(0xffe9e9ed));
45 static constexpr gfx::sRGBColor sColorGrey10Alpha50(
46 gfx::sRGBColor::UnusualFromARGB(0x7fe9e9ed));
47 static constexpr gfx::sRGBColor sColorGrey20(
48 gfx::sRGBColor::UnusualFromARGB(0xffd0d0d7));
49 static constexpr gfx::sRGBColor sColorGrey30(
50 gfx::sRGBColor::UnusualFromARGB(0xffb1b1b9));
51 static constexpr gfx::sRGBColor sColorGrey40(
52 gfx::sRGBColor::UnusualFromARGB(0xff8f8f9d));
53 static constexpr gfx::sRGBColor sColorGrey40Alpha50(
54 gfx::sRGBColor::UnusualFromARGB(0x7f8f8f9d));
55 static constexpr gfx::sRGBColor sColorGrey50(
56 gfx::sRGBColor::UnusualFromARGB(0xff676774));
57 static constexpr gfx::sRGBColor sColorGrey60(
58 gfx::sRGBColor::UnusualFromARGB(0xff484851));
60 static constexpr gfx::sRGBColor sColorMeterGreen10(
61 gfx::sRGBColor::UnusualFromARGB(0xff00ab60));
62 static constexpr gfx::sRGBColor sColorMeterGreen20(
63 gfx::sRGBColor::UnusualFromARGB(0xff056139));
64 static constexpr gfx::sRGBColor sColorMeterYellow10(
65 gfx::sRGBColor::UnusualFromARGB(0xffffbd4f));
66 static constexpr gfx::sRGBColor sColorMeterYellow20(
67 gfx::sRGBColor::UnusualFromARGB(0xffd2811e));
68 static constexpr gfx::sRGBColor sColorMeterRed10(
69 gfx::sRGBColor::UnusualFromARGB(0xffe22850));
70 static constexpr gfx::sRGBColor sColorMeterRed20(
71 gfx::sRGBColor::UnusualFromARGB(0xff810220));
73 static const CSSCoord kMinimumColorPickerHeight = 32.0f;
74 static const CSSCoord kMinimumRangeThumbSize = 20.0f;
75 static const CSSCoord kMinimumDropdownArrowButtonWidth = 18.0f;
76 static const CSSCoord kMinimumSpinnerButtonWidth = 18.0f;
77 static const CSSCoord kMinimumSpinnerButtonHeight = 9.0f;
78 static const CSSCoord kButtonBorderWidth = 1.0f;
79 static const CSSCoord kMenulistBorderWidth = 1.0f;
80 static const CSSCoord kTextFieldBorderWidth = 1.0f;
81 static const CSSCoord kRangeHeight = 6.0f;
82 static const CSSCoord kProgressbarHeight = 6.0f;
83 static const CSSCoord kMeterHeight = 12.0f;
85 // nsCheckboxRadioFrame takes the bottom of the content box as the baseline.
86 // This border-width makes its baseline 2px under the bottom, which is nice.
87 static constexpr CSSCoord kCheckboxRadioBorderWidth = 2.0f;
89 static constexpr sRGBColor sTransparent = sRGBColor::White(0.0);
91 // This pushes and pops a clip rect to the draw target.
93 // This is done to reduce fuzz in places where we may have antialiasing,
94 // because skia is not clip-invariant: given different clips, it does not
95 // guarantee the same result, even if the painted content doesn't intersect
96 // the clips.
98 // This is a bit sad, overall, but...
99 struct MOZ_RAII AutoClipRect {
100 AutoClipRect(DrawTarget& aDt, const LayoutDeviceRect& aRect) : mDt(aDt) {
101 mDt.PushClipRect(aRect.ToUnknownRect());
104 ~AutoClipRect() { mDt.PopClip(); }
106 private:
107 DrawTarget& mDt;
110 static StaticRefPtr<Theme> gNativeInstance;
111 static StaticRefPtr<Theme> gNonNativeInstance;
112 static StaticRefPtr<Theme> gRDMInstance;
114 } // namespace
116 #ifdef ANDROID
117 already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() {
118 // Android doesn't have a native theme.
119 return do_AddRef(new Theme(Theme::ScrollbarStyle()));
121 #endif
123 already_AddRefed<nsITheme> do_GetBasicNativeThemeDoNotUseDirectly() {
124 if (MOZ_UNLIKELY(!gNonNativeInstance)) {
125 UniquePtr<ScrollbarDrawing> scrollbarDrawing = Theme::ScrollbarStyle();
126 #ifdef MOZ_WIDGET_COCOA
127 gNonNativeInstance = new ThemeCocoa(std::move(scrollbarDrawing));
128 #else
129 gNonNativeInstance = new Theme(std::move(scrollbarDrawing));
130 #endif
131 ClearOnShutdown(&gNonNativeInstance);
133 return do_AddRef(gNonNativeInstance);
136 already_AddRefed<nsITheme> do_GetNativeThemeDoNotUseDirectly() {
137 if (MOZ_UNLIKELY(!gNativeInstance)) {
138 gNativeInstance = do_CreateNativeThemeDoNotUseDirectly();
139 ClearOnShutdown(&gNativeInstance);
141 return do_AddRef(gNativeInstance);
144 already_AddRefed<nsITheme> do_GetRDMThemeDoNotUseDirectly() {
145 if (MOZ_UNLIKELY(!gRDMInstance)) {
146 gRDMInstance = new Theme(MakeUnique<ScrollbarDrawingAndroid>());
147 ClearOnShutdown(&gRDMInstance);
149 return do_AddRef(gRDMInstance);
152 namespace mozilla::widget {
154 NS_IMPL_ISUPPORTS_INHERITED(Theme, nsNativeTheme, nsITheme)
156 static constexpr nsLiteralCString kPrefs[] = {
157 "widget.non-native-theme.use-theme-accent"_ns,
158 "widget.non-native-theme.win.scrollbar.use-system-size"_ns,
159 "widget.non-native-theme.scrollbar.size.override"_ns,
160 "widget.non-native-theme.scrollbar.style"_ns,
163 void Theme::Init() {
164 for (const auto& pref : kPrefs) {
165 Preferences::RegisterCallback(PrefChangedCallback, pref);
167 LookAndFeelChanged();
170 void Theme::Shutdown() {
171 for (const auto& pref : kPrefs) {
172 Preferences::UnregisterCallback(PrefChangedCallback, pref);
176 /* static */
177 void Theme::LookAndFeelChanged() {
178 ThemeColors::RecomputeAccentColors();
179 if (gNonNativeInstance) {
180 gNonNativeInstance->SetScrollbarDrawing(ScrollbarStyle());
182 if (gNativeInstance) {
183 gNativeInstance->SetScrollbarDrawing(ScrollbarStyle());
187 /* static */
188 auto Theme::GetDPIRatio(nsPresContext* aPc, StyleAppearance aAppearance)
189 -> DPIRatio {
190 // Widgets react to zoom, except scrollbars.
191 if (IsWidgetScrollbarPart(aAppearance)) {
192 return ScrollbarDrawing::GetDPIRatioForScrollbarPart(aPc);
194 return DPIRatio(float(AppUnitsPerCSSPixel()) / aPc->AppUnitsPerDevPixel());
197 /* static */
198 auto Theme::GetDPIRatio(nsIFrame* aFrame, StyleAppearance aAppearance)
199 -> DPIRatio {
200 return GetDPIRatio(aFrame->PresContext(), aAppearance);
203 // Checkbox and radio need to preserve aspect-ratio for compat. We also snap the
204 // size to exact device pixels to avoid snapping disorting the circles.
205 static LayoutDeviceRect CheckBoxRadioRect(const LayoutDeviceRect& aRect) {
206 // Place a square rect in the center of aRect.
207 auto size = std::trunc(std::min(aRect.width, aRect.height));
208 auto position = aRect.Center() - LayoutDevicePoint(size * 0.5, size * 0.5);
209 return LayoutDeviceRect(position, LayoutDeviceSize(size, size));
212 std::pair<sRGBColor, sRGBColor> Theme::ComputeCheckboxColors(
213 const EventStates& aState, StyleAppearance aAppearance,
214 const Colors& aColors) {
215 MOZ_ASSERT(aAppearance == StyleAppearance::Checkbox ||
216 aAppearance == StyleAppearance::Radio);
218 bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED);
219 bool isChecked = aState.HasState(NS_EVENT_STATE_CHECKED);
220 bool isIndeterminate = aAppearance == StyleAppearance::Checkbox &&
221 aState.HasState(NS_EVENT_STATE_INDETERMINATE);
223 if (isChecked || isIndeterminate) {
224 if (isDisabled) {
225 auto color = ComputeBorderColor(aState, aColors, OutlineCoversBorder::No);
226 return std::make_pair(color, color);
229 bool isActive =
230 aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE);
231 bool isHovered = aState.HasState(NS_EVENT_STATE_HOVER);
232 const auto& color = isActive ? aColors.Accent().GetDarker()
233 : isHovered ? aColors.Accent().GetDark()
234 : aColors.Accent().Get();
235 return std::make_pair(color, color);
238 return ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::No);
241 sRGBColor Theme::ComputeCheckmarkColor(const EventStates& aState,
242 const Colors& aColors) {
243 if (aColors.HighContrast()) {
244 return aColors.System(StyleSystemColor::Selecteditemtext);
246 if (aState.HasState(NS_EVENT_STATE_DISABLED)) {
247 return sRGBColor::White(.8f);
249 return aColors.Accent().GetForeground();
252 sRGBColor Theme::ComputeBorderColor(const EventStates& aState,
253 const Colors& aColors,
254 OutlineCoversBorder aOutlineCoversBorder) {
255 bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED);
256 if (aColors.HighContrast()) {
257 return aColors.System(isDisabled ? StyleSystemColor::Graytext
258 : StyleSystemColor::Buttontext);
260 bool isActive =
261 aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE);
262 bool isHovered = aState.HasState(NS_EVENT_STATE_HOVER);
263 bool isFocused = aState.HasState(NS_EVENT_STATE_FOCUSRING);
264 if (isDisabled) {
265 return sColorGrey40Alpha50;
267 if (isFocused && aOutlineCoversBorder == OutlineCoversBorder::Yes) {
268 // If we draw the outline over the border, prevent issues where the border
269 // shows underneath if it snaps in the wrong direction by using a
270 // transparent border. An alternative to this is ensuring that we snap the
271 // offset in PaintRoundedFocusRect the same was a we snap border widths, so
272 // that negative offsets are guaranteed to cover the border.
273 // But this looks harder to mess up.
274 return sTransparent;
276 bool dark = aColors.IsDark();
277 if (isActive) {
278 return dark ? sColorGrey20 : sColorGrey60;
280 if (isHovered) {
281 return dark ? sColorGrey30 : sColorGrey50;
283 return sColorGrey40;
286 std::pair<sRGBColor, sRGBColor> Theme::ComputeButtonColors(
287 const EventStates& aState, const Colors& aColors, nsIFrame* aFrame) {
288 bool isActive =
289 aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE);
290 bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED);
291 bool isHovered = aState.HasState(NS_EVENT_STATE_HOVER);
293 nscolor backgroundColor = [&] {
294 if (isDisabled) {
295 return aColors.SystemNs(StyleSystemColor::MozButtondisabledface);
297 if (isActive) {
298 return aColors.SystemNs(StyleSystemColor::MozButtonactiveface);
300 if (isHovered) {
301 return aColors.SystemNs(StyleSystemColor::MozButtonhoverface);
303 return aColors.SystemNs(StyleSystemColor::Buttonface);
304 }();
306 if (aState.HasState(NS_EVENT_STATE_AUTOFILL)) {
307 backgroundColor = NS_ComposeColors(
308 backgroundColor,
309 aColors.SystemNs(StyleSystemColor::MozAutofillBackground));
312 const sRGBColor borderColor =
313 ComputeBorderColor(aState, aColors, OutlineCoversBorder::Yes);
314 return std::make_pair(sRGBColor::FromABGR(backgroundColor), borderColor);
317 std::pair<sRGBColor, sRGBColor> Theme::ComputeTextfieldColors(
318 const EventStates& aState, const Colors& aColors,
319 OutlineCoversBorder aOutlineCoversBorder) {
320 nscolor backgroundColor = [&] {
321 if (aState.HasState(NS_EVENT_STATE_DISABLED)) {
322 return aColors.SystemNs(StyleSystemColor::MozDisabledfield);
324 return aColors.SystemNs(StyleSystemColor::Field);
325 }();
327 if (aState.HasState(NS_EVENT_STATE_AUTOFILL)) {
328 backgroundColor = NS_ComposeColors(
329 backgroundColor,
330 aColors.SystemNs(StyleSystemColor::MozAutofillBackground));
333 const sRGBColor borderColor =
334 ComputeBorderColor(aState, aColors, aOutlineCoversBorder);
335 return std::make_pair(sRGBColor::FromABGR(backgroundColor), borderColor);
338 std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeProgressColors(
339 const EventStates& aState, const Colors& aColors) {
340 if (aColors.HighContrast()) {
341 return aColors.SystemPair(StyleSystemColor::Selecteditem,
342 StyleSystemColor::Buttontext);
345 bool isActive =
346 aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE);
347 bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED);
348 bool isHovered = aState.HasState(NS_EVENT_STATE_HOVER);
350 if (isDisabled) {
351 return std::make_pair(sColorGrey40Alpha50, sColorGrey40Alpha50);
353 if (isActive || isHovered) {
354 return std::make_pair(aColors.Accent().GetDark(),
355 aColors.Accent().GetDarker());
357 return std::make_pair(aColors.Accent().Get(), aColors.Accent().GetDark());
360 std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeTrackColors(
361 const EventStates& aState, const Colors& aColors) {
362 if (aColors.HighContrast()) {
363 return aColors.SystemPair(StyleSystemColor::Window,
364 StyleSystemColor::Buttontext);
366 bool isActive =
367 aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE);
368 bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED);
369 bool isHovered = aState.HasState(NS_EVENT_STATE_HOVER);
371 if (isDisabled) {
372 return std::make_pair(sColorGrey10Alpha50, sColorGrey40Alpha50);
374 if (isActive || isHovered) {
375 return std::make_pair(sColorGrey20, sColorGrey50);
377 return std::make_pair(sColorGrey10, sColorGrey40);
380 std::pair<sRGBColor, sRGBColor> Theme::ComputeRangeThumbColors(
381 const EventStates& aState, const Colors& aColors) {
382 if (aColors.HighContrast()) {
383 return aColors.SystemPair(StyleSystemColor::Selecteditemtext,
384 StyleSystemColor::Selecteditem);
387 bool isActive =
388 aState.HasAllStates(NS_EVENT_STATE_HOVER | NS_EVENT_STATE_ACTIVE);
389 bool isDisabled = aState.HasState(NS_EVENT_STATE_DISABLED);
390 bool isHovered = aState.HasState(NS_EVENT_STATE_HOVER);
392 const sRGBColor& backgroundColor = [&] {
393 if (isDisabled) {
394 return sColorGrey40;
396 if (isActive) {
397 return aColors.Accent().Get();
399 if (isHovered) {
400 return sColorGrey60;
402 return sColorGrey50;
403 }();
405 const sRGBColor borderColor = sRGBColor::OpaqueWhite();
406 return std::make_pair(backgroundColor, borderColor);
409 std::pair<sRGBColor, sRGBColor> Theme::ComputeProgressColors(
410 const Colors& aColors) {
411 if (aColors.HighContrast()) {
412 return aColors.SystemPair(StyleSystemColor::Selecteditem,
413 StyleSystemColor::Buttontext);
415 return std::make_pair(aColors.Accent().Get(), aColors.Accent().GetDark());
418 std::pair<sRGBColor, sRGBColor> Theme::ComputeProgressTrackColors(
419 const Colors& aColors) {
420 if (aColors.HighContrast()) {
421 return aColors.SystemPair(StyleSystemColor::Buttonface,
422 StyleSystemColor::Buttontext);
424 return std::make_pair(sColorGrey10, sColorGrey40);
427 std::pair<sRGBColor, sRGBColor> Theme::ComputeMeterchunkColors(
428 const EventStates& aMeterState, const Colors& aColors) {
429 if (aColors.HighContrast()) {
430 return ComputeProgressColors(aColors);
432 sRGBColor borderColor = sColorMeterGreen20;
433 sRGBColor chunkColor = sColorMeterGreen10;
435 if (aMeterState.HasState(NS_EVENT_STATE_SUB_OPTIMUM)) {
436 borderColor = sColorMeterYellow20;
437 chunkColor = sColorMeterYellow10;
438 } else if (aMeterState.HasState(NS_EVENT_STATE_SUB_SUB_OPTIMUM)) {
439 borderColor = sColorMeterRed20;
440 chunkColor = sColorMeterRed10;
443 return std::make_pair(chunkColor, borderColor);
446 std::array<sRGBColor, 3> Theme::ComputeFocusRectColors(const Colors& aColors) {
447 if (aColors.HighContrast()) {
448 return {aColors.System(StyleSystemColor::Selecteditem),
449 aColors.System(StyleSystemColor::Buttontext),
450 aColors.System(StyleSystemColor::Window)};
452 const auto& accent = aColors.Accent();
453 const sRGBColor middle =
454 aColors.IsDark() ? sRGBColor::Black(.3f) : sRGBColor::White(.3f);
455 return {accent.Get(), middle, accent.GetLight()};
458 static const CSSCoord kInnerFocusOutlineWidth = 2.0f;
460 template <typename PaintBackendData>
461 void Theme::PaintRoundedFocusRect(PaintBackendData& aBackendData,
462 const LayoutDeviceRect& aRect,
463 const Colors& aColors, DPIRatio aDpiRatio,
464 CSSCoord aRadius, CSSCoord aOffset) {
465 // NOTE(emilio): If the widths or offsets here change, make sure to tweak
466 // the GetWidgetOverflow path for FocusOutline.
467 auto [innerColor, middleColor, outerColor] = ComputeFocusRectColors(aColors);
469 LayoutDeviceRect focusRect(aRect);
471 // The focus rect is painted outside of the border area (aRect), see:
473 // data:text/html,<div style="border: 1px solid; outline: 2px solid
474 // red">Foobar</div>
476 // But some controls might provide a negative offset to cover the border, if
477 // necessary.
478 CSSCoord strokeWidth = kInnerFocusOutlineWidth;
479 auto strokeWidthDevPx =
480 LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
481 CSSCoord strokeRadius = aRadius;
482 focusRect.Inflate(aOffset * aDpiRatio + strokeWidthDevPx);
484 ThemeDrawing::PaintRoundedRectWithRadius(
485 aBackendData, focusRect, sTransparent, innerColor, strokeWidth,
486 strokeRadius, aDpiRatio);
488 strokeWidth = CSSCoord(1.0f);
489 strokeWidthDevPx =
490 LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
491 strokeRadius += strokeWidth;
492 focusRect.Inflate(strokeWidthDevPx);
494 ThemeDrawing::PaintRoundedRectWithRadius(
495 aBackendData, focusRect, sTransparent, middleColor, strokeWidth,
496 strokeRadius, aDpiRatio);
498 strokeWidth = CSSCoord(2.0f);
499 strokeWidthDevPx =
500 LayoutDeviceCoord(ThemeDrawing::SnapBorderWidth(strokeWidth, aDpiRatio));
501 strokeRadius += strokeWidth;
502 focusRect.Inflate(strokeWidthDevPx);
504 ThemeDrawing::PaintRoundedRectWithRadius(
505 aBackendData, focusRect, sTransparent, outerColor, strokeWidth,
506 strokeRadius, aDpiRatio);
509 void Theme::PaintCheckboxControl(DrawTarget& aDrawTarget,
510 const LayoutDeviceRect& aRect,
511 const EventStates& aState,
512 const Colors& aColors, DPIRatio aDpiRatio) {
513 auto [backgroundColor, borderColor] =
514 ComputeCheckboxColors(aState, StyleAppearance::Checkbox, aColors);
516 const CSSCoord radius = 2.0f;
517 CSSCoord borderWidth = kCheckboxRadioBorderWidth;
518 if (backgroundColor == borderColor) {
519 borderWidth = 0.0f;
521 ThemeDrawing::PaintRoundedRectWithRadius(aDrawTarget, aRect,
522 backgroundColor, borderColor,
523 borderWidth, radius, aDpiRatio);
526 if (aState.HasState(NS_EVENT_STATE_INDETERMINATE)) {
527 PaintIndeterminateMark(aDrawTarget, aRect, aState, aColors);
528 } else if (aState.HasState(NS_EVENT_STATE_CHECKED)) {
529 PaintCheckMark(aDrawTarget, aRect, aState, aColors);
532 if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) {
533 PaintRoundedFocusRect(aDrawTarget, aRect, aColors, aDpiRatio, 5.0f, 1.0f);
537 constexpr CSSCoord kCheckboxRadioContentBoxSize = 10.0f;
538 constexpr CSSCoord kCheckboxRadioBorderBoxSize =
539 kCheckboxRadioContentBoxSize + kCheckboxRadioBorderWidth * 2.0f;
541 void Theme::PaintCheckMark(DrawTarget& aDrawTarget,
542 const LayoutDeviceRect& aRect,
543 const EventStates& aState, const Colors& aColors) {
544 // Points come from the coordinates on a 14X14 (kCheckboxRadioBorderBoxSize)
545 // unit box centered at 0,0
546 const float checkPolygonX[] = {-4.5f, -1.5f, -0.5f, 5.0f, 4.75f,
547 3.5f, -0.5f, -1.5f, -3.5f};
548 const float checkPolygonY[] = {0.5f, 4.0f, 4.0f, -2.5f, -4.0f,
549 -4.0f, 1.0f, 1.25f, -1.0f};
550 const int32_t checkNumPoints = sizeof(checkPolygonX) / sizeof(float);
551 const float scale =
552 ThemeDrawing::ScaleToFillRect(aRect, kCheckboxRadioBorderBoxSize);
553 auto center = aRect.Center().ToUnknownPoint();
555 RefPtr<PathBuilder> builder = aDrawTarget.CreatePathBuilder();
556 Point p = center + Point(checkPolygonX[0] * scale, checkPolygonY[0] * scale);
557 builder->MoveTo(p);
558 for (int32_t i = 1; i < checkNumPoints; i++) {
559 p = center + Point(checkPolygonX[i] * scale, checkPolygonY[i] * scale);
560 builder->LineTo(p);
562 RefPtr<Path> path = builder->Finish();
564 sRGBColor fillColor = ComputeCheckmarkColor(aState, aColors);
565 aDrawTarget.Fill(path, ColorPattern(ToDeviceColor(fillColor)));
568 void Theme::PaintIndeterminateMark(DrawTarget& aDrawTarget,
569 const LayoutDeviceRect& aRect,
570 const EventStates& aState,
571 const Colors& aColors) {
572 const CSSCoord borderWidth = 2.0f;
573 const float scale =
574 ThemeDrawing::ScaleToFillRect(aRect, kCheckboxRadioBorderBoxSize);
576 Rect rect = aRect.ToUnknownRect();
577 rect.y += (rect.height / 2) - (borderWidth * scale / 2);
578 rect.height = borderWidth * scale;
579 rect.x += (borderWidth * scale) + (borderWidth * scale / 8);
580 rect.width -= ((borderWidth * scale) + (borderWidth * scale / 8)) * 2;
582 sRGBColor fillColor = ComputeCheckmarkColor(aState, aColors);
583 aDrawTarget.FillRect(rect, ColorPattern(ToDeviceColor(fillColor)));
586 template <typename PaintBackendData>
587 void Theme::PaintStrokedCircle(PaintBackendData& aPaintData,
588 const LayoutDeviceRect& aRect,
589 const sRGBColor& aBackgroundColor,
590 const sRGBColor& aBorderColor,
591 const CSSCoord aBorderWidth,
592 DPIRatio aDpiRatio) {
593 auto radius = LayoutDeviceCoord(aRect.Size().width) / aDpiRatio;
594 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, aBackgroundColor,
595 aBorderColor, aBorderWidth, radius,
596 aDpiRatio);
599 void Theme::PaintCircleShadow(WebRenderBackendData& aWrData,
600 const LayoutDeviceRect& aBoxRect,
601 const LayoutDeviceRect& aClipRect,
602 float aShadowAlpha, const CSSPoint& aShadowOffset,
603 CSSCoord aShadowBlurStdDev, DPIRatio aDpiRatio) {
604 const bool kBackfaceIsVisible = true;
605 const LayoutDeviceCoord stdDev = aShadowBlurStdDev * aDpiRatio;
606 const LayoutDevicePoint shadowOffset = aShadowOffset * aDpiRatio;
607 const IntSize inflation =
608 gfxAlphaBoxBlur::CalculateBlurRadius(gfxPoint(stdDev, stdDev));
609 LayoutDeviceRect shadowRect = aBoxRect;
610 shadowRect.MoveBy(shadowOffset);
611 shadowRect.Inflate(inflation.width, inflation.height);
612 const auto boxRect = wr::ToLayoutRect(aBoxRect);
613 aWrData.mBuilder.PushBoxShadow(
614 wr::ToLayoutRect(shadowRect), wr::ToLayoutRect(aClipRect),
615 kBackfaceIsVisible, boxRect,
616 wr::ToLayoutVector2D(aShadowOffset * aDpiRatio),
617 wr::ToColorF(DeviceColor(0.0f, 0.0f, 0.0f, aShadowAlpha)), stdDev,
618 /* aSpread = */ 0.0f,
619 wr::ToBorderRadius(gfx::RectCornerRadii(aBoxRect.Size().width)),
620 wr::BoxShadowClipMode::Outset);
623 void Theme::PaintCircleShadow(DrawTarget& aDrawTarget,
624 const LayoutDeviceRect& aBoxRect,
625 const LayoutDeviceRect& aClipRect,
626 float aShadowAlpha, const CSSPoint& aShadowOffset,
627 CSSCoord aShadowBlurStdDev, DPIRatio aDpiRatio) {
628 Float stdDev = aShadowBlurStdDev * aDpiRatio;
629 Point offset = (aShadowOffset * aDpiRatio).ToUnknownPoint();
631 RefPtr<FilterNode> blurFilter =
632 aDrawTarget.CreateFilter(FilterType::GAUSSIAN_BLUR);
633 if (!blurFilter) {
634 return;
637 blurFilter->SetAttribute(ATT_GAUSSIAN_BLUR_STD_DEVIATION, stdDev);
639 IntSize inflation =
640 gfxAlphaBoxBlur::CalculateBlurRadius(gfxPoint(stdDev, stdDev));
641 Rect inflatedRect = aBoxRect.ToUnknownRect();
642 inflatedRect.Inflate(inflation.width, inflation.height);
643 Rect sourceRectInFilterSpace =
644 inflatedRect - aBoxRect.TopLeft().ToUnknownPoint();
645 Point destinationPointOfSourceRect = inflatedRect.TopLeft() + offset;
647 IntSize dtSize = RoundedToInt(aBoxRect.Size().ToUnknownSize());
648 RefPtr<DrawTarget> ellipseDT = aDrawTarget.CreateSimilarDrawTargetForFilter(
649 dtSize, SurfaceFormat::A8, blurFilter, blurFilter,
650 sourceRectInFilterSpace, destinationPointOfSourceRect);
651 if (!ellipseDT) {
652 return;
655 AutoClipRect clipRect(aDrawTarget, aClipRect);
657 RefPtr<Path> ellipse = MakePathForEllipse(
658 *ellipseDT, (aBoxRect - aBoxRect.TopLeft()).Center().ToUnknownPoint(),
659 aBoxRect.Size().ToUnknownSize());
660 ellipseDT->Fill(ellipse,
661 ColorPattern(DeviceColor(0.0f, 0.0f, 0.0f, aShadowAlpha)));
662 RefPtr<SourceSurface> ellipseSurface = ellipseDT->Snapshot();
664 blurFilter->SetInput(IN_GAUSSIAN_BLUR_IN, ellipseSurface);
665 aDrawTarget.DrawFilter(blurFilter, sourceRectInFilterSpace,
666 destinationPointOfSourceRect);
669 template <typename PaintBackendData>
670 void Theme::PaintRadioControl(PaintBackendData& aPaintData,
671 const LayoutDeviceRect& aRect,
672 const EventStates& aState, const Colors& aColors,
673 DPIRatio aDpiRatio) {
674 auto [backgroundColor, borderColor] =
675 ComputeCheckboxColors(aState, StyleAppearance::Radio, aColors);
677 CSSCoord borderWidth = kCheckboxRadioBorderWidth;
678 if (backgroundColor == borderColor) {
679 borderWidth = 0.0f;
681 PaintStrokedCircle(aPaintData, aRect, backgroundColor, borderColor,
682 borderWidth, aDpiRatio);
685 if (aState.HasState(NS_EVENT_STATE_CHECKED)) {
686 LayoutDeviceRect rect(aRect);
687 auto width = LayoutDeviceCoord(
688 ThemeDrawing::SnapBorderWidth(kCheckboxRadioBorderWidth, aDpiRatio));
689 rect.Deflate(width);
691 auto checkColor = ComputeCheckmarkColor(aState, aColors);
692 PaintStrokedCircle(aPaintData, rect, backgroundColor, checkColor,
693 kCheckboxRadioBorderWidth, aDpiRatio);
696 if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) {
697 PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio, 5.0f, 1.0f);
701 template <typename PaintBackendData>
702 void Theme::PaintTextField(PaintBackendData& aPaintData,
703 const LayoutDeviceRect& aRect,
704 const EventStates& aState, const Colors& aColors,
705 DPIRatio aDpiRatio) {
706 auto [backgroundColor, borderColor] =
707 ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::Yes);
709 const CSSCoord radius = 2.0f;
711 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
712 borderColor, kTextFieldBorderWidth,
713 radius, aDpiRatio);
715 if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) {
716 PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
717 radius + kTextFieldBorderWidth,
718 -kTextFieldBorderWidth);
722 template <typename PaintBackendData>
723 void Theme::PaintListbox(PaintBackendData& aPaintData,
724 const LayoutDeviceRect& aRect,
725 const EventStates& aState, const Colors& aColors,
726 DPIRatio aDpiRatio) {
727 const CSSCoord radius = 2.0f;
728 auto [backgroundColor, borderColor] =
729 ComputeTextfieldColors(aState, aColors, OutlineCoversBorder::Yes);
731 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
732 borderColor, kMenulistBorderWidth,
733 radius, aDpiRatio);
735 if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) {
736 PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
737 radius + kMenulistBorderWidth, -kMenulistBorderWidth);
741 template <typename PaintBackendData>
742 void Theme::PaintMenulist(PaintBackendData& aDrawTarget,
743 const LayoutDeviceRect& aRect,
744 const EventStates& aState, const Colors& aColors,
745 DPIRatio aDpiRatio) {
746 const CSSCoord radius = 4.0f;
747 auto [backgroundColor, borderColor] = ComputeButtonColors(aState, aColors);
749 ThemeDrawing::PaintRoundedRectWithRadius(aDrawTarget, aRect, backgroundColor,
750 borderColor, kMenulistBorderWidth,
751 radius, aDpiRatio);
753 if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) {
754 PaintRoundedFocusRect(aDrawTarget, aRect, aColors, aDpiRatio,
755 radius + kMenulistBorderWidth, -kMenulistBorderWidth);
759 void Theme::PaintMenulistArrowButton(nsIFrame* aFrame, DrawTarget& aDrawTarget,
760 const LayoutDeviceRect& aRect,
761 const EventStates& aState) {
762 const float kPolygonX[] = {-4.0f, -0.5f, 0.5f, 4.0f, 4.0f,
763 3.0f, 0.0f, 0.0f, -3.0f, -4.0f};
764 const float kPolygonY[] = {-1, 3.0f, 3.0f, -1.0f, -2.0f,
765 -2.0f, 1.5f, 1.5f, -2.0f, -2.0f};
767 const float kPolygonSize = kMinimumDropdownArrowButtonWidth;
769 const auto arrowColor = sRGBColor::FromABGR(
770 nsLayoutUtils::GetColor(aFrame, &nsStyleText::mWebkitTextFillColor));
771 ThemeDrawing::PaintArrow(aDrawTarget, aRect, kPolygonX, kPolygonY,
772 kPolygonSize, ArrayLength(kPolygonX), arrowColor);
775 void Theme::PaintSpinnerButton(nsIFrame* aFrame, DrawTarget& aDrawTarget,
776 const LayoutDeviceRect& aRect,
777 const EventStates& aState,
778 StyleAppearance aAppearance,
779 const Colors& aColors, DPIRatio aDpiRatio) {
780 auto [backgroundColor, borderColor] = ComputeButtonColors(aState, aColors);
782 aDrawTarget.FillRect(aRect.ToUnknownRect(),
783 ColorPattern(ToDeviceColor(backgroundColor)));
785 const float kPolygonX[] = {-3.5f, -0.5f, 0.5f, 3.5f, 3.5f,
786 2.5f, 0.0f, 0.0f, -2.5f, -3.5f};
787 float polygonY[] = {-1.5f, 1.5f, 1.5f, -1.5f, -2.5f,
788 -2.5f, 0.0f, 0.0f, -2.5f, -2.5f};
790 const float kPolygonSize = kMinimumSpinnerButtonHeight;
791 if (aAppearance == StyleAppearance::SpinnerUpbutton) {
792 for (auto& coord : polygonY) {
793 coord = -coord;
797 ThemeDrawing::PaintArrow(aDrawTarget, aRect, kPolygonX, polygonY,
798 kPolygonSize, ArrayLength(kPolygonX), borderColor);
801 template <typename PaintBackendData>
802 void Theme::PaintRange(nsIFrame* aFrame, PaintBackendData& aPaintData,
803 const LayoutDeviceRect& aRect, const EventStates& aState,
804 const Colors& aColors, DPIRatio aDpiRatio,
805 bool aHorizontal) {
806 nsRangeFrame* rangeFrame = do_QueryFrame(aFrame);
807 if (!rangeFrame) {
808 return;
811 double progress = rangeFrame->GetValueAsFractionOfRange();
812 auto rect = aRect;
813 LayoutDeviceRect thumbRect(0, 0, kMinimumRangeThumbSize * aDpiRatio,
814 kMinimumRangeThumbSize * aDpiRatio);
815 LayoutDeviceRect progressClipRect(aRect);
816 LayoutDeviceRect trackClipRect(aRect);
817 const LayoutDeviceCoord verticalSize = kRangeHeight * aDpiRatio;
818 if (aHorizontal) {
819 rect.height = verticalSize;
820 rect.y = aRect.y + (aRect.height - rect.height) / 2;
821 thumbRect.y = aRect.y + (aRect.height - thumbRect.height) / 2;
823 if (IsFrameRTL(aFrame)) {
824 thumbRect.x =
825 aRect.x + (aRect.width - thumbRect.width) * (1.0 - progress);
826 float midPoint = thumbRect.Center().X();
827 trackClipRect.SetBoxX(aRect.X(), midPoint);
828 progressClipRect.SetBoxX(midPoint, aRect.XMost());
829 } else {
830 thumbRect.x = aRect.x + (aRect.width - thumbRect.width) * progress;
831 float midPoint = thumbRect.Center().X();
832 progressClipRect.SetBoxX(aRect.X(), midPoint);
833 trackClipRect.SetBoxX(midPoint, aRect.XMost());
835 } else {
836 rect.width = verticalSize;
837 rect.x = aRect.x + (aRect.width - rect.width) / 2;
838 thumbRect.x = aRect.x + (aRect.width - thumbRect.width) / 2;
840 thumbRect.y =
841 aRect.y + (aRect.height - thumbRect.height) * (1.0 - progress);
842 float midPoint = thumbRect.Center().Y();
843 trackClipRect.SetBoxY(aRect.Y(), midPoint);
844 progressClipRect.SetBoxY(midPoint, aRect.YMost());
847 const CSSCoord borderWidth = 1.0f;
848 const CSSCoord radius = 3.0f;
850 auto [progressColor, progressBorderColor] =
851 ComputeRangeProgressColors(aState, aColors);
852 auto [trackColor, trackBorderColor] =
853 ComputeRangeTrackColors(aState, aColors);
855 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, progressClipRect,
856 progressColor, progressBorderColor,
857 borderWidth, radius, aDpiRatio);
859 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, trackClipRect,
860 trackColor, trackBorderColor,
861 borderWidth, radius, aDpiRatio);
863 if (!aState.HasState(NS_EVENT_STATE_DISABLED)) {
864 // Ensure the shadow doesn't expand outside of our overflow rect declared in
865 // GetWidgetOverflow().
866 auto overflowRect = aRect;
867 overflowRect.Inflate(CSSCoord(6.0f) * aDpiRatio);
868 // Thumb shadow
869 PaintCircleShadow(aPaintData, thumbRect, overflowRect, 0.3f,
870 CSSPoint(0.0f, 2.0f), 2.0f, aDpiRatio);
873 // Draw the thumb on top.
874 const CSSCoord thumbBorderWidth = 2.0f;
875 auto [thumbColor, thumbBorderColor] =
876 ComputeRangeThumbColors(aState, aColors);
878 PaintStrokedCircle(aPaintData, thumbRect, thumbColor, thumbBorderColor,
879 thumbBorderWidth, aDpiRatio);
881 if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) {
882 PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio, radius, 1.0f);
886 template <typename PaintBackendData>
887 void Theme::PaintProgress(nsIFrame* aFrame, PaintBackendData& aPaintData,
888 const LayoutDeviceRect& aRect,
889 const EventStates& aState, const Colors& aColors,
890 DPIRatio aDpiRatio, bool aIsMeter) {
891 const CSSCoord borderWidth = 1.0f;
892 const CSSCoord radius = aIsMeter ? 6.0f : 3.0f;
894 LayoutDeviceRect rect(aRect);
895 const LayoutDeviceCoord thickness =
896 (aIsMeter ? kMeterHeight : kProgressbarHeight) * aDpiRatio;
898 const bool isHorizontal = !nsNativeTheme::IsVerticalProgress(aFrame);
899 if (isHorizontal) {
900 // Center it vertically.
901 rect.y += (rect.height - thickness) / 2;
902 rect.height = thickness;
903 } else {
904 // Center it horizontally.
905 rect.x += (rect.width - thickness) / 2;
906 rect.width = thickness;
910 // Paint the track, unclipped.
911 auto [backgroundColor, borderColor] = ComputeProgressTrackColors(aColors);
912 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, rect,
913 backgroundColor, borderColor,
914 borderWidth, radius, aDpiRatio);
917 // Now paint the chunk, clipped as needed.
918 LayoutDeviceRect clipRect = rect;
919 if (aState.HasState(NS_EVENT_STATE_INDETERMINATE)) {
920 // For indeterminate progress, we paint an animated chunk of 1/3 of the
921 // progress size.
923 // Animation speed and math borrowed from GTK.
924 const LayoutDeviceCoord size = isHorizontal ? rect.width : rect.height;
925 const LayoutDeviceCoord barSize = size * 0.3333f;
926 const LayoutDeviceCoord travel = 2.0f * (size - barSize);
928 // Period equals to travel / pixelsPerMillisecond where pixelsPerMillisecond
929 // equals progressSize / 1000.0. This is equivalent to 1600.
930 const unsigned kPeriod = 1600;
932 const int t = PR_IntervalToMilliseconds(PR_IntervalNow()) % kPeriod;
933 const LayoutDeviceCoord dx = travel * float(t) / float(kPeriod);
934 if (isHorizontal) {
935 rect.width = barSize;
936 rect.x += (dx < travel * .5f) ? dx : travel - dx;
937 } else {
938 rect.height = barSize;
939 rect.y += (dx < travel * .5f) ? dx : travel - dx;
941 clipRect = rect;
942 // Queue the next frame if needed.
943 if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 60)) {
944 NS_WARNING("Couldn't refresh indeterminate <progress>");
946 } else {
947 // This is the progress chunk, clip it to the right amount.
948 double position = [&] {
949 if (aIsMeter) {
950 auto* meter = dom::HTMLMeterElement::FromNode(aFrame->GetContent());
951 if (!meter) {
952 return 0.0;
954 return meter->Position();
956 auto* progress = dom::HTMLProgressElement::FromNode(aFrame->GetContent());
957 if (!progress) {
958 return 0.0;
960 return progress->Position();
961 }();
962 if (isHorizontal) {
963 double clipWidth = rect.width * position;
964 clipRect.width = clipWidth;
965 if (IsFrameRTL(aFrame)) {
966 clipRect.x += rect.width - clipWidth;
968 } else {
969 double clipHeight = rect.height * position;
970 clipRect.height = clipHeight;
971 clipRect.y += rect.height - clipHeight;
975 auto [backgroundColor, borderColor] =
976 aIsMeter ? ComputeMeterchunkColors(aState, aColors)
977 : ComputeProgressColors(aColors);
978 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, rect, clipRect,
979 backgroundColor, borderColor,
980 borderWidth, radius, aDpiRatio);
983 template <typename PaintBackendData>
984 void Theme::PaintButton(nsIFrame* aFrame, PaintBackendData& aPaintData,
985 const LayoutDeviceRect& aRect,
986 const EventStates& aState, const Colors& aColors,
987 DPIRatio aDpiRatio) {
988 const CSSCoord radius = 4.0f;
989 auto [backgroundColor, borderColor] =
990 ComputeButtonColors(aState, aColors, aFrame);
992 ThemeDrawing::PaintRoundedRectWithRadius(aPaintData, aRect, backgroundColor,
993 borderColor, kButtonBorderWidth,
994 radius, aDpiRatio);
996 if (aState.HasState(NS_EVENT_STATE_FOCUSRING)) {
997 PaintRoundedFocusRect(aPaintData, aRect, aColors, aDpiRatio,
998 radius + kButtonBorderWidth, -kButtonBorderWidth);
1002 NS_IMETHODIMP
1003 Theme::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
1004 StyleAppearance aAppearance, const nsRect& aRect,
1005 const nsRect& /* aDirtyRect */,
1006 DrawOverflow aDrawOverflow) {
1007 if (!DoDrawWidgetBackground(*aContext->GetDrawTarget(), aFrame, aAppearance,
1008 aRect, aDrawOverflow)) {
1009 return NS_ERROR_NOT_IMPLEMENTED;
1011 return NS_OK;
1014 bool Theme::CreateWebRenderCommandsForWidget(
1015 mozilla::wr::DisplayListBuilder& aBuilder,
1016 mozilla::wr::IpcResourceUpdateQueue& aResources,
1017 const mozilla::layers::StackingContextHelper& aSc,
1018 mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
1019 StyleAppearance aAppearance, const nsRect& aRect) {
1020 if (!StaticPrefs::widget_non_native_theme_webrender()) {
1021 return false;
1023 WebRenderBackendData data{aBuilder, aResources, aSc, aManager};
1024 return DoDrawWidgetBackground(data, aFrame, aAppearance, aRect,
1025 DrawOverflow::Yes);
1028 static LayoutDeviceRect ToSnappedRect(const nsRect& aRect,
1029 nscoord aTwipsPerPixel, DrawTarget& aDt) {
1030 return LayoutDeviceRect::FromUnknownRect(
1031 NSRectToSnappedRect(aRect, aTwipsPerPixel, aDt));
1034 static LayoutDeviceRect ToSnappedRect(const nsRect& aRect,
1035 nscoord aTwipsPerPixel,
1036 WebRenderBackendData& aDt) {
1037 // TODO: Do we need to do any more snapping here?
1038 return LayoutDeviceRect::FromAppUnits(aRect, aTwipsPerPixel);
1041 template <typename PaintBackendData>
1042 bool Theme::DoDrawWidgetBackground(PaintBackendData& aPaintData,
1043 nsIFrame* aFrame,
1044 StyleAppearance aAppearance,
1045 const nsRect& aRect,
1046 DrawOverflow aDrawOverflow) {
1047 static_assert(std::is_same_v<PaintBackendData, DrawTarget> ||
1048 std::is_same_v<PaintBackendData, WebRenderBackendData>);
1050 const nsPresContext* pc = aFrame->PresContext();
1051 const nscoord twipsPerPixel = pc->AppUnitsPerDevPixel();
1052 const auto devPxRect = ToSnappedRect(aRect, twipsPerPixel, aPaintData);
1054 const EventStates docState = pc->Document()->GetDocumentState();
1055 EventStates eventState = GetContentState(aFrame, aAppearance);
1056 if (aAppearance == StyleAppearance::MozMenulistArrowButton) {
1057 bool isHTML = IsHTMLContent(aFrame);
1058 nsIFrame* parentFrame = aFrame->GetParent();
1059 bool isMenulist = !isHTML && parentFrame->IsMenuFrame();
1060 // HTML select and XUL menulist dropdown buttons get state from the
1061 // parent.
1062 if (isHTML || isMenulist) {
1063 aFrame = parentFrame;
1064 eventState = GetContentState(parentFrame, aAppearance);
1068 // Paint the outline iff we're asked to draw overflow and we have
1069 // outline-style: auto.
1070 if (aDrawOverflow == DrawOverflow::Yes &&
1071 aFrame->StyleOutline()->mOutlineStyle.IsAuto()) {
1072 eventState |= NS_EVENT_STATE_FOCUSRING;
1073 } else {
1074 eventState &= ~NS_EVENT_STATE_FOCUSRING;
1077 // Hack to avoid skia fuzziness: Add a dummy clip if the widget doesn't
1078 // overflow devPxRect.
1079 Maybe<AutoClipRect> maybeClipRect;
1080 if constexpr (std::is_same_v<PaintBackendData, DrawTarget>) {
1081 if (aAppearance != StyleAppearance::FocusOutline &&
1082 aAppearance != StyleAppearance::Range &&
1083 !eventState.HasState(NS_EVENT_STATE_FOCUSRING)) {
1084 maybeClipRect.emplace(aPaintData, devPxRect);
1088 const Colors colors(aFrame, aAppearance);
1089 DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
1091 switch (aAppearance) {
1092 case StyleAppearance::Radio: {
1093 auto rect = CheckBoxRadioRect(devPxRect);
1094 PaintRadioControl(aPaintData, rect, eventState, colors, dpiRatio);
1095 break;
1097 case StyleAppearance::Checkbox: {
1098 if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
1099 // TODO: Need to figure out how to best draw this using WR.
1100 return false;
1101 } else {
1102 auto rect = CheckBoxRadioRect(devPxRect);
1103 PaintCheckboxControl(aPaintData, rect, eventState, colors, dpiRatio);
1105 break;
1107 case StyleAppearance::Textarea:
1108 case StyleAppearance::Textfield:
1109 case StyleAppearance::NumberInput:
1110 PaintTextField(aPaintData, devPxRect, eventState, colors, dpiRatio);
1111 break;
1112 case StyleAppearance::Listbox:
1113 PaintListbox(aPaintData, devPxRect, eventState, colors, dpiRatio);
1114 break;
1115 case StyleAppearance::MenulistButton:
1116 case StyleAppearance::Menulist:
1117 PaintMenulist(aPaintData, devPxRect, eventState, colors, dpiRatio);
1118 break;
1119 case StyleAppearance::MozMenulistArrowButton:
1120 if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
1121 // TODO: Need to figure out how to best draw this using WR.
1122 return false;
1123 } else {
1124 PaintMenulistArrowButton(aFrame, aPaintData, devPxRect, eventState);
1126 break;
1127 case StyleAppearance::SpinnerUpbutton:
1128 case StyleAppearance::SpinnerDownbutton:
1129 if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
1130 // TODO: Need to figure out how to best draw this using WR.
1131 return false;
1132 } else {
1133 PaintSpinnerButton(aFrame, aPaintData, devPxRect, eventState,
1134 aAppearance, colors, dpiRatio);
1136 break;
1137 case StyleAppearance::Range:
1138 PaintRange(aFrame, aPaintData, devPxRect, eventState, colors, dpiRatio,
1139 IsRangeHorizontal(aFrame));
1140 break;
1141 case StyleAppearance::RangeThumb:
1142 // Painted as part of StyleAppearance::Range.
1143 break;
1144 case StyleAppearance::ProgressBar:
1145 PaintProgress(aFrame, aPaintData, devPxRect, eventState, colors, dpiRatio,
1146 /* aIsMeter = */ false);
1147 break;
1148 case StyleAppearance::Progresschunk:
1149 /* Painted as part of the progress bar */
1150 break;
1151 case StyleAppearance::Meter:
1152 PaintProgress(aFrame, aPaintData, devPxRect, eventState, colors, dpiRatio,
1153 /* aIsMeter = */ true);
1154 break;
1155 case StyleAppearance::Meterchunk:
1156 /* Painted as part of the meter bar */
1157 break;
1158 case StyleAppearance::ScrollbarthumbHorizontal:
1159 case StyleAppearance::ScrollbarthumbVertical: {
1160 bool isHorizontal =
1161 aAppearance == StyleAppearance::ScrollbarthumbHorizontal;
1162 return GetScrollbarDrawing().PaintScrollbarThumb(
1163 aPaintData, devPxRect, isHorizontal, aFrame,
1164 *nsLayoutUtils::StyleForScrollbar(aFrame), eventState, docState,
1165 colors, dpiRatio);
1167 case StyleAppearance::ScrollbartrackHorizontal:
1168 case StyleAppearance::ScrollbartrackVertical: {
1169 bool isHorizontal =
1170 aAppearance == StyleAppearance::ScrollbartrackHorizontal;
1171 return GetScrollbarDrawing().PaintScrollbarTrack(
1172 aPaintData, devPxRect, isHorizontal, aFrame,
1173 *nsLayoutUtils::StyleForScrollbar(aFrame), docState, colors,
1174 dpiRatio);
1176 case StyleAppearance::ScrollbarHorizontal:
1177 case StyleAppearance::ScrollbarVertical: {
1178 bool isHorizontal = aAppearance == StyleAppearance::ScrollbarHorizontal;
1179 return GetScrollbarDrawing().PaintScrollbar(
1180 aPaintData, devPxRect, isHorizontal, aFrame,
1181 *nsLayoutUtils::StyleForScrollbar(aFrame), eventState, docState,
1182 colors, dpiRatio);
1184 case StyleAppearance::Scrollcorner:
1185 return GetScrollbarDrawing().PaintScrollCorner(
1186 aPaintData, devPxRect, aFrame,
1187 *nsLayoutUtils::StyleForScrollbar(aFrame), docState, colors,
1188 dpiRatio);
1189 case StyleAppearance::ScrollbarbuttonUp:
1190 case StyleAppearance::ScrollbarbuttonDown:
1191 case StyleAppearance::ScrollbarbuttonLeft:
1192 case StyleAppearance::ScrollbarbuttonRight:
1193 // For scrollbar-width:thin, we don't display the buttons.
1194 if (!ScrollbarDrawing::IsScrollbarWidthThin(aFrame)) {
1195 if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
1196 // TODO: Need to figure out how to best draw this using WR.
1197 return false;
1198 } else {
1199 GetScrollbarDrawing().PaintScrollbarButton(
1200 aPaintData, aAppearance, devPxRect, aFrame,
1201 *nsLayoutUtils::StyleForScrollbar(aFrame), eventState, docState,
1202 colors, dpiRatio);
1205 break;
1206 case StyleAppearance::Button:
1207 PaintButton(aFrame, aPaintData, devPxRect, eventState, colors, dpiRatio);
1208 break;
1209 case StyleAppearance::FocusOutline:
1210 PaintAutoStyleOutline(aFrame, aPaintData, devPxRect, colors, dpiRatio);
1211 break;
1212 default:
1213 // Various appearance values are used for XUL elements. Normally these
1214 // will not be available in content documents (and thus in the content
1215 // processes where the native basic theme can be used), but tests are
1216 // run with the remote XUL pref enabled and so we can get in here. So
1217 // we just return an error rather than assert.
1218 return false;
1221 return true;
1224 template <typename PaintBackendData>
1225 void Theme::PaintAutoStyleOutline(nsIFrame* aFrame,
1226 PaintBackendData& aPaintData,
1227 const LayoutDeviceRect& aRect,
1228 const Colors& aColors, DPIRatio aDpiRatio) {
1229 auto [innerColor, middleColor, outerColor] = ComputeFocusRectColors(aColors);
1230 Unused << middleColor;
1231 Unused << outerColor;
1233 LayoutDeviceRect rect(aRect);
1234 auto width = LayoutDeviceCoord(
1235 ThemeDrawing::SnapBorderWidth(kInnerFocusOutlineWidth, aDpiRatio));
1236 rect.Inflate(width);
1238 const nscoord offset = aFrame->StyleOutline()->mOutlineOffset.ToAppUnits();
1239 nscoord cssRadii[8];
1240 if (!aFrame->GetBorderRadii(cssRadii)) {
1241 const CSSCoord cssOffset = CSSCoord::FromAppUnits(offset);
1242 const CSSCoord radius =
1243 cssOffset >= 0.0f
1244 ? kInnerFocusOutlineWidth
1245 : std::max(kInnerFocusOutlineWidth + cssOffset, CSSCoord(0.0f));
1246 return ThemeDrawing::PaintRoundedRectWithRadius(
1247 aPaintData, rect, sRGBColor::White(0.0f), innerColor,
1248 kInnerFocusOutlineWidth, radius, aDpiRatio);
1251 nsPresContext* pc = aFrame->PresContext();
1252 const Float devPixelOffset = pc->AppUnitsToFloatDevPixels(offset);
1254 RectCornerRadii innerRadii;
1255 nsCSSRendering::ComputePixelRadii(cssRadii, pc->AppUnitsPerDevPixel(),
1256 &innerRadii);
1258 const auto borderColor = ToDeviceColor(innerColor);
1259 // NOTE(emilio): This doesn't use PaintRoundedRectWithRadius because we need
1260 // to support arbitrary radii.
1261 RectCornerRadii outerRadii;
1262 if constexpr (std::is_same_v<PaintBackendData, WebRenderBackendData>) {
1263 const Float widths[4] = {width + devPixelOffset, width + devPixelOffset,
1264 width + devPixelOffset, width + devPixelOffset};
1265 nsCSSBorderRenderer::ComputeOuterRadii(innerRadii, widths, &outerRadii);
1267 const auto dest = wr::ToLayoutRect(rect);
1268 const auto side = wr::ToBorderSide(borderColor, StyleBorderStyle::Solid);
1269 const wr::BorderSide sides[4] = {side, side, side, side};
1270 const bool kBackfaceIsVisible = true;
1271 const auto wrWidths = wr::ToBorderWidths(width, width, width, width);
1272 const auto wrRadius = wr::ToBorderRadius(outerRadii);
1273 aPaintData.mBuilder.PushBorder(dest, dest, kBackfaceIsVisible, wrWidths,
1274 {sides, 4}, wrRadius);
1275 } else {
1276 const LayoutDeviceCoord halfWidth = width * 0.5f;
1277 rect.Deflate(halfWidth);
1278 const Float widths[4] = {
1279 halfWidth + devPixelOffset, halfWidth + devPixelOffset,
1280 halfWidth + devPixelOffset, halfWidth + devPixelOffset};
1281 nsCSSBorderRenderer::ComputeOuterRadii(innerRadii, widths, &outerRadii);
1282 RefPtr<Path> path =
1283 MakePathForRoundedRect(aPaintData, rect.ToUnknownRect(), outerRadii);
1284 aPaintData.Stroke(path, ColorPattern(borderColor), StrokeOptions(width));
1288 LayoutDeviceIntMargin Theme::GetWidgetBorder(nsDeviceContext* aContext,
1289 nsIFrame* aFrame,
1290 StyleAppearance aAppearance) {
1291 switch (aAppearance) {
1292 case StyleAppearance::Textfield:
1293 case StyleAppearance::Textarea:
1294 case StyleAppearance::NumberInput:
1295 case StyleAppearance::Listbox:
1296 case StyleAppearance::Menulist:
1297 case StyleAppearance::MenulistButton:
1298 case StyleAppearance::Button:
1299 // Return the border size from the UA sheet, even though what we paint
1300 // doesn't actually match that. We know this is the UA sheet border
1301 // because we disable native theming when different border widths are
1302 // specified by authors, see Theme::IsWidgetStyled.
1304 // The Rounded() bit is technically redundant, but needed to appease the
1305 // type system, we should always end up with full device pixels due to
1306 // round_border_to_device_pixels at style time.
1307 return LayoutDeviceIntMargin::FromAppUnits(
1308 aFrame->StyleBorder()->GetComputedBorder(),
1309 aFrame->PresContext()->AppUnitsPerDevPixel())
1310 .Rounded();
1311 case StyleAppearance::Checkbox:
1312 case StyleAppearance::Radio: {
1313 DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
1314 LayoutDeviceIntCoord w =
1315 ThemeDrawing::SnapBorderWidth(kCheckboxRadioBorderWidth, dpiRatio);
1316 return LayoutDeviceIntMargin(w, w, w, w);
1318 default:
1319 return LayoutDeviceIntMargin();
1323 bool Theme::GetWidgetPadding(nsDeviceContext* aContext, nsIFrame* aFrame,
1324 StyleAppearance aAppearance,
1325 LayoutDeviceIntMargin* aResult) {
1326 switch (aAppearance) {
1327 // Radios and checkboxes return a fixed size in GetMinimumWidgetSize
1328 // and have a meaningful baseline, so they can't have
1329 // author-specified padding.
1330 case StyleAppearance::Radio:
1331 case StyleAppearance::Checkbox:
1332 aResult->SizeTo(0, 0, 0, 0);
1333 return true;
1334 default:
1335 break;
1337 return false;
1340 bool Theme::GetWidgetOverflow(nsDeviceContext* aContext, nsIFrame* aFrame,
1341 StyleAppearance aAppearance,
1342 nsRect* aOverflowRect) {
1343 nsIntMargin overflow;
1344 switch (aAppearance) {
1345 case StyleAppearance::FocusOutline:
1346 // 2px * one segment
1347 overflow.SizeTo(2, 2, 2, 2);
1348 break;
1349 case StyleAppearance::Radio:
1350 case StyleAppearance::Checkbox:
1351 case StyleAppearance::Range:
1352 // 2px for each outline segment, plus 1px separation, plus we paint with a
1353 // 1px extra offset, so 6px.
1354 overflow.SizeTo(6, 6, 6, 6);
1355 break;
1356 case StyleAppearance::Textarea:
1357 case StyleAppearance::Textfield:
1358 case StyleAppearance::NumberInput:
1359 case StyleAppearance::Listbox:
1360 case StyleAppearance::MenulistButton:
1361 case StyleAppearance::Menulist:
1362 case StyleAppearance::Button:
1363 // 2px for each segment, plus 1px separation, but we paint 1px inside
1364 // the border area so 4px overflow.
1365 overflow.SizeTo(4, 4, 4, 4);
1366 break;
1367 default:
1368 return false;
1371 // TODO: This should convert from device pixels to app units, not from CSS
1372 // pixels. And it should take the dpi ratio into account.
1373 // Using CSS pixels can cause the overflow to be too small if the page is
1374 // zoomed out.
1375 aOverflowRect->Inflate(nsMargin(CSSPixel::ToAppUnits(overflow.top),
1376 CSSPixel::ToAppUnits(overflow.right),
1377 CSSPixel::ToAppUnits(overflow.bottom),
1378 CSSPixel::ToAppUnits(overflow.left)));
1380 return true;
1383 auto Theme::GetScrollbarSizes(nsPresContext* aPresContext,
1384 StyleScrollbarWidth aWidth, Overlay aOverlay)
1385 -> ScrollbarSizes {
1386 return GetScrollbarDrawing().GetScrollbarSizes(aPresContext, aWidth,
1387 aOverlay);
1390 nscoord Theme::GetCheckboxRadioPrefSize() {
1391 return CSSPixel::ToAppUnits(kCheckboxRadioContentBoxSize);
1394 /* static */
1395 UniquePtr<ScrollbarDrawing> Theme::ScrollbarStyle() {
1396 switch (StaticPrefs::widget_non_native_theme_scrollbar_style()) {
1397 case 1:
1398 return MakeUnique<ScrollbarDrawingCocoa>();
1399 case 2:
1400 return MakeUnique<ScrollbarDrawingGTK>();
1401 case 3:
1402 return MakeUnique<ScrollbarDrawingAndroid>();
1403 case 4:
1404 return MakeUnique<ScrollbarDrawingWin>();
1405 case 5:
1406 return MakeUnique<ScrollbarDrawingWin11>();
1407 default:
1408 break;
1410 // Default to native scrollbar style for each platform.
1411 #ifdef XP_WIN
1412 if (IsWin11OrLater()) {
1413 return MakeUnique<ScrollbarDrawingWin11>();
1415 return MakeUnique<ScrollbarDrawingWin>();
1416 #elif MOZ_WIDGET_COCOA
1417 return MakeUnique<ScrollbarDrawingCocoa>();
1418 #elif MOZ_WIDGET_GTK
1419 return MakeUnique<ScrollbarDrawingGTK>();
1420 #elif ANDROID
1421 return MakeUnique<ScrollbarDrawingAndroid>();
1422 #else
1423 # error "Unknown platform, need scrollbar implementation."
1424 #endif
1427 NS_IMETHODIMP
1428 Theme::GetMinimumWidgetSize(nsPresContext* aPresContext, nsIFrame* aFrame,
1429 StyleAppearance aAppearance,
1430 LayoutDeviceIntSize* aResult,
1431 bool* aIsOverridable) {
1432 DPIRatio dpiRatio = GetDPIRatio(aFrame, aAppearance);
1434 aResult->width = aResult->height = 0;
1435 *aIsOverridable = true;
1437 if (IsWidgetScrollbarPart(aAppearance)) {
1438 *aResult = GetScrollbarDrawing().GetMinimumWidgetSize(aPresContext,
1439 aAppearance, aFrame);
1440 return NS_OK;
1443 switch (aAppearance) {
1444 case StyleAppearance::Button:
1445 if (aFrame->IsColorControlFrame()) {
1446 aResult->height = (kMinimumColorPickerHeight * dpiRatio).Rounded();
1448 break;
1449 case StyleAppearance::RangeThumb:
1450 aResult->SizeTo((kMinimumRangeThumbSize * dpiRatio).Rounded(),
1451 (kMinimumRangeThumbSize * dpiRatio).Rounded());
1452 break;
1453 case StyleAppearance::MozMenulistArrowButton:
1454 aResult->width = (kMinimumDropdownArrowButtonWidth * dpiRatio).Rounded();
1455 break;
1456 case StyleAppearance::SpinnerUpbutton:
1457 case StyleAppearance::SpinnerDownbutton:
1458 aResult->width = (kMinimumSpinnerButtonWidth * dpiRatio).Rounded();
1459 aResult->height = (kMinimumSpinnerButtonHeight * dpiRatio).Rounded();
1460 break;
1461 default:
1462 break;
1465 return NS_OK;
1468 nsITheme::Transparency Theme::GetWidgetTransparency(
1469 nsIFrame* aFrame, StyleAppearance aAppearance) {
1470 return GetScrollbarDrawing()
1471 .GetScrollbarPartTransparency(aFrame, aAppearance)
1472 .valueOr(eUnknownTransparency);
1475 NS_IMETHODIMP
1476 Theme::WidgetStateChanged(nsIFrame* aFrame, StyleAppearance aAppearance,
1477 nsAtom* aAttribute, bool* aShouldRepaint,
1478 const nsAttrValue* aOldValue) {
1479 if (!aAttribute) {
1480 // Hover/focus/active changed. Always repaint.
1481 *aShouldRepaint = true;
1482 } else {
1483 // Check the attribute to see if it's relevant.
1484 // disabled, checked, dlgtype, default, etc.
1485 *aShouldRepaint = false;
1486 if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
1487 aAttribute == nsGkAtoms::selected ||
1488 aAttribute == nsGkAtoms::visuallyselected ||
1489 aAttribute == nsGkAtoms::menuactive ||
1490 aAttribute == nsGkAtoms::sortDirection ||
1491 aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::_default ||
1492 aAttribute == nsGkAtoms::open || aAttribute == nsGkAtoms::hover) {
1493 *aShouldRepaint = true;
1497 return NS_OK;
1500 NS_IMETHODIMP
1501 Theme::ThemeChanged() { return NS_OK; }
1503 bool Theme::WidgetAppearanceDependsOnWindowFocus(StyleAppearance aAppearance) {
1504 return IsWidgetScrollbarPart(aAppearance);
1507 nsITheme::ThemeGeometryType Theme::ThemeGeometryTypeForWidget(
1508 nsIFrame* aFrame, StyleAppearance aAppearance) {
1509 return eThemeGeometryTypeUnknown;
1512 bool Theme::ThemeSupportsWidget(nsPresContext* aPresContext, nsIFrame* aFrame,
1513 StyleAppearance aAppearance) {
1514 switch (aAppearance) {
1515 case StyleAppearance::Radio:
1516 case StyleAppearance::Checkbox:
1517 case StyleAppearance::FocusOutline:
1518 case StyleAppearance::Textarea:
1519 case StyleAppearance::Textfield:
1520 case StyleAppearance::Range:
1521 case StyleAppearance::RangeThumb:
1522 case StyleAppearance::ProgressBar:
1523 case StyleAppearance::Progresschunk:
1524 case StyleAppearance::Meter:
1525 case StyleAppearance::Meterchunk:
1526 case StyleAppearance::ScrollbarbuttonUp:
1527 case StyleAppearance::ScrollbarbuttonDown:
1528 case StyleAppearance::ScrollbarbuttonLeft:
1529 case StyleAppearance::ScrollbarbuttonRight:
1530 case StyleAppearance::ScrollbarthumbHorizontal:
1531 case StyleAppearance::ScrollbarthumbVertical:
1532 case StyleAppearance::ScrollbartrackHorizontal:
1533 case StyleAppearance::ScrollbartrackVertical:
1534 case StyleAppearance::ScrollbarHorizontal:
1535 case StyleAppearance::ScrollbarVertical:
1536 case StyleAppearance::Scrollcorner:
1537 case StyleAppearance::Button:
1538 case StyleAppearance::Listbox:
1539 case StyleAppearance::Menulist:
1540 case StyleAppearance::MenulistButton:
1541 case StyleAppearance::NumberInput:
1542 case StyleAppearance::MozMenulistArrowButton:
1543 case StyleAppearance::SpinnerUpbutton:
1544 case StyleAppearance::SpinnerDownbutton:
1545 return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
1546 default:
1547 return false;
1551 bool Theme::WidgetIsContainer(StyleAppearance aAppearance) {
1552 switch (aAppearance) {
1553 case StyleAppearance::MozMenulistArrowButton:
1554 case StyleAppearance::Radio:
1555 case StyleAppearance::Checkbox:
1556 return false;
1557 default:
1558 return true;
1562 bool Theme::ThemeDrawsFocusForWidget(nsIFrame*, StyleAppearance) {
1563 return true;
1566 bool Theme::ThemeNeedsComboboxDropmarker() { return true; }
1568 bool Theme::ThemeSupportsScrollbarButtons() {
1569 return GetScrollbarDrawing().ShouldDrawScrollbarButtons();
1572 } // namespace mozilla::widget