Browse Source

Add support for the `not` prefix in media queries (#564)

Jonathan 1 year ago
parent
commit
4c7ba7acfa

+ 8 - 2
Include/RmlUi/Core/StyleSheetTypes.h

@@ -84,14 +84,20 @@ struct DecoratorDeclarationList {
 	String value;
 };
 
+enum class MediaQueryModifier {
+	None,
+	Not // passes only if the query is false instead of true
+};
+
 struct MediaBlock {
 	MediaBlock() {}
-	MediaBlock(PropertyDictionary _properties, SharedPtr<StyleSheet> _stylesheet) :
-		properties(std::move(_properties)), stylesheet(std::move(_stylesheet))
+	MediaBlock(PropertyDictionary _properties, SharedPtr<StyleSheet> _stylesheet, MediaQueryModifier _modifier) :
+		properties(std::move(_properties)), stylesheet(std::move(_stylesheet)), modifier(_modifier)
 	{}
 
 	PropertyDictionary properties; // Media query properties
 	SharedPtr<StyleSheet> stylesheet;
+	MediaQueryModifier modifier = MediaQueryModifier::None;
 };
 using MediaBlockList = Vector<MediaBlock>;
 

+ 6 - 4
Source/Core/StyleSheetContainer.cpp

@@ -65,6 +65,8 @@ bool StyleSheetContainer::UpdateCompiledStyleSheet(const Context* context)
 	{
 		const MediaBlock& media_block = media_blocks[media_block_index];
 		bool all_match = true;
+		bool expected_match_value = media_block.modifier == MediaQueryModifier::Not ? false : true;
+
 		for (const auto& property : media_block.properties.GetProperties())
 		{
 			const MediaQueryId id = static_cast<MediaQueryId>(property.first);
@@ -138,11 +140,11 @@ bool StyleSheetContainer::UpdateCompiledStyleSheet(const Context* context)
 			case MediaQueryId::NumDefinedIds: break;
 			}
 
-			if (!all_match)
+			if (all_match != expected_match_value)
 				break;
 		}
 
-		if (all_match)
+		if (all_match == expected_match_value)
 			new_active_media_block_indices.push_back(media_block_index);
 	}
 
@@ -194,7 +196,7 @@ SharedPtr<StyleSheetContainer> StyleSheetContainer::CombineStyleSheetContainer(c
 
 	for (const MediaBlock& media_block : media_blocks)
 	{
-		new_sheet->media_blocks.emplace_back(media_block.properties, media_block.stylesheet);
+		new_sheet->media_blocks.emplace_back(media_block.properties, media_block.stylesheet, media_block.modifier);
 	}
 
 	new_sheet->MergeStyleSheetContainer(container);
@@ -234,7 +236,7 @@ void StyleSheetContainer::MergeStyleSheetContainer(const StyleSheetContainer& ot
 	for (auto it = it_other_begin; it != other.media_blocks.end(); ++it)
 	{
 		const MediaBlock& block_other = *it;
-		media_blocks.emplace_back(block_other.properties, block_other.stylesheet);
+		media_blocks.emplace_back(block_other.properties, block_other.stylesheet, block_other.modifier);
 	}
 }
 

+ 37 - 12
Source/Core/StyleSheetParser.cpp

@@ -154,7 +154,7 @@ static UniquePtr<SpritesheetPropertyParser> spritesheet_property_parser;
 
 /*
  * Media queries need a special parser because they have unique properties that
- * aren't admissible in other property declaration contexts and the syntax of
+ * aren't admissible in other property declaration contexts.
  */
 class MediaQueryPropertyParser final : public AbstractPropertyParser {
 private:
@@ -398,27 +398,49 @@ bool StyleSheetParser::ParseDecoratorBlock(const String& at_name, DecoratorSpeci
 	return true;
 }
 
-bool StyleSheetParser::ParseMediaFeatureMap(PropertyDictionary& properties, const String& rules)
+bool StyleSheetParser::ParseMediaFeatureMap(const String& rules, PropertyDictionary& properties, MediaQueryModifier &modifier)
 {
 	media_query_property_parser->SetTargetProperties(&properties);
 
 	enum ParseState { Global, Name, Value };
-	ParseState state = Name;
+	ParseState state = Global;
 
 	char character = 0;
 
-	size_t cursor = 0;
-
 	String name;
 
 	String current_string;
 
-	while (cursor++ < rules.length())
+	modifier = MediaQueryModifier::None;
+
+	for (size_t cursor = 0; cursor < rules.length(); cursor++)
 	{
 		character = rules[cursor];
 
 		switch (character)
 		{
+		case ' ':
+		{
+			if (state == Global)
+			{
+				current_string = StringUtilities::StripWhitespace(StringUtilities::ToLower(std::move(current_string)));
+
+				if (current_string == "not")
+				{
+					// we can only ever see one "not" on the entire global query.
+					if (modifier != MediaQueryModifier::None)
+					{
+						Log::Message(Log::LT_WARNING, "Unexpected '%s' in @media query list at %s:%d.", current_string.c_str(), stream_file_name.c_str(), line_number);
+						return false;
+					}
+
+					modifier = MediaQueryModifier::Not;
+					current_string.clear();
+				}
+			}
+
+			break;
+		}
 		case '(':
 		{
 			if (state != Global)
@@ -429,7 +451,9 @@ bool StyleSheetParser::ParseMediaFeatureMap(PropertyDictionary& properties, cons
 
 			current_string = StringUtilities::StripWhitespace(StringUtilities::ToLower(std::move(current_string)));
 
-			if (current_string != "and")
+			// allow an empty string to pass through only if we had just parsed a modifier.
+			if (current_string != "and" &&
+				(properties.GetNumProperties() != 0 || !current_string.empty()))
 			{
 				Log::Message(Log::LT_WARNING, "Unexpected '%s' in @media query list at %s:%d. Expected 'and'.", current_string.c_str(),
 					stream_file_name.c_str(), line_number);
@@ -529,7 +553,7 @@ bool StyleSheetParser::Parse(MediaBlockList& style_sheets, Stream* _stream, int
 					// Initialize current block if not present
 					if (!current_block.stylesheet)
 					{
-						current_block = MediaBlock{PropertyDictionary{}, UniquePtr<StyleSheet>(new StyleSheet())};
+						current_block = MediaBlock{PropertyDictionary{}, UniquePtr<StyleSheet>(new StyleSheet()), MediaQueryModifier::None};
 					}
 
 					const int rule_line_number = line_number;
@@ -586,7 +610,7 @@ bool StyleSheetParser::Parse(MediaBlockList& style_sheets, Stream* _stream, int
 					// Initialize current block if not present
 					if (!current_block.stylesheet)
 					{
-						current_block = {PropertyDictionary{}, UniquePtr<StyleSheet>(new StyleSheet())};
+						current_block = {PropertyDictionary{}, UniquePtr<StyleSheet>(new StyleSheet()), MediaQueryModifier::None};
 					}
 
 					const String at_rule_identifier = StringUtilities::StripWhitespace(pre_token_str.substr(0, pre_token_str.find(' ')));
@@ -654,8 +678,9 @@ bool StyleSheetParser::Parse(MediaBlockList& style_sheets, Stream* _stream, int
 
 						// parse media query list into block
 						PropertyDictionary feature_map;
-						ParseMediaFeatureMap(feature_map, at_rule_name);
-						current_block = {std::move(feature_map), UniquePtr<StyleSheet>(new StyleSheet())};
+						MediaQueryModifier modifier;
+						ParseMediaFeatureMap(at_rule_name, feature_map, modifier);
+						current_block = {std::move(feature_map), UniquePtr<StyleSheet>(new StyleSheet()), modifier};
 
 						inside_media_block = true;
 						state = State::Global;
@@ -684,7 +709,7 @@ bool StyleSheetParser::Parse(MediaBlockList& style_sheets, Stream* _stream, int
 					// Initialize current block if not present
 					if (!current_block.stylesheet)
 					{
-						current_block = {PropertyDictionary{}, UniquePtr<StyleSheet>(new StyleSheet())};
+						current_block = {PropertyDictionary{}, UniquePtr<StyleSheet>(new StyleSheet()), MediaQueryModifier::None};
 					}
 
 					// Each keyframe in keyframes has its own block which is processed here

+ 5 - 2
Source/Core/StyleSheetParser.h

@@ -108,8 +108,11 @@ private:
 	bool ParseDecoratorBlock(const String& at_name, DecoratorSpecificationMap& decorator_map, const StyleSheet& style_sheet,
 		const SharedPtr<const PropertySource>& source);
 
-	// Attempts to parse the properties of a @media query
-	bool ParseMediaFeatureMap(PropertyDictionary& properties, const String& rules);
+    /// Attempts to parse the properties of a @media query.
+	/// @param[in] rules The rules to parse.
+	/// @param[out] properties Parsed properties representing all values to be matched.
+	/// @param[out] modifier Media query modifier.
+	bool ParseMediaFeatureMap(const String& rules, PropertyDictionary& properties, MediaQueryModifier &modifier);
 
 	// Attempts to find one of the given character tokens in the active stream
 	// If it's found, buffer is filled with all content up until the token

+ 199 - 0
Tests/Source/UnitTests/MediaQuery.cpp

@@ -204,6 +204,122 @@ static const String document_media_query4_rml = R"(
 </rml>
 )";
 
+static const String document_media_query5_rml = R"(
+<rml>
+<head>
+	<title>Test</title>
+	<link type="text/rcss" href="/assets/rml.rcss"/>
+	<style>
+		body {
+			left: 0;
+			top: 0;
+			right: 0;
+			bottom: 0;
+		}
+
+		div {
+			height: 48px;
+			width: 48px;
+			background: white;
+		}
+
+		@media not (theme: tiny) {
+			div {
+				height: 32px;
+				width: 32px;
+			}
+		}
+
+		@media (theme: big) {
+			div {
+				height: 96px;
+				width: 96px;
+			}
+		}
+	</style>
+</head>
+
+<body>
+<div/>
+</body>
+</rml>
+)";
+
+static const String document_media_query6_rml = R"(
+<rml>
+<head>
+	<title>Test</title>
+	<link type="text/rcss" href="/assets/rml.rcss"/>
+	<style>
+		body {
+			left: 0;
+			top: 0;
+			right: 0;
+			bottom: 0;
+		}
+
+		div {
+			height: 48px;
+			width: 48px;
+			background: white;
+		}
+
+		@media not not (theme: big) {
+			div {
+				height: 32px;
+				width: 32px;
+			}
+		}
+	</style>
+</head>
+
+<body>
+<div/>
+</body>
+</rml>
+)";
+
+static const String document_media_query7_rml = R"(
+<rml>
+<head>
+	<title>Test</title>
+	<link type="text/rcss" href="/assets/rml.rcss"/>
+	<style>
+		body {
+			left: 0;
+			top: 0;
+			right: 0;
+			bottom: 0;
+		}
+
+		div {
+			height: 48px;
+			width: 48px;
+			background: white;
+		}
+
+		@media (theme: big) (theme: small) {
+			div {
+				height: 32px;
+				width: 32px;
+			}
+		}
+
+		@media not (theme: big) (theme: small) {
+			div {
+				height: 32px;
+				width: 32px;
+			}
+		}
+	</style>
+</head>
+
+<body>
+<div/>
+</body>
+</rml>
+)";
+
 TEST_CASE("mediaquery.basic")
 {
 	Context* context = TestsShell::GetContext();
@@ -372,3 +488,86 @@ TEST_CASE("mediaquery.theme")
 
 	TestsShell::ShutdownShell();
 }
+
+// test of `not`; `not` should be an inverted case
+TEST_CASE("mediaquery.theme.notonly")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_media_query5_rml);
+	REQUIRE(document);
+	document->Show();
+
+	context->Update();
+	context->Render();
+
+	TestsShell::RenderLoop();
+
+	ElementList elems;
+	document->GetElementsByTagName(elems, "div");
+	CHECK(elems.size() == 1);
+
+	CHECK(elems[0]->GetBox().GetSize().x == 32.0f);
+
+	context->ActivateTheme("big", true);
+	context->Update();
+	context->Render();
+
+	CHECK(elems[0]->GetBox().GetSize().x == 96.0f);
+
+	context->ActivateTheme("big", false);
+	context->Update();
+	context->Render();
+
+	CHECK(elems[0]->GetBox().GetSize().x == 32.0f);
+
+	context->ActivateTheme("tiny", true);
+	context->Update();
+	context->Render();
+
+	CHECK(elems[0]->GetBox().GetSize().x == 48.0f);
+
+	context->ActivateTheme("big", true);
+	context->Update();
+	context->Render();
+
+	CHECK(elems[0]->GetBox().GetSize().x == 96.0f);
+
+	document->Close();
+
+	TestsShell::ShutdownShell();
+}
+
+// test that `not` cannot appear multiple times
+TEST_CASE("mediaquery.theme.notonly_one")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+	
+	INFO("Expected warnings: unexpected 'not'.");
+	TestsShell::SetNumExpectedWarnings(1);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_media_query6_rml);
+	REQUIRE(document);
+	document->Close();
+
+	TestsShell::ShutdownShell();
+}
+
+
+// test that an `and` must be between multiple conditions.
+TEST_CASE("mediaquery.theme.condition_checks")
+{
+	Context* context = TestsShell::GetContext();
+	REQUIRE(context);
+	
+	INFO("Expected warnings: expected 'and'.");
+	TestsShell::SetNumExpectedWarnings(2);
+
+	ElementDocument* document = context->LoadDocumentFromMemory(document_media_query7_rml);
+	REQUIRE(document);
+	document->Close();
+
+	TestsShell::ShutdownShell();
+}