tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 746265b214cfb2e87d508febc005c34e53a75a3c
parent a5576f10a02db5fe488df7006c304e5f248f2384
Author: Emilio Cobos Álvarez <emilio@crisal.io>
Date:   Tue,  7 Oct 2025 17:09:26 +0000

Bug 1985190 - Snap outline offsets like border-widths. r=firefox-style-system-reviewers,layout-reviewers,dshin

This implements https://github.com/w3c/csswg-drafts/issues/12906 for
chrome contexts and nightly, for now, pending working group discussion.

We might want to do this only for negative offset but that's a bit
weird. For now do it unconditionally.

Add a test and fix border-width-rounding to also test outline-width, and
also report tests per entry, since we fail one subtest due to floating
point precision.

Differential Revision: https://phabricator.services.mozilla.com/D267770

Diffstat:
Mlayout/painting/nsDisplayList.cpp | 12++++--------
Mlayout/style/nsStyleStruct.cpp | 5++---
Mlayout/style/nsStyleStruct.h | 2+-
Mmodules/libpref/init/StaticPrefList.yaml | 15+++++++++++++++
Mservo/components/style/properties/longhands/outline.mako.rs | 4++--
Mservo/components/style/values/computed/border.rs | 3+++
Mservo/components/style/values/computed/mod.rs | 2+-
Mservo/components/style/values/specified/border.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mservo/components/style/values/specified/mod.rs | 3++-
Mtesting/web-platform/meta/css/css-borders/border-width-rounding.tentative.html.ini | 5++++-
Mtesting/web-platform/tests/css/css-borders/border-width-rounding.tentative.html | 18++++++++----------
Atesting/web-platform/tests/css/css-borders/outline-offset-rounding.tentative.html | 37+++++++++++++++++++++++++++++++++++++
Mtesting/web-platform/tests/css/css-ui/parsing/outline-offset-computed.html | 2+-
Mwidget/Theme.cpp | 2+-
14 files changed, 132 insertions(+), 43 deletions(-)

diff --git a/layout/painting/nsDisplayList.cpp b/layout/painting/nsDisplayList.cpp @@ -4116,15 +4116,11 @@ bool nsDisplayOutline::HasRadius() const { } bool nsDisplayOutline::IsInvisibleInRect(const nsRect& aRect) const { - const nsStyleOutline* outline = mFrame->StyleOutline(); nsRect borderBox(ToReferenceFrame(), mFrame->GetSize()); - if (borderBox.Contains(aRect) && !HasRadius() && - outline->mOutlineOffset.ToCSSPixels() >= 0.0f) { - // aRect is entirely inside the border-rect, and the outline isn't rendered - // inside the border-rect, so the outline is not visible. - return true; - } - return false; + // aRect is entirely inside the border-rect, and the outline isn't rendered + // inside the border-rect, so the outline is not visible. + return borderBox.Contains(aRect) && !HasRadius() && + mFrame->StyleOutline()->mOutlineOffset >= 0; } void nsDisplayEventReceiver::HitTest(nsDisplayListBuilder* aBuilder, diff --git a/layout/style/nsStyleStruct.cpp b/layout/style/nsStyleStruct.cpp @@ -552,7 +552,7 @@ nsChangeHint nsStyleBorder::CalcDifference( nsStyleOutline::nsStyleOutline() : mOutlineWidth(kMediumBorderWidth), - mOutlineOffset({0.0f}), + mOutlineOffset(0), mOutlineColor(StyleColor::CurrentColor()), mOutlineStyle(StyleOutlineStyle::BorderStyle(StyleBorderStyle::None)), mActualOutlineWidth(0) { @@ -596,8 +596,7 @@ nsChangeHint nsStyleOutline::CalcDifference( } nsSize nsStyleOutline::EffectiveOffsetFor(const nsRect& aRect) const { - const nscoord offset = mOutlineOffset.ToAppUnits(); - + const nscoord offset = mOutlineOffset; if (offset >= 0) { // Fast path for non-negative offset values return nsSize(offset, offset); diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h @@ -703,7 +703,7 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleOutline { // style struct resolution reasons that we do nsStyleBorder::mBorder; // see that field's comment.) nscoord mOutlineWidth; - mozilla::Length mOutlineOffset; + mozilla::StyleAu mOutlineOffset; mozilla::StyleColor mOutlineColor; mozilla::StyleOutlineStyle mOutlineStyle; diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -11002,6 +11002,21 @@ value: false mirror: always +# Determines how we snap css outline-offset: +# 0: No snapping (historical behavior) +# 1: Always snap as border-width (proposed behavior in +# https://github.com/w3c/csswg-drafts/issues/12906) +# 2: The above, but only on chrome code, for now. +- name: layout.css.outline-offset.snapping + type: RelaxedAtomicInt32 +#ifdef NIGHTLY_BUILD + value: 1 +#else + value: 2 +#endif + mirror: always + rust: true + - name: layout.visibility.min-recompute-interval-ms type: uint32_t value: 1000 diff --git a/servo/components/style/properties/longhands/outline.mako.rs b/servo/components/style/properties/longhands/outline.mako.rs @@ -41,8 +41,8 @@ ${helpers.predefined_type( ${helpers.predefined_type( "outline-offset", - "Length", - "crate::values::computed::Length::new(0.)", + "BorderSideOffset", + "app_units::Au(0)", engines="gecko servo", spec="https://drafts.csswg.org/css-ui/#propdef-outline-offset", servo_restyle_damage="repaint", diff --git a/servo/components/style/values/computed/border.rs b/servo/components/style/values/computed/border.rs @@ -24,6 +24,9 @@ pub type LineWidth = Au; /// A computed value for border-width (and the like). pub type BorderSideWidth = Au; +/// A computed value for outline-offset +pub type BorderSideOffset = Au; + /// A computed value for the `border-image-width` property. pub type BorderImageWidth = Rect<BorderImageSideWidth>; diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs @@ -50,7 +50,7 @@ pub use self::background::{BackgroundRepeat, BackgroundSize}; pub use self::basic_shape::FillRule; pub use self::border::{ BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice, - BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, LineWidth, + BorderImageWidth, BorderRadius, BorderSideOffset, BorderSideWidth, BorderSpacing, LineWidth, }; pub use self::box_::{ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize, diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs @@ -145,7 +145,7 @@ impl Parse for LineWidth { } impl ToComputedValue for LineWidth { - type ComputedValue = app_units::Au; + type ComputedValue = Au; #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { @@ -199,23 +199,29 @@ impl Parse for BorderSideWidth { } } +// https://drafts.csswg.org/css-values-4/#snap-a-length-as-a-border-width +fn snap_as_border_width(len: Au, context: &Context) -> Au { + debug_assert!(len >= Au(0)); + + // Round `width` down to the nearest device pixel, but any non-zero value that would round + // down to zero is clamped to 1 device pixel. + if len == Au(0) { + return len; + } + + let au_per_dev_px = context.device().app_units_per_device_pixel(); + std::cmp::max( + Au(au_per_dev_px), + Au(len.0 / au_per_dev_px * au_per_dev_px), + ) +} + impl ToComputedValue for BorderSideWidth { - type ComputedValue = app_units::Au; + type ComputedValue = Au; #[inline] fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { - let width = self.0.to_computed_value(context); - // Round `width` down to the nearest device pixel, but any non-zero value that would round - // down to zero is clamped to 1 device pixel. - if width == Au(0) { - return width; - } - - let au_per_dev_px = context.device().app_units_per_device_pixel(); - std::cmp::max( - Au(au_per_dev_px), - Au(width.0 / au_per_dev_px * au_per_dev_px), - ) + snap_as_border_width(self.0.to_computed_value(context), context) } #[inline] @@ -224,6 +230,37 @@ impl ToComputedValue for BorderSideWidth { } } +/// A specified value for outline-offset. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)] +pub struct BorderSideOffset(Length); + +impl ToComputedValue for BorderSideOffset { + type ComputedValue = Au; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let offset = Au::from_f32_px(self.0.to_computed_value(context).px()); + let should_snap = match static_prefs::pref!("layout.css.outline-offset.snapping") { + 1 => true, + 2 => context.device().chrome_rules_enabled_for_document(), + _ => false, + }; + if !should_snap { + return offset; + } + if offset < Au(0) { + -snap_as_border_width(-offset, context) + } else { + snap_as_border_width(offset, context) + } + } + + #[inline] + fn from_computed_value(computed: &Au) -> Self { + Self(Length::from_px(computed.to_f32_px())) + } +} + impl BorderImageSideWidth { /// Returns `1`. #[inline] diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs @@ -38,7 +38,8 @@ pub use self::background::{BackgroundRepeat, BackgroundSize}; pub use self::basic_shape::FillRule; pub use self::border::{ BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice, - BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle, LineWidth, + BorderImageWidth, BorderRadius, BorderSideOffset, BorderSideWidth, BorderSpacing, BorderStyle, + LineWidth, }; pub use self::box_::{ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize, diff --git a/testing/web-platform/meta/css/css-borders/border-width-rounding.tentative.html.ini b/testing/web-platform/meta/css/css-borders/border-width-rounding.tentative.html.ini @@ -1,3 +1,6 @@ [border-width-rounding.tentative.html] - [Test that border widths are rounded up when they are greater than 0px but less than 1px, and rounded down when they are greater than 1px.] + [border-width: 2.999px] + expected: FAIL + + [outline-width: 2.999px] expected: FAIL diff --git a/testing/web-platform/tests/css/css-borders/border-width-rounding.tentative.html b/testing/web-platform/tests/css/css-borders/border-width-rounding.tentative.html @@ -35,19 +35,17 @@ { input: "2.999px", expected: "2px" }, ]; - for (const value of values) { + for (const { input, expected } of values) { const div = document.createElement("div"); - div.style = `border: solid ${value.input} blue; margin-bottom: 20px;`; + div.style = `border: solid ${input} blue; outline: solid ${input} purple; margin-bottom: 20px;`; document.body.appendChild(div); + test(function() { + assert_equals(getComputedStyle(div).borderWidth, expected); + }, `border-width: ${input}`); + test(function() { + assert_equals(getComputedStyle(div).outlineWidth, expected); + }, `outline-width: ${input}`); } - - test(function() { - var targets = document.querySelectorAll("div"); - - for (var i=0; i < targets.length; ++i) { - assert_equals(getComputedStyle(targets[i]).borderWidth, values[i].expected); - } - }, "Test that border widths are rounded up when they are greater than 0px but less than 1px, and rounded down when they are greater than 1px."); </script> </body> </html> diff --git a/testing/web-platform/tests/css/css-borders/outline-offset-rounding.tentative.html b/testing/web-platform/tests/css/css-borders/outline-offset-rounding.tentative.html @@ -0,0 +1,37 @@ +<!doctype html> +<title>outline-offset gets snapped like outline-width</title> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/12906"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + const values = [ + { input: "0px", expected: "0px" }, + { input: "0.1px", expected: "1px" }, + { input: "0.25px", expected: "1px" }, + { input: "0.5px", expected: "1px" }, + { input: "0.9px", expected: "1px" }, + { input: "1px", expected: "1px" }, + { input: "1.25px", expected: "1px" }, + { input: "1.5px", expected: "1px" }, + { input: "2px", expected: "2px" }, + { input: "2.75px", expected: "2px" }, + ]; + + for (const {input, expected} of values) { + test(function() { + const div = document.createElement("div"); + div.style.outlineOffset = input; + document.body.appendChild(div); + assert_equals(getComputedStyle(div).outlineOffset, expected); + }, input) + + let negExpected = input == "0px" ? "0px" : "-" + expected; + test(function() { + const div = document.createElement("div"); + div.style.outlineOffset = "-" + input; + document.body.appendChild(div); + assert_equals(getComputedStyle(div).outlineOffset, negExpected); + }, "-" + input) + } +</script> diff --git a/testing/web-platform/tests/css/css-ui/parsing/outline-offset-computed.html b/testing/web-platform/tests/css/css-ui/parsing/outline-offset-computed.html @@ -17,7 +17,7 @@ </style> <div id="target"></div> <script> -test_computed_value("outline-offset", "2.5px"); +test_computed_value("outline-offset", "2.5px", ["2.5px", "2px"]); // See https://github.com/w3c/csswg-drafts/issues/12906 test_computed_value("outline-offset", "10px"); test_computed_value("outline-offset", "0.5em", "20px"); test_computed_value("outline-offset", "calc(10px + 0.5em)", "30px"); diff --git a/widget/Theme.cpp b/widget/Theme.cpp @@ -1290,7 +1290,7 @@ void Theme::PaintAutoStyleOutline(nsIFrame* aFrame, const LayoutDeviceRect& aRect, const Colors& aColors, DPIRatio aDpiRatio) { const nscoord a2d = aFrame->PresContext()->AppUnitsPerDevPixel(); - const auto cssOffset = aFrame->StyleOutline()->mOutlineOffset.ToAppUnits(); + const auto cssOffset = aFrame->StyleOutline()->mOutlineOffset; LayoutDeviceRect rect(aRect); auto devOffset = LayoutDevicePixel::FromAppUnits(cssOffset, a2d);