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/. */
7 /* rendering object for CSS "display: ruby" */
9 #include "nsRubyFrame.h"
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"
24 using namespace mozilla
;
26 //----------------------------------------------------------------------
28 // Frame class boilerplate
29 // =======================
31 NS_QUERYFRAME_HEAD(nsRubyFrame
)
32 NS_QUERYFRAME_ENTRY(nsRubyFrame
)
33 NS_QUERYFRAME_TAIL_INHERITING(nsInlineFrame
)
35 NS_IMPL_FRAMEARENA_HELPERS(nsRubyFrame
)
37 nsContainerFrame
* NS_NewRubyFrame(PresShell
* aPresShell
,
38 ComputedStyle
* aStyle
) {
39 return new (aPresShell
) nsRubyFrame(aStyle
, aPresShell
->GetPresContext());
42 //----------------------------------------------------------------------
44 // nsRubyFrame Method Implementations
45 // ==================================
47 #ifdef DEBUG_FRAME_DUMP
48 nsresult
nsRubyFrame::GetFrameName(nsAString
& aResult
) const {
49 return MakeFrameName(u
"Ruby"_ns
, aResult
);
54 void nsRubyFrame::AddInlineMinISize(const IntrinsicSizeInput
& aInput
,
55 InlineMinISizeData
* aData
) {
56 auto handleChildren
= [&](auto frame
, auto data
) {
57 // Ruby frames shouldn't have percentage block sizes that require a
58 // percentage basis for resolution.
59 const IntrinsicSizeInput
input(aInput
.mContext
, Nothing(), Nothing());
60 for (RubySegmentEnumerator
e(static_cast<nsRubyFrame
*>(frame
)); !e
.AtEnd();
62 e
.GetBaseContainer()->AddInlineMinISize(input
, data
);
65 DoInlineIntrinsicISize(aData
, handleChildren
);
69 void nsRubyFrame::AddInlinePrefISize(const IntrinsicSizeInput
& aInput
,
70 InlinePrefISizeData
* aData
) {
71 auto handleChildren
= [&](auto frame
, auto data
) {
72 // Ruby frames shouldn't have percentage block sizes that require a
73 // percentage basis for resolution.
74 const IntrinsicSizeInput
input(aInput
.mContext
, Nothing(), Nothing());
75 for (RubySegmentEnumerator
e(static_cast<nsRubyFrame
*>(frame
)); !e
.AtEnd();
77 e
.GetBaseContainer()->AddInlinePrefISize(input
, data
);
80 DoInlineIntrinsicISize(aData
, handleChildren
);
81 aData
->mLineIsEmpty
= false;
84 static nsRubyBaseContainerFrame
* FindRubyBaseContainerAncestor(
86 for (nsIFrame
* ancestor
= aFrame
->GetParent();
87 ancestor
&& ancestor
->IsLineParticipant();
88 ancestor
= ancestor
->GetParent()) {
89 if (ancestor
->IsRubyBaseContainerFrame()) {
90 return static_cast<nsRubyBaseContainerFrame
*>(ancestor
);
97 void nsRubyFrame::Reflow(nsPresContext
* aPresContext
,
98 ReflowOutput
& aDesiredSize
,
99 const ReflowInput
& aReflowInput
,
100 nsReflowStatus
& aStatus
) {
102 DO_GLOBAL_REFLOW_COUNT("nsRubyFrame");
103 MOZ_ASSERT(aStatus
.IsEmpty(), "Caller should pass a fresh reflow status!");
105 if (!aReflowInput
.mLineLayout
) {
106 NS_ASSERTION(aReflowInput
.mLineLayout
,
107 "No line layout provided to RubyFrame reflow method.");
111 // Grab overflow frames from prev-in-flow and its own.
112 MoveInlineOverflowToChildList(aReflowInput
.mLineLayout
->LineContainerFrame());
117 // Begin the span for the ruby frame
118 WritingMode frameWM
= aReflowInput
.GetWritingMode();
119 WritingMode lineWM
= aReflowInput
.mLineLayout
->GetWritingMode();
120 LogicalMargin borderPadding
=
121 aReflowInput
.ComputedLogicalBorderPadding(frameWM
);
122 nsLayoutUtils::SetBSizeFromFontMetrics(this, aDesiredSize
, borderPadding
,
125 nscoord startEdge
= 0;
126 const bool boxDecorationBreakClone
=
127 StyleBorder()->mBoxDecorationBreak
== StyleBoxDecorationBreak::Clone
;
128 if (boxDecorationBreakClone
|| !GetPrevContinuation()) {
129 startEdge
= borderPadding
.IStart(frameWM
);
131 NS_ASSERTION(aReflowInput
.AvailableISize() != NS_UNCONSTRAINEDSIZE
,
132 "should no longer use available widths");
133 nscoord endEdge
= aReflowInput
.AvailableISize() - borderPadding
.IEnd(frameWM
);
134 aReflowInput
.mLineLayout
->BeginSpan(this, &aReflowInput
, startEdge
, endEdge
,
137 for (RubySegmentEnumerator
e(this); !e
.AtEnd(); e
.Next()) {
138 ReflowSegment(aPresContext
, aReflowInput
, aDesiredSize
.BlockStartAscent(),
139 aDesiredSize
.BSize(lineWM
), e
.GetBaseContainer(), aStatus
);
141 if (aStatus
.IsInlineBreak()) {
142 // A break occurs when reflowing the segment.
143 // Don't continue reflowing more segments.
148 ContinuationTraversingState
pullState(this);
149 while (aStatus
.IsEmpty()) {
150 nsRubyBaseContainerFrame
* baseContainer
=
151 PullOneSegment(aReflowInput
.mLineLayout
, pullState
);
152 if (!baseContainer
) {
153 // No more continuations after, finish now.
156 ReflowSegment(aPresContext
, aReflowInput
, aDesiredSize
.BlockStartAscent(),
157 aDesiredSize
.BSize(lineWM
), baseContainer
, aStatus
);
159 // We never handle overflow in ruby.
160 MOZ_ASSERT(!aStatus
.IsOverflowIncomplete());
162 aDesiredSize
.ISize(lineWM
) = aReflowInput
.mLineLayout
->EndSpan(this);
163 if (boxDecorationBreakClone
|| !GetPrevContinuation()) {
164 aDesiredSize
.ISize(lineWM
) += borderPadding
.IStart(frameWM
);
166 if (boxDecorationBreakClone
|| aStatus
.IsComplete()) {
167 aDesiredSize
.ISize(lineWM
) += borderPadding
.IEnd(frameWM
);
170 // Update descendant leadings of ancestor ruby base container.
171 if (nsRubyBaseContainerFrame
* rbc
= FindRubyBaseContainerAncestor(this)) {
172 rbc
->UpdateDescendantLeadings(mLeadings
);
175 ReflowAbsoluteFrames(aPresContext
, aDesiredSize
, aReflowInput
, aStatus
);
178 void nsRubyFrame::ReflowSegment(nsPresContext
* aPresContext
,
179 const ReflowInput
& aReflowInput
,
180 nscoord aBlockStartAscent
, nscoord aBlockSize
,
181 nsRubyBaseContainerFrame
* aBaseContainer
,
182 nsReflowStatus
& aStatus
) {
183 WritingMode lineWM
= aReflowInput
.mLineLayout
->GetWritingMode();
184 LogicalSize
availSize(lineWM
, aReflowInput
.AvailableISize(),
185 aReflowInput
.AvailableBSize());
186 NS_ASSERTION(!GetWritingMode().IsOrthogonalTo(lineWM
),
187 "Ruby frame writing-mode shouldn't be orthogonal to its line");
189 AutoRubyTextContainerArray
textContainers(aBaseContainer
);
190 const uint32_t rtcCount
= textContainers
.Length();
192 ReflowOutput
baseMetrics(aReflowInput
);
194 aReflowInput
.mLineLayout
->ReflowFrame(aBaseContainer
, aStatus
, &baseMetrics
,
197 if (aStatus
.IsInlineBreakBefore()) {
198 if (aBaseContainer
!= mFrames
.FirstChild()) {
199 // Some segments may have been reflowed before, hence it is not
200 // a break-before for the ruby container.
202 aStatus
.SetInlineLineBreakAfter();
203 aStatus
.SetIncomplete();
204 PushChildrenToOverflow(aBaseContainer
, aBaseContainer
->GetPrevSibling());
205 aReflowInput
.mLineLayout
->SetDirtyNextLine();
207 // This base container is not placed at all, we can skip all
208 // text containers paired with it.
211 if (aStatus
.IsIncomplete()) {
212 // It always promise that if the status is incomplete, there is a
213 // break occurs. Break before has been processed above. However,
214 // it is possible that break after happens with the frame reflow
215 // completed. It happens if there is a force break at the end.
216 MOZ_ASSERT(aStatus
.IsInlineBreakAfter());
217 // Find the previous sibling which we will
218 // insert new continuations after.
221 lastChild
= textContainers
.LastElement();
223 lastChild
= aBaseContainer
;
226 // Create continuations for the base container
227 nsIFrame
* newBaseContainer
= CreateNextInFlow(aBaseContainer
);
228 // newBaseContainer is null if there are existing next-in-flows.
229 // We only need to move and push if there were not.
230 if (newBaseContainer
) {
231 // Move the new frame after all the text containers
232 mFrames
.RemoveFrame(newBaseContainer
);
233 mFrames
.InsertFrame(nullptr, lastChild
, newBaseContainer
);
235 // Create continuations for text containers
236 nsIFrame
* newLastChild
= newBaseContainer
;
237 for (uint32_t i
= 0; i
< rtcCount
; i
++) {
238 nsIFrame
* newTextContainer
= CreateNextInFlow(textContainers
[i
]);
239 MOZ_ASSERT(newTextContainer
,
240 "Next-in-flow of rtc should not exist "
241 "if the corresponding rbc does not");
242 mFrames
.RemoveFrame(newTextContainer
);
243 mFrames
.InsertFrame(nullptr, newLastChild
, newTextContainer
);
244 newLastChild
= newTextContainer
;
247 if (lastChild
!= mFrames
.LastChild()) {
248 // Always push the next frame after the last child in this segment.
249 // It is possible that we pulled it back before our next-in-flow
250 // drain our overflow.
251 PushChildrenToOverflow(lastChild
->GetNextSibling(), lastChild
);
252 aReflowInput
.mLineLayout
->SetDirtyNextLine();
254 } else if (rtcCount
) {
255 DestroyContext
context(PresShell());
256 // If the ruby base container is reflowed completely, the line
257 // layout will remove the next-in-flows of that frame. But the
258 // line layout is not aware of the ruby text containers, hence
259 // it is necessary to remove them here.
260 for (uint32_t i
= 0; i
< rtcCount
; i
++) {
261 if (nsIFrame
* nextRTC
= textContainers
[i
]->GetNextInFlow()) {
262 nextRTC
->GetParent()->DeleteNextInFlowChild(context
, nextRTC
, true);
267 nscoord segmentISize
= baseMetrics
.ISize(lineWM
);
268 const nsSize dummyContainerSize
;
269 LogicalRect baseRect
=
270 aBaseContainer
->GetLogicalRect(lineWM
, dummyContainerSize
);
271 // We need to position our rtc frames on one side or the other of the
272 // base container's rect, using a coordinate space that's relative to
273 // the ruby frame. Right now, the base container's rect's block-axis
274 // position is relative to the block container frame containing the
275 // lines, so here we reset it to the different between the ascents of
276 // the ruby container and the ruby base container, assuming they are
277 // aligned with the baseline.
278 // XXX We may need to add border/padding here. See bug 1055667.
279 baseRect
.BStart(lineWM
) = aBlockStartAscent
- baseMetrics
.BlockStartAscent();
280 // The rect for offsets of text containers.
281 LogicalRect offsetRect
= baseRect
;
282 RubyBlockLeadings descLeadings
= aBaseContainer
->GetDescendantLeadings();
283 offsetRect
.BStart(lineWM
) -= descLeadings
.mStart
;
284 offsetRect
.BSize(lineWM
) += descLeadings
.mStart
+ descLeadings
.mEnd
;
285 Maybe
<LineRelativeDir
> lastLineSide
;
286 for (uint32_t i
= 0; i
< rtcCount
; i
++) {
287 nsRubyTextContainerFrame
* textContainer
= textContainers
[i
];
288 WritingMode rtcWM
= textContainer
->GetWritingMode();
289 nsReflowStatus textReflowStatus
;
290 ReflowOutput
textMetrics(aReflowInput
);
291 ReflowInput
textReflowInput(aPresContext
, aReflowInput
, textContainer
,
292 availSize
.ConvertTo(rtcWM
, lineWM
));
293 textContainer
->Reflow(aPresContext
, textMetrics
, textReflowInput
,
295 // Ruby text containers always return complete reflow status even when
296 // they have continuations, because the breaking has already been
297 // handled when reflowing the base containers.
298 NS_ASSERTION(textReflowStatus
.IsEmpty(),
299 "Ruby text container must not break itself inside");
300 const LogicalSize size
= textMetrics
.Size(lineWM
);
301 textContainer
->SetSize(lineWM
, size
);
303 nscoord reservedISize
= RubyUtils::GetReservedISize(textContainer
);
304 segmentISize
= std::max(segmentISize
, size
.ISize(lineWM
) + reservedISize
);
306 Maybe
<LineRelativeDir
> lineSide
;
307 switch (textContainer
->StyleText()->mRubyPosition
) {
308 case StyleRubyPosition::Over
:
309 lineSide
.emplace(LineRelativeDir::Over
);
311 case StyleRubyPosition::Under
:
312 lineSide
.emplace(LineRelativeDir::Under
);
314 case StyleRubyPosition::AlternateOver
:
315 if (lastLineSide
.isSome() &&
316 lastLineSide
.value() == LineRelativeDir::Over
) {
317 lineSide
.emplace(LineRelativeDir::Under
);
319 lineSide
.emplace(LineRelativeDir::Over
);
322 case StyleRubyPosition::AlternateUnder
:
323 if (lastLineSide
.isSome() &&
324 lastLineSide
.value() == LineRelativeDir::Under
) {
325 lineSide
.emplace(LineRelativeDir::Over
);
327 lineSide
.emplace(LineRelativeDir::Under
);
331 // XXX inter-character support in bug 1055672
332 MOZ_ASSERT_UNREACHABLE("Unsupported ruby-position");
334 lastLineSide
= lineSide
;
336 LogicalPoint
position(lineWM
);
337 if (lineSide
.isSome()) {
338 LogicalSide logicalSide
=
339 lineWM
.LogicalSideForLineRelativeDir(lineSide
.value());
340 if (StaticPrefs::layout_css_ruby_intercharacter_enabled() &&
341 rtcWM
.IsVerticalRL() &&
342 lineWM
.GetInlineDir() == WritingMode::InlineDir::LTR
) {
343 // Inter-character ruby annotations are only supported for vertical-rl
344 // in ltr horizontal writing. Fall back to non-inter-character behavior
347 lineWM
, offsetRect
.ISize(lineWM
),
348 offsetRect
.BSize(lineWM
) > size
.BSize(lineWM
)
349 ? (offsetRect
.BSize(lineWM
) - size
.BSize(lineWM
)) / 2
351 position
= offsetRect
.Origin(lineWM
) + offset
;
352 aReflowInput
.mLineLayout
->AdvanceICoord(size
.ISize(lineWM
));
353 } else if (logicalSide
== LogicalSide::BStart
) {
354 offsetRect
.BStart(lineWM
) -= size
.BSize(lineWM
);
355 offsetRect
.BSize(lineWM
) += size
.BSize(lineWM
);
356 position
= offsetRect
.Origin(lineWM
);
357 } else if (logicalSide
== LogicalSide::BEnd
) {
358 position
= offsetRect
.Origin(lineWM
) +
359 LogicalPoint(lineWM
, 0, offsetRect
.BSize(lineWM
));
360 offsetRect
.BSize(lineWM
) += size
.BSize(lineWM
);
362 MOZ_ASSERT_UNREACHABLE("???");
365 // Using a dummy container-size here, so child positioning may not be
366 // correct. We will fix it in nsLineLayout after the whole line is
368 FinishReflowChild(textContainer
, aPresContext
, textMetrics
,
369 &textReflowInput
, lineWM
, position
, dummyContainerSize
,
370 ReflowChildFlags::Default
);
372 MOZ_ASSERT(baseRect
.ISize(lineWM
) == offsetRect
.ISize(lineWM
),
373 "Annotations should only be placed on the block directions");
375 nscoord deltaISize
= segmentISize
- baseMetrics
.ISize(lineWM
);
376 if (deltaISize
<= 0) {
377 RubyUtils::ClearReservedISize(aBaseContainer
);
379 RubyUtils::SetReservedISize(aBaseContainer
, deltaISize
);
380 aReflowInput
.mLineLayout
->AdvanceICoord(deltaISize
);
383 // Set block leadings of the base container.
384 // The leadings are the difference between the offsetRect and the rect
385 // of this ruby container, which has block start zero and block size
387 nscoord startLeading
= -offsetRect
.BStart(lineWM
);
388 nscoord endLeading
= offsetRect
.BEnd(lineWM
) - aBlockSize
;
389 // XXX When bug 765861 gets fixed, this warning should be upgraded.
390 NS_WARNING_ASSERTION(startLeading
>= 0 && endLeading
>= 0,
391 "Leadings should be non-negative (because adding "
392 "ruby annotation can only increase the size)");
393 mLeadings
.Update(startLeading
, endLeading
);
396 nsRubyBaseContainerFrame
* nsRubyFrame::PullOneSegment(
397 const nsLineLayout
* aLineLayout
, ContinuationTraversingState
& aState
) {
398 // Pull a ruby base container
399 nsIFrame
* baseFrame
= GetNextInFlowChild(aState
);
403 MOZ_ASSERT(baseFrame
->IsRubyBaseContainerFrame());
405 // Get the float containing block of the base frame before we pull it.
406 nsBlockFrame
* oldFloatCB
= nsLayoutUtils::GetFloatContainingBlock(baseFrame
);
407 PullNextInFlowChild(aState
);
409 // Pull all ruby text containers following the base container
411 while ((nextFrame
= GetNextInFlowChild(aState
)) != nullptr &&
412 nextFrame
->IsRubyTextContainerFrame()) {
413 PullNextInFlowChild(aState
);
416 if (nsBlockFrame
* newFloatCB
=
417 do_QueryFrame(aLineLayout
->LineContainerFrame())) {
418 if (oldFloatCB
&& oldFloatCB
!= newFloatCB
) {
419 newFloatCB
->ReparentFloats(baseFrame
, oldFloatCB
, true);
423 return static_cast<nsRubyBaseContainerFrame
*>(baseFrame
);