Bug 1532122 - Make word-spacing, letter-spacing, and line-height use Rust lengths. r=boris
authorEmilio Cobos Álvarez <emilio@crisal.io>
Mon, 04 Mar 2019 18:19:40 +0000
changeset 520138 ea800ec9b4ae8877c2bc4b56aa7cf892a2612205
parent 520137 8f8fffb64cd134c4fc468c4d020ae9c5e199f304
child 520139 292b61ced52c4144fedc4e2cf455410cf85cde66
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersboris
bugs1532122
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1532122 - Make word-spacing, letter-spacing, and line-height use Rust lengths. r=boris This also adopts the resolution from [1] while at it, making letter-spacing compute to a length, serializing 0 to normal rather than keeping normal in the computed value, which matches every other engine. This removes the SMIL tests for percentages from letter-spacing since letter-spacing does in fact not support percentages, so they were passing just by chance. [1]: https://github.com/w3c/csswg-drafts/issues/1484 Differential Revision: https://phabricator.services.mozilla.com/D21850
dom/smil/test/db_smilCSSFromTo.js
layout/forms/nsTextControlFrame.cpp
layout/generic/ReflowInput.cpp
layout/generic/nsLineLayout.cpp
layout/generic/nsTextFrame.cpp
layout/style/ServoBindings.toml
layout/style/ServoCSSPropList.mako.py
layout/style/nsComputedDOMStyle.cpp
layout/style/nsComputedDOMStyle.h
layout/style/nsStyleCoord.h
layout/style/nsStyleStruct.cpp
layout/style/nsStyleStruct.h
layout/style/test/property_database.js
layout/style/test/test_transitions_per_property.html
servo/components/style/cbindgen.toml
servo/components/style/properties/gecko.mako.rs
servo/components/style/properties/longhands/inherited_text.mako.rs
servo/components/style/values/computed/text.rs
servo/components/style/values/generics/text.rs
testing/web-platform/meta/web-animations/animation-model/animation-types/accumulation-per-property.html.ini
testing/web-platform/meta/web-animations/animation-model/animation-types/addition-per-property.html.ini
testing/web-platform/meta/web-animations/animation-model/animation-types/interpolation-per-property.html.ini
--- a/dom/smil/test/db_smilCSSFromTo.js
+++ b/dom/smil/test/db_smilCSSFromTo.js
@@ -144,16 +144,26 @@ var _fromToTestLists = {
     new AnimTestcaseFromTo("none", "url(#idB)",
                            { toComp: "url(\"#idB\")"}),
     new AnimTestcaseFromTo("url(#idB)", "inherit",
                            { fromComp: "url(\"#idB\")",
                              toComp: "none"}),
   ],
 };
 
+function _tweakForLetterSpacing(testcases) {
+  return testcases.map(function(t) {
+    let valMap = Object.assign({}, t.computedValMap);
+    for (let prop of Object.keys(valMap))
+      if (valMap[prop] == "0px")
+        valMap[prop] = "normal";
+    return new AnimTestcaseFromTo(t.from, t.to, valMap)
+  });
+}
+
 // List of attribute/testcase-list bundles to be tested
 var gFromToBundles = [
   new TestcaseBundle(gPropList.clip, [
     new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)",
                            "rect(11px, 22px, 33px, 44px)",
                            { midComp: "rect(6px, 12px, 18px, 24px)" }),
     new AnimTestcaseFromTo("rect(1px, auto, 3px, 4px)",
                            "rect(11px, auto, 33px, 44px)",
@@ -336,23 +346,19 @@ var gFromToBundles = [
   new TestcaseBundle(gPropList.image_rendering, [
     new AnimTestcaseFromTo("auto", "optimizeQuality",
                            { toComp: "optimizequality" }),
     new AnimTestcaseFromTo("optimizeQuality", "optimizeSpeed",
                            { fromComp: "optimizequality",
                              toComp: "optimizespeed" }),
   ]),
   new TestcaseBundle(gPropList.letter_spacing,
-                     [].concat(_fromToTestLists.lengthNoUnits,
-                               _fromToTestLists.lengthPx,
-                               _fromToTestLists.lengthPxPctSVG)),
-  new TestcaseBundle(gPropList.letter_spacing,
-                     _fromToTestLists.lengthPctSVG,
-                     "pct->pct animations don't currently work for " +
-                     "*-spacing properties"),
+                     _tweakForLetterSpacing(
+                       [].concat(_fromToTestLists.lengthNoUnits,
+                                 _fromToTestLists.lengthPx))),
   new TestcaseBundle(gPropList.lighting_color,
                      [].concat(_fromToTestLists.color,
                                _fromToTestLists.colorFromInheritWhite)),
   new TestcaseBundle(gPropList.marker, _fromToTestLists.URIsAndNone),
   new TestcaseBundle(gPropList.marker_end, _fromToTestLists.URIsAndNone),
   new TestcaseBundle(gPropList.marker_mid, _fromToTestLists.URIsAndNone),
   new TestcaseBundle(gPropList.marker_start, _fromToTestLists.URIsAndNone),
   new TestcaseBundle(gPropList.mask, _fromToTestLists.URIsAndNone),
--- a/layout/forms/nsTextControlFrame.cpp
+++ b/layout/forms/nsTextControlFrame.cpp
@@ -196,22 +196,19 @@ LogicalSize nsTextControlFrame::CalcIntr
     // in Full Standards mode, see BRFrame::Reflow and bug 228752.
     if (PresContext()->CompatibilityMode() == eCompatibility_FullStandards) {
       intrinsicSize.ISize(aWM) += 1;
     }
   }
 
   // Increment width with cols * letter-spacing.
   {
-    const nsStyleCoord& lsCoord = StyleText()->mLetterSpacing;
-    if (eStyleUnit_Coord == lsCoord.GetUnit()) {
-      nscoord letterSpacing = lsCoord.GetCoordValue();
-      if (letterSpacing != 0) {
-        intrinsicSize.ISize(aWM) += cols * letterSpacing;
-      }
+    const StyleLength& letterSpacing = StyleText()->mLetterSpacing;
+    if (!letterSpacing.IsZero()) {
+      intrinsicSize.ISize(aWM) += cols * letterSpacing.ToAppUnits();
     }
   }
 
   // Set the height equal to total number of rows (times the height of each
   // line, of course)
   intrinsicSize.BSize(aWM) = lineHeight * GetRows();
 
   // Add in the size of the scrollbars for textarea
--- a/layout/generic/ReflowInput.cpp
+++ b/layout/generic/ReflowInput.cpp
@@ -729,21 +729,17 @@ void ReflowInput::InitResizeFlags(nsPres
       (mStylePosition->BSizeDependsOnContainer(wm) &&
        // FIXME: condition this on not-abspos?
        !mStylePosition->BSize(wm).IsAuto()) ||
       mStylePosition->MinBSizeDependsOnContainer(wm) ||
       mStylePosition->MaxBSizeDependsOnContainer(wm) ||
       mStylePosition->OffsetHasPercent(wm.PhysicalSide(eLogicalSideBStart)) ||
       !mStylePosition->mOffset.GetBEnd(wm).IsAuto() || mFrame->IsXULBoxFrame();
 
-  if (mStyleText->mLineHeight.GetUnit() == eStyleUnit_Enumerated) {
-    NS_ASSERTION(mStyleText->mLineHeight.GetIntValue() ==
-                     NS_STYLE_LINE_HEIGHT_BLOCK_HEIGHT,
-                 "bad line-height value");
-
+  if (mStyleText->mLineHeight.IsMozBlockHeight()) {
     // line-height depends on block bsize
     mFrame->AddStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE);
     // but only on containing blocks if this frame is not a suitable block
     dependsOnCBBSize |= !nsLayoutUtils::IsNonWrapperBlock(mFrame);
   }
 
   // If we're the descendant of a table cell that performs special bsize
   // reflows and we could be the child that requires them, always set
@@ -2844,43 +2840,36 @@ static nscoord GetNormalLineHeight(nsFon
   }
   return normalLineHeight;
 }
 
 static inline nscoord ComputeLineHeight(ComputedStyle* aComputedStyle,
                                         nsPresContext* aPresContext,
                                         nscoord aBlockBSize,
                                         float aFontSizeInflation) {
-  const nsStyleCoord& lhCoord = aComputedStyle->StyleText()->mLineHeight;
-
-  if (lhCoord.GetUnit() == eStyleUnit_Coord) {
-    nscoord result = lhCoord.GetCoordValue();
+  const StyleLineHeight& lineHeight = aComputedStyle->StyleText()->mLineHeight;
+  if (lineHeight.IsLength()) {
+    nscoord result = lineHeight.length._0.ToAppUnits();
     if (aFontSizeInflation != 1.0f) {
       result = NSToCoordRound(result * aFontSizeInflation);
     }
     return result;
   }
 
-  if (lhCoord.GetUnit() == eStyleUnit_Factor)
+  if (lineHeight.IsNumber()) {
     // For factor units the computed value of the line-height property
     // is found by multiplying the factor by the font's computed size
     // (adjusted for min-size prefs and text zoom).
-    return NSToCoordRound(lhCoord.GetFactorValue() * aFontSizeInflation *
+    return NSToCoordRound(lineHeight.number._0 * aFontSizeInflation *
                           aComputedStyle->StyleFont()->mFont.size);
-
-  NS_ASSERTION(lhCoord.GetUnit() == eStyleUnit_Normal ||
-                   lhCoord.GetUnit() == eStyleUnit_Enumerated,
-               "bad line-height unit");
+  }
 
-  if (lhCoord.GetUnit() == eStyleUnit_Enumerated) {
-    NS_ASSERTION(lhCoord.GetIntValue() == NS_STYLE_LINE_HEIGHT_BLOCK_HEIGHT,
-                 "bad line-height value");
-    if (aBlockBSize != NS_AUTOHEIGHT) {
-      return aBlockBSize;
-    }
+  MOZ_ASSERT(lineHeight.IsNormal() || lineHeight.IsMozBlockHeight());
+  if (lineHeight.IsMozBlockHeight() && aBlockBSize != NS_AUTOHEIGHT) {
+    return aBlockBSize;
   }
 
   RefPtr<nsFontMetrics> fm = nsLayoutUtils::GetFontMetricsForComputedStyle(
       aComputedStyle, aPresContext, aFontSizeInflation);
   return GetNormalLineHeight(fm);
 }
 
 nscoord ReflowInput::CalcLineHeight() const {
--- a/layout/generic/nsLineLayout.cpp
+++ b/layout/generic/nsLineLayout.cpp
@@ -1872,17 +1872,17 @@ void nsLineLayout::VerticalAlignFrames(P
         mBlockReflowInput->ComputedHeight(), inflation);
     nscoord contentBSize = spanFramePFD->mBounds.BSize(lineWM) -
                            spanFramePFD->mBorderPadding.BStartEnd(lineWM);
 
     // Special-case for a ::first-letter frame, set the line height to
     // the frame block size if the user has left line-height == normal
     const nsStyleText* styleText = spanFrame->StyleText();
     if (spanFramePFD->mIsLetterFrame && !spanFrame->GetPrevInFlow() &&
-        styleText->mLineHeight.GetUnit() == eStyleUnit_Normal) {
+        styleText->mLineHeight.IsNormal()) {
       logicalBSize = spanFramePFD->mBounds.BSize(lineWM);
     }
 
     nscoord leading = logicalBSize - contentBSize;
     psd->mBStartLeading = leading / 2;
     psd->mBEndLeading = leading - psd->mBStartLeading;
     psd->mLogicalBSize = logicalBSize;
     AdjustLeadings(spanFrame, psd, styleText, inflation, &zeroEffectiveSpanBox);
@@ -2186,17 +2186,17 @@ void nsLineLayout::VerticalAlignFrames(P
       //           rules of <hr> in quirks.css have pseudo text contents with LF
       //           in them.
       bool canUpdate;
       if (pfd->mIsTextFrame) {
         // Only consider text frames if they're not empty and
         // line-height=normal.
         canUpdate =
             pfd->mIsNonWhitespaceTextFrame &&
-            frame->StyleText()->mLineHeight.GetUnit() == eStyleUnit_Normal;
+            frame->StyleText()->mLineHeight.IsNormal();
       } else {
         canUpdate = !pfd->mIsPlaceholder;
       }
 
       if (canUpdate) {
         nscoord blockStart, blockEnd;
         if (frameSpan) {
           // For spans that were are now placing, use their position
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -1724,17 +1724,17 @@ static bool HasTerminalNewline(const nsT
 static gfxFont::Metrics GetFirstFontMetrics(gfxFontGroup* aFontGroup,
                                             bool aVerticalMetrics) {
   if (!aFontGroup) return gfxFont::Metrics();
   gfxFont* font = aFontGroup->GetFirstValidFont();
   return font->GetMetrics(aVerticalMetrics ? gfxFont::eVertical
                                            : gfxFont::eHorizontal);
 }
 
-static gfxFloat GetSpaceWidthAppUnits(const gfxTextRun* aTextRun) {
+static nscoord GetSpaceWidthAppUnits(const gfxTextRun* aTextRun) {
   // Round the space width when converting to appunits the same way textruns
   // do.
   gfxFloat spaceWidthAppUnits =
       NS_round(GetFirstFontMetrics(aTextRun->GetFontGroup(),
                                    aTextRun->UseCenterBaseline())
                    .spaceWidth *
                aTextRun->GetAppUnitsPerDevUnit());
 
@@ -1752,65 +1752,51 @@ static gfxFloat GetMinTabAdvanceAppUnits
 static nscoord LetterSpacing(nsIFrame* aFrame,
                              const nsStyleText* aStyleText = nullptr) {
   if (nsSVGUtils::IsInSVGTextSubtree(aFrame)) {
     return 0;
   }
   if (!aStyleText) {
     aStyleText = aFrame->StyleText();
   }
-
-  const nsStyleCoord& coord = aStyleText->mLetterSpacing;
-  if (eStyleUnit_Coord == coord.GetUnit()) {
-    return coord.GetCoordValue();
-  }
-  return 0;
+  return aStyleText->mLetterSpacing.ToAppUnits();
 }
 
 // This function converts non-coord values (e.g. percentages) to nscoord.
 static nscoord WordSpacing(nsIFrame* aFrame, const gfxTextRun* aTextRun,
                            const nsStyleText* aStyleText = nullptr) {
   if (nsSVGUtils::IsInSVGTextSubtree(aFrame)) {
     return 0;
   }
   if (!aStyleText) {
     aStyleText = aFrame->StyleText();
   }
 
-  const nsStyleCoord& coord = aStyleText->mWordSpacing;
-  if (coord.IsCoordPercentCalcUnit()) {
-    nscoord pctBasis = coord.HasPercent() ? GetSpaceWidthAppUnits(aTextRun) : 0;
-    return coord.ComputeCoordPercentCalc(pctBasis);
-  }
-  return 0;
+  return aStyleText->mWordSpacing.Resolve([&] {
+    return GetSpaceWidthAppUnits(aTextRun);
+  });
 }
 
 // Returns gfxTextRunFactory::TEXT_ENABLE_SPACING if non-standard
 // letter-spacing or word-spacing is present.
 static gfx::ShapedTextFlags GetSpacingFlags(
     nsIFrame* aFrame, const nsStyleText* aStyleText = nullptr) {
   if (nsSVGUtils::IsInSVGTextSubtree(aFrame)) {
     return gfx::ShapedTextFlags();
   }
 
   const nsStyleText* styleText = aFrame->StyleText();
-  const nsStyleCoord& ls = styleText->mLetterSpacing;
-  const nsStyleCoord& ws = styleText->mWordSpacing;
+  const auto& ls = styleText->mLetterSpacing;
+  const auto& ws = styleText->mWordSpacing;
 
   // It's possible to have a calc() value that computes to zero but for which
   // IsDefinitelyZero() is false, in which case we'll return
   // TEXT_ENABLE_SPACING unnecessarily. That's ok because such cases are likely
   // to be rare, and avoiding TEXT_ENABLE_SPACING is just an optimization.
-  bool nonStandardSpacing =
-      (eStyleUnit_Coord == ls.GetUnit() && ls.GetCoordValue() != 0) ||
-      (eStyleUnit_Coord == ws.GetUnit() && ws.GetCoordValue() != 0) ||
-      (eStyleUnit_Percent == ws.GetUnit() && ws.GetPercentValue() != 0) ||
-      (eStyleUnit_Calc == ws.GetUnit() &&
-       !ws.GetCalcValue()->IsDefinitelyZero());
-
+  bool nonStandardSpacing = !ls.IsZero() || !ws.IsDefinitelyZero();
   return nonStandardSpacing ? gfx::ShapedTextFlags::TEXT_ENABLE_SPACING
                             : gfx::ShapedTextFlags();
 }
 
 bool BuildTextRunsScanner::ContinueTextRunAcrossFrames(nsTextFrame* aFrame1,
                                                        nsTextFrame* aFrame2) {
   // We don't need to check font size inflation, since
   // |FindLineContainer| above (via |nsIFrame::CanContinueTextRun|)
--- a/layout/style/ServoBindings.toml
+++ b/layout/style/ServoBindings.toml
@@ -441,16 +441,18 @@ cbindgen-types = [
     { gecko = "StyleNonNegativeNumber", servo = "values::computed::NonNegativeNumber" },
     { gecko = "StylePercentage", servo = "values::computed::Percentage" },
     { gecko = "StylePerspective", servo = "values::computed::Perspective" },
     { gecko = "StyleGenericPerspective", servo = "values::generics::box_::Perspective" },
     { gecko = "StyleZIndex", servo = "values::computed::ZIndex" },
     { gecko = "StyleGenericZIndex", servo = "values::generics::position::ZIndex" },
     { gecko = "StyleTransformOrigin", servo = "values::computed::TransformOrigin" },
     { gecko = "StyleGenericBorderRadius", servo = "values::generics::border::BorderRadius" },
+    { gecko = "StyleLetterSpacing", servo = "values::computed::text::LetterSpacing" },
+    { gecko = "StyleGenericLineHeight", servo = "values::generics::text::LineHeight" },
 ]
 
 mapped-generic-types = [
     { generic = true, gecko = "mozilla::RustCell", servo = "::std::cell::Cell" },
     { generic = false, gecko = "ServoNodeData", servo = "AtomicRefCell<ElementData>" },
     { generic = false, gecko = "mozilla::ServoWritingMode", servo = "::logical_geometry::WritingMode" },
     { generic = false, gecko = "mozilla::ServoCustomPropertiesMap", servo = "Option<::servo_arc::Arc<::custom_properties::CustomPropertiesMap>>" },
     { generic = false, gecko = "mozilla::ServoRuleNode", servo = "Option<::rule_tree::StrongRuleNode>" },
--- a/layout/style/ServoCSSPropList.mako.py
+++ b/layout/style/ServoCSSPropList.mako.py
@@ -96,17 +96,16 @@ LONGHANDS_NOT_SERIALIZED_WITH_SERVO = [
     "grid-auto-flow",
     "grid-auto-rows",
     "grid-column-end",
     "grid-column-start",
     "grid-row-end",
     "grid-row-start",
     "grid-template-areas",
     "initial-letter",
-    "letter-spacing",
     "marker-end",
     "marker-mid",
     "marker-start",
     "max-block-size",
     "max-height",
     "max-inline-size",
     "max-width",
     "min-block-size",
@@ -135,17 +134,16 @@ LONGHANDS_NOT_SERIALIZED_WITH_SERVO = [
     "text-shadow",
     "touch-action",
     "transition-delay",
     "transition-duration",
     "transition-property",
     "vertical-align",
     "-webkit-text-stroke-width",
     "will-change",
-    "word-spacing",
 ]
 
 def serialized_by_servo(prop):
     # If the property requires layout information, no such luck.
     if "GETCS_NEEDS_LAYOUT_FLUSH" in prop.flags:
         return False
     if prop.type() == "shorthand":
         # FIXME: Need to serialize a value interpolated with currentcolor
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -2007,24 +2007,35 @@ already_AddRefed<CSSValue> nsComputedDOM
     valueList->AppendCSSValue(second.forget());
     return valueList.forget();
   }
 }
 
 already_AddRefed<CSSValue> nsComputedDOMStyle::DoGetLineHeight() {
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
 
-  nscoord lineHeight;
-  if (GetLineHeightCoord(lineHeight)) {
-    val->SetAppUnits(lineHeight);
+  {
+    nscoord lineHeight;
+    if (GetLineHeightCoord(lineHeight)) {
+      val->SetAppUnits(lineHeight);
+      return val.forget();
+    }
+  }
+
+  auto& lh = StyleText()->mLineHeight;
+  if (lh.IsLength()) {
+    val->SetAppUnits(lh.length._0.ToAppUnits());
+  } else if (lh.IsNumber()) {
+    val->SetNumber(lh.number._0);
+  } else if (lh.IsMozBlockHeight()) {
+    val->SetIdent(eCSSKeyword__moz_block_height);
   } else {
-    SetValueToCoord(val, StyleText()->mLineHeight, true, nullptr,
-                    nsCSSProps::kLineHeightKTable);
+    MOZ_ASSERT(lh.IsNormal());
+    val->SetIdent(eCSSKeyword_normal);
   }
-
   return val.forget();
 }
 
 already_AddRefed<CSSValue> nsComputedDOMStyle::DoGetVerticalAlign() {
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
   SetValueToCoord(val, StyleDisplay()->mVerticalAlign, false, nullptr,
                   nsCSSProps::kVerticalAlignKTable);
   return val.forget();
@@ -2183,28 +2194,16 @@ already_AddRefed<CSSValue> nsComputedDOM
   valueList->AppendCSSValue(second.forget());
   return valueList.forget();
 }
 
 already_AddRefed<CSSValue> nsComputedDOMStyle::DoGetTextShadow() {
   return GetCSSShadowArray(StyleText()->mTextShadow, false);
 }
 
-already_AddRefed<CSSValue> nsComputedDOMStyle::DoGetLetterSpacing() {
-  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
-  SetValueToCoord(val, StyleText()->mLetterSpacing, false);
-  return val.forget();
-}
-
-already_AddRefed<CSSValue> nsComputedDOMStyle::DoGetWordSpacing() {
-  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
-  SetValueToCoord(val, StyleText()->mWordSpacing, false);
-  return val.forget();
-}
-
 already_AddRefed<CSSValue> nsComputedDOMStyle::DoGetWebkitTextStrokeWidth() {
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
   val->SetAppUnits(StyleText()->mWebkitTextStrokeWidth);
   return val.forget();
 }
 
 static_assert(NS_STYLE_UNICODE_BIDI_NORMAL == 0,
               "unicode-bidi style constants not as expected");
@@ -2669,17 +2668,17 @@ already_AddRefed<CSSValue> nsComputedDOM
 
   return val.forget();
 }
 
 bool nsComputedDOMStyle::GetLineHeightCoord(nscoord& aCoord) {
   AssertFlushedPendingReflows();
 
   nscoord blockHeight = NS_AUTOHEIGHT;
-  if (StyleText()->mLineHeight.GetUnit() == eStyleUnit_Enumerated) {
+  if (StyleText()->mLineHeight.IsMozBlockHeight()) {
     if (!mInnerFrame) return false;
 
     if (nsLayoutUtils::IsNonWrapperBlock(mInnerFrame)) {
       blockHeight = mInnerFrame->GetContentRect().height;
     } else {
       GetCBContentHeight(blockHeight);
     }
   }
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -289,17 +289,16 @@ class nsComputedDOMStyle final : public 
   already_AddRefed<CSSValue> DoGetBorderBottomWidth();
   already_AddRefed<CSSValue> DoGetBorderLeftWidth();
   already_AddRefed<CSSValue> DoGetBorderRightWidth();
   already_AddRefed<CSSValue> DoGetBorderBottomLeftRadius();
   already_AddRefed<CSSValue> DoGetBorderBottomRightRadius();
   already_AddRefed<CSSValue> DoGetBorderTopLeftRadius();
   already_AddRefed<CSSValue> DoGetBorderTopRightRadius();
 
-
   /* Border Image */
   already_AddRefed<CSSValue> DoGetBorderImageWidth();
 
   /* Box Shadow */
   already_AddRefed<CSSValue> DoGetBoxShadow();
 
   /* Margin Properties */
   already_AddRefed<CSSValue> DoGetMarginTopWidth();
@@ -309,30 +308,27 @@ class nsComputedDOMStyle final : public 
 
   /* Outline Properties */
   already_AddRefed<CSSValue> DoGetOutlineWidth();
   already_AddRefed<CSSValue> DoGetOutlineRadiusBottomLeft();
   already_AddRefed<CSSValue> DoGetOutlineRadiusBottomRight();
   already_AddRefed<CSSValue> DoGetOutlineRadiusTopLeft();
   already_AddRefed<CSSValue> DoGetOutlineRadiusTopRight();
 
-
   /* Text Properties */
   already_AddRefed<CSSValue> DoGetInitialLetter();
   already_AddRefed<CSSValue> DoGetLineHeight();
   already_AddRefed<CSSValue> DoGetTextDecoration();
   already_AddRefed<CSSValue> DoGetTextDecorationColor();
   already_AddRefed<CSSValue> DoGetTextDecorationLine();
   already_AddRefed<CSSValue> DoGetTextDecorationStyle();
   already_AddRefed<CSSValue> DoGetTextEmphasisPosition();
   already_AddRefed<CSSValue> DoGetTextEmphasisStyle();
   already_AddRefed<CSSValue> DoGetTextOverflow();
   already_AddRefed<CSSValue> DoGetTextShadow();
-  already_AddRefed<CSSValue> DoGetLetterSpacing();
-  already_AddRefed<CSSValue> DoGetWordSpacing();
   already_AddRefed<CSSValue> DoGetWebkitTextStrokeWidth();
 
   /* Display properties */
   already_AddRefed<CSSValue> DoGetBinding();
   already_AddRefed<CSSValue> DoGetDisplay();
   already_AddRefed<CSSValue> DoGetContain();
   already_AddRefed<CSSValue> DoGetWillChange();
   already_AddRefed<CSSValue> DoGetTouchAction();
@@ -374,18 +370,16 @@ class nsComputedDOMStyle final : public 
 
   /* SVG properties */
   already_AddRefed<CSSValue> DoGetFill();
   already_AddRefed<CSSValue> DoGetStroke();
   already_AddRefed<CSSValue> DoGetMarkerEnd();
   already_AddRefed<CSSValue> DoGetMarkerMid();
   already_AddRefed<CSSValue> DoGetMarkerStart();
 
-
-
   already_AddRefed<CSSValue> DoGetFilter();
   already_AddRefed<CSSValue> DoGetPaintOrder();
 
   // For working around a MSVC bug. See related comment in
   // GenerateComputedDOMStyleGenerated.py.
   already_AddRefed<CSSValue> DummyGetter();
 
   /* Helper functions */
--- a/layout/style/nsStyleCoord.h
+++ b/layout/style/nsStyleCoord.h
@@ -41,16 +41,20 @@ enum LogicalCorner {
 
 using LengthPercentage = StyleLengthPercentage;
 using LengthPercentageOrAuto = StyleLengthPercentageOrAuto;
 using NonNegativeLengthPercentage = StyleNonNegativeLengthPercentage;
 using NonNegativeLengthPercentageOrAuto =
     StyleNonNegativeLengthPercentageOrAuto;
 using BorderRadius = StyleBorderRadius;
 
+bool StyleCSSPixelLength::IsZero() const {
+  return _0 == 0.0f;
+}
+
 nscoord StyleCSSPixelLength::ToAppUnits() const {
   // We want to resolve the length part of the calc() expression rounding 0.5
   // away from zero, instead of the default behavior of NSToCoordRoundWithClamp
   // which is floor(x + 0.5).
   //
   // This is what the rust code in the app_units crate does, and not doing this
   // would regress bug 1323735, for example.
   //
@@ -90,28 +94,32 @@ bool LengthPercentage::HasPercent() cons
 bool LengthPercentage::ConvertsToLength() const { return !HasPercent(); }
 
 nscoord LengthPercentage::ToLength() const {
   MOZ_ASSERT(ConvertsToLength());
   return length.ToAppUnits();
 }
 
 bool LengthPercentage::ConvertsToPercentage() const {
-  return has_percentage && length._0 == 0.0f;
+  return has_percentage && length.IsZero();
 }
 
 float LengthPercentage::ToPercentage() const {
   MOZ_ASSERT(ConvertsToPercentage());
   return Percentage();
 }
 
 bool LengthPercentage::HasLengthAndPercentage() const {
   return !ConvertsToLength() && !ConvertsToPercentage();
 }
 
+bool LengthPercentage::IsDefinitelyZero() const {
+  return length.IsZero() && Percentage() == 0.0f;
+}
+
 CSSCoord LengthPercentage::ResolveToCSSPixels(CSSCoord aPercentageBasis) const {
   return LengthInCSSPixels() + Percentage() * aPercentageBasis;
 }
 
 template <typename T>
 CSSCoord LengthPercentage::ResolveToCSSPixelsWith(T aPercentageGetter) const {
   static_assert(std::is_same<decltype(aPercentageGetter()), CSSCoord>::value,
                 "Should return CSS pixels");
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -3706,19 +3706,19 @@ nsStyleText::nsStyleText(const Document&
           nsLayoutUtils::ControlCharVisibilityDefault()),
       mTextEmphasisStyle(NS_STYLE_TEXT_EMPHASIS_STYLE_NONE),
       mTextRendering(StyleTextRendering::Auto),
       mTextEmphasisColor(StyleComplexColor::CurrentColor()),
       mWebkitTextFillColor(StyleComplexColor::CurrentColor()),
       mWebkitTextStrokeColor(StyleComplexColor::CurrentColor()),
       mMozTabSize(
           StyleNonNegativeLengthOrNumber::Number(NS_STYLE_TABSIZE_INITIAL)),
-      mWordSpacing(0, nsStyleCoord::CoordConstructor),
-      mLetterSpacing(eStyleUnit_Normal),
-      mLineHeight(eStyleUnit_Normal),
+      mWordSpacing(LengthPercentage::Zero()),
+      mLetterSpacing({0.}),
+      mLineHeight(StyleLineHeight::Normal()),
       mTextIndent(LengthPercentage::Zero()),
       mWebkitTextStrokeWidth(0),
       mTextShadow(nullptr) {
   MOZ_COUNT_CTOR(nsStyleText);
   RefPtr<nsAtom> language = aDocument.GetContentLanguageAsAtomForStyle();
   mTextEmphasisPosition =
       language && nsStyleUtil::MatchesLanguagePrefix(language, u"zh")
           ? NS_STYLE_TEXT_EMPHASIS_POSITION_DEFAULT_ZH
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -1471,19 +1471,19 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   uint8_t mTextEmphasisPosition;    // NS_STYLE_TEXT_EMPHASIS_POSITION_*
   uint8_t mTextEmphasisStyle;       // NS_STYLE_TEXT_EMPHASIS_STYLE_*
   mozilla::StyleTextRendering mTextRendering;
   mozilla::StyleComplexColor mTextEmphasisColor;
   mozilla::StyleComplexColor mWebkitTextFillColor;
   mozilla::StyleComplexColor mWebkitTextStrokeColor;
 
   mozilla::StyleNonNegativeLengthOrNumber mMozTabSize;
-  nsStyleCoord mWordSpacing;    // coord, percent, calc
-  nsStyleCoord mLetterSpacing;  // coord, normal
-  nsStyleCoord mLineHeight;     // coord, factor, normal
+  mozilla::LengthPercentage mWordSpacing;
+  mozilla::StyleLetterSpacing mLetterSpacing;
+  mozilla::StyleLineHeight mLineHeight;
   mozilla::LengthPercentage mTextIndent;
   nscoord mWebkitTextStrokeWidth;  // coord
 
   RefPtr<nsCSSShadowArray> mTextShadow;  // nullptr in case of a zero-length
 
   nsString mTextEmphasisStyleString;
 
   mozilla::StyleWordBreak EffectiveWordBreak() const {
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -3957,21 +3957,19 @@ var gCSSProperties = {
   },
   "letter-spacing": {
     domProp: "letterSpacing",
     inherited: true,
     type: CSS_TYPE_LONGHAND,
     applies_to_first_letter: true,
     applies_to_first_line: true,
     applies_to_placeholder: true,
-    initial_values: [ "normal" ],
-    other_values: [ "0", "0px", "1em", "2px", "-3px",
-      "calc(0px)", "calc(1em)", "calc(1em + 3px)",
-      "calc(15px / 2)", "calc(15px/2)", "calc(-3px)"
-    ],
+    initial_values: [ "normal", "0", "0px", "calc(0px)" ],
+    other_values: [ "1em", "2px", "-3px", "calc(1em)", "calc(1em + 3px)",
+      "calc(15px / 2)", "calc(15px/2)", "calc(-3px)" ],
     invalid_values: [],
     quirks_values: { "5": "5px" },
   },
   "line-height": {
     domProp: "lineHeight",
     inherited: true,
     type: CSS_TYPE_LONGHAND,
     applies_to_first_letter: true,
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -1279,21 +1279,20 @@ function test_length_clamped(prop) {
 function test_length_unclamped(prop) {
   test_length_clamped_or_unclamped(prop, false);
 }
 
 function test_length_clamped_or_unclamped(prop, is_clamped) {
   div.style.setProperty("transition-timing-function", FUNC_NEGATIVE, "");
   div.style.setProperty("transition-property", "none", "");
   div.style.setProperty(prop, "0px", "");
-  is(cs.getPropertyValue(prop), "0px",
-     "length-valued property " + prop + ": flush before clamping test");
+  let zero_val = cs.getPropertyValue(prop); // Flushes
   div.style.setProperty("transition-property", prop, "");
   div.style.setProperty(prop, "100px", "");
-  (is_clamped ? is : isnot)(cs.getPropertyValue(prop), "0px",
+  (is_clamped ? is : isnot)(cs.getPropertyValue(prop), zero_val,
      "length-valued property " + prop + ": clamping of negatives");
   div.style.setProperty("transition-timing-function", "linear", "");
 }
 
 // Test transition to/from the special 'flex-basis: content' keyword.
 function test_flex_basis_content_transition(prop) {
   is(prop, "flex-basis", "this test function should only be called for 'flex-basis'");
 
--- a/servo/components/style/cbindgen.toml
+++ b/servo/components/style/cbindgen.toml
@@ -73,18 +73,20 @@ include = [
   "OverscrollBehavior",
   "ScrollSnapAlign",
   "ScrollSnapType",
   "OverflowAnchor",
   "OverflowClipBox",
   "Resize",
   "Overflow",
   "LengthPercentage",
+  "LetterSpacing",
   "NonNegativeLengthPercentage",
   "LengthPercentageOrAuto",
+  "LineHeight",
   "NonNegativeLengthPercentageOrAuto",
   "Rect",
   "IntersectionObserverRootMargin",
   "Size",
   "MaxSize",
   "FlexBasis",
   "Position",
   "BackgroundSize",
@@ -97,32 +99,34 @@ include = [
   "TransformOrigin",
   "WordBreak",
 ]
 item_types = ["enums", "structs", "typedefs"]
 
 [export.body]
 "CSSPixelLength" = """
   inline nscoord ToAppUnits() const;
+  inline bool IsZero() const;
 """
 
 "LengthPercentage" = """
   // Defined in nsStyleCoord.h
   static constexpr inline StyleLengthPercentage Zero();
   static inline StyleLengthPercentage FromAppUnits(nscoord);
   static inline StyleLengthPercentage FromPixels(CSSCoord);
   static inline StyleLengthPercentage FromPercentage(float);
   inline CSSCoord LengthInCSSPixels() const;
   inline float Percentage() const;
   inline bool HasPercent() const;
   inline bool ConvertsToLength() const;
   inline nscoord ToLength() const;
   inline bool ConvertsToPercentage() const;
   inline bool HasLengthAndPercentage() const;
   inline float ToPercentage() const;
+  inline bool IsDefinitelyZero() const;
   inline CSSCoord ResolveToCSSPixels(CSSCoord aPercentageBasisInCSSPixels) const;
   template<typename T> inline CSSCoord ResolveToCSSPixelsWith(T aPercentageGetter) const;
   template<typename T, typename U>
   inline nscoord Resolve(T aPercentageGetter, U aPercentRoundingFunction) const;
   template<typename T>
   inline nscoord Resolve(nscoord aPercentageBasis, T aPercentRoundingFunction) const;
   template<typename T> inline nscoord Resolve(T aPercentageGetter) const;
   inline nscoord Resolve(nscoord aPercentageBasis) const;
--- a/servo/components/style/properties/gecko.mako.rs
+++ b/servo/components/style/properties/gecko.mako.rs
@@ -1222,101 +1222,65 @@ impl Clone for ${style_struct.gecko_stru
 <%def name="impl_trait(style_struct_name, skip_longhands='')">
 <%
     style_struct = next(x for x in data.style_structs if x.name == style_struct_name)
     longhands = [x for x in style_struct.longhands
                 if not (skip_longhands == "*" or x.name in skip_longhands.split())]
 
     # Types used with predefined_type()-defined properties that we can auto-generate.
     predefined_types = {
-        "Appearance": impl_simple,
-        "OverscrollBehavior": impl_simple,
-        "OverflowClipBox": impl_simple,
-        "ScrollSnapAlign": impl_simple,
-        "ScrollSnapType": impl_simple,
-        "Float": impl_simple,
-        "Overflow": impl_simple,
-        "BreakBetween": impl_simple,
-        "BreakWithin": impl_simple,
-        "Resize": impl_simple,
         "Color": impl_color,
         "ColorOrAuto": impl_color,
-        "GreaterThanOrEqualToOneNumber": impl_simple,
-        "Integer": impl_simple,
         "length::LengthOrAuto": impl_style_coord,
         "length::LengthOrNormal": impl_style_coord,
         "length::NonNegativeLengthOrAuto": impl_style_coord,
         "length::NonNegativeLengthPercentageOrNormal": impl_style_coord,
-        "FillRule": impl_simple,
-        "FlexBasis": impl_simple,
         "Length": impl_absolute_length,
         "LengthOrNormal": impl_style_coord,
-        "LengthPercentage": impl_simple,
         "LengthPercentageOrAuto": impl_style_coord,
-        "MaxSize": impl_simple,
-        "Size": impl_simple,
         "MozScriptMinSize": impl_absolute_length,
-        "MozScriptSizeMultiplier": impl_simple,
-        "NonNegativeLengthPercentage": impl_simple,
-        "NonNegativeLengthOrNumber": impl_simple,
-        "NonNegativeLengthOrNumberRect": impl_simple,
-        "BorderImageSlice": impl_simple,
-        "NonNegativeNumber": impl_simple,
-        "Number": impl_simple,
-        "Opacity": impl_simple,
-        "OverflowWrap": impl_simple,
-        "OverflowAnchor": impl_simple,
-        "Perspective": impl_simple,
-        "Position": impl_simple,
         "RGBAColor": impl_rgba_color,
         "SVGLength": impl_svg_length,
         "SVGOpacity": impl_svg_opacity,
         "SVGPaint": impl_svg_paint,
         "SVGWidth": impl_svg_length,
         "Transform": impl_transform,
-        "TransformOrigin": impl_simple,
-        "UserSelect": impl_simple,
         "url::UrlOrNone": impl_css_url,
-        "WordBreak": impl_simple,
-        "ZIndex": impl_simple,
     }
 
     def longhand_method(longhand):
         args = dict(ident=longhand.ident, gecko_ffi_name=longhand.gecko_ffi_name)
 
         # get the method and pass additional keyword or type-specific arguments
         if longhand.logical:
             method = impl_logical
             args.update(name=longhand.name)
         elif longhand.keyword:
             method = impl_keyword
             args.update(keyword=longhand.keyword)
             if "font" in longhand.ident:
                 args.update(cast_type=longhand.cast_type)
+        elif longhand.predefined_type in predefined_types:
+            method = predefined_types[longhand.predefined_type]
         else:
-            method = predefined_types[longhand.predefined_type]
+            method = impl_simple
 
         method(**args)
-
-    picked_longhands = []
-    for x in longhands:
-        if x.keyword or x.predefined_type in predefined_types or x.logical:
-            picked_longhands.append(x)
 %>
 impl ${style_struct.gecko_struct_name} {
     /*
      * Manually-Implemented Methods.
      */
     ${caller.body().strip()}
 
     /*
      * Auto-Generated Methods.
      */
     <%
-    for longhand in picked_longhands:
+    for longhand in longhands:
         longhand_method(longhand)
     %>
 }
 </%def>
 
 <%!
 class Side(object):
     def __init__(self, name, index):
@@ -1987,16 +1951,17 @@ fn static_assert() {
 
     pub fn outline_has_nonzero_width(&self) -> bool {
         self.gecko.mActualOutlineWidth != 0
     }
 </%self:impl_trait>
 
 <%
     skip_font_longhands = """font-family font-size font-size-adjust font-weight
+                             font-style font-stretch -moz-script-level
                              font-synthesis -x-lang font-variant-alternates
                              font-variant-east-asian font-variant-ligatures
                              font-variant-numeric font-language-override
                              font-feature-settings font-variation-settings
                              -moz-min-font-size-ratio -x-text-zoom"""
 %>
 <%self:impl_trait style_struct_name="Font"
     skip_longhands="${skip_font_longhands}">
@@ -2778,16 +2743,17 @@ fn static_assert() {
 </%def>
 
 <% skip_box_longhands= """display vertical-align
                           animation-name animation-delay animation-duration
                           animation-direction animation-fill-mode animation-play-state
                           animation-iteration-count animation-timing-function
                           clear transition-duration transition-delay
                           transition-timing-function transition-property
+                          transform-style
                           rotate scroll-snap-points-x scroll-snap-points-y
                           scroll-snap-coordinate -moz-binding will-change
                           offset-path shape-outside contain touch-action
                           translate scale""" %>
 <%self:impl_trait style_struct_name="Box" skip_longhands="${skip_box_longhands}">
     #[inline]
     pub fn generate_combined_transform(&mut self) {
         unsafe { bindings::Gecko_StyleDisplay_GenerateCombinedTransform(&mut self.gecko) };
@@ -4153,17 +4119,17 @@ fn static_assert() {
             Au(self.gecko.mBorderSpacingCol).into(),
             Au(self.gecko.mBorderSpacingRow).into()
         )
     }
 </%self:impl_trait>
 
 
 <%self:impl_trait style_struct_name="InheritedText"
-                  skip_longhands="text-align text-emphasis-style text-shadow line-height letter-spacing word-spacing
+                  skip_longhands="text-align text-emphasis-style text-shadow
                                   -webkit-text-stroke-width text-emphasis-position">
 
     <% text_align_keyword = Keyword("text-align",
                                     "start end left right center justify -moz-center -moz-left -moz-right char",
                                     gecko_strip_moz_prefix=False) %>
     ${impl_keyword('text_align', 'mTextAlign', text_align_keyword)}
 
     pub fn set_text_shadow<I>(&mut self, v: I)
@@ -4185,88 +4151,16 @@ fn static_assert() {
         self.copy_text_shadow_from(other)
     }
 
     pub fn clone_text_shadow(&self) -> longhands::text_shadow::computed_value::T {
         let buf = self.gecko.mTextShadow.iter().map(|v| v.to_simple_shadow()).collect();
         longhands::text_shadow::computed_value::List(buf)
     }
 
-    pub fn set_line_height(&mut self, v: longhands::line_height::computed_value::T) {
-        use crate::values::generics::text::LineHeight;
-        // FIXME: Align binary representations and ditch |match| for cast + static_asserts
-        let en = match v {
-            LineHeight::Normal => CoordDataValue::Normal,
-            LineHeight::Length(val) => CoordDataValue::Coord(val.0.to_i32_au()),
-            LineHeight::Number(val) => CoordDataValue::Factor(val.0),
-            LineHeight::MozBlockHeight =>
-                    CoordDataValue::Enumerated(structs::NS_STYLE_LINE_HEIGHT_BLOCK_HEIGHT),
-        };
-        self.gecko.mLineHeight.set_value(en);
-    }
-
-    pub fn clone_line_height(&self) -> longhands::line_height::computed_value::T {
-        use crate::values::generics::text::LineHeight;
-        return match self.gecko.mLineHeight.as_value() {
-            CoordDataValue::Normal => LineHeight::Normal,
-            CoordDataValue::Coord(coord) => LineHeight::Length(Au(coord).into()),
-            CoordDataValue::Factor(n) => LineHeight::Number(n.into()),
-            CoordDataValue::Enumerated(val) if val == structs::NS_STYLE_LINE_HEIGHT_BLOCK_HEIGHT =>
-                LineHeight::MozBlockHeight,
-            _ => panic!("this should not happen"),
-        }
-    }
-
-    <%call expr="impl_coord_copy('line_height', 'mLineHeight')"></%call>
-
-    pub fn set_letter_spacing(&mut self, v: longhands::letter_spacing::computed_value::T) {
-        use crate::values::generics::text::Spacing;
-        match v {
-            Spacing::Value(value) => self.gecko.mLetterSpacing.set(value),
-            Spacing::Normal => self.gecko.mLetterSpacing.set_value(CoordDataValue::Normal)
-        }
-    }
-
-    pub fn clone_letter_spacing(&self) -> longhands::letter_spacing::computed_value::T {
-        use crate::values::computed::Length;
-        use crate::values::generics::text::Spacing;
-        debug_assert!(
-            matches!(self.gecko.mLetterSpacing.as_value(),
-                     CoordDataValue::Normal |
-                     CoordDataValue::Coord(_)),
-            "Unexpected computed value for letter-spacing");
-        Length::from_gecko_style_coord(&self.gecko.mLetterSpacing).map_or(Spacing::Normal, Spacing::Value)
-    }
-
-    <%call expr="impl_coord_copy('letter_spacing', 'mLetterSpacing')"></%call>
-
-    pub fn set_word_spacing(&mut self, v: longhands::word_spacing::computed_value::T) {
-        use crate::values::generics::text::Spacing;
-        match v {
-            Spacing::Value(lp) => self.gecko.mWordSpacing.set(lp),
-            // https://drafts.csswg.org/css-text-3/#valdef-word-spacing-normal
-            Spacing::Normal => self.gecko.mWordSpacing.set_value(CoordDataValue::Coord(0)),
-        }
-    }
-
-    pub fn clone_word_spacing(&self) -> longhands::word_spacing::computed_value::T {
-        use crate::values::computed::LengthPercentage;
-        use crate::values::generics::text::Spacing;
-        debug_assert!(
-            matches!(self.gecko.mWordSpacing.as_value(),
-                     CoordDataValue::Normal |
-                     CoordDataValue::Coord(_) |
-                     CoordDataValue::Percent(_) |
-                     CoordDataValue::Calc(_)),
-            "Unexpected computed value for word-spacing");
-        LengthPercentage::from_gecko_style_coord(&self.gecko.mWordSpacing).map_or(Spacing::Normal, Spacing::Value)
-    }
-
-    <%call expr="impl_coord_copy('word_spacing', 'mWordSpacing')"></%call>
-
     fn clear_text_emphasis_style_if_string(&mut self) {
         if self.gecko.mTextEmphasisStyle == structs::NS_STYLE_TEXT_EMPHASIS_STYLE_STRING as u8 {
             self.gecko.mTextEmphasisStyleString.truncate();
             self.gecko.mTextEmphasisStyle = structs::NS_STYLE_TEXT_EMPHASIS_STYLE_NONE as u8;
         }
     }
 
     ${impl_simple_type_with_conversion("text_emphasis_position")}
--- a/servo/components/style/properties/longhands/inherited_text.mako.rs
+++ b/servo/components/style/properties/longhands/inherited_text.mako.rs
@@ -152,17 +152,17 @@
     flags="APPLIES_TO_FIRST_LETTER APPLIES_TO_FIRST_LINE APPLIES_TO_PLACEHOLDER",
     spec="https://drafts.csswg.org/css-text/#propdef-letter-spacing",
     servo_restyle_damage="rebuild_and_reflow",
 )}
 
 ${helpers.predefined_type(
     "word-spacing",
     "WordSpacing",
-    "computed::WordSpacing::normal()",
+    "computed::WordSpacing::zero()",
     animation_value_type="ComputedValue",
     flags="APPLIES_TO_FIRST_LETTER APPLIES_TO_FIRST_LINE APPLIES_TO_PLACEHOLDER",
     spec="https://drafts.csswg.org/css-text/#propdef-word-spacing",
     servo_restyle_damage="rebuild_and_reflow",
 )}
 
 <%helpers:single_keyword
     name="white-space"
--- a/servo/components/style/values/computed/text.rs
+++ b/servo/components/style/values/computed/text.rs
@@ -2,38 +2,107 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
 
 //! Computed types for text properties.
 
 #[cfg(feature = "servo")]
 use crate::properties::StyleBuilder;
 use crate::values::computed::length::{Length, LengthPercentage};
-use crate::values::computed::{NonNegativeLength, NonNegativeNumber};
+use crate::values::computed::{Context, NonNegativeLength, NonNegativeNumber, ToComputedValue};
 use crate::values::generics::text::InitialLetter as GenericInitialLetter;
 use crate::values::generics::text::LineHeight as GenericLineHeight;
 use crate::values::generics::text::Spacing;
-use crate::values::specified::text::TextOverflowSide;
+use crate::values::specified::text::{self as specified, TextOverflowSide};
 use crate::values::specified::text::{TextEmphasisFillMode, TextEmphasisShapeKeyword};
 use crate::values::{CSSFloat, CSSInteger};
+use crate::Zero;
 use std::fmt::{self, Write};
 use style_traits::{CssWriter, ToCss};
 
 pub use crate::values::specified::{OverflowWrap, WordBreak};
 pub use crate::values::specified::TextAlignKeyword as TextAlign;
 pub use crate::values::specified::TextEmphasisPosition;
 
 /// A computed value for the `initial-letter` property.
 pub type InitialLetter = GenericInitialLetter<CSSFloat, CSSInteger>;
 
 /// A computed value for the `letter-spacing` property.
-pub type LetterSpacing = Spacing<Length>;
+#[repr(transparent)]
+#[derive(
+    Animate,
+    Clone,
+    ComputeSquaredDistance,
+    Copy,
+    Debug,
+    MallocSizeOf,
+    PartialEq,
+    ToAnimatedValue,
+    ToAnimatedZero,
+)]
+pub struct LetterSpacing(Length);
+
+impl LetterSpacing {
+    /// Return the `normal` computed value, which is just zero.
+    #[inline]
+    pub fn normal() -> Self {
+        LetterSpacing(Length::zero())
+    }
+}
+
+impl ToCss for LetterSpacing {
+    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+    where
+        W: Write,
+    {
+        // https://drafts.csswg.org/css-text/#propdef-letter-spacing
+        //
+        // For legacy reasons, a computed letter-spacing of zero yields a
+        // resolved value (getComputedStyle() return value) of normal.
+        if self.0.is_zero() {
+            return dest.write_str("normal");
+        }
+        self.0.to_css(dest)
+    }
+}
+
+impl ToComputedValue for specified::LetterSpacing {
+    type ComputedValue = LetterSpacing;
+    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+        match *self {
+            Spacing::Normal => LetterSpacing(Length::zero()),
+            Spacing::Value(ref v) => LetterSpacing(v.to_computed_value(context)),
+        }
+    }
+
+    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+        if computed.0.is_zero() {
+            return Spacing::Normal;
+        }
+        Spacing::Value(ToComputedValue::from_computed_value(&computed.0))
+    }
+}
 
 /// A computed value for the `word-spacing` property.
-pub type WordSpacing = Spacing<LengthPercentage>;
+pub type WordSpacing = LengthPercentage;
+
+impl ToComputedValue for specified::WordSpacing {
+    type ComputedValue = WordSpacing;
+
+    fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+        match *self {
+            Spacing::Normal => LengthPercentage::zero(),
+            Spacing::Value(ref v) => v.to_computed_value(context),
+        }
+    }
+
+    fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+        Spacing::Value(ToComputedValue::from_computed_value(computed))
+    }
+}
 
 /// A computed value for the `line-height` property.
 pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLength>;
 
 #[derive(Clone, Debug, MallocSizeOf, PartialEq)]
 /// text-overflow.
 /// When the specified value only has one side, that's the "second"
 /// side, and the sides are logical, so "second" means "end".  The
--- a/servo/components/style/values/generics/text.rs
+++ b/servo/components/style/values/generics/text.rs
@@ -1,18 +1,16 @@
 /* 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 https://mozilla.org/MPL/2.0/. */
 
 //! Generic types for text properties.
 
 use crate::parser::ParserContext;
-use crate::values::animated::{Animate, Procedure, ToAnimatedZero};
-use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
-use app_units::Au;
+use crate::values::animated::ToAnimatedZero;
 use cssparser::Parser;
 use style_traits::ParseError;
 
 /// A generic value for the `initial-letter` property.
 #[derive(
     Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss,
 )]
 pub enum InitialLetter<Number, Integer> {
@@ -27,17 +25,17 @@ impl<N, I> InitialLetter<N, I> {
     #[inline]
     pub fn normal() -> Self {
         InitialLetter::Normal
     }
 }
 
 /// A generic spacing value for the `letter-spacing` and `word-spacing` properties.
 #[derive(
-    Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss,
+    Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss,
 )]
 pub enum Spacing<Value> {
     /// `normal`
     Normal,
     /// `<value>`
     Value(Value),
 }
 
@@ -58,88 +56,46 @@ impl<Value> Spacing<Value> {
     where
         F: FnOnce(&ParserContext, &mut Parser<'i, 't>) -> Result<Value, ParseError<'i>>,
     {
         if input.try(|i| i.expect_ident_matching("normal")).is_ok() {
             return Ok(Spacing::Normal);
         }
         parse(context, input).map(Spacing::Value)
     }
-
-    /// Returns the spacing value, if not `normal`.
-    #[inline]
-    pub fn value(&self) -> Option<&Value> {
-        match *self {
-            Spacing::Normal => None,
-            Spacing::Value(ref value) => Some(value),
-        }
-    }
-}
-
-impl<Value> Animate for Spacing<Value>
-where
-    Value: Animate + From<Au>,
-{
-    #[inline]
-    fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
-        if let (&Spacing::Normal, &Spacing::Normal) = (self, other) {
-            return Ok(Spacing::Normal);
-        }
-        let zero = Value::from(Au(0));
-        let this = self.value().unwrap_or(&zero);
-        let other = other.value().unwrap_or(&zero);
-        Ok(Spacing::Value(this.animate(other, procedure)?))
-    }
-}
-
-impl<V> ComputeSquaredDistance for Spacing<V>
-where
-    V: ComputeSquaredDistance + From<Au>,
-{
-    #[inline]
-    fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
-        let zero = V::from(Au(0));
-        let this = self.value().unwrap_or(&zero);
-        let other = other.value().unwrap_or(&zero);
-        this.compute_squared_distance(other)
-    }
-}
-
-impl<V> ToAnimatedZero for Spacing<V> {
-    #[inline]
-    fn to_animated_zero(&self) -> Result<Self, ()> {
-        Err(())
-    }
 }
 
 /// A generic value for the `line-height` property.
 #[derive(
     Animate,
     Clone,
     ComputeSquaredDistance,
     Copy,
     Debug,
     MallocSizeOf,
     PartialEq,
     SpecifiedValueInfo,
     ToAnimatedValue,
     ToCss,
 )]
-pub enum LineHeight<Number, LengthPercentage> {
+#[repr(C, u8)]
+pub enum GenericLineHeight<N, L> {
     /// `normal`
     Normal,
     /// `-moz-block-height`
     #[cfg(feature = "gecko")]
     MozBlockHeight,
     /// `<number>`
-    Number(Number),
-    /// `<length-or-percentage>`
-    Length(LengthPercentage),
+    Number(N),
+    /// `<length-percentage>`
+    Length(L),
 }
 
+pub use self::GenericLineHeight as LineHeight;
+
 impl<N, L> ToAnimatedZero for LineHeight<N, L> {
     #[inline]
     fn to_animated_zero(&self) -> Result<Self, ()> {
         Err(())
     }
 }
 
 impl<N, L> LineHeight<N, L> {
--- a/testing/web-platform/meta/web-animations/animation-model/animation-types/accumulation-per-property.html.ini
+++ b/testing/web-platform/meta/web-animations/animation-model/animation-types/accumulation-per-property.html.ini
@@ -1,38 +1,21 @@
 prefs: [layout.css.font-variations.enabled:true, layout.css.overflow-clip-box.enabled:true, layout.css.individual-transform.enabled:true]
 [accumulation-per-property.html]
-  [word-spacing: calc]
-    expected: FAIL
-
-  [word-spacing: units "calc" onto "px"]
-    expected: FAIL
-
-  [word-spacing: units "rem" onto "%"]
-    expected: FAIL
-
-  [word-spacing: units "px" onto "%"]
-    expected: FAIL
 
   [flex-basis: units "%" onto "px"]
     expected: FAIL
 
   [flex-basis: units "%" onto "rem"]
     expected: FAIL
 
-  [word-spacing: units "%" onto "px"]
-    expected: FAIL
-
   [flex-basis: units "px" onto "%"]
     expected: FAIL
 
   [flex-basis: calc]
     expected: FAIL
 
   [flex-basis: units "rem" onto "%"]
     expected: FAIL
 
-  [word-spacing: units "%" onto "rem"]
-    expected: FAIL
-
   [flex-basis: units "calc" onto "px"]
     expected: FAIL
 
--- a/testing/web-platform/meta/web-animations/animation-model/animation-types/addition-per-property.html.ini
+++ b/testing/web-platform/meta/web-animations/animation-model/animation-types/addition-per-property.html.ini
@@ -1,38 +1,21 @@
 prefs: [layout.css.font-variations.enabled:true, layout.css.overflow-clip-box.enabled:true, layout.css.individual-transform.enabled:true]
 [addition-per-property.html]
-  [word-spacing: calc]
-    expected: FAIL
-
-  [word-spacing: units "rem" onto "%"]
-    expected: FAIL
-
-  [word-spacing: units "px" onto "%"]
-    expected: FAIL
 
   [flex-basis: units "px" onto "%"]
     expected: FAIL
 
   [flex-basis: units "%" onto "rem"]
     expected: FAIL
 
-  [word-spacing: units "calc" onto "px"]
-    expected: FAIL
-
-  [word-spacing: units "%" onto "px"]
-    expected: FAIL
-
   [flex-basis: calc]
     expected: FAIL
 
   [flex-basis: units "rem" onto "%"]
     expected: FAIL
 
   [flex-basis: units "%" onto "px"]
     expected: FAIL
 
-  [word-spacing: units "%" onto "rem"]
-    expected: FAIL
-
   [flex-basis: units "calc" onto "px"]
     expected: FAIL
 
--- a/testing/web-platform/meta/web-animations/animation-model/animation-types/interpolation-per-property.html.ini
+++ b/testing/web-platform/meta/web-animations/animation-model/animation-types/interpolation-per-property.html.ini
@@ -2,29 +2,17 @@ prefs: [layout.css.font-variations.enabl
 [interpolation-per-property.html]
   [filter: interpolate different length of filter-function-list with drop-shadow function]
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1345709
 
   [flex-basis supports animating as combination units "px" and "%"]
     expected: FAIL
 
-  [word-spacing supports animating as combination units "px" and "%"]
-    expected: FAIL
-
   [flex-basis supports animating as combination units "px" and "calc"]
     expected: FAIL
 
   [flex-basis supports animating as a calc]
     expected: FAIL
 
-  [word-spacing supports animating as combination units "px" and "calc"]
-    expected: FAIL
-
-  [word-spacing supports animating as a calc]
-    expected: FAIL
-
-  [word-spacing supports animating as combination units "%" and "em"]
-    expected: FAIL
-
   [flex-basis supports animating as combination units "%" and "em"]
     expected: FAIL