/* * This source file is part of RmlUi, the HTML/CSS Interface Middleware * * For the latest information, see http://github.com/mikke89/RmlUi * * Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd * Copyright (c) 2019-2023 The RmlUi Team, and contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ #include "BlockFormattingContext.h" #include "../../../Include/RmlUi/Core/ComputedValues.h" #include "../../../Include/RmlUi/Core/Element.h" #include "../../../Include/RmlUi/Core/Profiling.h" #include "../../../Include/RmlUi/Core/PropertyDefinition.h" #include "../../../Include/RmlUi/Core/StyleSheetSpecification.h" #include "../../../Include/RmlUi/Core/SystemInterface.h" #include "BlockContainer.h" #include "FloatedBoxSpace.h" #include "LayoutDetails.h" namespace Rml { // Table elements should be handled within FormatElementTable, log a warning when it seems like we're encountering table parts in the wild. static void LogUnexpectedFlowElement(Element* element, Style::Display display) { RMLUI_ASSERT(element); String value = "*unknown"; StyleSheetSpecification::GetPropertySpecification().GetProperty(PropertyId::Display)->GetValue(value, Property(display)); Log::Message(Log::LT_WARNING, "Element has a display type '%s' which cannot be located in normal flow layout. Element will not be formatted: %s", value.c_str(), element->GetAddress().c_str()); } #ifdef RMLUI_DEBUG static bool g_debug_dumping_layout_tree = false; struct DebugDumpLayoutTree { Element* element; BlockContainer* block_box; bool is_printing_tree_root = false; DebugDumpLayoutTree(Element* element, BlockContainer* block_box) : element(element), block_box(block_box) { // When an element with this ID is encountered, dump the formatted layout tree (including for all descendant formatting contexts). static const String debug_trigger_id = "rmlui-debug-layout"; is_printing_tree_root = element->HasAttribute(debug_trigger_id); if (is_printing_tree_root) g_debug_dumping_layout_tree = true; } ~DebugDumpLayoutTree() { if (g_debug_dumping_layout_tree) { const String header = ":: " + LayoutDetails::GetDebugElementName(element) + " ::\n"; const String layout_tree = header + block_box->DumpLayoutTree(); if (SystemInterface* system_interface = GetSystemInterface()) system_interface->LogMessage(Log::LT_INFO, layout_tree); if (is_printing_tree_root) g_debug_dumping_layout_tree = false; } } }; #else struct DebugDumpLayoutTree { DebugDumpLayoutTree(Element* /*element*/, BlockContainer* /*block_box*/) {} }; #endif enum class OuterDisplayType { BlockLevel, InlineLevel, Invalid }; static OuterDisplayType GetOuterDisplayType(Style::Display display) { switch (display) { case Style::Display::Block: case Style::Display::FlowRoot: case Style::Display::Flex: case Style::Display::Table: return OuterDisplayType::BlockLevel; case Style::Display::Inline: case Style::Display::InlineBlock: case Style::Display::InlineFlex: case Style::Display::InlineTable: return OuterDisplayType::InlineLevel; case Style::Display::TableRow: case Style::Display::TableRowGroup: case Style::Display::TableColumn: case Style::Display::TableColumnGroup: case Style::Display::TableCell: case Style::Display::None: break; } return OuterDisplayType::Invalid; } UniquePtr BlockFormattingContext::Format(ContainerBox* parent_container, Element* element, Vector2f containing_block, const Box& box) { RMLUI_ASSERT(parent_container && element); RMLUI_ZoneScopedC(0xB22222); RMLUI_ZoneName(CreateString("%s %x", element->GetAddress(false, false).c_str(), element)); float min_height, max_height; LayoutDetails::GetDefiniteMinMaxHeight(min_height, max_height, element->GetComputedValues(), box, containing_block.y); UniquePtr container = MakeUnique(parent_container, nullptr, element, box, min_height, max_height); DebugDumpLayoutTree debug_dump_tree(element, container.get()); container->ResetScrollbars(box); // Format the element's children. In rare cases, it is possible that we need three iterations: Once to enable the // horizontal scrollbar, then to enable the vertical scrollbar, and finally to format with both scrollbars enabled. for (int layout_iteration = 0; layout_iteration < 3; layout_iteration++) { bool all_children_formatted = true; for (int i = 0; i < element->GetNumChildren() && all_children_formatted; i++) { if (!FormatBlockContainerChild(container.get(), element->GetChild(i))) all_children_formatted = false; } if (all_children_formatted && container->Close(nullptr)) // Success, break out of the loop. break; // Otherwise, restart formatting now that one or both scrollbars have been enabled. container->ResetContents(); } return container; } float BlockFormattingContext::DetermineMaxContentWidth(Element* element, const Box& initial_box, const FormattingMode& formatting_mode) { RMLUI_ASSERT(formatting_mode.constraint == FormattingMode::Constraint::MaxContent); const Vector2f containing_block(-1.f); RootBox root(Box(containing_block), formatting_mode); UniquePtr layout_box = BlockFormattingContext::Format(&root, element, containing_block, initial_box); return layout_box->GetShrinkToFitWidth(); } bool BlockFormattingContext::FormatBlockBox(BlockContainer* parent_container, Element* element) { RMLUI_ZoneScopedC(0x2F4F4F); const Vector2f containing_block = parent_container->GetContainingBlockSize(element->GetPosition()); Box box; LayoutDetails::BuildBox(box, containing_block, element, BuildBoxMode::StretchFit); float min_height, max_height; LayoutDetails::GetDefiniteMinMaxHeight(min_height, max_height, element->GetComputedValues(), box, containing_block.y); BlockContainer* container = parent_container->OpenBlockBox(element, box, min_height, max_height); if (!container) return false; // Format our children. This may result in scrollbars being added to our formatting context root, then we need to // bail out and restart formatting for the current block formatting context. for (int i = 0; i < element->GetNumChildren(); i++) { if (!FormatBlockContainerChild(container, element->GetChild(i))) return false; } if (!container->Close(parent_container)) return false; return true; } bool BlockFormattingContext::FormatInlineBox(BlockContainer* parent_container, Element* element) { RMLUI_ZoneScopedC(0x3F6F6F); const Vector2f containing_block = parent_container->GetContainingBlockSize(element->GetPosition()); Box box; LayoutDetails::BuildBox(box, containing_block, element, BuildBoxMode::Inline); auto inline_box_handle = parent_container->AddInlineElement(element, box); // Format the element's children. for (int i = 0; i < element->GetNumChildren(); i++) { if (!FormatBlockContainerChild(parent_container, element->GetChild(i))) return false; } parent_container->CloseInlineElement(inline_box_handle); return true; } bool BlockFormattingContext::FormatBlockContainerChild(BlockContainer* parent_container, Element* element) { RMLUI_ZoneScoped; RMLUI_ZoneName(CreateString(">%s %x", element->GetAddress(false, false).c_str(), element)); // Check for special formatting tags. if (element->GetTagName() == "br") { parent_container->AddBreak(); return true; } auto& computed = element->GetComputedValues(); const Style::Display display = computed.display(); // Don't lay this element out if it is set to a display type of none. if (display == Style::Display::None) return true; // Check for absolutely positioned elements: they are removed from the flow and added to the box representing their // containing block, to be layed out and positioned once that box has been closed and sized. const Style::Position position_property = computed.position(); if (position_property == Style::Position::Absolute || position_property == Style::Position::Fixed) { // TODO: When laying out an absolutely positioned element independently, we need to do the equivalent of this, // and the following `ContainerBox::ClosePositionedElements`. Do we need to store the static position and parent // container element? Hmm, I guess we can assume that the static position stays the same, since otherwise we // would have had an update occur in our parent, thereby closing it in the formatting context of the static // element. ... Looking more closely at it, looks like all we need is to call // `UpdateRelativeOffsetFromInsetConstraints`? // TODO Part 2: UpdateRelativeOffsetFromInsetConstraints alone solves some problems, but not all. Another issue // is when the box on the absolute element is changed, particularly margin edges. Then `ClosePositionedElements` // would change the submitted offset. Should we store this in the layout cache? Should we store the static // position and element, directly on the element? Maybe either/or that we are using the element's margin. Hmm, // don't really like that. Can we know that we are in a block formatting context? No, shouldn't depend directly // on that. Hacky solution: If absolutely positioned element layed out separately, check changed margin edges, // and apply the diff to SetOffset with otherwise the same as existing. // Can we set the static position directly on the element (here basically), and just call a function to update // everything at the end? const Vector2f static_position = parent_container->GetOpenStaticPosition(display) - parent_container->GetPosition(); parent_container->AddAbsoluteElement(element, static_position, parent_container->GetElement()); return true; } const OuterDisplayType outer_display = GetOuterDisplayType(display); if (outer_display == OuterDisplayType::Invalid) { LogUnexpectedFlowElement(element, display); return true; } // If the element creates an independent formatting context, then format it accordingly. if (UniquePtr layout_box = FormattingContext::FormatIndependent(parent_container, element, nullptr, FormattingContextType::None)) { // If the element is floating, we remove it from the flow. if (computed.float_() != Style::Float::None) { parent_container->AddFloatElement(element, layout_box->GetVisibleOverflowSize()); } // Otherwise, check if we have a sized block-level box. else if (layout_box && outer_display == OuterDisplayType::BlockLevel) { if (!parent_container->AddBlockLevelBox(std::move(layout_box), element, element->GetBox())) return false; } // Nope, then this must be an inline-level box. else { RMLUI_ASSERT(outer_display == OuterDisplayType::InlineLevel); auto inline_box_handle = parent_container->AddInlineElement(element, element->GetBox()); parent_container->CloseInlineElement(inline_box_handle); } return true; } // The element is an in-flow box participating in this same block formatting context. switch (display) { case Style::Display::Block: return FormatBlockBox(parent_container, element); case Style::Display::Inline: return FormatInlineBox(parent_container, element); default: RMLUI_ERROR; // Should have been handled above. break; } return true; } } // namespace Rml