From 19ded9cd7eb426e2f4e53a596bbff9f98a6df4a0 Mon Sep 17 00:00:00 2001 From: Michael Hynson Date: Wed, 2 Sep 2026 14:21:05 +0000 Subject: [PATCH] Bug 2056749 - Add -moz-scrollbar-inset-{block, inline} for improved scrollbar boundaries for rounded chrome corners. r=emilio,tabbrowser-reviewers,firefox-style-system-reviewers,layout-reviewers,sthompson - Adds `-moz-scrollbar-inset-block` and `-moz-scrollbar-inset-inline`, chrome-only `{1,2}` longhands. Each names the scrollbar's axis it applies to. The two values are the axis' logical start and end and flip with the writing mode. - ScrollContainerFrame::ScrollbarInsets() serves as the one place to extract the inset from for both nsScrollbarFrame and GetBorderRadii - Applies inset to nsScrollbarFrame::Reflow similar to stepper buttons - ScrollContainerFrame::GetBorderRadii no longer squares a corner off when the inset already holds the scrollbar clear of the curve. - Forward the embedder's inset to a root scrollbar for the content viewport - Add reftests for css-scrollbars - This commit does NOT implement the css property anywhere Differential Revision: https://phabricator.services.mozilla.com/D313900 --- .../server/actors/animation-type-longhand.js | 2 + docshell/base/BrowsingContext.cpp | 9 ++ docshell/base/BrowsingContext.h | 13 ++ dom/base/use_counter_metrics.yaml | 142 +++++++++++++----- layout/base/nsPresContext.cpp | 19 +++ layout/base/nsPresContext.h | 10 ++ layout/generic/ScrollContainerFrame.cpp | 64 +++++++- layout/generic/ScrollContainerFrame.h | 2 + layout/generic/WritingModes.h | 8 + layout/generic/nsSubDocumentFrame.cpp | 32 ++++ layout/generic/nsSubDocumentFrame.h | 1 + layout/reftests/css-scrollbars/reftest.list | 37 +++++ .../scrollbar-inset-change-ref.html | 16 ++ .../scrollbar-inset-change.html | 27 ++++ .../scrollbar-inset-forward-change.html | 29 ++++ .../scrollbar-inset-forward-inner.html | 17 +++ .../scrollbar-inset-forward-notref.html | 9 ++ .../scrollbar-inset-forward.html | 15 ++ .../scrollbar-inset-inline-notref.html | 16 ++ .../scrollbar-inset-inline.html | 19 +++ .../scrollbar-inset-radii-change.html | 48 ++++++ .../scrollbar-inset-radii-inline-notref.html | 36 +++++ .../scrollbar-inset-radii-inline.html | 36 +++++ .../scrollbar-inset-radii-notref.html | 35 +++++ .../scrollbar-inset-radii-ref.html | 34 +++++ .../scrollbar-inset-radii-rtl.html | 40 +++++ .../css-scrollbars/scrollbar-inset-radii.html | 35 +++++ .../css-scrollbars/scrollbar-inset-rtl.html | 32 ++++ .../css-scrollbars/scrollbar-inset-zoom.html | 27 ++++ layout/style/GenerateCSSProperties.py | 2 + layout/style/ServoBindings.toml | 1 + layout/style/nsStyleStruct.cpp | 13 +- layout/style/nsStyleStruct.h | 9 ++ layout/style/test/property_database.js | 18 +++ layout/tools/reftest/jar.mn | 1 + layout/xul/nsScrollbarFrame.cpp | 37 ++++- layout/xul/nsScrollbarFrame.h | 2 + .../style/properties/longhands.toml | 24 +++ servo/components/style/values/computed/box.rs | 5 +- servo/components/style/values/computed/mod.rs | 4 +- servo/components/style/values/generics/box.rs | 50 ++++++ .../components/style/values/specified/box.rs | 18 ++- .../components/style/values/specified/mod.rs | 4 +- servo/ports/geckolib/cbindgen.toml | 1 + 44 files changed, 940 insertions(+), 59 deletions(-) create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-change-ref.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-change.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-forward-change.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-forward-inner.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-forward-notref.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-forward.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-inline-notref.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-inline.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-radii-change.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-radii-inline-notref.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-radii-inline.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-radii-notref.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-radii-ref.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-radii-rtl.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-radii.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-rtl.html create mode 100644 layout/reftests/css-scrollbars/scrollbar-inset-zoom.html diff --git a/devtools/server/actors/animation-type-longhand.js b/devtools/server/actors/animation-type-longhand.js index 272186e8bf53..6d6a27dfc0d1 100644 --- a/devtools/server/actors/animation-type-longhand.js +++ b/devtools/server/actors/animation-type-longhand.js @@ -160,6 +160,8 @@ exports.ANIMATION_TYPE_FOR_LONGHANDS = [ "scroll-snap-type", "shape-rendering", "scrollbar-gutter", + "-moz-scrollbar-inset-block", + "-moz-scrollbar-inset-inline", "scrollbar-width", "stroke-linecap", "stroke-linejoin", diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp index a77e8cc62c0a..ccfffa570c47 100644 --- a/docshell/base/BrowsingContext.cpp +++ b/docshell/base/BrowsingContext.cpp @@ -5,6 +5,7 @@ #include "mozilla/dom/BrowsingContext.h" #include "ipc/IPCMessageUtils.h" +#include "mozilla/GfxMessageUtils.h" #ifdef ACCESSIBILITY # include "mozilla/a11y/DocAccessibleParent.h" @@ -3492,6 +3493,14 @@ void BrowsingContext::DidSet(FieldIndex, PresContextAffectingFieldChanged(); } +void BrowsingContext::DidSet(FieldIndex, + LayoutDeviceIntMargin&& aOldValue) { + if (GetEmbedderScrollbarInset() == aOldValue) { + return; + } + PresContextAffectingFieldChanged(); +} + void BrowsingContext::DidSet(FieldIndex, dom::PrefersColorSchemeOverride aOldValue) { MOZ_ASSERT(IsTop()); diff --git a/docshell/base/BrowsingContext.h b/docshell/base/BrowsingContext.h index efc4c4fd62de..f197f748c683 100644 --- a/docshell/base/BrowsingContext.h +++ b/docshell/base/BrowsingContext.h @@ -7,6 +7,7 @@ #include #include "GVAutoplayRequestUtils.h" +#include "Units.h" #include "mozilla/ErrorResult.h" #include "mozilla/HalScreenConfiguration.h" #include "mozilla/LinkedList.h" @@ -254,6 +255,10 @@ struct EmbedderColorSchemes { /* prefers-color-scheme override based on the color-scheme style of our \ * embedder element. */ \ FIELD(EmbedderColorSchemes, EmbedderColorSchemes) \ + /* Content-area scrollbar insets forwarded from the embedder's \ + * -moz-scrollbar-inset-{block,inline}, so the top-level content viewport \ + * scrollbars clear the rounded content-area corners. */ \ + FIELD(EmbedderScrollbarInset, LayoutDeviceIntMargin) \ FIELD(DisplayMode, dom::DisplayMode) \ /* The number of entries added to the session history because of this \ * browsing context. */ \ @@ -1332,6 +1337,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { return CheckOnlyEmbedderCanSet(aSource); } + bool CanSet(FieldIndex, + const LayoutDeviceIntMargin&, ContentParent* aSource) { + return CheckOnlyEmbedderCanSet(aSource); + } + bool CanSet(FieldIndex, dom::PrefersColorSchemeOverride, ContentParent*) { return IsTop(); @@ -1365,6 +1375,9 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache { void DidSet(FieldIndex, EmbedderColorSchemes&& aOldValue); + void DidSet(FieldIndex, + LayoutDeviceIntMargin&& aOldValue); + void DidSet(FieldIndex, dom::PrefersColorSchemeOverride aOldValue); diff --git a/dom/base/use_counter_metrics.yaml b/dom/base/use_counter_metrics.yaml index 925ddfcc59a4..37f9b78c65c0 100644 --- a/dom/base/use_counter_metrics.yaml +++ b/dom/base/use_counter_metrics.yaml @@ -108,7 +108,7 @@ use.counter: send_in_pings: - use-counters -# Total of 2628 use counter metrics (excludes denominators). +# Total of 2632 use counter metrics (excludes denominators). # Total of 416 'page' use counters. use.counter.page: svgsvgelement_getelementbyid: @@ -19843,7 +19843,7 @@ use.counter.deprecated_ops.doc: send_in_pings: - use-counters -# Total of 734 'CSS (page)' use counters. +# Total of 736 'CSS (page)' use counters. use.counter.css.page: css_align_items: type: counter @@ -24860,6 +24860,40 @@ use.counter.css.page: send_in_pings: - use-counters + css_moz_scrollbar_inset_block: + type: counter + description: > + Whether a page used the CSS property -moz-scrollbar-inset-block. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + + css_moz_scrollbar_inset_inline: + type: counter + description: > + Whether a page used the CSS property -moz-scrollbar-inset-inline. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_moz_window_transform: type: counter description: > @@ -28736,6 +28770,23 @@ use.counter.css.page: send_in_pings: - use-counters + css_moz_appearance: + type: counter + description: > + Whether a page used the CSS property -moz-appearance. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_all: type: counter description: > @@ -29110,23 +29161,6 @@ use.counter.css.page: send_in_pings: - use-counters - css_moz_appearance: - type: counter - description: > - Whether a page used the CSS property -moz-appearance. - Compare against `use.counter.top_level_content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - css_webkit_appearance: type: counter description: > @@ -32323,7 +32357,7 @@ use.counter.css.page: send_in_pings: - use-counters -# Total of 734 'CSS (document)' use counters. +# Total of 736 'CSS (document)' use counters. use.counter.css.doc: css_align_items: type: counter @@ -37340,6 +37374,40 @@ use.counter.css.doc: send_in_pings: - use-counters + css_moz_scrollbar_inset_block: + type: counter + description: > + Whether a document used the CSS property -moz-scrollbar-inset-block. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + + css_moz_scrollbar_inset_inline: + type: counter + description: > + Whether a document used the CSS property -moz-scrollbar-inset-inline. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_moz_window_transform: type: counter description: > @@ -41216,6 +41284,23 @@ use.counter.css.doc: send_in_pings: - use-counters + css_moz_appearance: + type: counter + description: > + Whether a document used the CSS property -moz-appearance. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + css_all: type: counter description: > @@ -41590,23 +41675,6 @@ use.counter.css.doc: send_in_pings: - use-counters - css_moz_appearance: - type: counter - description: > - Whether a document used the CSS property -moz-appearance. - Compare against `use.counter.content_documents_destroyed` - to calculate the rate. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 - notification_emails: - - dom-core@mozilla.com - - emilio@mozilla.com - expires: never - send_in_pings: - - use-counters - css_webkit_appearance: type: counter description: > diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp index 02a771ca9348..06fe27c8d6cd 100644 --- a/layout/base/nsPresContext.cpp +++ b/layout/base/nsPresContext.cpp @@ -35,6 +35,7 @@ #include "mozilla/PresShellInlines.h" #include "mozilla/RestyleManager.h" #include "mozilla/SMILAnimationController.h" +#include "mozilla/ScrollContainerFrame.h" #include "mozilla/ServoBindings.h" #include "mozilla/ServoStyleSet.h" #include "mozilla/StaticPrefs_bidi.h" @@ -896,6 +897,21 @@ void nsPresContext::SetLinkParametersOverride( RebuildAllStyleData(nsChangeHint(0), RestyleHint::RecascadeSubtree()); } +void nsPresContext::SetEmbedderScrollbarInset(const nsMargin& aInset) { + if (mEmbedderScrollbarInset == aInset) { + return; + } + mEmbedderScrollbarInset = aInset; + + // Read directly by nsScrollbarFrame::Reflow for the viewport scroll frame, + // so nothing needs re-cascading; just reflow the scrollbars. + if (mozilla::PresShell* presShell = GetPresShell()) { + if (ScrollContainerFrame* sf = presShell->GetRootScrollContainerFrame()) { + sf->MarkScrollbarsDirtyForReflow(); + } + } +} + void nsPresContext::UpdateAnimationsPlayBackRateMultiplier(double aMultiplier) { if (mAnimationsPlayBackRateMultiplier == aMultiplier) { return; @@ -939,6 +955,9 @@ void nsPresContext::RecomputeBrowsingContextDependentData() { return browsingContext->GetEmbedderColorSchemes().mPreferred; }()); + SetEmbedderScrollbarInset(LayoutDevicePixel::ToAppUnits( + browsingContext->GetEmbedderScrollbarInset(), AppUnitsPerDevPixel())); + UpdateForcedColors(); SetInRDMPane(top->GetInRDMPane()); diff --git a/layout/base/nsPresContext.h b/layout/base/nsPresContext.h index 5cdf0f1aa932..dd13711ab28e 100644 --- a/layout/base/nsPresContext.h +++ b/layout/base/nsPresContext.h @@ -601,6 +601,15 @@ class nsPresContext : public nsISupports, void SetLinkParametersOverride( const mozilla::StyleLinkParameters& aLinkParameters); + /** + * Content-area scrollbar insets forwarded from the embedder, per + * physical side, in app units. + */ + const nsMargin& EmbedderScrollbarInset() const { + return mEmbedderScrollbarInset; + } + void SetEmbedderScrollbarInset(const nsMargin& aInset); + /** * Return the device's screen size in inches, for font size * inflation. @@ -1433,6 +1442,7 @@ class nsPresContext : public nsISupports, mozilla::dom::PrefersColorSchemeOverride mOverriddenOrEmbedderColorScheme; mozilla::StyleForcedColors mForcedColors; mozilla::StyleLinkParameters mLinkParameters; + nsMargin mEmbedderScrollbarInset; protected: virtual ~nsPresContext(); diff --git a/layout/generic/ScrollContainerFrame.cpp b/layout/generic/ScrollContainerFrame.cpp index 688bdbed9e39..db36f17b6702 100644 --- a/layout/generic/ScrollContainerFrame.cpp +++ b/layout/generic/ScrollContainerFrame.cpp @@ -5882,6 +5882,11 @@ GetWebkitScrollbarWidthAndHeight( return {toSize(webkitScrollbarWidth), toSize(webkitScrollbarHeight)}; } +static nsMargin StyleScrollbarInsets(const ComputedStyle& aStyle, + const WritingMode aWm) { + return aStyle.StyleDisplay()->GetScrollbarInset(aWm).GetPhysicalMargin(aWm); +} + void ScrollContainerFrame::DidSetComputedStyle( ComputedStyle* aOldComputedStyle) { nsContainerFrame::DidSetComputedStyle(aOldComputedStyle); @@ -5932,10 +5937,15 @@ void ScrollContainerFrame::DidSetComputedStyle( } } - if (aOldComputedStyle && !mIsRoot && - StyleDisplay()->mScrollSnapType != - aOldComputedStyle->StyleDisplay()->mScrollSnapType) { - PostPendingResnap(); + if (aOldComputedStyle && !mIsRoot) { + if (StyleDisplay()->mScrollSnapType != + aOldComputedStyle->StyleDisplay()->mScrollSnapType) { + PostPendingResnap(); + } + if (ScrollbarInsets() != + StyleScrollbarInsets(*aOldComputedStyle, GetWritingMode())) { + MarkScrollbarsDirtyForReflow(); + } } } @@ -6969,6 +6979,13 @@ void ScrollContainerFrame::LayoutScrollbars(ScrollReflowInput& aState, } } +nsMargin ScrollContainerFrame::ScrollbarInsets() const { + if (mIsRoot) { + return PresContext()->EmbedderScrollbarInset(); + } + return StyleScrollbarInsets(*Style(), GetWritingMode()); +} + static void ReduceRadii(nscoord aXBorder, nscoord aYBorder, nsSize& aRadius) { // In order to ensure that the inside edge of the border has no // curvature, we need at least one of its radii to be zero. @@ -6982,6 +6999,20 @@ static void ReduceRadii(nscoord aXBorder, nscoord aYBorder, nsSize& aRadius) { aRadius.width *= ratio; aRadius.height *= ratio; } +// Whether a scrollbar on one side of a corner reaches that corner's curve, and +// so forces it square. +// aInset: the scrollbar's inset at the given corner +// aRadius: the corner radius measured along the scrollbar's axis +// aBorder: border width on the side the curve starts from +static bool ScrollbarReachesCorner(bool aHasScrollbar, nscoord aInset, + nscoord aRadius, nscoord aBorder) { + if (!aHasScrollbar) { + return false; + } + // Subtracting aBorder because ReduceRadii only squares the border's inner + // edge; that also matches its early-out when the border swallows the radius. + return aInset < aRadius - aBorder; +} /** * Implement an override for nsIFrame::GetBorderRadii to ensure that @@ -7006,16 +7037,33 @@ bool ScrollContainerFrame::GetBorderRadii(const nsSize& aFrameSize, nsMargin sb = GetActualScrollbarSizes(); nsMargin border = GetUsedBorder(); - if (sb.left > 0 || sb.top > 0) { + // A scrollbar with an inset holds clear of a corner leaves that corner's + // radius alone. A vertical scrollbar is inset at the top and bottom, and a + // horizontal one at the left and right. + const nsMargin inset = ScrollbarInsets(); + + if (ScrollbarReachesCorner(sb.left, inset.top, aRadii.TopLeft().height, + border.top) || + ScrollbarReachesCorner(sb.top, inset.left, aRadii.TopLeft().width, + border.left)) { ReduceRadii(border.left, border.top, aRadii.TopLeft()); } - if (sb.top > 0 || sb.right > 0) { + if (ScrollbarReachesCorner(sb.right, inset.top, aRadii.TopRight().height, + border.top) || + ScrollbarReachesCorner(sb.top, inset.right, aRadii.TopRight().width, + border.right)) { ReduceRadii(border.right, border.top, aRadii.TopRight()); } - if (sb.right > 0 || sb.bottom > 0) { + if (ScrollbarReachesCorner(sb.right, inset.bottom, + aRadii.BottomRight().height, border.bottom) || + ScrollbarReachesCorner(sb.bottom, inset.right, aRadii.BottomRight().width, + border.right)) { ReduceRadii(border.right, border.bottom, aRadii.BottomRight()); } - if (sb.bottom > 0 || sb.left > 0) { + if (ScrollbarReachesCorner(sb.left, inset.bottom, aRadii.BottomLeft().height, + border.bottom) || + ScrollbarReachesCorner(sb.bottom, inset.left, aRadii.BottomLeft().width, + border.left)) { ReduceRadii(border.left, border.bottom, aRadii.BottomLeft()); } return true; diff --git a/layout/generic/ScrollContainerFrame.h b/layout/generic/ScrollContainerFrame.h index 1750a49ea61f..5fe149c3d084 100644 --- a/layout/generic/ScrollContainerFrame.h +++ b/layout/generic/ScrollContainerFrame.h @@ -111,6 +111,8 @@ class ScrollContainerFrame : public nsContainerFrame, nsIFrame::Sides aSkipSides, nsRectCornerRadii&) const final; + nsMargin ScrollbarInsets() const; + nscoord IntrinsicISize(const IntrinsicSizeInput& aInput, IntrinsicISizeType aType) override; diff --git a/layout/generic/WritingModes.h b/layout/generic/WritingModes.h index d27ff68f9ee7..9add1b6246a1 100644 --- a/layout/generic/WritingModes.h +++ b/layout/generic/WritingModes.h @@ -2314,6 +2314,14 @@ inline bool nsStyleMargin::HasAuto( : HasBlockAxisAuto(aWM, aParams); } +inline mozilla::LogicalMargin nsStyleDisplay::GetScrollbarInset( + mozilla::WritingMode aWM) const { + return mozilla::LogicalMargin(aWM, mScrollbarInsetBlock.start.ToAppUnits(), + mScrollbarInsetInline.end.ToAppUnits(), + mScrollbarInsetBlock.end.ToAppUnits(), + mScrollbarInsetInline.start.ToAppUnits()); +} + inline AnchorResolvedMargin nsStyleMargin::GetMargin( mozilla::LogicalSide aSide, mozilla::WritingMode aWM, const AnchorPosResolutionParams& aParams) const { diff --git a/layout/generic/nsSubDocumentFrame.cpp b/layout/generic/nsSubDocumentFrame.cpp index bb74784825b8..37836d62c467 100644 --- a/layout/generic/nsSubDocumentFrame.cpp +++ b/layout/generic/nsSubDocumentFrame.cpp @@ -177,6 +177,7 @@ void nsSubDocumentFrame::UpdateEmbeddedBrowsingContextDependentData() { } MaybeUpdateRemoteStyle(); MaybeUpdateEmbedderColorScheme(); + MaybeUpdateEmbedderScrollbarInset(); MaybeUpdateEmbedderZoom(); PropagateIsUnderHiddenEmbedderElement( PresShell()->IsUnderHiddenEmbedderElement() || @@ -775,6 +776,36 @@ void nsSubDocumentFrame::MaybeUpdateEmbedderColorScheme() { (void)bc->SetEmbedderColorSchemes(schemes); } +void nsSubDocumentFrame::MaybeUpdateEmbedderScrollbarInset() { + nsFrameLoader* fl = mFrameLoader.get(); + if (!fl) { + return; + } + + BrowsingContext* bc = fl->GetExtantBrowsingContext(); + if (!bc) { + return; + } + + // The content document can't express this in CSS: its viewport scroll frame's + // style is a non-inheriting anonymous box. Forward our own + // -moz-scrollbar-inset-{block,inline} instead, resolved to physical sides in + // our writing mode and converted to device pixels, since the subdocument's + // writing mode and zoom are both its own. + const WritingMode wm = GetWritingMode(); + nsPresContext* pc = PresContext(); + const nsMargin physical = + StyleDisplay()->GetScrollbarInset(wm).GetPhysicalMargin(wm); + + const LayoutDeviceIntMargin inset = LayoutDevicePixel::FromAppUnitsRounded( + physical, pc->AppUnitsPerDevPixel()); + if (bc->GetEmbedderScrollbarInset() == inset) { + return; + } + + (void)bc->SetEmbedderScrollbarInset(inset); +} + void nsSubDocumentFrame::MaybeUpdateEmbedderZoom() { nsFrameLoader* fl = mFrameLoader.get(); if (!fl) { @@ -850,6 +881,7 @@ void nsSubDocumentFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { // If there's no old style, the call in Init() or ShowViewer() should have // us covered. MaybeUpdateEmbedderColorScheme(); + MaybeUpdateEmbedderScrollbarInset(); MaybeUpdateRemoteStyle(aOldComputedStyle); if (aOldComputedStyle->EffectiveZoom() != Style()->EffectiveZoom()) { MaybeUpdateEmbedderZoom(); diff --git a/layout/generic/nsSubDocumentFrame.h b/layout/generic/nsSubDocumentFrame.h index 23e7816383c2..01c397ed4327 100644 --- a/layout/generic/nsSubDocumentFrame.h +++ b/layout/generic/nsSubDocumentFrame.h @@ -160,6 +160,7 @@ class nsSubDocumentFrame final : public nsAtomicContainerFrame, friend class AsyncFrameInit; void MaybeUpdateEmbedderColorScheme(); + void MaybeUpdateEmbedderScrollbarInset(); void MaybeUpdateEmbedderZoom(); void MaybeUpdateRemoteStyle(ComputedStyle* aOldComputedStyle = nullptr); void PropagateIsUnderHiddenEmbedderElement(bool aValue); diff --git a/layout/reftests/css-scrollbars/reftest.list b/layout/reftests/css-scrollbars/reftest.list index 4720bad94245..2d23b89e9a37 100644 --- a/layout/reftests/css-scrollbars/reftest.list +++ b/layout/reftests/css-scrollbars/reftest.list @@ -1,3 +1,40 @@ == viewport-scrollbar-color-change.html viewport-scrollbar-color-change-ref.html == scrollbar-thin-overflow-change.html scrollbar-thin-overflow-change-ref.html test-pref(layout.css.scrollbar-width-thin.disabled,true) == scrollbar-width-thin.html scrollbar-width-auto.html +# -moz-scrollbar-inset-{block,inline} are chrome-only, so these load over +# chrome://. +# Overlay scrollbars are forced off so the thumb is actually painted. +pref(ui.useOverlayScrollbars,0) == chrome://reftest/content/css-scrollbars/scrollbar-inset-change.html chrome://reftest/content/css-scrollbars/scrollbar-inset-change-ref.html +# An embedder's inset must reach the subdocument's viewport scrollbar. The != +# assertion fails if the forwarding does nothing, since the two iframes are +# otherwise identical. +pref(ui.useOverlayScrollbars,0) != chrome://reftest/content/css-scrollbars/scrollbar-inset-forward.html chrome://reftest/content/css-scrollbars/scrollbar-inset-forward-notref.html +pref(ui.useOverlayScrollbars,0) == chrome://reftest/content/css-scrollbars/scrollbar-inset-forward-change.html chrome://reftest/content/css-scrollbars/scrollbar-inset-forward.html +# The inset is physical, so full-zooming the subdocument must not change it. The +# fuzz is the thumb snapping to device pixels at a different app-unit +# granularity; a CSS-px inset would be out by ~600 pixels, not 40. Verified: the +# same test at fullZoom 1.0 matches exactly. +pref(ui.useOverlayScrollbars,0) fuzzy(0-255,0-40) == chrome://reftest/content/css-scrollbars/scrollbar-inset-zoom.html chrome://reftest/content/css-scrollbars/scrollbar-inset-forward.html +# -moz-scrollbar-inset-inline must reach the horizontal scrollbar at all... +pref(ui.useOverlayScrollbars,0) != chrome://reftest/content/css-scrollbars/scrollbar-inset-inline.html chrome://reftest/content/css-scrollbars/scrollbar-inset-inline-notref.html +# ...and the logical start must resolve per writing mode, so an RTL inline-end +# inset matches an LTR inline-start one. Needs the != above to mean something. +pref(ui.useOverlayScrollbars,0) == chrome://reftest/content/css-scrollbars/scrollbar-inset-rtl.html chrome://reftest/content/css-scrollbars/scrollbar-inset-inline.html +# An inset that holds the slider clear of the corner means GetBorderRadii no +# longer has to square that corner off, so the ring matches a container with no +# scrollbar at all. The cover hides the padding box, leaving only the ring. +pref(ui.useOverlayScrollbars,0) == chrome://reftest/content/css-scrollbars/scrollbar-inset-radii.html chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-ref.html +# The same, but applying the inset dynamically, so the corner has to go back to +# being round after having been squared off. +pref(ui.useOverlayScrollbars,0) == chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-change.html chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-ref.html +# Without an inset the corner must still be squared off. This also catches the +# == above passing for free if the scrollbar ever stops taking up layout space. +pref(ui.useOverlayScrollbars,0) != chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-notref.html chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-ref.html +# The corner GetBorderRadii spares must be picked per writing mode, so an RTL +# inline-end inset spares the same physical corner as an LTR inline-start one. +# Uses the inline axis because the block axis doesn't flip in RTL at all, and an +# asymmetric inset because a symmetric one is its own mirror image. +pref(ui.useOverlayScrollbars,0) == chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-rtl.html chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-inline.html +# The two ends must be distinguishable for the == above to mean anything: swap +# the inset and the opposite corner is spared instead. +pref(ui.useOverlayScrollbars,0) != chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-inline-notref.html chrome://reftest/content/css-scrollbars/scrollbar-inset-radii-inline.html diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-change-ref.html b/layout/reftests/css-scrollbars/scrollbar-inset-change-ref.html new file mode 100644 index 000000000000..68e8ccc7d82f --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-change-ref.html @@ -0,0 +1,16 @@ + + +
+
+
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-change.html b/layout/reftests/css-scrollbars/scrollbar-inset-change.html new file mode 100644 index 000000000000..f87a2ad100cf --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-change.html @@ -0,0 +1,27 @@ + + + +
+
+
+ diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-forward-change.html b/layout/reftests/css-scrollbars/scrollbar-inset-forward-change.html new file mode 100644 index 000000000000..4a2346f70f16 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-forward-change.html @@ -0,0 +1,29 @@ + + + + + + diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-forward-inner.html b/layout/reftests/css-scrollbars/scrollbar-inset-forward-inner.html new file mode 100644 index 000000000000..f4901c309023 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-forward-inner.html @@ -0,0 +1,17 @@ + + + + diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-forward-notref.html b/layout/reftests/css-scrollbars/scrollbar-inset-forward-notref.html new file mode 100644 index 000000000000..3b779041ed11 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-forward-notref.html @@ -0,0 +1,9 @@ + + + diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-forward.html b/layout/reftests/css-scrollbars/scrollbar-inset-forward.html new file mode 100644 index 000000000000..80d5bf1a2ee6 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-forward.html @@ -0,0 +1,15 @@ + + + + diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-inline-notref.html b/layout/reftests/css-scrollbars/scrollbar-inset-inline-notref.html new file mode 100644 index 000000000000..a3ee6dbb9997 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-inline-notref.html @@ -0,0 +1,16 @@ + + +
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-inline.html b/layout/reftests/css-scrollbars/scrollbar-inset-inline.html new file mode 100644 index 000000000000..81733ed0fd5f --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-inline.html @@ -0,0 +1,19 @@ + + + +
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-radii-change.html b/layout/reftests/css-scrollbars/scrollbar-inset-radii-change.html new file mode 100644 index 000000000000..1b5162276107 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-radii-change.html @@ -0,0 +1,48 @@ + +Applying an inset dynamically un-squares the corner + + + +
+
+
+
+ + diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-radii-inline-notref.html b/layout/reftests/css-scrollbars/scrollbar-inset-radii-inline-notref.html new file mode 100644 index 000000000000..c28bc9863e6f --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-radii-inline-notref.html @@ -0,0 +1,36 @@ + +The mirrored inset keeps the bottom-right corner instead + + +
+
+
+
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-radii-inline.html b/layout/reftests/css-scrollbars/scrollbar-inset-radii-inline.html new file mode 100644 index 000000000000..bd0233676bd6 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-radii-inline.html @@ -0,0 +1,36 @@ + +An inline-start inset keeps the bottom-left corner's radius + + +
+
+
+
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-radii-notref.html b/layout/reftests/css-scrollbars/scrollbar-inset-radii-notref.html new file mode 100644 index 000000000000..9c2af18342f1 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-radii-notref.html @@ -0,0 +1,35 @@ + +Without an inset the corner is squared off + + +
+
+
+
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-radii-ref.html b/layout/reftests/css-scrollbars/scrollbar-inset-radii-ref.html new file mode 100644 index 000000000000..2eff1bdb40ac --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-radii-ref.html @@ -0,0 +1,34 @@ + +No scrollbar, so the radius is never reduced + + +
+
+
+
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-radii-rtl.html b/layout/reftests/css-scrollbars/scrollbar-inset-radii-rtl.html new file mode 100644 index 000000000000..5c8f91d2e83b --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-radii-rtl.html @@ -0,0 +1,40 @@ + +In RTL the inline-end inset is the one that keeps the bottom-left corner + + +
+
+
+
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-radii.html b/layout/reftests/css-scrollbars/scrollbar-inset-radii.html new file mode 100644 index 000000000000..587cdb18669b --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-radii.html @@ -0,0 +1,35 @@ + +An inset that clears the corner keeps the border-radius + + +
+
+
+
diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-rtl.html b/layout/reftests/css-scrollbars/scrollbar-inset-rtl.html new file mode 100644 index 000000000000..c9154a6da242 --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-rtl.html @@ -0,0 +1,32 @@ + + + +
+ diff --git a/layout/reftests/css-scrollbars/scrollbar-inset-zoom.html b/layout/reftests/css-scrollbars/scrollbar-inset-zoom.html new file mode 100644 index 000000000000..e708a6d6cdfb --- /dev/null +++ b/layout/reftests/css-scrollbars/scrollbar-inset-zoom.html @@ -0,0 +1,27 @@ + + + + + + diff --git a/layout/style/GenerateCSSProperties.py b/layout/style/GenerateCSSProperties.py index f5585d296344..e492d3eeaba2 100644 --- a/layout/style/GenerateCSSProperties.py +++ b/layout/style/GenerateCSSProperties.py @@ -582,6 +582,8 @@ def gen_css_properties_js(output): "-moz-box-collapse", # chrome-only internal properties "-moz-line-scroll-amount", # chrome-only internal properties "-moz-image-decoding", # chrome-only internal properties + "-moz-scrollbar-inset-block", # chrome-only internal properties + "-moz-scrollbar-inset-inline", # chrome-only internal properties "-moz-subtree-hidden-only-visually", # chrome-only internal properties "-moz-user-focus", # chrome-only internal properties "-moz-window-input-region-margin", # chrome-only internal properties diff --git a/layout/style/ServoBindings.toml b/layout/style/ServoBindings.toml index 4030ffd72aea..4b8a2b0eb4e4 100644 --- a/layout/style/ServoBindings.toml +++ b/layout/style/ServoBindings.toml @@ -444,6 +444,7 @@ cbindgen-types = [ { gecko = "StyleViewTransitionName", servo = "crate::values::computed::ViewTransitionName" }, { gecko = "StyleResize", servo = "crate::values::computed::Resize" }, { gecko = "StyleOverflowClipMargin", servo = "crate::values::computed::OverflowClipMargin" }, + { gecko = "StyleScrollbarInset", servo = "crate::values::computed::ScrollbarInset" }, { gecko = "StyleFloat", servo = "crate::values::computed::Float" }, { gecko = "StyleClear", servo = "crate::values::computed::Clear" }, { gecko = "StyleOverscrollBehavior", servo = "crate::values::computed::OverscrollBehavior" }, diff --git a/layout/style/nsStyleStruct.cpp b/layout/style/nsStyleStruct.cpp index 07b5aceff0b5..224da7add824 100644 --- a/layout/style/nsStyleStruct.cpp +++ b/layout/style/nsStyleStruct.cpp @@ -33,6 +33,7 @@ #include "nsCOMPtr.h" #include "nsCRTGlue.h" #include "nsCSSProps.h" +#include "nsChangeHint.h" #include "nsContainerFrame.h" #include "nsDeviceContext.h" #include "nsIURI.h" @@ -2264,6 +2265,8 @@ nsStyleDisplay::nsStyleDisplay() mScrollSnapStop{StyleScrollSnapStop::Normal}, mScrollSnapType{StyleScrollSnapAxis::Both, StyleScrollSnapStrictness::None}, + mScrollbarInsetBlock{StyleLength{0.}, StyleLength{0.}}, + mScrollbarInsetInline{StyleLength{0.}, StyleLength{0.}}, mBackfaceVisibility(StyleBackfaceVisibility::Visible), mTransformStyle(StyleTransformStyle::Flat), mTransformBox(StyleTransformBox::ViewBox), @@ -2323,6 +2326,8 @@ nsStyleDisplay::nsStyleDisplay(const nsStyleDisplay& aSource) mScrollSnapAlign(aSource.mScrollSnapAlign), mScrollSnapStop(aSource.mScrollSnapStop), mScrollSnapType(aSource.mScrollSnapType), + mScrollbarInsetBlock(aSource.mScrollbarInsetBlock), + mScrollbarInsetInline(aSource.mScrollbarInsetInline), mBackfaceVisibility(aSource.mBackfaceVisibility), mTransformStyle(aSource.mTransformStyle), mTransformBox(aSource.mTransformBox), @@ -2736,6 +2741,10 @@ nsChangeHint nsStyleDisplay::CalcDifference( // container-query selection on descendants). // container-type / contain / content-visibility are handled by the // mEffectiveContainment check. + // + // scrollbar-inset changes are dealt with in + // nsSubDocumentFrame::DidSetComputedStyle and + // ScrollContainerFrame::DidSetComputedStyle. if (!hint && (mWillChange != aNewData.mWillChange || mOverflowAnchor != aNewData.mOverflowAnchor || mContentVisibility != aNewData.mContentVisibility || @@ -2743,7 +2752,9 @@ nsChangeHint nsStyleDisplay::CalcDifference( mContain != aNewData.mContain || mContainerName != aNewData.mContainerName || mAnchorName != aNewData.mAnchorName || - mAnchorScope != aNewData.mAnchorScope)) { + mAnchorScope != aNewData.mAnchorScope || + mScrollbarInsetBlock != aNewData.mScrollbarInsetBlock || + mScrollbarInsetInline != aNewData.mScrollbarInsetInline)) { hint |= nsChangeHint_NeutralChange; } diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h index ab87faa274bb..6f54ae75fb96 100644 --- a/layout/style/nsStyleStruct.h +++ b/layout/style/nsStyleStruct.h @@ -43,6 +43,7 @@ class ComputedStyle; struct AnchorPosResolutionCache; class AnchorPosReferenceData; struct IntrinsicSize; +class LogicalMargin; struct SizeComputationInput; } // namespace mozilla @@ -1662,6 +1663,8 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay { mozilla::StyleScrollSnapAlign mScrollSnapAlign; mozilla::StyleScrollSnapStop mScrollSnapStop; mozilla::StyleScrollSnapType mScrollSnapType; + mozilla::StyleScrollbarInset mScrollbarInsetBlock; + mozilla::StyleScrollbarInset mScrollbarInsetInline; mozilla::StyleBackfaceVisibility mBackfaceVisibility; mozilla::StyleTransformStyle mTransformStyle; @@ -1848,6 +1851,12 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleDisplay { mOverflowY == mozilla::StyleOverflow::Visible; } + // How much to shorten each scrollbar by at each of its ends. Each side holds + // the inset measured from it, so the block-axis pair shortens the scrollbar + // running along the block axis. Defined in WritingModes.h. + inline mozilla::LogicalMargin GetScrollbarInset( + mozilla::WritingMode aWM) const; + bool IsContainPaint() const { // Short circuit for no containment whatsoever if (!mEffectiveContainment) { diff --git a/layout/style/test/property_database.js b/layout/style/test/property_database.js index e033996c5033..a915ff99fcf8 100644 --- a/layout/style/test/property_database.js +++ b/layout/style/test/property_database.js @@ -13899,6 +13899,24 @@ if (false) { "2px", ], }; + + gCSSProperties["-moz-scrollbar-inset-block"] = { + // domProp: "MozScrollbarInsetBlock", + inherited: false, + type: CSS_TYPE_LONGHAND, + initial_values: ["0", "0 0"], + other_values: ["1px 2px", "calc(2em + 3ex)", "1px calc(2em)"], + invalid_values: ["auto", "20%", "-10px", "1px 2px 3px", "1px 2px 3px 4px"], + }; + + gCSSProperties["-moz-scrollbar-inset-inline"] = { + // domProp: "MozScrollbarInsetInline", + inherited: false, + type: CSS_TYPE_LONGHAND, + initial_values: ["0", "0 0"], + other_values: ["1px 2px", "calc(2em + 3ex)", "1px calc(2em)"], + invalid_values: ["auto", "20%", "-10px", "1px 2px 3px", "1px 2px 3px 4px"], + }; } gCSSProperties["scrollbar-color"] = { diff --git a/layout/tools/reftest/jar.mn b/layout/tools/reftest/jar.mn index 4232480c13d4..922398238f2f 100644 --- a/layout/tools/reftest/jar.mn +++ b/layout/tools/reftest/jar.mn @@ -9,6 +9,7 @@ reftest.jar: content/bugs (../../reftests/bugs/*) content/image (../../reftests/image/*) content/css-display (../../reftests/css-display/*) + content/css-scrollbars (../../reftests/css-scrollbars/*) content/fonts (../../reftests/fonts/*) content/forms/input/file (../../reftests/forms/input/file/*) content/forms/input/text (../../reftests/forms/input/text/*) diff --git a/layout/xul/nsScrollbarFrame.cpp b/layout/xul/nsScrollbarFrame.cpp index 2f4081a94d12..88c87a1642fc 100644 --- a/layout/xul/nsScrollbarFrame.cpp +++ b/layout/xul/nsScrollbarFrame.cpp @@ -124,6 +124,18 @@ void nsScrollbarFrame::Destroy(DestroyContext& aContext) { nsContainerFrame::Destroy(aContext); } +std::pair nsScrollbarFrame::ScrollbarInset() const { + // The scroll container owns the property and resolves the writing mode, so + // the margin it hands back is already physical and needs no further mapping. + ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(GetParent()); + if (!scrollContainerFrame) { + return {0, 0}; + } + const nsMargin inset = scrollContainerFrame->ScrollbarInsets(); + return IsHorizontal() ? std::pair{inset.left, inset.right} + : std::pair{inset.top, inset.bottom}; +} + void nsScrollbarFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, const ReflowInput& aReflowInput, @@ -152,11 +164,22 @@ void nsScrollbarFrame::Reflow(nsPresContext* aPresContext, const nsSize containerSize = aDesiredSize.PhysicalSize(); const LogicalSize totalAvailSize = aDesiredSize.Size(wm); - LogicalPoint nextKidPos(wm); MOZ_ASSERT(!wm.IsVertical()); const bool movesInInlineDirection = horizontal; + // We stay full-length so our track still spans the whole scrollport, and hold + // the slider back at each end instead. Unlike a scrollbar button the two ends + // are independent, so the symmetry assumption below doesn't apply to them. + const auto [insetStart, insetEnd] = ScrollbarInset(); + + LogicalPoint nextKidPos(wm); + if (movesInInlineDirection) { + nextKidPos.I(wm) = insetStart; + } else { + nextKidPos.B(wm) = insetStart; + } + // Layout our kids left to right / top to bottom. for (nsIFrame* kid : mFrames) { MOZ_ASSERT(!kid->GetWritingMode().IsOrthogonalTo(wm), @@ -164,16 +187,18 @@ void nsScrollbarFrame::Reflow(nsPresContext* aPresContext, const bool isSlider = kid->GetContent() == mSlider; LogicalSize availSize = totalAvailSize; { + const nscoord consumed = + movesInInlineDirection ? nextKidPos.I(wm) : nextKidPos.B(wm); // Assume we'll consume the same size before and after the slider. This is // not a technically correct assumption if we have weird scrollbar button // setups, but those will be going away, see bug 1824254. - const int32_t factor = isSlider ? 2 : 1; + const nscoord reserved = + isSlider ? (consumed - insetStart) * 2 + insetStart + insetEnd + : consumed; if (movesInInlineDirection) { - availSize.ISize(wm) = - std::max(0, totalAvailSize.ISize(wm) - nextKidPos.I(wm) * factor); + availSize.ISize(wm) = std::max(0, totalAvailSize.ISize(wm) - reserved); } else { - availSize.BSize(wm) = - std::max(0, totalAvailSize.BSize(wm) - nextKidPos.B(wm) * factor); + availSize.BSize(wm) = std::max(0, totalAvailSize.BSize(wm) - reserved); } } diff --git a/layout/xul/nsScrollbarFrame.h b/layout/xul/nsScrollbarFrame.h index 08257783ab89..63e8c314959d 100644 --- a/layout/xul/nsScrollbarFrame.h +++ b/layout/xul/nsScrollbarFrame.h @@ -68,6 +68,8 @@ class nsScrollbarFrame final : public nsContainerFrame, nsSize ScrollbarMinSize() const; bool IsHorizontal() const; + std::pair ScrollbarInset() const; + void Destroy(DestroyContext&) override; void Init(nsIContent* aContent, nsContainerFrame* aParent, diff --git a/servo/components/style/properties/longhands.toml b/servo/components/style/properties/longhands.toml index bb5cdd84cf33..fcefe9e12848 100644 --- a/servo/components/style/properties/longhands.toml +++ b/servo/components/style/properties/longhands.toml @@ -4168,6 +4168,30 @@ animation_type = "discrete" affects = "layout" keyword = { values = ["auto", "thin", "none"] } +# Reserves space at each end of a vertical scrollbar. +[-moz-scrollbar-inset-block] +type = "ScrollbarInset" +initial = "computed::ScrollbarInset::zero()" +struct = "box" +engine = "gecko" +animation_type = "discrete" +enabled_in = "chrome" +gecko_ffi_name = "mScrollbarInsetBlock" +spec = "None (Nonstandard Firefox-only property)" +affects = "layout" + +# Reserves space at each end of a horizontal scrollbar. +[-moz-scrollbar-inset-inline] +type = "ScrollbarInset" +initial = "computed::ScrollbarInset::zero()" +struct = "box" +engine = "gecko" +animation_type = "discrete" +enabled_in = "chrome" +gecko_ffi_name = "mScrollbarInsetInline" +spec = "None (Nonstandard Firefox-only property)" +affects = "layout" + [-moz-window-dragging] struct = "ui" engine = "gecko" diff --git a/servo/components/style/values/computed/box.rs b/servo/components/style/values/computed/box.rs index 94e385419781..2b0fe062b0cc 100644 --- a/servo/components/style/values/computed/box.rs +++ b/servo/components/style/values/computed/box.rs @@ -10,7 +10,7 @@ use crate::values::computed::length::{LengthPercentage, NonNegativeLength}; use crate::values::computed::{Context, Integer, Number, ToComputedValue}; use crate::values::generics::box_::{ GenericBaselineShift, GenericContainIntrinsicSize, GenericLineClamp, GenericOverflowClipMargin, - GenericPerspective, + GenericPerspective, GenericScrollbarInset, }; use crate::values::generics::GreaterThanOrEqualToOne; use crate::values::specified::box_ as specified; @@ -31,6 +31,9 @@ pub type BaselineShift = GenericBaselineShift; /// A computed value for the `overflow-clip-margin` property. pub type OverflowClipMargin = GenericOverflowClipMargin; +/// A computed value for the `-moz-scrollbar-inset-block` / `-inline` properties. +pub type ScrollbarInset = GenericScrollbarInset; + /// A computed value for the `contain-intrinsic-size` property. pub type ContainIntrinsicSize = GenericContainIntrinsicSize; diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs index bf6f4b03fc10..12e39ea4af27 100644 --- a/servo/components/style/values/computed/mod.rs +++ b/servo/components/style/values/computed/mod.rs @@ -61,8 +61,8 @@ pub use self::box_::{ Contain, ContainIntrinsicSize, ContainerName, ContainerType, ContentVisibility, Display, DominantBaseline, Float, LineClamp, MarginTrim, Overflow, OverflowAnchor, OverflowClipMargin, OverscrollBehavior, Perspective, PositionProperty, Resize, ScrollSnapAlign, ScrollSnapAxis, - ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, WillChange, - WritingModeProperty, Zoom, + ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, ScrollbarInset, + TouchAction, WillChange, WritingModeProperty, Zoom, }; pub use self::color::{ Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust, diff --git a/servo/components/style/values/generics/box.rs b/servo/components/style/values/generics/box.rs index aa44d0e02b70..473af6288b1f 100644 --- a/servo/components/style/values/generics/box.rs +++ b/servo/components/style/values/generics/box.rs @@ -500,3 +500,53 @@ impl ToCss for OverflowClipMargin { Ok(()) } } + +/// The two insets of one scrollbar, for the chrome-only +/// `-moz-scrollbar-inset-block` / `-moz-scrollbar-inset-inline`. Each property +/// names the axis the scrollbar runs along, so `start` and `end` are the logical +/// start and end of that axis and flip with the writing mode. +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, + ToTyped, +)] +#[repr(C)] +pub struct GenericScrollbarInset { + /// The inset at the logical start of the axis. + pub start: L, + /// The inset at the logical end of the axis. + pub end: L, +} + +pub use self::GenericScrollbarInset as ScrollbarInset; + +impl ScrollbarInset { + /// Returns the initial, all-zero value. + pub fn zero() -> Self { + Self { + start: Zero::zero(), + end: Zero::zero(), + } + } +} + +impl ToCss for ScrollbarInset { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.start.to_css(dest)?; + if self.end != self.start { + dest.write_char(' ')?; + self.end.to_css(dest)?; + } + Ok(()) + } +} diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs index c9896f6fb123..747819dfbc50 100644 --- a/servo/components/style/values/specified/box.rs +++ b/servo/components/style/values/specified/box.rs @@ -11,8 +11,8 @@ use crate::properties::{LonghandId, PropertyDeclarationId, PropertyId}; pub use crate::typed_om::{KeywordValue, ToTyped, TypedValue}; use crate::values::generics::box_::{ BaselineShiftKeyword, BlockEllipsis, GenericBaselineShift, GenericContainIntrinsicSize, - GenericLineClamp, GenericOverflowClipMargin, GenericPerspective, MaxLines, - OverflowClipMarginBox, + GenericLineClamp, GenericOverflowClipMargin, GenericPerspective, GenericScrollbarInset, + MaxLines, OverflowClipMarginBox, }; use crate::values::specified::length::{LengthPercentage, NonNegativeLength}; use crate::values::specified::{AllowQuirks, NonNegativeNumberOrPercentage, PositiveInteger}; @@ -117,6 +117,20 @@ impl Parse for OverflowClipMargin { } } +/// The specified value of `-moz-scrollbar-inset-block` / `-inline`. +pub type ScrollbarInset = GenericScrollbarInset; + +impl Parse for ScrollbarInset { + // {1,2} + fn parse(context: &ParserContext, input: &mut Parser) -> Result { + let start = NonNegativeLength::parse(context, input)?; + let end = input + .try_parse(|i| NonNegativeLength::parse(context, i)) + .unwrap_or_else(|_| start.clone()); + Ok(Self { start, end }) + } +} + /// Defines an element’s display type, which consists of /// the two basic qualities of how an element generates boxes /// diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs index d09ed6532102..5191f96ee534 100644 --- a/servo/components/style/values/specified/mod.rs +++ b/servo/components/style/values/specified/mod.rs @@ -45,8 +45,8 @@ pub use self::box_::{ Contain, ContainIntrinsicSize, ContainerName, ContainerType, ContentVisibility, Display, DominantBaseline, Float, LineClamp, MarginTrim, Overflow, OverflowAnchor, OverflowClipMargin, OverscrollBehavior, Perspective, PositionProperty, Resize, ScrollSnapAlign, ScrollSnapAxis, - ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, WillChange, - WillChangeBits, WritingModeProperty, Zoom, + ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, ScrollbarInset, + TouchAction, WillChange, WillChangeBits, WritingModeProperty, Zoom, }; pub use self::calc::{CalcLengthPercentage, CalcNumeric}; pub use self::color::{ diff --git a/servo/ports/geckolib/cbindgen.toml b/servo/ports/geckolib/cbindgen.toml index b2d52a9e8b96..fd6229fc92d8 100644 --- a/servo/ports/geckolib/cbindgen.toml +++ b/servo/ports/geckolib/cbindgen.toml @@ -190,6 +190,7 @@ include = [ "ViewTimelineInset", "OverflowAnchor", "OverflowClipMargin", + "ScrollbarInset", "Resize", "Overflow", "LengthPercentage",