summaryrefslogtreecommitdiffstats
path: root/widget/gtk/nsNativeThemeGTK.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--widget/gtk/nsNativeThemeGTK.cpp1611
1 files changed, 1611 insertions, 0 deletions
diff --git a/widget/gtk/nsNativeThemeGTK.cpp b/widget/gtk/nsNativeThemeGTK.cpp
new file mode 100644
index 0000000000..28d51205e5
--- /dev/null
+++ b/widget/gtk/nsNativeThemeGTK.cpp
@@ -0,0 +1,1611 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsNativeThemeGTK.h"
+#include "nsStyleConsts.h"
+#include "gtkdrawing.h"
+#include "ScreenHelperGTK.h"
+#include "WidgetUtilsGtk.h"
+
+#include "gfx2DGlue.h"
+#include "nsIObserverService.h"
+#include "nsIFrame.h"
+#include "nsIContent.h"
+#include "nsViewManager.h"
+#include "nsNameSpaceManager.h"
+#include "nsGfxCIID.h"
+#include "nsTransform2D.h"
+#include "nsXULPopupManager.h"
+#include "tree/nsTreeBodyFrame.h"
+#include "prlink.h"
+#include "nsGkAtoms.h"
+#include "nsAttrValueInlines.h"
+
+#include "mozilla/dom/HTMLInputElement.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Services.h"
+
+#include <gdk/gdkprivate.h>
+#include <gtk/gtk.h>
+
+#include "gfxContext.h"
+#include "mozilla/dom/XULButtonElement.h"
+#include "mozilla/gfx/BorrowedContext.h"
+#include "mozilla/gfx/HelpersCairo.h"
+#include "mozilla/gfx/PathHelpers.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/layers/StackingContextHelper.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/StaticPrefs_widget.h"
+#include "nsWindow.h"
+#include "nsLayoutUtils.h"
+#include "Theme.h"
+
+#ifdef MOZ_X11
+# ifdef CAIRO_HAS_XLIB_SURFACE
+# include "cairo-xlib.h"
+# endif
+#endif
+
+#include <algorithm>
+#include <dlfcn.h>
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::widget;
+
+static int gLastGdkError;
+
+// Return scale factor of the monitor where the window is located
+// by the most part or layout.css.devPixelsPerPx pref if set to > 0.
+static inline gint GetMonitorScaleFactor(nsPresContext* aPresContext) {
+ nsCOMPtr<nsIWidget> rootWidget = aPresContext->GetRootWidget();
+ auto scale = rootWidget ? rootWidget->GetDefaultScale()
+ : aPresContext->CSSToDevPixelScale();
+ // We prefer the root widget scale since it doesn't account for text scale
+ // factor, this is the same scrollbars do in GTK.
+ int monitorScale = int(round(scale.scale));
+ // When the layout.css.devPixelsPerPx is set the scale can be < 1, the real
+ // monitor scale cannot go under 1.
+ return std::max(1, monitorScale);
+}
+
+static inline gint GetMonitorScaleFactor(nsIFrame* aFrame) {
+ return GetMonitorScaleFactor(aFrame->PresContext());
+}
+
+nsNativeThemeGTK::nsNativeThemeGTK() : Theme(ScrollbarStyle()) {
+ if (moz_gtk_init() != MOZ_GTK_SUCCESS) {
+ memset(mDisabledWidgetTypes, 0xff, sizeof(mDisabledWidgetTypes));
+ return;
+ }
+
+ ThemeChanged();
+}
+
+nsNativeThemeGTK::~nsNativeThemeGTK() { moz_gtk_shutdown(); }
+
+void nsNativeThemeGTK::RefreshWidgetWindow(nsIFrame* aFrame) {
+ MOZ_ASSERT(aFrame);
+ MOZ_ASSERT(aFrame->PresShell());
+
+ nsViewManager* vm = aFrame->PresShell()->GetViewManager();
+ if (!vm) {
+ return;
+ }
+ vm->InvalidateAllViews();
+}
+
+static bool IsFrameContentNodeInNamespace(nsIFrame* aFrame,
+ uint32_t aNamespace) {
+ nsIContent* content = aFrame ? aFrame->GetContent() : nullptr;
+ if (!content) return false;
+ return content->IsInNamespace(aNamespace);
+}
+
+static bool IsWidgetTypeDisabled(const uint8_t* aDisabledVector,
+ StyleAppearance aAppearance) {
+ auto type = static_cast<size_t>(aAppearance);
+ MOZ_ASSERT(type < static_cast<size_t>(StyleAppearance::Count));
+ return (aDisabledVector[type >> 3] & (1 << (type & 7))) != 0;
+}
+
+static void SetWidgetTypeDisabled(uint8_t* aDisabledVector,
+ StyleAppearance aAppearance) {
+ auto type = static_cast<size_t>(aAppearance);
+ MOZ_ASSERT(type < static_cast<size_t>(mozilla::StyleAppearance::Count));
+ aDisabledVector[type >> 3] |= (1 << (type & 7));
+}
+
+static inline uint16_t GetWidgetStateKey(StyleAppearance aAppearance,
+ GtkWidgetState* aWidgetState) {
+ return (aWidgetState->active | aWidgetState->focused << 1 |
+ aWidgetState->inHover << 2 | aWidgetState->disabled << 3 |
+ aWidgetState->isDefault << 4 |
+ static_cast<uint16_t>(aAppearance) << 5);
+}
+
+static bool IsWidgetStateSafe(uint8_t* aSafeVector, StyleAppearance aAppearance,
+ GtkWidgetState* aWidgetState) {
+ MOZ_ASSERT(static_cast<size_t>(aAppearance) <
+ static_cast<size_t>(mozilla::StyleAppearance::Count));
+ uint16_t key = GetWidgetStateKey(aAppearance, aWidgetState);
+ return (aSafeVector[key >> 3] & (1 << (key & 7))) != 0;
+}
+
+static void SetWidgetStateSafe(uint8_t* aSafeVector,
+ StyleAppearance aAppearance,
+ GtkWidgetState* aWidgetState) {
+ MOZ_ASSERT(static_cast<size_t>(aAppearance) <
+ static_cast<size_t>(mozilla::StyleAppearance::Count));
+ uint16_t key = GetWidgetStateKey(aAppearance, aWidgetState);
+ aSafeVector[key >> 3] |= (1 << (key & 7));
+}
+
+/* static */
+GtkTextDirection nsNativeThemeGTK::GetTextDirection(nsIFrame* aFrame) {
+ // IsFrameRTL() treats vertical-rl modes as right-to-left (in addition to
+ // horizontal text with direction=RTL), rather than just considering the
+ // text direction. GtkTextDirection does not have distinct values for
+ // vertical writing modes, but considering the block flow direction is
+ // important for resizers and scrollbar elements, at least.
+ return IsFrameRTL(aFrame) ? GTK_TEXT_DIR_RTL : GTK_TEXT_DIR_LTR;
+}
+
+// Returns positive for negative margins (otherwise 0).
+gint nsNativeThemeGTK::GetTabMarginPixels(nsIFrame* aFrame) {
+ nscoord margin = IsBottomTab(aFrame) ? aFrame->GetUsedMargin().top
+ : aFrame->GetUsedMargin().bottom;
+
+ return std::min<gint>(
+ MOZ_GTK_TAB_MARGIN_MASK,
+ std::max(0, aFrame->PresContext()->AppUnitsToDevPixels(-margin)));
+}
+
+bool nsNativeThemeGTK::GetGtkWidgetAndState(StyleAppearance aAppearance,
+ nsIFrame* aFrame,
+ WidgetNodeType& aGtkWidgetType,
+ GtkWidgetState* aState,
+ gint* aWidgetFlags) {
+ if (aWidgetFlags) {
+ *aWidgetFlags = 0;
+ }
+
+ ElementState elementState = GetContentState(aFrame, aAppearance);
+ if (aState) {
+ memset(aState, 0, sizeof(GtkWidgetState));
+
+ // For XUL checkboxes and radio buttons, the state of the parent
+ // determines our state.
+ if (aWidgetFlags) {
+ if (elementState.HasState(ElementState::CHECKED)) {
+ *aWidgetFlags |= MOZ_GTK_WIDGET_CHECKED;
+ }
+ if (elementState.HasState(ElementState::INDETERMINATE)) {
+ *aWidgetFlags |= MOZ_GTK_WIDGET_INCONSISTENT;
+ }
+ }
+
+ aState->disabled =
+ elementState.HasState(ElementState::DISABLED) || IsReadOnly(aFrame);
+ aState->active = elementState.HasState(ElementState::ACTIVE);
+ aState->focused = elementState.HasState(ElementState::FOCUS);
+ aState->inHover = elementState.HasState(ElementState::HOVER);
+ aState->isDefault = IsDefaultButton(aFrame);
+ aState->canDefault = FALSE; // XXX fix me
+
+ if (aAppearance == StyleAppearance::FocusOutline) {
+ aState->disabled = FALSE;
+ aState->active = FALSE;
+ aState->inHover = FALSE;
+ aState->isDefault = FALSE;
+ aState->canDefault = FALSE;
+
+ aState->focused = TRUE;
+ aState->depressed = TRUE; // see moz_gtk_entry_paint()
+ } else if (aAppearance == StyleAppearance::Button ||
+ aAppearance == StyleAppearance::Toolbarbutton ||
+ aAppearance == StyleAppearance::Dualbutton ||
+ aAppearance == StyleAppearance::ToolbarbuttonDropdown ||
+ aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton ||
+ aAppearance == StyleAppearance::MozMenulistArrowButton) {
+ aState->active &= aState->inHover;
+ } else if (aAppearance == StyleAppearance::Treetwisty ||
+ aAppearance == StyleAppearance::Treetwistyopen) {
+ nsTreeBodyFrame* treeBodyFrame = do_QueryFrame(aFrame);
+ if (treeBodyFrame) {
+ const mozilla::AtomArray& atoms =
+ treeBodyFrame->GetPropertyArrayForCurrentDrawingItem();
+ aState->selected = atoms.Contains((nsStaticAtom*)nsGkAtoms::selected);
+ aState->inHover = atoms.Contains((nsStaticAtom*)nsGkAtoms::hover);
+ }
+ }
+
+ if (IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL)) {
+ // For these widget types, some element (either a child or parent)
+ // actually has element focus, so we check the focused attribute
+ // to see whether to draw in the focused state.
+ aState->focused = elementState.HasState(ElementState::FOCUSRING);
+ if (aAppearance == StyleAppearance::Radio ||
+ aAppearance == StyleAppearance::Checkbox) {
+ // In XUL, checkboxes and radios shouldn't have focus rings, their
+ // labels do
+ aState->focused = FALSE;
+ }
+
+ // menu item state is determined by the attribute "_moz-menuactive",
+ // and not by the mouse hovering (accessibility). as a special case,
+ // menus which are children of a menu bar are only marked as prelight
+ // if they are open, not on normal hover.
+
+ if (aAppearance == StyleAppearance::Menuitem ||
+ aAppearance == StyleAppearance::Checkmenuitem ||
+ aAppearance == StyleAppearance::Radiomenuitem ||
+ aAppearance == StyleAppearance::Menuseparator ||
+ aAppearance == StyleAppearance::Menuarrow) {
+ auto* item = dom::XULButtonElement::FromNode(aFrame->GetContent());
+ if (item && item->IsOnMenuBar()) {
+ aState->inHover = CheckBooleanAttr(aFrame, nsGkAtoms::open);
+ } else {
+ aState->inHover = CheckBooleanAttr(aFrame, nsGkAtoms::menuactive);
+ }
+
+ aState->active = FALSE;
+
+ if (aAppearance == StyleAppearance::Checkmenuitem ||
+ aAppearance == StyleAppearance::Radiomenuitem) {
+ *aWidgetFlags = 0;
+ if (aFrame && aFrame->GetContent() &&
+ aFrame->GetContent()->IsElement()) {
+ *aWidgetFlags = aFrame->GetContent()->AsElement()->AttrValueIs(
+ kNameSpaceID_None, nsGkAtoms::checked, nsGkAtoms::_true,
+ eIgnoreCase);
+ }
+ }
+ }
+
+ // A button with drop down menu open or an activated toggle button
+ // should always appear depressed.
+ if (aAppearance == StyleAppearance::Button ||
+ aAppearance == StyleAppearance::Toolbarbutton ||
+ aAppearance == StyleAppearance::Dualbutton ||
+ aAppearance == StyleAppearance::ToolbarbuttonDropdown ||
+ aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton ||
+ aAppearance == StyleAppearance::MozMenulistArrowButton) {
+ bool menuOpen = IsOpenButton(aFrame);
+ aState->depressed = IsCheckedButton(aFrame) || menuOpen;
+ // we must not highlight buttons with open drop down menus on hover.
+ aState->inHover = aState->inHover && !menuOpen;
+ }
+
+ // When the input field of the drop down button has focus, some themes
+ // should draw focus for the drop down button as well.
+ if ((aAppearance == StyleAppearance::MenulistButton ||
+ aAppearance == StyleAppearance::MozMenulistArrowButton) &&
+ aWidgetFlags) {
+ *aWidgetFlags = CheckBooleanAttr(aFrame, nsGkAtoms::parentfocused);
+ }
+ }
+
+ if (aAppearance == StyleAppearance::MozWindowTitlebar ||
+ aAppearance == StyleAppearance::MozWindowTitlebarMaximized ||
+ aAppearance == StyleAppearance::MozWindowButtonClose ||
+ aAppearance == StyleAppearance::MozWindowButtonMinimize ||
+ aAppearance == StyleAppearance::MozWindowButtonMaximize ||
+ aAppearance == StyleAppearance::MozWindowButtonRestore) {
+ aState->backdrop = !nsWindow::GetTopLevelWindowActiveState(aFrame);
+ }
+ }
+
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ if (aWidgetFlags) *aWidgetFlags = GTK_RELIEF_NORMAL;
+ aGtkWidgetType = MOZ_GTK_BUTTON;
+ break;
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Dualbutton:
+ if (aWidgetFlags) *aWidgetFlags = GTK_RELIEF_NONE;
+ aGtkWidgetType = MOZ_GTK_TOOLBAR_BUTTON;
+ break;
+ case StyleAppearance::FocusOutline:
+ aGtkWidgetType = MOZ_GTK_ENTRY;
+ break;
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ aGtkWidgetType = (aAppearance == StyleAppearance::Radio)
+ ? MOZ_GTK_RADIOBUTTON
+ : MOZ_GTK_CHECKBUTTON;
+ break;
+ case StyleAppearance::Spinner:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON;
+ break;
+ case StyleAppearance::SpinnerUpbutton:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON_UP;
+ break;
+ case StyleAppearance::SpinnerDownbutton:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON_DOWN;
+ break;
+ case StyleAppearance::SpinnerTextfield:
+ aGtkWidgetType = MOZ_GTK_SPINBUTTON_ENTRY;
+ break;
+ case StyleAppearance::Range: {
+ if (IsRangeHorizontal(aFrame)) {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_HORIZONTAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_HORIZONTAL;
+ } else {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_VERTICAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_VERTICAL;
+ }
+ break;
+ }
+ case StyleAppearance::RangeThumb: {
+ if (IsRangeHorizontal(aFrame)) {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_HORIZONTAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_THUMB_HORIZONTAL;
+ } else {
+ if (aWidgetFlags) *aWidgetFlags = GTK_ORIENTATION_VERTICAL;
+ aGtkWidgetType = MOZ_GTK_SCALE_THUMB_VERTICAL;
+ }
+ break;
+ }
+ case StyleAppearance::Separator:
+ aGtkWidgetType = MOZ_GTK_TOOLBAR_SEPARATOR;
+ break;
+ case StyleAppearance::Toolbargripper:
+ aGtkWidgetType = MOZ_GTK_GRIPPER;
+ break;
+ case StyleAppearance::Resizer:
+ aGtkWidgetType = MOZ_GTK_RESIZER;
+ break;
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ aGtkWidgetType = MOZ_GTK_ENTRY;
+ break;
+ case StyleAppearance::Textarea:
+ aGtkWidgetType = MOZ_GTK_TEXT_VIEW;
+ break;
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ aGtkWidgetType = MOZ_GTK_TREEVIEW;
+ break;
+ case StyleAppearance::Treeheadercell:
+ if (aWidgetFlags) {
+ // In this case, the flag denotes whether the header is the sorted one
+ // or not
+ if (GetTreeSortDirection(aFrame) == eTreeSortDirection_Natural)
+ *aWidgetFlags = false;
+ else
+ *aWidgetFlags = true;
+ }
+ aGtkWidgetType = MOZ_GTK_TREE_HEADER_CELL;
+ break;
+ case StyleAppearance::Treeheadersortarrow:
+ if (aWidgetFlags) {
+ switch (GetTreeSortDirection(aFrame)) {
+ case eTreeSortDirection_Ascending:
+ *aWidgetFlags = GTK_ARROW_DOWN;
+ break;
+ case eTreeSortDirection_Descending:
+ *aWidgetFlags = GTK_ARROW_UP;
+ break;
+ case eTreeSortDirection_Natural:
+ default:
+ /* This prevents the treecolums from getting smaller
+ * and wider when switching sort direction off and on
+ * */
+ *aWidgetFlags = GTK_ARROW_NONE;
+ break;
+ }
+ }
+ aGtkWidgetType = MOZ_GTK_TREE_HEADER_SORTARROW;
+ break;
+ case StyleAppearance::Treetwisty:
+ aGtkWidgetType = MOZ_GTK_TREEVIEW_EXPANDER;
+ if (aWidgetFlags) *aWidgetFlags = GTK_EXPANDER_COLLAPSED;
+ break;
+ case StyleAppearance::Treetwistyopen:
+ aGtkWidgetType = MOZ_GTK_TREEVIEW_EXPANDER;
+ if (aWidgetFlags) *aWidgetFlags = GTK_EXPANDER_EXPANDED;
+ break;
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Menulist:
+ aGtkWidgetType = MOZ_GTK_DROPDOWN;
+ if (aWidgetFlags)
+ *aWidgetFlags =
+ IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XHTML);
+ break;
+ case StyleAppearance::MenulistText:
+ return false; // nothing to do, but prevents the bg from being drawn
+ case StyleAppearance::MozMenulistArrowButton:
+ aGtkWidgetType = MOZ_GTK_DROPDOWN_ARROW;
+ break;
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ aGtkWidgetType = MOZ_GTK_TOOLBARBUTTON_ARROW;
+ if (aWidgetFlags) {
+ *aWidgetFlags = GTK_ARROW_DOWN;
+
+ if (aAppearance == StyleAppearance::ButtonArrowUp)
+ *aWidgetFlags = GTK_ARROW_UP;
+ else if (aAppearance == StyleAppearance::ButtonArrowNext)
+ *aWidgetFlags = GTK_ARROW_RIGHT;
+ else if (aAppearance == StyleAppearance::ButtonArrowPrevious)
+ *aWidgetFlags = GTK_ARROW_LEFT;
+ }
+ break;
+ case StyleAppearance::CheckboxContainer:
+ aGtkWidgetType = MOZ_GTK_CHECKBUTTON_CONTAINER;
+ break;
+ case StyleAppearance::RadioContainer:
+ aGtkWidgetType = MOZ_GTK_RADIOBUTTON_CONTAINER;
+ break;
+ case StyleAppearance::CheckboxLabel:
+ aGtkWidgetType = MOZ_GTK_CHECKBUTTON_LABEL;
+ break;
+ case StyleAppearance::RadioLabel:
+ aGtkWidgetType = MOZ_GTK_RADIOBUTTON_LABEL;
+ break;
+ case StyleAppearance::Toolbar:
+ aGtkWidgetType = MOZ_GTK_TOOLBAR;
+ break;
+ case StyleAppearance::Tooltip:
+ aGtkWidgetType = MOZ_GTK_TOOLTIP;
+ break;
+ case StyleAppearance::Statusbarpanel:
+ case StyleAppearance::Resizerpanel:
+ aGtkWidgetType = MOZ_GTK_FRAME;
+ break;
+ case StyleAppearance::ProgressBar:
+ aGtkWidgetType = MOZ_GTK_PROGRESSBAR;
+ break;
+ case StyleAppearance::Progresschunk: {
+ nsIFrame* stateFrame = aFrame->GetParent();
+ ElementState elementState = GetContentState(stateFrame, aAppearance);
+
+ aGtkWidgetType = elementState.HasState(ElementState::INDETERMINATE)
+ ? IsVerticalProgress(stateFrame)
+ ? MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE
+ : MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE
+ : MOZ_GTK_PROGRESS_CHUNK;
+ } break;
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ if (aWidgetFlags)
+ *aWidgetFlags = aAppearance == StyleAppearance::TabScrollArrowBack
+ ? GTK_ARROW_LEFT
+ : GTK_ARROW_RIGHT;
+ aGtkWidgetType = MOZ_GTK_TAB_SCROLLARROW;
+ break;
+ case StyleAppearance::Tabpanels:
+ aGtkWidgetType = MOZ_GTK_TABPANELS;
+ break;
+ case StyleAppearance::Tab: {
+ if (IsBottomTab(aFrame)) {
+ aGtkWidgetType = MOZ_GTK_TAB_BOTTOM;
+ } else {
+ aGtkWidgetType = MOZ_GTK_TAB_TOP;
+ }
+
+ if (aWidgetFlags) {
+ /* First bits will be used to store max(0,-bmargin) where bmargin
+ * is the bottom margin of the tab in pixels (resp. top margin,
+ * for bottom tabs). */
+ *aWidgetFlags = GetTabMarginPixels(aFrame);
+
+ if (IsSelectedTab(aFrame)) *aWidgetFlags |= MOZ_GTK_TAB_SELECTED;
+
+ if (IsFirstTab(aFrame)) *aWidgetFlags |= MOZ_GTK_TAB_FIRST;
+ }
+ } break;
+ case StyleAppearance::Splitter:
+ if (IsHorizontal(aFrame))
+ aGtkWidgetType = MOZ_GTK_SPLITTER_VERTICAL;
+ else
+ aGtkWidgetType = MOZ_GTK_SPLITTER_HORIZONTAL;
+ break;
+ case StyleAppearance::Menubar:
+ aGtkWidgetType = MOZ_GTK_MENUBAR;
+ break;
+ case StyleAppearance::Menuitem: {
+ auto* item = dom::XULButtonElement::FromNode(aFrame->GetContent());
+ if (item && item->IsOnMenuBar()) {
+ aGtkWidgetType = MOZ_GTK_MENUBARITEM;
+ break;
+ }
+ }
+ aGtkWidgetType = MOZ_GTK_MENUITEM;
+ break;
+ case StyleAppearance::Menuseparator:
+ aGtkWidgetType = MOZ_GTK_MENUSEPARATOR;
+ break;
+ case StyleAppearance::Menuarrow:
+ aGtkWidgetType = MOZ_GTK_MENUARROW;
+ break;
+ case StyleAppearance::Checkmenuitem:
+ aGtkWidgetType = MOZ_GTK_CHECKMENUITEM;
+ break;
+ case StyleAppearance::Radiomenuitem:
+ aGtkWidgetType = MOZ_GTK_RADIOMENUITEM;
+ break;
+ case StyleAppearance::MozWindowTitlebar:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR;
+ break;
+ case StyleAppearance::MozWindowTitlebarMaximized:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_MAXIMIZED;
+ break;
+ case StyleAppearance::MozWindowButtonBox:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_BOX;
+ break;
+ case StyleAppearance::MozWindowButtonClose:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_CLOSE;
+ break;
+ case StyleAppearance::MozWindowButtonMinimize:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE;
+ break;
+ case StyleAppearance::MozWindowButtonMaximize:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE;
+ break;
+ case StyleAppearance::MozWindowButtonRestore:
+ aGtkWidgetType = MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE_RESTORE;
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+class SystemCairoClipper : public ClipExporter {
+ public:
+ explicit SystemCairoClipper(cairo_t* aContext, gint aScaleFactor = 1)
+ : mContext(aContext), mScaleFactor(aScaleFactor) {}
+
+ void BeginClip(const Matrix& aTransform) override {
+ cairo_matrix_t mat;
+ GfxMatrixToCairoMatrix(aTransform, mat);
+ // We also need to remove the scale factor effect from the matrix
+ mat.y0 = mat.y0 / mScaleFactor;
+ mat.x0 = mat.x0 / mScaleFactor;
+ cairo_set_matrix(mContext, &mat);
+
+ cairo_new_path(mContext);
+ }
+
+ void MoveTo(const Point& aPoint) override {
+ cairo_move_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor);
+ mBeginPoint = aPoint;
+ mCurrentPoint = aPoint;
+ }
+
+ void LineTo(const Point& aPoint) override {
+ cairo_line_to(mContext, aPoint.x / mScaleFactor, aPoint.y / mScaleFactor);
+ mCurrentPoint = aPoint;
+ }
+
+ void BezierTo(const Point& aCP1, const Point& aCP2,
+ const Point& aCP3) override {
+ cairo_curve_to(mContext, aCP1.x / mScaleFactor, aCP1.y / mScaleFactor,
+ aCP2.x / mScaleFactor, aCP2.y / mScaleFactor,
+ aCP3.x / mScaleFactor, aCP3.y / mScaleFactor);
+ mCurrentPoint = aCP3;
+ }
+
+ void QuadraticBezierTo(const Point& aCP1, const Point& aCP2) override {
+ Point CP0 = CurrentPoint();
+ Point CP1 = (CP0 + aCP1 * 2.0) / 3.0;
+ Point CP2 = (aCP2 + aCP1 * 2.0) / 3.0;
+ Point CP3 = aCP2;
+ cairo_curve_to(mContext, CP1.x / mScaleFactor, CP1.y / mScaleFactor,
+ CP2.x / mScaleFactor, CP2.y / mScaleFactor,
+ CP3.x / mScaleFactor, CP3.y / mScaleFactor);
+ mCurrentPoint = aCP2;
+ }
+
+ void Arc(const Point& aOrigin, float aRadius, float aStartAngle,
+ float aEndAngle, bool aAntiClockwise) override {
+ ArcToBezier(this, aOrigin, Size(aRadius, aRadius), aStartAngle, aEndAngle,
+ aAntiClockwise);
+ }
+
+ void Close() override {
+ cairo_close_path(mContext);
+ mCurrentPoint = mBeginPoint;
+ }
+
+ void EndClip() override { cairo_clip(mContext); }
+
+ private:
+ cairo_t* mContext;
+ gint mScaleFactor;
+};
+
+static void DrawThemeWithCairo(gfxContext* aContext, DrawTarget* aDrawTarget,
+ GtkWidgetState aState,
+ WidgetNodeType aGTKWidgetType, gint aFlags,
+ GtkTextDirection aDirection, gint aScaleFactor,
+ bool aSnapped, const Point& aDrawOrigin,
+ const nsIntSize& aDrawSize,
+ GdkRectangle& aGDKRect,
+ nsITheme::Transparency aTransparency) {
+ static auto sCairoSurfaceSetDeviceScalePtr =
+ (void (*)(cairo_surface_t*, double, double))dlsym(
+ RTLD_DEFAULT, "cairo_surface_set_device_scale");
+ const bool useHiDPIWidgets =
+ aScaleFactor != 1 && sCairoSurfaceSetDeviceScalePtr;
+
+ Point drawOffsetScaled;
+ Point drawOffsetOriginal;
+ Matrix transform;
+ if (!aSnapped) {
+ // If we are not snapped, we depend on the DT for translation.
+ drawOffsetOriginal = aDrawOrigin;
+ drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor
+ : drawOffsetOriginal;
+ transform = aDrawTarget->GetTransform().PreTranslate(drawOffsetScaled);
+ } else {
+ // Otherwise, we only need to take the device offset into account.
+ drawOffsetOriginal = aDrawOrigin - aContext->GetDeviceOffset();
+ drawOffsetScaled = useHiDPIWidgets ? drawOffsetOriginal / aScaleFactor
+ : drawOffsetOriginal;
+ transform = Matrix::Translation(drawOffsetScaled);
+ }
+
+ if (!useHiDPIWidgets && aScaleFactor != 1) {
+ transform.PreScale(aScaleFactor, aScaleFactor);
+ }
+
+ cairo_matrix_t mat;
+ GfxMatrixToCairoMatrix(transform, mat);
+
+ nsIntSize clipSize((aDrawSize.width + aScaleFactor - 1) / aScaleFactor,
+ (aDrawSize.height + aScaleFactor - 1) / aScaleFactor);
+
+ // A direct Cairo draw target is not available, so we need to create a
+ // temporary one.
+#if defined(MOZ_X11) && defined(CAIRO_HAS_XLIB_SURFACE)
+ if (GdkIsX11Display()) {
+ // If using a Cairo xlib surface, then try to reuse it.
+ BorrowedXlibDrawable borrow(aDrawTarget);
+ if (borrow.GetDrawable()) {
+ nsIntSize size = borrow.GetSize();
+ cairo_surface_t* surf = cairo_xlib_surface_create(
+ borrow.GetDisplay(), borrow.GetDrawable(), borrow.GetVisual(),
+ size.width, size.height);
+ if (!NS_WARN_IF(!surf)) {
+ Point offset = borrow.GetOffset();
+ if (offset != Point()) {
+ cairo_surface_set_device_offset(surf, offset.x, offset.y);
+ }
+ cairo_t* cr = cairo_create(surf);
+ if (!NS_WARN_IF(!cr)) {
+ RefPtr<SystemCairoClipper> clipper = new SystemCairoClipper(cr);
+ aContext->ExportClip(*clipper);
+
+ cairo_set_matrix(cr, &mat);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height);
+ cairo_clip(cr);
+
+ moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags,
+ aDirection);
+
+ cairo_destroy(cr);
+ }
+ cairo_surface_destroy(surf);
+ }
+ borrow.Finish();
+ return;
+ }
+ }
+#endif
+
+ // Check if the widget requires complex masking that must be composited.
+ // Try to directly write to the draw target's pixels if possible.
+ uint8_t* data;
+ nsIntSize size;
+ int32_t stride;
+ SurfaceFormat format;
+ IntPoint origin;
+ if (aDrawTarget->LockBits(&data, &size, &stride, &format, &origin)) {
+ // Create a Cairo image surface context the device rectangle.
+ cairo_surface_t* surf = cairo_image_surface_create_for_data(
+ data, GfxFormatToCairoFormat(format), size.width, size.height, stride);
+ if (!NS_WARN_IF(!surf)) {
+ if (useHiDPIWidgets) {
+ sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor);
+ }
+ if (origin != IntPoint()) {
+ cairo_surface_set_device_offset(surf, -origin.x, -origin.y);
+ }
+ cairo_t* cr = cairo_create(surf);
+ if (!NS_WARN_IF(!cr)) {
+ RefPtr<SystemCairoClipper> clipper =
+ new SystemCairoClipper(cr, useHiDPIWidgets ? aScaleFactor : 1);
+ aContext->ExportClip(*clipper);
+
+ cairo_set_matrix(cr, &mat);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0, 0, clipSize.width, clipSize.height);
+ cairo_clip(cr);
+
+ moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags,
+ aDirection);
+
+ cairo_destroy(cr);
+ }
+ cairo_surface_destroy(surf);
+ }
+ aDrawTarget->ReleaseBits(data);
+ } else {
+ // If the widget has any transparency, make sure to choose an alpha format.
+ format = aTransparency != nsITheme::eOpaque ? SurfaceFormat::B8G8R8A8
+ : aDrawTarget->GetFormat();
+ // Create a temporary data surface to render the widget into.
+ RefPtr<DataSourceSurface> dataSurface = Factory::CreateDataSourceSurface(
+ aDrawSize, format, aTransparency != nsITheme::eOpaque);
+ DataSourceSurface::MappedSurface map;
+ if (!NS_WARN_IF(
+ !(dataSurface &&
+ dataSurface->Map(DataSourceSurface::MapType::WRITE, &map)))) {
+ // Create a Cairo image surface wrapping the data surface.
+ cairo_surface_t* surf = cairo_image_surface_create_for_data(
+ map.mData, GfxFormatToCairoFormat(format), aDrawSize.width,
+ aDrawSize.height, map.mStride);
+ cairo_t* cr = nullptr;
+ if (!NS_WARN_IF(!surf)) {
+ cr = cairo_create(surf);
+ if (!NS_WARN_IF(!cr)) {
+ if (aScaleFactor != 1) {
+ if (useHiDPIWidgets) {
+ sCairoSurfaceSetDeviceScalePtr(surf, aScaleFactor, aScaleFactor);
+ } else {
+ cairo_scale(cr, aScaleFactor, aScaleFactor);
+ }
+ }
+
+ moz_gtk_widget_paint(aGTKWidgetType, cr, &aGDKRect, &aState, aFlags,
+ aDirection);
+ }
+ }
+
+ // Unmap the surface before using it as a source
+ dataSurface->Unmap();
+
+ if (cr) {
+ // The widget either needs to be masked or has transparency, so use the
+ // slower drawing path.
+ aDrawTarget->DrawSurface(
+ dataSurface,
+ Rect(aSnapped ? drawOffsetOriginal -
+ aDrawTarget->GetTransform().GetTranslation()
+ : drawOffsetOriginal,
+ Size(aDrawSize)),
+ Rect(0, 0, aDrawSize.width, aDrawSize.height));
+ cairo_destroy(cr);
+ }
+
+ if (surf) {
+ cairo_surface_destroy(surf);
+ }
+ }
+ }
+}
+
+bool nsNativeThemeGTK::GetExtraSizeForWidget(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsIntMargin* aExtra) {
+ *aExtra = nsIntMargin(0, 0, 0, 0);
+ // Allow an extra one pixel above and below the thumb for certain
+ // GTK2 themes (Ximian Industrial, Bluecurve, Misty, at least);
+ // We modify the frame's overflow area. See bug 297508.
+ switch (aAppearance) {
+ case StyleAppearance::Button: {
+ if (IsDefaultButton(aFrame)) {
+ // Some themes draw a default indicator outside the widget,
+ // include that in overflow
+ gint top, left, bottom, right;
+ moz_gtk_button_get_default_overflow(&top, &left, &bottom, &right);
+ aExtra->top = top;
+ aExtra->right = right;
+ aExtra->bottom = bottom;
+ aExtra->left = left;
+ break;
+ }
+ return false;
+ }
+ case StyleAppearance::FocusOutline: {
+ moz_gtk_get_focus_outline_size(&aExtra->left, &aExtra->top);
+ aExtra->right = aExtra->left;
+ aExtra->bottom = aExtra->top;
+ break;
+ }
+ case StyleAppearance::Tab: {
+ if (!IsSelectedTab(aFrame)) return false;
+
+ gint gap_height = moz_gtk_get_tab_thickness(
+ IsBottomTab(aFrame) ? MOZ_GTK_TAB_BOTTOM : MOZ_GTK_TAB_TOP);
+ if (!gap_height) return false;
+
+ int32_t extra = gap_height - GetTabMarginPixels(aFrame);
+ if (extra <= 0) return false;
+
+ if (IsBottomTab(aFrame)) {
+ aExtra->top = extra;
+ } else {
+ aExtra->bottom = extra;
+ }
+ return false;
+ }
+ default:
+ return false;
+ }
+ gint scale = GetMonitorScaleFactor(aFrame);
+ aExtra->top *= scale;
+ aExtra->right *= scale;
+ aExtra->bottom *= scale;
+ aExtra->left *= scale;
+ return true;
+}
+
+bool nsNativeThemeGTK::IsWidgetVisible(StyleAppearance aAppearance) {
+ switch (aAppearance) {
+ case StyleAppearance::MozWindowButtonBox:
+ return false;
+ default:
+ break;
+ }
+ return true;
+}
+
+NS_IMETHODIMP
+nsNativeThemeGTK::DrawWidgetBackground(gfxContext* aContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ const nsRect& aRect,
+ const nsRect& aDirtyRect,
+ DrawOverflow aDrawOverflow) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::DrawWidgetBackground(aContext, aFrame, aAppearance, aRect,
+ aDirtyRect, aDrawOverflow);
+ }
+
+ GtkWidgetState state;
+ WidgetNodeType gtkWidgetType;
+ GtkTextDirection direction = GetTextDirection(aFrame);
+ gint flags;
+
+ if (!IsWidgetVisible(aAppearance) ||
+ !GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, &state,
+ &flags)) {
+ return NS_OK;
+ }
+
+ gfxContext* ctx = aContext;
+ nsPresContext* presContext = aFrame->PresContext();
+
+ gfxRect rect = presContext->AppUnitsToGfxUnits(aRect);
+ gfxRect dirtyRect = presContext->AppUnitsToGfxUnits(aDirtyRect);
+ gint scaleFactor = GetMonitorScaleFactor(aFrame);
+
+ // Align to device pixels where sensible
+ // to provide crisper and faster drawing.
+ // Don't snap if it's a non-unit scale factor. We're going to have to take
+ // slow paths then in any case.
+ // We prioritize the size when snapping in order to avoid distorting widgets
+ // that should be square, which can occur if edges are snapped independently.
+ bool snapped = ctx->UserToDevicePixelSnapped(
+ rect, gfxContext::SnapOption::PrioritizeSize);
+ if (snapped) {
+ // Leave rect in device coords but make dirtyRect consistent.
+ dirtyRect = ctx->UserToDevice(dirtyRect);
+ }
+
+ // Translate the dirty rect so that it is wrt the widget top-left.
+ dirtyRect.MoveBy(-rect.TopLeft());
+ // Round out the dirty rect to gdk pixels to ensure that gtk draws
+ // enough pixels for interpolation to device pixels.
+ dirtyRect.RoundOut();
+
+ // GTK themes can only draw an integer number of pixels
+ // (even when not snapped).
+ nsIntRect widgetRect(0, 0, NS_lround(rect.Width()), NS_lround(rect.Height()));
+ nsIntRect overflowRect(widgetRect);
+ nsIntMargin extraSize;
+ if (GetExtraSizeForWidget(aFrame, aAppearance, &extraSize)) {
+ overflowRect.Inflate(extraSize);
+ }
+
+ // This is the rectangle that will actually be drawn, in gdk pixels
+ nsIntRect drawingRect(int32_t(dirtyRect.X()), int32_t(dirtyRect.Y()),
+ int32_t(dirtyRect.Width()),
+ int32_t(dirtyRect.Height()));
+ if (widgetRect.IsEmpty() ||
+ !drawingRect.IntersectRect(overflowRect, drawingRect)) {
+ return NS_OK;
+ }
+
+ NS_ASSERTION(!IsWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance),
+ "Trying to render an unsafe widget!");
+
+ bool safeState = IsWidgetStateSafe(mSafeWidgetStates, aAppearance, &state);
+ if (!safeState) {
+ gLastGdkError = 0;
+ gdk_error_trap_push();
+ }
+
+ Transparency transparency = GetWidgetTransparency(aFrame, aAppearance);
+
+ // gdk rectangles are wrt the drawing rect.
+ GdkRectangle gdk_rect = {
+ -drawingRect.x / scaleFactor, -drawingRect.y / scaleFactor,
+ widgetRect.width / scaleFactor, widgetRect.height / scaleFactor};
+
+ // Save actual widget scale to GtkWidgetState as we don't provide
+ // nsFrame to gtk3drawing routines.
+ state.scale = scaleFactor;
+
+ // translate everything so (0,0) is the top left of the drawingRect
+ gfxPoint origin = rect.TopLeft() + drawingRect.TopLeft();
+
+ DrawThemeWithCairo(ctx, aContext->GetDrawTarget(), state, gtkWidgetType,
+ flags, direction, scaleFactor, snapped, ToPoint(origin),
+ drawingRect.Size(), gdk_rect, transparency);
+
+ if (!safeState) {
+ // gdk_flush() call from expose event crashes Gtk+ on Wayland
+ // (Gnome BZ #773307)
+ if (GdkIsX11Display()) {
+ gdk_flush();
+ }
+ gLastGdkError = gdk_error_trap_pop();
+
+ if (gLastGdkError) {
+#ifdef DEBUG
+ printf(
+ "GTK theme failed for widget type %d, error was %d, state was "
+ "[active=%d,focused=%d,inHover=%d,disabled=%d]\n",
+ static_cast<int>(aAppearance), gLastGdkError, state.active,
+ state.focused, state.inHover, state.disabled);
+#endif
+ NS_WARNING("GTK theme failed; disabling unsafe widget");
+ SetWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance);
+ // force refresh of the window, because the widget was not
+ // successfully drawn it must be redrawn using the default look
+ RefreshWidgetWindow(aFrame);
+ } else {
+ SetWidgetStateSafe(mSafeWidgetStates, aAppearance, &state);
+ }
+ }
+
+ // Indeterminate progress bar are animated.
+ if (gtkWidgetType == MOZ_GTK_PROGRESS_CHUNK_INDETERMINATE ||
+ gtkWidgetType == MOZ_GTK_PROGRESS_CHUNK_VERTICAL_INDETERMINATE) {
+ if (!QueueAnimatedContentForRefresh(aFrame->GetContent(), 30)) {
+ NS_WARNING("unable to animate widget!");
+ }
+ }
+
+ return NS_OK;
+}
+
+bool nsNativeThemeGTK::CreateWebRenderCommandsForWidget(
+ mozilla::wr::DisplayListBuilder& aBuilder,
+ mozilla::wr::IpcResourceUpdateQueue& aResources,
+ const mozilla::layers::StackingContextHelper& aSc,
+ mozilla::layers::RenderRootStateManager* aManager, nsIFrame* aFrame,
+ StyleAppearance aAppearance, const nsRect& aRect) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::CreateWebRenderCommandsForWidget(
+ aBuilder, aResources, aSc, aManager, aFrame, aAppearance, aRect);
+ }
+ return false;
+}
+
+WidgetNodeType nsNativeThemeGTK::NativeThemeToGtkTheme(
+ StyleAppearance aAppearance, nsIFrame* aFrame) {
+ WidgetNodeType gtkWidgetType;
+ gint unusedFlags;
+
+ if (!GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr,
+ &unusedFlags)) {
+ MOZ_ASSERT_UNREACHABLE("Unknown native widget to gtk widget mapping");
+ return MOZ_GTK_WINDOW;
+ }
+ return gtkWidgetType;
+}
+
+static void FixupForVerticalWritingMode(WritingMode aWritingMode,
+ LayoutDeviceIntMargin* aResult) {
+ if (aWritingMode.IsVertical()) {
+ bool rtl = aWritingMode.IsBidiRTL();
+ LogicalMargin logical(aWritingMode, aResult->top,
+ rtl ? aResult->left : aResult->right, aResult->bottom,
+ rtl ? aResult->right : aResult->left);
+ nsMargin physical = logical.GetPhysicalMargin(aWritingMode);
+ aResult->top = physical.top;
+ aResult->right = physical.right;
+ aResult->bottom = physical.bottom;
+ aResult->left = physical.left;
+ }
+}
+
+void nsNativeThemeGTK::GetCachedWidgetBorder(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ GtkTextDirection aDirection,
+ LayoutDeviceIntMargin* aResult) {
+ aResult->SizeTo(0, 0, 0, 0);
+
+ WidgetNodeType gtkWidgetType;
+ gint unusedFlags;
+ if (GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr,
+ &unusedFlags)) {
+ MOZ_ASSERT(0 <= gtkWidgetType && gtkWidgetType < MOZ_GTK_WIDGET_NODE_COUNT);
+ uint8_t cacheIndex = gtkWidgetType / 8;
+ uint8_t cacheBit = 1u << (gtkWidgetType % 8);
+
+ if (mBorderCacheValid[cacheIndex] & cacheBit) {
+ *aResult = mBorderCache[gtkWidgetType];
+ } else {
+ moz_gtk_get_widget_border(gtkWidgetType, &aResult->left, &aResult->top,
+ &aResult->right, &aResult->bottom, aDirection);
+ if (gtkWidgetType != MOZ_GTK_DROPDOWN) { // depends on aDirection
+ mBorderCacheValid[cacheIndex] |= cacheBit;
+ mBorderCache[gtkWidgetType] = *aResult;
+ }
+ }
+ }
+ FixupForVerticalWritingMode(aFrame->GetWritingMode(), aResult);
+}
+
+LayoutDeviceIntMargin nsNativeThemeGTK::GetWidgetBorder(
+ nsDeviceContext* aContext, nsIFrame* aFrame, StyleAppearance aAppearance) {
+ LayoutDeviceIntMargin result;
+ GtkTextDirection direction = GetTextDirection(aFrame);
+ switch (aAppearance) {
+ case StyleAppearance::Toolbox:
+ // gtk has no toolbox equivalent. So, although we map toolbox to
+ // gtk's 'toolbar' for purposes of painting the widget background,
+ // we don't use the toolbar border for toolbox.
+ break;
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Radiomenuitem:
+ // We use GetWidgetPadding for these.
+ break;
+ case StyleAppearance::Dualbutton:
+ // TOOLBAR_DUAL_BUTTON is an interesting case. We want a border to draw
+ // around the entire button + dropdown, and also an inner border if you're
+ // over the button part. But, we want the inner button to be right up
+ // against the edge of the outer button so that the borders overlap.
+ // To make this happen, we draw a button border for the outer button,
+ // but don't reserve any space for it.
+ break;
+ case StyleAppearance::Tab: {
+ WidgetNodeType gtkWidgetType;
+ gint flags;
+
+ if (!GetGtkWidgetAndState(aAppearance, aFrame, gtkWidgetType, nullptr,
+ &flags)) {
+ return result;
+ }
+ moz_gtk_get_tab_border(&result.left, &result.top, &result.right,
+ &result.bottom, direction, (GtkTabFlags)flags,
+ gtkWidgetType);
+ } break;
+ default: {
+ GetCachedWidgetBorder(aFrame, aAppearance, direction, &result);
+ }
+ }
+
+ gint scale = GetMonitorScaleFactor(aFrame);
+ result.top *= scale;
+ result.right *= scale;
+ result.bottom *= scale;
+ result.left *= scale;
+ return result;
+}
+
+bool nsNativeThemeGTK::GetWidgetPadding(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ LayoutDeviceIntMargin* aResult) {
+ switch (aAppearance) {
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::MozWindowButtonBox:
+ case StyleAppearance::MozWindowButtonClose:
+ case StyleAppearance::MozWindowButtonMinimize:
+ case StyleAppearance::MozWindowButtonMaximize:
+ case StyleAppearance::MozWindowButtonRestore:
+ case StyleAppearance::Dualbutton:
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ case StyleAppearance::MozMenulistArrowButton:
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ case StyleAppearance::RangeThumb:
+ // Radios and checkboxes return a fixed size in GetMinimumWidgetSize
+ // and have a meaningful baseline, so they can't have
+ // author-specified padding.
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio:
+ aResult->SizeTo(0, 0, 0, 0);
+ return true;
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Radiomenuitem: {
+ GetCachedWidgetBorder(aFrame, aAppearance, GetTextDirection(aFrame),
+ aResult);
+
+ gint horizontal_padding = 0;
+ if (aAppearance == StyleAppearance::Menuitem)
+ moz_gtk_menuitem_get_horizontal_padding(&horizontal_padding);
+ else
+ moz_gtk_checkmenuitem_get_horizontal_padding(&horizontal_padding);
+
+ aResult->left += horizontal_padding;
+ aResult->right += horizontal_padding;
+
+ gint scale = GetMonitorScaleFactor(aFrame);
+ aResult->top *= scale;
+ aResult->right *= scale;
+ aResult->bottom *= scale;
+ aResult->left *= scale;
+
+ return true;
+ }
+ default:
+ break;
+ }
+
+ return false;
+}
+
+bool nsNativeThemeGTK::GetWidgetOverflow(nsDeviceContext* aContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsRect* aOverflowRect) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::GetWidgetOverflow(aContext, aFrame, aAppearance,
+ aOverflowRect);
+ }
+
+ nsIntMargin extraSize;
+ if (!GetExtraSizeForWidget(aFrame, aAppearance, &extraSize)) {
+ return false;
+ }
+
+ int32_t p2a = aContext->AppUnitsPerDevPixel();
+ nsMargin m(NSIntPixelsToAppUnits(extraSize.top, p2a),
+ NSIntPixelsToAppUnits(extraSize.right, p2a),
+ NSIntPixelsToAppUnits(extraSize.bottom, p2a),
+ NSIntPixelsToAppUnits(extraSize.left, p2a));
+
+ aOverflowRect->Inflate(m);
+ return true;
+}
+
+auto nsNativeThemeGTK::IsWidgetNonNative(nsIFrame* aFrame,
+ StyleAppearance aAppearance)
+ -> NonNative {
+ if (IsWidgetScrollbarPart(aAppearance)) {
+ return NonNative::Always;
+ }
+ // We can't draw light widgets if the current GTK theme is dark or vice versa.
+ if (Theme::ThemeSupportsWidget(aFrame->PresContext(), aFrame, aAppearance) &&
+ LookAndFeel::ColorSchemeForFrame(aFrame) !=
+ LookAndFeel::ColorSchemeForChrome()) {
+ return NonNative::BecauseColorMismatch;
+ }
+ return NonNative::No;
+}
+
+LayoutDeviceIntSize nsNativeThemeGTK::GetMinimumWidgetSize(
+ nsPresContext* aPresContext, nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aFrame, aAppearance) == NonNative::Always) {
+ return Theme::GetMinimumWidgetSize(aPresContext, aFrame, aAppearance);
+ }
+
+ LayoutDeviceIntSize result;
+ switch (aAppearance) {
+ case StyleAppearance::Splitter: {
+ if (IsHorizontal(aFrame)) {
+ moz_gtk_splitter_get_metrics(GTK_ORIENTATION_HORIZONTAL, &result.width);
+ } else {
+ moz_gtk_splitter_get_metrics(GTK_ORIENTATION_VERTICAL, &result.height);
+ }
+ } break;
+ case StyleAppearance::RangeThumb: {
+ if (IsRangeHorizontal(aFrame)) {
+ moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_HORIZONTAL,
+ &result.width, &result.height);
+ } else {
+ moz_gtk_get_scalethumb_metrics(GTK_ORIENTATION_VERTICAL, &result.width,
+ &result.width);
+ }
+ } break;
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward: {
+ moz_gtk_get_tab_scroll_arrow_size(&result.width, &result.height);
+ } break;
+ case StyleAppearance::MozMenulistArrowButton: {
+ moz_gtk_get_combo_box_entry_button_size(&result.width, &result.height);
+ } break;
+ case StyleAppearance::Menuseparator: {
+ moz_gtk_get_menu_separator_height(&result.height);
+ } break;
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Radio: {
+ const ToggleGTKMetrics* metrics = GetToggleMetrics(
+ aAppearance == StyleAppearance::Radio ? MOZ_GTK_RADIOBUTTON
+ : MOZ_GTK_CHECKBUTTON);
+ result.width = metrics->minSizeWithBorder.width;
+ result.height = metrics->minSizeWithBorder.height;
+ } break;
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious: {
+ moz_gtk_get_arrow_size(MOZ_GTK_TOOLBARBUTTON_ARROW, &result.width,
+ &result.height);
+ } break;
+ case StyleAppearance::MozWindowButtonClose: {
+ const ToolbarButtonGTKMetrics* metrics =
+ GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_CLOSE);
+ result.width = metrics->minSizeWithBorderMargin.width;
+ result.height = metrics->minSizeWithBorderMargin.height;
+ break;
+ }
+ case StyleAppearance::MozWindowButtonMinimize: {
+ const ToolbarButtonGTKMetrics* metrics =
+ GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MINIMIZE);
+ result.width = metrics->minSizeWithBorderMargin.width;
+ result.height = metrics->minSizeWithBorderMargin.height;
+ break;
+ }
+ case StyleAppearance::MozWindowButtonMaximize:
+ case StyleAppearance::MozWindowButtonRestore: {
+ const ToolbarButtonGTKMetrics* metrics =
+ GetToolbarButtonMetrics(MOZ_GTK_HEADER_BAR_BUTTON_MAXIMIZE);
+ result.width = metrics->minSizeWithBorderMargin.width;
+ result.height = metrics->minSizeWithBorderMargin.height;
+ break;
+ }
+ case StyleAppearance::CheckboxContainer:
+ case StyleAppearance::RadioContainer:
+ case StyleAppearance::CheckboxLabel:
+ case StyleAppearance::RadioLabel:
+ case StyleAppearance::Button:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Treeheadercell: {
+ if (aAppearance == StyleAppearance::Menulist ||
+ aAppearance == StyleAppearance::MenulistButton) {
+ // Include the arrow size.
+ moz_gtk_get_arrow_size(MOZ_GTK_DROPDOWN, &result.width, &result.height);
+ }
+ // else the minimum size is missing consideration of container
+ // descendants; the value returned here will not be helpful, but the
+ // box model may consider border and padding with child minimum sizes.
+
+ LayoutDeviceIntMargin border;
+ GetCachedWidgetBorder(aFrame, aAppearance, GetTextDirection(aFrame),
+ &border);
+ result.width += border.LeftRight();
+ result.height += border.TopBottom();
+ } break;
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield: {
+ gint contentHeight = 0;
+ gint borderPaddingHeight = 0;
+ moz_gtk_get_entry_min_height(&contentHeight, &borderPaddingHeight);
+
+ // Scale the min content height proportionately with the font-size if it's
+ // smaller than the default one. This prevents <input type=text
+ // style="font-size: .5em"> from keeping a ridiculously large size, for
+ // example.
+ const gfxFloat fieldFontSizeInCSSPixels = [] {
+ gfxFontStyle fieldFontStyle;
+ nsAutoString unusedFontName;
+ DebugOnly<bool> result = LookAndFeel::GetFont(
+ LookAndFeel::FontID::MozField, unusedFontName, fieldFontStyle);
+ MOZ_ASSERT(result, "GTK look and feel supports the field font");
+ // NOTE: GetFont returns font sizes in CSS pixels, and we want just
+ // that.
+ return fieldFontStyle.size;
+ }();
+
+ const gfxFloat fontSize = aFrame->StyleFont()->mFont.size.ToCSSPixels();
+ if (fieldFontSizeInCSSPixels > fontSize) {
+ contentHeight =
+ std::round(contentHeight * fontSize / fieldFontSizeInCSSPixels);
+ }
+
+ gint height = contentHeight + borderPaddingHeight;
+ if (aFrame->GetWritingMode().IsVertical()) {
+ result.width = height;
+ } else {
+ result.height = height;
+ }
+ } break;
+ case StyleAppearance::Separator: {
+ moz_gtk_get_toolbar_separator_width(&result.width);
+ } break;
+ case StyleAppearance::Spinner:
+ // hard code these sizes
+ result.width = 14;
+ result.height = 26;
+ break;
+ case StyleAppearance::Treeheadersortarrow:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ // hard code these sizes
+ result.width = 14;
+ result.height = 13;
+ break;
+ case StyleAppearance::Resizer:
+ // same as Windows to make our lives easier
+ result.width = result.height = 15;
+ break;
+ case StyleAppearance::Treetwisty:
+ case StyleAppearance::Treetwistyopen: {
+ gint expander_size;
+ moz_gtk_get_treeview_expander_size(&expander_size);
+ result.width = result.height = expander_size;
+ } break;
+ default:
+ break;
+ }
+
+ return result * GetMonitorScaleFactor(aFrame);
+}
+
+NS_IMETHODIMP
+nsNativeThemeGTK::WidgetStateChanged(nsIFrame* aFrame,
+ StyleAppearance aAppearance,
+ nsAtom* aAttribute, bool* aShouldRepaint,
+ const nsAttrValue* aOldValue) {
+ *aShouldRepaint = false;
+
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::WidgetStateChanged(aFrame, aAppearance, aAttribute,
+ aShouldRepaint, aOldValue);
+ }
+
+ // Some widget types just never change state.
+ if (aAppearance == StyleAppearance::Toolbox ||
+ aAppearance == StyleAppearance::Toolbar ||
+ aAppearance == StyleAppearance::Statusbar ||
+ aAppearance == StyleAppearance::Statusbarpanel ||
+ aAppearance == StyleAppearance::Resizerpanel ||
+ aAppearance == StyleAppearance::Progresschunk ||
+ aAppearance == StyleAppearance::ProgressBar ||
+ aAppearance == StyleAppearance::Menubar ||
+ aAppearance == StyleAppearance::Tooltip ||
+ aAppearance == StyleAppearance::Menuseparator) {
+ return NS_OK;
+ }
+
+ if (aAppearance == StyleAppearance::MozWindowTitlebar ||
+ aAppearance == StyleAppearance::MozWindowTitlebarMaximized ||
+ aAppearance == StyleAppearance::MozWindowButtonClose ||
+ aAppearance == StyleAppearance::MozWindowButtonMinimize ||
+ aAppearance == StyleAppearance::MozWindowButtonMaximize ||
+ aAppearance == StyleAppearance::MozWindowButtonRestore) {
+ *aShouldRepaint = true;
+ return NS_OK;
+ }
+
+ // XXXdwh Not sure what can really be done here. Can at least guess for
+ // specific widgets that they're highly unlikely to have certain states.
+ // For example, a toolbar doesn't care about any states.
+ if (!aAttribute) {
+ // Hover/focus/active changed. Always repaint.
+ *aShouldRepaint = true;
+ return NS_OK;
+ }
+
+ // Check the attribute to see if it's relevant.
+ // disabled, checked, dlgtype, default, etc.
+ *aShouldRepaint = false;
+ if (aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::checked ||
+ aAttribute == nsGkAtoms::selected ||
+ aAttribute == nsGkAtoms::visuallyselected ||
+ aAttribute == nsGkAtoms::focused || aAttribute == nsGkAtoms::readonly ||
+ aAttribute == nsGkAtoms::_default ||
+ aAttribute == nsGkAtoms::menuactive || aAttribute == nsGkAtoms::open ||
+ aAttribute == nsGkAtoms::parentfocused) {
+ *aShouldRepaint = true;
+ return NS_OK;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNativeThemeGTK::ThemeChanged() {
+ memset(mDisabledWidgetTypes, 0, sizeof(mDisabledWidgetTypes));
+ memset(mSafeWidgetStates, 0, sizeof(mSafeWidgetStates));
+ memset(mBorderCacheValid, 0, sizeof(mBorderCacheValid));
+ return NS_OK;
+}
+
+NS_IMETHODIMP_(bool)
+nsNativeThemeGTK::ThemeSupportsWidget(nsPresContext* aPresContext,
+ nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetTypeDisabled(mDisabledWidgetTypes, aAppearance)) {
+ return false;
+ }
+
+ if (IsWidgetNonNative(aFrame, aAppearance) == NonNative::Always) {
+ return Theme::ThemeSupportsWidget(aPresContext, aFrame, aAppearance);
+ }
+
+ switch (aAppearance) {
+ // Combobox dropdowns don't support native theming in vertical mode.
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::MenulistText:
+ if (aFrame && aFrame->GetWritingMode().IsVertical()) {
+ return false;
+ }
+ [[fallthrough]];
+
+ case StyleAppearance::Button:
+ case StyleAppearance::Radio:
+ case StyleAppearance::Checkbox:
+ case StyleAppearance::Toolbox: // N/A
+ case StyleAppearance::Toolbar:
+ case StyleAppearance::Toolbarbutton:
+ case StyleAppearance::Dualbutton: // so we can override the border with 0
+ case StyleAppearance::ToolbarbuttonDropdown:
+ case StyleAppearance::ButtonArrowUp:
+ case StyleAppearance::ButtonArrowDown:
+ case StyleAppearance::ButtonArrowNext:
+ case StyleAppearance::ButtonArrowPrevious:
+ case StyleAppearance::Separator:
+ case StyleAppearance::Toolbargripper:
+ case StyleAppearance::Statusbar:
+ case StyleAppearance::Statusbarpanel:
+ case StyleAppearance::Resizerpanel:
+ case StyleAppearance::Resizer:
+ case StyleAppearance::Listbox:
+ case StyleAppearance::Treeview:
+ // case StyleAppearance::Treeitem:
+ case StyleAppearance::Treetwisty:
+ // case StyleAppearance::Treeline:
+ // case StyleAppearance::Treeheader:
+ case StyleAppearance::Treeheadercell:
+ case StyleAppearance::Treeheadersortarrow:
+ case StyleAppearance::Treetwistyopen:
+ case StyleAppearance::ProgressBar:
+ case StyleAppearance::Progresschunk:
+ case StyleAppearance::Tab:
+ // case StyleAppearance::Tabpanel:
+ case StyleAppearance::Tabpanels:
+ case StyleAppearance::TabScrollArrowBack:
+ case StyleAppearance::TabScrollArrowForward:
+ case StyleAppearance::Tooltip:
+ case StyleAppearance::Spinner:
+ case StyleAppearance::SpinnerUpbutton:
+ case StyleAppearance::SpinnerDownbutton:
+ case StyleAppearance::SpinnerTextfield:
+ case StyleAppearance::NumberInput:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Range:
+ case StyleAppearance::RangeThumb:
+ case StyleAppearance::CheckboxContainer:
+ case StyleAppearance::RadioContainer:
+ case StyleAppearance::CheckboxLabel:
+ case StyleAppearance::RadioLabel:
+ case StyleAppearance::Menubar:
+ case StyleAppearance::Menuitem:
+ case StyleAppearance::Menuarrow:
+ case StyleAppearance::Menuseparator:
+ case StyleAppearance::Checkmenuitem:
+ case StyleAppearance::Radiomenuitem:
+ case StyleAppearance::Splitter:
+ case StyleAppearance::MozWindowButtonBox:
+ case StyleAppearance::MozWindowButtonClose:
+ case StyleAppearance::MozWindowButtonMinimize:
+ case StyleAppearance::MozWindowButtonMaximize:
+ case StyleAppearance::MozWindowButtonRestore:
+ case StyleAppearance::MozWindowTitlebar:
+ case StyleAppearance::MozWindowTitlebarMaximized:
+ return !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+
+ case StyleAppearance::MozMenulistArrowButton:
+ if (aFrame && aFrame->GetWritingMode().IsVertical()) {
+ return false;
+ }
+ // "Native" dropdown buttons cause padding and margin problems, but only
+ // in HTML so allow them in XUL.
+ return (!aFrame ||
+ IsFrameContentNodeInNamespace(aFrame, kNameSpaceID_XUL)) &&
+ !IsWidgetStyled(aPresContext, aFrame, aAppearance);
+
+ case StyleAppearance::FocusOutline:
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP_(bool)
+nsNativeThemeGTK::WidgetIsContainer(StyleAppearance aAppearance) {
+ // XXXdwh At some point flesh all of this out.
+ if (aAppearance == StyleAppearance::MozMenulistArrowButton ||
+ aAppearance == StyleAppearance::Radio ||
+ aAppearance == StyleAppearance::RangeThumb ||
+ aAppearance == StyleAppearance::Checkbox ||
+ aAppearance == StyleAppearance::TabScrollArrowBack ||
+ aAppearance == StyleAppearance::TabScrollArrowForward ||
+ aAppearance == StyleAppearance::ButtonArrowUp ||
+ aAppearance == StyleAppearance::ButtonArrowDown ||
+ aAppearance == StyleAppearance::ButtonArrowNext ||
+ aAppearance == StyleAppearance::ButtonArrowPrevious)
+ return false;
+ return true;
+}
+
+bool nsNativeThemeGTK::ThemeDrawsFocusForWidget(nsIFrame* aFrame,
+ StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::ThemeDrawsFocusForWidget(aFrame, aAppearance);
+ }
+ switch (aAppearance) {
+ case StyleAppearance::Button:
+ case StyleAppearance::Menulist:
+ case StyleAppearance::MenulistButton:
+ case StyleAppearance::Textarea:
+ case StyleAppearance::Textfield:
+ case StyleAppearance::Treeheadercell:
+ case StyleAppearance::NumberInput:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool nsNativeThemeGTK::ThemeNeedsComboboxDropmarker() { return false; }
+
+nsITheme::Transparency nsNativeThemeGTK::GetWidgetTransparency(
+ nsIFrame* aFrame, StyleAppearance aAppearance) {
+ if (IsWidgetNonNative(aFrame, aAppearance) != NonNative::No) {
+ return Theme::GetWidgetTransparency(aFrame, aAppearance);
+ }
+
+ switch (aAppearance) {
+ // Tooltips use gtk_paint_flat_box() on Gtk2
+ // but are shaped on Gtk3
+ case StyleAppearance::Tooltip:
+ return eTransparent;
+ default:
+ return eUnknownTransparency;
+ }
+}
+
+already_AddRefed<Theme> do_CreateNativeThemeDoNotUseDirectly() {
+ if (gfxPlatform::IsHeadless()) {
+ return do_AddRef(new Theme(Theme::ScrollbarStyle()));
+ }
+ return do_AddRef(new nsNativeThemeGTK());
+}