diff options
Diffstat (limited to '')
-rw-r--r-- | widget/gtk/nsNativeThemeGTK.cpp | 1611 |
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()); +} |