Selaa lähdekoodia

Add 'text-overflow' property, see #849

Includes support for ellipsis as well as custom strings.

Currently only applies directly to text, not to inline-level atomic boxes.
Michael Ragazzon 1 kuukausi sitten
vanhempi
sitoutus
4462a47276

+ 10 - 5
Include/RmlUi/Core/ComputedValues.h

@@ -113,10 +113,10 @@ namespace Style {
 
 	struct InheritedValues {
 		InheritedValues() :
-			font_weight(FontWeight::Normal), font_kerning(FontKerning::Auto), has_letter_spacing(0), font_style(FontStyle::Normal), has_font_effect(false),
-			pointer_events(PointerEvents::Auto), focus(Focus::Auto), text_align(TextAlign::Left), text_decoration(TextDecoration::None),
-			text_transform(TextTransform::None), white_space(WhiteSpace::Normal), word_break(WordBreak::Normal), direction(Direction::Auto),
-			line_height_inherit_type(LineHeight::Number)
+			font_weight(FontWeight::Normal), font_kerning(FontKerning::Auto), has_letter_spacing(0), font_style(FontStyle::Normal),
+			has_font_effect(false), pointer_events(PointerEvents::Auto), focus(Focus::Auto), text_align(TextAlign::Left),
+			text_decoration(TextDecoration::None), text_transform(TextTransform::None), white_space(WhiteSpace::Normal),
+			word_break(WordBreak::Normal), direction(Direction::Auto), line_height_inherit_type(LineHeight::Number)
 		{}
 
 		// Font face used to render text and resolve ex properties. Does not represent a true property
@@ -165,7 +165,7 @@ namespace Style {
 
 			vertical_align_type(VerticalAlign::Baseline), drag(Drag::None), tab_index(TabIndex::None), overscroll_behavior(OverscrollBehavior::Auto),
 
-			has_mask_image(false), has_filter(false), has_backdrop_filter(false), has_box_shadow(false)
+			has_mask_image(false), has_filter(false), has_backdrop_filter(false), has_box_shadow(false), text_overflow(TextOverflow::Clip)
 		{}
 
 		LengthPercentage::Type min_width_type : 1, max_width_type : 1;
@@ -188,6 +188,8 @@ namespace Style {
 		bool has_backdrop_filter : 1;
 		bool has_box_shadow : 1;
 
+		TextOverflow text_overflow : 2;
+
 		Clip clip;
 
 		float min_width = 0, max_width = FLT_MAX;
@@ -306,6 +308,8 @@ namespace Style {
 		float             border_bottom_left_radius()  const { return (float)rare.border_bottom_left_radius; }
 		CornerSizes       border_radius()              const { return {(float)rare.border_top_left_radius,     (float)rare.border_top_right_radius,
 		                                                               (float)rare.border_bottom_right_radius, (float)rare.border_bottom_left_radius}; }
+		TextOverflow      text_overflow()              const { return rare.text_overflow; }
+		String            text_overflow_string()       const { return GetLocalProperty(PropertyId::TextOverflow, String());; }
 		Clip              clip()                       const { return rare.clip; }
 		Drag              drag()                       const { return rare.drag; }
 		TabIndex          tab_index()                  const { return rare.tab_index; }
@@ -395,6 +399,7 @@ namespace Style {
 		void border_top_right_radius   (float value)             { rare.border_top_right_radius    = (int16_t)value; }
 		void border_bottom_right_radius(float value)             { rare.border_bottom_right_radius = (int16_t)value; }
 		void border_bottom_left_radius (float value)             { rare.border_bottom_left_radius  = (int16_t)value; }
+		void text_overflow             (TextOverflow value)      { rare.text_overflow              = value; }
 		void clip                      (Clip value)              { rare.clip                       = value; }
 		void drag                      (Drag value)              { rare.drag                       = value; }
 		void tab_index                 (TabIndex value)          { rare.tab_index                  = value; }

+ 2 - 0
Include/RmlUi/Core/FontMetrics.h

@@ -43,6 +43,8 @@ struct FontMetrics {
 
 	float underline_position;  // Position of the underline relative to the baseline [px, positive below baseline].
 	float underline_thickness; // Width of underline [px].
+
+	bool has_ellipsis;         // True if the ellipsis character (U+2026) is available in this font.
 };
 
 } // namespace Rml

+ 1 - 0
Include/RmlUi/Core/ID.h

@@ -119,6 +119,7 @@ enum class PropertyId : uint8_t {
 	OverflowY,
 	Clip,
 	Visibility,
+	TextOverflow,
 	BackgroundColor,
 	Color,
 	CaretColor,

+ 1 - 0
Include/RmlUi/Core/StyleTypes.h

@@ -128,6 +128,7 @@ namespace Style {
 	};
 
 	enum class Visibility : uint8_t { Visible, Hidden };
+	enum class TextOverflow : uint8_t { Clip, Ellipsis, String };
 
 	enum class FontStyle : uint8_t { Normal, Italic };
 	enum class FontWeight : uint16_t { Auto = 0, Normal = 400, Bold = 700 }; // Any definite value in the range [1,1000] is valid.

+ 2 - 0
Samples/basic/bitmap_font/src/FontEngineBitmap.cpp

@@ -264,6 +264,8 @@ void FontParserBitmap::HandleElementStart(const String& name, const Rml::XMLAttr
 
 		if (character == (Character)'x')
 			metrics.x_height = glyph.dimension.y;
+		if (character == (Character)0x2026)
+			metrics.has_ellipsis = true;
 	}
 	else if (name == "kerning")
 	{

+ 3 - 0
Samples/basic/harfbuzz/src/FreeTypeInterface.cpp

@@ -297,6 +297,9 @@ static void GenerateMetrics(FT_Face ft_face, FontMetrics& metrics, float bitmap_
 		metrics.x_height = ft_face->glyph->metrics.height * bitmap_scaling_factor / float(1 << 6);
 	else
 		metrics.x_height = 0.5f * metrics.line_spacing;
+
+	FT_UInt ellipsis_index = FT_Get_Char_Index(ft_face, 0x2026);
+	metrics.has_ellipsis = (ellipsis_index != 0);
 }
 
 static bool SetFontSize(FT_Face ft_face, int font_size, float& out_bitmap_scaling_factor)

+ 8 - 0
Source/Core/ElementStyle.cpp

@@ -744,6 +744,9 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 		case PropertyId::Visibility:
 			values.visibility((Visibility)p->Get< int >());
 			break;
+		case PropertyId::TextOverflow:
+			values.text_overflow(p->unit == Unit::KEYWORD ? p->Get<TextOverflow>() : TextOverflow::String);
+			break;
 
 		case PropertyId::BackgroundColor:
 			values.background_color(p->Get<Colourb>());
@@ -922,6 +925,11 @@ PropertyIdSet ElementStyle::ComputeValues(Style::ComputedValues& values, const S
 	// Next, pass inheritable dirty properties onto our children
 	PropertyIdSet dirty_inherited_properties = (dirty_properties & StyleSheetSpecification::GetRegisteredInheritedProperties());
 
+	// Special case for text-overflow: It's not really inherited, but the value is used by the element's children. Insert
+	// it here so they are notified of a change.
+	if (dirty_properties.Contains(PropertyId::TextOverflow))
+		dirty_inherited_properties.Insert(PropertyId::TextOverflow);
+
 	if (!dirty_inherited_properties.Empty())
 	{
 		for (int i = 0; i < element->GetNumChildren(true); i++)

+ 91 - 12
Source/Core/ElementText.cpp

@@ -61,6 +61,51 @@ static int RoundDownToIntegerClamped(float value)
 	return static_cast<int>(value);
 }
 
+struct TextOverflowResolved {
+	bool enabled = false;
+	float overflow_width = 0.f;
+	String overflow_text;
+};
+
+static TextOverflowResolved ResolveTextOverflow(Element* parent, FontFaceHandle font_face_handle)
+{
+	if (!parent)
+		return {};
+
+	const auto& parent_computed = parent->GetComputedValues();
+	if (parent_computed.overflow_x() == Style::Overflow::Visible && parent_computed.overflow_y() == Style::Overflow::Visible)
+		return {};
+
+	const Style::TextOverflow text_overflow = parent_computed.text_overflow();
+	if (text_overflow == Style::TextOverflow::Clip)
+		return {};
+
+	const Box& box = parent->GetBox();
+	const BoxArea clip_area = parent->GetClipArea();
+
+	auto AccumulateRightSideEdgesUpTo = [](const Box& box, BoxArea up_to_area) -> float {
+		float result = 0;
+		for (int i = (int)up_to_area; i < int(BoxArea::Content); i++)
+			result += box.GetEdge((BoxArea)i, BoxEdge::Right);
+		return result;
+	};
+
+	const float overflow_width = parent->GetScrollLeft() + box.GetSize().x + AccumulateRightSideEdgesUpTo(box, clip_area);
+
+	constexpr char ellipsis_chars[] = "\xE2\x80\xA6"; // U+2026
+	constexpr char dots_chars[] = "...";
+
+	String overflow_text = (text_overflow == Style::TextOverflow::String
+			? parent->GetComputedValues().text_overflow_string()
+			: (GetFontEngineInterface()->GetFontMetrics(font_face_handle).has_ellipsis ? ellipsis_chars : dots_chars));
+
+	return TextOverflowResolved{
+		true,
+		overflow_width,
+		std::move(overflow_text),
+	};
+}
+
 void LogMissingFontFace(Element* element)
 {
 	const String font_family_property = element->GetProperty<String>("font-family");
@@ -369,14 +414,15 @@ void ElementText::OnPropertyChange(const PropertyIdSet& changed_properties)
 		}
 	}
 
-	if (changed_properties.Contains(PropertyId::FontFamily) ||     //
-		changed_properties.Contains(PropertyId::FontWeight) ||     //
-		changed_properties.Contains(PropertyId::FontStyle) ||      //
-		changed_properties.Contains(PropertyId::FontSize) ||       //
-		changed_properties.Contains(PropertyId::FontKerning) ||    //
-		changed_properties.Contains(PropertyId::LetterSpacing) ||  //
-		changed_properties.Contains(PropertyId::RmlUi_Language) || //
-		changed_properties.Contains(PropertyId::RmlUi_Direction))
+	if (changed_properties.Contains(PropertyId::FontFamily) ||      //
+		changed_properties.Contains(PropertyId::FontWeight) ||      //
+		changed_properties.Contains(PropertyId::FontStyle) ||       //
+		changed_properties.Contains(PropertyId::FontSize) ||        //
+		changed_properties.Contains(PropertyId::FontKerning) ||     //
+		changed_properties.Contains(PropertyId::LetterSpacing) ||   //
+		changed_properties.Contains(PropertyId::RmlUi_Language) ||  //
+		changed_properties.Contains(PropertyId::RmlUi_Direction) || //
+		changed_properties.Contains(PropertyId::TextOverflow))
 	{
 		font_face_changed = true;
 		geometry_dirty = true;
@@ -463,17 +509,50 @@ void ElementText::GenerateGeometry(RenderManager& render_manager, const FontFace
 {
 	RMLUI_ZoneScopedC(0xD2691E);
 
+	const TextOverflowResolved text_overflow = ResolveTextOverflow(GetParentNode(), font_face_handle);
+
 	const auto& computed = GetComputedValues();
 	const TextShapingContext text_shaping_context{computed.language(), computed.direction(), computed.font_kerning(), computed.letter_spacing()};
 
 	TexturedMeshList mesh_list;
 	mesh_list.reserve(geometry.size());
 
-	// Generate the new geometry, one line at a time.
-	for (size_t i = 0; i < lines.size(); ++i)
+	for (Line& line : lines)
+	{
+		line.width = GetFontEngineInterface()->GenerateString(render_manager, font_face_handle, font_effects_handle, line.text, line.position, colour,
+			opacity, text_shaping_context, mesh_list);
+	}
+
+	const auto text_overflows_on_line = [&](const Line& line) { return line.position.x + line.width > text_overflow.overflow_width; };
+	if (text_overflow.enabled && std::any_of(lines.begin(), lines.end(), text_overflows_on_line))
 	{
-		lines[i].width = GetFontEngineInterface()->GenerateString(render_manager, font_face_handle, font_effects_handle, lines[i].text,
-			lines[i].position, colour, opacity, text_shaping_context, mesh_list);
+		mesh_list.clear();
+
+		for (Line& line : lines)
+		{
+			if (line.text.empty())
+				continue;
+
+			String abbreviated_text;
+			StringView text_submit_view = line.text;
+			StringIteratorU8 view(line.text, line.text.size());
+			--view;
+
+			// If we have text overflow, reduce the string one character at a time, append the ellipsis or custom
+			// string, and try again until it fits. @performance Can be improved by e.g. logarithmic search. Consider
+			// combining it with the word-breaking algorithm of 'GenerateLine'.
+			for (; text_overflows_on_line(line) && view && view.get() != line.text.c_str(); --view)
+			{
+				abbreviated_text.reserve(line.text.size() + text_overflow.overflow_text.size());
+				abbreviated_text.assign(line.text.c_str(), view.get());
+				abbreviated_text.append(text_overflow.overflow_text);
+				line.width = GetFontEngineInterface()->GetStringWidth(font_face_handle, abbreviated_text, text_shaping_context);
+				text_submit_view = abbreviated_text;
+			}
+
+			line.width = GetFontEngineInterface()->GenerateString(render_manager, font_face_handle, font_effects_handle, text_submit_view,
+				line.position, colour, opacity, text_shaping_context, mesh_list);
+		}
 	}
 
 	// Apply the new geometry and textures. Reuse the old geometry if the mesh matches, which can be relatively common

+ 5 - 2
Source/Core/FontEngineDefault/FreeTypeInterface.cpp

@@ -138,8 +138,8 @@ FontFaceHandleFreetype FreeType::LoadFace(Span<const byte> data, const String& s
 	RMLUI_ASSERT(ft_library);
 
 	FT_Face face = nullptr;
-	FT_Error error =
-		FT_New_Memory_Face(ft_library, static_cast<const FT_Byte*>(data.data()), static_cast<FT_Long>(data.size()), (named_style_index << 16) | face_index, &face);
+	FT_Error error = FT_New_Memory_Face(ft_library, static_cast<const FT_Byte*>(data.data()), static_cast<FT_Long>(data.size()),
+		(named_style_index << 16) | face_index, &face);
 
 	if (error)
 	{
@@ -468,6 +468,9 @@ static void GenerateMetrics(FT_Face ft_face, FontMetrics& metrics, float bitmap_
 		metrics.x_height = ft_face->glyph->metrics.height * bitmap_scaling_factor / float(1 << 6);
 	else
 		metrics.x_height = 0.5f * metrics.line_spacing;
+
+	FT_UInt ellipsis_index = FT_Get_Char_Index(ft_face, 0x2026);
+	metrics.has_ellipsis = (ellipsis_index != 0);
 }
 
 static bool SetFontSize(FT_Face ft_face, int font_size, float& out_bitmap_scaling_factor)

+ 1 - 0
Source/Core/StyleSheetSpecification.cpp

@@ -365,6 +365,7 @@ void StyleSheetSpecification::RegisterDefaultProperties()
 	RegisterShorthand(ShorthandId::Overflow, "overflow", "overflow-x, overflow-y", ShorthandType::Replicate);
 	RegisterProperty(PropertyId::Clip, "clip", "auto", false, false).AddParser("keyword", "auto, none, always").AddParser("number");
 	RegisterProperty(PropertyId::Visibility, "visibility", "visible", false, false).AddParser("keyword", "visible, hidden");
+	RegisterProperty(PropertyId::TextOverflow, "text-overflow", "clip", false, false).AddParser("keyword", "clip, ellipsis").AddParser("string");
 
 	// Need some work on this if we are to include images.
 	RegisterProperty(PropertyId::BackgroundColor, "background-color", "transparent", false, false).AddParser("color");

+ 96 - 0
Tests/Data/VisualTests/text_overflow_01.rml

@@ -0,0 +1,96 @@
+<rml>
+<head>
+	<link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<title>Text overflow 01</title>
+	<link href="https://drafts.csswg.org/css-overflow/#example-cb85f151" rel="source"/>
+	<link href="https://drafts.csswg.org/css-overflow/#text-overflow" rel="help"/>
+	<meta name="Description" content="The 'text-overflow' property can be used in combination with the 'overflow' property to render an ellipsis or other text when it overflows its line. This property should not affect layouting, only text rendering."/>
+	<meta name="Assert" content="Hovering over any lines with ellipsis should remove the ellipsis (except for the scrolling container), without changing the layout."/>
+	<style>
+		.group > div {
+			line-height: 1.1;
+			width: 3.1em;
+			padding: 0.2em;
+			border:  1px #000;
+			margin: 0.5em 0;
+		}
+		.group p { margin: 0.5em 0.3em;}
+		.ellipsis {
+			text-overflow: ellipsis;
+		}
+		.ellipsis:hover:not(.scroll-overflow) {
+		  text-overflow: clip;
+		}
+		.overflow-hidden {
+			overflow: hidden;
+		}
+		.scroll-overflow.scroll-overflow {
+			overflow: auto;
+			width: 300px;
+			height: 3em;
+		}
+		.nowrap {
+			white-space: nowrap;
+		}
+		.break-word {
+			word-break: break-word;
+		}
+		div.narrow1 { width: 30px; }
+		div.narrow2 { width: 20px; }
+		div.narrow3 { width: 15px; }
+		div.narrow4 { width: 10px; }
+		div.narrow5 { width: 3px; }
+		div.narrow6 { width: 1px; }
+		div.narrowest { width: 0px; }
+	</style>
+</head>
+
+<body>
+<div class="group">
+	<div>Is that guacamole, chef?</div>
+	<div class="ellipsis">Is that guacamole, chef?</div>
+	<div class="overflow-hidden">Is that guacamole, chef?</div>
+	<div class="overflow-hidden ellipsis">Is that guacamole, chef?</div>
+	<div class="overflow-hidden ellipsis">
+		NESTED
+		<p>PARAGRAPH</p>
+		WON'T ELLIPSE.
+	</div>
+</div>
+
+<hr/>
+Narrowing width.
+<div class="group nowrap">
+	<div class="overflow-hidden ellipsis narrow1">ABCDEF</div>
+	<div class="overflow-hidden ellipsis narrow2">ABCDEF</div>
+	<div class="overflow-hidden ellipsis narrow3">ABCDEF</div>
+	<div class="overflow-hidden ellipsis narrow4">ABCDEF</div>
+	<div class="overflow-hidden ellipsis narrow5">ABCDEF</div>
+	<div class="overflow-hidden ellipsis narrow6">ABCDEF</div>
+	<div class="overflow-hidden ellipsis narrowest">ABCDEF</div>
+	<div class="overflow-hidden ellipsis narrowest"></div>
+	<div class="overflow-hidden ellipsis narrowest">A</div>
+	<div class="overflow-hidden ellipsis narrowest">🌍</div>
+</div>
+
+<hr/>
+Scrolling container.
+<div class="group nowrap">
+	<div class="scroll-overflow ellipsis">Once upon a time, there was a chef with guacamole. He disturbed the guacamole from its blissful sleep, giving off a bittersweet taste.</div>
+</div>
+
+<hr/>
+With 'word-break: break-word' no text should overflow, thus no ellipsis should be visible.
+<div class="group break-word">
+	<div>Is that guacamole, chef?</div>
+	<div class="ellipsis">Is that guacamole, chef?</div>
+	<div class="overflow-hidden">Is that guacamole, chef?</div>
+	<div class="overflow-hidden ellipsis">Is that guacamole, chef?</div>
+	<div class="overflow-hidden ellipsis">
+		NESTED
+		<p>PARAGRAPH</p>
+		WON'T ELLIPSE.
+	</div>
+</div>
+</body>
+</rml>

+ 80 - 0
Tests/Data/VisualTests/text_overflow_02.rml

@@ -0,0 +1,80 @@
+<rml>
+<head>
+	<link type="text/rcss" href="/../Tests/Data/style.rcss"/>
+	<title>Text overflow 02</title>
+	<link href="https://drafts.csswg.org/css-overflow-4/#text-overflow" rel="help"/>
+	<meta name="Description" content="The 'text-overflow' property can be used with custom text. The properly should also apply to atomic inline-level elements, but support for this is not currently implemented."/>
+	<style>
+		.group > div {
+			line-height: 1.1;
+			width: 3.1em;
+			padding: 0.2em;
+			border:  1px #000;
+			margin: 0.5em 0;
+		}
+		.group p { margin: 0.5em 0.3em;}
+		.overflow-hidden { overflow: hidden; }
+		.nowrap { white-space: nowrap; }
+		.red, .orange {
+			display: inline-block;
+			width: 10px;
+			height: 10px;
+		}
+		.red { background: red; }
+		.orange { background: orange; }
+		.custom-string1 { text-overflow: "" }
+		.custom-string2 { text-overflow: "." }
+		.custom-string3 { text-overflow: "•••" }
+		.custom-string4 { text-overflow: "🌍" }
+		.custom-string5 { text-overflow: "🌍🐑" }
+		.custom-string6 { text-overflow: "🌍🐑💛" }
+		.custom-string7 { text-overflow: " THE END" }
+		div.long-custom-string1 { width: 100px; text-overflow: " THE END" }
+		.wide1 { width: 20px; }
+		.wide2 { width: 30px; }
+		.wide3 { width: 40px; }
+		.wide4 { width: 50px; }
+		.wide5 { width: 60px; }
+		div:hover { text-overflow: clip; }
+	</style>
+</head>
+
+<body>
+Custom overflow text.
+<div class="group">
+	<div class="overflow-hidden">Guacamole</div>
+	<div class="overflow-hidden hover-clip custom-string1">Guacamole</div>
+	<div class="overflow-hidden hover-clip custom-string2">Guacamole</div>
+	<div class="overflow-hidden hover-clip custom-string3">Guacamole</div>
+	<div class="overflow-hidden hover-clip custom-string4">Guacamole</div>
+	<div class="overflow-hidden hover-clip custom-string5">Guacamole</div>
+	<div class="overflow-hidden hover-clip custom-string6">Guacamole</div>
+	<div class="overflow-hidden hover-clip custom-string7">Guacamole</div>
+	<div class="overflow-hidden hover-clip long-custom-string1 nowrap">Guacamole from the chef</div>
+</div>
+
+<hr/>
+Atomic inline-level elements.
+<div class="group nowrap">
+	<div>AB<span class="red"></span><span class="orange"></span><span class="red"></span><span class="orange"></span>, CDE</div>
+	<div class="ellipsis">AB<span class="red"></span><span class="orange"></span><span class="red"></span><span class="orange"></span>, CDE</div>
+	<div class="overflow-hidden">AB<span class="red"></span><span class="orange"></span><span class="red"></span><span class="orange"></span>, CDE</div>
+	<div class="overflow-hidden ellipsis">AB<span class="red"></span><span class="orange"></span><span class="red"></span><span class="orange"></span>, CDE</div>
+	<div class="overflow-hidden ellipsis">
+		AB<span class="red"></span><span class="orange"></span><span class="red"></span><span class="orange"></span>, CDE
+		<p>CD<span class="red"></span><span class="orange"></span><span class="red"></span><span class="orange"></span>, CDE</p>
+		EF<span class="red"></span><span class="orange"></span><span class="red"></span><span class="orange"></span>, CDE
+	</div>
+</div>
+
+<hr/>
+Wide atomic inline-level elements.
+<div class="group nowrap">
+	<div class="overflow-hidden ellipsis"><span class="red wide1"></span><span class="orange"></span></div>
+	<div class="overflow-hidden ellipsis"><span class="red wide2"></span><span class="orange"></span></div>
+	<div class="overflow-hidden ellipsis"><span class="red wide3"></span><span class="orange"></span></div>
+	<div class="overflow-hidden ellipsis"><span class="red wide4"></span><span class="orange"></span></div>
+	<div class="overflow-hidden ellipsis"><span class="red wide5"></span><span class="orange"></span></div>
+</div>
+</body>
+</rml>