mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 15:32:46 +00:00 
			
		
		
		
	LibHTML: Implement LayoutText
This commit is contained in:
		
							parent
							
								
									9f8d776c70
								
							
						
					
					
						commit
						93003bfda1
					
				
					 4 changed files with 190 additions and 4 deletions
				
			
		|  | @ -78,8 +78,10 @@ void dump_tree(const LayoutNode& layout_node) | |||
|         layout_node.style().border().bottom.to_px(), | ||||
|         layout_node.style().margin().bottom.to_px()); | ||||
| 
 | ||||
|     if (layout_node.is_text()) | ||||
|         printf(" \"%s\"", static_cast<const LayoutText&>(layout_node).text().characters()); | ||||
|     if (layout_node.is_text()) { | ||||
|         const LayoutText& layout_text = static_cast<const LayoutText&>(layout_node); | ||||
|         printf(" \"%s\", %d runs", layout_text.text().characters(), layout_text.runs().size()); | ||||
|     } | ||||
| 
 | ||||
|     printf("\n"); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,3 +1,7 @@ | |||
| #include <AK/StringBuilder.h> | ||||
| #include <LibCore/CDirIterator.h> | ||||
| #include <LibDraw/Font.h> | ||||
| #include <LibHTML/Layout/LayoutBlock.h> | ||||
| #include <LibHTML/Layout/LayoutText.h> | ||||
| #include <ctype.h> | ||||
| 
 | ||||
|  | @ -10,6 +14,55 @@ LayoutText::~LayoutText() | |||
| { | ||||
| } | ||||
| 
 | ||||
| void LayoutText::load_font() | ||||
| { | ||||
|     auto font_family = style_properties().string_or_fallback("font-family", "Katica"); | ||||
|     auto font_weight = style_properties().string_or_fallback("font-weight", "normal"); | ||||
| 
 | ||||
|     String weight; | ||||
|     if (font_weight == "lighter") | ||||
|         weight = "Thin"; | ||||
|     else if (font_weight == "normal") | ||||
|         weight = ""; | ||||
|     else if (font_weight == "bold") | ||||
|         weight = "Bold"; | ||||
|     else | ||||
|         ASSERT_NOT_REACHED(); | ||||
| 
 | ||||
|     auto look_for_file = [](const StringView& expected_name) -> String { | ||||
|         // TODO: handle font sizes properly?
 | ||||
|         CDirIterator it { "/res/fonts/", CDirIterator::Flags::SkipDots }; | ||||
|         while (it.has_next()) { | ||||
|             String name = it.next_path(); | ||||
|             ASSERT(name.ends_with(".font")); | ||||
|             if (!name.starts_with(expected_name)) | ||||
|                 continue; | ||||
| 
 | ||||
|             // Check that a numeric size immediately
 | ||||
|             // follows the font name. This prevents,
 | ||||
|             // for example, matching KaticaBold when
 | ||||
|             // the regular Katica is requested.
 | ||||
|             if (!isdigit(name[expected_name.length()])) | ||||
|                 continue; | ||||
| 
 | ||||
|             return name; | ||||
|         } | ||||
|         return {}; | ||||
|     }; | ||||
| 
 | ||||
|     String file_name = look_for_file(String::format("%s%s", font_family.characters(), weight.characters())); | ||||
|     if (file_name.is_null() && weight == "") | ||||
|         file_name = look_for_file(String::format("%sRegular", font_family.characters())); | ||||
| 
 | ||||
|     if (file_name.is_null()) { | ||||
|         dbg() << "Failed to find a font for family " << font_family << " weight " << font_weight; | ||||
|         dbg() << "My text is " << node().data(); | ||||
|         ASSERT_NOT_REACHED(); | ||||
|     } | ||||
|     dbg() << "Found font " << file_name << " for family " << font_family << " weight " << font_weight; | ||||
|     m_font = Font::load_from_file(String::format("/res/fonts/%s", file_name.characters())); | ||||
| } | ||||
| 
 | ||||
| static bool is_all_whitespace(const String& string) | ||||
| { | ||||
|     for (int i = 0; i < string.length(); ++i) { | ||||
|  | @ -23,16 +76,143 @@ const String& LayoutText::text() const | |||
| { | ||||
|     static String one_space = " "; | ||||
|     if (is_all_whitespace(node().data())) | ||||
|         return one_space; | ||||
|         if (style_properties().string_or_fallback("white-space", "normal") == "normal") | ||||
|             return one_space; | ||||
|     return node().data(); | ||||
| } | ||||
| 
 | ||||
| static void split_first_word(const StringView& str, StringView& out_space, StringView& out_word) | ||||
| { | ||||
|     int first_nonspace = -1; | ||||
|     for (int i = 0; i < str.length(); i++) | ||||
|         if (!isspace(str[i])) { | ||||
|             first_nonspace = i; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|     if (first_nonspace == -1) { | ||||
|         out_space = str; | ||||
|         out_word = {}; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     int first_space = str.length(); | ||||
|     for (int i = first_nonspace + 1; i < str.length(); i++) | ||||
|         if (isspace(str[i])) { | ||||
|             first_space = i; | ||||
|             break; | ||||
|         } | ||||
| 
 | ||||
|     out_space = str.substring_view(0, first_nonspace); | ||||
|     out_word = str.substring_view(first_nonspace, first_space - first_nonspace); | ||||
| } | ||||
| 
 | ||||
| void LayoutText::compute_runs() | ||||
| { | ||||
|     StringView remaining_text = node().data(); | ||||
|     if (remaining_text.is_empty()) | ||||
|         return; | ||||
| 
 | ||||
|     int right_border = containing_block()->rect().x() + containing_block()->rect().width(); | ||||
| 
 | ||||
|     StringBuilder builder; | ||||
|     Point run_origin = rect().location(); | ||||
| 
 | ||||
|     int total_right_margin = style().full_margin().right; | ||||
|     bool is_preformatted = style_properties().string_or_fallback("white-space", "normal") != "normal"; | ||||
| 
 | ||||
|     while (!remaining_text.is_empty()) { | ||||
|         String saved_text = builder.string_view(); | ||||
| 
 | ||||
|         // Try to append a new word.
 | ||||
|         StringView space; | ||||
|         StringView word; | ||||
|         split_first_word(remaining_text, space, word); | ||||
| 
 | ||||
|         int forced_line_break_index = -1; | ||||
|         if (is_preformatted) | ||||
|             for (int i = 0; i < space.length(); i++) | ||||
|                 if (space[i] == '\n') { | ||||
|                     forced_line_break_index = i; | ||||
|                     break; | ||||
|                 } | ||||
| 
 | ||||
|         if (!space.is_empty()) { | ||||
|             if (!is_preformatted) { | ||||
|                 builder.append(' '); | ||||
|             } else if (forced_line_break_index != -1) { | ||||
|                 builder.append(space.substring_view(0, forced_line_break_index)); | ||||
|             } else { | ||||
|                 builder.append(space); | ||||
|             } | ||||
|         } | ||||
|         if (forced_line_break_index == -1) | ||||
|             builder.append(word); | ||||
| 
 | ||||
|         if (forced_line_break_index != -1) | ||||
|             remaining_text = remaining_text.substring_view(forced_line_break_index + 1, remaining_text.length() - forced_line_break_index - 1); | ||||
|         else if (!word.is_null()) | ||||
|             remaining_text = remaining_text.substring_view_starting_after_substring(word); | ||||
|         else | ||||
|             remaining_text = {}; | ||||
| 
 | ||||
|         // See if that fits.
 | ||||
|         int width = m_font->width(builder.string_view()); | ||||
|         if (forced_line_break_index == -1 && run_origin.x() + width + total_right_margin < right_border) | ||||
|             continue; | ||||
| 
 | ||||
|         // If it doesn't, create a run from
 | ||||
|         // what we had there previously.
 | ||||
|         if (forced_line_break_index == -1) | ||||
|             m_runs.append({ run_origin, move(saved_text) }); | ||||
|         else | ||||
|             m_runs.append({ run_origin, builder.string_view() }); | ||||
| 
 | ||||
|         // Start a new run at the new line.
 | ||||
|         int line_spacing = 4; | ||||
|         run_origin.set_x(containing_block()->rect().x() + style().full_margin().left); | ||||
|         run_origin.move_by(0, m_font->glyph_height() + line_spacing); | ||||
|         builder = StringBuilder(); | ||||
|         if (forced_line_break_index != -1) | ||||
|             continue; | ||||
|         if (is_preformatted) | ||||
|             builder.append(space); | ||||
|         builder.append(word); | ||||
|     } | ||||
| 
 | ||||
|     // Add the last run.
 | ||||
|     m_runs.append({ run_origin, builder.build() }); | ||||
| } | ||||
| 
 | ||||
| void LayoutText::layout() | ||||
| { | ||||
|     ASSERT(!has_children()); | ||||
| 
 | ||||
|     if (!m_font) | ||||
|         load_font(); | ||||
| 
 | ||||
|     int origin_x = -1; | ||||
|     int origin_y = -1; | ||||
|     if (previous_sibling() != nullptr) { | ||||
|         auto& previous_sibling_rect = previous_sibling()->rect(); | ||||
|         auto& previous_sibling_style = previous_sibling()->style(); | ||||
|         origin_x = previous_sibling_rect.x() + previous_sibling_rect.width(); | ||||
|         origin_x += previous_sibling_style.full_margin().right; | ||||
|         origin_y = previous_sibling_rect.y() + previous_sibling_rect.height() - m_font->glyph_height() - previous_sibling_style.full_margin().top; | ||||
|     } else { | ||||
|         origin_x = parent()->rect().x(); | ||||
|         origin_y = parent()->rect().y(); | ||||
|     } | ||||
|     rect().set_x(origin_x + style().full_margin().left); | ||||
|     rect().set_y(origin_y + style().full_margin().top); | ||||
| 
 | ||||
|     m_runs.clear(); | ||||
|     compute_runs(); | ||||
| 
 | ||||
|     if (m_runs.is_empty()) | ||||
|         return; | ||||
| 
 | ||||
|     const Run& last_run = m_runs[m_runs.size() - 1]; | ||||
|     rect().set_right(last_run.pos.x() + m_font->width(last_run.text)); | ||||
|     rect().set_bottom(last_run.pos.y() + m_font->glyph_height()); | ||||
| } | ||||
|  |  | |||
|  | @ -3,6 +3,8 @@ | |||
| #include <LibHTML/DOM/Text.h> | ||||
| #include <LibHTML/Layout/LayoutNode.h> | ||||
| 
 | ||||
| class Font; | ||||
| 
 | ||||
| class LayoutText : public LayoutNode { | ||||
| public: | ||||
|     LayoutText(const Text&, StyleProperties&&); | ||||
|  | @ -24,7 +26,9 @@ public: | |||
|     const Vector<Run>& runs() const { return m_runs; } | ||||
| 
 | ||||
| private: | ||||
|     void load_font(); | ||||
|     void compute_runs(); | ||||
| 
 | ||||
|     Vector<Run> m_runs; | ||||
|     RefPtr<Font> m_font; | ||||
| }; | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ all: $(LIBRARY) tho | |||
| include Makefile.shared | ||||
| 
 | ||||
| tho: $(TEST_OBJS) $(LIBRARY) | ||||
| 	$(LD) -o $@ $(LDFLAGS) -L. $(TEST_OBJS) -lhtml -lgui -lcore -lc | ||||
| 	$(LD) -o $@ $(LDFLAGS) -L. $(TEST_OBJS) -lhtml -lgui -ldraw -lcore -lc | ||||
| 
 | ||||
| $(LIBRARY): $(LIBHTML_OBJS) | ||||
| 	@echo "LIB $@"; $(AR) rcs $@ $(LIBHTML_OBJS) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Sergey Bugaev
						Sergey Bugaev