nsRubyFrame.cpp (20426B)
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 "nsContainerFrameInlines.h" 18 #include "nsLayoutUtils.h" 19 #include "nsLineLayout.h" 20 #include "nsPresContext.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) 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 #ifdef DEBUG_FRAME_DUMP 48 nsresult nsRubyFrame::GetFrameName(nsAString& aResult) const { 49 return MakeFrameName(u"Ruby"_ns, aResult); 50 } 51 #endif 52 53 /* virtual */ 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(); 61 e.Next()) { 62 e.GetBaseContainer()->AddInlineMinISize(input, data); 63 } 64 }; 65 DoInlineIntrinsicISize(aData, handleChildren); 66 } 67 68 /* virtual */ 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(); 76 e.Next()) { 77 e.GetBaseContainer()->AddInlinePrefISize(input, data); 78 } 79 }; 80 DoInlineIntrinsicISize(aData, handleChildren); 81 aData->mLineIsEmpty = false; 82 } 83 84 static nsRubyBaseContainerFrame* FindRubyBaseContainerAncestor( 85 nsIFrame* aFrame) { 86 for (nsIFrame* ancestor = aFrame->GetParent(); 87 ancestor && ancestor->IsLineParticipant(); 88 ancestor = ancestor->GetParent()) { 89 if (ancestor->IsRubyBaseContainerFrame()) { 90 return static_cast<nsRubyBaseContainerFrame*>(ancestor); 91 } 92 } 93 return nullptr; 94 } 95 96 /* virtual */ 97 void nsRubyFrame::Reflow(nsPresContext* aPresContext, 98 ReflowOutput& aDesiredSize, 99 const ReflowInput& aReflowInput, 100 nsReflowStatus& aStatus) { 101 MarkInReflow(); 102 DO_GLOBAL_REFLOW_COUNT("nsRubyFrame"); 103 MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); 104 105 if (!aReflowInput.mLineLayout) { 106 NS_ASSERTION(aReflowInput.mLineLayout, 107 "No line layout provided to RubyFrame reflow method."); 108 return; 109 } 110 111 // Grab overflow frames from prev-in-flow and its own. 112 MoveInlineOverflowToChildList(aReflowInput.mLineLayout->LineContainerFrame()); 113 114 // Clear leadings and ruby-positioning metrics. 115 mLeadings.Reset(); 116 mRubyMetrics = mozilla::RubyMetrics(); 117 118 // Begin the span for the ruby frame 119 WritingMode frameWM = aReflowInput.GetWritingMode(); 120 WritingMode lineWM = aReflowInput.mLineLayout->GetWritingMode(); 121 LogicalMargin borderPadding = 122 aReflowInput.ComputedLogicalBorderPadding(frameWM); 123 nsLayoutUtils::SetBSizeFromFontMetrics(this, aDesiredSize, borderPadding, 124 lineWM, frameWM); 125 126 nscoord startEdge = 0; 127 const bool boxDecorationBreakClone = 128 StyleBorder()->mBoxDecorationBreak == StyleBoxDecorationBreak::Clone; 129 if (boxDecorationBreakClone || !GetPrevContinuation()) { 130 startEdge = borderPadding.IStart(frameWM); 131 } 132 NS_ASSERTION(aReflowInput.AvailableISize() != NS_UNCONSTRAINEDSIZE, 133 "should no longer use available widths"); 134 nscoord endEdge = aReflowInput.AvailableISize() - borderPadding.IEnd(frameWM); 135 aReflowInput.mLineLayout->BeginSpan(this, &aReflowInput, startEdge, endEdge, 136 &mBaseline); 137 138 for (RubySegmentEnumerator e(this); !e.AtEnd(); e.Next()) { 139 ReflowSegment(aPresContext, aReflowInput, aDesiredSize.BlockStartAscent(), 140 aDesiredSize.BSize(lineWM), e.GetBaseContainer(), aStatus); 141 142 if (aStatus.IsInlineBreak()) { 143 // A break occurs when reflowing the segment. 144 // Don't continue reflowing more segments. 145 break; 146 } 147 } 148 149 ContinuationTraversingState pullState(this); 150 while (aStatus.IsEmpty()) { 151 nsRubyBaseContainerFrame* baseContainer = 152 PullOneSegment(aReflowInput.mLineLayout, pullState); 153 if (!baseContainer) { 154 // No more continuations after, finish now. 155 break; 156 } 157 ReflowSegment(aPresContext, aReflowInput, aDesiredSize.BlockStartAscent(), 158 aDesiredSize.BSize(lineWM), baseContainer, aStatus); 159 } 160 // We never handle overflow in ruby. 161 MOZ_ASSERT(!aStatus.IsOverflowIncomplete()); 162 163 aDesiredSize.ISize(lineWM) = aReflowInput.mLineLayout->EndSpan(this); 164 if (boxDecorationBreakClone || !GetPrevContinuation()) { 165 aDesiredSize.ISize(lineWM) += borderPadding.IStart(frameWM); 166 } 167 if (boxDecorationBreakClone || aStatus.IsComplete()) { 168 aDesiredSize.ISize(lineWM) += borderPadding.IEnd(frameWM); 169 } 170 171 // Update descendant leadings of ancestor ruby base container. 172 if (nsRubyBaseContainerFrame* rbc = FindRubyBaseContainerAncestor(this)) { 173 rbc->UpdateDescendantLeadings(mLeadings); 174 } 175 176 ReflowAbsoluteFrames(aPresContext, aDesiredSize, aReflowInput, aStatus); 177 } 178 179 void nsRubyFrame::ReflowSegment(nsPresContext* aPresContext, 180 const ReflowInput& aReflowInput, 181 nscoord aBlockStartAscent, nscoord aBlockSize, 182 nsRubyBaseContainerFrame* aBaseContainer, 183 nsReflowStatus& aStatus) { 184 WritingMode lineWM = aReflowInput.mLineLayout->GetWritingMode(); 185 LogicalSize availSize(lineWM, aReflowInput.AvailableISize(), 186 aReflowInput.AvailableBSize()); 187 NS_ASSERTION(!GetWritingMode().IsOrthogonalTo(lineWM), 188 "Ruby frame writing-mode shouldn't be orthogonal to its line"); 189 190 AutoRubyTextContainerArray textContainers(aBaseContainer); 191 const uint32_t rtcCount = textContainers.Length(); 192 193 ReflowOutput baseMetrics(aReflowInput); 194 bool pushedFrame; 195 aReflowInput.mLineLayout->ReflowFrame(aBaseContainer, aStatus, &baseMetrics, 196 pushedFrame); 197 198 if (aStatus.IsInlineBreakBefore()) { 199 if (aBaseContainer != mFrames.FirstChild()) { 200 // Some segments may have been reflowed before, hence it is not 201 // a break-before for the ruby container. 202 aStatus.Reset(); 203 aStatus.SetInlineLineBreakAfter(); 204 aStatus.SetIncomplete(); 205 PushChildrenToOverflow(aBaseContainer, aBaseContainer->GetPrevSibling()); 206 aReflowInput.mLineLayout->SetDirtyNextLine(); 207 } 208 // This base container is not placed at all, we can skip all 209 // text containers paired with it. 210 return; 211 } 212 if (aStatus.IsIncomplete()) { 213 // It always promise that if the status is incomplete, there is a 214 // break occurs. Break before has been processed above. However, 215 // it is possible that break after happens with the frame reflow 216 // completed. It happens if there is a force break at the end. 217 MOZ_ASSERT(aStatus.IsInlineBreakAfter()); 218 // Find the previous sibling which we will 219 // insert new continuations after. 220 nsIFrame* lastChild; 221 if (rtcCount > 0) { 222 lastChild = textContainers.LastElement(); 223 } else { 224 lastChild = aBaseContainer; 225 } 226 227 // Create continuations for the base container 228 nsIFrame* newBaseContainer = CreateNextInFlow(aBaseContainer); 229 // newBaseContainer is null if there are existing next-in-flows. 230 // We only need to move and push if there were not. 231 if (newBaseContainer) { 232 // Move the new frame after all the text containers 233 mFrames.RemoveFrame(newBaseContainer); 234 mFrames.InsertFrame(nullptr, lastChild, newBaseContainer); 235 236 // Create continuations for text containers 237 nsIFrame* newLastChild = newBaseContainer; 238 for (uint32_t i = 0; i < rtcCount; i++) { 239 nsIFrame* newTextContainer = CreateNextInFlow(textContainers[i]); 240 MOZ_ASSERT(newTextContainer, 241 "Next-in-flow of rtc should not exist " 242 "if the corresponding rbc does not"); 243 mFrames.RemoveFrame(newTextContainer); 244 mFrames.InsertFrame(nullptr, newLastChild, newTextContainer); 245 newLastChild = newTextContainer; 246 } 247 } 248 if (lastChild != mFrames.LastChild()) { 249 // Always push the next frame after the last child in this segment. 250 // It is possible that we pulled it back before our next-in-flow 251 // drain our overflow. 252 PushChildrenToOverflow(lastChild->GetNextSibling(), lastChild); 253 aReflowInput.mLineLayout->SetDirtyNextLine(); 254 } 255 } else if (rtcCount) { 256 DestroyContext context(PresShell()); 257 // If the ruby base container is reflowed completely, the line 258 // layout will remove the next-in-flows of that frame. But the 259 // line layout is not aware of the ruby text containers, hence 260 // it is necessary to remove them here. 261 for (uint32_t i = 0; i < rtcCount; i++) { 262 if (nsIFrame* nextRTC = textContainers[i]->GetNextInFlow()) { 263 nextRTC->GetParent()->DeleteNextInFlowChild(context, nextRTC, true); 264 } 265 } 266 } 267 268 nscoord segmentISize = baseMetrics.ISize(lineWM); 269 const nsSize dummyContainerSize; 270 LogicalRect baseRect = 271 aBaseContainer->GetLogicalRect(lineWM, dummyContainerSize); 272 // We need to position our rtc frames on one side or the other of the 273 // base container's rect, using a coordinate space that's relative to 274 // the ruby frame. Right now, the base container's rect's block-axis 275 // position is relative to the block container frame containing the 276 // lines, so here we reset it to the different between the ascents of 277 // the ruby container and the ruby base container, assuming they are 278 // aligned with the baseline. 279 // XXX We may need to add border/padding here. See bug 1055667. 280 baseRect.BStart(lineWM) = aBlockStartAscent - baseMetrics.BlockStartAscent(); 281 // The rect for offsets of text containers. 282 LogicalRect offsetRect = baseRect; 283 284 // Should we do ruby positioning based on em-normalized ascent/descent? 285 // This seems to be roughly what Chrome does, and with many fonts it gives 286 // a better result (ruby closer to the base text) than using the font's 287 // declared metrics, if its ascent/descent incorporate lots of extra space. 288 bool normalizeRubyMetrics = aPresContext->NormalizeRubyMetrics(); 289 float rubyMetricsFactor = 290 normalizeRubyMetrics ? aPresContext->RubyPositioningFactor() : 0.0f; 291 mozilla::RubyMetrics rubyMetrics; 292 293 if (normalizeRubyMetrics) { 294 // Get base container's ascent & descent, normalized to 1em (scaled by the 295 // ruby positioning factor) overall extent, and adjust offsetRect to match. 296 rubyMetrics = aBaseContainer->RubyMetrics(rubyMetricsFactor); 297 offsetRect.BStart(lineWM) += 298 baseMetrics.BlockStartAscent() - rubyMetrics.mAscent; 299 offsetRect.BSize(lineWM) = rubyMetrics.mAscent + rubyMetrics.mDescent; 300 } else { 301 RubyBlockLeadings descLeadings = aBaseContainer->GetDescendantLeadings(); 302 offsetRect.BStart(lineWM) -= descLeadings.mStart; 303 offsetRect.BSize(lineWM) += descLeadings.mStart + descLeadings.mEnd; 304 } 305 306 Maybe<LineRelativeDir> lastLineSide; 307 308 // Keep track of any leading required on block-start and/or block-end sides 309 // of the base frame to accommodate annotations that project beyond it. 310 nscoord startLeading = 0, endLeading = 0; 311 312 for (uint32_t i = 0; i < rtcCount; i++) { 313 nsRubyTextContainerFrame* textContainer = textContainers[i]; 314 WritingMode rtcWM = textContainer->GetWritingMode(); 315 nsReflowStatus textReflowStatus; 316 ReflowOutput textMetrics(aReflowInput); 317 ReflowInput textReflowInput(aPresContext, aReflowInput, textContainer, 318 availSize.ConvertTo(rtcWM, lineWM)); 319 textContainer->Reflow(aPresContext, textMetrics, textReflowInput, 320 textReflowStatus); 321 // Ruby text containers always return complete reflow status even when 322 // they have continuations, because the breaking has already been 323 // handled when reflowing the base containers. 324 NS_ASSERTION(textReflowStatus.IsEmpty(), 325 "Ruby text container must not break itself inside"); 326 327 nscoord textEmHeight = 0; 328 nscoord ascentDelta = 0; 329 nscoord bStartMargin = 0; 330 if (normalizeRubyMetrics) { 331 auto [ascent, descent] = textContainer->RubyMetrics(rubyMetricsFactor); 332 const auto* firstChild = textContainer->PrincipalChildList().FirstChild(); 333 textEmHeight = ascent + descent; 334 nscoord textBlockStartAscent = 335 firstChild && textMetrics.BlockStartAscent() == 336 ReflowOutput::ASK_FOR_BASELINE 337 ? firstChild->GetLogicalBaseline(lineWM) 338 : textMetrics.BlockStartAscent(); 339 ascentDelta = textBlockStartAscent - ascent; 340 bStartMargin = 341 firstChild ? firstChild->GetLogicalUsedMargin(lineWM).BStart(lineWM) 342 : 0; 343 } 344 345 const LogicalSize size = textMetrics.Size(lineWM); 346 textContainer->SetSize(lineWM, size); 347 348 nscoord reservedISize = RubyUtils::GetReservedISize(textContainer); 349 segmentISize = std::max(segmentISize, size.ISize(lineWM) + reservedISize); 350 351 Maybe<LineRelativeDir> lineSide; 352 switch (textContainer->StyleText()->mRubyPosition) { 353 case StyleRubyPosition::Over: 354 lineSide.emplace(LineRelativeDir::Over); 355 break; 356 case StyleRubyPosition::Under: 357 lineSide.emplace(LineRelativeDir::Under); 358 break; 359 case StyleRubyPosition::AlternateOver: 360 if (lastLineSide.isSome() && 361 lastLineSide.value() == LineRelativeDir::Over) { 362 lineSide.emplace(LineRelativeDir::Under); 363 } else { 364 lineSide.emplace(LineRelativeDir::Over); 365 } 366 break; 367 case StyleRubyPosition::AlternateUnder: 368 if (lastLineSide.isSome() && 369 lastLineSide.value() == LineRelativeDir::Under) { 370 lineSide.emplace(LineRelativeDir::Over); 371 } else { 372 lineSide.emplace(LineRelativeDir::Under); 373 } 374 break; 375 default: 376 // XXX inter-character support in bug 1055672 377 MOZ_ASSERT_UNREACHABLE("Unsupported ruby-position"); 378 } 379 lastLineSide = lineSide; 380 381 LogicalPoint position(lineWM); 382 if (lineSide.isSome()) { 383 LogicalSide logicalSide = 384 lineWM.LogicalSideForLineRelativeDir(lineSide.value()); 385 if (StaticPrefs::layout_css_ruby_intercharacter_enabled() && 386 rtcWM.IsVerticalRL() && 387 lineWM.GetInlineDir() == WritingMode::InlineDir::LTR) { 388 // Inter-character ruby annotations are only supported for vertical-rl 389 // in ltr horizontal writing. Fall back to non-inter-character behavior 390 // otherwise. 391 LogicalPoint offset( 392 lineWM, offsetRect.ISize(lineWM), 393 offsetRect.BSize(lineWM) > size.BSize(lineWM) 394 ? (offsetRect.BSize(lineWM) - size.BSize(lineWM)) / 2 395 : 0); 396 position = offsetRect.Origin(lineWM) + offset; 397 aReflowInput.mLineLayout->AdvanceICoord(size.ISize(lineWM)); 398 } else if (logicalSide == LogicalSide::BStart) { 399 if (normalizeRubyMetrics) { 400 offsetRect.BStart(lineWM) -= textEmHeight; 401 offsetRect.BSize(lineWM) += textEmHeight; 402 position.I(lineWM) = offsetRect.IStart(lineWM); 403 position.B(lineWM) = offsetRect.BStart(lineWM) - ascentDelta; 404 rubyMetrics.mAscent += textEmHeight; 405 } else { 406 offsetRect.BStart(lineWM) -= size.BSize(lineWM); 407 offsetRect.BSize(lineWM) += size.BSize(lineWM); 408 position = offsetRect.Origin(lineWM); 409 } 410 // Record leading that will be needed on the block-start side to 411 // accommodate the ruby text. 412 startLeading = -position.B(lineWM); 413 } else if (logicalSide == LogicalSide::BEnd) { 414 if (normalizeRubyMetrics) { 415 position.I(lineWM) = offsetRect.IStart(lineWM); 416 position.B(lineWM) = 417 offsetRect.BEnd(lineWM) - ascentDelta - bStartMargin; 418 offsetRect.BSize(lineWM) += textEmHeight; 419 rubyMetrics.mDescent += textEmHeight; 420 } else { 421 position = offsetRect.Origin(lineWM) + 422 LogicalPoint(lineWM, 0, offsetRect.BSize(lineWM)); 423 offsetRect.BSize(lineWM) += size.BSize(lineWM); 424 } 425 // Record leading that will be needed on the block-end side to 426 // accommodate the ruby text. 427 endLeading = position.B(lineWM) + size.BSize(lineWM) - aBlockSize; 428 } else { 429 MOZ_ASSERT_UNREACHABLE("???"); 430 } 431 } 432 // Using a dummy container-size here, so child positioning may not be 433 // correct. We will fix it in nsLineLayout after the whole line is 434 // reflowed. 435 FinishReflowChild(textContainer, aPresContext, textMetrics, 436 &textReflowInput, lineWM, position, dummyContainerSize, 437 ReflowChildFlags::Default); 438 } 439 MOZ_ASSERT(baseRect.ISize(lineWM) == offsetRect.ISize(lineWM), 440 "Annotations should only be placed on the block directions"); 441 442 nscoord deltaISize = segmentISize - baseMetrics.ISize(lineWM); 443 if (deltaISize <= 0) { 444 RubyUtils::ClearReservedISize(aBaseContainer); 445 } else { 446 RubyUtils::SetReservedISize(aBaseContainer, deltaISize); 447 aReflowInput.mLineLayout->AdvanceICoord(deltaISize); 448 } 449 450 // Record leadings needed for the block container. 451 // The leadings are the difference between the rect of this ruby container, 452 // which has block start zero and block size aBlockSize, and the block-start/ 453 // block-end edges of the outermost annotations. 454 // We clamp the values here because if there was significant leading built 455 // into the font's original metrics, the annotations may be entirely within 456 // this space, but we don't want to introduce negative leading. 457 mLeadings.Update(std::max(0, startLeading), std::max(0, endLeading)); 458 459 // Record metrics to be used if any further annotations are added. 460 mRubyMetrics.CombineWith(rubyMetrics); 461 } 462 463 nsRubyBaseContainerFrame* nsRubyFrame::PullOneSegment( 464 const nsLineLayout* aLineLayout, ContinuationTraversingState& aState) { 465 // Pull a ruby base container 466 nsIFrame* baseFrame = GetNextInFlowChild(aState); 467 if (!baseFrame) { 468 return nullptr; 469 } 470 MOZ_ASSERT(baseFrame->IsRubyBaseContainerFrame()); 471 472 // Get the float containing block of the base frame before we pull it. 473 nsBlockFrame* oldFloatCB = nsLayoutUtils::GetFloatContainingBlock(baseFrame); 474 PullNextInFlowChild(aState); 475 476 // Pull all ruby text containers following the base container 477 nsIFrame* nextFrame; 478 while ((nextFrame = GetNextInFlowChild(aState)) != nullptr && 479 nextFrame->IsRubyTextContainerFrame()) { 480 PullNextInFlowChild(aState); 481 } 482 483 if (nsBlockFrame* newFloatCB = 484 do_QueryFrame(aLineLayout->LineContainerFrame())) { 485 if (oldFloatCB && oldFloatCB != newFloatCB) { 486 newFloatCB->ReparentFloats(baseFrame, oldFloatCB, true); 487 } 488 } 489 490 return static_cast<nsRubyBaseContainerFrame*>(baseFrame); 491 }