1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 /* rendering object for CSS "display: ruby" */
8 
9 #include "nsRubyFrame.h"
10 
11 #include "RubyUtils.h"
12 #include "mozilla/ComputedStyle.h"
13 #include "mozilla/Maybe.h"
14 #include "mozilla/PresShell.h"
15 #include "mozilla/StaticPrefs_layout.h"
16 #include "mozilla/WritingModes.h"
17 #include "nsLayoutUtils.h"
18 #include "nsLineLayout.h"
19 #include "nsPresContext.h"
20 #include "nsContainerFrameInlines.h"
21 #include "nsRubyBaseContainerFrame.h"
22 #include "nsRubyTextContainerFrame.h"
23 
24 using namespace mozilla;
25 
26 //----------------------------------------------------------------------
27 
28 // Frame class boilerplate
29 // =======================
30 
31 NS_QUERYFRAME_HEAD(nsRubyFrame)
NS_QUERYFRAME_ENTRY(nsRubyFrame)32   NS_QUERYFRAME_ENTRY(nsRubyFrame)
33 NS_QUERYFRAME_TAIL_INHERITING(nsInlineFrame)
34 
35 NS_IMPL_FRAMEARENA_HELPERS(nsRubyFrame)
36 
37 nsContainerFrame* NS_NewRubyFrame(PresShell* aPresShell,
38                                   ComputedStyle* aStyle) {
39   return new (aPresShell) nsRubyFrame(aStyle, aPresShell->GetPresContext());
40 }
41 
42 //----------------------------------------------------------------------
43 
44 // nsRubyFrame Method Implementations
45 // ==================================
46 
47 /* virtual */
IsFrameOfType(uint32_t aFlags) const48 bool nsRubyFrame::IsFrameOfType(uint32_t aFlags) const {
49   if (aFlags & eBidiInlineContainer) {
50     return false;
51   }
52   return nsInlineFrame::IsFrameOfType(aFlags);
53 }
54 
55 #ifdef DEBUG_FRAME_DUMP
GetFrameName(nsAString & aResult) const56 nsresult nsRubyFrame::GetFrameName(nsAString& aResult) const {
57   return MakeFrameName(u"Ruby"_ns, aResult);
58 }
59 #endif
60 
61 /* virtual */
AddInlineMinISize(gfxContext * aRenderingContext,nsIFrame::InlineMinISizeData * aData)62 void nsRubyFrame::AddInlineMinISize(gfxContext* aRenderingContext,
63                                     nsIFrame::InlineMinISizeData* aData) {
64   auto handleChildren = [aRenderingContext](auto frame, auto data) {
65     for (RubySegmentEnumerator e(static_cast<nsRubyFrame*>(frame)); !e.AtEnd();
66          e.Next()) {
67       e.GetBaseContainer()->AddInlineMinISize(aRenderingContext, data);
68     }
69   };
70   DoInlineIntrinsicISize(aData, handleChildren);
71 }
72 
73 /* virtual */
AddInlinePrefISize(gfxContext * aRenderingContext,nsIFrame::InlinePrefISizeData * aData)74 void nsRubyFrame::AddInlinePrefISize(gfxContext* aRenderingContext,
75                                      nsIFrame::InlinePrefISizeData* aData) {
76   auto handleChildren = [aRenderingContext](auto frame, auto data) {
77     for (RubySegmentEnumerator e(static_cast<nsRubyFrame*>(frame)); !e.AtEnd();
78          e.Next()) {
79       e.GetBaseContainer()->AddInlinePrefISize(aRenderingContext, data);
80     }
81   };
82   DoInlineIntrinsicISize(aData, handleChildren);
83   aData->mLineIsEmpty = false;
84 }
85 
FindRubyBaseContainerAncestor(nsIFrame * aFrame)86 static nsRubyBaseContainerFrame* FindRubyBaseContainerAncestor(
87     nsIFrame* aFrame) {
88   for (nsIFrame* ancestor = aFrame->GetParent();
89        ancestor && ancestor->IsFrameOfType(nsIFrame::eLineParticipant);
90        ancestor = ancestor->GetParent()) {
91     if (ancestor->IsRubyBaseContainerFrame()) {
92       return static_cast<nsRubyBaseContainerFrame*>(ancestor);
93     }
94   }
95   return nullptr;
96 }
97 
98 /* virtual */
Reflow(nsPresContext * aPresContext,ReflowOutput & aDesiredSize,const ReflowInput & aReflowInput,nsReflowStatus & aStatus)99 void nsRubyFrame::Reflow(nsPresContext* aPresContext,
100                          ReflowOutput& aDesiredSize,
101                          const ReflowInput& aReflowInput,
102                          nsReflowStatus& aStatus) {
103   MarkInReflow();
104   DO_GLOBAL_REFLOW_COUNT("nsRubyFrame");
105   DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus);
106   MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
107 
108   if (!aReflowInput.mLineLayout) {
109     NS_ASSERTION(aReflowInput.mLineLayout,
110                  "No line layout provided to RubyFrame reflow method.");
111     return;
112   }
113 
114   // Grab overflow frames from prev-in-flow and its own.
115   MoveInlineOverflowToChildList(aReflowInput.mLineLayout->LineContainerFrame());
116 
117   // Clear leadings
118   mLeadings.Reset();
119 
120   // Begin the span for the ruby frame
121   WritingMode frameWM = aReflowInput.GetWritingMode();
122   WritingMode lineWM = aReflowInput.mLineLayout->GetWritingMode();
123   LogicalMargin borderPadding =
124       aReflowInput.ComputedLogicalBorderPadding(frameWM);
125   nsLayoutUtils::SetBSizeFromFontMetrics(this, aDesiredSize, borderPadding,
126                                          lineWM, frameWM);
127 
128   nscoord startEdge = 0;
129   const bool boxDecorationBreakClone =
130       StyleBorder()->mBoxDecorationBreak == StyleBoxDecorationBreak::Clone;
131   if (boxDecorationBreakClone || !GetPrevContinuation()) {
132     startEdge = borderPadding.IStart(frameWM);
133   }
134   NS_ASSERTION(aReflowInput.AvailableISize() != NS_UNCONSTRAINEDSIZE,
135                "should no longer use available widths");
136   nscoord endEdge = aReflowInput.AvailableISize() - borderPadding.IEnd(frameWM);
137   aReflowInput.mLineLayout->BeginSpan(this, &aReflowInput, startEdge, endEdge,
138                                       &mBaseline);
139 
140   for (RubySegmentEnumerator e(this); !e.AtEnd(); e.Next()) {
141     ReflowSegment(aPresContext, aReflowInput, aDesiredSize.BlockStartAscent(),
142                   aDesiredSize.BSize(lineWM), e.GetBaseContainer(), aStatus);
143 
144     if (aStatus.IsInlineBreak()) {
145       // A break occurs when reflowing the segment.
146       // Don't continue reflowing more segments.
147       break;
148     }
149   }
150 
151   ContinuationTraversingState pullState(this);
152   while (aStatus.IsEmpty()) {
153     nsRubyBaseContainerFrame* baseContainer =
154         PullOneSegment(aReflowInput.mLineLayout, pullState);
155     if (!baseContainer) {
156       // No more continuations after, finish now.
157       break;
158     }
159     ReflowSegment(aPresContext, aReflowInput, aDesiredSize.BlockStartAscent(),
160                   aDesiredSize.BSize(lineWM), baseContainer, aStatus);
161   }
162   // We never handle overflow in ruby.
163   MOZ_ASSERT(!aStatus.IsOverflowIncomplete());
164 
165   aDesiredSize.ISize(lineWM) = aReflowInput.mLineLayout->EndSpan(this);
166   if (boxDecorationBreakClone || !GetPrevContinuation()) {
167     aDesiredSize.ISize(lineWM) += borderPadding.IStart(frameWM);
168   }
169   if (boxDecorationBreakClone || aStatus.IsComplete()) {
170     aDesiredSize.ISize(lineWM) += borderPadding.IEnd(frameWM);
171   }
172 
173   // Update descendant leadings of ancestor ruby base container.
174   if (nsRubyBaseContainerFrame* rbc = FindRubyBaseContainerAncestor(this)) {
175     rbc->UpdateDescendantLeadings(mLeadings);
176   }
177 
178   ReflowAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, aStatus);
179 }
180 
ReflowSegment(nsPresContext * aPresContext,const ReflowInput & aReflowInput,nscoord aBlockStartAscent,nscoord aBlockSize,nsRubyBaseContainerFrame * aBaseContainer,nsReflowStatus & aStatus)181 void nsRubyFrame::ReflowSegment(nsPresContext* aPresContext,
182                                 const ReflowInput& aReflowInput,
183                                 nscoord aBlockStartAscent, nscoord aBlockSize,
184                                 nsRubyBaseContainerFrame* aBaseContainer,
185                                 nsReflowStatus& aStatus) {
186   WritingMode lineWM = aReflowInput.mLineLayout->GetWritingMode();
187   LogicalSize availSize(lineWM, aReflowInput.AvailableISize(),
188                         aReflowInput.AvailableBSize());
189   NS_ASSERTION(!GetWritingMode().IsOrthogonalTo(lineWM),
190                "Ruby frame writing-mode shouldn't be orthogonal to its line");
191 
192   AutoRubyTextContainerArray textContainers(aBaseContainer);
193   const uint32_t rtcCount = textContainers.Length();
194 
195   ReflowOutput baseMetrics(aReflowInput);
196   bool pushedFrame;
197   aReflowInput.mLineLayout->ReflowFrame(aBaseContainer, aStatus, &baseMetrics,
198                                         pushedFrame);
199 
200   if (aStatus.IsInlineBreakBefore()) {
201     if (aBaseContainer != mFrames.FirstChild()) {
202       // Some segments may have been reflowed before, hence it is not
203       // a break-before for the ruby container.
204       aStatus.Reset();
205       aStatus.SetInlineLineBreakAfter();
206       aStatus.SetIncomplete();
207       PushChildrenToOverflow(aBaseContainer, aBaseContainer->GetPrevSibling());
208       aReflowInput.mLineLayout->SetDirtyNextLine();
209     }
210     // This base container is not placed at all, we can skip all
211     // text containers paired with it.
212     return;
213   }
214   if (aStatus.IsIncomplete()) {
215     // It always promise that if the status is incomplete, there is a
216     // break occurs. Break before has been processed above. However,
217     // it is possible that break after happens with the frame reflow
218     // completed. It happens if there is a force break at the end.
219     MOZ_ASSERT(aStatus.IsInlineBreakAfter());
220     // Find the previous sibling which we will
221     // insert new continuations after.
222     nsIFrame* lastChild;
223     if (rtcCount > 0) {
224       lastChild = textContainers.LastElement();
225     } else {
226       lastChild = aBaseContainer;
227     }
228 
229     // Create continuations for the base container
230     nsIFrame* newBaseContainer = CreateNextInFlow(aBaseContainer);
231     // newBaseContainer is null if there are existing next-in-flows.
232     // We only need to move and push if there were not.
233     if (newBaseContainer) {
234       // Move the new frame after all the text containers
235       mFrames.RemoveFrame(newBaseContainer);
236       mFrames.InsertFrame(nullptr, lastChild, newBaseContainer);
237 
238       // Create continuations for text containers
239       nsIFrame* newLastChild = newBaseContainer;
240       for (uint32_t i = 0; i < rtcCount; i++) {
241         nsIFrame* newTextContainer = CreateNextInFlow(textContainers[i]);
242         MOZ_ASSERT(newTextContainer,
243                    "Next-in-flow of rtc should not exist "
244                    "if the corresponding rbc does not");
245         mFrames.RemoveFrame(newTextContainer);
246         mFrames.InsertFrame(nullptr, newLastChild, newTextContainer);
247         newLastChild = newTextContainer;
248       }
249     }
250     if (lastChild != mFrames.LastChild()) {
251       // Always push the next frame after the last child in this segment.
252       // It is possible that we pulled it back before our next-in-flow
253       // drain our overflow.
254       PushChildrenToOverflow(lastChild->GetNextSibling(), lastChild);
255       aReflowInput.mLineLayout->SetDirtyNextLine();
256     }
257   } else {
258     // If the ruby base container is reflowed completely, the line
259     // layout will remove the next-in-flows of that frame. But the
260     // line layout is not aware of the ruby text containers, hence
261     // it is necessary to remove them here.
262     for (uint32_t i = 0; i < rtcCount; i++) {
263       nsIFrame* nextRTC = textContainers[i]->GetNextInFlow();
264       if (nextRTC) {
265         nextRTC->GetParent()->DeleteNextInFlowChild(nextRTC, true);
266       }
267     }
268   }
269 
270   nscoord segmentISize = baseMetrics.ISize(lineWM);
271   const nsSize dummyContainerSize;
272   LogicalRect baseRect =
273       aBaseContainer->GetLogicalRect(lineWM, dummyContainerSize);
274   // We need to position our rtc frames on one side or the other of the
275   // base container's rect, using a coordinate space that's relative to
276   // the ruby frame. Right now, the base container's rect's block-axis
277   // position is relative to the block container frame containing the
278   // lines, so here we reset it to the different between the ascents of
279   // the ruby container and the ruby base container, assuming they are
280   // aligned with the baseline.
281   // XXX We may need to add border/padding here. See bug 1055667.
282   baseRect.BStart(lineWM) = aBlockStartAscent - baseMetrics.BlockStartAscent();
283   // The rect for offsets of text containers.
284   LogicalRect offsetRect = baseRect;
285   RubyBlockLeadings descLeadings = aBaseContainer->GetDescendantLeadings();
286   offsetRect.BStart(lineWM) -= descLeadings.mStart;
287   offsetRect.BSize(lineWM) += descLeadings.mStart + descLeadings.mEnd;
288   Maybe<LineRelativeDir> lastLineSide;
289   for (uint32_t i = 0; i < rtcCount; i++) {
290     nsRubyTextContainerFrame* textContainer = textContainers[i];
291     WritingMode rtcWM = textContainer->GetWritingMode();
292     nsReflowStatus textReflowStatus;
293     ReflowOutput textMetrics(aReflowInput);
294     ReflowInput textReflowInput(aPresContext, aReflowInput, textContainer,
295                                 availSize.ConvertTo(rtcWM, lineWM));
296     textContainer->Reflow(aPresContext, textMetrics, textReflowInput,
297                           textReflowStatus);
298     // Ruby text containers always return complete reflow status even when
299     // they have continuations, because the breaking has already been
300     // handled when reflowing the base containers.
301     NS_ASSERTION(textReflowStatus.IsEmpty(),
302                  "Ruby text container must not break itself inside");
303     const LogicalSize size = textMetrics.Size(lineWM);
304     textContainer->SetSize(lineWM, size);
305 
306     nscoord reservedISize = RubyUtils::GetReservedISize(textContainer);
307     segmentISize = std::max(segmentISize, size.ISize(lineWM) + reservedISize);
308 
309     Maybe<LineRelativeDir> lineSide;
310     switch (textContainer->StyleText()->mRubyPosition) {
311       case StyleRubyPosition::Over:
312         lineSide.emplace(eLineRelativeDirOver);
313         break;
314       case StyleRubyPosition::Under:
315         lineSide.emplace(eLineRelativeDirUnder);
316         break;
317       case StyleRubyPosition::AlternateOver:
318         if (lastLineSide.isSome() &&
319             lastLineSide.value() == eLineRelativeDirOver) {
320           lineSide.emplace(eLineRelativeDirUnder);
321         } else {
322           lineSide.emplace(eLineRelativeDirOver);
323         }
324         break;
325       case StyleRubyPosition::AlternateUnder:
326         if (lastLineSide.isSome() &&
327             lastLineSide.value() == eLineRelativeDirUnder) {
328           lineSide.emplace(eLineRelativeDirOver);
329         } else {
330           lineSide.emplace(eLineRelativeDirUnder);
331         }
332         break;
333       default:
334         // XXX inter-character support in bug 1055672
335         MOZ_ASSERT_UNREACHABLE("Unsupported ruby-position");
336     }
337     lastLineSide = lineSide;
338 
339     LogicalPoint position(lineWM);
340     if (lineSide.isSome()) {
341       LogicalSide logicalSide =
342           lineWM.LogicalSideForLineRelativeDir(lineSide.value());
343       if (StaticPrefs::layout_css_ruby_intercharacter_enabled() &&
344           rtcWM.IsVerticalRL() &&
345           lineWM.GetInlineDir() == WritingMode::eInlineLTR) {
346         // Inter-character ruby annotations are only supported for vertical-rl
347         // in ltr horizontal writing. Fall back to non-inter-character behavior
348         // otherwise.
349         LogicalPoint offset(
350             lineWM, offsetRect.ISize(lineWM),
351             offsetRect.BSize(lineWM) > size.BSize(lineWM)
352                 ? (offsetRect.BSize(lineWM) - size.BSize(lineWM)) / 2
353                 : 0);
354         position = offsetRect.Origin(lineWM) + offset;
355         aReflowInput.mLineLayout->AdvanceICoord(size.ISize(lineWM));
356       } else if (logicalSide == eLogicalSideBStart) {
357         offsetRect.BStart(lineWM) -= size.BSize(lineWM);
358         offsetRect.BSize(lineWM) += size.BSize(lineWM);
359         position = offsetRect.Origin(lineWM);
360       } else if (logicalSide == eLogicalSideBEnd) {
361         position = offsetRect.Origin(lineWM) +
362                    LogicalPoint(lineWM, 0, offsetRect.BSize(lineWM));
363         offsetRect.BSize(lineWM) += size.BSize(lineWM);
364       } else {
365         MOZ_ASSERT_UNREACHABLE("???");
366       }
367     }
368     // Using a dummy container-size here, so child positioning may not be
369     // correct. We will fix it in nsLineLayout after the whole line is
370     // reflowed.
371     FinishReflowChild(textContainer, aPresContext, textMetrics,
372                       &textReflowInput, lineWM, position, dummyContainerSize,
373                       ReflowChildFlags::Default);
374   }
375   MOZ_ASSERT(baseRect.ISize(lineWM) == offsetRect.ISize(lineWM),
376              "Annotations should only be placed on the block directions");
377 
378   nscoord deltaISize = segmentISize - baseMetrics.ISize(lineWM);
379   if (deltaISize <= 0) {
380     RubyUtils::ClearReservedISize(aBaseContainer);
381   } else {
382     RubyUtils::SetReservedISize(aBaseContainer, deltaISize);
383     aReflowInput.mLineLayout->AdvanceICoord(deltaISize);
384   }
385 
386   // Set block leadings of the base container.
387   // The leadings are the difference between the offsetRect and the rect
388   // of this ruby container, which has block start zero and block size
389   // aBlockSize.
390   nscoord startLeading = -offsetRect.BStart(lineWM);
391   nscoord endLeading = offsetRect.BEnd(lineWM) - aBlockSize;
392   // XXX When bug 765861 gets fixed, this warning should be upgraded.
393   NS_WARNING_ASSERTION(startLeading >= 0 && endLeading >= 0,
394                        "Leadings should be non-negative (because adding "
395                        "ruby annotation can only increase the size)");
396   mLeadings.Update(startLeading, endLeading);
397 }
398 
PullOneSegment(const nsLineLayout * aLineLayout,ContinuationTraversingState & aState)399 nsRubyBaseContainerFrame* nsRubyFrame::PullOneSegment(
400     const nsLineLayout* aLineLayout, ContinuationTraversingState& aState) {
401   // Pull a ruby base container
402   nsIFrame* baseFrame = GetNextInFlowChild(aState);
403   if (!baseFrame) {
404     return nullptr;
405   }
406   MOZ_ASSERT(baseFrame->IsRubyBaseContainerFrame());
407 
408   // Get the float containing block of the base frame before we pull it.
409   nsBlockFrame* oldFloatCB = nsLayoutUtils::GetFloatContainingBlock(baseFrame);
410   PullNextInFlowChild(aState);
411 
412   // Pull all ruby text containers following the base container
413   nsIFrame* nextFrame;
414   while ((nextFrame = GetNextInFlowChild(aState)) != nullptr &&
415          nextFrame->IsRubyTextContainerFrame()) {
416     PullNextInFlowChild(aState);
417   }
418 
419   if (nsBlockFrame* newFloatCB =
420           do_QueryFrame(aLineLayout->LineContainerFrame())) {
421     if (oldFloatCB && oldFloatCB != newFloatCB) {
422       newFloatCB->ReparentFloats(baseFrame, oldFloatCB, true);
423     }
424   }
425 
426   return static_cast<nsRubyBaseContainerFrame*>(baseFrame);
427 }
428