Bug 1495153 part 2 - Implement cropping the filename for <input type=file>. r=emilio,jfkthame
☠☠ backed out by 90853a59691e ☠ ☠
authorMats Palmgren <mats@mozilla.com>
Sat, 06 Oct 2018 19:31:51 +0200
changeset 495652 6d95f0e0cc76d43171e8c16ee04f56204da38462
parent 495651 6800eb348ebe37c9beb67fe76da7ec86be0f7f1f
child 495653 c62823871aca562b75ca2ed5113b5bd3788880ce
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersemilio, jfkthame
bugs1495153
milestone64.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 1495153 part 2 - Implement cropping the filename for <input type=file>. r=emilio,jfkthame
layout/forms/nsFileControlFrame.cpp
layout/forms/nsFileControlFrame.h
layout/generic/nsTextFrame.cpp
layout/generic/nsTextFrame.h
layout/reftests/forms/input/file/dynamic-max-width-ref.html
layout/reftests/forms/input/file/dynamic-max-width.html
layout/reftests/forms/input/file/reftest.list
--- a/layout/forms/nsFileControlFrame.cpp
+++ b/layout/forms/nsFileControlFrame.cpp
@@ -22,16 +22,17 @@
 #include "mozilla/dom/MutationEventBinding.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/StaticPrefs.h"
 #include "nsNodeInfoManager.h"
 #include "nsContentCreatorFunctions.h"
 #include "nsContentUtils.h"
 #include "mozilla/EventStates.h"
 #include "nsTextNode.h"
+#include "nsTextFrame.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 nsIFrame*
 NS_NewFileControlFrame(nsIPresShell* aPresShell, ComputedStyle* aStyle)
 {
   return new (aPresShell) nsFileControlFrame(aStyle);
@@ -51,16 +52,154 @@ nsFileControlFrame::Init(nsIContent*    
                          nsContainerFrame* aParent,
                          nsIFrame*         aPrevInFlow)
 {
   nsBlockFrame::Init(aContent, aParent, aPrevInFlow);
 
   mMouseListener = new DnDListener(this);
 }
 
+bool
+nsFileControlFrame::CropTextToWidth(gfxContext&     aRenderingContext,
+                                    const nsIFrame* aFrame,
+                                    nscoord         aWidth,
+                                    nsString&       aText)
+{
+  if (aText.IsEmpty()) {
+    return false;
+  }
+
+  RefPtr<nsFontMetrics> fm =
+    nsLayoutUtils::GetFontMetricsForFrame(aFrame, 1.0f);
+
+  // see if the text will completely fit in the width given
+  nscoord textWidth =
+    nsLayoutUtils::AppUnitWidthOfStringBidi(aText, aFrame, *fm,
+                                            aRenderingContext);
+  if (textWidth <= aWidth) {
+    return false;
+  }
+
+  DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+  const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis();
+
+  // see if the width is even smaller than the ellipsis
+  fm->SetTextRunRTL(false);
+  textWidth = nsLayoutUtils::AppUnitWidthOfString(kEllipsis, *fm, drawTarget);
+  if (textWidth >= aWidth) {
+    aText = kEllipsis;
+    return true;
+  }
+
+  // determine how much of the string will fit in the max width
+  nscoord totalWidth = textWidth;
+  using mozilla::unicode::ClusterIterator;
+  using mozilla::unicode::ClusterReverseIterator;
+  ClusterIterator leftIter(aText.Data(), aText.Length());
+  ClusterReverseIterator rightIter(aText.Data(), aText.Length());
+  const char16_t* leftPos = leftIter;
+  const char16_t* rightPos = rightIter;
+  const char16_t* pos;
+  ptrdiff_t length;
+  nsAutoString leftString, rightString;
+
+  while (leftPos < rightPos) {
+    leftIter.Next();
+    pos = leftIter;
+    length = pos - leftPos;
+    textWidth = nsLayoutUtils::AppUnitWidthOfString(leftPos, length,
+                                                    *fm, drawTarget);
+    if (totalWidth + textWidth > aWidth) {
+      break;
+    }
+
+    leftString.Append(leftPos, length);
+    leftPos = pos;
+    totalWidth += textWidth;
+
+    if (leftPos >= rightPos) {
+      break;
+    }
+
+    rightIter.Next();
+    pos = rightIter;
+    length = rightPos - pos;
+    textWidth = nsLayoutUtils::AppUnitWidthOfString(pos, length,
+                                                    *fm, drawTarget);
+    if (totalWidth + textWidth > aWidth) {
+      break;
+    }
+
+    rightString.Insert(pos, 0, length);
+    rightPos = pos;
+    totalWidth += textWidth;
+  }
+
+  aText = leftString + kEllipsis + rightString;
+  return true;
+}
+
+void
+nsFileControlFrame::Reflow(nsPresContext*     aPresContext,
+                           ReflowOutput&      aMetrics,
+                           const ReflowInput& aReflowInput,
+                           nsReflowStatus&    aStatus)
+{
+  // Restore the uncropped filename.
+  nsAutoString filename;
+  HTMLInputElement::FromNode(mContent)->GetDisplayFileName(filename);
+
+  bool done = false;
+  while (true) {
+    UpdateDisplayedValue(filename, false);  // update the text node
+    AddStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION);
+    LinesBegin()->MarkDirty();
+    nsBlockFrame::Reflow(aPresContext, aMetrics, aReflowInput, aStatus);
+    if (done) {
+      break;
+    }
+    nscoord lineISize = LinesBegin()->ISize();
+    const auto cbWM = aMetrics.GetWritingMode();
+    const auto wm = GetWritingMode();
+    nscoord iSize = wm.IsOrthogonalTo(cbWM) ? aMetrics.BSize(cbWM)
+                                            : aMetrics.ISize(cbWM);
+    auto bp = GetLogicalUsedBorderAndPadding(wm);
+    nscoord contentISize = iSize - bp.IStartEnd(wm);
+    if (lineISize > contentISize) {
+      // The filename overflows - crop it and reflow again (once).
+      // NOTE: the label frame might have bidi-continuations
+      auto* labelFrame = mTextContent->GetPrimaryFrame();
+      nscoord labelBP =
+        labelFrame->GetLogicalUsedBorderAndPadding(wm).IStartEnd(wm);
+      auto* lastLabelCont = labelFrame->LastContinuation();
+      if (lastLabelCont != labelFrame) {
+        labelBP +=
+          lastLabelCont->GetLogicalUsedBorderAndPadding(wm).IStartEnd(wm);
+      }
+      auto* buttonFrame = mBrowseFilesOrDirs->GetPrimaryFrame();
+      nscoord availableISizeForLabel = contentISize - buttonFrame->ISize(wm) -
+        buttonFrame->GetLogicalUsedMargin(wm).IStartEnd(wm);
+      if (CropTextToWidth(*aReflowInput.mRenderingContext,
+                          labelFrame,
+                          availableISizeForLabel - labelBP,
+                          filename)) {
+        nsBlockFrame::DidReflow(aPresContext, &aReflowInput);
+        aStatus.Reset();
+        labelFrame->AddStateBits(NS_FRAME_IS_DIRTY |
+                                 NS_BLOCK_NEEDS_BIDI_RESOLUTION);
+        mMinWidth = NS_INTRINSIC_WIDTH_UNKNOWN;
+        mPrefWidth = NS_INTRINSIC_WIDTH_UNKNOWN;
+        done = true;
+        continue;
+      }
+    }
+    break;
+  }
+}
+
 void
 nsFileControlFrame::DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData)
 {
   NS_ENSURE_TRUE_VOID(mContent);
 
   // Remove the events.
   if (mContent) {
     mContent->RemoveSystemEventListener(NS_LITERAL_STRING("drop"),
@@ -405,26 +544,43 @@ nsFileControlFrame::DnDListener::CanDrop
   uint32_t listLength = 0;
   if (fileList) {
     listLength = fileList->Length();
   }
   return listLength <= 1 || aSupportsMultiple;
 }
 
 nscoord
-nsFileControlFrame::GetMinISize(gfxContext *aRenderingContext)
+nsFileControlFrame::GetMinISize(gfxContext* aRenderingContext)
 {
   nscoord result;
   DISPLAY_MIN_INLINE_SIZE(this, result);
 
   // Our min inline size is our pref inline size
   result = GetPrefISize(aRenderingContext);
   return result;
 }
 
+nscoord
+nsFileControlFrame::GetPrefISize(gfxContext* aRenderingContext)
+{
+  nscoord result;
+  DISPLAY_MIN_INLINE_SIZE(this, result);
+
+  // Make sure we measure with the uncropped filename.
+  if (mPrefWidth == NS_INTRINSIC_WIDTH_UNKNOWN) {
+    nsAutoString filename;
+    HTMLInputElement::FromNode(mContent)->GetDisplayFileName(filename);
+    UpdateDisplayedValue(filename, false);
+  }
+
+  result = nsBlockFrame::GetPrefISize(aRenderingContext);
+  return result;
+}
+
 void
 nsFileControlFrame::SyncDisabledState()
 {
   EventStates eventStates = mContent->AsElement()->State();
   if (eventStates.HasState(NS_EVENT_STATE_DISABLED)) {
     mBrowseFilesOrDirs->SetAttr(kNameSpaceID_None, nsGkAtoms::disabled,
                                 EmptyString(), true);
   } else {
@@ -465,17 +621,25 @@ nsFileControlFrame::GetFrameName(nsAStri
   return MakeFrameName(NS_LITERAL_STRING("FileControl"), aResult);
 }
 #endif
 
 void
 nsFileControlFrame::UpdateDisplayedValue(const nsAString& aValue, bool aNotify)
 {
   auto* text = Text::FromNode(mTextContent->GetFirstChild());
+  uint32_t oldLength = aNotify ? 0 : text->TextLength();
   text->SetText(aValue, aNotify);
+  if (!aNotify) {
+    // We can't notify during Reflow so we need to tell the text frame
+    // about the text content change we just did.
+    if (auto* textFrame = static_cast<nsTextFrame*>(text->GetPrimaryFrame())) {
+      textFrame->NotifyNativeAnonymousTextnodeChange(oldLength);
+    }
+  }
 }
 
 nsresult
 nsFileControlFrame::SetFormProperty(nsAtom* aName,
                                     const nsAString& aValue)
 {
   if (nsGkAtoms::value == aName) {
     UpdateDisplayedValue(aValue, true);
--- a/layout/forms/nsFileControlFrame.h
+++ b/layout/forms/nsFileControlFrame.h
@@ -22,43 +22,49 @@ class DataTransfer;
 } // namespace dom
 } // namespace mozilla
 
 class nsFileControlFrame final : public nsBlockFrame,
                                  public nsIFormControlFrame,
                                  public nsIAnonymousContentCreator
 {
 public:
+  NS_DECL_QUERYFRAME
+  NS_DECL_FRAMEARENA_HELPERS(nsFileControlFrame)
+
   explicit nsFileControlFrame(ComputedStyle* aStyle);
 
   virtual void Init(nsIContent*       aContent,
                     nsContainerFrame* aParent,
                     nsIFrame*         aPrevInFlow) override;
 
+  void Reflow(nsPresContext*     aPresContext,
+              ReflowOutput&      aDesiredSize,
+              const ReflowInput& aReflowInput,
+              nsReflowStatus&    aStatus) override;
+
   virtual void BuildDisplayList(nsDisplayListBuilder*   aBuilder,
                                 const nsDisplayListSet& aLists) override;
 
-  NS_DECL_QUERYFRAME
-  NS_DECL_FRAMEARENA_HELPERS(nsFileControlFrame)
-
   // nsIFormControlFrame
   virtual nsresult SetFormProperty(nsAtom* aName, const nsAString& aValue) override;
   virtual void SetFocus(bool aOn, bool aRepaint) override;
 
-  virtual nscoord GetMinISize(gfxContext *aRenderingContext) override;
+  nscoord GetMinISize(gfxContext* aRenderingContext) override;
+  nscoord GetPrefISize(gfxContext* aRenderingContext) override;
 
   virtual void DestroyFrom(nsIFrame* aDestructRoot, PostDestroyData& aPostDestroyData) override;
 
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override;
 #endif
 
-  virtual nsresult AttributeChanged(int32_t         aNameSpaceID,
-                                    nsAtom*        aAttribute,
-                                    int32_t         aModType) override;
+  nsresult AttributeChanged(int32_t aNameSpaceID,
+                            nsAtom* aAttribute,
+                            int32_t aModType) override;
   virtual void ContentStatesChanged(mozilla::EventStates aStates) override;
 
   // nsIAnonymousContentCreator
   virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements) override;
   virtual void AppendAnonymousContentTo(nsTArray<nsIContent*>& aElements,
                                         uint32_t aFilter) override;
 
 #ifdef ACCESSIBILITY
@@ -147,16 +153,25 @@ protected:
   /**
    * Drag and drop mouse listener.
    * This makes sure we don't get used after destruction.
    */
   RefPtr<DnDListener> mMouseListener;
 
 protected:
   /**
+   * Crop aText to fit inside aWidth using the styles of aFrame.
+   * @return true if aText was modified
+   */
+  static bool CropTextToWidth(gfxContext&     aRenderingContext,
+                              const nsIFrame* aFrame,
+                              nscoord         aWidth,
+                              nsString&       aText);
+
+  /**
    * Sync the disabled state of the content with anonymous children.
    */
   void SyncDisabledState();
 
   /**
    * Updates the displayed value by using aValue.
    */
   void UpdateDisplayedValue(const nsAString& aValue, bool aNotify);
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -4791,16 +4791,38 @@ nsTextFrame::DisconnectTextRuns()
   MOZ_ASSERT(!IsInTextRunUserData(),
              "Textrun mentions this frame in its user data so we can't just disconnect");
   mTextRun = nullptr;
   if ((GetStateBits() & TEXT_HAS_FONT_INFLATION)) {
     DeleteProperty(UninflatedTextRunProperty());
   }
 }
 
+void
+nsTextFrame::NotifyNativeAnonymousTextnodeChange(uint32_t aOldLength)
+{
+  MOZ_ASSERT(mContent->IsInNativeAnonymousSubtree());
+
+  MarkIntrinsicISizesDirty();
+
+  // This is to avoid making a new Reflow request in CharacterDataChanged:
+  for (nsTextFrame* f = this; f; f = f->GetNextContinuation()) {
+    f->AddStateBits(NS_FRAME_IS_DIRTY);
+    f->mReflowRequestedForCharDataChange = true;
+  }
+
+  // Pretend that all the text changed.
+  CharacterDataChangeInfo info;
+  info.mAppend = false;
+  info.mChangeStart = 0;
+  info.mChangeEnd = aOldLength;
+  info.mReplaceLength = mContent->TextLength();
+  CharacterDataChanged(info);
+}
+
 nsresult
 nsTextFrame::CharacterDataChanged(const CharacterDataChangeInfo& aInfo)
 {
   if (mContent->HasFlag(NS_HAS_NEWLINE_PROPERTY)) {
     mContent->DeleteProperty(nsGkAtoms::newline);
     mContent->UnsetFlags(NS_HAS_NEWLINE_PROPERTY);
   }
   if (mContent->HasFlag(NS_HAS_FLOWLENGTH_PROPERTY)) {
--- a/layout/generic/nsTextFrame.h
+++ b/layout/generic/nsTextFrame.h
@@ -663,16 +663,23 @@ public:
 
   void AssignJustificationGaps(const mozilla::JustificationAssignment& aAssign);
   mozilla::JustificationAssignment GetJustificationAssignment() const;
 
   uint32_t CountGraphemeClusters() const;
 
   bool HasAnyNoncollapsedCharacters() override;
 
+  /**
+   * Call this after you have manually changed the text node contents without
+   * notifying that change.  This behaves as if all the text contents changed.
+   * (You should only use this for native anonymous content.)
+   */
+  void NotifyNativeAnonymousTextnodeChange(uint32_t aOldLength);
+
 protected:
   virtual ~nsTextFrame();
 
   RefPtr<gfxTextRun> mTextRun;
   nsTextFrame* mNextContinuation;
   // The key invariant here is that mContentOffset never decreases along
   // a next-continuation chain. And of course mContentOffset is always <= the
   // the text node's content length, and the mContentOffset for the first frame
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/file/dynamic-max-width-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html><head>
+  <meta charset="utf-8">
+  <title>Reference for dynamic-max-width.html</title>
+</head>
+<body>
+
+<input type=file dir=rtl>
+
+<br>
+
+<input type=file>
+
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/forms/input/file/dynamic-max-width.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<!--
+     Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html class="reftest-wait"><head>
+  <meta charset="utf-8">
+  <title>CSS Test: file control with dynamic change to max-width</title>
+  <style type="text/css">
+
+    input { max-width: 10em; }
+
+  </style>
+  <script>
+    function tweak() {
+      [...document.querySelectorAll('input')].forEach(function(e) {
+        e.style.maxWidth = 'initial';
+      });
+      document.documentElement.removeAttribute("class");
+    }
+    window.addEventListener("MozReftestInvalidate", tweak);
+  </script>
+</head>
+<body onload="test()">
+
+<input type=file dir=rtl>
+
+<br>
+
+<input type=file>
+
+
+</body>
+</html>
--- a/layout/reftests/forms/input/file/reftest.list
+++ b/layout/reftests/forms/input/file/reftest.list
@@ -1,7 +1,8 @@
 fuzzy-if(gtkWidget||webrender,0-1,0-34) fails-if(Android) == simple.html simple-ref.xul
 fuzzy-if(gtkWidget||webrender,0-1,0-17) fails-if(Android) == rtl.html rtl-ref.xul
 fuzzy-if(gtkWidget||webrender,0-1,0-34) fails-if(Android) == size.html simple-ref.xul
 fuzzy-if(gtkWidget||webrender,0-1,0-10) fails-if(Android) == background.html background-ref.xul
 fuzzy-if(gtkWidget,0-1,0-10) fails-if(Android) == style.html style-ref.xul
 != width-clip.html width-clip-ref.html
 fails-if(Android) == color-inherit.html color-inherit-ref.html
+fuzzy-if(Android,1-2,2-2) == dynamic-max-width.html dynamic-max-width-ref.html